Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

.NET Standard - Merge a certificate and a private key into a .pfx file programmatically

Is there any way to combine a certificate and a key (both received separately as Base64 strings from a service) into a .pfx file using C#/.NET Standard programmatically? Using a tool is not an option, since I need to automate it.

The context: I need to load a certificate and a private key (separate Base64 strings without headers and footers) to a X509Certificate2 object to pass it on to the app from a .NET Standard 1.6 library. The problem is, there is no PrivateKey property in the X509Certificate2 class in .NET Standard! So the only way for me to actually load the private key in a X509Certificate2 object is to combine it with the certificate itself in a .pfx file and load it like that in a constructor.

like image 375
Maxim Avatar asked Jun 09 '17 19:06

Maxim


1 Answers

There's not a way to do this with framework types. It may be possible with BouncyCastle, or other libraries.

.NET Core 2.0 has added the ability to merge a certificate and a key object together (into a new X509Certificate2 object) via extension methods:

X509Certificate2 mergedCert = cert.CopyWithPrivateKey(rsaPrivateKey);
X509Certificate2 mergedCert = cert.CopyWithPrivateKey(dsaPrivateKey);
X509Certificate2 mergedCert = cert.CopyWithPrivateKey(ecdsaPrivateKey);

But that requires compiling specifically for netcoreapp20 (not netstandard20).

The framework types also don't have a way of loading key objects from binary representations (with the exception of CngKey.Import, but that only works on Windows), only from the pre-parsed structures (RSAParameters, DSAParameters, ECParameters).

