【问题标题】:.NET Standard - Merge a certificate and a private key into a .pfx file programmatically.NET Standard - 以编程方式将证书和私钥合并到 .pfx 文件中
【发布时间】:2017-06-09 19:40:54
【问题描述】:

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

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

【问题讨论】:

    标签: c# ssl ssl-certificate x509certificate .net-standard


    【解决方案1】:

    没有办法对框架类型执行此操作。 BouncyCastle 或其他库也许可以实现。

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

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

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

    框架类型也没有办法从二进制表示中加载关键对象(CngKey.Import 除外,但仅适用于 Windows),只能从预解析的结构(RSAParameters、@ 987654325@, ECParameters)。

    在 Linux 上实现此目标的最简单方法(如果 BouncyCastle 无法帮助您)是使用 System.Process 产生类似于 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


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

    此代码检查密钥文件是否使用 .NET 5.0 PemEncoding 类进行 PEM 编码(而不是二进制/DER 编码),然后使用支持的格式加载私钥,匹配私钥直到证书,然后出口。 .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 PRIVATE KEY),那么.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)

    【讨论】:

    • PFX/PKCS#12 不仅仅是连接证书和密钥。
    • @jww 是的,但是一旦您可以获得关联的密钥,cert.Export(对 PFX)就会处理它
    • @bartonjs 您能否提供一个示例,说明如何使用 BouncyCastle 做到这一点?
    • @Maxim 不,我从未使用过 BouncyCastle,只知道它存在。
    猜你喜欢
    • 1970-01-01
    • 2012-08-09
    • 1970-01-01
    • 2011-09-12
    • 2020-01-18
    • 1970-01-01
    • 2015-01-20
    • 1970-01-01
    • 2010-10-23
    相关资源
    最近更新 更多