Security Certificate enrollment: Creating a certificate signing request by using CertEnroll

In the last posts, we looked at the basics of certificate enrollment, how to manually create a certificate signing request, and Windows’ APIs for key and certificate management. This post will look at how you can programmatically create a certificate signing requests by using the Certificate Enrollment API (CertEnroll).

Using CertEnroll is by no means the only way to create a certificate signing request (CSR) on Windows. Notable alternatives include BouncyCastle, System.Security.Cryptography.X509Certificates, or the OpenSSL C API. However, if you want your CSR to show up in the Certificate Management MMC snap-in, or if you plan to interact with Active Directory Certificate Services (AD CS), then using CertEnroll is the way to go.

Steps to create a certificate signing request

Regardless of the library you select, the basic steps to generate a CSR programmatically are:

  1. Create a private/public key pair to initialize the CSR. You are free to decide which key store to store this keypair in. If in doubt, use the Microsoft Software Key Storage Provider CNG Key Storage Provider.
  2. Set the subject.
  3. Set hash algorithm to SHA256.
  4. Restrict what the certificate can be used for. This requires both, a key usage extension and a enhanced key usage extension.
  5. Add any subject alternative names (if any).
  6. Generate the certificate signing request and emit it in PEM format.

Creating a CSR by using CertEnroll

The following code demonstrates how you to use the CertEnroll API to generate a CSR:

class CertificateSigningRequestWithCertEnroll
{
    private const string ServerAuthenticationOid = "1.3.6.1.5.5.7.3.1";

    public IX509PrivateKey PrivateKey { get; }
    public string CertificateSigningRequest { get; }

    public CertificateSigningRequestWithCertEnroll(
        IX509PrivateKey privateKey,
        string requestPem)
    {
        this.PrivateKey = privateKey;
        this.CertificateSigningRequest = requestPem;
    }

    /// <summary>
    /// Helper: Create a new public/private key pair by using CNG. The keypair
    /// can be either created in the machine context ("Local Machine") or
    /// user context ("Current User").
    /// </summary>
    private static IX509PrivateKey CreatePrivateKey(bool machineContext, int keyLength)
    {
        var key = new CX509PrivateKey
        {
            ProviderName = "Microsoft Software Key Storage Provider",   // Use CNG, not CryptoAPI
            MachineContext = machineContext,
            Length = keyLength,
            KeySpec = X509KeySpec.XCN_AT_SIGNATURE,
            KeyUsage = X509PrivateKeyUsageFlags.XCN_NCRYPT_ALLOW_SIGNING_FLAG
        };

        key.Create();
        return key;
    }

    /// <summary>
    /// Create an OID idenifying a given hash algorithm.
    /// </summary>
    private static CObjectId CreateHashAlgorithm(string hash)
    {
        var algorithm = new CObjectId();
        algorithm.InitializeFromAlgorithmName(ObjectIdGroupId.XCN_CRYPT_HASH_ALG_OID_GROUP_ID,
            ObjectIdPublicKeyFlags.XCN_CRYPT_OID_INFO_PUBKEY_ANY,
            AlgorithmFlags.AlgorithmFlagsNone, hash);
        return algorithm;
    }

    /// <summary>
    /// Create an extension attribute that contains subject alternative names.
    /// </summary>
    private static IX509ExtensionAlternativeNames CreateAlternativeNamesExtension(
        IEnumerable<string> subjectAlternativeNames)
    {
        var alternativeNames = new CAlternativeNames();
        foreach (var name in subjectAlternativeNames)
        {
            var alternativeName = new CAlternativeName();
            alternativeName.InitializeFromString(
                AlternativeNameType.XCN_CERT_ALT_NAME_DNS_NAME, name);
            alternativeNames.Add(alternativeName);
        }

        var alternativeNamesExtension = new CX509ExtensionAlternativeNames();
        alternativeNamesExtension.InitializeEncode(alternativeNames);
        return alternativeNamesExtension;
    }

