java Sm2SignWithSM3转php

发布于:2025-05-24 ⋅ 阅读:(11) ⋅ 点赞:(0)

注意:
1.java的签名转base64位时使用的方法
2.String sign = URLEncoder.encode(new String(Base64.encodeBase64URLSafe(sm2SignWithSM3)));
3.使用base64安全模式转为字符串再次urlencode
4.php在加签时候需要使用同样的逻辑

php 8
java 1.8
php使用插件
“lpilp/guomi”: “^2.0”

java代码

public class test {
    public static void main(String[] args) {
        String pub = "0443392b4145a906fecdf801b23cc9562fde69985a1988b02131a4b31b81bc931813c8777a1d3927bc6ad4048c922e2f2f7edc55e5335278f7b61c6cc06537b065";
        String pri = "23ba4a398e88183519873cedf8f0df9c9756c53a5ef35ef38a516266aef4cf91";
        System.out.println("公钥:"+pub);
        System.out.println("私钥:"+pri);
        byte[] hxPubKey = Hex.decode(pub);
        byte[] privKey = Hex.decode(pri);
        System.out.println("私钥:"+Hex.toHexString(privKey)+" ,长度:"+Hex.toHexString(privKey).length());
        String splicSignStr = "123";//拼接待加签参数(按key的ascii码拼接)
        System.out.println("待加签参数:"+splicSignStr+" ,长度:"+splicSignStr.length());
        ISM2 sm2 = SM2.getInstance();
        byte[] sm2SignWithSM3 = sm2.sm2SignWithSM3(privKey, splicSignStr.getBytes());
//        String signHex = Hex.toHexString(sm2SignWithSM3);
//        System.out.println("签名hex为:"+signHex+" ,长度:"+signHex.length());
        String sign = URLEncoder.encode(new String(Base64.encodeBase64URLSafe(sm2SignWithSM3)));
        System.out.println("签名为:"+sign+" ,长度:"+sign.length());
        byte[] signByte = Base64.decodeBase64(sign);
        //验证签名
        boolean result = sm2.sm2VerifyWithSM3(hxPubKey, splicSignStr.getBytes(), signByte);
        System.out.println("校验结果:"+result);

    }
}

php

<?php
namespace extend\util;

use PHPUnit\Util\Exception;
use Rtgm\sm\RtSm2;

class Sm2SignWithSM3
{
    private $sm2;
    private $pub;
    private $pri;
    private $debug;
    public function __construct($pub,$pri,$debug=false)
    {
        $this->sm2 = new RtSm2('base64', false);
        $this->pub = $pub;
        $this->pri = $pri;
        $this->debug = $debug;
    }

    public  function makeSign($str){
        $this->output("带加签字符:$str,长度:".strlen($str));

        $signature = $this->sm2->doSign($str, $this->pri);
        $signature = $this->base64UrlSafeEncode($this->derToRawRS(base64_decode($signature)));
        $this->output("签名:$signature,长度:".strlen($signature));
        return $signature;
    }

    public  function verify($str,$signature):bool
    {
        $signature = base64_encode($this->base64UrlSafeDecode($signature));
        $valid = $this->verifyRawSignature($str, $signature, $this->pub);
        $this->output("验签结果:" . ($valid ? '通过' : '失败'));
        return $valid;
    }

    private function output($str = ''){
        if($this->debug){
            echo $str.PHP_EOL;
        }
    }

    public static function test(){

// Java中用的是这个私钥
        $privateKey = '23ba4a398e88183519873cedf8f0df9c9756c53a5ef35ef38a516266aef4cf91';
        $publicKey = "0443392b4145a906fecdf801b23cc9562fde69985a1988b02131a4b31b81bc931813c8777a1d3927bc6ad4048c922e2f2f7edc55e5335278f7b61c6cc06537b065";
        $sm2SignWithSM3 =  new self($publicKey,$privateKey,true);
        $str = "123";
        $signature = $sm2SignWithSM3->makeSign($str);
        $signature = $sm2SignWithSM3->verify($str,$signature);
    }

