Java国密SM2签名验签实战:从Bouncy Castle配置到在线工具验证(附完整代码)
Java国密SM2签名验签实战从Bouncy Castle配置到在线工具验证附完整代码最近在对接一个金融项目对方要求必须使用国密SM2算法进行数据签名和验签。本以为是个标准流程结果在配置Bouncy Castle库和实现带UserID的签名时踩了不少坑。从依赖冲突到签名格式解析再到在线验证工具的细节处理每一步都可能遇到意想不到的问题。这篇文章就是把我趟过的这些坑整理成一份实战指南希望能帮你少走弯路。SM2作为国密标准中的非对称加密算法在政务、金融、物联网等领域应用越来越广泛。但很多Java开发者在实际集成时会发现官方文档相对简略社区资料也多是理论讲解真正能跑通的完整示例并不多。特别是那个恼人的UserID参数到底该怎么用生成的签名为什么在线工具验证失败Bouncy Castle的不同版本有哪些兼容性问题这些才是实际开发中最需要解决的问题。1. 环境搭建与Bouncy Castle配置实战配置开发环境是第一步也是最容易出问题的一步。很多人以为简单加个Maven依赖就行结果运行时各种ClassNotFoundException、NoSuchProviderException接踵而至。1.1 依赖选择与版本陷阱Bouncy Castle提供了多个artifact选错一个就可能无法支持SM2。目前最稳定的是bcprov-jdk15on但要注意JDK版本兼容性。dependencies !-- 核心加密提供者 -- dependency groupIdorg.bouncycastle/groupId artifactIdbcprov-jdk15on/artifactId version1.70/version /dependency !-- 如果需要PKIX相关功能如证书处理 -- dependency groupIdorg.bouncycastle/groupId artifactIdbcpkix-jdk15on/artifactId version1.70/version /dependency /dependencies注意版本号中的jdk15on表示支持JDK 1.5及以上版本但实际使用时建议JDK 8。如果使用JDK 17需要额外注意模块化系统的配置。版本选择上有个常见的坑有些项目可能已经引入了其他版本的Bouncy Castle导致冲突。检查依赖树很重要mvn dependency:tree | grep bouncycastle如果发现多个版本需要在pom.xml中排除冲突的依赖dependency groupIdcom.some.library/groupId artifactIdsome-artifact/artifactId version1.0/version exclusions exclusion groupIdorg.bouncycastle/groupId artifactId*/artifactId /exclusion /exclusions /dependency1.2 安全提供者注册的正确姿势注册Bouncy Castle提供者看似简单但不同的注册方式会影响整个JVM的加密行为。我推荐两种方式各有适用场景。方式一静态注册推荐用于独立应用在应用启动时注册最简单直接import org.bouncycastle.jce.provider.BouncyCastleProvider; import java.security.Security; public class SM2Demo { static { // 检查是否已注册避免重复注册 if (Security.getProvider(BC) null) { Security.addProvider(new BouncyCastleProvider()); } } public static void main(String[] args) { // 现在可以使用BC提供者了 } }方式二动态注册适合容器环境在某些容器或框架中静态注册可能影响其他组件。这时可以按需注册public class SM2Service { private static final Provider BC_PROVIDER new BouncyCastleProvider(); public byte[] sign(byte[] data, PrivateKey privateKey, byte[] userId) { // 临时注册使用后清理 Security.addProvider(BC_PROVIDER); try { // 执行签名操作 return doSign(data, privateKey, userId); } finally { // 移除提供者谨慎使用可能影响其他线程 Security.removeProvider(BC); } } }提示动态注册在多线程环境下要特别注意移除提供者可能影响其他正在进行的加密操作。通常建议在应用启动时一次性注册。常见问题排查表问题现象可能原因解决方案NoSuchProviderException: BC提供者未注册或注册失败检查static块是否执行确认依赖版本NoSuchAlgorithmException: SM2提供者不支持SM2算法确认使用bcprov-jdk15on而非bcprov-jdk14签名结果与其他系统不一致默认曲线参数不匹配显式指定sm2p256v1曲线性能低下重复创建Provider实例复用单例Provider对象2. SM2密钥对生成与管理生成SM2密钥对是签名验签的基础但这里有几个细节需要注意特别是密钥格式和存储方式。2.1 标准密钥生成代码先看一个完整的密钥生成示例import org.bouncycastle.jce.ECNamedCurveTable; import org.bouncycastle.jce.provider.BouncyCastleProvider; import org.bouncycastle.jce.spec.ECNamedCurveParameterSpec; import java.security.*; public class SM2KeyGenerator { static { Security.addProvider(new BouncyCastleProvider()); } // 使用国密标准曲线 private static final ECNamedCurveParameterSpec SM2_SPEC ECNamedCurveTable.getParameterSpec(sm2p256v1); /** * 生成SM2密钥对 * return 包含公钥和私钥的KeyPair */ public static KeyPair generateKeyPair() throws Exception { KeyPairGenerator keyPairGenerator KeyPairGenerator.getInstance(EC, BC); // 关键必须指定SM2曲线参数 keyPairGenerator.initialize(SM2_SPEC, new SecureRandom()); return keyPairGenerator.generateKeyPair(); } /** * 获取十六进制格式的公钥压缩格式 */ public static String getHexPublicKey(PublicKey publicKey) { java.security.interfaces.ECPublicKey ecPubKey (java.security.interfaces.ECPublicKey) publicKey; java.security.spec.ECPoint point ecPubKey.getW(); // SM2公钥通常以04开头表示非压缩格式 byte[] xBytes point.getAffineX().toByteArray(); byte[] yBytes point.getAffineY().toByteArray(); // 处理可能的符号位问题 xBytes trimBytes(xBytes, 32); yBytes trimBytes(yBytes, 32); return 04 bytesToHex(xBytes) bytesToHex(yBytes); } private static byte[] trimBytes(byte[] bytes, int targetLength) { if (bytes.length targetLength) { return bytes; } else if (bytes.length targetLength) { // 去除符号位填充的0 byte[] result new byte[targetLength]; System.arraycopy(bytes, bytes.length - targetLength, result, 0, targetLength); return result; } else { // 补0到指定长度 byte[] result new byte[targetLength]; System.arraycopy(bytes, 0, result, targetLength - bytes.length, bytes.length); return result; } } private static String bytesToHex(byte[] bytes) { StringBuilder sb new StringBuilder(); for (byte b : bytes) { sb.append(String.format(%02x, b 0xFF)); } return sb.toString(); } }2.2 密钥格式详解与转换SM2密钥有多种表示格式理解这些格式对于跨系统交互至关重要。公钥的三种常见格式非压缩格式04开头04 X坐标(32字节) Y坐标(32字节) 示例04d1a1065f36c116040a5aef12c2f9f34fd26a0af4e639f6602f9ad252fdaddcbe62bb4c7e065b6391822ec56e6822baded04bd98cf909a846e4a17b61cc9ae7de压缩格式02或03开头根据Y坐标的奇偶性选择02偶或03奇只存储X坐标02/03 X坐标(32字节)DER编码格式ASN.1 DER编码包含算法标识和公钥位串// 获取DER格式公钥 byte[] derEncoded publicKey.getEncoded();私钥格式 SM2私钥本质上是一个大整数但Java中通常以PKCS#8格式存储// 获取私钥的PKCS#8 DER编码 byte[] privateKeyDer privateKey.getEncoded(); // 解析PKCS#8获取原始私钥值 PKCS8EncodedKeySpec keySpec new PKCS8EncodedKeySpec(privateKeyDer); KeyFactory keyFactory KeyFactory.getInstance(EC, BC); PrivateKey parsedKey keyFactory.generatePrivate(keySpec); BigInteger privateValue ((ECPrivateKey) parsedKey).getS();2.3 密钥存储最佳实践在实际项目中密钥管理是个重要课题。这里分享几个实用技巧安全存储私钥import javax.crypto.SealedObject; import javax.crypto.Cipher; import java.io.*; public class KeyStorage { // 使用AES加密私钥后存储 public static void saveEncryptedPrivateKey(PrivateKey privateKey, String filePath, char[] password) throws Exception { // 生成密钥加密密钥KEK SecretKey aesKey deriveKeyFromPassword(password); Cipher cipher Cipher.getInstance(AES/GCM/NoPadding); cipher.init(Cipher.ENCRYPT_MODE, aesKey); SealedObject sealedKey new SealedObject(privateKey, cipher); try (ObjectOutputStream oos new ObjectOutputStream( new FileOutputStream(filePath))) { oos.writeObject(sealedKey); } } public static PrivateKey loadEncryptedPrivateKey(String filePath, char[] password) throws Exception { try (ObjectInputStream ois new ObjectInputStream( new FileInputStream(filePath))) { SealedObject sealedKey (SealedObject) ois.readObject(); SecretKey aesKey deriveKeyFromPassword(password); Cipher cipher Cipher.getInstance(AES/GCM/NoPadding); cipher.init(Cipher.DECRYPT_MODE, aesKey); return (PrivateKey) sealedKey.getObject(cipher); } } private static SecretKey deriveKeyFromPassword(char[] password) { // 使用PBKDF2生成密钥 // 实际实现略 return null; } }密钥轮换策略 对于高安全要求的系统建议实现密钥轮换机制主密钥长期存储用于加密工作密钥工作密钥定期更换如每30天新旧密钥并存期支持平滑过渡3. 带UserID的签名实现详解UserID是SM2签名的一个重要特性但也是最容易让人困惑的部分。它不仅仅是用户ID这么简单而是一个身份绑定机制。3.1 UserID的本质与作用很多人误以为UserID就是用户名或用户ID字段实际上它的作用要深刻得多。在SM2标准中UserID参与哈希计算将用户身份与公钥强绑定防止公钥替换攻击。UserID的选择原则长度建议16字节128位国密标准推荐值内容可以是身份证号、邮箱、员工号等唯一标识编码必须明确编码格式UTF-8最常用一致性签名和验签必须使用相同的UserID// UserID的常见处理方式 public class UserIDManager { // 标准长度UserID16字节 public static byte[] getStandardUserId(String userIdStr) { byte[] rawBytes userIdStr.getBytes(StandardCharsets.UTF_8); byte[] result new byte[16]; if (rawBytes.length 16) { System.arraycopy(rawBytes, 0, result, 0, 16); } else { // 不足16字节时补0 System.arraycopy(rawBytes, 0, result, 0, rawBytes.length); // 剩余部分保持为0 } return result; } // 计算ZA值SM2标准中的用户身份哈希 public static byte[] calculateZA(byte[] userId, ECPublicKeyParameters publicKey, ECDomainParameters domainParams) { SM3Digest digest new SM3Digest(); // 1. 哈希UserID长度2字节 int userIdLen userId.length * 8; // 位长度 digest.update((byte) (userIdLen 8)); digest.update((byte) userIdLen); // 2. 哈希UserID内容 digest.update(userId, 0, userId.length); // 3. 哈希曲线参数a byte[] a domainParams.getCurve().getA().toBigInteger().toByteArray(); digest.update(a, 0, a.length); // 4. 哈希曲线参数b byte[] b domainParams.getCurve().getB().toBigInteger().toByteArray(); digest.update(b, 0, b.length); // 5. 哈希基点G的x坐标 byte[] gx domainParams.getG().getXCoord().getEncoded(); digest.update(gx, 0, gx.length); // 6. 哈希基点G的y坐标 byte[] gy domainParams.getG().getYCoord().getEncoded(); digest.update(gy, 0, gy.length); // 7. 哈希公钥的x坐标 byte[] pubX publicKey.getQ().getXCoord().getEncoded(); digest.update(pubX, 0, pubX.length); // 8. 哈希公钥的y坐标 byte[] pubY publicKey.getQ().getYCoord().getEncoded(); digest.update(pubY, 0, pubY.length); byte[] za new byte[digest.getDigestSize()]; digest.doFinal(za, 0); return za; } }3.2 完整签名实现代码理解了UserID的原理后我们来看完整的签名实现。这里我封装了一个更健壮的签名工具类import org.bouncycastle.crypto.CipherParameters; import org.bouncycastle.crypto.params.*; import org.bouncycastle.crypto.signers.SM2Signer; import org.bouncycastle.jce.ECNamedCurveTable; import org.bouncycastle.jce.provider.BouncyCastleProvider; import org.bouncycastle.jce.spec.ECNamedCurveParameterSpec; import org.bouncycastle.math.ec.ECPoint; import java.math.BigInteger; import java.security.*; import java.util.Arrays; public class SM2SignatureService { private static final ECNamedCurveParameterSpec SM2_SPEC; private static final ECDomainParameters DOMAIN_PARAMS; static { Security.addProvider(new BouncyCastleProvider()); SM2_SPEC ECNamedCurveTable.getParameterSpec(sm2p256v1); DOMAIN_PARAMS new ECDomainParameters( SM2_SPEC.getCurve(), SM2_SPEC.getG(), SM2_SPEC.getN(), SM2_SPEC.getH() ); } /** * 生成带UserID的SM2签名 * param privateKey 私钥 * param userId 用户标识建议16字节 * param message 待签名消息 * return DER编码的签名数据 */ public static byte[] signWithUserId(PrivateKey privateKey, byte[] userId, byte[] message) throws Exception { // 参数校验 validateSignParams(privateKey, userId, message); // 转换私钥参数 BigInteger d ((java.security.interfaces.ECPrivateKey) privateKey).getS(); ECPrivateKeyParameters privKeyParams new ECPrivateKeyParameters(d, DOMAIN_PARAMS); // 创建签名器 SM2Signer signer new SM2Signer(); // 使用ParametersWithID包装私钥和UserID CipherParameters paramsWithId new ParametersWithID(privKeyParams, userId); signer.init(true, paramsWithId); // 更新消息并生成签名 signer.update(message, 0, message.length); byte[] signature signer.generateSignature(); // 验证生成的签名格式 validateSignatureFormat(signature); return signature; } /** * 验证带UserID的SM2签名 */ public static boolean verifyWithUserId(PublicKey publicKey, byte[] userId, byte[] message, byte[] signature) throws Exception { // 参数校验 validateVerifyParams(publicKey, userId, message, signature); // 转换公钥参数 java.security.spec.ECPoint publicPoint ((java.security.interfaces.ECPublicKey) publicKey).getW(); ECPoint bcPublicPoint DOMAIN_PARAMS.getCurve().createPoint( publicPoint.getAffineX(), publicPoint.getAffineY() ); ECPublicKeyParameters pubKeyParams new ECPublicKeyParameters(bcPublicPoint, DOMAIN_PARAMS); // 创建验证器 SM2Signer verifier new SM2Signer(); CipherParameters paramsWithId new ParametersWithID(pubKeyParams, userId); verifier.init(false, paramsWithId); // 更新消息并验证签名 verifier.update(message, 0, message.length); return verifier.verifySignature(signature); } private static void validateSignParams(PrivateKey privateKey, byte[] userId, byte[] message) { if (privateKey null) { throw new IllegalArgumentException(私钥不能为空); } if (!(privateKey instanceof java.security.interfaces.ECPrivateKey)) { throw new IllegalArgumentException(私钥必须是EC私钥类型); } if (userId null || userId.length 0) { throw new IllegalArgumentException(UserID不能为空); } if (message null || message.length 0) { throw new IllegalArgumentException(待签名消息不能为空); } // 检查私钥范围 BigInteger d ((java.security.interfaces.ECPrivateKey) privateKey).getS(); BigInteger n SM2_SPEC.getN(); if (d.compareTo(BigInteger.ONE) 0 || d.compareTo(n.subtract(BigInteger.ONE)) 0) { throw new IllegalArgumentException(私钥值不在有效范围内); } } private static void validateVerifyParams(PublicKey publicKey, byte[] userId, byte[] message, byte[] signature) { // 类似validateSignParams的实现 // 省略详细代码... } private static void validateSignatureFormat(byte[] signature) { if (signature null || signature.length 70 || signature.length 72) { throw new IllegalStateException(签名长度异常应为70-72字节); } // 检查ASN.1结构 if (signature[0] ! 0x30) { // SEQUENCE标签 throw new IllegalStateException(签名格式错误不是有效的ASN.1 SEQUENCE); } } }3.3 常见问题与调试技巧在实际使用中你可能会遇到这些问题问题1签名验证失败但不知道原因解决方案添加详细的日志记录public class DebugSM2Signer extends SM2Signer { private byte[] lastZA; private BigInteger lastR; private BigInteger lastS; Override public void init(boolean forSigning, CipherParameters param) { super.init(forSigning, param); if (param instanceof ParametersWithID) { ParametersWithID paramsWithId (ParametersWithID) param; // 可以在这里记录UserID等信息用于调试 System.out.println(UserID: Hex.toHexString(paramsWithId.getID())); } } Override public byte[] generateSignature() { byte[] sig super.generateSignature(); // 解析并记录r,s值 try { ASN1Sequence seq ASN1Sequence.getInstance(sig); lastR ASN1Integer.getInstance(seq.getObjectAt(0)).getValue(); lastS ASN1Integer.getInstance(seq.getObjectAt(1)).getValue(); System.out.println(Generated r: lastR.toString(16)); System.out.println(Generated s: lastS.toString(16)); } catch (Exception e) { // 忽略解析错误 } return sig; } }问题2与其他系统交互时签名不匹配可能的原因和检查点UserID编码不一致检查是否都使用UTF-8编码检查长度处理逻辑是否一致验证ZA计算过程是否相同公钥格式不一致确认是否都使用非压缩格式04开头检查坐标值是否做了正确的字节对齐消息预处理差异确认消息是否经过哈希处理检查是否有额外的编码转换如Base644. 签名格式解析与在线验证生成签名只是第一步理解签名格式和能够验证签名同样重要。SM2签名默认使用ASN.1 DER编码这个格式虽然标准但解析起来有些细节需要注意。4.1 ASN.1 DER格式深度解析让我们通过一个实际的签名例子来理解ASN.1结构import org.bouncycastle.asn1.*; import org.bouncycastle.util.encoders.Hex; public class SM2SignatureParser { /** * 解析DER编码的SM2签名 */ public static void parseSignature(byte[] derSignature) { try { ASN1Sequence sequence ASN1Sequence.getInstance(derSignature); // 第一个元素是r ASN1Integer rInt ASN1Integer.getInstance(sequence.getObjectAt(0)); BigInteger r rInt.getValue(); // 第二个元素是s ASN1Integer sInt ASN1Integer.getInstance(sequence.getObjectAt(1)); BigInteger s sInt.getValue(); System.out.println(解析结果); System.out.println(r (32字节): toFixedLengthHex(r, 32)); System.out.println(s (32字节): toFixedLengthHex(s, 32)); // 验证长度 byte[] rBytes r.toByteArray(); byte[] sBytes s.toByteArray(); System.out.println(r 实际字节数: rBytes.length); System.out.println(s 实际字节数: sBytes.length); // 处理可能的00前缀 if (rBytes.length 32 rBytes[0] 0) { System.out.println(注意r值有00前缀实际值从第2字节开始); } if (sBytes.length 32 sBytes[0] 0) { System.out.println(注意s值有00前缀实际值从第2字节开始); } } catch (Exception e) { throw new IllegalArgumentException(无效的DER签名格式, e); } } /** * 将BigInteger转换为固定长度的十六进制字符串 */ private static String toFixedLengthHex(BigInteger value, int byteLength) { byte[] bytes value.toByteArray(); StringBuilder hex new StringBuilder(); // 处理符号位导致的额外字节 int start 0; if (bytes.length byteLength bytes[0] 0) { start 1; } // 补零到指定长度 int padding byteLength - (bytes.length - start); for (int i 0; i padding; i) { hex.append(00); } // 添加实际值 for (int i start; i bytes.length; i) { hex.append(String.format(%02x, bytes[i] 0xFF)); } return hex.toString(); } /** * 将r和s值重新编码为DER格式 */ public static byte[] encodeToDer(BigInteger r, BigInteger s) { try { // 创建ASN.1整数自动处理00前缀 ASN1Integer rInteger new ASN1Integer(r); ASN1Integer sInteger new ASN1Integer(s); // 创建序列 ASN1EncodableVector vector new ASN1EncodableVector(); vector.add(rInteger); vector.add(sInteger); DERSequence sequence new DERSequence(vector); return sequence.getEncoded(); } catch (IOException e) { throw new RuntimeException(DER编码失败, e); } } /** * 将原始r,s字节数组转换为DER格式 */ public static byte[] rawToDer(byte[] rBytes, byte[] sBytes) { // 确保是正数BigInteger会认为最高位为1的字节数组是负数 BigInteger r new BigInteger(1, rBytes); // 1表示正数 BigInteger s new BigInteger(1, sBytes); return encodeToDer(r, s); } }4.2 在线验证工具使用技巧在线验证工具是调试SM2签名的利器但使用不当会导致验证失败。这里分享几个实用技巧技巧1正确处理公钥格式大多数在线工具要求公钥为16进制字符串且去掉04前缀。但有些工具又需要保留04前缀这需要根据具体工具调整。public class OnlineVerificationHelper { /** * 准备在线验证所需的参数 */ public static VerificationParams prepareForOnlineVerify( PublicKey publicKey, byte[] message, byte[] signature, byte[] userId) { VerificationParams params new VerificationParams(); // 1. 处理公钥 ECPublicKey ecPubKey (ECPublicKey) publicKey; ECPoint point ecPubKey.getW(); // 获取32字节的x,y坐标 byte[] xBytes point.getAffineX().toByteArray(); byte[] yBytes point.getAffineY().toByteArray(); // 处理可能的符号位问题 xBytes ensure32Bytes(xBytes); yBytes ensure32Bytes(yBytes); // 组合成非压缩格式04 x y params.publicKeyHex 04 Hex.toHexString(xBytes) Hex.toHexString(yBytes); // 2. 处理消息 params.messageHex Hex.toHexString(message); // 3. 处理签名 // 先解析DER格式获取r,s ASN1Sequence seq ASN1Sequence.getInstance(signature); BigInteger r ASN1Integer.getInstance(seq.getObjectAt(0)).getValue(); BigInteger s ASN1Integer.getInstance(seq.getObjectAt(1)).getValue(); // 转换为固定长度的十六进制 params.rHex toFixedLengthHex(r, 32); params.sHex toFixedLengthHex(s, 32); // 4. 处理UserID params.userIdHex Hex.toHexString(userId); return params; } /** * 确保字节数组为32字节 */ private static byte[] ensure32Bytes(byte[] bytes) { if (bytes.length 32) { return bytes; } else if (bytes.length 32) { // 去除开头的0 byte[] result new byte[32]; System.arraycopy(bytes, bytes.length - 32, result, 0, 32); return result; } else { // 前面补0 byte[] result new byte[32]; System.arraycopy(bytes, 0, result, 32 - bytes.length, bytes.length); return result; } } public static class VerificationParams { public String publicKeyHex; // 公钥带04前缀 public String publicKeyNoPrefix; // 公钥不带04前缀 public String messageHex; // 消息原文的16进制 public String rHex; // 签名的r值 public String sHex; // 签名的s值 public String signatureHex; // 完整的DER签名 public String userIdHex; // UserID的16进制 public VerificationParams() { // 自动生成不带04前缀的公钥 if (publicKeyHex ! null publicKeyHex.startsWith(04)) { publicKeyNoPrefix publicKeyHex.substring(2); } // 自动生成完整的DER签名 if (rHex ! null sHex ! null) { BigInteger r new BigInteger(rHex, 16); BigInteger s new BigInteger(sHex, 16); byte[] der encodeToDer(r, s); signatureHex Hex.toHexString(der); } } } }技巧2常见验证失败原因排查表验证失败现象可能原因解决方案公钥格式错误公钥未去除04前缀提交时去掉开头的04签名长度不正确r或s值不足32字节前面补0到32字节UserID不匹配UserID编码不一致统一使用UTF-8编码验签失败消息原文格式不对确认是否转换为16进制曲线参数不匹配使用了非SM2曲线确认使用sm2p256v1曲线技巧3自动化验证脚本为了方便调试可以编写一个简单的验证脚本#!/usr/bin/env python3 SM2签名在线验证辅助脚本 使用前需要安装requests库pip install requests import requests import json import sys def verify_online(public_key_hex, message_hex, r_hex, s_hex, user_id_hex): 调用在线验证接口 注意实际URL和参数格式需要根据具体工具调整 url https://const.net.cn/tool/sm2/verify/ # 构造请求参数 payload { publicKey: public_key_hex[2:], # 去掉04前缀 message: message_hex, r: r_hex, s: s_hex, userId: user_id_hex, curve: sm2p256v1 } try: response requests.post(url, jsonpayload, timeout10) result response.json() if result.get(success): print(✅ 验证成功) print(f 签名有效) else: print(❌ 验证失败) print(f 错误信息: {result.get(error, 未知错误)}) print(f 建议检查:) print(f 1. 公钥格式是否正确是否去掉了04前缀) print(f 2. 消息是否为16进制格式) print(f 3. r,s值是否为32字节64个十六进制字符) print(f 4. UserID是否与签名时一致) except Exception as e: print(f⚠️ 请求失败: {e}) if __name__ __main__: if len(sys.argv) ! 6: print(用法: python verify_sm2.py 公钥 消息 r值 s值 UserID) print(注意所有参数都应该是16进制字符串) sys.exit(1) verify_online(sys.argv[1], sys.argv[2], sys.argv[3], sys.argv[4], sys.argv[5])4.3 签名验证的单元测试策略为了保证签名验证的可靠性建议编写全面的单元测试import org.junit.jupiter.api.Test; import org.junit.jupiter.api.BeforeAll; import static org.junit.jupiter.api.Assertions.*; public class SM2SignatureTest { private static KeyPair keyPair; private static final byte[] USER_ID 1234567812345678.getBytes(); private static final byte[] MESSAGE 测试SM2签名验签.getBytes(StandardCharsets.UTF_8); BeforeAll static void setup() throws Exception { Security.addProvider(new BouncyCastleProvider()); keyPair SM2KeyGenerator.generateKeyPair(); } Test void testSignAndVerifyWithUserId() throws Exception { // 生成签名 byte[] signature SM2SignatureService.signWithUserId( keyPair.getPrivate(), USER_ID, MESSAGE); // 验证签名 boolean isValid SM2SignatureService.verifyWithUserId( keyPair.getPublic(), USER_ID, MESSAGE, signature); assertTrue(isValid, 签名验证应该成功); } Test void testDifferentUserIdFails() throws Exception { byte[] signature SM2SignatureService.signWithUserId( keyPair.getPrivate(), USER_ID, MESSAGE); // 使用不同的UserID验证 byte[] wrongUserId 8765432187654321.getBytes(); boolean isValid SM2SignatureService.verifyWithUserId( keyPair.getPublic(), wrongUserId, MESSAGE, signature); assertFalse(isValid, 不同UserID的验证应该失败); } Test void testTamperedMessageFails() throws Exception { byte[] signature SM2SignatureService.signWithUserId( keyPair.getPrivate(), USER_ID, MESSAGE); // 篡改消息 byte[] tamperedMessage 篡改后的消息.getBytes(StandardCharsets.UTF_8); boolean isValid SM2SignatureService.verifyWithUserId( keyPair.getPublic(), USER_ID, tamperedMessage, signature); assertFalse(isValid, 消息被篡改后验证应该失败); } Test void testSignatureFormat() throws Exception { byte[] signature SM2SignatureService.signWithUserId( keyPair.getPrivate(), USER_ID, MESSAGE); // 验证签名格式 assertNotNull(signature, 签名不能为空); assertTrue(signature.length 70 signature.length 72, 签名长度应该在70-72字节之间); assertEquals(0x30, signature[0] 0xFF, 签名应该以SEQUENCE标签(0x30)开头); // 解析并验证r,s值 ASN1Sequence seq ASN1Sequence.getInstance(signature); BigInteger r ASN1Integer.getInstance(seq.getObjectAt(0)).getValue(); BigInteger s ASN1Integer.getInstance(seq.getObjectAt(1)).getValue(); BigInteger n SM2_SPEC.getN(); assertTrue(r.compareTo(BigInteger.ONE) 0 r.compareTo(n.subtract(BigInteger.ONE)) 0, r值应该在[1, n-1]范围内); assertTrue(s.compareTo(BigInteger.ONE) 0 s.compareTo(n.subtract(BigInteger.ONE)) 0, s值应该在[1, n-1]范围内); } Test void testOnlineVerificationCompatibility() throws Exception { // 生成签名 byte[] signature SM2SignatureService.signWithUserId( keyPair.getPrivate(), USER_ID, MESSAGE); // 准备在线验证参数 VerificationParams params OnlineVerificationHelper.prepareForOnlineVerify( keyPair.getPublic(), MESSAGE, signature, USER_ID); // 验证参数格式 assertNotNull(params.publicKeyHex); assertTrue(params.publicKeyHex.startsWith(04), 公钥应该以04开头); assertEquals(130, params.publicKeyHex.length(), 非压缩公钥应该是130个字符04 64字节x 64字节y); assertNotNull(params.rHex); assertEquals(64, params.rHex.length(), r值应该是64个十六进制字符32字节); assertNotNull(params.sHex); assertEquals(64, params.sHex.length(), s值应该是64个十六进制字符32字节); // 可以在这里添加实际的在线验证调用 // 注意这需要网络连接可能不适合在CI/CD中运行 } }在实际项目中集成SM2签名验签最麻烦的往往不是算法本身而是那些边界情况和兼容性问题。比如不同系统对公钥格式的要求不同有的要带04前缀有的不要有的要求UserID固定16字节有的允许变长。这些细节问题需要在实际对接中不断调试和总结。我建议在项目初期就建立完善的测试用例覆盖各种边界情况。特别是与其他系统对接时先用小数据量测试确认格式和编码都正确后再上生产数据。SM2的在线验证工具是个很好的调试助手但要注意不同工具的实现可能有细微差别最好以国密标准文档为准。

相关新闻

WPF中利用ConverterParameter实现动态UI样式切换

WPF中利用ConverterParameter实现动态UI样式切换

1. 从静态到动态:为什么你的WPF界面需要ConverterParameter? 做WPF开发的朋友,肯定都遇到过这样的场景:界面上有个按钮,用户点击后,某个区域的背景色要从灰色变成蓝色;或者有个开关,…

2026/5/17 12:13:58 阅读更多 →
Ubuntu 22.04终端行距问题终极解决方案:修改LC_CTYPE的隐藏技巧

Ubuntu 22.04终端行距问题终极解决方案:修改LC_CTYPE的隐藏技巧

Ubuntu 22.04终端行距异常:从现象到本质的深度排查与根治方案 最近在深度使用Ubuntu 22.04 LTS时,一个看似微小却极其恼人的问题频繁出现:终端里的行间距变得异常宽大。无论是执行neofetch时被拉长的ASCII艺术Logo,还是ls -la命令…

2026/5/17 12:13:56 阅读更多 →
低延迟AI服务端实战:TTS流式推理的选型与优化

低延迟AI服务端实战:TTS流式推理的选型与优化

1. 技术方案选型:开源模型 vs. 在线API,到底怎么选? 上次我们聊完了LLM的流式推理,把延迟压到了0.5秒以内,感觉胜利在望。现在,接力棒传到了TTS(文本转语音)手里。说实话&#xff0c…

2026/5/17 12:13:55 阅读更多 →

最新新闻

VMPDump终极指南:如何快速破解VMProtect保护的Windows程序

VMPDump终极指南:如何快速破解VMProtect保护的Windows程序

VMPDump终极指南:如何快速破解VMProtect保护的Windows程序 【免费下载链接】vmpdump A dynamic VMP dumper and import fixer, powered by VTIL. 项目地址: https://gitcode.com/gh_mirrors/vm/vmpdump 你是否曾经面对VMProtect保护的软件感到束手无策&#…

2026/7/3 16:32:36 阅读更多 →
把 Claude Code 规则拆进 .claude/rules/,项目协作会清爽很多

把 Claude Code 规则拆进 .claude/rules/,项目协作会清爽很多

最近在整理 Claude Code 项目指令时,一个很容易被低估的目录开始变得特别重要,.claude/rules/。 很多团队刚开始用 Claude Code,通常会把所有项目约定都塞进 CLAUDE.md。构建命令放进去,测试命令放进去,代码风格放进去,接口规范放进去,安全要求也放进去。刚开始文件只有…

2026/7/3 16:30:35 阅读更多 →
CBCX外汇服务节奏顺手吗?清楚吗?

CBCX外汇服务节奏顺手吗?清楚吗?

如果围绕基础体验评估CBCX,用户通常更在意办理路径是否容易跟上,而不是热闹包装。这种偏简洁的表达,不会制造压力,反而更利于建立稳定印象。这些细节拼在一起,才构成CBCX外汇比较自然、也比较稳健的整体印象。从细节处…

2026/7/3 16:28:34 阅读更多 →
Spring Cloud OpenFeign负载均衡算法深度解析:源码、可扩展性与面试题

Spring Cloud OpenFeign负载均衡算法深度解析:源码、可扩展性与面试题

本文深入剖析Spring Cloud OpenFeign的负载均衡机制,从核心组件架构、RoundRobin/Random/Weighted等算法源码、ServiceInstanceListSupplier装饰器模式的可扩展性设计,到自定义负载均衡实战,最后附带10道高频面试题及答案剖析,助你…

2026/7/3 16:26:33 阅读更多 →
直流电机静音控制方案设计与实现

直流电机静音控制方案设计与实现

1. 项目概述:直流电机静音控制方案设计 在工业自动化和消费电子领域,直流电机的噪声问题一直是工程师面临的常见挑战。传统PWM控制方式虽然简单高效,但开关噪声和电磁干扰问题尤为突出。本项目采用东芝TB9051FTG电机驱动IC搭配德州仪器TM4C12…

2026/7/3 16:26:33 阅读更多 →
基于STM32单片机宠物自动喂食系统喂水控制系统 WIFI监控宠物喂养1(设计源文件+万字报告+讲解)(支持资料、图片参考_降重降ai)

基于STM32单片机宠物自动喂食系统喂水控制系统 WIFI监控宠物喂养1(设计源文件+万字报告+讲解)(支持资料、图片参考_降重降ai)

基于STM32单片机宠物自动喂食系统喂水控制系统 WIFI监控宠物喂养1(设计源文件万字报告讲解)(支持资料、图片参考_降重降ai) 版本0 :5个定时喂食喂食提醒自动/手动模式TFT液晶显示年,月,日,十,分…

2026/7/3 16:24:33 阅读更多 →

日新闻

Nginx防御TLS重协商攻击实战:从原理到配置与监控

Nginx防御TLS重协商攻击实战:从原理到配置与监控

1. 项目概述:为什么TLS重协商攻击至今仍需警惕十多年前的CVE-2011-1473,一个关于TLS/SSL协议重协商机制的漏洞,现在提起来还有必要吗?很多运维和开发朋友可能会觉得,这都老掉牙了,现代服务器和客户端不都默…

2026/7/3 0:03:59 阅读更多 →
华为防火墙双通道远程管理实战:Web与SSH配置详解

华为防火墙双通道远程管理实战:Web与SSH配置详解

1. 项目概述:为什么需要双通道远程管理防火墙?在任何一个稍具规模的企业网络里,防火墙都是那个默默守护在边界的关键角色。作为网络工程师,我们不可能每次都跑到机房,插上console线去配置它。远程管理能力,…

2026/7/3 0:03:59 阅读更多 →
AD74413R与PIC18F65K40的高精度工业数据采集方案

AD74413R与PIC18F65K40的高精度工业数据采集方案

1. 项目概述:AD74413R与PIC18F65K40的协同工作在工业自动化和精密测量领域,同时实现高精度模数转换(ADC)和数模转换(DAC)功能是许多复杂系统的核心需求。AD74413R作为一款四通道可配置模拟输入/输出器件,与PIC18F65K40微控制器的组合&#xf…

2026/7/3 0:05:59 阅读更多 →

周新闻

月新闻