
简介面向C#开发者的SM2加签、验签工具包帮助在Windows平台上快速实现基于椭圆曲线密码体系的国产算法签名与验证适用于安全敏感型应用开发。工具包共29个文件约2.75MB包含C#源码文件、窗体界面、可执行程序及BouncyCastle.Crypto.dll核心加密库另有配置文件、调试符号等结构完整可直接打开Visual Studio工程运行或参考源码进行二次集成。其中BouncyCastle.Crypto.dll负责SM2算法底层运算开发者可聚焦业务逻辑而不必重复实现密码学细节。通过界面可直观操作加签、验签流程也能将核心逻辑嵌入自有业务系统满足等保、国密改造等场景需求。目前已有680人学习下载适合正在调研SM2落地方式、希望在C#项目中快速集成国密算法的初中级开发者。整体将SM2原理演示与代码实现合二为一省去从零搭建环境与调试的时间是实用的工程参考。1. 把 SM2 加签验签封装成工具一次交付省掉整个项目组的重复劳动做 C# 对接国密改造时你会发现最耗时间的不是写业务逻辑而是每个对接方拿出来的 SM2 签名规范都不一样。有的给你十六进制串有的给你 Base64有的签名结果是 64 字节的 r||s有的非要 ASN.1 DER 编码还有的在原文上先做了摘要再让你签。这套东西每个项目都重写一遍写的都是同样的坑。把 C# SM2 加签、验签工具整成一个 rar 交付物本质是把这些边界情况都收拾干净给出一个稳定的、可复用的接口让调用方只关心原文和密钥不关心字节序和编码格式。它解决的是“国密对接时签名验签代码到处复制、改出毛病没人管”的痛点。适合正在做等保改造、政务系统对接、或是接手了国产化适配项目的 .NET 工程师。下面这套方案我按自己交付过多次的结构来讲。2. 先搞懂 SM2 签名验签的底层逻辑为什么你的签名总是对不上2.1 SM2 签名不是简单的“私钥加密、公钥解密”常见误解是把 RSA 那套搬到 SM2 上拿私钥对哈希值做“加密”拿公钥“解密”回来比对。SM2 完全不是这个路子。SM2 签名基于椭圆曲线离散对数难题签名过程生成一个随机数 k然后通过椭圆曲线点乘计算出 r 和 s 两个大整数。验签时把 r、s 和原文摘要代进验签方程验证等式是否成立——它是不存在“解密”这一说的。这意味着两个直接后果。第一签名结果是两个大整数不是一段密文落地成字节数组的时候就有排列顺序的问题。第二签名过程引入了随机数 k同一份原文同一把私钥每次签出来的结果都不一样你没法用“比对签名结果是否相同”来验证只能调验签接口去验。还有一层是摘要算法。SM2 签名规范里 Z 值计算要拼接用户 ID、曲线参数和公钥然后对 Z 值加原文一起算 SM3 摘要。不少对接方在“只对原文做 SM3”还是“按规范算 Z 值再加原文”这两个口径上打架。工具包如果只暴露一个 Sm2Sign(原文) 方法内部就必须把这条规则定死不然某天换成别的单位对接同样的代码会出完全不同的签名结果。2.2 公私钥的两种主流编码形态十六进制串和 ASN.1 DER实战里遇到最多的是公私钥的编码问题。国密局推荐的 SM2 密钥是 32 字节私钥、65 字节未压缩公钥04 开头x 和 y 各 32 字节。但落到具体系统里各家给的资料五花八门。常见形态可以整理成一张表形态格式说明典型场景裸十六进制私钥64 个十六进制字符对应 32 字节大整数很多数据交换平台直接下发 d 值04 开头的裸公钥130 个十六进制字符04 x y最常见各家基本统一PEM 格式私钥-----BEGIN PRIVATE KEY----- 包裹的 DER 数据用 OpenSSL 生成或用工具导出的PKCS#8 或 PKCS#1 变体私钥外层 ASN.1 结构不同Java 或 .NET 自带类库导出的格式你写的工具如果只支持一种格式项目里就会有人拿 Java 那边导出的 PKCS#8 私钥来问你“为什么一直报密钥长度错误”。所以工具第一层要做的就是格式归一化把各种输入格式统一解析成曲线上的 d 值和公钥点坐标后续签名验签全部基于解析后的内部结构不再关心外部格式。另外要留意 04 开头公钥和 C1C3C2 加密输出的区别。签名工具里只需要 04 开头的公钥但如果你把加密接口也放进来就要认清 SM2 加密密文是 C1C3C2 结构而不是 C1C2C3很多对接文档里这一点会写反。2.3 Z 值计算和用户 ID验签不过的第一大元凶验签失败十有八九是 Z 值计算口径不一致。SM2 签名规范原文里定义了 Z 值计算对 ENTLA用户 ID 的长度、用户 ID、曲线参数 a、b、Gx、Gy、公钥 Px、Py 做 SM3 摘要。用户 ID 默认值是 1234567812345678但制定标准时同时说明具体应用可以自定义。问题是很多对接方文档里不写 ID默认你没设另一头可能用了空字符串。工具里必须把 UserID 作为可配置参数暴露出来默认用标准值同时允许调用方指定。我在实际项目里遇到过一个数据交换平台他们的 UserID 是单位机构代码不传就验签失败。这类问题不通过工具参数暴露出来排查起来能浪费一天。摘要顺序也要注意。规范是先对 Z 值做一次 SM3再把摘要结果拼接原文再做一次 SM3 得到最终摘要 e。很多自研实现偷懒直接对原文做 SM3这在自测时能通过因为自签自验同一套逻辑但一对接正规系统就暴露。2.4 签名结果的两种编码r||s 和 DER 的取舍签名结果是两个 32 字节大整数 r 和 s。落到字节流有两种约定方式直接拼接 r||s 是最简单的总共 64 字节很多内部系统用这个。但要注意 r 或 s 不足 32 字节时前面要补 0这个细节容易忽略。另一个是 ASN.1 DER 编码结构是 SEQUENCE { INTEGER r, INTEGER s }长度不固定一般在 70 字节左右。Java 的 Signature 类默认生成 DER你在 C# 里验签就要两条路都支持。工具里我会把签名接口做成两个层次底层返回 r 和 s 的 BigInteger让高级用户自己拼上层提供 SignToHex 和 SignToBase64分别对应 r||s 十六进制和 DER 编码。验签同理做成自动识别格式先看长度再尝试解析比让调用方传参更省心。3. 把工具拆成三层结构从底层算法到调用方友好接口3.1 第一层BouncyCastle 的 SM2 支持到底能不能直接用C# 里做国密BouncyCastle 是绕不开的底座。它提供了 Org.BouncyCastle.Crypto.EC.CustomNamedCurves 里的 SM2P256V1 曲线定义以及 SM3Digest、SM2Signer 这些实现。但直接用原生 API 写业务代码你很快会发现三个别扭的地方第一SM2Signer 初始化时要把 ID 转成字节数组作为 IParameters 传进去这个参数很容易被忽略。第二它生成的签名结果默认是 DER 格式想要 r||s 得自己解析 DER 结构。第三密钥参数要从 BigInteger 和 ECPoint 构造而手头的私钥是十六进制字符串还要自己把字符串转成曲线点。所以底层封装要做的就是把 BouncyCastle 的这些“能跑但不顺手”的 API 包一层对外只暴露简单方法。我用的是自定义类库命名为 SM2CryptoService内部持有曲线参数对象避免每次签名都重新查表加载曲线。3.2 第二层密钥解析器的四个入口覆盖实战中 95% 的密钥格式密钥解析是工具的核心价值之一我给它做了四个静态入口public static SM2KeyPair ParseFromHex(string privateKeyHex, string publicKeyHex) { var curve CustomNamedCurves.GetByName(sm2p256v1); var domain new ECDomainParameters(curve, curve.G, curve.N, curve.H); var d new BigInteger(1, Hex.Decode(privateKeyHex)); var q curve.Curve.DecodePoint(Hex.Decode(publicKeyHex)); return new SM2KeyPair( new ECPrivateKeyParameters(d, domain), new ECPublicKeyParameters(q, domain) ); }这个入口要求私钥是 32 字节裸十六进制、公钥是 04 开头的裸公钥是最常用的形态。逻辑上先拿到曲线定义再分别解码私钥大整数和公钥曲线点最后构造出 BouncyCastle 的密钥参数对象。参数说明Hex.Decode 是 BouncyCastle 自带的十六进制解码工具不要自己写DecodePoint 会检查公钥长度和解压格式如果公钥不是 04 开头会直接抛异常。public static SM2KeyPair ParseFromPem(string privateKeyPem, string publicKeyPem null) { var reader new StringReader(privateKeyPem); var pemReader new PemReader(reader); var pemObject pemReader.ReadObject(); if (pemObject is AsymmetricCipherKeyPair keyPair) { var privKey (ECPrivateKeyParameters)keyPair.Private; var pubKey (ECPublicKeyParameters)keyPair.Public; return new SM2KeyPair(privKey, pubKey); } throw new InvalidOperationException(PEM 文件格式不可识别); }PEM 入口兼容 OpenSSL 生成的密钥对。注意 PemReader 读出来可能是 AsymmetricCipherKeyPair同时含公私钥也可能是 ECPrivateKeyParameters只有私钥要分情况处理。实战中 Java 系统导出的私钥往往只有私钥部分公钥要另外传入。第三个入口是解析 PKCS#8 格式的。PKCS#8 里 SM2 私钥的 OID 是 1.2.156.10197.1.301解析时要用 PrivateKeyInfoFactory。第四个入口是从公钥坐标直接构造x 和 y 各 32 字节可以用两个 BigInteger 直接构造 ECPoint适用于对接方只给你 x、y 坐标的文档场景。3.3 第三层加签验签的对外接口把编码差异全部吞掉对外接口我设计成四种组合全部以字符串形式输入输出这样调用方不需要引用任何加密库的类型public static string SignHex(string privateKeyHex, string data, string userId null) { var keyPair ParseFromHex(privateKeyHex, null); var signer new SM2Signer(); var parameters new ParametersWithID( new ParametersWithRandom(keyPair.Private, new SecureRandom()), Encoding.UTF8.GetBytes(userId ?? DEFAULT_USER_ID) ); signer.Init(true, parameters); var dataBytes Encoding.UTF8.GetBytes(data); signer.BlockUpdate(dataBytes, 0, dataBytes.Length); var derSign signer.GenerateSignature(); // 将 DER 格式转换成 r||s 64字节十六进制 var r derSign[0]; var s derSign[1]; var rBytes r.ToByteArrayUnsigned(); var sBytes s.ToByteArrayUnsigned(); // 统一补足 32 字节 var padded new byte[64]; Buffer.BlockCopy(rBytes, 0, padded, 32 - rBytes.Length, rBytes.Length); Buffer.BlockCopy(sBytes, 0, padded, 64 - sBytes.Length, sBytes.Length); return Hex.ToHexString(padded); }这段代码有四个关键点。SM2Signer.Init 的第一个参数 true 表示签名模式false 是验签模式写反了不会报错但结果会对不上。ParametersWithID 嵌套了两层外层是 ID内层是带随机数的私钥参数。GenerateSignature 返回的是 BigInteger 数组下标 0 是 r下标 1 是 s。最后一步补零很关键如果 r 或 s 的实际位数不足 256 位不补零会变短对方验签时按 32 字节解析就会错位。验签接口对称实现public static bool VerifyHex(string publicKeyHex, string data, string signatureHex, string userId null) { var keyPair ParseFromHex(null, publicKeyHex); var signer new SM2Signer(); var parameters new ParametersWithID(keyPair.Public, Encoding.UTF8.GetBytes(userId ?? DEFAULT_USER_ID)); signer.Init(false, parameters); var dataBytes Encoding.UTF8.GetBytes(data); signer.BlockUpdate(dataBytes, 0, dataBytes.Length); // 先尝试按 r||s 解析 var sigBytes Hex.Decode(signatureHex); if (sigBytes.Length 64) { var r new BigInteger(1, sigBytes, 0, 32); var s new BigInteger(1, sigBytes, 32, 32); return signer.VerifySignature(r, s); } // 否则尝试 DER 格式解析 try { var derSig DerSequence.GetInstance(sigBytes); var r ((DerInteger)derSig[0]).Value; var s ((DerInteger)derSig[1]).Value; return signer.VerifySignature(r, s); } catch (Exception) { return false; } }验签接口做了一个自动识别。先判断长度64 字节按 r||s 处理否则尝试按 DER 解析。这个设计避免了调用方在两种格式之间传错参数的问题。DER 解析用 Asn1Sequence.GetInstance 而不是手动掐头去尾因为 DER 里 r 或 s 可能带符号位补充字节手动解析很容易踩坑。3.4 顺手加一个 SM3 哈希工具和文件签名重载既然做了 SM2SM3 几乎是必然配套的。我在工具包里放了一个 Sm3Helper用来暴露摘要能力。SM3 摘要结果是 32 字节 64 位十六进制这个在很多对接场景里要用来做数据完整性校验而 SM2 签名前实际上也要算摘要只是被封装在 SM2Signer 内部了。文件签名是另一个高频需求。对接方有时要求对文件的原始字节做签名而不是对文件内容的字符串做签名。我把接口重载成public static string SignFile(string privateKeyHex, string filePath, string userId null) { var bytes File.ReadAllBytes(filePath); return SignBytes(privateKeyHex, bytes, userId); }这里的重点是文件读取要按二进制读不能用 ReadAllText。有个项目对接时对方对 PDF 文件做签名我方按 UTF-8 文本读入再转字节两边算出来的摘要完全不同。文件签名时建议额外记录文件长度和最后修改时间排查问题时有参照。4. 用命令行程跑通最小验证从签名到验签的完整闭环4.1 搭建一个 30 行的命令行 Demo验证工具包本身可用工具类库写好后第一步要验证它自己签出来的签名自己能验过。我习惯在解决方案里加一个 Console 项目做自测代码很短但覆盖面要够static void Main(string[] args) { // 测试向量用了国密标准文档里的官方密钥对 const string privateKeyHex 3945208F7B2144B13F36E38AC6D39F95889393692860B51A42FB81EF4DF7C5B8; const string publicKeyHex 09F9DF311E5421A150DD7D161E4BC5C672179FAD1833FC076BB08FF356F35020 CCEA490CE26775A52DC6EA718CC1AA600AED05FBF35E084A6632F6072DA9AD13; const string userId 1234567812345678; const string message 国密SM2签名验签测试; var signature Sm2Service.SignHex(privateKeyHex, message, userId); Console.WriteLine($签名结果: {signature}); Console.WriteLine($签名长度: {signature.Length}); var valid Sm2Service.VerifyHex(publicKeyHex, message, signature, userId); Console.WriteLine($验签结果: {valid}); // 篡改测试 var tampered message ; var valid2 Sm2Service.VerifyHex(publicKeyHex, tampered, signature, userId); Console.WriteLine($篡改后验签: {valid2}); }这里用的测试密钥对来自国密标准文档的示例不是随便生成的。用标准向量自测好处是如果你的算法实现有误和标准结果对不上但只跑自签自验无法发现“两边实现口径不一致”的问题所以还要准备交叉测试。4.2 时间戳随机性验证同一原文签两次结果必须不同SM2 签名的随机数特性可以用一个小脚本验证顺带确认 SecureRandom 工作正常。同一把私钥签同一段原文十次如果十次结果完全一样说明随机源出了问题这在真实场景中等于私钥泄露。写一个循环把每次的签名结果收集起来比较var signatures new Liststring(); for (var i 0; i 10; i) { signatures.Add(Sm2Service.SignHex(privateKeyHex, message, userId)); } var distinct signatures.Distinct().Count(); Console.WriteLine($10次签名去重后: {distinct} 个不同结果);正常情况下 distinct 应该等于 10。如果出现重复检查 SecureRandom 的初始化方式看看是不是把随机种子固定了。这是我在代码审查时经常会发现的问题有同事为了测试可复现性把随机种子用固定值初始化一上线签名就不安全了。生产环境绝不能用固定随机源这是底线。4.3 与标准工具交叉验证用 OpenSSL 拉起一份对照数据C# 侧自测通过不算数真正的考验是和其他语言实现交叉验签。常见做法是让 Java 或 OpenSSL 用同样密钥对同一原文签名然后 C# 工具去验反过来也成立。以 OpenSSL 为例它 3.0 以上版本支持 SM2操作两步先用 EC 参数生成 SM2 密钥对再对原文做签名。验签时把 OpenSSL 产生的签名文件读入注意它输出的默认是 DER 格式你工具的自动识别在这时就派上用场了。这个交叉验证解决了一个隐蔽问题你的实现可能和自己的验签代码配合良好但在和外部系统对接时仍然失败。因为双方对 UserID、签名格式、甚至原文编码UTF-8 还是 GBK的理解不同。我一般会在对接前准备一个“签名样例清单”包含原文、私钥、UserID、期望签名值发给对方确认两边先对准这个清单再联调。4.4 在 rar 交付包里附带这个 Demo 项目整个工具压缩成 rar 交付时我的目录结构是这样组织的SM2Toolkit/ ├── src/ │ ├── SM2Crypto/ # 工具类库源码 │ ├── SM2Demo/ # 命令行自测项目 │ └── SM2WinForm/ # 简单的图形界面 ├── docs/ │ ├── 接口说明.md │ └── 对接样例.md ├── lib/ │ └── BouncyCastle.Cryptography.dll └── README.txt把源码完整放进去而不是只放编译好的 dll是考虑到接手的人可能要改 UserID 处理逻辑或是增删编码格式。docs 目录里我会写清楚每个接口的输入输出示例这样不熟悉国密的后端也能照着对接。dll 目录放依赖是为了让 Demo 解压就能跑不需要现场翻 NuGet。5. 常见问题与避坑指南SM2 联调现场的血泪经验5.1 签出来的长度不对64 位还是 70 位十六进制现象对方说收到的签名长度不是 128 个十六进制字符或者你收到对方的签名发现是 140 个字符。原因签名编码一个是 r||s 拼接64 字节 128 位十六进制字符一个是 DER 编码长度不固定通常 70 字节左右 140 位十六进制字符。解决先问清楚对方要哪种格式或者工具验签时自动兼容。我的经验是如果是对接正规的密码机或第三方平台多数用 r||s 十六进制如果是 Java 的 Signature 类标准 API 生成的多半是 DER。设计上让验签自动识别两种格式能让联调阶段少一半扯皮。5.2 验签一直返回 false但双方都说自己的实现没问题现象两边各自自测都通过交叉验签就是不过。原因九成是 UserID 不一致剩下的可能是摘要算法顺序、原文编码。常见的是 A 系统用默认的 1234567812345678B 系统用空字符串或者机构代码。解决把 UserID 作为参数显式传给接口并且先问清楚对接方的 UserID 是什么。如果对方自己也不明确建议用默认值并两端确认。另一个隐藏点是 UserID 要转成 UTF-8 字节再参与运算有些系统用了 GBK 编码也会导致摘要对不上。5.3 公钥解析报错“invalid point coordinates”现象ParseFromHex 时 DecodePoint 抛出异常。原因公钥长度不对或者不是 04 开头。有些系统会把公钥去掉 04 前缀只传 128 位十六进制字符串x 和 y 拼接DecodePoint 不认识这种格式。解决解析入口里加一层自动补全如果长度是 128自动在前面拼 04 再解析。还要注意有些系统把 04 写成 04 的十六进制形式即 3034要仔细看字符串内容不能只看开头两位字符是不是 04。5.4 签名结果每次不同导致对方无法比对现象对方拿着你第一次签名的结果做字符串比对发现第二次签名结果不同判定“签名不稳定”。原因这不是 bugSM2 签名本来就有随机性。但这说明对方对 SM2 特性不够了解如果你的工具文档里没写清楚会白白消耗双方排查时间。解决文档里明确写“SM2 签名具有随机性同一原文签名结果不同属于正常现象”并且在 README 里给出一个验签步骤教对方怎么判断签名有效性而不是比较字符串。这个解释在联调第一天就要发给对方。5.5 离线包解压后运行报缺少依赖现象用户解压 rar 后运行 Demo提示找不到 BouncyCastle.Cryptography.dll 或者版本冲突。原因BouncyCastle 有两个命名空间的版本并存旧包命名空间是 Org.BouncyCastle新包改了命名空间二者不能同时引用。解决交付的 lib 目录里放一个锁定版本的 dll并在 README 里写明目标框架是 .NET Framework 4.6.1 还是 .NET 6/8不同框架下依赖行为不同。另一个踩坑是 x86 和 x64 的 AnyCPU 切换问题如果目标机器是 32 位进程要注意某些 BC 版本在 32 位下使用 SecureRandom 时的兼容性。5.6 大文件签名时内存暴涨现象对几百 MB 文件签名程序内存占用突增甚至 OOM。原因一次性把整个文件读入内存再签名。解决改用流式更新SM2Signer 支持 BlockUpdate 分段喂数据但要注意文件哈希计算和签名计算要同步做。更稳妥的方式是先算出文件 SM3 摘要再对摘要做签名因为很多对接方接受的是“先摘要后签名”模式。我在工具里提供了两个文件签名入口一个直接签名原文件字节一个先摘要再签名对接时按对方要求选择。5.7 私钥以字符串硬编码在代码里现象代码仓库里躺着明文的 SM2 私钥十六进制字符串。原因开发阶段图省事直接把测试私钥写在常量里忘了移除。解决工具提供从加密配置读取私钥的能力比如从环境变量或者独立的密钥配置文件读取并做基本混淆。但坦白说纯 C# 程序里的私钥保护只能做到增加门槛真正要安全得用硬件密码机或 KSP 存储工具层面只能做好接口约定。6. 进阶生成密钥对、签名结果归一化和自动化回归测试6.1 给工具加上 SM2 密钥对生成器对接时对方经常说“你生成一对密钥给我”所以工具要内置生成能力。用 SecureRandom 生成 32 字节私钥大整数确保它在 1 到 n-1 之间然后做椭圆曲线点乘得到公钥public static SM2KeyPair GenerateKeyPair() { var curve CustomNamedCurves.GetByName(sm2p256v1); var domain new ECDomainParameters(curve, curve.G, curve.N, curve.H); var secureRandom new SecureRandom(); var d new BigInteger(256, secureRandom); // 确保私钥在合法范围内 d d.Mod(domain.N.Subtract(BigInteger.One)).Add(BigInteger.One); var q curve.G.Multiply(d).Normalize(); return new SM2KeyPair( new ECPrivateKeyParameters(d, domain), new ECPublicKeyParameters(q, domain) ); }这里要注意 d 的范围检查。BigInteger(256, random) 生成的是 0 到 2^256-1 的随机数直接用它当私钥有极小概率为 0必须先 Mod 再加 1。生成结果导出时私钥输出 64 位十六进制公钥输出 04 开头的 130 位十六进制并打印一份 PEM 格式方便导入 OpenSSL。6.2 签名结果归一化把 DER 转成 r||s 的标准方法不少对接场景要求统一成 r||s 格式但 Java 那边给过来的是 DER。写一个转换函数很有用public static string DerToRawHex(string derHex) { var derBytes Hex.Decode(derHex); var seq DerSequence.GetInstance(derBytes); var r ((DerInteger)seq[0]).Value.ToByteArrayUnsigned(); var s ((DerInteger)seq[1]).Value.ToByteArrayUnsigned(); var raw new byte[64]; Buffer.BlockCopy(r, 0, raw, 32 - r.Length, r.Length); Buffer.BlockCopy(s, 0, raw, 64 - s.Length, s.Length); return Hex.ToHexString(raw); }这里的核心陷阱是 DerInteger 的 Value 在处理负数时会反码所以要用 ToByteArrayUnsigned。做转换前先验证 DER 结构合法性避免对方发来一段乱码导致解码异常被当成业务报错。6.3 自动化回归测试一组固定向量守住工具不回归工具交付后会被反复修改改一次挂一次的情况很常见。我写完工具后会写一组 NUnit 回归测试把曲线参数、密钥对、UserID、原文和期望的签名摘要锁死。这样任何人动过底层解析代码一跑测试就知道改坏了哪里。测试用例不用太多五个就够标准向量签名验签、短私钥补零场景私钥开头是 00 的用例、DER 格式兼容验签、错 UserID 必须验签失败、篡改原文必须验签失败。每个用例都写清楚注释说明对应的是实际项目里哪个坑。这组测试随着 rar 包一并交出去对方改代码时也能自保。6.4 我对这个工具的使用习惯这套工具我迭代过几个版本最大的感悟是国密对接的难题不在算法而在“口径”。密钥格式、签名编码、UserID、原文编码这四个变量只要对上一个项目就是成功。我现在的交付习惯是拿到对接文档先做一份“参数确认清单”把这四个变量列出来发给对方书面确认然后才动手写代码。这个习惯帮我避开过不少返工。如果你打算在自己的项目里做 SM2 加签验签不用重复造轮子把上面的解析器和接口照着自己的项目结构整理一遍重点保住密钥格式兼容和签名格式自动识别这两条。前者让你不挑对接方后者让你的联调顺畅许多。从这套结构开始再按自己项目的依赖管理方式把 BouncyCastle 引进来跑通一次签名验签闭环剩下的事情就都是体力和联调了。希望帮到你。本文还有配套的精品资源点击获取