    function verifyRawSignature(string $data, string $base64RawSignature, string $publicKey): bool
    {
        // 1. 解码 r||s 签名
        $raw = base64_decode($base64RawSignature);
        if (strlen($raw) !== 64) {
            throw new Exception("签名长度必须为 64 字节(r||s 裸签名)");
        }

        $r = substr($raw, 0, 32);
        $s = substr($raw, 32, 32);

        // 2. 转换为 ASN.1 DER 格式
        $der = $this->encodeDerSignature($r, $s);

        // 3. base64 编码 DER 签名
        $derBase64 = base64_encode($der);

        // 4. 使用 RtSm2 进行验签
        $sm2 = new RtSm2('base64', false);
        return $sm2->verifySign($data, $derBase64, $publicKey);
    }
    function encodeDerSignature(string $r, string $s): string
    {
        $r = ltrim($r, "\x00");
        $s = ltrim($s, "\x00");

        if (ord($r[0]) > 0x7f) $r = "\x00" . $r;
        if (ord($s[0]) > 0x7f) $s = "\x00" . $s;

        $der = "\x02" . chr(strlen($r)) . $r .
            "\x02" . chr(strlen($s)) . $s;

        return "\x30" . chr(strlen($der)) . $der;
    }



    /**
     * 将 DER 编码的 SM2 签名转换为裸 r||s 格式
     * @param string $derSig 二进制形式的 DER 签名
     * @return string|false  返回 r||s 裸签名(64字节),失败返回 false
     */
    function derToRawRS(string $derSig): string|false
    {
        $offset = 0;

        // 检查开头是否为 SEQUENCE (0x30)
        if (ord($derSig[$offset++]) !== 0x30) return false;

        $length = ord($derSig[$offset++]); // 读取长度,假设小于128
        if ($length > 0x80) return false;

        // 第一个 INTEGER -> r
        if (ord($derSig[$offset++]) !== 0x02) return false;
        $rLen = ord($derSig[$offset++]);
        $r = substr($derSig, $offset, $rLen);
        $offset += $rLen;

        // 第二个 INTEGER -> s
        if (ord($derSig[$offset++]) !== 0x02) return false;
        $sLen = ord($derSig[$offset++]);
        $s = substr($derSig, $offset, $sLen);
        $offset += $sLen;

        // 去除前导 0(补齐到 32 字节)
        $r = ltrim($r, "\x00");
        $s = ltrim($s, "\x00");
        $r = str_pad($r, 32, "\x00", STR_PAD_LEFT);
        $s = str_pad($s, 32, "\x00", STR_PAD_LEFT);

        return $r . $s; // 裸签名 64 字节
    }


    function base64UrlSafeEncode($data)
    {
        return rtrim(strtr(base64_encode($data), '+/', '-_'), '=');
    }

    function base64UrlSafeDecode($data)
    {
        // 恢复 + 和 /
        $data = strtr($data, '-_', '+/');

        // 补齐 = 到长度为 4 的倍数
        $remainder = strlen($data) % 4;
        if ($remainder) {
            $data .= str_repeat('=', 4 - $remainder);
        }

        return base64_decode($data);
    }
}



其它参考

为什么java的是88个字符但是php是96个

你现在 SM2 签名用 base64_encode() 输出是 96 个字符,而你想要的是 88 个字符,这说明你当前的签名是 72 字节,而目标是 64 字节。要实现这个,你需要:
✅ 目标:
将签名限制为 64 字节的原始数据(即 SM2 的 r + s 各 32 字节,非 DER 编码),再进行 base64_encode(),就会是 88 个字符。
✅ 解决方案:
你当前可能使用的是 ASN.1 DER 编码签名格式,它长度可变(一般是 70~72 字节),而你要的是 纯 r||s 拼接的裸签名格式。
✅ 修改方式(PHP 示例):
你可以手动将 r 和 s 提取成 32 字节的裸格式签名(64 字节),然后再 base64 编码:
php

