Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Cannot export certificate data even with X509KeyStorageFlags.Exportable #77590

Closed
BethanyZhou opened this issue Oct 28, 2022 · 18 comments
Closed

Comments

@BethanyZhou
Copy link

Description

Come from #26031

  1. I have created a self signed RSA certificate and stored the Private key as .pfx file.
  2. I'm writing dotnet standard code and trying to instantiate the X509Certificate2 object with the .pfx file
  3. The X509Certificate2 instance is created successfully with X509KeyStorageFlags.Exportable
  4. when I export parameters by ExportParameters(true), I received "The requested operation is not supported."

Reproduction Steps

X509Certificate2 certificate = new X509Certificate2(pfxFileName, pfxPassword, X509KeyStorageFlags.Exportable);
var key = certificate.PrivateKey as RSA;
RSAParameters rSAParameters = key.ExportParameters(true);

Expected behavior

export parameters sucessfully

Actual behavior

I received "The requested operation is not supported."

Regression?

No response

Known Workarounds

No response

Configuration

dotnet standard 2.0.3

Other information

No response

@ghost ghost added the untriaged New issue has not been triaged by the area owner label Oct 28, 2022
@ghost
Copy link

ghost commented Oct 28, 2022

Tagging subscribers to this area: @dotnet/area-system-security, @vcsjones
See info in area-owners.md if you want to be subscribed.

Issue Details

Description

Come from #26031

  1. I have created a self signed RSA certificate and stored the Private key as .pfx file.
  2. I'm writing dotnet standard code and trying to instantiate the X509Certificate2 object with the .pfx file
  3. The X509Certificate2 instance is created successfully with X509KeyStorageFlags.Exportable
  4. when I export parameters by ExportParameters(true), I received "The requested operation is not supported."

Reproduction Steps

X509Certificate2 certificate = new X509Certificate2(pfxFileName, pfxPassword, X509KeyStorageFlags.Exportable);
var key = certificate.PrivateKey as RSA;
RSAParameters rSAParameters = key.ExportParameters(true);

Expected behavior

export parameters sucessfully

Actual behavior

I received "The requested operation is not supported."

Regression?

No response

Known Workarounds

No response

Configuration

dotnet standard 2.0.3

Other information

No response

Author: BethanyZhou
Assignees: -
Labels:

area-System.Security

Milestone: -

@vcsjones
Copy link
Member

This is a duplicate of #26031 (comment). The would around should be to do an encrypted export, import, and export unencrypted. Stealing @bartonjs's solution...

using (RSA tmp = RSA.Create())
using (RSA key = certificate.GetRSAPrivateKey())
{
    cont string pwd = "TempPassword";
    PbeParameters pbeParameters = new PbeParameters(PbeEncryptionAlgorithm.Aes128Cbc, HashAlgorithmName.SHA256, 1);
    tmp.ImportPkcs8PrivateKey(key.ExportPkcs8PrivateKey(pwd, pbeParameters), pwd);
    return tmp.ExportParameters(true);
}

@BethanyZhou
Copy link
Author

BethanyZhou commented Oct 31, 2022

Thanks for your response, @vcsjones .

I can't find class PbeParameters and method ImportPkcs8PrivateKey and ExportPkcs8PrivateKey on dotnet standard 2.0. How can I include them? We need to support user on windows powershell so that we can't upgrade our runtime version, do you have any advice for our case?

@bartonjs
Copy link
Member

bartonjs commented Nov 8, 2022

Why do you need to use ExportParameters(true)? (While there are solutions that can work on .NET Standard 2.0 it feels like maybe questioning the scenario is better here)

@bartonjs bartonjs added this to the Future milestone Nov 8, 2022
@ghost ghost removed the untriaged New issue has not been triaged by the area owner label Nov 8, 2022
@BethanyZhou
Copy link
Author

Hi @bartonjs , thanks for your response.

We need to use many RSA parameters, including rsaParameters.Modulus, rsaParameters.Exponent, rsaParameters.DP and so on in PFX file, so that we can pass data to our backend . You can find details on our open source repo if you are interested: https://github.com/Azure/azure-powershell/blob/a440db085c71a9ceae797bddcf6450dbc01835db/src/KeyVault/KeyVault/Models/PfxWebKeyConverter.cs#L125-L138.

