Slow performance of X509Certificate2.PrivateKey - c#

I'm loading a certificate from the certificate store. So I have a X509Certificate2 object.
Next I load the private key which I use for signing a message. The code I'm using is:
public static RSACryptoServiceProvider GetPrivateKey(X509Certificate2 cert)
{
var enhCsp = new RSACryptoServiceProvider().CspKeyContainerInfo;
var privateKey = (RSACryptoServiceProvider)cert.PrivateKey; //very slow
var cspparams = new CspParameters(enhCsp.ProviderType, enhCsp.ProviderName, privateKey.CspKeyContainerInfo.KeyContainerName);
return new RSACryptoServiceProvider(cspparams);
}
The issue I'm having is that the line:
var privateKey = (RSACryptoServiceProvider)cert.PrivateKey;
takes very very long. Around 9 seconds on my laptop. It would seem that this does not happen every time I try to access the private key. Sometimes it's in the order of milliseconds and other times it takes as long as described.
Any insight into why? Is there a way to avoid that?
UPDATE:
I've been experimenting a bit after noticing this KB article and some other SO issues.
It really seems like that delay is never there when there is no network connectivity.
I wonder if I really have to go decompiling .NET assemblies to sort this out...
UPDATE 1:
It appears as though certificates/key pairs that are issued by a real authority are not affected. I'm seeing this issue when trying to load private key for issuers which I've manually added to the trusted root certification authorities

Related

Unable to export RSA private parameters when running as administrator

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 :(
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();
}
}

C# Certificate Renewal Request

The code below tries to renew existing certificate.
The certificate is renewed, but new public/private key is generated despite that the option X509RequestInheritOptions.InheritPrivateKey is specified.
What is wrong in the code below, because the intention was to keep the existing private key?
In the certficates management console, I can renew the certificate and keep the exisintg private key.
string certificateSerial = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx";
X509Certificate certificate = getCertificate(certificateSerial);
var objPkcs7 = new CX509CertificateRequestPkcs7();
objPkcs7.InitializeFromCertificate(X509CertificateEnrollmentContext.ContextUser, true,
Convert.ToBase64String(enrollmentAgentCertificate.GetRawCertData()),
EncodingType.XCN_CRYPT_STRING_BASE64,
X509RequestInheritOptions.InheritPrivateKey & X509RequestInheritOptions.InheritValidityPeriodFlag);
IX509Enrollment ca = new CX509EnrollmentClass();
ca.InitializeFromRequest(objPkcs7);
ca.Enroll();
Thanks
It seems the problem is in the MSDN documentation:
https://msdn.microsoft.com/en-us/library/windows/desktop/aa379430%28v=vs.85%29.aspx
The page states: "..You can also use a bitwise-AND operation to combine the key inheritance choice with InheritNone or with any combination of the following flags...".
However, if we use bitwise-AND between InheritPrivateKey = 0x00000003 and InheritValidityPeriodFlag= 0x00000400 we get 0 which is InheritDefault (i.e. no private key inheritance)
For my use case we need to use bitwise-OR. It seems the C++ SDK example does the same:
https://github.com/theonlylawislove/WindowsSDK7-Samples/blob/master/security/x509%20certificate%20enrollment/vc/enrollpkcs7/enrollPKCS7.cpp
hr = pPkcs7->InitializeFromCertificate(
ContextUser,VARIANT_FALSE, strOldCert,
XCN_CRYPT_STRING_BINARY,
(X509RequestInheritOptions)(InheritPrivateKey|InheritTemplateFlag));
In that context the code above shall be modified as:
X509RequestInheritOptions.InheritPrivateKey | X509RequestInheritOptions.InheritTemplateFlag);

Read Private Key from PFX-file

