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
814 views
in Technique[技术] by (71.8m points)

c# - Store PKCS#12 Container (pfx) with Bouncycastle

I am struggling with the creation of a pfx file with Xamarin and BouncyCastle. I have the following settings/ specification

  • .NET: PCL .Net Framework 4.5.1
  • Xamarin: 4.5.0.476
  • BouncyCastle: BouncyCastle-Signed 1.7.0.1 (NuGet Package)

I want to generate a Self-Signed Certificate for my mobile client to autheniticate itself against my server. The creation works pretty well using BouncyCastle. My problem is, when I want to store the certificate with its private key as a PKCS#12 (pfx) container and restore it from that container to send my web requests signed by it an exception is thrown telling me Input data cannot be coded as a valide certificate.

Here are the steps how i create my certificate, keys and store the pfx container.

Create the key pair:

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

Create the certificate, which will call to create the keys and create the container:

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

Create the PFX container:

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

The methods GetCertificate() and ReadPrivateKey() will return an X509Certificate and an AsymmetricKeyParameter. The method StorePfxContainer will just store the base64 string into a settings variable.

Note: I removed the line store.SetCertificateEntry(Core.Constants.CertificateAlias, certEntry); because it caused the pfx container to contain the certificate twice.

Please not, since now we are just using BouncyCastle stuff.

To sign my request i use the following code. The parameter cert will be created by retrieving the base64 string from the settings and use Convert.FromBase64String(settingsValue) to get the bytes.

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

Now we are using the Certificate from .Net (System.Security.Cryptography.X509Certificates.X509Certificate2). The exception which is thrown at the constructor of the x509Certifcate2 when I want to load the certificate is Input data cannot be coded as a valide certificate thrown by Mono.Security

I tested the certificate and key with openssl, also i tried to generate a pfx container from my pem Certificate and pem private key, also works fine. Only when i load the certificate inside my code it will not work. So i think that the creation of the container has a bug which I haven't figured out by now.

Thanks for your help.

See Question&Answers more detail:os

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

1 Reply

0 votes
by (71.8m points)

After some more trial and error I figured out what was the error of my implementation.

First of all, I think that the behavior I experienced was a combination of a Bouncy Castle for PCL and Mono.

My new method to generate the pfx container looks like the following.

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

First change was to wrap the MemoryStream into a using. I am not so sure what this would change but it got rid of an inner exception.

Second change was to use the Pkcs12StoreBuilder and set the SetUseDerEcnoding(true) and build a store from this.

Third change was to use the Pkcs12Urilities.ConvertToDefiniteLength() method to set a definite length to the data.

After these changes my certificate could be stored and restored without any problems.

Note: Also make sure that you are not using string.Empty as password to save the container. I have seen this often to be the answer to problems with the container.


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

...