招商银行验签问题

1. 运行环境

windows10 .docker中

1). 当前使用的 Laravel 版本?

laravel10
//: <> (使用 php artisan --version 命令查看)

2). 当前使用的 php/php-fpm 版本?

PHP 版本: php8.2

php-fpm 版本:

3). 当前系统

Windows 10
//: <> (期待数值 Windows 10 / Ubuntu 20.4 / CentOS 8 )

4). 业务环境

开发环境
//: <> (期待信息 开发环境生产环境)
//: <> (是否使用负载均衡?请提供相关信息)

5). 相关软件版本

Nginx 、MySQL
//: <> (提供相关软件的版本,如 Nginx 、MySQL、MongoDB 等)

2. 问题描述?

对接的招商银行的银企直连接口。
1、使用了 lpilp/guomi 的包
2、签名用的给的示例代码
签名是可以的,但是验签的时候始终为 false
//: <> (代码问题的话,请提供一份最短的,可复现问题的代码。或者相关代码)

<?php
#招商银行银企直联国密免前置/SaaS对接示例,本示例仅供参考,不保证各种异常场景运行,请勿直接使用,如有错漏请联系对接人员。运行时,请使用所获取的测试资源替换 用户编号、公私钥、对称密钥、服务商编号等信息。

namespace cmb;

use Mdanter\Ecc\Crypto\Signature\Signature;
use Mdanter\Ecc\Serializer\Signature\DerSignatureSerializer;
use \FG\ASN1\ASNObject;
use Rtgm\sm\RtSm2;
use Rtgm\sm\RtSm4;

class DcHelper
{
    // 请求URL
  var $url;

    // 企业网银用户号
  var $uid;

    // 国密算法向量,根据用户号生成
  var $userId;

    // 算法,固定为国密算法
  var $alg;

    // 客户私钥
  var $privateKey;

    // 银行公钥
  var $publicKey;

    // 协商的对称密钥
  var $symKey;

    function __construct($url, $uid, $privateKey, $publicKey, $symKey)
    {
        $this->url = $url;
        $this->uid = $uid;
        $this->userId = sprintf('%-016s', $uid);
        $this->alg = "SM";
        $this->privateKey = unpack("H*", base64_decode($privateKey))[1];
        $this->publicKey = unpack("H*", base64_decode($publicKey))[1];
        $this->symKey = $symKey;
    }

    function sendRequest($data, $funcode)
    {
        // 对请求报文做排序
  $toSortArray = json_decode($data, true);
        $this->recursiveArraySort($toSortArray);
        $data = json_encode($toSortArray, JSON_UNESCAPED_UNICODE);

        // 生成签名
  $sm2 = new RtSm2("base64");
        $sign = $sm2->doSign($data, $this->privateKey, $this->userId);

        // 处理签名
  $sign = base64_decode($sign);
        $point = ASNObject::fromBinary($sign)->getChildren();
        $pointX = $this->formatHex($point[0]->getContent());
        $pointY = $this->formatHex($point[1]->getContent());
        $sign = $pointX . $pointY;
        $sign = base64_encode(hex2bin($sign));

        // 替换签名字段
  $data = str_replace('__signature_sigdat__', $sign, $data);

        // 对数据进行对称加密
  $sm4 = new RtSm4($this->symKey);
        $encryptData = $sm4->encrypt($data, 'sm4-cbc', $this->userId, "base64");

        // 发送请求
  $response = $this->httpPost($this->getFormData($encryptData, $funcode));

        // 返回结果解密
  $json = $sm4->decrypt($response, 'sm4-cbc', $this->userId, 'base64');
        $respdata = json_decode($json, true);

        // 验证签名是否正确
  $sign = $respdata["signature"]["sigdat"];
        // 将数据中的签名重置
  $respdata["signature"]["sigdat"] = "__signature_sigdat__";
        $json = str_replace($sign, "__signature_sigdat__", $json);

        $signHex = bin2hex(base64_decode($sign));
        $r = substr($signHex, 0, 64);
        $s = substr($signHex, 64, 64);
        $r = gmp_init($r, 16);
        $s = gmp_init($s, 16);
        $signature = new Signature($r, $s);
        $serializer = new DerSignatureSerializer();
        $serializedSig = $serializer->serialize($signature);
        $sign = base64_encode($serializedSig);

        $b = $sm2->verifySign($json, $sign, $this->publicKey, $this->userId);
        if ($b === true) {
            return $json;
        } else {
            throw new \Exception("响应报文的签名无效");
        }

    }

    private function httpPost($data)
    {
        $urlInfo = parse_url($this->url);
        foreach ($data as $key => $value)
            $values[] = "$key=" . urlencode($value);
        $dataString = implode("&", $values);

        $ch = curl_init();
        curl_setopt($ch, CURLOPT_URL, $this->url);
        curl_setopt($ch, CURLOPT_POST, 1);
        curl_setopt($ch, CURLOPT_POSTFIELDS, $dataString);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);

        $response = curl_exec($ch);
        $status = curl_getinfo($ch, CURLINFO_HTTP_CODE);

        if ($response === false) {
            throw new \Exception("Error in making the request: " . curl_error($ch));
        }
        if ($status != 200) {
            throw new \Exception("Status Code: " . $status . " Response: " . $response);
        }
        if (substr($response, 0, 10) === "CDCServer:") {
            throw new \Exception("访问目标地址 " . $this->url . " 失败:" . $response);
        }