The easiest way to accomplish this goal on Linux (if BouncyCastle can't help you out) is to use System.Process to spawn a call similar to openssl pkcs12 -export -out tmp.pfx -in tmp.cer -inkey tmp.key -password pass:"".

On Windows you could maybe use CngKey.Import and P/Invoke CertSetCertificateContextProperty (for CERT_NCRYPT_KEY_HANDLE_PROP_ID (78)) to then call cert.Export on the mutated certificate.


Update (2020-09-30): With .NET 3.0 this is reasonably straightforward (doesn't answer the original question of "with .NET Standard", since it requires compiling for netcoreapp3.0 or higher), and it's easy to also add support for PEM-encoded keys with .NET 5.0.

This code checks to see if the key file is PEM encoded (instead of binary/DER encoded), using the .NET 5.0 PemEncoding class, then loads the private key using supported formats, matches the private key up to the certificate, then exports. The key import methods were all added in .NET Core 3.0.

private enum KeyFileKinds
{
    None = 0,
    Pkcs8,
    EncryptedPkcs8,
    RsaPrivateKey,
    Any = -1,
}

public static byte[] MakePfx(string certPath, string keyPath, string exportPassword)
{
    using X509Certificate2 cert = new X509Certificate2(certPath);
    byte[] keyBytes;
    KeyFileKinds kinds;
    ReadOnlySpan<char> keyFileText = File.ReadAllText(keyPath).AsSpan();

    // PemEncoding.TryFind requires net5.0+
    if (PemEncoding.TryFind(keyFileText, out PemFields pemFields))
    {
        keyBytes = new byte[pemFields.DecodedDataLength];

        if (!Convert.TryFromBase64Chars(keyFileText[pemFields.Base64Data], keyBytes, out int written) ||
            written != keyBytes.Length)
        {
            Debug.Fail("PemEncoding.TryFind and Convert.TryFromBase64Chars disagree on Base64 encoding");
            throw new InvalidOperationException();
        }

        ReadOnlySpan<char> label = keyFileText[pemFields.Label];

        if (label.SequenceEqual("PRIVATE KEY"))
        {
            kinds = KeyFileKinds.Pkcs8;
        }
        else if (label.SequenceEqual("ENCRYPTED PRIVATE KEY"))
        {
            kinds = KeyFileKinds.EncryptedPkcs8;
        }
        else if (label.SequenceEqual("RSA PRIVATE KEY"))
        {
            kinds = KeyFileKinds.RsaPrivateKey;
        }
        else
        {
            throw new NotSupportedException($"The PEM file type '{label.ToString()}' is not supported.");
        }
    }
    else
    {
        kinds = KeyFileKinds.Any;
        keyBytes = File.ReadAllBytes(keyPath);
    }

    RSA rsa = null;
    ECDsa ecdsa = null;
    DSA dsa = null;

    switch (cert.GetKeyAlgorithm())
    {
        case "1.2.840.113549.1.1.1":
            rsa = RSA.Create();
            break;
        case "1.2.840.10045.2.1":
            ecdsa = ECDsa.Create();
            break;
        case "1.2.840.10040.4.1":
            dsa = DSA.Create();
            break;
        default:
            throw new NotSupportedException($"The certificate key algorithm '{cert.GetKeyAlgorithm()}' is unknown");
    }

    AsymmetricAlgorithm anyAlg = rsa ?? ecdsa ?? (AsymmetricAlgorithm)dsa;
    bool loaded = false;
    int bytesRead;

    using (rsa)
    using (ecdsa)
    using (dsa)
    {
        if (!loaded && rsa != null && kinds.HasFlag(KeyFileKinds.RsaPrivateKey))
        {
            try
            {
                rsa.ImportRSAPrivateKey(keyBytes, out bytesRead);
                loaded = bytesRead == keyBytes.Length;
            }
            catch (CryptographicException)
            {
            }
        }

        if (!loaded && kinds.HasFlag(KeyFileKinds.Pkcs8))
        {
            try
            {
                anyAlg.ImportPkcs8PrivateKey(keyBytes, out bytesRead);
                loaded = bytesRead == keyBytes.Length;
            }
            catch (CryptographicException)
            {
            }
        }

        if (!loaded && kinds.HasFlag(KeyFileKinds.EncryptedPkcs8))
        {
            try
            {
                // This assumes that the private key was already exported
                // with the same password that the PFX will be exported with.
                // Not true? Add a parameter :).
                anyAlg.ImportEncryptedPkcs8PrivateKey(exportPassword, keyBytes, out bytesRead);
                loaded = bytesRead == keyBytes.Length;
            }
            catch (CryptographicException)
            {
            }
        }

        if (!loaded)
        {
            throw new InvalidOperationException("Could not load the key as any known format.");
        }

        X509Certificate2 withKey;

        if (rsa != null)
        {
            withKey = cert.CopyWithPrivateKey(rsa);
        }
        else if (ecdsa != null)
        {
            withKey = cert.CopyWithPrivateKey(ecdsa);
        }
        else
        {
            Debug.Assert(dsa != null);
            withKey = cert.CopyWithPrivateKey(dsa);
        }

        using (withKey)
        {
            return withKey.Export(X509ContentType.Pfx, exportPassword);
        }
    }
}

Update (2020-10-09): The previous update showed better code from .NET Core 3.1, but then also some looking ahead code from .NET 5. If the certificate file is in the PEM format (-----BEGIN CERTIFICIATE-----) and the key file is in a PEM format (BEGIN PRIVATE KEY / BEGIN RSA PRIVATE KEY / BEGIN EC PRIVATE KEY / BEGIN ENCRYPTED PRIVATE KEY) then there's an even simpler approach with .NET 5:

using (X509Certificate2 certWithKey = X509Certificate2.CreateFromPemFile(certPath, keyPath))
{
    return certWithKey.Export(X509ContentType.Pfx, exportPassword);
}

Also available as CreateFromPem(loadedCertPem, loadedKeyPem), CreateFromEncryptedPem(loadedCertPem, loadedKeyPem, keyPassword), and CreateFromEncryptedPemFile(certPath, keyPath, keyPassword).

like image 56
bartonjs Avatar answered Oct 22 '22 07:10

bartonjs