1

I'm learning about certificate signing requests and signing servers via Nuget package System.Security.Cryptography.Cng. And what better way than to try to re-create one. There seem to be a block I don't get around currently, that is the server signing party, namely in the following code I get System.InvalidOperationException: 'An X509Extension with OID '2.5.29.37' has already been specified.' at request.Create( in the using clause. I see at http://oid-info.com/get/2.5.29.37 it's about extended key usage.

Questions:

  1. The MakeLocalhostCert is likely wrong, what should be changed to make it a certificate to sign CSRs?
  2. Is it possible to add/remove extensions/OIDs to the CSR being returned? I believe it is, but somehow this part eludes me currently.

I used the excellent answers of https://stackoverflow.com/users/6535399/bartonjs at https://stackoverflow.com/a/45240640/1332416 and at https://stackoverflow.com/a/44073726/1332416 to get this far. :)

    private static void CsrSigningTest()
    {
        //Both ECDSA and RSA included here, though ECDSA is probably better.
        using(ECDsa privateClientEcdsaKey = ECDsa.Create(ECCurve.NamedCurves.nistP256))
        //using(RSA privateClientRsaKey = RSA.Create(2048))
        {
            //A client creates a certificate signing request.
            CertificateRequest request = new CertificateRequest(
                new X500DistinguishedName("CN=example.com, O=Example Ltd, OU=AllOver, L=Sacremento, ST=\"California\", C=US, E=some@example.com"),
                privateClientEcdsaKey,
                HashAlgorithmName.SHA256);
            /*CertificateRequest request = new CertificateRequest(
                new X500DistinguishedName("CN=example.com, O=Example Ltd, OU=AllOver, L=Sacremento, ST=\"California\", C=US, E=some@example.com"),
                privateClientRsaKey,
                HashAlgorithmName.SHA256,
                RSASignaturePadding.Pkcs1);*/

            var sanBuilder = new SubjectAlternativeNameBuilder();
            sanBuilder.AddDnsName("example.com");
            request.CertificateExtensions.Add(sanBuilder.Build());

            //Not a CA, a server certificate.
            request.CertificateExtensions.Add(new X509BasicConstraintsExtension(false, false, 0, false));
            request.CertificateExtensions.Add(new X509EnhancedKeyUsageExtension(new OidCollection { new Oid("1.3.6.1.5.5.7.3.1") }, false));
            request.CertificateExtensions.Add(new X509EnhancedKeyUsageExtension(new OidCollection { new Oid("1.3.6.1.5.5.7.3.8") }, false));

            byte[] derEncodedCsr = request.CreateSigningRequest();
            var csrSb = new StringBuilder();
            csrSb.AppendLine("-----BEGIN CERTIFICATE REQUEST-----");
            csrSb.AppendLine(Convert.ToBase64String(derEncodedCsr));
            csrSb.AppendLine("-----END CERTIFICATE REQUEST-----");

            //Thus far OK, this csr seems to be working when using an online checker.
            var csr = csrSb.ToString();

            //Now, sending this to a server... How does the server function:
            //1) Read the CSR to be processed?
            //2) How does this CSR get signed?
            //In the following, can the signing cert be self-signed could be had from
            //https://stackoverflow.com/a/45240640/1332416

            byte[] serial = new byte[16];
            using(var rng = RandomNumberGenerator.Create())
            {
                rng.GetBytes(serial);
            }

            DateTimeOffset notBefore = DateTimeOffset.UtcNow;
            DateTimeOffset notAfter = notBefore.AddYears(1);
            var issuerCertificate = MakeLocalhostCert();
            //For the part 1) there, this doesn't seem to work, likely since CSR isn't a X509 certificate.
            //But then again, there doesn't seem to be anything in CertificateRequest to read this.
            //In reality in the server the prologue and epilogue strings should be removed and the string read.
            //var testRequest = new X509Certificate2(derEncodedCsr);
            using(X509Certificate2 responseToCsr = request.Create(issuerCertificate, notBefore, notAfter, serial))
            {
                //How to add extensions here?
                var csrResSb = new StringBuilder();
                csrResSb.AppendLine("-----BEGIN CERTIFICATE-----");
                csrResSb.AppendLine(Convert.ToBase64String(responseToCsr.GetRawCertData()));
                csrResSb.AppendLine("-----END CERTIFICATE-----");

                var signedCert = csrResSb.ToString();
            }
        }
    }

    private static X509Certificate2 MakeLocalhostCert()
    {
        using(ECDsa key = ECDsa.Create(ECCurve.NamedCurves.nistP384))
        {
            var request = new CertificateRequest(
                "CN=localhost",
                key,
                HashAlgorithmName.SHA384);

            request.CertificateExtensions.Add(
                new X509BasicConstraintsExtension(true, false, 0, true));

            const X509KeyUsageFlags endEntityTypicalUsages =
                X509KeyUsageFlags.DataEncipherment |
                X509KeyUsageFlags.KeyEncipherment |
                X509KeyUsageFlags.DigitalSignature |
                X509KeyUsageFlags.NonRepudiation |
                X509KeyUsageFlags.KeyCertSign;

            request.CertificateExtensions.Add(
                new X509KeyUsageExtension(endEntityTypicalUsages, true));

            var sanBuilder = new SubjectAlternativeNameBuilder();
            sanBuilder.AddDnsName("localhost");
            sanBuilder.AddIpAddress(IPAddress.Loopback);
            sanBuilder.AddIpAddress(IPAddress.IPv6Loopback);

            request.CertificateExtensions.Add(sanBuilder.Build());

            /*request.CertificateExtensions.Add(
                new X509EnhancedKeyUsageExtension(
                    new OidCollection
                    {
                // server and client authentication
                new Oid("1.3.6.1.5.5.7.3.1"),
                new Oid("1.3.6.1.5.5.7.3.2")
                    },
                    false));*/

            DateTimeOffset now = DateTimeOffset.UtcNow.AddMinutes(-1);

            return request.CreateSelfSigned(now, now.AddYears(2));
        }
    }

<edit: Once I apply the fix about OIDs and change the last bits to

using(X509Certificate2 responseToCsr = request.Create(issuerCertificate, notBefore, notAfter, serial))
            {
                request.CertificateExtensions.Add(new X509SubjectKeyIdentifierExtension(issuerCertificate.PublicKey, false));
                var csrResSb = new StringBuilder();
                csrResSb.AppendLine("-----BEGIN CERTIFICATE-----");
                csrResSb.AppendLine(Convert.ToBase64String(responseToCsr.Export(X509ContentType.Cert), Base64FormattingOptions.InsertLineBreaks));
                csrResSb.AppendLine("-----END CERTIFICATE-----");

                var signedCert = csrResSb.ToString();
            }

I get back a certificate in signedCert that looks like the CSR that's been signed. The missing piece would be to construct the CSR if read from an actual CSR file.

<edit 2: There is an issue in CoreFx GH tracking some of the issues touches here: Security crypto - Roadmap.

<edit 3: Nice to know: How to load a certificate request and create a certificate from it in .NET, How to convert a CSR text file into .NET Core/ Standard CertificateRequest for Signing?, How to convert a CSR text file into .NET Core/ Standard CertificateRequest for Signing? for more about this in .NET5/6.

Veksi
  • 3,250
  • 2
  • 25
  • 59
  • I would go with standard CA software that does the thing. Attempts to use raw APIs (.NET, OpenSSL, etc.) to mimic CA server functionality never was a good idea. – Crypt32 Jul 01 '18 at 14:58
  • 1
    Out of curiosity, do you have some standard CA software in mind? I'm educating myself mostly here, pulled an allnighter to educate myself on stuff around this and thought that maybe I could spare a bit effort, be quicker and maybe be even helpful to others if there's something "revealing" to someone. For instance, some of the bits in the temporary signing certificate (CA enabled etc.). :) – Veksi Jul 01 '18 at 16:57
  • 1
    Two in mind: Microsoft ADCS (Standalone will be good) and EJBCA. – Crypt32 Jul 01 '18 at 17:09
  • Hey! Thanks! This gets me moving a bit. In the meanwhile I edited a bit the contents, I made some little progress. :) – Veksi Jul 01 '18 at 18:27

