新版.NET应使用RSA.Create()生成密钥对,导出私钥用ExportPkcs8PrivateKey()、公钥用ExportSubjectPublicKeyInfo(),签名验签必须统一RSASignaturePadding.Pkcs1或.Pss,原文字节严格一致,私钥禁止硬编码,公钥可公开但需防重放。

用 RSA.Create() 生成密钥对,别碰过时的 RSACryptoServiceProvider
新版 .NET(Core 3.0+ / .NET 5+)默认走跨平台、无依赖的 RSA.Create(),它返回的是抽象基类 RSA 实例,底层自动选最优实现(Windows 上是 CNG,Linux/macOS 是 OpenSSL)。硬用老式的 RSACryptoServiceProvider 不仅在非 Windows 平台抛 PlatformNotSupportedException,还会因密钥导出格式不一致导致签名验签失败。
实操建议:
- 生成密钥对统一用
RSA.Create(2048)或RSA.Create(new RSAParameters { ... }),避免手动拼RSACryptoServiceProvider构造函数 - 私钥导出必须用
ExportPkcs8PrivateKey()(含私钥)或ExportSubjectPublicKeyInfo()(仅公钥),二者字节流格式不同,混用必验签失败 - 如果对接 Java/Node.js,公钥务必用
ExportSubjectPublicKeyInfo()导出 PEM,再 Base64 编码后加-----BEGIN PUBLIC KEY-----头尾——Java 的X509EncodedKeySpec只认这个格式
RSASignaturePadding.Pkcs1 和 .Pss 别乱换,接口双方必须严格一致
PKCS#1 v1.5(Pkcs1)和 PSS(Pss)是两种互不兼容的填充方案。哪怕密钥完全一样,用 Pkcs1 签名,用 Pss 验签,结果永远是 false,且不会报错,只会静默失败。
实操建议:
- 对外提供 API 时,在文档里白纸黑字写明用的是
RSASignaturePadding.Pkcs1还是.Pss,别让调用方猜 -
.Pss更安全但需要指定盐长(saltLength),常用new RSA-PSS-params中的hashAlgorithm对应长度(如 SHA256 → 盐长 32),C# 里直接传RSA.SaltLength = 32 - 老系统对接(尤其 PHP
openssl_sign()默认 PKCS#1)、微信/支付宝 SDK,基本都锁定Pkcs1,别强行升级成 PSS
验签前必须确保数据原文完全一致,空格、换行、编码一个都不能差
验签失败最常见的原因不是密钥或算法错了,而是“你以为的原文”和“签名时的原文”根本不是同一个字节序列。比如前端 JS 拼接 JSON 后没做 JSON.stringify() 标准化(属性顺序、空格、引号),或者后端收到时被反序列化又重序列化了一次。
实操建议:
- 约定签名原文为 UTF-8 字节数组,且不经过任何中间处理:签名时用
Encoding.UTF8.GetBytes(jsonString),验签时也必须用同一份原始字符串字节,而不是从 Model 再序列化一次 - HTTP Header 传签名值时,用 Base64Url 编码(替换
+为-,/为_,去掉末尾=),避免 URL 解码污染 - 调试时直接打印原文的十六进制(
BitConverter.ToString(bytes).Replace("-", "")),比看字符串更可靠
别把私钥硬编码进代码或配置文件,运行时加载才可控
把 privateKeyPem 直接写死在 appsettings.json 或代码里,等于把房门钥匙焊死在门框上。一旦泄露,所有历史签名都能被伪造。
实操建议:
- 私钥只存于受控环境:Windows 用 DPAPI(
ProtectedData),Linux/macOS 用密钥管理服务(如 HashiCorp Vault、Azure Key Vault),或至少存在带访问控制的文件系统(chmod 600) - 启动时读取并缓存
RSA实例,别每次签名都重新Create()+ImportPkcs8PrivateKey(),开销大还容易漏异常处理 - 公钥可以放心放配置或数据库,但注意:公钥本身不敏感,可公开;但若用它做加密(非签名),则必须确认对方确实持有对应私钥——这已超出签名防篡改范畴
最常被忽略的一点:签名验签本质只保证「这段数据没被改过」,不保证「谁发的」。如果没绑定身份(比如没把用户 ID、设备指纹等不可篡改字段一起签名),攻击者截获合法请求后反复重放,验签依然通过。防重放得靠时间戳+随机 nonce,那是另一层事了。


