        return $response;
    }

    private function getFormData($data, $funcode)
    {
        return array('UID' => $this->uid, 'ALG' => $this->alg, 'DATA' => $data, 'FUNCODE' => $funcode);
    }

    private function formatHex($dec)
    {
        $hex = gmp_strval(gmp_init($dec, 10), 16);
        $len = strlen($hex);
        if ($len == 64) {
            return $hex;
        }
        if ($len < 64) {
            $hex = str_pad($hex, 64, "0", STR_PAD_LEFT);
        } else {
            $hex = substr($hex, $len - 64, 64);
        }
        return $hex;
    }

    private function recursiveArraySort(&$array)
    {
        ksort($array);
        foreach ($array as &$value) {
            if (is_array($value)) {
                $this->recursiveArraySort($value);
            }
        }
    }

}

下面是调用结果

<?php
#招商银行银企直联国密免前置/SaaS对接示例,本示例仅供参考,不保证各种异常场景运行,请勿直接使用,如有错漏请联系对接人员。运行时,请使用所获取的测试资源替换 用户编号、公私钥、对称密钥、服务商编号等信息。

require 'vendor/autoload.php';
require 'dchelper.php';
use cmb\DcHelper;

// 测试地址,生产需要替换
$url = "http://cdctest.cmburl.cn:80/cdcserver/api/v2";
// 生产地址
// $url = "https://cdc.cmbchina.com/cdcserver/api/v2";
// 银行公钥,生产需要替换
$publicKey = 'BNsIe9U0x8IeSe4h/dxUzVEz9pie0hDSfMRINRXc7s1UIXfkExnYECF4QqJ2SnHxLv3z/99gsfDQrQ6dzN5lZj0=';
// 生产环境银行公钥
// $publicKey = 'BEynMEZOjNpwZIiD9jXtZSGr3Ecpwn7r+m+wtafXHb6VIZTnugfuxhcKASq3hX+KX9JlHODDl9/RDKQv4XLOFak=';
// 客户私钥,生产需要替换
$privateKey = 'NBtl7WnuUtA2v5FaebEkU0/Jj1IodLGT6lQqwkzmd2E=';
// 对称密钥,生产需要替换
$symKey = 'VuAzSWQhsoNqzn0K';

// 企业网银用户号,生产需要替换
$uid = "N003261207";
// 业务接口名,这里是查询业务模式接口,生产请替换为对应接口名
$funcode = "DCLISMOD";
// 准备接口数据,生产请替换为具体接口请求报文,包含所需的请求字段
$currentDatetime = date("YmdHis");
$reqid = date("YmdHisu") . rand(1000000, 9999999);
$data = '{"request":{"body":{"buscod":"N02030"},"head":{"funcode":"' . $funcode . '","userid":"' . $uid . '","reqid":"' . $reqid . '"}},"signature":{"sigdat":"__signature_sigdat__","sigtim":"' . $currentDatetime . '"}}';

// 创建帮助类,并传入构造参数
$dchelper = new DcHelper($url, $uid, $privateKey, $publicKey, $symKey);
// 发送请求内容,并接收返回数据
$response = $dchelper->sendRequest($data, $funcode);
echo '响应报文:<br/>' . $response;

3. 您期望得到的结果?

给的示例可以跑。签名也可以。但是他的回调数据验签就是过不去
回调数据 。
这是返回数据 就很奇怪

{

 "notdat": "{\"msgdat\":{\"trsInfo\":{\"nusAge\":\"打款\",\"rtnFlg\":\"S\",\"reqNbr\":\"8782029704\",\"crtBnk\":\"招商银行\",\"dbtAcc\":\"128964320010000\",\"eptTim\":\"0\",\"eptDat\":\"20241024\",\"crtNam\":\"殷萌涵\",\"busNar\":\"\",\"crtAdr\":\"招商银行\",\"dbtBbk\":\"75\",\"ntfCh2\":\"\",\"ntfCh1\":\"\",\"stlChn\":\"Q\",\"crtBbk\":\"\",\"oprDat\":\"20241024\",\"trxSeq\":\"C0146ZR0001QF2Z\",\"crtAcc\":\"6214837811201866\",\"bnkFlg\":\"Y\",\"reqSts\":\"FIN\",\"trxSet\":\"BHP6ZR0800558CT\",\"dbtNam\":\"银企直连测试用户专用06\",\"yurRef\":\"202410241717460000003222545\",\"ccyNbr\":\"10\",\"trsAmt\":\"5.55\"}},\"msgtyp\":\"FINS\"}",

 "notkey": "128964320010000",

 "notnbr": "258409058866954240",

 "nottyp": "YQN02030",

 "sigdat": "GtNpYJTX9bnpFbO8eFzBCwQGrj+rPmOq7gb0wCIlxbWMGImlrhO8gIP8z4UmlBKnn1cvtmIuTtoDqs8+QMz28g==",

 "sigtim": "20241024171815",

 "usrnbr": "U006566025"

}

4. 您实际得到的结果?

验签过不去
//: <> (有报错信息的话把堆栈信息提供出来)

《L05 电商实战》
从零开发一个电商项目,功能包括电商后台、商品 & SKU 管理、购物车、订单管理、支付宝支付、微信支付、订单退款流程、优惠券等
《L01 基础入门》
我们将带你从零开发一个项目并部署到线上,本课程教授 Web 开发中专业、实用的技能,如 Git 工作流、Laravel Mix 前端工作流等。
讨论数量: 3

把文章整理一下再发,这个文章你自己看得下去吗

5个月前 评论
aq009 (楼主) 5个月前

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!