I know, there are many posts about this, but still I cannot find a solution to get this to work. I have generated a PFX-file with openssl on my machine like this:
openssl x509 -req -days 365 -in "myReqest.csr" -signkey "myPrivateKey.pem" -out "myCertificate.crt"
openssl pkcs12 -export -out "myCertificate.pfx" -inkey "myPrivateKey.pem" -in "myCertificate.crt" -certfile "myCertificate.crt"
In my C# app, I access the private key like this:
var cert = new X509Certificate2("myCertificate.pfx", "myPassword");
cert.HasPrivateKey; // This is always true!
cert.PrivateKey; // Works on my machine (only)
This works perfectly fine (on my machine), but when I run the same code on another machine, it throws: "Key set not found", even though HasPrivateKey returns true! Shouldn't the private key be included in the *.pfx-file? Can you tell me:
Was the certificate/private key somehow automatically installed on my machine by openssl when I created it?
How can I read the private key from the *.PFX-file (or alternatively from the *.PEM-file)?
StackTrace of Exception:
at System.Security.Cryptography.Utils.CreateProvHandle(CspParameters parameters, Boolean randomKeyContaier)
at System.Security.Cryptography.Utils.GetKeyPairHelper(CspAlgorithmType keyType, CspParameters parameters, Boolean randomKeyContaier, Int32 dwKeySize, SafeProvHandle& safeProvHandle, SafeKeyHandle& safeKeyHandle)
at System.Security.Cryptography.RSACryptoServiceProvider.GetKeyPair()
at System.Security.Cryptography.RSACryptoServiceProvider..ctor(Int32 dwKeySize, CspParameters parameters, Boolean useDefaultKeySize)
at System.Security.Cryptography.RSACryptoServiceProvider..ctor(CspParameters parameter)
at System.Security.Cryptography.X509Certificates.X509Certificate2.get_PrivateKey()
Update:
I've found out, that the following does work:
// on my machine
// read certificate from file (exportable!)
X509Certificate2 cert = new X509Certificate2("filename.pfx", "password", X509KeyStorageFlags.Exportable)
// sign data etc.
((RSACryptoServiceProvider)cert.PrivateKey).SignData(...
// export private key to XML-file
File.WriteAllText("filename.xml", cert.PrivateKey.ToXmlString(true));
// on the other machine
// create new RSA object
RSACryptoServiceProvider rsa = new RSACryptoServiceProvider();
// import private key from xml
rsa.FromXmlString(File.ReadAllText("filename.xml"));
// verify data etc.
rsa.VerifyData(...
However, to me, this is only a workaround, I would like to do it an a more conventional/standard compliant way!
You have to load the certificate like this:
X509Certificate2 cert = new X509Certificate2("a.pfx", "password", X509KeyStorageFlags.MachineKeySet | X509KeyStorageFlags.PersistKeySet)
This post explains why http://paulstovell.com/blog/x509certificate2
It seems, there is no straight-forward way to do this in .NET. Therefore I've decided now to load the certificate directly from the certificate store:
X509Store store = new X509Store(StoreName.My, StoreLocation.CurrentUser);
store.Open(OpenFlags.OpenExistingOnly | OpenFlags.ReadOnly);
X509Certificate2Collection certificates = store.Certificates.Find(X509FindType.FindByThumbprint, CERTIFICATE_THUMB_PRINT, false);
if (certificates.Count == 0)
{
// "Certificate not installed."
}
else
{
certificate = certificates[0];
}
store.Close();
For that, of course, if has to be installed on the machine.
I think this is a nice solution to this problem, because it adds an additional layer of security to it (the app must be run on the machine, on which the certificate is installed and as the user who installed it, also the file itself can be stored in a safe place somewhere else).
Perhaps you can tell us a bit more about why you want to do this. There certainly are good reasons to want to do this, such as writing a program that would sit on your company's internal server to automate product builds.
But if you intend to distribute this application outside of a high trust zone (e.g. to customers) then the answer is DO NOT DO IT! You should never give out your private key file. That opens it up to brute force attack of your password, (which is certainly much weaker than the private key itself). And if you distribute your application, then your password is included in plain text in the MSIL code. There it can be easily viewed using any managed code disassembler (e.g. Reflector) , and it can probably even be viewed with a text or hex editor.
In summary, distributing your private key file along with your application to someone allows them to easily sign anything they want with YOUR cert. The whole point of a private key is to keep it safely locked away in a location where it can never be accessed by anyone but you (or your organization, etc).
The default key set doesn't exist on the other machine (The user key set is usually the default), probably because it's an asp.net application (i.e. it has no user profile). If you pass X509KeyStorageFlags.MachineKeySet as the third argument to the X509Certificate2 constructor, then it should work in the same way on both machines.
The reason it happens only when accessing the PrivateKey property is that it's the first place where an actual CSP object is created to use the key.
This worked for me:
// Loading the pfx file with passphrase
x509certificate2 cert = new x509certificate2("d:\\mycer.pfx", "123456789", x509keystorageflags.exportable);
// Getting the private key from the pfx file
// https://www.asptricks.net/2016/09/how-to-export-private-key-from.html
RSACryptoServiceProvider rsa = (RSACryptoServiceProvider)cert.PrivateKey;
AsymmetricCipherKeyPair keyPair = DotNetUtilities.GetRsaKeyPair(rsa);
var myCAprivateKey = keyPair.Private;
You can also obtain the private through the below method.
System.Security.Cryptography.X509Certificates.X509Certificate2 certificate = LoadCertificate("Certificate.pfx", "PasswordofCertificate");
RSACryptoServiceProvider key = certificate.PrivateKey as RSACryptoServiceProvider;
From certificate variable, you can also obtain other information such as Public Key etc.

Loading X509Certificate results in exception CryptographicException "Cannot find the original signer"

I am trying to instantiate an X509Certificate object, but I keep running into the same CryptographicException, with the error message:
"Cannot find the original signer".
I have tried several ways of loading the certificate:
// some different things that I have tried
var cert = X509Certificate.CreateFromCertFile(certFileName);
var cert2 = new X509Certificate(byteArray);
var cert3 = new X509Certificate(byteArray, secretString);
var cert4 = X509Certificate.CreateFromSignedFile(certFileName);
I have tried both loading from a file and from a byte array. Each time I get the same error. What could be causing this?
I figured out the problem. I was attempting to load just the certificate file, which did not include the private key. To fix the problem, I had to install the private key on the machine on which the certificate was purchased, and then export it as a .pfx file and move it to where I actually wanted to use it. I'm sure was a newbie mistake. Hopefully, my silly issue will help other newbies in the future.
var collection = new X509Certificate2Collection();
collection.Import(byteArray);
return collection;
via https://stackoverflow.com/a/44073265, by https://stackoverflow.com/users/6535399, who writes crypto for msft - e.g. https://github.com/dotnet/corefx/pull/25920

Private key of certificate in certificate-store not readable

I think I've got the same issue like this guy, but I wasn't as lucky as him/her since the solution provided doesn't work for me.
The solution provided looks for files on the C:\ProgramData\Microsoft\Crypto\RSA\MachineKeys (not in sub directories) and C:\Users\[Username]\AppData\Roaming\Microsoft\Crypto\RSA (and subdirectories)
However since I want the setup to install the application to all users, the custom action is running under the SYSTEM-User, which leads the files beeing actually created in C:\ProgramData\Application Data\Microsoft\Crypto\RSA\S-1-5-18.
When running an "normal" application as Admin (right click -> Run as Admin) executing exactly the same code, a file is created at C:\Users\[Username]\AppData\Roaming\Microsoft\Crypto\RSA\S-1-5-21-1154405193-2177794320-4133247715-1000.
The certificate generated using the WIX custom action seems to not have a private key ("The key set does not exists"), while the cert generated by the "normal" application does.
When looking at the permissions of the files, they seem to be alright, even if they differ (the working one does include the SYSTEM user), even after adding the SYSTEM one to the ("non-working")file I am not able to read the private key, same error here.
Then I used the FindPrivateKey util the find the corresponding file but all I get is "Unable to obtain private key file name".
Ok whats going one here? Where does Windows store the private keys for certificates stored by the SYSTEM user? Maybe there isn't any private key file created? Why?
I got a solution by googleing up nearly everything ... as I understand there are some things to do:
Generate a X509Certificate2
Make sure the private key container is persistent (not temporary)
Make sure to have acccess rules for authenticated users, so they can see the private key
So the final code a came up with is the following:
X509Certificate2 nonPersistentCert = CreateACertSomehow();
// this is only required since there's no constructor for X509Certificate2 that uses X509KeyStorageFlags but a password
// so we create a tmp password, which is not reqired to be secure since it's only used in memory
// and the private key will be included (plain) in the final cert anyway
const string TMP_PFX_PASSWORD = "password";
// create a pfx in memory ...
byte[] nonPersistentCertPfxBytes = nonPersistentCert.Export(X509ContentType.Pfx, TMP_PFX_PASSWORD);
// ... to get an X509Certificate2 object with the X509KeyStorageFlags.PersistKeySet flag set
X509Certificate2 serverCert = new X509Certificate2(nonPersistentCertPfxBytes, TMP_PFX_PASSWORD,
X509KeyStorageFlags.PersistKeySet | X509KeyStorageFlags.MachineKeySet | X509KeyStorageFlags.Exportable); // use X509KeyStorageFlags.Exportable only if you want the private key to tbe exportable
// get the private key, which currently only the SYSTEM-User has access to
RSACryptoServiceProvider systemUserOnlyReadablePrivateKey = serverCert.PrivateKey as RSACryptoServiceProvider;
// create cspParameters
CspParameters cspParameters = new CspParameters(systemUserOnlyReadablePrivateKey.CspKeyContainerInfo.ProviderType,
systemUserOnlyReadablePrivateKey.CspKeyContainerInfo.ProviderName,
systemUserOnlyReadablePrivateKey.CspKeyContainerInfo.KeyContainerName)
{
// CspProviderFlags.UseArchivableKey means the key is exportable, if you don't want that use CspProviderFlags.UseExistingKey instead
Flags = CspProviderFlags.UseMachineKeyStore | CspProviderFlags.UseArchivableKey,
CryptoKeySecurity = systemUserOnlyReadablePrivateKey.CspKeyContainerInfo.CryptoKeySecurity
};
// add the access rules
cspParameters.CryptoKeySecurity.AddAccessRule(new CryptoKeyAccessRule(new SecurityIdentifier(WellKnownSidType.AuthenticatedUserSid, null), CryptoKeyRights.GenericRead, AccessControlType.Allow));
// create a new RSACryptoServiceProvider from the cspParameters and assign that as the private key
RSACryptoServiceProvider allUsersReadablePrivateKey = new RSACryptoServiceProvider(cspParameters);
serverCert.PrivateKey = allUsersReadablePrivateKey;
// finally place it into the cert store
X509Store rootStore = new X509Store(StoreName.My, StoreLocation.LocalMachine);
rootStore.Open(OpenFlags.ReadWrite);
rootStore.Add(serverCert);
rootStore.Close();
// :)

Categories