java Sm2SignWithSM3转php

注意:

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代码

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 复制代码
<?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

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 复制代码
<?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

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

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

✅ 方式二:验签时使用原始 DER 签名(不要转成88字符)

也就是签名后不做 derToRawRS() 转换,直接:

php 复制代码
$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 签名):

php 复制代码
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):

php 复制代码
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;
}

✅ 用法示例:

php 复制代码
$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 格式就能顺利验签。

相关推荐
北漂老男孩15 分钟前
Flink基于Yarn多种启动方式详解
java·大数据·flink
王蛋11116 分钟前
后端环境配置
java·spring·maven
养-乐多17 分钟前
梳理Spring Boot中三种异常处理
java·spring boot·后端
找不到、了21 分钟前
字符串和常量池的进一步研究
java·开发语言
Code哈哈笑26 分钟前
【基于SpringBoot的图书购买系统】深度讲解 分页查询用户信息,分析前后端交互的原理
java·数据库·spring boot·后端·spring·交互
kingwebo'sZone31 分钟前
sqlite的拼接字段的方法(sqlite没有convert函数)
java·数据库·sqlite
星沁城34 分钟前
212. 单词搜索 II
java·数据结构·算法·leetcode
.生产的驴1 小时前
Vue3 数据可视化屏幕大屏适配 页面自适应 响应式 数据大屏 大屏适配
java·c++·vue.js·后端·信息可视化·前端框架·vue
龙泉寺天下行走1 小时前
《进化陷阱》--AI 生成文章 《连载 1》
java·服务器·前端
xzkyd outpaper1 小时前
Android中Framework用到了哪些跨进程通信方式
android·计算机八股