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代码
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++]); // 读取长度,假设小于128if ($length > 0x80) return false;// 第一个 INTEGER -> rif (ord($derSig[$offset++]) !== 0x02) return false;$rLen = ord($derSig[$offset++]);$r = substr($derSig, $offset, $rLen);$offset += $rLen;// 第二个 INTEGER -> sif (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++]); // 读取长度,假设小于128if ($length > 0x80) return false;// 第一个 INTEGER -> rif (ord($derSig[$offset++]) !== 0x02) return false;$rLen = ord($derSig[$offset++]);$r = substr($derSig, $offset, $rLen);$offset += $rLen;// 第二个 INTEGER -> sif (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); // 直接可以通过
✅ 建议你这么改:
- 要求输出 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 格式就能顺利验签。