news 2026/6/2 2:33:16

SM2国密算法在C#里到底怎么用?一个控制台程序带你搞定加密、解密和签名验签

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
SM2国密算法在C#里到底怎么用?一个控制台程序带你搞定加密、解密和签名验签

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快,但在高并发场景下仍需优化:

  1. 重用密钥对象:避免在每次操作时都解析密钥
  2. 使用对象池:对于频繁的加密/解密操作,重用Cipher对象
  3. 异步处理:对于大量数据的处理,使用异步方法
// 对象池示例 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的加密和签名能力,确保数据的机密性、真实性和完整性。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/5/29 9:03:52

系列开篇:为什么用户来了又走?认知缺口理论解密

系列开篇&#xff1a;为什么用户来了又走&#xff1f;认知缺口理论解密这个系列将用4篇文章&#xff0c;帮你系统掌握用户留存的心理学机制&#xff0c;让用户来了就不想走。一个让产品人失眠的问题 你花了三个月开发产品&#xff0c;上线第一周来了1000个用户。 你兴奋得睡不着…

作者头像 李华
网站建设 2026/5/29 9:03:48

3分钟上手XHS-Downloader:小白也能掌握的小红书无水印下载神器

3分钟上手XHS-Downloader&#xff1a;小白也能掌握的小红书无水印下载神器 【免费下载链接】XHS-Downloader 小红书&#xff08;XiaoHongShu、RedNote&#xff09;链接提取/作品采集工具&#xff1a;提取账号发布、收藏、点赞、专辑作品链接&#xff1b;提取搜索结果作品、用户…

作者头像 李华
网站建设 2026/5/29 9:03:46

微信数据恢复神器:3步解密找回丢失的聊天记录

微信数据恢复神器&#xff1a;3步解密找回丢失的聊天记录 【免费下载链接】WechatDecrypt 微信消息解密工具 项目地址: https://gitcode.com/gh_mirrors/we/WechatDecrypt 你是否曾因误删微信聊天记录而懊恼&#xff1f;是否在更换设备时发现重要对话无法迁移&#xff1…

作者头像 李华
网站建设 2026/5/29 9:02:02

Python-Nose Plugin(AI 增强)

接口文档: Plugin Interface — nose 1.3.7 documentation 实战,自定义plugin: 运行命令 nosetests -s --with-customplugin --name=test1 test.py from nose.plugins.base import Pluginclass DefinedPlugin(Plugin):"""custom class about plugin"…

作者头像 李华