Please feel free to let me know if you need further information.

@bartonjs
Copy link
Member

bartonjs commented Nov 8, 2022

OK, so you need the parameters because you're exporting the private key in a different format. That makes sense.

The best advice I have for .NET Standard 2.0 or .NET Framework is to do something like

private const CngExportPolicies FullyExportable = CngExportPolicies.AllowExport | CngExportPolicies.AllowPlaintextExport;

private static readonly s_fullyExportableProperty = new CngProperty(
    "Export Policy",
    BitConverter.GetBytes((int)FullyExportable),
    CngPropertyOptions.Persist));

...

X509Certificate2 cert = new X509Certificate2(pfx, pwd, flags);

using (RSA rsa = cert.GetRSAPrivateKey())
{
    if (rsa != null)
    {
        if (rsa is RSACng rsaCng)
        {
            rsaCng.Key.SetProperty(s_fullyExportableProperty);
        }

        return CreateTrack2SdkJWK(rsa, extraInfo);
    }
}

using (ECDsa ecdsa = cert.GetECDsaPrivateKey())
{
    if (ecdsa != null)
    {
        if (ecdsa is ECDsaCng ecdsaCng)
        {
            ecdsaCng.Key.SetProperty(s_fullyExportableProperty);
        }

        return CreateTrack2SdkJWK(ecdsa, extraInfo);
    }
}

...

Note also that X509Certificate2.PrivateKey is deprecated, and will never return an ECDSA key, so var ecKey = certificate.PrivateKey as ECDsa; is ECDsa ecKey = null;.

@BethanyZhou
Copy link
Author

Hi @vcsjones , thanks for reminding me about the deprecation of X509Certificate2.PrivateKey.

Seems like CngProperty is not available on dotnet standard 2.0, could you help?

@bartonjs
Copy link
Member

bartonjs commented Nov 9, 2022

Seems like CngProperty is not available on dotnet standard 2

It is, but it's part of the System.Security.Cryptography.Cng package that you might have to reference explicitly (though if you can see RSACng/ECDsaCng you're already referencing it).

I do have a syntax error in my snippet, the static-readonly field doesn't have a type; so you could also be seeing that?

@BethanyZhou
Copy link
Author

I think the correct snippet should be

         private const CngExportPolicies FullyExportable = CngExportPolicies.AllowExport | CngExportPolicies.AllowPlaintextExport;

        private static readonly CngProperty s_fullyExportableProperty = new CngProperty("ExportPolicy",
            BitConverter.GetBytes((int)FullyExportable),
            CngPropertyOptions.Persist);
...
         var rsaKey = certificate.GetRSAPrivateKey();
         if (rsaKey != null)
            {
                if(rsaKey is RSACng rsaCng)
                {
                    rsaCng.Key.SetProperty(s_fullyExportableProperty);
                }
                return CreateTrack2SdkJWK(rsaKey, extraInfo);
            }

I got an exception when code goes to rsaCng.Key.SetProperty(s_fullyExportableProperty), the exception message is The requested operation is not supported.

@panagiotis-bitharis
Copy link

panagiotis-bitharis commented Nov 11, 2022

