diff --git a/addons/shopro/config/ccblife.php b/addons/shopro/config/ccblife.php index 98c8606..bdedf43 100644 --- a/addons/shopro/config/ccblife.php +++ b/addons/shopro/config/ccblife.php @@ -54,16 +54,27 @@ return [ 'private_key' => $envVars['private_key'] ?? '', /** - * 商户公钥 (商户自己生成的RSA公钥,对应上面的私钥) + * 服务方公钥 (服务方自己生成的RSA公钥) * 用途: - * - 提交给建行用于验证商户签名 * - 支付下单的MD5签名计算(PLATFORMPUB字段) - * - 加密商户公钥(ENCPUB字段) + * - 用于加密商户公钥生成ENCPUB + * 格式: BASE64格式(不含PEM头尾) 或 PEM格式(含头尾) + * + * 📌 注意: 这是参与支付签名计算的服务方公钥 + */ + 'public_key' => $envVars['public_key'] ?? '', + + /** + * 商户公钥 (商户自己生成的RSA公钥,与上面的private_key对应) + * 用途: + * - 生成ENCPUB密文(使用服务方公钥加密商户公钥后30位) + * - 提交给建行用于验证商户签名 * 格式: BASE64格式(不含PEM头尾) 或 PEM格式(含头尾) * * 📌 注意: 需要将此公钥提交给建行进行配置 + * 📌 如果已上架建行生活并同步公钥,可以不再上送ENCPUB */ - 'public_key' => $envVars['public_key'] ?? '', + 'merchant_public_key' => $envVars['merchant_public_key'] ?? ($envVars['public_key'] ?? ''), /** * 建行平台API公钥 (建行生活平台提供的RSA公钥) diff --git a/addons/shopro/library/ccblife/CcbPaymentService.php b/addons/shopro/library/ccblife/CcbPaymentService.php index 39e4aa4..3204447 100644 --- a/addons/shopro/library/ccblife/CcbPaymentService.php +++ b/addons/shopro/library/ccblife/CcbPaymentService.php @@ -119,6 +119,16 @@ class CcbPaymentService $macParams['PROINFO'] = $proinfo; $macParams['REFERER'] = ''; // 商户URL(空字符串) + + // ⚠️ 关键修复:INSTALLNUM必须在THIRDAPPINFO之前(严格按照文档4.1表格顺序) + // 1.3 可选参数(按文档表格顺序,有值才参与MAC) + // 注意:根据文档4.2,橙色字段有值时才参与MAC,空值不参与 + + // 分期期数(在REFERER之后,THIRDAPPINFO之前) + if (!empty($this->config['install_num'])) { + $macParams['INSTALLNUM'] = $this->config['install_num']; + } + $macParams['THIRDAPPINFO'] = 'comccbpay1234567890cloudmerchant'; // 客户端标识(固定值) // 记录关键参数 @@ -126,14 +136,6 @@ class CcbPaymentService Log::info('[建行支付] 商户信息 merchant_id:' . ($macParams['MERCHANTID'] ?? 'N/A') . ' pos_id:' . ($macParams['POSID'] ?? 'N/A') . ' branch_id:' . ($macParams['BRANCHID'] ?? 'N/A')); Log::info('[建行支付] 商品信息 proinfo:' . $proinfo); - // 1.3 可选参数(按文档表格顺序,有值才参与MAC) - // ⚠️ 注意:根据文档4.2,橙色字段有值时才参与MAC,空值不参与 - - // 分期期数(在THIRDAPPINFO之后) - if (!empty($this->config['install_num'])) { - $macParams['INSTALLNUM'] = $this->config['install_num']; - } - // 超时时间 if (!empty($this->config['timeout'])) { $macParams['TIMEOUT'] = $this->config['timeout']; @@ -413,40 +415,95 @@ class CcbPaymentService } /** - * 加密商户公钥后30位(用于支付串的ENCPUB字段) + * 生成ENCPUB(商户公钥密文) * - * 根据建行文档v2.2规范: - * "使用服务方公钥对商户公钥后30位进行RSA加密并base64后的密文" + * 根据建行文档4.4: + * "使用服务方公钥对商户公钥后30位进行RSA加密,再进行base64后,生成的密文串" + * + * 注意:这里的"商户公钥后30位"是指RSA公钥模数(modulus)的十六进制表示的后30位 + * 不是BASE64字符串的后30位! * * @return string BASE64编码的加密密文 * @throws \Exception */ private function encryptPublicKeyLast30() { - $publicKey = $this->config['public_key'] ?? ''; + // 获取商户公钥(被加密的公钥) + $merchantPublicKey = $this->config['merchant_public_key'] ?? ''; - if (empty($publicKey)) { + // 获取服务方公钥(用于加密的公钥) + $servicePublicKey = $this->config['public_key'] ?? ''; + + if (empty($merchantPublicKey)) { + throw new \Exception('商户公钥未配置'); + } + + if (empty($servicePublicKey)) { throw new \Exception('服务方公钥未配置'); } - // 去除 PEM 格式的头尾和空白字符,获取纯 BASE64 内容 - $publicKeyContent = str_replace([ - '-----BEGIN PUBLIC KEY-----', - '-----END PUBLIC KEY-----', - '-----BEGIN RSA PUBLIC KEY-----', - '-----END RSA PUBLIC KEY-----', - "\r", "\n", " ", "\t" - ], '', $publicKey); + // ✅ 关键修复:提取商户公钥模数的十六进制表示的后30位 + try { + // 格式化商户公钥为PEM格式 + $merchantPemKey = $this->formatPublicKeyToPem($merchantPublicKey); - // 取后30位 - $last30Chars = substr($publicKeyContent, -30); + // 解析商户公钥,提取模数(modulus) + $keyResource = openssl_pkey_get_public($merchantPemKey); + if (!$keyResource) { + throw new \Exception('商户公钥格式错误: ' . openssl_error_string()); + } - if (strlen($last30Chars) < 30) { - throw new \Exception('商户公钥长度不足30位'); + $keyDetails = openssl_pkey_get_details($keyResource); + openssl_free_key($keyResource); + + if (!isset($keyDetails['rsa']['n'])) { + throw new \Exception('无法提取商户公钥RSA模数'); + } + + // 将模数转换为十六进制字符串(去掉前导0x) + $modulusHex = bin2hex($keyDetails['rsa']['n']); + + // 取后30位十六进制字符 + $last30Chars = substr($modulusHex, -30); + + if (strlen($last30Chars) < 30) { + throw new \Exception('商户公钥模数长度不足30位'); + } + + Log::info('[建行支付] 商户公钥模数后30位: ' . $last30Chars); + + // 使用服务方公钥加密这30位十六进制字符串 + // CcbRSA::encrypt 会自动进行base64编码 + return CcbRSA::encrypt($last30Chars, $servicePublicKey); + + } catch (\Exception $e) { + Log::error('[建行支付] ENCPUB生成失败: ' . $e->getMessage()); + throw $e; + } + } + + /** + * 格式化公钥为PEM格式 + * + * @param string $publicKey BASE64或PEM格式的公钥 + * @return string PEM格式的公钥 + */ + private function formatPublicKeyToPem($publicKey) + { + // 移除可能存在的空格和换行 + $publicKey = preg_replace('/\s+/', '', $publicKey); + + // 如果已经是PEM格式,直接返回 + if (strpos($publicKey, '-----BEGIN PUBLIC KEY-----') !== false) { + return $publicKey; } - // ✅ 使用 CcbRSA 类进行加密(统一密钥格式化逻辑) - return CcbRSA::encrypt($last30Chars, $publicKey); + // 转换为PEM格式 + $pem = "-----BEGIN PUBLIC KEY-----\n"; + $pem .= rtrim(chunk_split($publicKey, 64, "\n"), "\n") . "\n"; + $pem .= "-----END PUBLIC KEY-----\n"; + + return $pem; } /**