Unable to export RSA private parameters when running as administrator - c#

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

Related

X509Certificate2.Export fails with "Keyset does not exist" in console application

I do not have deep knowledge of how storing and working with certificates from within code should be done "properly", so will use rather "simple language" to explain my problem.
I have certificate, which was generated by code (using Bouncy Castle), actually this is the code which was used to create certificate.
The code returns Org.BouncyCastle.X509.X509Certificate certificate.
This certificate object is then converted into byte[] array, using Pkcs12Store (Bouncy Castle class) and saved into c:\temp\cert.pfx file.
Now I need to load the certificate from file into code, update Cryptographic Service Provider (CSP) (which is not correct) on the private key and write the certificate and private key to new cert2.pfx file.
UPDATE
On the first run my console app would went through, and all other runs I will get error "Keyset does not exist" on this line: certificate.Export(X509ContentType.Pkcs12, "password")
NOTES:
The code is running in Console application and folder c:\temp\ got full permissions for Everyone.
Most topics related to "Keyset does not exist" are related to permissions (e.g. Network Service does not have right access), I suppose this is not my case.
I haven't imported cert.pfx into windows store (Personal nor Local Machine) and prefer not to import it.
This is code sample:
var certificate = new X509Certificate2(certFileBytes, "password", X509KeyStorageFlags.Exportable);
var privKey = certificate.PrivateKey as RSACryptoServiceProvider;
// will be needed later
var exported = privKey.ToXmlString(true);
// change CSP
var cspParams = new CspParameters()
{
ProviderType = 24,
ProviderName = "Microsoft Enhanced RSA and AES Cryptographic Provider"
};
var rule = new CryptoKeyAccessRule("everyone", CryptoKeyRights.FullControl, AccessControlType.Allow);
cspParams.CryptoKeySecurity = new CryptoKeySecurity();
cspParams.CryptoKeySecurity.SetAccessRule(rule);
// create new PrivateKey from CspParameters and exported privkey
var newPrivKey = new RSACryptoServiceProvider(cspParams);
newPrivKey.PersistKeyInCsp = true;
newPrivKey.FromXmlString(exported);
// Assign edited private key back
certificate.PrivateKey = newPrivKey;
// export as PKCS#12/PFX
var bytes = certificate.Export(X509ContentType.Pkcs12, "password");
File.WriteAllBytes(#"c:\temp\cert2.pfx", bytes);
Thanks.
I ran into this problem as well as using the Export-PfxCertificate cmdlet (it gave a similar error). Running the powershell window as admin resolved the problem.

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.

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

Encrypted payload signature different when generated in .NET on different OS (Win7 vs Win2008R2)

A developer where I work is running into some difficulty using a certificate's private key to sign a payload on our production server. The code works on both his development box and the production server, but the two different locations end up with a different signature for the same payload. We've confirmed that it's the same certificate in both locations, but for some reason, the RSACryptoServiceProvider.SignData method seems to return a different value depending on whether it's being run on Windows 7 or Server 2008 R2.
Here's the code we're using - you can see that we've replaced the payload with a Base64 string from our config file, so it's not even possible that it could be a difference in the payload that's causing the different signatures.
byte[] encryptedSignature = new byte[1];
CspParameters cp = new CspParameters(24, "Microsoft Enhanced RSA and AES Cryptographic Provider", "{4FC30434-29E5-482D-B817-72102A046137}");
cp.Flags = CspProviderFlags.UseMachineKeyStore;
cp.KeyNumber = (int)KeyNumber.Exchange;
bool signatureVerified = false;
using (RSACryptoServiceProvider rsaCrypto = new RSACryptoServiceProvider(2048, cp))
{
encryptedSignature = rsaCrypto.SignData(Convert.FromBase64String(Properties.Settings.Default.EncryptedSessionData), "SHA256");
// Signature verifies properly on both servers
signatureVerified = rsaCrypto.VerifyData(Convert.FromBase64String(Properties.Settings.Default.EncryptedSessionData), "SHA256", encryptedSignature);
}
Is it possible that the server, an x64 box, could be handling the signature differently than our x86 development workstations? It doesn't seem likely, but we're stumped as to why the production server would produce a different signature. Also, not sure if it matters, but this code comes from an ASP.NET page where we create the payload and then forward the visitor to a vendor's page along with the encrypted payload.
Hopefully somebody has some light to shed here or has seen something similar.
Change
CspParameters(24, "Microsoft Enhanced RSA and AES Cryptographic Provider", "{4FC30434-29E5-482D-B817-72102A046137}");
to
CspParameters cp = new CspParameters(24, "Microsoft Enhanced RSA and AES Cryptographic Provider", ((RSACryptoServiceProvider)cert.PrivateKey).CspKeyContainerInfo.KeyContainerName);
that will get the ContainerName from the PrivateKey.
Nice avatar pic ;)
Random Padding is NOT used in PKCS1 V 1.5 SIGNATURE (EMSA-PKCS1-v1_5 as stated in PKCS1 RSA document). It is used only in ENCRYPTION (ie Encryption using Public Keys)
RSA employs random padding. You can't guarantee two signatures of the same plain text be the same.
try to verify the different signatures against the public key ... as long as both signatures are valid, there is no problem

Categories