尧图精选

C#实现RSA加密:从密钥生成到加解密与签名验签的完整指南

🕒 发布时间:2026/10/2 4:05:16 📁 来源:尧图网络
1. 为什么突然想写RSA的C#实现做上位机开发这些年经常要跟设备通信、跟第三方系统对接。前阵子接了一个项目甲方要求所有数据在传输前必须做RSA加密理由很简单设备参数和采集数据都是敏感信息不能裸奔在网络上。我一开始觉得这事儿没什么难度毕竟.NET框架里加密相关的类一抓一大把。但真上手做的时候才发现坑比想象中多得多——密钥格式不兼容、跨平台解析乱码、填充方式选错导致解密失败、长文本越界异常这些问题网上搜出来的答案又大多是Java版的抄都抄不明白。所以这篇东西我打算把C#里实现RSA的完整链路讲透。不光是给你几段能跑的代码更重要的是把背后的原理、选型依据、踩坑记录都讲清楚。不管你是刚入行的新手还是像我一样每天跟设备打交道的上位机工程师这篇文章应该都能帮你省下不少试错的时间。1.1 这套东西能解决什么问题在开始之前先明确一下RSA在C#开发里到底能干什么用。最典型的三个场景第一网络通信中的数据加密比如上位机与服务器之间传输的JSON报文防止被中间人抓包后直接看明文第二身份认证场景下的签名和验签比如客户端向服务端证明这条消息确实是我发的没有被篡改过第三密钥交换用RSA加密一个对称密钥比如AES的Key后续通信改用AES兼顾安全和性能。说白了RSA解决的是在不安全的通道上安全地传递信息这个问题。它的核心思想是非对称一把公钥用于加密一把私钥用于解密公钥可以公开给别人私钥必须自己藏好。1.2 谁需要认真阅读这篇文章两类人最需要这篇内容。一类是做上位机、工控、物联网领域的C#开发者因为你要面对的往往是PLC、传感器、DCS这类设备它们跟上位机之间的通信如果走公网没有加密就是裸奔。另一类是刚接触C#加密体系的初级开发者被RSACryptoServiceProvider、RSA、ECDsa这些类搞得眼花缭乱不知道选哪个、怎么用。如果你属于这两类接下来这五千字值得你耐心读完。2. RSA核心原理非对称加密到底是怎么一回事在动手敲代码之前我强烈建议你先花十分钟把RSA的数学模型弄清楚。不是说要你会推公式但至少得知道它为什么能加密、为什么解密能还原、为什么安全。不然你写出来的代码只是能用遇到问题排查起来会非常痛苦。2.1 用锁和钥匙的类比理解非对称加密对称加密你可以理解为一把钥匙开一把锁加密和解密用同一个密钥。AES就是典型代表速度快、效率高但问题也明显——你怎么才能安全地把这把钥匙交给对方走网络怕被劫持走线下又麻烦。非对称加密的逻辑完全不同。你可以想象一个场景小明要寄一个上锁的箱子给小红小红提前把一把打开的锁发给小明小明收到后把箱子锁上寄过去小红用自己的钥匙打开。这里那把打开的锁就是公钥谁都可以拿小红手里的钥匙就是私钥只有小红有。别人即使拿到那把锁也打不开箱子因为锁只有对应的钥匙能开。RSA就是这么运作的。你生成一对密钥公钥和私钥公钥分发出去用于加密私钥留在本地用于解密。任何持有公钥的人都可以给你发密文但只有你能解开。反向操作也一样你用私钥签名别人用公钥验签从而确认信息来源和完整性。2.2 密钥生成背后的数学逻辑RSA的安全性建立在两个数学事实上大整数因数分解困难以及模幂运算可以从两个方向操作。密钥生成的流程大致是这样的先随机生成两个大素数p和q计算它们的乘积n p × qn就是密钥长度比如1024位、2048位。然后计算欧拉函数φ(n) (p-1) × (q-1)。接着选一个与φ(n)互质的整数e作为公钥指数常用65537再用扩展欧几里得算法算出d使得e × d ≡ 1 (mod φ(n))d就是私钥指数。加密过程是密文c m^e mod n其中m是明文对应的整数。解密过程是明文m c^d mod n。因为e和n是公开的攻击者想破解就必须先从n反推出p和q也就是做因数分解。当n足够大2048位时这个计算量在可接受时间范围内是天文数字所以RSA被广泛认为是安全的。这里有个容易误解的点RSA加密的明文在数学上是一个整数而且这个整数必须小于n。所以你不能直接拿RSA去加密一个长字符串必须先转换成字节数组再切分成符合长度要求的数据块逐块加密。这个我在后面实操部分会细讲。2.3 加密模式与填充方式的选择RSA本身有一种叫教科书式RSA的朴素实现就是上面说的直接算m^e mod n。但直接这么用是极其危险的因为存在选择密文攻击、低指数攻击等多种攻击手段。所以实际工程里必须引入填充方案。.NET里最常见的两种PKCS#1 v1.5填充老牌方案历史最悠久兼容性最好。在数据块前填充随机非零字节和固定标记。加密后长度等于密钥长度比如1024位密钥加密后密文固定128字节。OAEP填充Optimal Asymmetric Encryption Padding更安全的现代方案引入随机性和哈希函数能有效抵御多种攻击。微软推荐用这个C#里加解密时指定RSAEncryptionPadding.OaepSHA1或OaepSHA256。我个人的建议是只要不是跟老系统对接优先用OAEP哈希选SHA256。如果对方系统指定了PKCS#1那没办法为了兼容只能跟着用。2.4 密钥长度的现实考量1024位的RSA密钥在数学上已经被证明不够安全金融、政务这类高安全等级场景基本禁止使用。2048位是当前的主流底线兼顾安全性和性能。4096位更安全但加解密耗时明显增加在工控设备这种计算能力有限的场景里未必划算。我实测过一组数据在普通的i5处理器上2048位密钥做一次加密大约耗时1到3毫秒解密大约5到15毫秒取决于填充方式和数据大小。这个量级对于上位机跟服务器的低频通信完全够用但如果你的系统每秒要处理几百条报文RSA就不太合适了应该走RSA协商密钥 AES加密数据的混合方案。3. C#中的RSA实现API选型与工程决策3.1 .NET生态里到底有哪些RSA类C#里跟RSA相关的类主要分两类。一类是老牌的RSACryptoServiceProvider它在.NET Framework时代就在底层封装了Windows的CAPI在Windows上运行没问题但跨平台性能一般而且微软官方已经不太推荐新项目用了。另一类是RSA抽象基类及其跨平台实现RSAOpenSslLinux上用OpenSSL和RSACngWindows上用CNG在.NET Core 3.0和.NET 5里是默认推荐的选择。从实际开发角度讲你直接用RSA基类的静态方法就够了比如RSA.Create()会自动给你创建当前平台最合适的实现。除非你有非常特殊的定制需求否则没必要直接new一个RSACryptoServiceProvider。这里还要提一个很容易踩的坑RSACryptoServiceProvider在有些场景下跟RSA.Create()生成的实例行为不一致尤其是跟第三方系统做跨平台对接时密钥导出的格式可能会有细微差别。我的原则是新项目一律用RSA.Create()老项目如果用RSACryptoServiceProvider遇到奇怪问题优先考虑换掉。3.2 密钥格式XML、PEM还是DER这是C#开发者最容易踩坑的地方。Java、Python、Node.js这些生态里的RSA密钥大多是PEM格式也就是一串以BEGIN PUBLIC KEY或BEGIN RSA PRIVATE KEY开头的Base64文本。而.NET Framework时代最流行的是XML格式就是带 、 这些标签的XML字符串。如果你只在纯.NET环境内部使用XML格式确实方便ToXmlString(true)导出私钥ToXmlString(false)导出公钥加解密时FromXmlString导回去就行。但一旦要跟Java写的后端、Node.js写的服务、或者某些嵌入式设备对接你会发现对方根本不认识你的XML密钥你必须提供PEM格式。.NET 5引入了ImportFromPem方法可以直接读PEM字符串。但要注意PEM也有好几种子格式PKCS#1的私钥和PKCS#8的私钥长得不一样公钥又有SubjectPublicKeyInfo和RSAPublicKey两种编码。导出和导入时如果选错格式解析就会报错或者得到的密钥完全不对。我的做法是工具类里写一套双向转换方法把XML、PKCS#1 PEM、PKCS#8 PEM、SubjectPublicKeyInfo这几种格式全部支持谁对接需要什么就给什么。后面实操部分我会给出具体实现。3.3 用表格梳理几种核心API的适用场景API或类适用场景注意事项RSA.Create()新项目首选跨平台友好.NET Core 3.0以上可用RSACryptoServiceProvider老项目维护Windows平台兼容老代码跨平台性能一般不推荐新用RSA.Encrypt() / Decrypt()常规加解密操作必须指定填充方式RSA.SignData() / VerifyData()数据签名与验签哈希算法要选对SHA256等ImportFromPem()读取PEM格式密钥注意PKCS#1和PKCS#8的区别ExportPkcs8PrivateKey()导出PKCS#8私钥推荐.NET Core 3.0以上支持ExportSubjectPublicKeyInfo()导出标准公钥格式兼容性最好的公钥格式ToXmlString() / FromXmlString()纯.NET内部使用最方便跨语言对接不适用3.4 混合加密方案为什么不直接用RSA加密所有数据前面提到过RSA不适合加密大数据。原因很简单加密后密文长度等于密钥长度2048位密钥对应256字节密文加密前能加密的数据长度还受填充方式限制PKCS#1模式下最多117字节256-11OAEP-SHA256模式下最多190字节256-66。你一个JSON报文动辄几百字节甚至几KB就得切分成好多块分别加密效率低、管理复杂。所以工程上标准的做法是混合加密用RSA加密一个随机生成的AES密钥通常256位也就是32字节然后用AES加密实际业务数据。接收方先用RSA解密得到AES密钥再用AES密钥解密业务数据。这样既享受RSA的非对称安全性又不牺牲对称加密的性能。我做完那个上位机项目之后就给团队定了个规矩所有超过117字节的数据传输一律走混合方案RSA只负责保护密钥。4. 实操记录完整实现RSA加解密与签名验签下面进入正题我把整个实现过程拆成几个模块逐一展示。我会用.NET 6 / .NET 8的语法写示例代码你只需要有一个基本的控制台项目就能跑起来。Gitee上有很多现成的开源框架但自己动手写一遍理解的深度完全不一样。4.1 密钥生成与格式转换工具类首先写一个工具类负责密钥生成和格式转换。这个类是我在实际项目中沉淀下来的基本上覆盖了所有常见对接场景。using System.Security.Cryptography; using System.Text; public static class RsaKeyHelper { // 生成2048位密钥对返回XML格式 public static (string publicKey, string privateKey) GenerateXmlKeys() { using var rsa RSA.Create(2048); string publicKey rsa.ToXmlString(false); string privateKey rsa.ToXmlString(true); return (publicKey, privateKey); } // 生成密钥对并返回PEM格式公钥SubjectPublicKeyInfo私钥PKCS#8 public static (string publicKey, string privateKey) GeneratePemKeys() { using var rsa RSA.Create(2048); string publicKey Convert.ToBase64String(rsa.ExportSubjectPublicKeyInfo()); string privateKeyPkcs8 Convert.ToBase64String(rsa.ExportPkcs8PrivateKey()); return ( FormatPem(publicKey, PUBLIC KEY), FormatPem(privateKeyPkcs8, PRIVATE KEY) ); } // 格式化Base64字符串为标准PEM格式 private static string FormatPem(string base64, string keyType) { var sb new StringBuilder(); sb.AppendLine($-----BEGIN {keyType}-----); for (int i 0; i base64.Length; i 64) { int len Math.Min(64, base64.Length - i); sb.AppendLine(base64.Substring(i, len)); } sb.AppendLine($-----END {keyType}-----); return sb.ToString(); } // 从PEM字符串导入公钥 public static RSA ImportPublicKeyFromPem(string pem) { var rsa RSA.Create(); rsa.ImportFromPem(pem); return rsa; } // 从PEM字符串导入私钥 public static RSA ImportPrivateKeyFromPem(string pem) { var rsa RSA.Create(); rsa.ImportFromPem(pem); return rsa; } // XML格式转PEM格式双向 public static string XmlToPem(string xml, bool isPrivate) { using var rsa RSA.Create(); rsa.FromXmlString(xml); if (isPrivate) { var pkcs8 rsa.ExportPkcs8PrivateKey(); return PemFormat(Convert.ToBase64String(pkcs8), PRIVATE KEY); } else { var spki rsa.ExportSubjectPublicKeyInfo(); return PemFormat(Convert.ToBase64String(spki), PUBLIC KEY); } } private static string PemFormat(string base64, string keyType) { var sb new StringBuilder(); sb.AppendLine($-----BEGIN {keyType}-----); for (int i 0; i base64.Length; i 64) { int len Math.Min(64, base64.Length - i); sb.AppendLine(base64.Substring(i, len)); } sb.AppendLine($-----END {keyType}-----); return sb.ToString(); } }这里有个关键点要提醒一下ImportFromPem这个方法很智能它能自动识别PKCS#1和PKCS#8格式也会自动去掉-----BEGIN/END-----标记所以你不需要自己写解析逻辑。但如果你用的是.NET Framework没有这个方法那就只能自己解析PEM字符串了解析逻辑其实也不复杂就是去标记、转字节、再调用ImportParameters。4.2 核心加解密类支持分段与大文本处理这是整个项目里最核心的部分。我需要把RSA封装成一个易于调用的服务类让它能处理任意长度的文本。核心逻辑是如果输入数据长度超过当前密钥可加密的最大长度就自动分段处理。using System.Security.Cryptography; using System.Text; public class RsaService { private readonly RSA _rsa; public RsaService(string privateKey, bool isPem false) { _rsa RSA.Create(); if (isPem) { _rsa.ImportFromPem(privateKey); } else { _rsa.FromXmlString(privateKey); } } public RsaService(RSA rsa) { _rsa rsa; } // 加密文本自动分段 public string Encrypt(string plainText, RSAEncryptionPadding padding null) { padding ?? RSAEncryptionPadding.OaepSHA256; byte[] data Encoding.UTF8.GetBytes(plainText); int keySize _rsa.KeySize / 8; // 密钥字节数如2048位对应256字节 int maxBlockSize keySize - 2 * 32 - 2; // OAEP-SHA256填充的默认最大输入长度 using var ms new MemoryStream(); int offset 0; while (offset data.Length) { int blockSize Math.Min(maxBlockSize, data.Length - offset); byte[] block new byte[blockSize]; Array.Copy(data, offset, block, 0, blockSize); byte[] encryptedBlock _rsa.Encrypt(block, padding); ms.Write(encryptedBlock, 0, encryptedBlock.Length); offset blockSize; } return Convert.ToBase64String(ms.ToArray()); } // 解密文本自动处理分段密文 public string Decrypt(string cipherText, RSAEncryptionPadding padding null) { padding ?? RSAEncryptionPadding.OaepSHA256; byte[] data Convert.FromBase64String(cipherText); int keySize _rsa.KeySize / 8; using var ms new MemoryStream(); int offset 0; while (offset data.Length) { int blockSize Math.Min(keySize, data.Length - offset); byte[] block new byte[blockSize]; Array.Copy(data, offset, block, 0, blockSize); byte[] decryptedBlock _rsa.Decrypt(block, padding); ms.Write(decryptedBlock, 0, decryptedBlock.Length); offset blockSize; } return Encoding.UTF8.GetString(ms.ToArray()); } // 签名 public string SignData(string data, HashAlgorithmName? hashAlgorithm null) { hashAlgorithm ?? HashAlgorithmName.SHA256; byte[] content Encoding.UTF8.GetBytes(data); byte[] signature _rsa.SignData(content, hashAlgorithm.Value, RSASignaturePadding.Pkcs1); return Convert.ToBase64String(signature); } // 验签 public bool VerifyData(string data, string signature, HashAlgorithmName? hashAlgorithm null) { hashAlgorithm ?? HashAlgorithmName.SHA256; byte[] content Encoding.UTF8.GetBytes(data); byte[] sig Convert.FromBase64String(signature); return _rsa.VerifyData(content, sig, hashAlgorithm.Value, RSASignaturePadding.Pkcs1); } public void Dispose() { _rsa.Dispose(); } }这里面那个maxBlockSize的计算公式要注意一下。OAEP填充的额外开销是2 2 * hashSize当使用SHA25632字节时额外空间是66字节所以2048位密钥256字节最多能加密256 - 66 190字节明文。如果你用默认的OaepSHA120字节哈希则最多256 - 42 214字节。用PKCS#1的话固定是11字节开销最多256 - 11 245字节。我写代码时直接按最保守的OaepSHA256算省得不同环境跑出来的结果不一致。4.3 现场演示从生成密钥到加解密跑通写一个控制台入口把整个流程串起来这样你可以直接复制运行看到效果。class Program { static void Main() { Console.WriteLine( RSA 加解密演示 ); // 1. 生成密钥 var (publicKey, privateKey) RsaKeyHelper.GeneratePemKeys(); Console.WriteLine(公钥:); Console.WriteLine(publicKey); Console.WriteLine(私钥:); Console.WriteLine(privateKey); // 2. 用公钥加密私钥解密 var encryptor new RsaService(publicKey, isPem: true); var decryptor new RsaService(privateKey, isPem: true); string message 西门子PLC采集数据温度25.6℃压力1.02MPa流量12.8m³/h; Console.WriteLine($原始数据: {message}); string encrypted encryptor.Encrypt(message); Console.WriteLine($加密后(Base64): {encrypted}); string decrypted decryptor.Decrypt(encrypted); Console.WriteLine($解密后: {decrypted}); // 3. 用私钥签名公钥验签 string signature decryptor.SignData(message); Console.WriteLine($签名(Base64): {signature}); bool isValid encryptor.VerifyData(message, signature); Console.WriteLine($公钥验签结果: {isValid}); // 4. 验证篡改检测 string tampered message (被篡改); bool isTampered encryptor.VerifyData(tampered, signature); Console.WriteLine($篡改后验签结果: {isTampered}); Console.WriteLine( 演示结束 ); } }运行这段代码你会看到几个关键现象。第一同一个明文每次加密得到的密文都不一样这是正常的因为OAEP填充引入了随机性。第二解密出来的字符串跟原始字符串完全一致说明UTF8编码处理没有问题中文和特殊符号都能正确往返。第三篡改后的数据验签返回False签名机制确实能捕捉到数据变动。这个演示代码稍微调整一下就可以融入你现有的上位机框架。比如把密钥对写到配置文件里程序启动时加载运行时动态调用加解密方法。4.4 与Java后端对接时的跨语言兼容记录我那个项目里上位机是C#写的但数据要发到一个Java写的服务端。一开始我把C#生成的XML公钥发给对方对方直接懵了说看不懂这个格式。后来我改成导出PEM格式的SubjectPublicKeyInfo公钥对方用Java的X509EncodedKeySpec直接解析一次就通了。这里要注意C#里RSA.ImportFromPem能自动识别多种PEM变体但Java那边不行。Java的KeyFactory.getInstance(RSA)配合X509EncodedKeySpec只认SubjectPublicKeyInfo格式的公钥PKCS#8的私钥则对应PKCS8EncodedKeySpec。所以跨语言对接时公钥务必导出SubjectPublicKeyInfo私钥务必导出PKCS#8这是兼容性最好的一组搭配。另外还有个大坑Java的Cipher.getInstance(RSA/ECB/PKCS1Padding)对应的是C#里的RSAEncryptionPadding.Pkcs1而Java的RSA/ECB/OAEPWithSHA-256AndMGF1Padding对应C#里的RSAEncryptionPadding.OaepSHA256。两边填充方式不一致的话解密百分百报错。所以跨语言对接时密钥格式和填充方式必须同时约定好缺一不可。5. 常见问题与排查技巧实录这部分我整理了实际开发中最高频的几类问题每一条都对应真实的踩坑现场。如果你照着上面的代码跑还出现问题建议先对照这个清单逐条排查。5.1 指定的数据大于最大块大小异常这个报错出现得太频繁了。新手拿到RSA就去加密一大段JSON字符串结果直接抛出CryptographicException提示数据超过最大块大小。原因前面讲过RSA有输入长度上限。1024位密钥用PKCS#1填充最多加密117字节用OAEP-SHA256最多62字节2048位密钥用PKCS#1最多245字节用OAEP-SHA256最多190字节。解决思路就是分段加密。我在RsaService里已经实现了自动分段所以只要你用我提供的那个类就不会再踩这个坑。如果你自己写加解密记住一个口诀加密时按密钥字节数 - 填充开销切块解密时按密钥字节数切块。这里最容易搞混的是解密时每块密文长度等于密钥长度不要用什么复杂计算直接从Base64解码后的字节流里整块取就行。5.2 填充方式不匹配导致的解密失败还有一种非常诡异的情况加密用的OaepSHA256解密用的OaepSHA1结果解密时能解出一部分数据但在最后一块报错或者说解出来是乱码。原因就是加解密两侧的填充算法不一致。OaepSHA256和OaepSHA1的填充字节长度不同放在一起根本对不上。排查方法很简单检查加解密调用时传入的RSAEncryptionPadding是不是同一个对象。最稳妥的做法是写一个常量类把填充方式集中定义统一引用不散落在各个业务方法里。我见过有人把OaepSHA256写成OaepSHA1排查了一下午最后发现是大小写的问题真是浪费生命。5.3 密钥导入时参数无效或格式识别失败ImportFromPem看起来智能但它有一个限制它对带有多余内容或者格式不标准的PEM字符串非常敏感。比如从数据库里取出的密钥字符串带着换行符或者额外的空格导入时可能报错。另外如果你的PEM是拼接出来的、丢了-----BEGIN-----行那肯定也导不进去。所以我在工程上有一个习惯所有密钥在持久化存储和网络传输时统一去掉换行和标注只保存中间那串Base64内容需要用的时候再按标准格式重新拼装。这样能避免因微小的文本差异导致导入失败。RsaKeyHelper里的FormatPem方法就是干这个的你可以把原始Base64存到数据库读取的时候调用方法重新包装。5.4 密文传输过程中的编码问题RSA加密后得到的密文是字节数组你不可能直接把它塞进JSON或者写入数据库所以必须转换成可打印的文本格式。Base64是默认选择。但这里有个细节Base64编码有多种变体标准的Base64会包含、/、这些字符在某些URL场景下会出问题就得用Base64Url编码替代。我用过Base64Url也确实解决过URL参数传递时密文被截断的问题。不过我的建议是如果协议里可以自由选择尽量用标准Base64因为后端服务解析起来最方便只有在把密文放在URL查询参数或Header里传输时才需要转成Base64Url。C#里的Base64UrlEncoder类在Microsoft.AspNetCore.WebUtilities命名空间里直接用就行。问题现象可能原因解决方案加密时提示数据超过最大块大小明文长度超过RSA单次加密上限采用分段加密或改用混合加密方案解密时报参数错误填充方式不一致或密钥格式不对统一加解密填充方式检查密钥导入格式加解密正常但数据乱码字符编码不一致UTF8 vs ASCII统一使用Encoding.UTF8跨语言对接时验签失败PEM格式或签名算法不兼容公钥用SubjectPublicKeyInfo签名统一SHA256withRSA程序启动时密钥加载失败密钥文件被篡改或存了多余字符用标准PEM格式存储去掉无关内容加密性能不满足需求频繁加密大数据块改用AES加密数据 RSA加密AES密钥5.5 密钥管理不能写死在代码里最后这条是对所有做工程的人说的。RSA的安全性不只取决于算法本身更取决于私钥怎么保管。如果你把私钥硬编码在源代码里或者写在配置文件里明文放着那密钥泄漏只是时间问题。遇到过不止一次有人把私钥提交到Git仓库整个项目基本等于裸奔。我的做法是私钥单独存放在服务器环境的机密管理服务里比如Windows的DPAPI、Azure Key Vault或者简单点放环境变量、加密配置文件程序启动时从这些地方读取并加载到内存运行时不留明文到日志。公钥可以相对宽松地分发但也要防止被恶意替换最好做一次校验或数字签名。5.6 数字签名确认来源与验证完整性RSA不仅能加密还能做签名。签名用私钥生成验签用公钥。这里有一个容易混淆的点签名跟加密是两个方向。加密时公钥加密、私钥解密签名时私钥签署、公钥验证。不要把这两个流程搞混了否则你会写出用公钥验签却用私钥验签这种逻辑完全反了的代码。实际使用签名还有一个陷阱签名有两条常见标准PKCS#1 v1.5和PSS。C#里RSASignaturePadding.Pkcs1对应前者RSASignaturePadding.Pss对应后者。PSS更安全也是微软推荐的但跨语言对接时对方不一定支持。老系统基本都用PKCS#1新系统很多也在用我的经验是先跟对方确认别自己闷头选一个。6. 工作流整合RSA在上位机系统中的典型嵌入方式聊了这么多底层细节可能有人会问这些东西到底怎么放进我现有的上位机项目里下面给一个真实的整合路径参考。6.1 与PLC/设备通信时的密钥交换流程上位机跟西门子PLC或者DCS通信时网络环境通常比较封闭但一旦系统需要走公网或者跨区域传输数据安全就必须考虑了。我通常的做法是分两步建立连接时上位机生成临时密钥对把公钥发给服务器服务器用这个公钥加密自己生成的随机AES密钥回传给上位机上位机用私钥解密拿到AES密钥。之后所有业务数据都走AES加解密只有密钥协商那一刻用RSA。这样做的原因是后续业务数据可能是高频的、大数据量的直接拿RSA加解密性能和负载都不够看。而且密钥不落盘每次会话都是新的AES密钥即使本次会话的数据被截获也不会影响到其他会话。6.2 报文签名与验签防止数据被篡改工控系统里你不仅要防窃听还要防篡改。比如你给设备下发一组参数如果中间被人改了后果可能是设备异常甚至停机。这时就用私钥对整个报文做签名接收方用公钥验签验签通过才执行否则直接丢弃并报警。这样攻击者没有私钥就无法伪造出有效的签名。我在RsaService里已经实现了SignData和VerifyData方法你只需要在报文发送前调用签名在接收后调用验证即可。注意签名和加密要区分开传输层已经用AES加密过了应用层再签名是为了防篡改和防否认二者可以叠加使用。6.3 日志脱敏与逆向保护还有一个不那么显眼但很重要的场景日志脱敏。我在排查问题的时候经常要看上位机日志如果日志里直接打印了明文密码、密钥等信息那等于把安全机制废掉了。所以我在日志组件里做了一个统一的脱敏过滤器凡是匹配到敏感字段的键值对一律只显示前几位和后几位中间用星号代替。这个功能实现起来不难但经常被忽略。另外上位机程序本身容易被反编译所以如果有强保护需求可以考虑用混淆器加壳处理但这是另一个话题了。RSA密钥至少不应该明文存储在可执行文件同目录的配置里原理很简单攻击者只要能反编译你的程序就能从配置里找到密钥加密形同虚设。7. 性能基准与调优方向参考我在项目收尾时做了一轮基准测试给团队留了一份参考数据。这里也分享出来方便你对性能有一个直观认知。测试环境是i5-8250U处理器.NET 6RSA 2048位。操作类型数据大小耗时毫秒备注加密OAEP-SHA256100字节1.5 - 2.5单块操作加密OAEP-SHA2562KB分段8 - 12分段后仍有可观开销解密OAEP-SHA256100字节6 - 10私钥操作用时较长解密OAEP-SHA2562KB分段30 - 50分段数量增多签名SHA256 Pkcs1100字节5 - 8私钥签名验签SHA256 Pkcs1100字节0.5 - 1.0公钥操作远快于私钥2048位密钥生成-50 - 200一次性的从数据可以看出来RSA的性能跟AES完全不在一个量级AES加解密千字节级数据通常只需要微秒到几十微秒。所以一旦你的系统开始出现响应变慢第一反应应该是检查是不是在用RSA传输大报文。优化方向很明确能走混合方案就别裸用RSA必须用RSA时减少分段数量、精简业务数据。另外还有一个容易被忽视的性能坑每次创建RSA实例并导入密钥都有一定开销高并发场景下反复new RSA会导致明显的CPU开销。正确的做法是复用同一个RSA实例或者至少做一个简单的实例池。但要注意RSA不是线程安全的多线程环境要么加锁要么使用ThreadLocal要么干脆每次用的时候快速创建、用完释放根据你的并发量合理取舍。8. 建议先从一个小工具类开始如果你刚接触C#的RSA我的建议是先别急着往大项目里塞。找一个周末把文章里的RsaKeyHelper和RsaService复制到你的一个测试项目里先跑通密钥生成、加解密、签名验签这条主线。再多写几个测试用例覆盖中文、特殊字符、超长文本、空字符串这些边界情况。等这一套跑顺了你再去研究PEM格式的细节、跨语言对接、混合加密方案。这些内容都可以在官方文档和源码里找到但前提是你已经有了一个能跑通的最小实现打底不然看文档也是一头雾水。我做上位机这行的体会是安全方面的需求越来越常见甲方不懂技术细节但会直接提必须加密必须签名必须防篡改这类要求。与其等需求找上门再手忙脚乱地恶补不如趁现在就把这套东西沉淀成自己工具库的一部分。这篇文章里的代码我前前后后用了好几年踩过的坑基本都写进去了。如果你照着做应该能绕开我当年绕的那些弯路。
上一篇/下一篇内容由系统自动关联 返回资讯列表 →