use Lpilp\Guomi\Sm2;

$sm2 = new Sm2();
$sm2->setPrivateKey($pri);
$sm2->setPublicKey($pub);

// 1. 获取 der 编码的签名
$derSign = $sm2->doSign($data);

// 2. 解析 DER,提取 r 和 s(使用 phpseclib 或手动)
// 简化起见,假设你用库内部支持返回裸 r||s:
$rawSign = $sm2->signRaw($data); // 64 字节:32(r) + 32(s)
$base64Sign = base64_encode($rawSign);

echo strlen($base64Sign); // 输出:88
echo $base64Sign;

❗注意:
如果 lpilp/guomi 没有提供 signRaw() 形式的裸签名输出,你需要使用其他支持裸签名输出的扩展(如 openssl 的 SM2 模式支持)。
DER 编码的签名(默认)不适合你目前这种固定88字符的格式需求。

下面是一个用于将 DER 编码的 SM2 签名 提取为 裸格式 r||s(共 64 字节) 的 PHP 示例代码,然后将其 base64_encode() 得到你需要的 88 位签名字符串:
✅ 示例代码:DER 签名转裸签名并输出 base64(88 字符)
php

<?php

/**
 * 将 DER 编码的 SM2 签名转换为裸 r||s 格式
 * @param string $derSig 二进制形式的 DER 签名
 * @return string|false  返回 r||s 裸签名(64字节),失败返回 false
 */
function derToRawRS(string $derSig): string|false
{
    $offset = 0;

    // 检查开头是否为 SEQUENCE (0x30)
    if (ord($derSig[$offset++]) !== 0x30) return false;

    $length = ord($derSig[$offset++]); // 读取长度,假设小于128
    if ($length > 0x80) return false;

    // 第一个 INTEGER -> r
    if (ord($derSig[$offset++]) !== 0x02) return false;
    $rLen = ord($derSig[$offset++]);
    $r = substr($derSig, $offset, $rLen);
    $offset += $rLen;

    // 第二个 INTEGER -> s
    if (ord($derSig[$offset++]) !== 0x02) return false;
    $sLen = ord($derSig[$offset++]);
    $s = substr($derSig, $offset, $sLen);
    $offset += $sLen;

    // 去除前导 0(补齐到 32 字节)
    $r = ltrim($r, "\x00");
    $s = ltrim($s, "\x00");
    $r = str_pad($r, 32, "\x00", STR_PAD_LEFT);
    $s = str_pad($s, 32, "\x00", STR_PAD_LEFT);

    return $r . $s; // 裸签名 64 字节
}

// 示例用法
$derHex = '3045022100f6b91d334c7e12accc8b5be1e27a9607c87ae50798e5cbfa70b94fd5b93b548d0220592f2b20eb9cb3c7f4fbe0b2c5d645273a8e058cc457361cd353e847f58c131d';
$derBin = hex2bin($derHex);

$rawSig = derToRawRS($derBin);
if ($rawSig !== false) {
    $base64Sig = base64_encode($rawSig);
    echo "签名(base64,88字符):\n$base64Sig\n";
    echo "长度: " . strlen($base64Sig) . "\n";
} else {
    echo "解析 DER 签名失败\n";
}

验签怎么过呢?

