在C#中处理敏感数据或接口鉴权时,RSA非对称加密与数据签名是最常用的安全手段。与对称加密不同,RSA通过一对数学关联的密钥实现加密与签名,公钥可以公开分发用于加密或验签,私钥必须严格保密用于解密或签名。理解这套机制后,我们才能在.NET平台写出既安全又兼容多系统的代码。

密钥的生成与导入导出
在C#里生成RSA密钥对最简单的方式是使用RSA.Create()工厂方法,它会在不同操作系统上返回对应的实现,避免直接依赖Windows的RSACryptoServiceProvider。生成之后,我们可以通过XML字符串或PEM字节流导出,方便服务端存储与客户端分发。注意密钥长度至少2048位,1024位已被证实存在被破解的理论风险。
下面的示例展示如何创建密钥并导出为XML格式。XML在旧版.NET中兼容性最好,但在跨语言系统中更推荐PEM。导出私钥时要确保运行环境有相应权限,不要将私钥明文写进日志或前端代码。
using System;
using System.Security.Cryptography;
class KeyDemo
{
static void Main()
{
using RSA rsa = RSA.Create(2048);
// 导出包含私钥的XML,用于服务端保存
string privateXml = rsa.ToXmlString(true);
// 导出仅含公钥的XML,可分发到客户端
string publicXml = rsa.ToXmlString(false);
Console.WriteLine("公钥XML长度:" + publicXml.Length);
}
}
如果需要在Linux或容器环境中与Java、Go交互,建议用ExportRSAPrivateKey导出PKCS#8的DER字节,再Base64成PEM。这样能绕开Windows专有API导致的NotSupportedException。密钥管理上,私钥应放在配置中心或HSM,公钥可随SDK下发。
RSA加密与解密的完整流程
RSA加密本身有长度限制,明文长度不能超过密钥长度减去填充开销。以2048位密钥、OAEP-SHA256填充为例,最大明文约190字节。因此实际项目中多用RSA加密一个对称密钥,再用AES加密业务数据,这种混合加密既安全又高效。单纯用RSA加密长文本会直接抛CryptographicException。
以下代码演示用公钥加密、私钥解密的标准做法。我们采用RSAEncryptionPadding.OaepSHA256,它比老的PKCS1填充更能抵抗选择密文攻击。加密结果通常是字节数组,网络传输前做Base64即可。
using System;
using System.Security.Cryptography;
using System.Text;
class RsaEnc
{
static byte[] Encrypt(RSA pubKey, string text)
{
byte[] data = Encoding.UTF8.GetBytes(text);
return pubKey.Encrypt(data, RSAEncryptionPadding.OaepSHA256);
}
static string Decrypt(RSA priKey, byte[] cipher)
{
byte[] plain = priKey.Decrypt(cipher, RSAEncryptionPadding.OaepSHA256);
return Encoding.UTF8.GetString(plain);
}
}
解密端必须捕获填充错误,因为攻击者可能提交非法密文。不要根据解密异常返回不同错误码,否则会泄露私钥状态。对于Web接口,建议把RSA加密仅用于握手阶段,后续通信用协商出的会话密钥,避免性能瓶颈。
使用RSA实现数据签名与验签
数据签名用于证明数据未被篡改且来自持有私钥的一方。流程是:对原始数据算哈希,再用私钥对哈希值做RSA运算得到签名;验证方用公钥对签名解密并比对哈希。C#中推荐SignData与VerifyData,它们内部自动处理哈希和编码,不易出错。
下面例子使用SHA256哈希配合PKCS1 v1.5签名方案,这是当前多数API平台兼容的做法。若系统自身可控,可换用PSS填充提升安全性。签名输出同样是字节数组,一般Base64后放进HTTP头或报文尾部。
using System;
using System.Security.Cryptography;
using System.Text;
class RsaSign
{
static byte[] Sign(RSA priKey, string msg)
{
byte[] data = Encoding.UTF8.GetBytes(msg);
return priKey.SignData(data, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
}
static bool Verify(RSA pubKey, string msg, byte[] sig)
{
byte[] data = Encoding.UTF8.GetBytes(msg);
return pubKey.VerifyData(data, sig, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
}
}
验签失败时要统一返回无效请求,不要提示具体是哪一步不对。私钥签名动作必须在受控服务端完成,绝不可下放到浏览器或App本地,否则等于公开签名权。对于高并发场景,可预加载RSA对象并复用,减少每次RSA.Create的构造开销。
常见错误与兼容性问题
开发者常把RSACryptoServiceProvider写死在代码里,导致程序丢到Linux容器就报错。正确做法是用RSA.Create抽象,再导入从文件或配置读出的密钥。另一个坑是填充模式不一致:一方用OAEP一方用Pkcs1,结果必然解密失败。
签名方面,哈希算法必须两端一致,.NET的HashAlgorithmName.SHA256要对应验签方的同样声明。还有一些旧系统只认SHA1,这时虽不推荐但为兼容可临时降级,同时规划迁移。密钥长度、填充、哈希三者任一不匹配都会让加密或签名彻底失效,排错时建议先写最小复现单元再逐步接入业务。
using System;
using System.Security.Cryptography;
class Check
{
static void Main()
{
// 错误示范:硬编码Windows实现
// var csp = new RSACryptoServiceProvider(2048);
// 正确:跨平台抽象
using RSA r = RSA.Create(2048);
Console.WriteLine(r.KeySize);
}
}
最后提醒,RSA不是用来替代传输层安全协议的。它适合做应用层鉴权、许可证签名或密钥协商,日常通信仍应结合TLS。把RSA私钥放进源码或前端包体是严重事故,应使用环境变量或密钥服务注入。