Hi, I am having the same issue. Tried with 6 and .net framework 4.7.2. As soon as you export the certificate as bytes, the .ExportParameters(true) will fail.

                        using (var certificate = GenerateCertificateAuthority("CN=Certificate-Authority-No-Passphrase", 2048))
			{
				var key = certificate.GetRSAPrivateKey();
				var parameters = key.ExportParameters(true); //All good succeded

				byte[] certData = certificate.Export(X509ContentType.Pkcs12, "hello");

				using (var cert = new X509Certificate2(certData, "hello",X509KeyStorageFlags.Exportable))
				{
					key = cert.GetRSAPrivateKey();
					parameters = key.ExportParameters(true); // fails here
				}
				
				var fullPath = Path.Combine(DestinationFolder, "certificate-authority.pfx");

				File.WriteAllBytes(fullPath, certData);
			}
			
			
			
		public static X509Certificate2 GenerateCertificateAuthority(string subjectName, int keySizeInBits)
		{
			using (RSA parent = RSA.Create(keySizeInBits))
			{
				CertificateRequest parentReq = new CertificateRequest(subjectName, parent, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);

				parentReq.CertificateExtensions.Add(new X509BasicConstraintsExtension(IsCertificateAuthority, true, 0, true));

				// Set the subjeck key identifier
				parentReq.CertificateExtensions.Add(new X509SubjectKeyIdentifierExtension(parentReq.PublicKey, false));

				// In a CA root cert, "Authority Key Identifier" should be the same as "Subject Key Identifier".
				if (parentReq.CertificateExtensions != null)
				{
					var skiExtension = (X509SubjectKeyIdentifierExtension)parentReq.CertificateExtensions.SingleOrDefault(e => e.Oid.FriendlyName == "Subject Key Identifier");
					parentReq.CertificateExtensions.Add(new AuthorityKeyIdentifierExtension(skiExtension, false));
				}

				;

				// Limit the usage of this certificate.
				parentReq.CertificateExtensions.Add(new X509KeyUsageExtension(X509KeyUsageFlags.KeyCertSign | X509KeyUsageFlags.CrlSign, true));

				X509Certificate2 rootCA = parentReq.CreateSelfSigned(DateTimeOffset.UtcNow.AddDays(-45), DateTimeOffset.UtcNow.AddDays(365));

				const String RSA = "1.2.840.113549.1.1.1";
				const String DSA = "1.2.840.10040.4.1";
				const String ECC = "1.2.840.10045.2.1";

				switch (rootCA.PublicKey.Oid.Value) {
					case RSA:
						RSA rsa = rootCA.GetRSAPrivateKey(); // or cert.GetRSAPublicKey() when need public key
						if (rsa is RSACng rsaCng)
						{
							rsaCng.Key.SetProperty(s_fullyExportableProperty);
						}
						break;
					case DSA:
						DSA dsa = rootCA.GetDSAPrivateKey(); // or cert.GetDSAPublicKey() when need public key
						if (dsa is DSACng dsaCng)
						{
							dsaCng.Key.SetProperty(s_fullyExportableProperty);
						}

						break;
					case ECC:
						ECDsa ecc = rootCA.GetECDsaPrivateKey(); // or cert.GetECDsaPublicKey() when need public key
						if (ecc is ECDsaCng ecdsaCng)
						{
							ecdsaCng.Key.SetProperty(s_fullyExportableProperty);
						}

						break;
				}

				return rootCA;
			}
		}

@bartonjs
Copy link
Member

As soon as you export the certificate as bytes, the .ExportParameters(true) will fail.

No, it's the import, not the export. The certificate instance that you exported from can still export its key, because it's an ephemeral key and ephemeral keys are plaintext exportable. The property sets you did are on the ephemeral key, don't get saved into the PFX, and thus have been discarded when the key is re-imported.

While I recall being surprised that the exportable property could be changed between import and first use, it looks like I'm now surprised that it doesn't (as just tested in net48 on Win11).

So now looks like I get to fall back on answers I've given on StackOverflow. Namely, https://stackoverflow.com/questions/57269726/x509certificate2-import-with-ncrypt-allow-plaintext-export-flag/57330499#57330499

@BethanyZhou
Copy link
Author

Sorry, I got lost. How can I export certificate data in my case?

@bartonjs
Copy link
Member

@BethanyZhou The easiest way is to fully move onto .NET Core/.NET 5+ and use the new encrypted exports. Failing that, you'd need to copy/paste all of the code from my StackOverflow answer (https://stackoverflow.com/questions/57269726/x509certificate2-import-with-ncrypt-allow-plaintext-export-flag/57330499#57330499), which will overwrite the key with a plaintext-exportable version of itself (and then calls to ExportParameters(true) will succeed).

@BethanyZhou
Copy link
Author

BethanyZhou commented Nov 23, 2022