你目前的签名流程是:
使用 s m 2 − > d o S i g n ( sm2->doSign( sm2>doSign(data, $privateKey) 生成 DER 编码的签名(base64 格式);
再手动 derToRawRS() 提取出 裸 r||s 格式(64 字节),再 base64_encode 为 88 字符签名;
但 verifySign() 还拿着这个裸签名去验签,当然过不了,因为 verifySign() 默认需要的是 原始 DER 签名格式。
✅ 解决方案有两个方向:
✅ 方式一:推荐做法,验签时使用裸签名的验签方法
如果你手动转换了 r||s 裸签名格式并 base64 了,就不能再直接用 verifySign() 验签。你需要手动还原 r、s,再构造 DER 签名或直接验裸签。

如果你用的是 lpilp/guomi 或 topthink/think-sm 等扩展库,可以这样操作(如果支持裸签验签):
php

// 1. 解码 base64 签名,得到 64 字节裸签名
$raw = base64_decode($signature);

// 2. 传给一个支持裸验签的函数(你得找或写这个方法)
$valid = $this->verifyRawSignature($data, $raw, $publicKey);

✅ 方式二:验签时使用原始 DER 签名(不要转成88字符)
也就是签名后不做 derToRawRS() 转换,直接:

$signature = $sm2->doSign($data, $privateKey); // 已是 base64 DER 签名
$valid = $sm2->verifySign($data, $signature, $publicKey); // 直接可以通过

✅ 建议你这么改:

  1. 要求输出 88 字符可传输签名,但又能验签:
    签名后 保留原 DER 签名(base64)
    若需要 88 字符传输格式,可以提供一个 raw_signature 字段专门传裸签
    验签时明确知道你用的是什么格式
    好的!以下是一个用于 裸签名(r||s 格式,长度 64 字节) 的 SM2 验签函数 verifyRawSignature,它适配的是你之前用的 lpilp/guomi 扩展(例如 RtSm2 类)。实现思路如下:

把 base64 编码的 r||s 签名解码成原始字节;
将 r 和 s 从中分离出来;
将 r 和 s 重新封装为 DER 格式;
调用原生的 verifySign() 来完成验签。

✅ verifyRawSignature 函数示例(支持 base64 编码的 r||s 签名):

use Lpilp\Guomi\RtSm2;

function verifyRawSignature(string $data, string $base64RawSignature, string $publicKey): bool
{
    // 1. 解码 r||s 签名
    $raw = base64_decode($base64RawSignature);
    if (strlen($raw) !== 64) {
        throw new InvalidArgumentException("签名长度必须为 64 字节(r||s 裸签名)");
    }

    $r = substr($raw, 0, 32);
    $s = substr($raw, 32, 32);

    // 2. 转换为 ASN.1 DER 格式
    $der = encodeDerSignature($r, $s);

    // 3. base64 编码 DER 签名
    $derBase64 = base64_encode($der);

    // 4. 使用 RtSm2 进行验签
    $sm2 = new RtSm2('base64', false);
    return $sm2->verifySign($data, $derBase64, $publicKey);
}

🔧 encodeDerSignature() 函数(将 r 和 s 封装为 DER):

function encodeDerSignature(string $r, string $s): string
{
    $r = ltrim($r, "\x00");
    $s = ltrim($s, "\x00");

    if (ord($r[0]) > 0x7f) $r = "\x00" . $r;
    if (ord($s[0]) > 0x7f) $s = "\x00" . $s;

    $der = "\x02" . chr(strlen($r)) . $r .
           "\x02" . chr(strlen($s)) . $s;

    return "\x30" . chr(strlen($der)) . $der;
}

✅ 用法示例:


$data = "123";
$rawSignature = $signature; // 这是你 base64_encode 的 r||s 签名
$publicKey = "04..."; // 64字节公钥 hex(未压缩)

if (verifyRawSignature($data, $rawSignature, $publicKey)) {
    echo "验签通过";
} else {
    echo "验签失败";
}

🧠 补充说明:
SM2 签名本质是 (r, s) 两个大整数;

DER 格式是 PKCS#1 标准的签名封装;

verifySign() 只能验 DER 签名;

所以你只要能从裸签名恢复 DER 格式就能顺利验签。


网站公告

今日签到

点亮在社区的每一天
去签到