SM2国密算法实战:从加密解密到签名验签的C#完整实现
国密算法作为信息安全领域的重要基础设施,正在金融、政务、物联网等行业快速普及。其中SM2作为非对称加密算法的代表,相比传统RSA在安全性和效率上都有显著优势。但对于大多数C#开发者来说,如何在实际项目中正确使用SM2仍然是个挑战——从加密解密的基本操作,到数字签名与验签的核心场景,再到各种格式兼容的"坑点",都需要系统的实战指导。
本文将带你用Visual Studio构建一个完整的控制台应用,不仅实现SM2的基础加密功能,更重点解决数字签名这一高频使用场景。我们会使用BouncyCastle这一成熟加密库,同时解释每个关键参数的技术含义,最后还会专门分析C1C2C3和C1C3C2格式差异这个"经典陷阱"。
1. 环境准备与基础配置
在开始编码前,我们需要准备好开发环境。创建一个新的.NET Core控制台应用(.NET 6或更高版本),然后通过NuGet添加必要的依赖包:
dotnet add package BouncyCastle.Cryptography --version 2.2.1 dotnet add package Portable.BouncyCastle --version 1.9.0这两个包提供了完整的SM2算法实现。接下来,我们定义一个静态类SM2Helper来封装所有操作:
using Org.BouncyCastle.Asn1.X9; using Org.BouncyCastle.Crypto; using Org.BouncyCastle.Crypto.Parameters; using Org.BouncyCastle.Math; using Org.BouncyCastle.Math.EC; using Org.BouncyCastle.Security; using System.Text; public static class SM2Helper { // 国密标准SM2椭圆曲线参数 private static readonly X9ECParameters sm2ECParameters = ECNamedCurveTable.GetByName("sm2p256v1"); private static readonly ECDomainParameters domainParameters = new ECDomainParameters( sm2ECParameters.Curve, sm2ECParameters.G, sm2ECParameters.N, sm2ECParameters.H); // 其他方法将在这里实现... }注意:
sm2p256v1是国密标准定义的椭圆曲线名称,包含了所有必要的参数,包括素数域、曲线方程系数和基点等。
2. 密钥对生成与管理
SM2作为非对称加密算法,密钥对生成是第一步。我们需要同时支持生成新密钥对和加载已有密钥:
public static (ECPrivateKeyParameters privateKey, ECPublicKeyParameters publicKey) GenerateKeyPair() { var generator = GeneratorUtilities.GetKeyPairGenerator("EC"); generator.Init(new ECKeyGenerationParameters(domainParameters, new SecureRandom())); AsymmetricCipherKeyPair keyPair = generator.GenerateKeyPair(); return ( (ECPrivateKeyParameters)keyPair.Private, (ECPublicKeyParameters)keyPair.Public ); } public static string PublicKeyToString(ECPublicKeyParameters publicKey) { byte[] encoded = publicKey.Q.GetEncoded(false); // false表示不压缩 return BitConverter.ToString(encoded).Replace("-", ""); } public static ECPublicKeyParameters PublicKeyFromString(string publicKeyHex) { byte[] bytes = HexToBytes(publicKeyHex); ECPoint point = domainParameters.Curve.DecodePoint(bytes); return new ECPublicKeyParameters(point, domainParameters); }密钥生成后,我们可以这样使用:
var (privateKey, publicKey) = SM2Helper.GenerateKeyPair(); string pubKeyHex = SM2Helper.PublicKeyToString(publicKey); Console.WriteLine($"生成的公钥:{pubKeyHex}"); // 保存和加载示例 ECPublicKeyParameters loadedPubKey = SM2Helper.PublicKeyFromString(pubKeyHex);3. 加密与解密实现
SM2的加密过程比RSA复杂,因为它涉及椭圆曲线点的运算。以下是完整的加密解密实现:
public static byte[] Encrypt(ECPublicKeyParameters publicKey, byte[] plainData) { var cipher = CipherUtilities.GetCipher("SM2"); cipher.Init(true, new ParametersWithRandom(publicKey, new SecureRandom())); return cipher.DoFinal(plainData); } public static byte[] Decrypt(ECPrivateKeyParameters privateKey, byte[] cipherData) { var cipher = CipherUtilities.GetCipher("SM2"); cipher.Init(false, privateKey); return cipher.DoFinal(cipherData); }实际使用时,我们通常会处理字符串而非原始字节数组,所以可以添加便捷方法:
public static string EncryptString(ECPublicKeyParameters publicKey, string plainText) { byte[] data = Encoding.UTF8.GetBytes(plainText); byte[] encrypted = Encrypt(publicKey, data); return BitConverter.ToString(encrypted).Replace("-", ""); } public static string DecryptString(ECPrivateKeyParameters privateKey, string cipherText) { byte[] data = HexToBytes(cipherText); byte[] decrypted = Decrypt(privateKey, data); return Encoding.UTF8.GetString(decrypted); }测试加密解密流程:
string original = "这是一条需要加密的敏感信息"; Console.WriteLine($"原始文本:{original}"); string encrypted = SM2Helper.EncryptString(publicKey, original); Console.WriteLine($"加密结果:{encrypted}"); string decrypted = SM2Helper.DecryptString(privateKey, encrypted); Console.WriteLine($"解密结果:{decrypted}");4. 数字签名与验签实战
数字签名是SM2最常用的场景之一,用于验证消息的真实性和完整性。以下是完整的签名验签实现:
public static byte[] Sign(ECPrivateKeyParameters privateKey, byte[] data, byte[] userId = null) { var signer = SignerUtilities.GetSigner("SM3withSM2"); signer.Init(true, new ParametersWithID(privateKey, userId ?? Encoding.UTF8.GetBytes("1234567812345678"))); signer.BlockUpdate(data, 0, data.Length); return signer.GenerateSignature(); } public static bool Verify(ECPublicKeyParameters publicKey, byte[] data, byte[] signature, byte[] userId = null) { var signer = SignerUtilities.GetSigner("SM3withSM2"); signer.Init(false, new ParametersWithID(publicKey, userId ?? Encoding.UTF8.GetBytes("1234567812345678"))); signer.BlockUpdate(data, 0, data.Length); return signer.VerifySignature(signature); }重要提示:SM2签名需要用户ID参数,通常使用默认值"1234567812345678",但在实际项目中应根据业务需求设置特定值。
签名验签的字符串版本:
public static string SignString(ECPrivateKeyParameters privateKey, string message, string userId = "1234567812345678") { byte[] data = Encoding.UTF8.GetBytes(message); byte[] userIdBytes = Encoding.UTF8.GetBytes(userId); byte[] signature = Sign(privateKey, data, userIdBytes); return BitConverter.ToString(signature).Replace("-", ""); } public static bool VerifyString(ECPublicKeyParameters publicKey, string message, string signatureHex, string userId = "1234567812345678") { byte[] data = Encoding.UTF8.GetBytes(message); byte[] signature = HexToBytes(signatureHex); byte[] userIdBytes = Encoding.UTF8.GetBytes(userId); return Verify(publicKey, data, signature, userIdBytes); }实际应用示例——模拟用户登录令牌的签名与验证:
// 模拟生成登录令牌 string userId = "user123"; DateTime expireTime = DateTime.Now.AddHours(2); string tokenData = $"{userId}|{expireTime:yyyy-MM-dd HH:mm:ss}"; // 用私钥签名 string signature = SM2Helper.SignString(privateKey, tokenData, userId); Console.WriteLine($"令牌签名:{signature}"); // 验证签名(服务端操作) bool isValid = SM2Helper.VerifyString(publicKey, tokenData, signature, userId); Console.WriteLine($"签名验证结果:{isValid}"); // 尝试篡改数据后的验证 string tamperedData = tokenData.Replace("user123", "attacker"); bool isTamperedValid = SM2Helper.VerifyString(publicKey, tamperedData, signature, userId); Console.WriteLine($"篡改后验证结果:{isTamperedValid}");5. 关键问题解析与实战技巧
5.1 C1C2C3与C1C3C2格式问题
这是SM2实现中最常见的兼容性问题。不同厂商可能采用不同的密文结构:
- 旧标准:C1C2C3(65字节C1 + 变长C2 + 32字节C3)
- 新标准:C1C3C2(65字节C1 + 32字节C3 + 变长C2)
处理这个问题的实用方法:
public static byte[] ConvertCipherFormat(byte[] cipherData, bool fromC1C2C3ToC1C3C2) { if (cipherData.Length < 97) throw new ArgumentException("Invalid cipher data length"); byte[] c1 = new byte[65]; // 04 + 32字节x + 32字节y byte[] c3 = new byte[32]; // SM3哈希值 byte[] c2 = new byte[cipherData.Length - 97]; // 实际密文 if (fromC1C2C3ToC1C3C2) { Buffer.BlockCopy(cipherData, 0, c1, 0, 65); Buffer.BlockCopy(cipherData, 65, c2, 0, c2.Length); Buffer.BlockCopy(cipherData, 65 + c2.Length, c3, 0, 32); } else { Buffer.BlockCopy(cipherData, 0, c1, 0, 65); Buffer.BlockCopy(cipherData, 65, c3, 0, 32); Buffer.BlockCopy(cipherData, 97, c2, 0, c2.Length); } // 转换为目标格式 byte[] result = new byte[cipherData.Length]; Buffer.BlockCopy(c1, 0, result, 0, 65); if (fromC1C2C3ToC1C3C2) { Buffer.BlockCopy(c3, 0, result, 65, 32); Buffer.BlockCopy(c2, 0, result, 97, c2.Length); } else { Buffer.BlockCopy(c2, 0, result, 65, c2.Length); Buffer.BlockCopy(c3, 0, result, 65 + c2.Length, 32); } return result; }5.2 公钥前缀04的含义
在SM2公钥中,开头的04表示这是一个非压缩格式的公钥,后面跟着的是X和Y坐标各32字节。处理时:
public static ECPublicKeyParameters PublicKeyFromString(string publicKeyHex) { if (publicKeyHex.StartsWith("04") && publicKeyHex.Length > 2) { publicKeyHex = publicKeyHex.Substring(2); } byte[] bytes = HexToBytes(publicKeyHex); if (bytes.Length != 64) // 32字节X + 32字节Y { throw new ArgumentException("Invalid public key length"); } // 重建带04前缀的完整公钥 byte[] fullKey = new byte[65]; fullKey[0] = 0x04; Buffer.BlockCopy(bytes, 0, fullKey, 1, 64); ECPoint point = domainParameters.Curve.DecodePoint(fullKey); return new ECPublicKeyParameters(point, domainParameters); }5.3 性能优化建议
SM2虽然比RSA快,但在高并发场景下仍需优化:
- 重用密钥对象:避免在每次操作时都解析密钥
- 使用对象池:对于频繁的加密/解密操作,重用Cipher对象
- 异步处理:对于大量数据的处理,使用异步方法
// 对象池示例 public class SM2CipherPool { private readonly ConcurrentBag<ISigner> _signerPool = new(); private readonly ConcurrentBag<IBufferedCipher> _cipherPool = new(); public ISigner GetSigner() { if (_signerPool.TryTake(out var signer)) { return signer; } signer = SignerUtilities.GetSigner("SM3withSM2"); return signer; } public void ReturnSigner(ISigner signer) { signer.Reset(); _signerPool.Add(signer); } // 类似实现Cipher的池化方法... }6. 完整示例:安全通信系统
让我们把这些知识点整合到一个实际场景中——两个系统之间的安全通信:
// 系统A准备发送安全消息 var (privateKeyA, publicKeyA) = SM2Helper.GenerateKeyPair(); var (_, publicKeyB) = SM2Helper.GenerateKeyPair(); // 系统B的公钥 string originalMessage = "这是一条机密业务数据"; Console.WriteLine($"原始消息:{originalMessage}"); // 1. 用B的公钥加密消息 string encryptedMessage = SM2Helper.EncryptString(publicKeyB, originalMessage); Console.WriteLine($"加密后消息:{encryptedMessage}"); // 2. 用A的私钥签名 string signature = SM2Helper.SignString(privateKeyA, encryptedMessage); Console.WriteLine($"消息签名:{signature}"); // 系统B接收并处理消息 // 3. 验证签名 bool isSignatureValid = SM2Helper.VerifyString(publicKeyA, encryptedMessage, signature); Console.WriteLine($"签名验证结果:{isSignatureValid}"); if (isSignatureValid) { // 4. 用B的私钥解密 string decryptedMessage = SM2Helper.DecryptString(privateKeyB, encryptedMessage); Console.WriteLine($"解密后消息:{decryptedMessage}"); } else { Console.WriteLine("警告:消息签名验证失败,可能被篡改!"); }这个示例展示了典型的端到端加密通信流程,结合了SM2的加密和签名能力,确保数据的机密性、真实性和完整性。