C#实现SM2国密算法:从原理到.NET/Java互通实战

📅 2026/7/5 9:39:15
C#实现SM2国密算法:从原理到.NET/Java互通实战
1. 项目概述为什么C#开发者需要掌握SM2国密算法如果你是一名C#开发者最近在对接银行、政府项目或者涉及数据安全合规的金融系统时很可能已经遇到了“国密算法”这个硬性要求。SM2作为国家密码管理局发布的椭圆曲线公钥密码算法正迅速成为国内商用密码体系的核心。与大家熟知的RSA相比SM2在同等安全强度下密钥更短、运算更快并且是国家明确要求优先采用的算法标准。这意味着未来越来越多的国内系统交互、数据加密、电子签章都将基于SM2构建。然而当你在.NET生态中寻找SM2的实现时可能会发现官方库并未直接提供支持网上的资料要么是零散的代码片段要么是与Java互通时各种“坑”的抱怨。这正是我写下这篇完整指南的原因。我将基于一个可运行、可互通的完整C#源码带你从零开始彻底吃透SM2在C#中的实现。不止是调用API我会深入BouncyCastle库的内部拆解密钥生成、加密解密、签名验签的每一个步骤并重点解决那个最让人头疼的“C1C2C3”与“C1C3C2”顺序问题以及.NET与Java跨语言互通的“04”前缀陷阱。无论你是需要紧急集成国密功能还是想深入理解其原理这篇文章都能让你直接“抄作业”避开我踩过的所有坑。2. 核心原理与设计思路SM2不仅仅是另一个ECC在动手写代码之前理解SM2的设计思路和与标准ECC椭圆曲线密码学的差异至关重要。这能帮你从根本上理解后续遇到的很多“奇怪”问题。2.1 SM2算法核心构成SM2不是一个单一的算法而是一个算法套件主要包含三个部分数字签名算法相当于ECDSA的国密版本用于身份认证和完整性校验。密钥交换协议用于双方在不安全的信道上协商出一个共享的会话密钥。公钥加密算法我们本文重点用于数据的加密和解密。SM2基于椭圆曲线密码学但它使用的是一条特定的、被标准化的椭圆曲线名为sm2p256v1。这条曲线的参数如素数域、方程系数、基点G、阶N都是固定的由国家密码管理局定义。这意味着所有遵循国密的实现都必须使用同一套曲线参数这是实现互通的基础。2.2 加密流程与“C1C2C3”之谜SM2的公钥加密过程比RSA的“直接加密”要复杂一些它本质上是一种基于椭圆曲线的集成加密方案ECIES的变体。其加密结果并非一个简单的密文块而是由三部分拼接而成C1: 椭圆曲线上的一个点由随机数k与基点G计算得出C1 [k]G。它代表了本次加密的临时公钥。C2: 实际的密文由明文与一个派生出的密钥流进行异或运算得到。C3: 消息摘要由多个要素通过SM3杂凑算法计算得出用于完整性校验。那么问题来了这三个部分拼接的顺序是什么这就是“C1C2C3”和“C1C3C2”两种标准的由来。旧标准C1C2C3早期国密标准文档中定义的顺序。BouncyCastle库的默认实现就采用此顺序。新标准C1C3C2在GM/T 0009-2012等后续标准中推荐的顺序。目前许多新的国密硬件、Java库如Hutool默认采用此顺序。关键点如果你的系统需要与其他系统尤其是Java端互通首要任务就是确认对方使用的是哪种顺序。顺序不对解密必然失败。我们的代码必须能同时处理这两种情况。2.3 为什么需要BouncyCastle.NET Framework/Core自身并没有提供SM2的实现。因此我们引入了密码学领域的“瑞士军刀”——BouncyCastle库。它是一个强大的开源密码学库提供了大量算法实现包括国密SM2、SM3、SM4。我们将通过NuGet包Portable.BouncyCastle来使用它。然而BouncyCastle的SM2实现有一些“坑”需要注意其SM2Engine默认输出是旧标准的C1C2C3。其SM2签名结果输出的是ASN.1 DER编码格式一种结构化的二进制编码而有时我们需要的是简单的R||S字节数组拼接。加密结果有时会带一个0x04前缀表示非压缩格式的椭圆曲线点这在某些解析严格的库中会报错。我们的工具类GmUtil核心工作之一就是封装BouncyCastle处理这些差异提供一个统一、易用且支持互通的接口。3. 环境准备与核心工具类封装3.1 创建项目与安装依赖首先创建一个新的.NET控制台应用.NET 6或更高版本均可。dotnet new console -n SM2Demo cd SM2Demo然后通过NuGet安装BouncyCastle库dotnet add package Portable.BouncyCastle --version 1.9.0我强烈建议锁定1.9.0版本因为不同版本间的API可能存在细微差异本文代码基于此版本测试通过。3.2 GmUtil工具类深度解析下面是我封装的核心工具类GmUtil.cs。我会逐部分解释其关键代码而不仅仅是贴出源码。using Org.BouncyCastle.Asn1; using Org.BouncyCastle.Asn1.GM; using Org.BouncyCastle.Crypto; using Org.BouncyCastle.Crypto.Digests; using Org.BouncyCastle.Crypto.Engines; // ... 其他必要的using using System.Collections.Generic; namespace CommonUtils { public class GmUtil { // 1. 初始化椭圆曲线参数 private static X9ECParameters x9ECParameters GMNamedCurves.GetByName(sm2p256v1); private static ECDomainParameters ecDomainParameters new ECDomainParameters(x9ECParameters.Curve, x9ECParameters.G, x9ECParameters.N); // 2. 核心C1C2C3 与 C1C3C2 的转换 private static byte[] ChangeC1C2C3ToC1C3C2(byte[] c1c2c3) { // C1长度固定对于sm2p256v1非压缩点表示为0x04 X坐标(32字节) Y坐标(32字节) 65字节 int c1Len (x9ECParameters.Curve.FieldSize 7) / 8 * 2 1; // 计算结果为65 const int c3Len 32; // SM3杂凑输出固定为32字节 byte[] result new byte[c1c2c3.Length]; // 复制C1部分 Buffer.BlockCopy(c1c2c3, 0, result, 0, c1Len); // 复制C3部分原在末尾 Buffer.BlockCopy(c1c2c3, c1c2c3.Length - c3Len, result, c1Len, c3Len); // 复制C2部分中间部分 Buffer.BlockCopy(c1c2c3, c1Len, result, c1Len c3Len, c1c2c3.Length - c1Len - c3Len); return result; } // 3. 新标准加密输出C1C3C2 public static byte[] Sm2Encrypt(byte[] data, AsymmetricKeyParameter key) { return ChangeC1C2C3ToC1C3C2(Sm2EncryptOld(data, key)); } // 4. 新标准解密输入C1C3C2 public static byte[] Sm2Decrypt(byte[] data, AsymmetricKeyParameter key) { return Sm2DecryptOld(ChangeC1C3C2ToC1C2C3(data), key); } // 5. 旧标准加密BouncyCastle原生输出C1C2C3 public static byte[] Sm2EncryptOld(byte[] data, AsymmetricKeyParameter pubkey) { try { SM2Engine sm2Engine new SM2Engine(); sm2Engine.Init(true, new ParametersWithRandom(pubkey, new SecureRandom())); return sm2Engine.ProcessBlock(data, 0, data.Length); } catch (Exception e) { // 实际项目中应使用日志记录 Console.Error.WriteLine($Sm2EncryptOld error: {e.Message}); return null; } } // ... 其他方法如Sm3, SignSm3WithSm2, GenerateKeyPair等 } }关键代码解读与注意事项曲线参数GMNamedCurves.GetByName(sm2p256v1)是获取国密标准曲线的正确方式。不要尝试使用其他曲线否则无法与其他国密实现互通。转换函数ChangeC1C2C3ToC1C3C2和其逆函数是互通的关键。其逻辑就是字节数组的重组。理解c1Len的计算方式很重要FieldSize是曲线的位宽256除以8得到字节数32乘以2X和Y坐标再加1开头的0x04标识符总共65字节。加密模式注意Sm2Encrypt方法内部调用了Sm2EncryptOld然后进行顺序转换。这意味着我们工具类默认提供的是**新标准C1C3C2**的加密方法。如果你需要旧标准直接调用Sm2EncryptOld。错误处理生产环境中绝不能像示例中简单返回null或打印到控制台。应该将异常记录到日志系统并根据业务逻辑向上抛出特定的业务异常。4. 实战演练从密钥生成到加解密与签名4.1 生成SM2密钥对SM2密钥对包含一个私钥一个大整数d和一个公钥椭圆曲线上的一个点Q [d]G。// 使用GmUtil生成密钥对 AsymmetricCipherKeyPair keyPair GmUtil.GenerateKeyPair(); ECPrivateKeyParameters privateKeyParam (ECPrivateKeyParameters)keyPair.Private; ECPublicKeyParameters publicKeyParam (ECPublicKeyParameters)keyPair.Public; // 获取私钥d32字节的十六进制字符串 string privateKeyHex privateKeyParam.D.ToString(16).PadLeft(64, 0); // 补零至64字符 Console.WriteLine($私钥 d: {privateKeyHex}); // 获取公钥点Q的X和Y坐标 string pubKeyXHex publicKeyParam.Q.XCoord.ToBigInteger().ToString(16).PadLeft(64, 0); string pubKeyYHex publicKeyParam.Q.YCoord.ToBigInteger().ToString(16).PadLeft(64, 0); Console.WriteLine($公钥 X: {pubKeyXHex}); Console.WriteLine($公钥 Y: {pubKeyYHex}); // 完整公钥通常表示法04 X Y string fullPublicKeyHex 04 pubKeyXHex pubKeyYHex; Console.WriteLine($完整公钥(130位): {fullPublicKeyHex});实操心得私钥d是一个在[1, n-1]范围内的大随机数务必妥善保管。公钥是曲线上的一个点通常有两种表示格式压缩格式一个坐标加一个标识位和非压缩格式0x04前缀后跟X和Y坐标。国密SM2普遍采用非压缩格式这也是我们见到04开头公钥的原因。4.2 公钥加密与私钥解密假设我们已经有了公钥和私钥的十六进制字符串。void TestEncryptDecrypt() { // 示例密钥与上文Java示例一致确保互通 string privateKeyHex FAB8BBE670FAE338C9E9382B9FB6485225C11A3ECB84C938F10F20A93B6215F0; string fullPublicKeyHex 049EF573019D9A03B16B0BE44FC8A5B4E8E098F56034C97B312282DD0B4810AFC3CC759673ED0FC9B9DC7E6FA38F0E2B121E02654BF37EA6B63FAF2A0D6013EADF; // 1. 从完整公钥字符串构造公钥对象 // 注意BouncyCastle的GetPublickeyFromXY方法需要的是去掉04的X和Y。 if (fullPublicKeyHex.StartsWith(04) fullPublicKeyHex.Length 130) { string pubKeyHex fullPublicKeyHex.Substring(2); // 去掉04剩128位 string xHex pubKeyHex.Substring(0, 64); string yHex pubKeyHex.Substring(64, 64); BigInteger x new BigInteger(xHex, 16); BigInteger y new BigInteger(yHex, 16); AsymmetricKeyParameter publicKey GmUtil.GetPublickeyFromXY(x, y); // 2. 从私钥字符串构造私钥对象 BigInteger d new BigInteger(privateKeyHex, 16); AsymmetricKeyParameter privateKey GmUtil.GetPrivatekeyFromD(d); // 3. 加密 string plainText 这是一段需要加密的敏感数据比如身份证号或交易金额。; byte[] plainData Encoding.UTF8.GetBytes(plainText); // 使用新标准C1C3C2加密 byte[] cipherData GmUtil.Sm2Encrypt(plainData, publicKey); string cipherTextHex Hex.ToHexString(cipherData); Console.WriteLine($加密结果(Hex, C1C3C2): {cipherTextHex}); // 4. 解密 byte[] decryptedData GmUtil.Sm2Decrypt(cipherData, privateKey); string decryptedText Encoding.UTF8.GetString(decryptedData); Console.WriteLine($解密结果: {decryptedText}); Console.WriteLine($解密是否成功: {plainText decryptedText}); } }4.3 SM3withSM2数字签名与验签签名用于验证数据的完整性和来源。SM2的签名算法通常与SM3杂凑算法结合使用。void TestSignVerify() { // 使用之前生成的密钥对 AsymmetricCipherKeyPair keyPair GmUtil.GenerateKeyPair(); byte[] userId Encoding.UTF8.GetBytes(ALICE123YAHOO.COM); // 用户ID国密标准要求 byte[] message Encoding.UTF8.GetBytes(重要的合同文件内容); // 1. 签名 byte[] signature GmUtil.SignSm3WithSm2(message, userId, keyPair.Private); Console.WriteLine($签名结果(Hex, R||S): {Hex.ToHexString(signature)}); // 2. 验签 (使用相同的userId) bool isValid GmUtil.VerifySm3WithSm2(message, userId, signature, keyPair.Public); Console.WriteLine($验签结果: {isValid}); // 3. 篡改消息后验签 message[0] ^ 0xFF; // 修改第一个字节 bool isTampered GmUtil.VerifySm3WithSm2(message, userId, signature, keyPair.Public); Console.WriteLine($消息篡改后验签结果: {isTampered} (应为False)); }注意事项用户IDUserId这是SM2签名算法的一个特色参数用于绑定签名者和特定身份。双方必须使用完全相同的UserId字节数组否则验签会失败。通常使用签名者的可识别标识如邮箱、身份证号等。签名格式SignSm3WithSm2方法返回的是R和S直接拼接的64字节数组。BouncyCastle内部生成的是ASN.1 DER格式该方法内部已经做了转换。如果你需要原始的ASN.1格式可以使用SignSm3WithSm2Asn1Rs方法。5. .NET与Java互通实战填平那些“坑”跨语言互通是SM2应用中最常见的需求也是问题高发区。下面我们针对最常见的两个问题给出解决方案。5.1 问题一“C1C2C3” vs “C1C3C2”顺序不一致症状.NET加密的数据Java解不开或者反之。控制台可能不会直接报错但解密出来是乱码。解决方案统一标准。在项目初期双方团队就必须约定好使用哪一种顺序。我们的GmUtil类同时提供了两种GmUtil.Sm2Encrypt/Decrypt对应新标准C1C3C2。GmUtil.Sm2EncryptOld/DecryptOld对应旧标准C1C2C3。与JavaHutool互通示例 假设Java端使用Hutool并设置了sm2.setMode(SM2Engine.Mode.C1C3C2)。那么.NET端也应使用GmUtil.Sm2EncryptC1C3C2。// .NET 端加密 (C1C3C2) byte[] cipherData GmUtil.Sm2Encrypt(plainData, publicKey); string cipherHexForJava Hex.ToHexString(cipherData); // 直接发送这个Hex给Java // .NET 端解密Java传来的密文 (假设Java也是C1C3C2) string cipherHexFromJava 04a7aaa9fd91aea6f99787ef431e19cb9feecc5bfb97fb445ce529c78c04676f1792e06b3a2814d1bda80bd3f63e530c149fc03911f1b81007dc86cef2c03f30c7fecc8b256272f881a8f2f4e71351c45d5bb27e8531f1e2ea6d55150c88f5026b8783ccef867a510a313178cfd26177; byte[] cipherDataFromJava Hex.Decode(cipherHexFromJava); byte[] decryptedData GmUtil.Sm2Decrypt(cipherDataFromJava, privateKey);5.2 问题二“Invalid point encoding”错误症状Java端报错Invalid point encoding或者.NET解密时出现类似错误。根因BouncyCastle库在表示椭圆曲线非压缩点时会在点数据前加上0x04作为标识符。但有些实现尤其是某些Java库或硬件在生成或解析密文C1部分时可能已经包含了04也可能不包含。当期望不匹配时就会报错。解决方案在互通时对密文Hex字符串进行预处理。场景A.NET加密Java解密报错Java解密时报Invalid point encoding很可能是因为Java库期望的C1部分不包含04前缀而.NET BouncyCastle默认生成的密文包含了。.NET端处理在将密文发送给Java前检查Hex字符串开头是否为04如果是则去掉。byte[] cipherData GmUtil.Sm2Encrypt(plainData, publicKey); string cipherHex Hex.ToHexString(cipherData); // 关键处理如果Java端不需要04前缀则去掉 if (cipherHex.StartsWith(04)) { cipherHex cipherHex.Substring(2); } // 现在 cipherHex 可以发送给Java了场景BJava加密.NET解密报错.NET的GmUtil.Sm2Decrypt方法内部期望的密文输入是完整的、包含04如果是非压缩格式的C1C3C2字节数组。如果Java传来的密文没有04需要补上。string cipherHexFromJava a7aaa9fd91aea6f99787ef431e19cb9feecc5bfb97fb445ce529c78c04676f1792e06b3a2814d1bda80bd3f63e530c149fc03911f1b81007dc86cef2c03f30c7fecc8b256272f881a8f2f4e71351c45d5bb27e8531f1e2ea6d55150c88f5026b8783ccef867a510a313178cfd26177; // 假设没有04 // 关键处理如果密文没有04前缀则加上 if (!cipherHexFromJava.StartsWith(04)) { cipherHexFromJava 04 cipherHexFromJava; } byte[] cipherData Hex.Decode(cipherHexFromJava); byte[] decryptedData GmUtil.Sm2Decrypt(cipherData, privateKey);核心排查步骤确定顺序首先和对方确认使用的是C1C2C3还是C1C3C2。确定04前缀让对方提供一段他们自己加密自己解密成功的密文样例Hex格式。观察该密文Hex字符串是否以04开头。适配处理根据对方样例决定在加密后是否去除04或在解密前是否补上04。最好将这部分处理逻辑封装成工具方法。5.3 Base64编码与Hex编码网络传输或存储时二进制密文通常编码为字符串。Hex编码Hex.ToHexString()/Hex.Decode()。直观但体积大一倍。Base64编码Convert.ToBase64String()/Convert.FromBase64String()。更紧凑。注意如果约定使用Base64且涉及04前缀的去除处理顺序应为// .NET 发送给 Java (Java不要04) byte[] cipherData GmUtil.Sm2Encrypt(plainData, publicKey); string cipherHex Hex.ToHexString(cipherData); if (cipherHex.StartsWith(04)) cipherHex cipherHex.Substring(2); string cipherBase64 Convert.ToBase64String(Hex.Decode(cipherHex)); // 先转Hex字节再Base64 // 发送 cipherBase64 // .NET 接收来自 Java 的Base64 (Java没给04) string cipherBase64FromJava ...; byte[] cipherBytesFromBase64 Convert.FromBase64String(cipherBase64FromJava); string cipherHexFromJava Hex.ToHexString(cipherBytesFromBase64); if (!cipherHexFromJava.StartsWith(04)) cipherHexFromJava 04 cipherHexFromJava; byte[] cipherData Hex.Decode(cipherHexFromJava); byte[] decryptedData GmUtil.Sm2Decrypt(cipherData, privateKey);6. 生产环境进阶证书、性能与最佳实践6.1 使用SM2证书在实际项目中更常见的不是直接使用裸的公私钥而是使用包含公钥和身份信息的X.509证书。// 从PFX/P12证书文件读取SM2密钥对假设证书是SM2算法 public static AsymmetricKeyParameter GetPrivateKeyFromPfx(string pfxPath, string password) { using (var fs new FileStream(pfxPath, FileMode.Open, FileAccess.Read)) { Pkcs12Store store new Pkcs12Store(fs, password.ToCharArray()); foreach (string alias in store.Aliases) { if (store.IsKeyEntry(alias)) { AsymmetricKeyEntry keyEntry store.GetKey(alias); return keyEntry.Key; } } } return null; } // 从CER证书文件读取SM2公钥 public static AsymmetricKeyParameter GetPublicKeyFromCer(string cerPath) { using (var fs new FileStream(cerPath, FileMode.Open, FileAccess.Read)) { X509CertificateParser parser new X509CertificateParser(); X509Certificate certificate parser.ReadCertificate(fs); return certificate.GetPublicKey(); } }GmUtil类中也提供了ReadSm2X509Cert方法可以解析特定的国密证书格式。6.2 性能考量与异步处理SM2的非对称加解密比对称算法如SM4/AES慢得多不适合加密大量数据。标准做法是随机生成一个对称密钥如SM4密钥。使用SM4对称加密算法加密原始数据。使用SM2公钥加密上一步生成的SM4密钥。将SM2加密后的密钥和SM4加密后的数据一起发送。解密时过程相反。我们的GmUtil也提供了Sm4EncryptECB/CBC等方法供你使用。对于高并发场景可以考虑将耗时的SM2操作如解密会话密钥放入线程池或使用异步方法避免阻塞主线程。6.3 错误处理与日志记录生产代码绝不能像示例一样简单输出到控制台或返回null。public class Sm2OperationResultT { public bool Success { get; set; } public string ErrorMessage { get; set; } public T Data { get; set; } } public Sm2OperationResultbyte[] SafeSm2Encrypt(byte[] data, AsymmetricKeyParameter publicKey) { var result new Sm2OperationResultbyte[](); try { result.Data GmUtil.Sm2Encrypt(data, publicKey); result.Success result.Data ! null; if (!result.Success) { result.ErrorMessage Encryption returned null.; _logger.LogWarning(SM2加密返回空结果数据长度{DataLength}, data?.Length); } } catch (Exception ex) { result.Success false; result.ErrorMessage $SM2加密失败: {ex.Message}; _logger.LogError(ex, SM2加密过程中发生异常。); } return result; }6.4 常见问题排查清单当你遇到SM2加解密失败时可以按照以下清单逐一排查问题现象可能原因排查步骤解密失败无具体错误1. 密文顺序不对 (C1C2C3/C1C3C2)2. 密钥不匹配1. 确认双方使用的顺序标准。2. 用对方公钥加密一个短字符串让对方用其私钥解密验证密钥对。Invalid point encoding密文C1部分的04前缀问题1. 检查对方提供的样例密文Hex是否以04开头。2. 根据对方要求在加密后去除或解密前补上04。签名验证失败1. UserId不一致2. 签名格式不一致 (ASN.1 vs R\S)3. 消息被篡改加密速度非常慢加密数据量过大SM2仅用于加密小块数据如密钥。大数据应使用SM4/AES加密再用SM2加密SM4密钥。与特定硬件加密机不通硬件可能有特殊规范1. 获取硬件厂商的SDK或示例代码。2. 重点关注密钥格式、调用顺序、是否需要调用特定初始化方法。7. 完整源码集成与测试案例最后我将提供一个完整的控制台应用示例将上述所有功能串联起来并进行单元测试。创建一个新的Program.cs文件using CommonUtils; // 引用我们的工具类 using Org.BouncyCastle.Utilities.Encoders; using System.Text; class Program { static void Main(string[] args) { Console.WriteLine( SM2国密算法C#完整测试 \n); // 测试1: 密钥生成与展示 TestKeyGeneration(); // 测试2: 加密解密自验 TestEncryptDecryptSelf(); // 测试3: 模拟与Java互通处理04前缀 TestInteropWithJava(); // 测试4: 签名验签 TestSignature(); // 测试5: 使用固定密钥对测试便于对比 TestWithFixedKeyPair(); Console.WriteLine(\n 所有测试完成 ); } static void TestKeyGeneration() { Console.WriteLine([测试1] 生成SM2密钥对); var kp GmUtil.GenerateKeyPair(); var pri (ECPrivateKeyParameters)kp.Private; var pub (ECPublicKeyParameters)kp.Public; Console.WriteLine($私钥 d (Hex): {pri.D.ToString(16).PadLeft(64, 0)}); Console.WriteLine($公钥 X (Hex): {pub.Q.XCoord.ToBigInteger().ToString(16).PadLeft(64, 0)}); Console.WriteLine($公钥 Y (Hex): {pub.Q.YCoord.ToBigInteger().ToString(16).PadLeft(64, 0)}); Console.WriteLine($完整公钥: 04{pub.Q.XCoord.ToBigInteger().ToString(16).PadLeft(64, 0)}{pub.Q.YCoord.ToBigInteger().ToString(16).PadLeft(64, 0)}); Console.WriteLine(---\n); } static void TestEncryptDecryptSelf() { Console.WriteLine([测试2] 加密解密自验 (C1C3C2标准)); var kp GmUtil.GenerateKeyPair(); string originalText Hello, SM2! 测试数据。; byte[] encrypted GmUtil.Sm2Encrypt(Encoding.UTF8.GetBytes(originalText), kp.Public); byte[] decrypted GmUtil.Sm2Decrypt(encrypted, kp.Private); string decryptedText Encoding.UTF8.GetString(decrypted); Console.WriteLine($原文: {originalText}); Console.WriteLine($加密后Hex长度: {Hex.ToHexString(encrypted).Length}); Console.WriteLine($解密后: {decryptedText}); Console.WriteLine($自验成功: {originalText decryptedText}); Console.WriteLine(---\n); } static void TestInteropWithJava() { Console.WriteLine([测试3] 模拟与Java Hutool互通); // 使用与网络资料中一致的密钥确保可复现 string privateKeyHex FAB8BBE670FAE338C9E9382B9FB6485225C11A3ECB84C938F10F20A93B6215F0; string fullPubKeyHex 049EF573019D9A03B16B0BE44FC8A5B4E8E098F56034C97B312282DD0B4810AFC3CC759673ED0FC9B9DC7E6FA38F0E2B121E02654BF37EA6B63FAF2A0D6013EADF; // 构造密钥对象 string pubKeyHex fullPubKeyHex.Substring(2); // 去掉04 string xHex pubKeyHex.Substring(0, 64); string yHex pubKeyHex.Substring(64); var publicKey GmUtil.GetPublickeyFromXY(new BigInteger(xHex, 16), new BigInteger(yHex, 16)); var privateKey GmUtil.GetPrivatekeyFromD(new BigInteger(privateKeyHex, 16)); string textToEncrypt 1234泰酷拉NET; Console.WriteLine($待加密文本: {textToEncrypt}); // .NET 加密 (C1C3C2) byte[] encryptedData GmUtil.Sm2Encrypt(Encoding.UTF8.GetBytes(textToEncrypt), publicKey); string encryptedHex Hex.ToHexString(encryptedData); Console.WriteLine($.NET加密结果 (含04): {encryptedHex}); Console.WriteLine($是否以04开头: {encryptedHex.StartsWith(04)}); // 模拟发送给Java去除04前缀 string encryptedHexForJava encryptedHex; if (encryptedHexForJava.StartsWith(04)) { encryptedHexForJava encryptedHexForJava.Substring(2); } Console.WriteLine($发送给Java的密文 (无04): {encryptedHexForJava}); // 模拟从Java接收密文 (假设Java发来的也没有04)并解密 string receivedFromJava encryptedHexForJava; // 这里用同一个模拟 if (!receivedFromJava.StartsWith(04)) { receivedFromJava 04 receivedFromJava; } byte[] dataToDecrypt Hex.Decode(receivedFromJava); byte[] decryptedData GmUtil.Sm2Decrypt(dataToDecrypt, privateKey); string decryptedText Encoding.UTF8.GetString(decryptedData); Console.WriteLine($解密Java风格密文结果: {decryptedText}); Console.WriteLine($互通解密成功: {textToEncrypt decryptedText}); Console.WriteLine(---\n); } static void TestSignature() { Console.WriteLine([测试4] SM3withSM2 签名验签); var kp GmUtil.GenerateKeyPair(); byte[] userId Encoding.UTF8.GetBytes(TEST_USER_ID_123); byte[] message Encoding.UTF8.GetBytes(这是一条需要签名的消息。); byte[] signature GmUtil.SignSm3WithSm2(message, userId, kp.Private); Console.WriteLine($签名结果 (R||S, 64字节Hex): {Hex.ToHexString(signature)}); bool verifyPass GmUtil.VerifySm3WithSm2(message, userId, signature, kp.Public); Console.WriteLine($验签结果 (正常): {verifyPass}); // 测试篡改 message[0] ^ 0x01; bool verifyFail GmUtil.VerifySm3WithSm2(message, userId, signature, kp.Public); Console.WriteLine($验签结果 (消息篡改后): {verifyFail} (应为False)); Console.WriteLine(---\n); } static void TestWithFixedKeyPair() { Console.WriteLine([测试5] 使用固定密钥对进行完整流程测试); // 生成一对固定密钥方便读者直接复制测试 BigInteger d new BigInteger(3945208F7B2144B13F36E38AC6D39F95889393692860B51A42FB81EF4DF7C5B8, 16); BigInteger x new BigInteger(09F9DF311E5421A150DD7D161E4BC5C672179FAD1833FC076BB08FF356F35020, 16); BigInteger y new BigInteger(CCEA490CE26775A52DC6EA718CC1AA600AED05FBF35E084A6632F6072DA9AD13, 16); var privateKey GmUtil.GetPrivatekeyFromD(d); var publicKey GmUtil.GetPublickeyFromXY(x, y); string testMsg 固定密钥测试; byte[] cipher GmUtil.Sm2Encrypt(Encoding.UTF8.GetBytes(testMsg), publicKey); byte[] plain GmUtil.Sm2Decrypt(cipher, privateKey); Console.WriteLine($私钥 d: {d.ToString(16)}); Console.WriteLine($公钥 X: {x.ToString(16)}); Console.WriteLine($公钥 Y: {y.ToString(16)}); Console.WriteLine($测试消息: {testMsg}); Console.WriteLine($加密后解密成功: {testMsg Encoding.UTF8.GetString(plain)}); Console.WriteLine(读者可使用上述密钥对自行验证。); } }运行这个程序你将看到一个完整的SM2功能演示涵盖了生成、加密、解密、签名、验签以及最重要的互通性处理。你可以将GmUtil.cs类和这个Program.cs复制到你的项目中快速搭建起SM2的加解密能力。国密算法的推广是必然趋势提前掌握SM2在C#中的实战应用无疑能为你的项目增加重要的合规筹码和技术保障。希望这篇近万字的详细指南能帮你扫清障碍顺利落地。如果在实际操作中遇到新的问题不妨回头仔细检查密钥格式、数据顺序和编码处理大部分问题都逃不出这几个范畴。