使用 Bouncycastle 存储 PKCS#12 容器 (pfx)

Posted

技术标签:

【中文标题】使用 Bouncycastle 存储 PKCS#12 容器 (pfx)【英文标题】:Store PKCS#12 Container (pfx) with Bouncycastle 【发布时间】:2017-11-29 00:34:58 【问题描述】:

我正在努力使用 Xamarin 和 BouncyCastle 创建 pfx 文件。 我有以下设置/规范

.NET:PCL .Net Framework 4.5.1 Xamarin:4.5.0.476 BouncyCastle:BouncyCastle 签名 1.7.0.1 (NuGet Package)

我想为我的移动客户端生成一个自签名证书,以针对我的服务器进行身份验证。使用 BouncyCastle 的创作效果很好。我的问题是,当我想将带有私钥的证书存储为 PKCS#12 (pfx) 容器并从该容器中恢复它以发送由它签名的 Web 请求时,会抛出一个异常,告诉我Input data cannot be coded as a valide certificate

以下是我如何创建证书、密钥和存储 pfx 容器的步骤。

创建密钥对:

private AsymmetricCipherKeyPair GenerateKeyPair()

    var random = new SecureRandom();
    var keyGenerationParameter = new KeyGenerationParameters(random, 4096);
    var keyPairGenerator = new RsaKeyPairGenerator();
    keyPairGenerator.Init(keyGenerationParameter);
    var keyPair = keyPairGenerator.GenerateKeyPair();

   return keyPair;

创建证书,它将调用创建密钥并创建容器:

public void CreateCertificate()

    var random = new SecureRandom();
    var keyPair = this.GenerateKeyPair();

    // generate certificate generator and set public key.
    var generator = new X509V3CertificateGenerator();
    generator.SetPublicKey(keyPair.Public);

    // generate and set serial number
    BigInteger serialNumber = BigIntegers.CreateRandomInRange(BigInteger.One, BigInteger.ValueOf(long.MaxValue), random);
    generator.SetSerialNumber(serialNumber);

    // set signing algorithm.
    generator.SetSignatureAlgorithm(Core.Constants.SignatureAlgorithmName);

    // set name
    string fullQualifiedName = $"CN=dummy,O=DummyOrg,OU=Dummy";
    var name = new X509Name(fullQualifiedName);
    generator.SetSubjectDN(name);
    generator.SetIssuerDN(name);

    // set valide time
    generator.SetNotBefore(DateTime.UtcNow.AddHours(-1));
    generator.SetNotAfter(DateTime.UtcNow.AddYears(10));

    // add extensions.
    var authorityKeyIdentifier = new AuthorityKeyIdentifier(
        SubjectPublicKeyInfoFactory.CreateSubjectPublicKeyInfo(keyPair.Public).GetEncoded(),
        new GeneralNames(new GeneralName(name)),
        serialNumber);
    var subjectKeyIdentifier = new SubjectKeyIdentifier(
        SubjectPublicKeyInfoFactory.CreateSubjectPublicKeyInfo(keyPair.Public).GetEncoded());
    generator.AddExtension(X509Extensions.AuthorityKeyIdentifier, false, authorityKeyIdentifier);
    generator.AddExtension(X509Extensions.SubjectKeyIdentifier, false, subjectKeyIdentifier);
    generator.AddExtension(X509Extensions.BasicConstraints.Id, true, new BasicConstraints(0));

    // generate and validate certificate.
    var cert = generator.Generate(keyPair.Private, random);
    cert.CheckValidity(DateTime.UtcNow);
    cert.Verify(keyPair.Public);

    // generate pem string
    using (var stringWriter = new StringWriter())
    
        var writer = new PemWriter(stringWriter);
        var pog = new PemObject("CERTIFICATE", cert.GetEncoded());
        writer.WriteObject(pog);
        // pem value
        var value = stringWriter.ToString();
        this.StoreCertificate(value);
    

    // store the private key
    using (var stringWriter = new StringWriter())
    
        var writer = new PemWriter(stringWriter);
        PrivateKeyInfo info = PrivateKeyInfoFactory.CreatePrivateKeyInfo(keyPair.Private);
        var pog = new PemObject("RSA PRIVATE KEY", info.ToAsn1Object().GetEncoded());
        writer.WriteObject(pog);
        // pem value
        var value = stringWriter.ToString();
        this.StorePrivateKey(value);
    
    try
    
        this.CreatePfxFile();
    
    catch (Exception ex)
    
        throw;
    

