Dbloom
Dbloom

Reputation: 1402

Generate a certificate request and submit to a CA using only .Net

I am trying to use only .Net code to create a certificate request and submit the request to our on premise Active Directory PKI certificate authority, and get a certificate back. I have a solution that has been working for a few years, but it uses CERTCLILib and CERTENROLLLib, and I would like to shed these dependencies and port this code over to .Net 5.

These certificates are then imported onto a Yubikey device. We generate the key pair on the Yubikey and then use the public key with the CSR.

This question here Generate and Sign Certificate Request using pure .net Framework has been very helpful in getting a DER encoded CSR, but I still have a few questions that I haven't been able to figure out.

  1. How do I specify the CA and the template to use in the CertificateRequest object?
  2. I have a public key that is a RSAParameters object. How can I get that into an RSA object to use with the CertificateRequst constructor?
  3. Once I have the DER encoded CSR, how do I submit that to the CA? I can't find any classes or methods in the System.Security.Cryptography.X509Certificates namespace that accomplishes that.

Here is my current code that is working that I want to port to .NET 5. Note that DeviceDetails contains properties about the Yubikey device and the CA and template. This code is part of a larger app that provisions Yubikey devices.

public static class CertificateUtilities
{
    //private const int CC_UIPICKCONFIG = 0x1;
    private const int CR_IN_BASE64 = 0x1;
    private const int CR_IN_FORMATANY = 0;
    private const int CR_DISP_ISSUED = 0x3;
    private const int CR_DISP_UNDER_SUBMISSION = 0x5;
    private const int CR_OUT_BASE64 = 0x1;

    public static string GenerateRequest(DeviceDetails deviceDetails)
    {
        //  Create all the objects that will be required
        var objPkcs10 = new CX509CertificateRequestPkcs10();
        var objDN = new CX500DistinguishedName();
        var objObjectIds = new CObjectIds();
        var objObjectId = new CObjectId();
        var objExtensionKeyUsage = new CX509ExtensionKeyUsage();
        var objX509ExtensionEnhancedKeyUsage = new CX509ExtensionEnhancedKeyUsage();
        var objPublicKey = new CX509PublicKey();

        try
        {
            var publicKey = Utilities.ExportPublicKeyToPEMFormat(deviceDetails.PublicKey);

            publicKey = string.Join("", publicKey.Split(new[] { "\r\n" }, StringSplitOptions.RemoveEmptyEntries).Where(s => !s.StartsWith("--")));

            objPublicKey.InitializeFromEncodedPublicKeyInfo(publicKey, EncodingType.XCN_CRYPT_STRING_BASE64);

            var sha512 = new CObjectId();
            sha512.InitializeFromValue("2.16.840.1.101.3.4.2.3");

            // Initialize the PKCS#10 certificate request object based on the public key.
            objPkcs10.InitializeFromPublicKey(X509CertificateEnrollmentContext.ContextUser, objPublicKey, "");
            objPkcs10.HashAlgorithm = sha512;

            //Key Usage Extension
            objExtensionKeyUsage.InitializeEncode(
                X509KeyUsageFlags.XCN_CERT_DIGITAL_SIGNATURE_KEY_USAGE |
                X509KeyUsageFlags.XCN_CERT_NON_REPUDIATION_KEY_USAGE |
                X509KeyUsageFlags.XCN_CERT_KEY_ENCIPHERMENT_KEY_USAGE |
                X509KeyUsageFlags.XCN_CERT_DATA_ENCIPHERMENT_KEY_USAGE
            );

            objPkcs10.X509Extensions.Add((CX509Extension)objExtensionKeyUsage);

            // Enhanced Key Usage Extension
            objObjectId.InitializeFromValue("1.3.6.1.5.5.7.3.2"); // OID for Client Authentication usage
            objObjectIds.Add(objObjectId);
            objX509ExtensionEnhancedKeyUsage.InitializeEncode(objObjectIds);
            objPkcs10.X509Extensions.Add((CX509Extension)objX509ExtensionEnhancedKeyUsage);

            //  Encode the name in using the Distinguished Name object
            objDN.Encode(
                deviceDetails.CertificateDetails.Subject,
                X500NameFlags.XCN_CERT_NAME_STR_NONE
            );

            //  Adding the subject name by using the Distinguished Name object initialized above
            objPkcs10.Subject = objDN;

            //  Adding the Subject Alternate Names
            var strRfc822Name = deviceDetails.UserDetails.OUN + "@corp.com";
            var strUpn = deviceDetails.UserDetails.OUN + "@corp.com";

            var objRfc822Name = new CAlternativeName();
            var objUserPrincipalName = new CAlternativeName();
            var objAlternativeNames = new CAlternativeNames();
            var objExtensionAlternativeNames = new CX509ExtensionAlternativeNames();

            // Set Alternative RFC822 Name
            objRfc822Name.InitializeFromString(AlternativeNameType.XCN_CERT_ALT_NAME_RFC822_NAME, strRfc822Name);
            

            // Set Alternative UPN
            objUserPrincipalName.InitializeFromString(AlternativeNameType.XCN_CERT_ALT_NAME_USER_PRINCIPLE_NAME, strUpn);

            // Set Alternative Names 
            objAlternativeNames.Add(objRfc822Name);
            
            objAlternativeNames.Add(objUserPrincipalName);
            objExtensionAlternativeNames.InitializeEncode(objAlternativeNames);
            objPkcs10.X509Extensions.Add((CX509Extension)objExtensionAlternativeNames);

            // Create a CMC outer request and initialize
            var cmcReq = new CX509CertificateRequestCmc();
            cmcReq.InitializeFromInnerRequestTemplateName(objPkcs10, deviceDetails.CertificateDetails.TemplateName);              

            // encode the request
            cmcReq.Encode();
            var strRequest = cmcReq.RawData[EncodingType.XCN_CRYPT_STRING_BASE64];
            return strRequest;
        }
        catch (Exception ex)
        {
            throw new Exception(ex.Message);
        }
    }

