Search code examples
c#certificatedsa

Is there a way to generate a DSA certificate using pure .NET Framework and if not, why?


Starting from .NET 4.7.2 it is possible to generate RSA and EC certificates using .NET CertificateRequest. However I can't find anything that would allow me to generate DSA certs. Here is how I'd do it for RSA and EC:

private static X509Certificate2 GenerateRsaCertificate()
{
    var hashAlgorithm = HashAlgorithmName.SHA256;
    var rsaKey = RSA.Create(2048);
    var subject = new X500DistinguishedName("CN=mycert");
    var request = new CertificateRequest(subject, rsaKey, hashAlgorithm, RSASignaturePadding.Pkcs1);
    var certificate = request.CreateSelfSigned(DateTime.Now - TimeSpan.FromDays(5), DateTime.Now + TimeSpan.FromDays(365));
    return certificate;
}

private static X509Certificate2 GenerateEcDsaCertificate()
{
    var hashAlgorithm = HashAlgorithmName.SHA256;
    var curve = ECCurve.NamedCurves.nistP256;
    var ecDsaKey = ECDsa.Create(curve);
    var subject = new X500DistinguishedName("CN=mycert");
    var request = new CertificateRequest(subject, ecDsaKey, hashAlgorithm);
    var certificate = request.CreateSelfSigned(DateTime.Now - TimeSpan.FromDays(5), DateTime.Now + TimeSpan.FromDays(365));
    return certificate;
}

Previously I used Bouncy Castle to generate all three types of certs, but with migration to .NET I'm able to use only RSA and ECDsa in CertificateRequest calls. Are there any reasons why DSA is not included? I can still generate a key with DSA.Create(keySize) however. Also .NET Framework includes other classes that work with DSA: DSA, DSACng, DSACryptoServiceProvider, DSACertificateExtensions, but I don't see anything for certificate generation. Are there any problems with the algorithm itself (maybe I shouldn't use it at all)? Or am I missing something in the API?


Solution

  • Are there any problems with the algorithm itself (maybe I shouldn't use it at all)?

    Non-EC DSA is dying.

    I'll speculate that the thing that really did it in is the original specification (FIPS 186-1) limited the keys to 1024-bit and the algorithm to SHA-1. In 2009 the algorithm got updated in FIPS 186-3 to support slightly larger keys and the SHA-2 hashes. FIPS 186-1 (and FIPS 186-2) DSA signatures only required data and a private key (verification only required data, signature, and a public key), FIPS 186-3 signatures also require the hash algorithm as an input... so the API isn't exactly compatible.

    Windows CAPI (the older of the two Windows cryptography platforms) ignored the FIPS 186-3 update, as did Apple's Security.framework. Windows CNG and OpenSSL both support "new DSA". Apple can't process certificates signed with "new DSA" (and maybe not even with "DSA classic", I forget), and Windows doesn't support "new DSA" in cert chains, only "DSA classic".

    So DSA certificates are generally limited to FIPS 186-1/186-2 restrictions, which means SHA-1 (not on anyone's good side these days) and 1024-bit keys (which are too small by today's reckoning). If you know you're being validated by OpenSSL you can use better DSA keys.

    DSA is also generally much slower at signature verification than RSA.

    At the 80 bits of security level, using OpenSSL's speed tool on a random VM of mine (output slightly modified for presentation purposes, sorted by verify/s descending):

                                    sign    verify    sign/s verify/s
    rsa  1024 bits               0.000301s 0.000018s   3326.3  56419.7
    dsa  1024 bits               0.000309s 0.000236s   3236.2   4240.5
    ecdsa 160 bits (secp160r1)   0.0005s   0.0004s     1984.6   2385.7
    

    112 bits of security

                                    sign    verify    sign/s verify/s
    rsa  2048 bits               0.002030s 0.000062s    492.6  16062.4
    ecdsa 224 bits (nistp224)    0.0001s   0.0002s     9020.6   4252.2
    dsa  2048 bits               0.000885s 0.000802s   1129.4   1247.3
    

    128 bits of security

                                          sign    verify    sign/s verify/s
    rsa  3072 bits                     0.006935s 0.000135s    144.2   7401.6
    ecdsa 256 bits (nistp256)          0.0001s   0.0002s    16901.5   5344.7
    ecdsa 256 bits (brainpoolP256t1)   0.0010s   0.0008s      980.1   1262.5
    ecdsa 256 bits (brainpoolP256r1)   0.0010s   0.0008s     1012.9   1209.5
    dsa  3072 bits (not in test suite)
    

    192 bits of security

                                          sign    verify    sign/s verify/s
    rsa  7680 bits                     0.122805s 0.000820s      8.1   1220.2
    ecdsa 384 bits (nistp384)          0.0024s   0.0018s      416.1    571.2
    ecdsa 384 bits (brainpoolP384t1)   0.0024s   0.0018s      410.0    545.1
    ecdsa 384 bits (brainpoolP384r1)   0.0025s   0.0019s      407.4    540.1
    dsa  7680 bits (beyond FIPS 186-3 DSA maximum of 3072 bits)
    

    256 bits of security

                                          sign    verify    sign/s verify/s
    ecdsa 521 bits (nistp521)          0.0006s   0.0012s     1563.1    841.3
    ecdsa 512 bits (brainpoolP512t1)   0.0038s   0.0027s      265.2    369.1
    ecdsa 512 bits (brainpoolP512r1)   0.0038s   0.0028s      262.4    360.5
    rsa 15360 bits                     0.783846s 0.003190s      1.3    313.5
    dsa 15360 bits (beyond FIPS 186-3 DSA maximum of 3072 bits)
    

    I'm able to use only RSA and ECDsa in CertificateRequest calls. Are there any reasons why DSA is not included?

    From the thread with the original feature proposal:

    Based on new data from Windows (and their lack of support for FIPS 186-3 DSA certificates) I'm going to pull the DSA typed constructor and leave DSA as a "power user" scenario (custom X509SignatureGenerator class, etc)

    So, it was removed mainly because DSA is dying.

    Or am I missing something in the API?

    The API allows for custom signature generators to be provided. In the tests for CertificateRequest it proves this out with a DSAX509SignatureGenerator

    X509SignatureGenerator dsaGen = new DSAX509SignatureGenerator(dsaCsp);
    
    // Use SHA-1 because that's all DSACryptoServiceProvider understands.
    HashAlgorithmName hashAlgorithm = HashAlgorithmName.SHA1;
    
    CertificateRequest request = new CertificateRequest(
        new X500DistinguishedName($"CN={KeyName}-{provType}"),
        dsaGen.PublicKey,
        hashAlgorithm);
    
    DateTimeOffset now = DateTimeOffset.UtcNow;
    
    using (X509Certificate2 cert = request.Create(request.SubjectName, dsaGen, now, now.AddDays(1), new byte[1]))
    using (X509Certificate2 certWithPrivateKey = cert.CopyWithPrivateKey(dsaCsp))
    using (DSA dsa = certWithPrivateKey.GetDSAPrivateKey())
    {
        byte[] signature = dsa.SignData(Array.Empty<byte>(), hashAlgorithm);
    
        Assert.True(dsaCsp.VerifyData(Array.Empty<byte>(), signature, hashAlgorithm));
    }
    

    (snippet from https://github.com/dotnet/runtime/blob/4f9ae42d861fcb4be2fcd5d3d55d5f227d30e723/src/libraries/System.Security.Cryptography.X509Certificates/tests/CertificateCreation/PrivateKeyAssociationTests.cs#L276-L295)