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实现中最常见的兼容性问题。不同厂商可能采用不同的密文结构旧标准C1C2C365字节C1 变长C2 32字节C3新标准C1C3C265字节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 ConcurrentBagISigner _signerPool new(); private readonly ConcurrentBagIBufferedCipher _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的加密和签名能力确保数据的机密性、真实性和完整性。
SM2国密算法在C#里到底怎么用?一个控制台程序带你搞定加密、解密和签名验签
发布时间:2026/6/28 5:39:04
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实现中最常见的兼容性问题。不同厂商可能采用不同的密文结构旧标准C1C2C365字节C1 变长C2 32字节C3新标准C1C3C265字节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 ConcurrentBagISigner _signerPool new(); private readonly ConcurrentBagIBufferedCipher _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的加密和签名能力确保数据的机密性、真实性和完整性。