Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
914 views
in Technique[技术] by (71.8m points)

c# - .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.

See Question&Answers more detail:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Reply

0 votes
by (71.8m points)

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).


与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
OGeek|极客中国-欢迎来到极客的世界,一个免费开放的程序员编程交流平台!开放,进步,分享!让技术改变生活,让极客改变未来! Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...