2

We currently have a solution to generate CA certificates using Microsoft Active Directory Certificate Services.

We have an application which you can login to and you can send a request for a CA certificate.

Behind the scenes this loads an iframe (pointing to a form on the MS CA certificate service) copies a load of data using jQuery and submits the form.

The form submission creates a CSR using ActiveX and CertEnroll and POSTs it to another ASP page. (The whole MS AD certificate service is ASP pages)

The issue is that this only works in IE (obviously)

My proposal was to move the CSR generation server side so that this wouldn't rely on an IE and could be used from any browser.

I have a solution that works, it sends the data to an api and within the api it generates a CSR and forwards it over to the MS CA cert server and this generates valid certificates.

The problem

The issue is that once I install the certificates, the information which states "You have a private key that corresponds to this certificate" is missing. On the existing system this message is displayed as below:

expected certificate

My assumption is that this is because the KeyProtection has not been set for the Private Key. Unfortunately, the flag that it used in the existing VBScript implementation requires user input: XCN_NCRYPT_UI_PROTECT_KEY_FLAG. Each time a certificate is requested the user is prompted to secure the key

For my .Net Framework implementation I have attempted two different CSR generators, one using Bouncy Castle and one using an interop for the CERTENROLLLib.

The code I'll display below will be the CERTENROLLLib implementation, but if anyone has ideas for either solution I would be happy to hear them

The CERTENROLLLib CSR generation

        var values = new Dictionary<string, string>
        {
            { "E", emailAddress },
            { "CN", commonName },
            { "OU", organizationalUnit },
            { "L", locality }
        };

        var privateKey = new CX509PrivateKeyClass
        {
            Length = 1024,
            MachineContext = false,
            CspInformations = GetCryptographicProvider(),
            // This can't be set as it requires UI feedback
            //KeyProtection = X509PrivateKeyProtection.XCN_NCRYPT_UI_PROTECT_KEY_FLAG,
            ExportPolicy = X509PrivateKeyExportFlags.XCN_NCRYPT_ALLOW_EXPORT_NONE,
        };


        //  Create the actual key pair
        privateKey.Create();

        //  Initialize the PKCS#10 certificate request object based on the private key.
        //  Using the context, indicate that this is a user certificate request and don't
        //  provide a template name
        var certificateRequest = new CX509CertificateRequestPkcs10Class();

        certificateRequest.InitializeFromPrivateKey(
            X509CertificateEnrollmentContext.ContextUser,
            privateKey,
            string.Empty
        );

        var extension = new CX509ExtensionsClass
        {
            GetExtensionKeyUsage(),
            GetExtensionEnhancedKeyUsage()
        };

        certificateRequest.X509Extensions.AddRange(extension);

        //  Encode the name in using the Distinguished Name object
        var valueCollection = values.Select(v => $"{v.Key}={v.Value}").Reverse();

        var subject = new CX500DistinguishedNameClass();
        subject.Encode(
            string.Join(";", valueCollection), X500NameFlags.XCN_CERT_NAME_STR_ENABLE_PUNYCODE_FLAG
        );

        //  Assigning the subject name by using the Distinguished Name object initialized above
        certificateRequest.Subject = subject;

        // Create enrollment request
        var enrollmentRequest = new CX509EnrollmentClass();
        enrollmentRequest.InitializeFromRequest(certificateRequest);

        var csr = enrollmentRequest.CreateRequest(EncodingType.XCN_CRYPT_STRING_BASE64REQUESTHEADER);

So my question is how can I add key protection to the private key as shown in the screenshot?

As this is server side I can't expect any user feedback, so is there another approach.

Any guidance will be welcome, as is probably painfully clear, I am quite new to CSR generation.

Eventually this will be rewritten and we'll probably be generating our own certificates, but I was simply trying to improve the current situation.

Many thanks

Corporalis
  • 1,032
  • 1
  • 9
  • 17

1 Answers1

0

First of all this is not a answer, but sharing some thoughts and experience which could help.

I used a similar code before with the version 1.0.0.0 of the library CERTENROLLLib (This is the way to go as it has most settings for generation of certificates, instead of Bouncy Castle).

I think your request is correct but somehow the MS CA is issuing a certificate without a private key, hence you import it and you end up with missing "You have a private key that corresponds to this certificate".

I am saying this because my code did not set KeyProtection and leave the default value of X509PrivateKeyProtection.XCN_NCRYPT_UI_NO_PROTECTION_FLAG, but I was able to generate a certificate with the private key included (I actually used CERTENROLLLib again to generate the certificate instead of MS CA so this is a major difference).

It is like exporting a certificate in .cer format (without PK) and in .pfx format (with PK).

So there must be some special setting for the enrollment request for the MS CA.

Here is my code for reference, which creates a certificate and adds it to the Personal Certificates on the local computer.

    // Create enrollment request
    var enrollmentRequest = new CX509Enrollment();
    enrollmentRequest.InitializeFromRequest(certificateRequest); 

    string csr = enrollmentRequest.CreateRequest();

    // and install it back as the response
    enrollmentRequest.InstallResponse(InstallResponseRestrictionFlags.AllowUntrustedCertificate,
        csr, EncodingType.XCN_CRYPT_STRING_BASE64, ""); // no password
    // output a base64 encoded PKCS#12 so we can import it back to the .Net security classes
    var base64encoded = enrollmentRequest.CreatePFX("", // no password, this is for internal consumption
        PFXExportOptions.PFXExportChainWithRoot);

    // instantiate the target class with the PKCS#12 data (and the empty password)
    var certificate = new System.Security.Cryptography.X509Certificates.X509Certificate2(
        System.Convert.FromBase64String(base64encoded), "",
        // mark the private key as exportable (this is usually what you want to do)
        System.Security.Cryptography.X509Certificates.X509KeyStorageFlags.Exportable
    );

I also decided to post this, because you said

we'll probably be generating our own certificates

Which is actually what my code does, and it includes the PK, so just perhaps this post could be helpful.

antanta
  • 618
  • 8
  • 16
  • I'm starting to think this may need to be done sooner rather than later and this looks like a really good starting point, so thank you very much! – Corporalis Jun 02 '20 at 12:33