Thanks for your patient. I use following snippet to let certificate support exportParameters(true) on dotnet standard 2.0

            byte[] certificateBytes = File.ReadAllBytes(fileName);
            // ImportExportable() is from your SO answer
             X509Certificate2 certificate = ImportExportable(certificateBytes, password, machineScope)
            ...
            var rsa= certificate.GetRSAPrivateKey();
            ...
            RSAParameters rsaParameters = rsa.ExportParameters(true);
            ...

Current solution works for me now. Just want to confirm two points:

  • Is this work around applicable for other OSs like Linux and MacOs
  • Is this work around applicable for EC key as well?

@vcsjones
Copy link
Member

Is this work around applicable for other OSs like Linux and MacOs

No. This issue is specific to Windows only. The work around makes use of APIs that are only on Windows and will fail on other platforms. You need to avoid the work around when not on Windows.

Is this work around applicable for EC key as well?

Yes. The issue applies to both RSA and EC keys. Probably DSA keys too, but DSA is largely irrelevant.

Since Jeremy's fix uses PKCS8 at first glance the work around should work for all keys types.

@BethanyZhou
Copy link
Author

BethanyZhou commented Nov 24, 2022

Much thanks for your quick responses @vcsjones .

Could you help me understand the basic principles of Jeremy's fix? Specially, what is internal static class NativeMethods for? Understanding is critical for the long-term maintenance of our project. Any comments for code is really helpful.

Appreciate again @vcsjones @bartonjs

@vcsjones
Copy link
Member

Could you help me understand the basic principles of Jeremy's fix?

The general problem is, when the PFX was imported, Windows likes to put it in a situation where the private CNG key can only be exported encrypted.

So the work around for this in .NET Core / .NET is, export the key encrypted. Then import the encrypted key again, but this time with a special bit on it that means "it's okay to export this key unencrypted". Then we can finally export the key unencrypted.

The problem with .NET Standard 2.0, is there isn't an API than can export a CNG encrypted. The ExportParameters API, by design, gives you back unencrypted private key data.

So Jeremy basically implemented the PKCS8 export and import again. .NET relies heavily on CNG, a set of native APIs available only available on Windows.

The work around starts by constructing an X509Certificate2 object as you normally would.

Then it uses the WinCrypt CryptAcquireCertificatePrivateKey API to get a handle to the certificate's private key as a CNG handle. It then passes this handle to CngKey.Open, which is a .NET API specifically meant to represent a CNG key.

Once it has a CngKey, it has a helpers, ExportEncryptedPkcs8, to do the PKCS8 export. Since .NET Standard doesn't have APIs for this, it again makes use of Windows' capabilities. It exports it encrypted using a dummy password. The strength of the password and the encryption for the PKCS8 export doesn't really matter - after all, we are trying to export it entirely unencrypted anyway. So it exports the private key in encrypted PKCS8 form. The password is literally "AcquireCertificateKeyOptions". This export does some set up to indicate what the password is, what the algorithm is, and how many rounds from the KDF. Ultimately, it calls the Win32 API NCryptExportKey.

So now we have a the private key exported. But it's encrypted, so we can't do what we want with it, yet.

The next step is to import it again, expect this time, specifically saying "it's fine to export this unencrypted". This is done in ImportEncryptedPkcs8Overwrite.

This basically does everything in reverse. It uses NCryptImportKey to import the key. But crucially, before the key is finalized, it opens it as a CngKey again, and tells the key to have the export policy AllowPlaintextExport. Then it calls NCryptFinalizeKey, which is to mean that the key's properties have been property set, and now the key is ready for use.

When the private key gets re-imported, it does so with the same name as the unexportable key. So it replaces the unexportable key with an exportable one.

After it has been replaced with the same name, the X509Certificate2 has the new key associated with it that is fully exportable. So GetRSAPrivateKey() (and ECdsa and etc) give back the exportable key. Then ExportParameters works as you expect it.

@BethanyZhou
Copy link
Author

Thank you @vcsjones and @bartonjs for your help. Close this issue now.

@ghost ghost locked as resolved and limited conversation to collaborators Dec 24, 2022
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

No branches or pull requests

4 participants