Search code examples
certificatelets-encryptprivate-key.net-5

.NET 5 fails to export letsencrypt certificate private key


I have generated Let's encrypt certificate stored in LocalMachine\My. Mail server must have fresh certificate every 90 days. My program should take every 30 days last valid certificate, export keys and reboot service.

I am facing problem during exporting private key. Let's encrypt certificate is not password protected.

My error:

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) at 
System.Security.Cryptography.RSA.WritePkcs1PrivateKey() at 
System.Security.Cryptography.RSA.ExportRSAPrivateKey()

Code:

try
{
    using X509Store store = new X509Store(StoreName.My, StoreLocation.LocalMachine);

    try
    {
        store.Open(OpenFlags.OpenExistingOnly);

        X509Certificate2Collection collection = store.Certificates.Find(X509FindType.FindBySubjectName, "my.domain.name", true);

        X509Certificate2 certificate = null;

        foreach(X509Certificate2 item in collection)
        {
            if (certificate is null)
                certificate = item;
            else if (item.NotAfter > certificate.NotAfter)
                certificate = item;
        }

        if(certificate is null)
        {
            logger.LogError($"Certificate not found.");

            return;
        }
                    

        byte[] certificateBytes = certificate.RawData;
        char[] certificatePem = PemEncoding.Write("CERTIFICATE", certificateBytes);

        RSA key = certificate.GetRSAPrivateKey();

        byte[] privKeyBytes = key.ExportRSAPrivateKey(); //key.ExportPkcs8PrivateKey(); // CRASH
        char[] privKeyPem = PemEncoding.Write("PRIVATE KEY", privKeyBytes);

        string privateKey = $"{new string(privKeyPem).Replace("\r\n\n", "\n")}\n";
        string certificateKey = $"{new string(certificatePem).Replace("\r\n\n", "\n")}\n";

        await File.WriteAllTextAsync("mypath\\key.private", privateKey);
        await File.WriteAllTextAsync("mypath\\key.public", certificateKey);

        // service reboot....
    }
    catch (CryptographicException ex)
    {
        logger.LogError($"Certificate error - {ex}");
    }
    finally
    {
        store.Close();
    }
}
catch(Exception ex)
{
    logger.LogError(ex.ToString());
}

Program is runned by user which has Read,Write,Modify,List permissions in C:\Program Data\Microsoft\Crypto\RSA\MachineKeys

.net version 5.0.3

target os: windows server 2016


Solution

  • This is because of a quirk in Windows where private keys in CNG have two notions of exportability (plaintext exportable vs encrypted exportable). It's an easy enough workaround:

    PbeParameters simplePbe = new PbeParameters(
        PbeEncryptionAlgorithm.TripleDes3KeyPkcs12,
        HashAlgorithmName.SHA1,
        1);
    
    byte[] = key.ExportEncryptedPkcs8PrivateKey("temp", simplePbe);
    
    using (RSA temp = RSA.Create())
    {
        temp.ImportEncryptedPkcs8PrivateKey("temp", exported, out _);
        privKeyBytes = temp.ExportPkcs8PrivateKey();
    }
    
    char[] privKeyPem = PemEncoding.Write("PRIVATE KEY", privKeyBytes);