1 Answers1

2

To fix your exception you want to make your code set one EKU extension with two purpose OIDs instead of two extensions with one each.

// Defined two EKU extensions
request.CertificateExtensions.Add(new X509EnhancedKeyUsageExtension(new OidCollection { new Oid("1.3.6.1.5.5.7.3.1") }, false));
request.CertificateExtensions.Add(new X509EnhancedKeyUsageExtension(new OidCollection { new Oid("1.3.6.1.5.5.7.3.8") }, false));

to

// One extension carrying multiple purpose OIDs
request.CertificateExtensions.Add(
    new X509EnhancedKeyUsageExtension(
        new OidCollection
        {
            new Oid("1.3.6.1.5.5.7.3.1"),
            new Oid("1.3.6.1.5.5.7.3.8"),
        },
        false));

Then there are a couple of other problems in your title/question:

  • "How to generate a response to a CSR in .NET Core (i.e. to write a CSR signing server)?"
    • This code doesn't read a CSR, so it's not responding to one.
      • The purpose of the class is for unit tests and other development environment needs, and to be able to generate a CSR to send off to a real CA product.
      • .NET Core doesn't even have the ability to read a CSR, just write them.
  • "I'm learning about certificate signing requests and signing servers via Nuget package System.Security.Cryptography.Cng"
    • None of your code seems to use Cng types (which is good, you should rarely care). CertificateRequest is part of System.Security.Cryptography.X509Certificates.dll, which is exposed via Microsoft.NETCore.App
  • "Is it possible to add/remove extensions/OIDs to the CSR being returned?"
    • Yes, you add them to the CertificateExtensions property before calling CreateSigningRequest.
  • (implied) "Is it possible to add/remove extensions/OIDs to the certificate being returned?"
    • Yes, you add them to the CertificateExtensions property before calling Create or CreateSelfSigned.
bartonjs
  • 26,783
  • 2
  • 64
  • 100
  • Yep, the code doesn't read a CSR from the file as you noted there aren't facilities (I might've missed even if they were there :)). And that's why I was mucking around with the actual class without reading the CSR text. But I thanks to your OID fix, I managed to parse together something that looks like getting the CSR signed if I don't read the CSR as a text. Maybe there's some way to easilyish parse the DER encoded text file too... – Veksi Jul 01 '18 at 18:26