【问题标题】:Unable to export RSA private parameters when running as administrator以管理员身份运行时无法导出 RSA 私有参数
【发布时间】:2020-12-11 22:09:50
【问题描述】:

我使用的是 NET Core 3.1,并且需要导出私有 RSA 参数(DPQ),因为它们被用作 HKDF 函数的密钥材料(基于 HMAC 的提取-and-Expand Key Derivation Function) 以提供确定性的共享秘密。

我的代码运行良好 - 但是,奇怪的是,如果它是从提升的管理员提示符运行的,它会抛出:

var flags = X509KeyStorageFlags.MachineKeySet | 
    X509KeyStorageFlags.PersistKeySet | 
    X509KeyStorageFlags.Exportable;

var certs = new X509Certificate2Collection();
certs.Import(@"C:\MyCert.pfx", String.Empty, flags);

var cert = certs.OfType<X509Certificate2>().Where(x => x.HasPrivateKey);

using (var rsa = cert.GetRSAPrivateKey())
{
    // This works - *unless* executed from an elevated admin prompt!?
    var rsaParms = rsa.ExportParameters(true);
            
    // use the params here...
}

堆栈跟踪:

Unhandled exception. Internal.Cryptography.CryptoThrowHelper+WindowsCryptographicException: The requested operation is not supported.
   at System.Security.Cryptography.CngKey.Export(CngKeyBlobFormat format)
   at System.Security.Cryptography.RSACng.ExportKeyBlob(Boolean includePrivateParameters)
   at System.Security.Cryptography.RSACng.ExportParameters(Boolean includePrivateParameters)

如果有任何不同,相关证书是自签名的,使用 C# 的System.Security.Cryptography.X509Certificates.CertificateRequest.CreateSelfSigned 生成。

知道为什么这只会在提升执行时抛出,或/以及如何让它不抛出?

更新

我做了更多的挖掘工作,如果我使用使用 OpenSSL 而不是 .NET 生成的自签名证书,它会按预期工作 - 所有 X509 扩展/设置都是相同的。

我做了一些调试,检查rsa.Key时发现有区别。

正常执行,.NET生成证书:

  • rsa.Key.ExportPolicyAllowExport | AllowPlaintextExport
  • rsa.Key.ProviderMicrosoft Enhanced Cryptographic Provider v1.0

提升执行,.NET 生成证书:

  • rsa.Key.ExportPolicyAllowExport
  • rsa.Key.ProviderMicrosoft Software Key Storage Provider

因此,它在未运行提升时使用已弃用的 CAPI 提供程序,而在提升时使用“现代”CNG 提供程序。我们可以看到 CNG 提供程序缺少 AllowPlaintextExport,我认为是问题所在。

使用 OpenSSL 生成的证书时,提供者始终是已弃用的 CAPI 提供者,无论是否提升。

更多的挖掘导致this answer,其中涉及使用互操作来获取使用CNG 时添加的AllowPlaintextExport 标志。现在,当使用 .NET 生成的证书时,这可以在管理员提示符下工作......但是当使用 OpenSSL 生成的证书时,对CryptAcquireCertificatePrivateKey 的调用返回false(意味着“没有获得私钥”),不分特权!

我找到了another, much simpler answer here,其中涉及将密钥导入RSACng,然后切换AllowPlaintextExport。但是,正如预期的那样,对rsa.ExportParameters(true) 的调用仍然失败并显示The requested operation is not supported,因此我无法导入RSACng