创建 PFX 容器:

    public void CreatePfxFile()

    var certificate = this.GetCertificate();
    var certEntry = new X509CertificateEntry(certificate);
    string friendlyName = certificate.SubjectDN.ToString();
    var privateKey = this.ReadPrivateKey();

    PrivateKeyInfo info = PrivateKeyInfoFactory.CreatePrivateKeyInfo(privateKey);
    byte[] keyBytes = info.ToAsn1Object().GetEncoded();

    var store = new Pkcs12StoreBuilder().Build();

    var keyEntry = new AsymmetricKeyEntry(privateKey, );

    ////store.SetCertificateEntry(Core.Constants.CertificateAlias, certEntry);
    store.SetKeyEntry(Core.Constants.PrivateKeyAlias, new AsymmetricKeyEntry(privateKey), new X509CertificateEntry[]  certEntry );

    MemoryStream stream = new MemoryStream();
    store.Save(stream, Core.Constants.Password.ToCharArray(), new SecureRandom());

    var pfxBytes = stream.ToArray();

    var base64 = Convert.ToBase64String(pfxBytes);

    this.StorePfxContainer(base64);
 

GetCertificate()ReadPrivateKey() 方法将返回 X509CertificateAsymmetricKeyParameterStorePfxContainer 方法只会将 base64 字符串存储到设置变量中。

注意:我删除了 store.SetCertificateEntry(Core.Constants.CertificateAlias, certEntry); 行,因为它导致 pfx 容器包含两次证书。

请不要,因为现在我们只是使用 BouncyCastle 的东西。

要签署我的请求,我使用以下代码。参数cert 将通过从设置中检索base64 字符串创建并使用Convert.FromBase64String(settingsValue) 获取字节。

private void SetCertificate(HttpWebRequest request, byte[] cert)

    try
    
        System.Security.Cryptography.X509Certificates.X509Certificate2 certificate =
        new System.Security.Cryptography.X509Certificates.X509Certificate2(cert, Core.Constants.Password); // <-- throws the exception

        request.ClientCertificates.Add(certificate);
    
    catch (Exception ex)
    
        string message = ex.Message;
    

现在我们正在使用来自 .Net 的证书 (System.Security.Cryptography.X509Certificates.X509Certificate2)。当我想加载证书时,x509Certifcate2 的构造函数抛出的异常是 Input data cannot be coded as a valide certificate Mono.Security 抛出

我使用 openssl 测试了证书和密钥,还尝试从我的 pem 证书和 pem 私钥生成 pfx 容器,也可以正常工作。只有当我在我的代码中加载证书时它才会起作用。所以我认为容器的创建有一个我现在还没有弄清楚的错误。

感谢您的帮助。

【问题讨论】:

【参考方案1】:

经过更多的尝试和错误,我发现了我的实施错误。

首先,我认为我所经历的行为是 PCL 和 Mono 的充气城堡的组合。

我生成 pfx 容器的新方法如下所示。

private void CreatePfxFile(X509Certificate certificate, AsymmetricKeyParameter privateKey)

    // create certificate entry
    var certEntry = new X509CertificateEntry(certificate);
    string friendlyName = certificate.SubjectDN.ToString();

    // get bytes of private key.
    PrivateKeyInfo keyInfo = PrivateKeyInfoFactory.CreatePrivateKeyInfo(privateKey);
    byte[] keyBytes = keyInfo.ToAsn1Object().GetEncoded();

    var builder = new Pkcs12StoreBuilder();
    builder.SetUseDerEncoding(true);
    var store = builder.Build();

    // create store entry
    store.SetKeyEntry(Core.Constants.PrivateKeyAlias, new AsymmetricKeyEntry(privateKey), new X509CertificateEntry[]  certEntry );

    byte[] pfxBytes = null;

    var password = Guid.NewGuid().ToString("N");

    using (MemoryStream stream = new MemoryStream())
    
        store.Save(stream, password.ToCharArray(), new SecureRandom());
        pfxBytes = stream.ToArray();
    

    var result = Pkcs12Utilities.ConvertToDefiniteLength(pfxBytes);
    this.StoreCertificate(Convert.ToBase64String(result));

第一个更改是将MemoryStream 包装成using。我不太确定这会改变什么,但它摆脱了内部异常。

第二个变化是使用Pkcs12StoreBuilder 并设置SetUseDerEcnoding(true) 并以此建立一个商店。

第三个更改是使用Pkcs12Urilities.ConvertToDefiniteLength() 方法为数据设置确定的长度。

在这些更改之后,我的证书可以毫无问题地存储和恢复。

注意: 还要确保您没有使用string.Empty 作为密码来保存容器。我经常看到这是解决容器问题的方法。

【讨论】:

以上是关于使用 Bouncycastle 存储 PKCS#12 容器 (pfx)的主要内容,如果未能解决你的问题,请参考以下文章

BouncyCastle 密钥转换 - Java pkcs1格式,pkcs8格式互转

Bouncycastle和PKCS#1 v2.1,使用RSASSA-PSS进行签名并使用带有RSAES-OAEP的AES CBC进行加密

读取pkcs12证书信息

java使用AES-256-ECB(PKCS7Padding)解密——微信支付退款通知接口指定解密方式

在 Java 中生成 PKCS#1 格式的 RSA 密钥

BouncyCastle 未定义长度 ASN1