.NET Standard - 以编程方式将证书和私钥合并到 .pfx 文件中 [英] .NET Standard - Merge a certificate and a private key into a .pfx file programmatically

查看:83
本文介绍了.NET Standard - 以编程方式将证书和私钥合并到 .pfx 文件中的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

有什么方法可以使用 C#/.NET Standard 以编程方式将证书和密钥(两者都作为 Base64 字符串从服务中单独接收)合并到一个 .pfx 文件中?使用工具不是一种选择,因为我需要使其自动化.

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.

上下文:我需要将证书和私钥(没有页眉和页脚的单独 Base64 字符串)加载到 X509Certificate2 对象,以将其从 .NET Standard 1.6 库传递给应用程序.问题是,.NET Standard 中的 X509Certificate2 类中没有 PrivateKey 属性!因此,我在 X509Certificate2 对象中实际加载私钥的唯一方法是将它与证书本身结合在一个 .pfx 文件中,并像在构造函数中那样加载它.

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.

推荐答案

没有办法用框架类型来做到这一点.使用 BouncyCastle 或其他库可能是可能的.

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

.NET Core 2.0 增加了通过扩展方法将证书和密钥对象合并在一起(到一个新的 X509Certificate2 对象)的能力:

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

但这需要专门为 netcoreapp20(不是 netstandard20)编译.

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

框架类型也无法从二进制表示中加载关键对象(CngKey.Import 除外,但仅适用于 Windows),只能从预解析的结构(RSAParametersDSAParametersECParameters).

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

在 Linux 上实现此目标的最简单方法(如果 BouncyCastle 无法帮助您)是使用 System.Process 生成类似于 openssl pkcs12 -export -out 的调用tmp.pfx -in tmp.cer -inkey tmp.key -password pass:".

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:"".

在 Windows 上,您可以使用 CngKey.Import 和 P/Invoke CertSetCertificateContextProperty(对于 CERT_NCRYPT_KEY_HANDLE_PROP_ID (78))然后在变异的证书上调用 cert.Export.

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.

更新 (2020-09-30): 使用 .NET 3.0 这相当简单(不回答使用 .NET Standard"的原始问题,因为它需要为 netcoreapp3 编译.0 或更高版本),并且还可以轻松地使用 .NET 5.0 添加对 PEM 编码密钥的支持.

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.

此代码使用 .NET 5.0 PemEncoding 类检查密钥文件是否为 PEM 编码(而不是二进制/DER 编码),然后使用支持的格式加载私钥,匹配私钥到证书,然后导出..NET Core 3.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);
        }
    }
}


更新 (2020-10-09):之前的更新显示了来自 .NET Core 3.1 的更好的代码,但也显示了来自 .NET 5 的一些前瞻性代码.​​如果证书文件在PEM 格式 (-----BEGIN CERTIFICIATE-----) 并且密钥文件为 PEM 格式 (BEGIN PRIVATE KEY/BEGIN RSA PRIVATE KEY/BEGIN EC PRIVATE KEY/BEGIN ENCRYPTED PRIVATEKEY)然后有一个更简单的方法与 .NET 5:


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

也可用作 CreateFromPem(loadedCertPem, loadedKeyPem)CreateFromEncryptedPem(loadedCertPem, loadedKeyPem, keyPassword)CreateFromEncryptedPemFile(certPath, keyPath, keyPassword)代码>.

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

这篇关于.NET Standard - 以编程方式将证书和私钥合并到 .pfx 文件中的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