真的不应该这么难:(

【问题讨论】:

  • 我唯一能想到的是它被导入到不允许检索键值的 CSP 中。但是由于设置了Exportable,这仍然很奇怪。也许您可以包装密钥然后将其导出(但这更像是一个想法,可能肯定不是答案)
  • @MaartenBodewes 通过“包装密钥”,您的意思是在密钥生成期间将其导出到单独的文件,然后简单地使用原始字节而不是单个 RSA 参数??
  • @bartonjs 这有点厚颜无耻,但对此有什么想法吗? :)
  • @MaartenBodewes 我认为您可能对 CSP 有所了解——我刚刚尝试使用从 OpenSSL(而不是从 C#)生成的证书,并且在从提升的提示符下运行时它按预期工作。不明白为什么这是一个问题,因为我是从磁盘上的文件加载证书,而不是从 Windows 证书存储...
  • 从文件开始并不重要,因为您将其导入某种密钥库。如果您导入它,它不会喜欢该文件或任何东西,它会主动从其中复制数据,然后将其存储......某处。显然目标系统存储是不同的,并且确实为您的私钥提供了“更好的”保护。但除此之外,我不知道发生了什么,我还得研究它。

标签: c# .net-core cryptography rsa x509certificate2


【解决方案1】:

我无法解释原始代码在提升运行时不起作用的原因,但我想出了 2 个解决方法,大概是确保存在 AllowPlaintextExport 策略标志。

解决方法 1 - 加载 PEM 密钥

在原始代码中,我从包含公共和私有部分的 PKCS#12 (.p12/.pfx) 文件加载证书。相反,如果我加载 PEM 密钥,它会按预期工作:

// We need to strip the labels from the beginning and end of the key - working 
// with PEM files is much easier in .NET 5, as it handles all this cruft for us
var regex = new Regex(@"^[-]+BEGIN.+[-]+\s(?<base64>[^-]+)[-]+", RegexOptions.Compiled | RegexOptions.ECMAScript | RegexOptions.Multiline);

var keyText = File.ReadAllText(@"C:\app\cert.key");
var keyBase64 = regex.Match(keyText).Groups["base64"].Value;
var keyBytes = keyBase64.FromBase64String();

using var rsaKey = RSA.Create();
rsaKey.ImportRSAPrivateKey(keyBytes, out _);
var rsaParams = rsaKey.ExportParameters(true);

解决方法 2 - 导出/导入密钥

在此解决方法中,我们将密钥导出到 blob,然后再次将其导入。当我第一次尝试这个时,它没有用 - 由于未知原因,您必须以加密形式导出它;尝试在不加密的情况下导出!

代码基于 this 内部 .NET 实用程序。

public static RSA GetExportableRSAPrivateKey(this X509Certificate2 cert)
{
    const CngExportPolicies exportability = CngExportPolicies.AllowExport | CngExportPolicies.AllowPlaintextExport;

    var rsa = cert.GetRSAPrivateKey();

    // Thankfully we don't have to deal with all this shit on Linux
    if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
        return rsa;

    // We always expect an RSACng on Windows these days, but that could change
    if (!(rsa is RSACng rsaCng))
        return rsa;

    // Is the AllowPlaintextExport policy flag already set?
    if ((rsaCng.Key.ExportPolicy & exportability) != CngExportPolicies.AllowExport)
        return rsa;

    try
    {
        // Export the original RSA private key to an encrypted blob - note you will get "The requested operation
        // is not supported" if trying to export without encryption, so we export with encryption!
        var exported = rsa.ExportEncryptedPkcs8PrivateKey(nameof(GetExportableRSAPrivateKey),
            new PbeParameters(PbeEncryptionAlgorithm.Aes256Cbc, HashAlgorithmName.SHA256, 2048));

        // Load the exported blob into a fresh RSA object, which will have the AllowPlaintextExport policy without
        // having to do anything else
        RSA copy = RSA.Create();
        copy.ImportEncryptedPkcs8PrivateKey(nameof(GetExportableRSAPrivateKey), exported, out _);

        return copy;
    }
    finally
    {
        rsa.Dispose();
    }
}

【讨论】:

    猜你喜欢
    • 2017-05-19
    • 1970-01-01
    • 2016-06-07
    • 1970-01-01
    • 2019-05-17
    • 1970-01-01
    • 2012-11-22
    • 2016-10-25
    • 2021-02-12
    相关资源
    最近更新 更多