Search code examples
c#securitycryptographyx509certificatebouncycastle

How can I create a PKCS12 p12 file with selfsigned certificate for DSA keypair in C#?


I need to generate my own DSA keypair and store it as a bundle of private key and certificate in .p12 file using C#.

This question

How do I create a PKCS12 .p12 file in C#?

seems to be very similar, but it does not help me, unfortunately, because there are some significant differences (RSA vs DSA, etc.)

I am trying to generate the keypair using System.Security.Cryptography.DSACryptoServiceProvider and then to generate a X509 certificate using Bouncy Castle:

using (DSACryptoServiceProvider csp = new DSACryptoServiceProvider(1024))
{
       privKeyDSA = csp.ExportParameters(true);
       pubKeyDSA = csp.ExportParameters(false);
       var keypair = DotNetUtilities.GetDsaKeyPair(privKeyDSA);

       var gen = new X509V3CertificateGenerator();

       var CN = new X509Name("CN=" + "TEST");
       var SN = BigInteger.ProbablePrime(120, new Random());

       gen.SetSerialNumber(SN);
       gen.SetSubjectDN(CN);
       gen.SetIssuerDN(CN);
       gen.SetNotAfter(DateTime.MaxValue);
       gen.SetNotBefore(DateTime.Now.Subtract(new TimeSpan(7, 0, 0, 0)));
       gen.SetSignatureAlgorithm("sha1WithDSA");
       gen.SetPublicKey(DotNetUtilities.GetDsaPublicKey(pubKeyDSA));
       var newCert = gen.Generate(keypair.Private);


       certificateDSA = new X509Certificate2(DotNetUtilities.ToX509Certificate((Org.BouncyCastle.X509.X509Certificate)newCert));

       certificateDSA.PrivateKey = csp;
       StringBuilder builder = new StringBuilder();

        builder.AppendLine("-----BEGIN CERTIFICATE-----");
        builder.AppendLine(Convert.ToBase64String(certificateDSA.Export(X509ContentType.Cert), Base64FormattingOptions.InsertLineBreaks));
        builder.AppendLine("-----END CERTIFICATE-----");

        string result = builder.ToString();
        byte[] pkcsData = certificateDSA.Export(X509ContentType.Pfx, "changeit");
}

However, the line certificateDSA.PrivateKey = csp; throws a CryptographicUnexpectedOperationException with message: "The public key of the certificate does not match the value specified."

I really do not understand what is happening. What am I doing wrong? Thanks!