    // Submit request to CA and get response
    public static X509Certificate2 SendRequest(DeviceDetails deviceDetails, string request, out string error)
    {
        error = "";

        //  Create all the objects that will be required
        //var objCertConfig = new CCertConfig();
        var objCertRequest = new CCertRequest();

        try
        {
            // Submit the request
            var iDisposition = objCertRequest.Submit(
                CR_IN_BASE64 | CR_IN_FORMATANY,
                request,
                null,
                deviceDetails.CertificateDetails.IssuingCa
            );

            // Check the submission status
            if (CR_DISP_ISSUED != iDisposition) // Not enrolled
            {
                var strDisposition = objCertRequest.GetDispositionMessage();

                if (CR_DISP_UNDER_SUBMISSION == iDisposition) // Pending
                {
                    error = "The submission is pending: " + strDisposition;
                    return null;
                }

                else // Failed
                {
                    error = $"The submission failed: {strDisposition}. Last status: {objCertRequest.GetLastStatus()}";
                    return null;
                }
            }

            // Get the certificate
            var strCert = objCertRequest.GetCertificate(CR_OUT_BASE64);
            var rawCert = Convert.FromBase64String(strCert);

            return new X509Certificate2(rawCert);
        }

        catch (Exception ex)
        {
            throw new Exception($"Error sending the request. {ex.Message}");
        }
    }
}

Upvotes: 2

Views: 3197

Answers (1)

bartonjs
bartonjs

Reputation: 33108

Multipart questions are hard, since they require multipart answers. Here are the parts I can answer:

How do I specify the CA and the template to use in the CertificateRequest object?

You can't, but that's OK, because you don't in the CertEnroll code, either. The CertificateRequest object is equivalent to your objPkcs10, the CA and template are for what you do with the CreateSigningRequest output.

I have a public key that is a RSAParameters object. How can I get that into an RSA object to use with the CertificateRequst constructor?

using (RSA key = RSA.Create())
{
    key.ImportParameters(rsaParameters);
    ...
}

Once I have the DER encoded CSR, how do I submit that to the CA? I can't find any classes or methods in the System.Security.Cryptography.X509Certificates namespace that accomplishes that.

There's nothing directly in the box for this. Based on the CX509CertificateRequest class name, it seems to be using Certificate Management over CMS (CMC), but then there's still the authentication and request submission parts to solve.

Upvotes: 1

Related Questions