    /// <summary>
    /// Create an extension that defines what the certificate can be used for.
    /// </summary>
    private static IX509ExtensionKeyUsage CreateKeyUsageExtension()
    {
        var extension = new CX509ExtensionKeyUsage();
        extension.InitializeEncode(
            X509KeyUsageFlags.XCN_CERT_DIGITAL_SIGNATURE_KEY_USAGE |
            X509KeyUsageFlags.XCN_CERT_DATA_ENCIPHERMENT_KEY_USAGE |
            X509KeyUsageFlags.XCN_CERT_KEY_ENCIPHERMENT_KEY_USAGE);
        extension.Critical = true;
        return extension;
    }

    /// <summary>
    /// Create an extension that defines what the certificate can be used for.
    /// </summary>
    private static IX509ExtensionEnhancedKeyUsage CreateEnhancedKeyUsageExtension(IEnumerable<string> oids)
    {
        var objectIds = new CObjectIds();
        foreach (var oid in oids)
        {
            var objectId = new CObjectId();
            objectId.InitializeFromValue(oid);
            objectIds.Add(objectId);
        }

        var extension = new CX509ExtensionEnhancedKeyUsage();
        extension.InitializeEncode(objectIds);
        return extension;
    }

    /// <summary>
    /// Create a new key pair and certificate signing request.
    /// </summary>
    public static CertificateSigningRequestWithCertEnroll Create(
        string friendlyName,
        string subject,
        IEnumerable<string> subjectAlternativeNames,
        bool machineContext, 
        int keyLength)
    {
        var privateKey = CreatePrivateKey(machineContext, keyLength);

        // (1) Create key pair.
        var csr = new CX509CertificateRequestPkcs10();
        csr.InitializeFromPrivateKey(
            machineContext
                ? X509CertificateEnrollmentContext.ContextMachine
                : X509CertificateEnrollmentContext.ContextUser,
            privateKey,
            string.Empty);

        // (2) Set subject.
        var subjectDn = new CX500DistinguishedName();
        subjectDn.Encode(subject, X500NameFlags.XCN_CERT_NAME_STR_NONE);
        csr.Subject = subjectDn;

        // (3) Set hash algorithm.
        csr.HashAlgorithm = CreateHashAlgorithm("SHA256");

        // (4) Set key usage.
        csr.X509Extensions.Add((CX509Extension)CreateKeyUsageExtension());
        csr.X509Extensions.Add((CX509Extension)CreateEnhancedKeyUsageExtension(
            new[] { ServerAuthenticationOid }));

        // (5) Add alternative names (if any).
        if (subjectAlternativeNames != null)
        {
            csr.X509Extensions.Add((CX509Extension)CreateAlternativeNamesExtension(subjectAlternativeNames));
        }


        // (6) Generate the CSR.
        var enrollment = new CX509Enrollment();
        enrollment.CertificateFriendlyName = friendlyName;
        enrollment.InitializeFromRequest(csr);

        return new CertificateSigningRequestWithCertEnroll(
            privateKey,
            enrollment.CreateRequest(EncodingType.XCN_CRYPT_STRING_BASE64REQUESTHEADER));
    }
}

Creating a test program

With the code to generate a CSR in place, we can add a little test program that generates a CSR and writes it to the console in PEM format:

class Program
{
    static void Main(string[] args)
    {
        var csr = CertificateSigningRequestWithCertEnroll.Create(
            "Test Certificate",
            "CN=Test",
            new[] { "CN=Alternate Name" },
            false,
            2048);

        Console.WriteLine(csr.CertificateSigningRequest);
    }
}

Running the program generates the following output, just as we’d expect:

-----BEGIN NEW CERTIFICATE REQUEST-----
MIIDpjCCAo4CAQAwDzENMAsGA1UEAwwEVGVzdDCCASIwDQYJKoZIhvcNAQEBBQAD
...
znSlsT38SwzUg6i0k4efMaDp59telakQBMA=
-----END NEW CERTIFICATE REQUEST-----

As a side-effect of using CertEnroll, the CSR now also shows up under Certificate Enrollment Requests > Certificates in the Certificate Management MMC snap-in. If you double-click the entry, you can also inspect and verify that the CSR contains the expected information such as the subject alternative names:

CSR in certificate manager

Any opinions expressed on this blog are Johannes' own. Refer to the respective vendor’s product documentation for authoritative information.
« Back to home