RSA签名是二进制数据,非文本,不能用UTF8.GetString转换,否则会因非法码点导致验签失败;应使用Base64或十六进制编码传输。

为什么 RSA.SignData 签出来的字节不能直接当字符串传?
很多人用 RSA.SignData 得到一个 byte[],立马调 Encoding.UTF8.GetString() 转成字符串,结果对方验签失败。这不是编码问题,是根本性误解:签名是二进制数据,不是文本。UTF-8 会把某些字节序列解释为非法码点,直接丢弃或替换,签名就废了。
正确做法只有两种:
- 用 Base64 编码:
Convert.ToBase64String(signedBytes)—— 最常用,适合 JSON、HTTP 传输 - 用十六进制(hex):
BitConverter.ToString(signedBytes).Replace("-", "").ToLower()—— 方便日志查看,但体积翻倍
别碰 Encoding 的任何 GetString 方法,哪怕你试过“偶尔能成功”——那是运气,不是逻辑。
用 RSACng 还是 RSACryptoServiceProvider?
.NET Core 3.0+ 和 .NET 5+ 默认推荐 RSACng,它基于 Windows CNG,支持 SHA2-256/384/512,密钥可持久化到 Windows DPAPI,且线程安全。而老的 RSACryptoServiceProvider 在 .NET Core 中已标记为 obsolete,且在 Linux/macOS 上不可用。
实操建议:
- 生成密钥时优先用
RSACng(2048)或RSACng(3072),别硬写new RSACryptoServiceProvider() - 如果必须兼容旧系统(如 .NET Framework 4.6.1),再考虑
RSACryptoServiceProvider,但得手动指定CspParameters避免默认使用不安全的 PROV_RSA_FULL -
RSACng的私钥导出默认是 Pkcs8(ExportPkcs8PrivateKey()),公钥是 X.509(ExportSubjectPublicKeyInfo()),和 OpenSSL 兼容;老版导出的是 XML 格式,跨语言痛苦
验签失败常见原因:哈希算法没对齐
签名和验签两端必须用**完全相同的哈希算法**,否则 RSA.VerifyData 直接返回 false,不抛异常,容易误判为“数据被篡改”。
典型错误场景:
- 签名端用
HashAlgorithmName.SHA256,验签端错写成HashAlgorithmName.SHA1 - 签名时传的是原始数据,验签时却对 Base64 字符串再次哈希(比如先
Encoding.UTF8.GetBytes(base64Str)再哈希) - JSON 数据验签前没做标准化:字段顺序不同、空格/换行差异、浮点数精度(
1.0vs1)都会导致哈希值不同
建议在签名/验签前统一走一遍 JsonSerializer.Serialize(data, new JsonSerializerOptions { WriteIndented = false, Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping }),确保 JSON 字节流一致。
如何安全保存私钥?别硬编码、别放配置文件
项目里写 private const string PrivateKeyXml = "..." 是高危操作。私钥一旦泄露,签名等同于公开授权。
可行方案按安全等级排序:
- 开发阶段:用
RSACng+CngKey.Import(..., CngKeyBlobFormat.Pkcs8PrivateBlob)从本地加密文件加载,文件权限设为仅当前用户可读 - 生产环境(Windows):用 Windows Certificate Store,通过证书指纹加载,私钥由系统保护,代码里只存 thumbprint
- 容器/K8s 场景:用密钥管理服务(如 Azure Key Vault、AWS KMS),调用
SignAsync接口让服务端完成签名,私钥永不离开 HSM
哪怕只是 Demo 项目,也别把私钥明文塞进源码——Git 历史里删不干净,CI 日志可能落盘,一次误提交就全暴露。


















