在C#后端开发中,RSA非对称加密和签名是保障数据传输安全和身份校验的核心手段。与对称加密不同,RSA通过一对数学上关联的密钥实现加解密分离,既解决了密钥分发难题,也天然支持数字签名。本文将深入讲解如何在C#中运用RSA完成加密、解密、签名与验签,并探讨生产环境中的进阶注意事项。

一、RSA基础与密钥生成
RSA算法基于大整数分解难题,公钥可公开用于加密或验签,私钥必须保密用于解密或签名。在C#中,现代做法推荐使用RSA抽象类而非过时的RSACryptoServiceProvider,后者在跨平台时存在密钥容器依赖问题。通过RSA.Create()能创建符合当前运行环境的最佳实现,例如在Windows上可能是CNG,在Linux上则是OpenSSL后端。
生成密钥时应当明确指定密钥长度,2048位是目前最低安全基线,3072位则更适合长期敏感数据。下面代码展示如何创建RSA实例并导出XML与PEM格式密钥,便于存储和跨语言交互。注意私钥导出需设置包含私钥标记,公钥则可自由分发。
using System;
using System.Security.Cryptography;
class KeyGen
{
static void Main()
{
// 创建RSA实例,指定3072位密钥长度
using RSA rsa = RSA.Create(3072);
// 导出包含私钥的XML字符串,便于C#内部使用
string privateXml = rsa.ToXmlString(true);
// 导出仅公钥XML
string publicXml = rsa.ToXmlString(false);
Console.WriteLine("私钥XML:" + privateXml);
Console.WriteLine("公钥XML:" + publicXml);
// 导出PEM格式,跨平台友好
string privatePem = rsa.ExportRSAPrivateKeyPem();
string publicPem = rsa.ExportRSAPublicKeyPem();
Console.WriteLine("私钥PEM:" + privatePem);
}
}
上述代码使用ExportRSAPrivateKeyPem方法直接获取PEM文本,避免了旧版需要手动处理ASN.1结构的麻烦。在微服务架构中,将公钥放入配置中心,私钥存于密钥管理服务,可大幅降低泄露风险。不建议将私钥硬编码在代码中,应使用环境变量或安全的密钥库加载。
二、非对称加密与解密实现
RSA加密有长度限制,单次加密数据不可超过密钥长度减去填充开销。以3072位密钥为例,使用OAEP填充时最多加密约300字节,因此实际业务中通常用RSA加密对称密钥,再用AES加密大数据体。以下示例演示用公钥加密短文本,私钥解密的过程,采用OAEP SHA256填充以提升安全性。
using System;
using System.Security.Cryptography;
using System.Text;
class RsaEncryptDemo
{
static void Main()
{
using RSA rsa = RSA.Create(3072);
string publicKeyXml = "<RSAKeyValue><Modulus>示例公钥</Modulus><Exponent>AQAB</Exponent></RSAKeyValue>";
rsa.FromXmlString(publicKeyXml);
byte[] data = Encoding.UTF8.GetBytes("敏感令牌123");
// 使用OAEP填充,哈希算法为SHA256
byte[] cipher = rsa.Encrypt(data, RSAEncryptionPadding.OaepSHA256);
// 模拟私钥持有方解密
using RSA priRsa = RSA.Create();
priRsa.FromXmlString("<RSAKeyValue>示例私钥</RSAKeyValue>");
byte[] plain = priRsa.Decrypt(cipher, RSAEncryptionPadding.OaepSHA256);
Console.WriteLine("解密结果:" + Encoding.UTF8.GetString(plain));
}
}
代码中Encrypt方法第二个参数指定填充模式,OAEP比传统PKCS1 v1.5更能抵抗选择密文攻击。需要强调的是,加密输出是字节数组,若需传输应转为Base64而非直接UTF8字符串,否则会出现乱码或数据丢失。解密时必须使用相同填充模式,否则会抛出密码学异常。
在Web API场景里,客户端可用JS的WebCrypto用公钥加密,服务端C#解密,只要两边填充参数一致即可互通。若遇到“不正确的数据”异常,优先检查是否误将密文当字符串存库导致字节变化,或者填充模式不匹配。
三、数字签名与验签进阶
签名用于证明数据未被篡改且来自私钥持有者。与加密方向相反,签名用私钥对数据哈希值加密,验签用公钥解密哈希并比对。C#中推荐PSS概率签名方案替代老旧的PKCS1 v1.5,因为PSS具备可证明安全性。下面展示签名生成与验证完整流程。
using System;
using System.Security.Cryptography;
using System.Text;
class RsaSignDemo
{
static void Main()
{
using RSA rsa = RSA.Create(3072);
byte[] msg = Encoding.UTF8.GetBytes("转账指令:金额100");
// 私钥签名,使用PSS填充,SHA256哈希
byte[] signature = rsa.SignData(msg, HashAlgorithmName.SHA256, RSASignaturePadding.Pss);
// 公钥验签
using RSA pubRsa = RSA.Create();
pubRsa.FromXmlString(rsa.ToXmlString(false));
bool ok = pubRsa.VerifyData(msg, signature, HashAlgorithmName.SHA256, RSASignaturePadding.Pss);
Console.WriteLine("验签通过:" + ok);
}
}
SignData内部会自动计算数据哈希再签名,无需手动调用哈希函数。若要对已计算的哈希签名,可用SignHash。验签失败常见于数据在传输中被修改,或签名方与验签方填充方案不一致。在API网关中,常将签名放在HTTP头,服务端拦截器统一验签,实现请求防抵赖。
对于高频接口,RSA签名较耗CPU,可引入缓存或改用Ed25519等椭圆曲线签名以提升吞吐。但RSA优势在于广泛兼容旧系统与现有证书体系,因此在金融合规场景仍为主流选择。
四、跨平台与密钥管理坑点
在Linux容器部署时,RSACryptoServiceProvider会因缺少Windows密钥存储而报错,必须切换至RSA.Create并加载PEM文件。下面代码演示从文件读取PEM私钥并导入,避免平台绑定问题。
using System;
using System.IO;
using System.Security.Cryptography;
class PemLoadDemo
{
static void Main()
{
string pem = File.ReadAllText("/app/keys/private.pem");
using RSA rsa = RSA.Create();
// 从PEM字符串导入私钥
rsa.ImportFromPem(pem.ToCharArray());
Console.WriteLine("密钥位数:" + rsa.KeySize);
}
}
使用ImportFromPem能直接解析标准PEM,包括PKCS8和SEC1格式,省去自行Base64解码的麻烦。密钥文件权限应设为600,防止其他进程读取。另外,.NET版本需5.0以上才完整支持PEM导入,老框架需用BouncyCastle补充。
总结来说,C#中RSA进阶运用关键在选对抽象类、用OAEP与PSS、管好密钥生命周期。将加密与签名组合,可构建既保密又可信的服务间通信,切实提升系统安全水位。