Private Key Permissions Breaking on Program Exit - c#

I have a small application as a 'certificate manager' to a larger program, so that users won't have to manually install and configure certificates.
It's fairly simple - it has some certificates as Embedded Resources, which it loads into the appropriate stores, then sets appropriately configured permissions.
This appears to work correctly while the program is running. Using MMC, I can see that the certificate is installed. If I Manage Private Keys, it has a new permission added correctly. However, as soon as I close the certificate manager, the permissions break. The certificate is still installed, but hitting Manage Private Keys pops up an error similar to "Key does not exist."
Additionally, if the program is run a second time, the permissions will 'stick' correctly after the program exits.
Here's code which the program uses to verify that the permissions were added. This method returns 'true' every time, even when the permissions break afterwards.
private bool GetSecurityStatus(X509Certificate2 cert, X509Store store)
{
store.Open(OpenFlags.ReadOnly);
//add Authenticated Users to private cert
RSACryptoServiceProvider privKeyRSA = cert.PrivateKey as RSACryptoServiceProvider;
string keyFilePath = FindKeyLocation(privKeyRSA.CspKeyContainerInfo.UniqueKeyContainerName);
FileInfo privateKeyFileInfo = new FileInfo(keyFilePath + "\\" + privKeyRSA.CspKeyContainerInfo.UniqueKeyContainerName);
FileSecurity privateKeyFileSecurity = privateKeyFileInfo.GetAccessControl();
AuthorizationRuleCollection rules = privateKeyFileSecurity.GetAccessRules(true, true, typeof(NTAccount));
foreach (FileSystemAccessRule fsar in rules)
{
if(fsar.IdentityReference.Value.Contains("Authenticated Users") && fsar.AccessControlType == AccessControlType.Allow && fsar.FileSystemRights == FileSystemRights.FullControl){
store.Close();return true;
}
}
//Close Private Cert store
store.Close();
return false;
}
The FindKeyLocation returns the appdata\Microsoft\Crypto\RSA\ path of the private key.
I'm thinking it has to do somehow with the exiting of the program altering the private key file itself, but I'm unsure why it would then work the second time.

I believe I've found a solution to my issue, based on the respones here:
Import certificate with private key programmatically
and from MSDN here
https://support.microsoft.com/en-us/help/950090/installing-a-pfx-file-using-x509certificate-from-a-standard-.net-application
I had to pass two flags within the certificate's parameters, instead of just MachineKey
X509KeyStorageFlags.MachineKeySet | X509KeyStorageFlags.PersistKeySet

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

.NET/C# reading certificate from store cause AccessViolationException

I've getting very strange code exception, which is should work but not work.
I have following code:
X509Certificate certificate;
X509CertificateStore store = X509CertificateStore.LocalMachineStore(X509CertificateStore.MyStore);
store.OpenRead();
var certs = new X509CertificateCollection();
Microsoft.Web.Services2.Security.X509.X509CertificateCollection findCertificateBySubjectName = store.FindCertificateBySubjectName("localhost");
foreach (X509Certificate tempCertificate in store.Certificates)
{
if (String.CompareOrdinal(tempCertificate.Subject, "") == 0)
{
certs.Add(tempCertificate);
}
}
Current code runs inside w3wp process under admin rights. But I get exception which you can see on screenshot. I tried many things, give all access to all certificates, change users, reimport certificate.
I will be appreciate for any help or assistance.

Securely Storing Database Password

I'd like to randomly generate an encryption key and password for an SQL Server CE database when it's created, and then save the key in some secure way that would allow the program to open a connection, but not be easily reachable by potential attackers.
I'm working on an offline WPF application that stores certain user and setting information in a local database.
My current implementation is to have one "Device Password" that the user sets up which is used as the encryption key for the generated SQL Server CE database password. The base64 encrypted database password is then saved in a simple .txt settings file. When the application starts up, the user enters the Device Password and that string is used as the decryption key for the saved password. If the resulting string is able to open a connection to the database, the password was correct and the program is opened with full access.
What I'm trying to do now is modify the system to allow multiple users with specific Username/Password credentials to open the program and access the database with varying levels of privilege. The way that I'm trying to achieve this is by handling the user authentication separately, and opening the database regardless of the credentials to load some basic application info.
Below is roughly my current implementation:
var candidateDBPwd = DecryptDatabasePassword(passwordBox.Password, Settings.Instance.EncryptedDatabasePassword);
if (candidateDBPwd.IsNullOrEmpty())
{
// User's password didn't decrypt database password.
return false;
}
if (File.Exists(Constants.DB_FILE))
{
// Normal operation: Try to open the database file to see that
// we've got the correct password.
string databaseArguments = Constants.DB_ARGS_SECURE + candidateDBPwd;
using (var conn = new SqlCeConnection(databaseArguments))
{
try
{
conn.Open();
}
catch (System.Data.SqlServerCe.SqlCeException ex)
{
// Failed to open the database: User's password must have been wrong!
return false;
}
}
I've spent the past few hours researching similar issues and am now beginning to wonder if it's possible. Consensus seems to state that storing passwords or connectionStrings in the App.config file is futile because if you encrypt the sections, you still need to store that key somewhere in code. Most of the existing SO threads on the issue seem to be several years out of date and it seems that that practice has deprecated. Is there some new respectable way to store a local database password? Or would you recommend a different approach to implementing the feature?
For you information here is the code snippet that can be used to encrypt certain sections of app.config. This is machine specific encryption and I think it is most simple and straightforward way to go.
I am using this with Click-once app, so that the config sections are encrypted during the first launch of the app. It means, that it is unecrypted on the publish server, it is downloaded also unencrypted and it is encrypted right after the installation finishes and application is started.
So using this method you have to distribute your files unencrypted and they are enrypted only after the installation is completed. I suppose it can be achieved by running this code during install, it depends on how you plan to install your app.
Also you can use UnprotectSection() to unencrypt previously encrypted section.
static void EncryptConfig()
{
// Encrypt config for ClickOnce deploys on first run
// ClickOnce deploys config into 2 dirs, so the parent dir is traversed to encrypt all
if (ApplicationDeployment.IsNetworkDeployed)
{
// Get paths
Assembly asm = Assembly.GetExecutingAssembly();
string exeName = Path.GetFileName(asm.Location);
string configName = exeName + ".config";
DirectoryInfo parentPath = Directory.GetParent(Directory.GetCurrentDirectory());
// Protect config files
foreach (DirectoryInfo dir in parentPath.GetDirectories())
{
foreach (FileInfo fil in dir.GetFiles())
{
if (fil.Name == configName)
{
ExeConfigurationFileMap fileMap = new ExeConfigurationFileMap();
fileMap.ExeConfigFilename = fil.FullName;
Configuration config = ConfigurationManager.OpenMappedExeConfiguration(fileMap, ConfigurationUserLevel.None);
ProtectSection(config, "connectionStrings");
config.Save(ConfigurationSaveMode.Modified);
}
}
}
}
}
private static void ProtectSection(Configuration config, string sectionName)
{
ConfigurationSection section = config.GetSection(sectionName);
if (section != null)
{
if (!section.SectionInformation.IsProtected)
{
section.SectionInformation.ProtectSection("DataProtectionConfigurationProvider");
}
section.SectionInformation.ForceSave = true;
}
else
Tools.LogWarning("Section {1} not found in {0}.",config.FilePath, sectionName);
}
You can store it in registry editor. You mention that your system is offline wpf application .

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

Categories