RSA 的 D 参数根据您访问证书私钥的方式而变化

D-Parameter of RSA change depending on how you access the private key of a certificate

我希望有人能告诉我我哪里出错了。我一直以为导出一个带私钥的证书再导入,私钥是稳定的,不会变的。尤其是跨电脑。

现在我被证明是错误的,我不明白。

给定一个证书Z。其中包含一个私钥pk。 我将此证书导入计算机 C1 和计算机 C2。

我在两者上都得到了私钥的参数。

Result on C1:
Exponent : {1, 0, 1}
Modulus  : {148, 19, 118, 153...}
P        : {246, 42, 172, 195...}
Q        : {153, 253, 180, 23...}
DP       : {161, 179, 194, 172...}
DQ       : {63, 22, 42, 14...}
InverseQ : {170, 228, 238, 93...}
D        : {60, 36, 199, 168...}

Result on C2:
Exponent : {1, 0, 1}
Modulus  : {148, 19, 118, 153...}
P        : {246, 42, 172, 195...}
Q        : {153, 253, 180, 23...}
DP       : {161, 179, 194, 172...}
DQ       : {63, 22, 42, 14...}
InverseQ : {170, 228, 238, 93...}
D        : {0, 233, 203, 106...}

使用代码:

using (var store = new X509Store(storeNameEnum, storeLocationEnum))
{

  X509Certificate2Collection certificates = null;
  store.Open(OpenFlags.ReadOnly);
  var certificates = store.Certificates;
  var cert = certificates.Cast<X509Certificate2>().Single(c => c.SubjectName.Name.ToLower().Contains(subject.ToLower()));
  var rsa = cert.GetRSAPrivateKey();
  var params = rsa.ExportParameters(true);
}

RSA 参数的 D 部分不同。为什么?

在计算机 C2 上,当我调用以下行时,我得到与在 C1 上相同的 D:

var params (cert.PrivateKey as RSACryptoServiceProvider).ExportParameters(true).D;

但我不能使用它,因为我正在编写一个 .NetStandard 库,该库也在 Net5 项目中使用。

接下来我尝试直接从 C2 上的文件中读取 PFX 并检查 PK。以下几行给出了 C1 上的 PK。

string pfxPassword = "pw";
Pkcs12Store pkcs12 = new Pkcs12Store(new FileStream(@"C:\tmp\cert.p12", FileMode.Open, FileAccess.Read), pfxPassword.ToArray());
var cert = pkcs12.GetCertificate("certName");
var keyEntry = pkcs12.GetKey("certName");    
RSAParameters rsaParams = Org.BouncyCastle.Security.DotNetUtilities.ToRSAParameters((RsaPrivateCrtKeyParameters)keyEntry.Key);
Console.WriteLine($"=> {String.Join(" ", rsaParams.D)}");

但是,当我随后将其转换为证书以将其导入商店时,PK 再次发生变化,因为它最初位于 C2 上。

X509Certificate2 certificate = new System.Security.Cryptography.X509Certificates.X509Certificate2(Org.BouncyCastle.Security.DotNetUtilities.ToX509Certificate(cert.Certificate));
RSACryptoServiceProvider csp = new RSACryptoServiceProvider(new CspParameters(1, "Microsoft Strong Cryptographic Provider", new Guid().ToString(), new System.Security.AccessControl.CryptoKeySecurity(), null));                       
csp.ImportParameters(rsaParams);
certificate.PrivateKey = csp;            
Console.WriteLine($"=> {String.Join(" ", certificate.GetRSAPrivateKey().ExportParameters(true).D)}");

显然,某些计算机设置为始终给出与 C1 相同的结果,而其他计算机则始终给出与 C2 相同的结果。但为什么它们会产生不同的结果呢?是什么影响了这种行为?

感谢任何提示。

基本上,D 值并不重要,您会看到它的结果。

“你刚才是说 D 值无关紧要吗?RSA 不是基于 m == modpow(modpow(m, e, n), d, n) 吗?”

是的,是的。但是 Chinese Remainder Theoremmodpow(m, d, n) 提供了更有效的实现,因此没有人真正为 D.

而烦恼

发生的另一件事是,当导入 RSA 私钥时,您有几个选择:1) 验证 n == (p * q) 和 d/dp/dq/qInv 是否有意义n/e/p/q,如果他们不这样做,则失败,2) 信任地导入密钥,处理不一致的后果(“垃圾输入,垃圾输出”),3) 执行 (1) 但修复任何不正确的数据。

好的,所以我们有了这样的前提:为什么值 可能 会改变(策略 (3)),但为什么它们 实际上 改变?

因为 D 至少有两个不同的常见答案。 (“D 不是唯一的吗?”不。“你不是说 D 不重要吗?”好吧,所以它在计算 CRT 参数时很重要,然后就不再重要了。)

最初的 RSA 论文将 D 定义为 e 模 N 的 Euler totient function of N. The usual symbol for the Euler totient function is the Greek letter phi. Many smart people later, the statement got changed to D being the modular multiplicative inverse of e modulo the Carmichael function 的模乘逆。Carmichael 函数的常用符号是希腊字母 lambda。

不同之处在于正方形与矩形的区别。所有 D-phi 值都适用于 RSA,因为 e * D-phi === 1 (mod lambda(N))。由于所有 D-lambda 值也适用于 RSA,但不遵守 e * D-lambda === 1 (mod phi(N)),因此重写了公式。

好的,有背景,那发生了什么?

  • Windows CAPI(在 Windows 上为 RSACryptoServiceProvider 提供支持,在 .NET Framework 上 RSA.Create())使用 lambda 生成密钥,但在 import/export 中保留 D 值。
  • OpenSSL(在 Linux 上支持 RSA 类)使用 phi 生成密钥,但在 import/export 中保留 D 值。
  • Windows CNG(在 Windows 上支持 RSACng,在 Windows 上支持 .NET5/.NET Core 上的 RSA.Create())使用 phi 生成密钥,但是 在导入时丢弃 D 并从 N/E/P/Q 重新计算它以用于导出
    • (这里有些细微差别...我觉得 CNG 更改为可能保留 Windows 10 20H1 左右的 D 值。)
  • 我不记得 Android 做了什么(可能是 OpenSSL 行为),或者 macOS 做了什么。

所以,我的猜测是 C1 和 C2 在不同的 OS 上是 运行(或相同 OS 的不同版本)。

https://github.com/dotnet/runtime/commit/700a07cae19fe64649c2fb4c6c10e6b9aa85dc29 展示了我们如何在 .NET 测试套件中处理它。对于应用程序代码,我的建议是只信任系统。