Solution

  • Got interested by this and here is my little investigation. When you set PrivateKey of certificateDSA, .NET code does roughly this:

    byte[] numArray1 = ((ICspAsymmetricAlgorithm) certificateDSA.PublicKey.Key).ExportCspBlob(false);
    byte[] numArray2 = csp.ExportCspBlob(false);
    // And then those two blobs are compared byte by byte
    

    Those blobs are different starting at position 420 (they have length of 444). So something is wrong in csp parameters. It's not easy to compare raw bytes, so let's convert them to readable xml with:

    var xml1 = certificateDSA.PublicKey.Key.ToXmlString(false);
    var xml2 = csp.ToXmlString(false);
    

    What we will get is this:

    <DSAKeyValue> <!--this is parameters of cert public key-->
        <P>2arEQPD3/tKm7pJF1y4gN0/4WzSGfkgFwdmtmoUf/gHoXpdBetRH/5j98qo4k1ybePxM4om4y6n9vhxijocMw5LaeQPceGyNOEScWXXrNKAcUsK74klQmiPOoI2qI1zU5v2HrilKmkOELH81U8/Qmmjmg7ouOdOHqlZAxW9Sv8M=</P>
        <Q>lzRdUtp56eZHIgxRemvdHciGIfc=</Q>
        <G>Z/2T+jXvv0ZLswbuMd9DxrHldakJxZ8JNGRf1QzN09B2VO9WYAzUy0S+J8hbYQjP/jzWbmL5LaK57v+MUOmOHzFwNqfVMe9OUglUfF3nN990ur9hp6csu8+vCEQt3EoI8Wmh/b2yqhtKRN6U494vf33WKo1NCNQapB+iWVQ/egQ=</G>
        <Y>ykcPXFIxWvYDDbbY05oD3hD6LsM5rk76FakUY8YiCo8ZwWbMIlQw+v5nOYS9vpQaZAzUqxx9OXIGSTUGItruTARkDqZ0nGKL0r94Zhog1Y0wU2AVKJh8Vjq/dLFyDDGZZsxBZtmI8TDyKGJbZqvzGbdGLhoRxRFmNi1fVsADv+U=</Y>
    </DSAKeyValue>
    
    <DSAKeyValue> <!-- this is paramteres of original DSACryptoServiceProvider-->
        <P>2arEQPD3/tKm7pJF1y4gN0/4WzSGfkgFwdmtmoUf/gHoXpdBetRH/5j98qo4k1ybePxM4om4y6n9vhxijocMw5LaeQPceGyNOEScWXXrNKAcUsK74klQmiPOoI2qI1zU5v2HrilKmkOELH81U8/Qmmjmg7ouOdOHqlZAxW9Sv8M=</P>
        <Q>lzRdUtp56eZHIgxRemvdHciGIfc=</Q>
        <G>Z/2T+jXvv0ZLswbuMd9DxrHldakJxZ8JNGRf1QzN09B2VO9WYAzUy0S+J8hbYQjP/jzWbmL5LaK57v+MUOmOHzFwNqfVMe9OUglUfF3nN990ur9hp6csu8+vCEQt3EoI8Wmh/b2yqhtKRN6U494vf33WKo1NCNQapB+iWVQ/egQ=</G>
        <Y>ykcPXFIxWvYDDbbY05oD3hD6LsM5rk76FakUY8YiCo8ZwWbMIlQw+v5nOYS9vpQaZAzUqxx9OXIGSTUGItruTARkDqZ0nGKL0r94Zhog1Y0wU2AVKJh8Vjq/dLFyDDGZZsxBZtmI8TDyKGJbZqvzGbdGLhoRxRFmNi1fVsADv+U=</Y>
        <Seed>1hiZoCQFivF9xDZdQEGue65oObA=</Seed>
        <PgenCounter>Og==</PgenCounter>
    </DSAKeyValue>
    

    You see that original DSACryptoServiceProvider includes Seed and PgenCounter, while after generating certificate with Bouncy Castle, certificate's public key does not contain them. Those parameters are optional (in a sense that public key may not contain then), but if they are present, they should be present on both sides (private and public). How can we workaround this? Here is the code:

    using (DSACryptoServiceProvider csp = new DSACryptoServiceProvider(1024)) {
                var parameters = csp.ExportParameters(true);                
                var keypair = DotNetUtilities.GetDsaKeyPair(parameters);
                var gen = new X509V3CertificateGenerator();
                var CN = new X509Name("CN=" + "TEST");
                var SN = BigInteger.ProbablePrime(120, new Random());
                gen.SetSerialNumber(SN);
                gen.SetSubjectDN(CN);
                gen.SetIssuerDN(CN);
                gen.SetNotAfter(DateTime.Now.AddDays(1));
                gen.SetNotBefore(DateTime.Now.Subtract(new TimeSpan(7, 0, 0, 0)));
                gen.SetSignatureAlgorithm("sha1WithDSA");
                gen.SetPublicKey(keypair.Public);
                var newCert = gen.Generate(keypair.Private);
    
                var certificateDSA = new X509Certificate2(DotNetUtilities.ToX509Certificate(newCert));
                // added block
                parameters.Seed = new byte[20];
                unchecked {
                    parameters.Counter = (int) 0xFFFFFFFF;
                }
                csp.ImportParameters(parameters);
                // end of added block
                certificateDSA.PrivateKey = csp;
                StringBuilder builder = new StringBuilder();
    
                builder.AppendLine("-----BEGIN CERTIFICATE-----");
                builder.AppendLine(Convert.ToBase64String(certificateDSA.Export(X509ContentType.Pkcs12), Base64FormattingOptions.InsertLineBreaks));
                builder.AppendLine("-----END CERTIFICATE-----");
    
                string result = builder.ToString();                
            }
    

    What we do here is after generating everything, but before assigning private key to certificate, we "removing" seed and counter from DSACryptoServiceProvider parameters. This code does not throw errors and completes fine. Maybe there are some caveats in this workaround, but nevertheless it might be useful to futher investigating the issue, even if it not fixes it completely.