1

I'm using NET Core 3.1, and need to export the private RSA parameters (D, P and Q), as they are being used as keying material for an HKDF function (HMAC-based Extract-and-Expand Key Derivation Function) in order to provide deterministic shared secrets.

The code I have is working great - but, bizarrely it throws if it's ran from an elevated admin prompt:

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...
}

Stack trace:

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)

If it makes any difference, the certificate in question is self-signed, generated using C#'s System.Security.Cryptography.X509Certificates.CertificateRequest.CreateSelfSigned.

Any idea why this would throw only when executed elevated, or/and how to get it to not throw?

UPDATE

I've done some more digging, and it works as expected if I use a self-signed cert generated using OpenSSL instead of .NET - all the X509 extensions/settings are the same.

I did some debugging, and see that there is a difference when I inspect rsa.Key.

Normal execution, .NET generated certificate:

  • rsa.Key.ExportPolicy is AllowExport | AllowPlaintextExport
  • rsa.Key.Provider is Microsoft Enhanced Cryptographic Provider v1.0

Elevated execution, .NET generated certificate:

  • rsa.Key.ExportPolicy is AllowExport
  • rsa.Key.Provider is Microsoft Software Key Storage Provider

So, it's using the deprecated CAPI provider when not running elevated, and the "modern" CNG provider when elevated. And we can see that AllowPlaintextExport is missing with the CNG provider, which I assume is the problem.

When using an OpenSSL-generated certificate the provider is always the deprecated CAPI provider, whether elevated or not.

Yet more digging led to this answer, which involves using interop to get the AllowPlaintextExport flag added when using the CNG. Now, this works from an admin prompt when using a .NET-generated certificate... but the call to CryptAcquireCertificatePrivateKey returns false when using an OpenSSL-generated certificate (means "didn't get the private key"), regardless of privileges!

I found another, much simpler answer here, which involves importing the key into an RSACng, and then toggling on the AllowPlaintextExport. However, as expected the call to rsa.ExportParameters(true) still fails with The requested operation is not supported, so I'm unable to import into the RSACng

It really shouldn't be this difficult :(

Cocowalla
  • 13,822
  • 6
  • 66
  • 112
  • The only thing I can come up with is that it is imported into a CSP that doesn't allow retrieval of the key value. But as `Exportable` is set that's still weird. Maybe you can wrap the key and then export it (but that's more like an idea and may certainly not be the answer) – Maarten Bodewes Dec 10 '20 at 23:16
  • @MaartenBodewes by "wrap the key", do you mean export it to a separate file during key generation, then simply use the raw bytes instead of the individual RSA parameters?? – Cocowalla Dec 11 '20 at 09:17
  • @bartonjs this is a bit cheeky, but any idea on this one? :) – Cocowalla Dec 11 '20 at 09:18
  • @MaartenBodewes I think you might be onto something regarding the CSP - I just tried using a cert generated from OpenSSL (instead of from C#), and it works as expected when running from elevated prompt. Don't understand why this is a problem though, as I'm loading the certificate from a file on disk, not from the Windows cert store... – Cocowalla Dec 11 '20 at 09:52
  • That you start off with a file doesn't matter much as you import it into some kind of keystore. If you import it it won't like the file or anything, it actively copies the data from it and then stores it...somewhere. Apparently the target system store is different and does provide "better" protection of your private key. But other than that I don't know what is happening, I would have to research it as well. – Maarten Bodewes Dec 11 '20 at 10:15
  • *...the private RSA parameters (D, P and Q) ... are being used as keying material for an HKDF function...* Hmm.... – President James K. Polk Dec 11 '20 at 12:14
  • @PresidentJamesK.Polk yes, we have *n* nodes in possession of the certificate, and they need to create deterministic shared secrets for use as KEKs. The private key of the certificate seems like pretty good input to a KDF. Discussions over in crypto: [here](https://crypto.stackexchange.com/questions/50118/derive-a-secret-from-an-rsa-private-key) and [here](https://crypto.stackexchange.com/questions/58935/using-hkdf-to-derive-symmetric-keys-from-a-hybrid-public-key-encryption-scheme) – Cocowalla Dec 11 '20 at 12:18

1 Answers1

0

I can't explain the reason the original code didn't work when running elevated, but I have come up with 2 workarounds, which presumably are ensuring the AllowPlaintextExport policy flag is present.

Workaround 1 - Load PEM Key

In the original code, I load a certificate from a PKCS#12 (.p12/.pfx) file, which contains both the public and private parts. If instead I load a PEM key, it works as expected:

// 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);

Workaround 2 - Export/Import Key

In this workaround, we export the key to a blob, then import it back again. When I first tried this, it didn't work - for reasons unknown, you have to export it in encrypted form; attempting to export without encryption throws!

Code is based on this internal .NET utility.

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();
    }
}
Cocowalla
  • 13,822
  • 6
  • 66
  • 112