注意:
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); // 直接可以通过
✅ 建议你这么改:
- 要求输出 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 格式就能顺利验签。