Search
Close this search box.

Working with Active Directory Certificate Service via C#

As many of you may already know that, I’m working at a global gaming and entertainment company taking the responsible for design and implement the next generation platform which will be running on the cloud, and also design the cloud platform as well. Currently one of the goal is to replace the active directory integrated security and identity solution with certificate-based solution in our product. In short, we need to work with Active Directory Certificate Service to request and issue the certificates for vary clients, so that they can use these certificates to connect our services from any devices, such as PC, smart phone, and pad, etc., securely.

Since what we need to implement first is a certificate service that can be used by any clients. This service will be talking to the Windows Active Directory Certificate Service (AD CS) through C#.

There are many articles for IT Pro on how to install and configure the AD CS, but very few on how to communicate with AD CS by C#. In this post I will describe and demonstrate how to work with the AD CS via C#. So I will not talk much about the theory of digital certificatepublic key infrastructure and certificate authority, but will focus on how to use them.

Basic Knowledge of Certificate

Certificate, also known as public key certificate or digital certificate, is an electronic document which uses a digital signature to bind a public key with an identity. The identity could be anything. For example it could be represent a user, a device, a service or even a few lines of code.

The certificate can be used to sign the identity and could be verified by others. For example a message being signed by a certificate could be verified by the receiver, so that it will be able to know whether the message is the original one or had been modified by someone else. The certificate can also be used to encrypt and decrypt. This is the reason why we can bind a certificate on a website so that the data between the browser and server would be secured, since they are been encrypted and signed by the certificate.

The certificate authority (CA) takes the responsible to issue the certificates. In Unix people can use OpenSSL’s ca command or SuSE’s gensslcert to issue certificate. In Windows we can use the Active Directory Certificate Service.

In an enterprise there might be more than one CAs and normally they will be organized hierarchically. The top level would be the Root CA, which have a certificate signed by itself. All subordinate CAs’ certificate should be requested to and signed by the root CA.

Each CA can receive the certificate request from the client and issue them. Normally, the root CA would not be reachable by the clients since it holds the root CA certificate which is very important. Clients may send the certificate request to some subordinate CAs and get the certificate installed.

The certificate contains a key pair, which includes a private key and a public key. In order to make the private key secured, when requesting and installing the certificate, the private key should never be passed out of the client. Certificate request could be in PKCS #10 or CMC format, sent from the client to the CA. The subordinate CA received the request, and based on the request handling and policies, it will mark the request as pending status and let the administrator issue or deny manually, or automatically issue them. The certificate response would be in PKCS #7 format, signed by the CA certificate. Then the client will verify the response and combine it with the original private key to a full certificate.

So when we need to create a certificate, what we need to do is to

  • Generate the key pair and some other stuff in order to send to the CA.
  • Generate the certificate request in PKCS #10 or CMC format, and submit to CA.
  • Download the CA response.
  • Combine and install the full certificate on client based on the local key pair and the CA response.

In Windows Server 2008 R2, the AD CS introduced a new component named CES and CEP, which are Certificate Enrollment Service and Certificate Enrollment Policy. The client can communicate with CA through these web services. But in the prior version we have to use two COM: CertCli and CertEnroll.

CertCli component takes the responsible for connecting the CA server to submit the certificate request, certificate renew request and look for the request ID that CA server has. When connect from other machine the CertCli utilizes DCOM technology to invoke the CA functionality. This means, the CertCli cannot be used out of the domain or between the firewall.

CertEnroll component takes the responsible for generate the PKCS message, install and export the certificate. It doesn’t need to communicate to the CA directly.

Since in .NET we can wrap the COM and use it in managed code, we should be able to communicate with the CA by using them.

Generate Certificate Request Message to Standalone CA

There are two types of CA: enterprise CA and standalone CA. There are many differences between them. But to be simplified, the standalone CA cannot use the certificate template. In this post I will firstly demonstrate how to request the certificate to a standalone CA.

It’s only two steps to request a certificate, first one is to generate the request message, and then send the message to CA. To generate a valid certificate request message we need to use the CertEnroll COM, to send the request to need to use the CertCli COM. So let’s create a new console application and added these 2 COM components into the references.

To make sure the code runs successfully, it’s recommended to execute the sample code on the CA server, or at least the server on the same domain with the CA server. I will explain more about this later.

There are many information we need to specify or provide in order to build the request message. The first one is to select the valid cryptographic service provider (CSP). There are many CSPs built in the Windows. We can choose one of them, or we can just let the operation system retrieve all valid CSPs to us to use.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using CERTENROLLLib;

namespace ShaunXu.ADCSviaCSharp {
class Program {
 private
  string CreateCertRequestMessage() {
    var objCSPs = new CCspInformations();
    objCSPs.AddAvailableCsps();
  }

  static void Main(string[] args) {}
}
}  // namespace ADCSviaCSharp

Then we will create the key pair of the certificate. In this step we need to specify information below:

  • Length: The key length of the private key. Normally the key length should NOT less than 1024 for security consideration.
  • Key Spec: Define how this key pair, and the certificate will be used. For example digital signature or key exchange.
  • Key Usage: The key usage value will be upgrade based on the Key Spec we defined.
  • Machine Context: Specify whether the certificate will be used for current user and machine.
  • Export Policy: Specify whether the private key can be exported or not from this machine.
  • CSP Information: The valid CSPs for this key pair.

When we finished to define all information listed above we can just invoke CX509PrivateKey.Create to let the operation system generate a key pair for us. It will be stored in the machine in a “magic” folder.

var objPrivateKey = new CX509PrivateKey();
objPrivateKey.Length = 2048;
objPrivateKey.KeySpec = X509KeySpec.XCN_AT_SIGNATURE;
objPrivateKey.KeyUsage = X509PrivateKeyUsageFlags.XCN_NCRYPT_ALLOW_ALL_USAGES;
objPrivateKey.MachineContext = false;
objPrivateKey.ExportPolicy =
    X509PrivateKeyExportFlags.XCN_NCRYPT_ALLOW_EXPORT_FLAG;
objPrivateKey.CspInformations = objCSPs;
objPrivateKey.Create();

Next step, initialize the PKCS #10 object from the private we had just created. We need to specify whether the certificate should be used for current user or machine, which must be as same as the value of Machine Context that we defined in previous step. Since we will send the request to a standalone CA we will not specify the template here.

var objPkcs10 = new CX509CertificateRequestPkcs10();
objPkcs10.InitializeFromPrivateKey(X509CertificateEnrollmentContext.ContextUser,
                                   objPrivateKey, string.Empty);

Next, specify some extension information to the certificate. I will not deep into these extensions. Just one thing, all extensions in certificate will be defined by an identity named Object ID (OID). So if we want to add some extensions to the certificate we need to specify the OID rather than the name. For example, in the code below I added “Client Authentication” enhanced key usage extension to the certificate by specifying its OID “1.3.6.1.5.5.7.3.2”.

var objExtensionKeyUsage = new CX509ExtensionKeyUsage();
objExtensionKeyUsage.InitializeEncode(
    CERTENROLLLib.X509KeyUsageFlags.XCN_CERT_DIGITAL_SIGNATURE_KEY_USAGE |
    CERTENROLLLib.X509KeyUsageFlags.XCN_CERT_NON_REPUDIATION_KEY_USAGE |
    CERTENROLLLib.X509KeyUsageFlags.XCN_CERT_KEY_ENCIPHERMENT_KEY_USAGE |
    CERTENROLLLib.X509KeyUsageFlags.XCN_CERT_DATA_ENCIPHERMENT_KEY_USAGE);
objPkcs10.X509Extensions.Add((CX509Extension)objExtensionKeyUsage);

var objObjectId = new CObjectId();
var objObjectIds = new CObjectIds();
var objX509ExtensionEnhancedKeyUsage = new CX509ExtensionEnhancedKeyUsage();
objObjectId.InitializeFromValue("1.3.6.1.5.5.7.3.2");
objObjectIds.Add(objObjectId);
objX509ExtensionEnhancedKeyUsage.InitializeEncode(objObjectIds);
objPkcs10.X509Extensions.Add((CX509Extension)objX509ExtensionEnhancedKeyUsage);

Next, we will specify the subject of the certificate. As I mentioned earlier, a certificate can represent anything. So the subject will take the information of what is being identified y this certificate. There are some fields in subject:

  • CN: Common Name
  • C: Country (Must be 2 letter.)
  • S: State
  • L: Locality
  • O: Organization
  • OU: Organization Unit
  • E: Email

We can define one or more fields when request the certificate and it will combine in the format like this.

[Field_Name_1] = [Field_Value_1], [Field_Name_2] = [Field_Value_2], [Field_Name_3] = [Field_Value_3]

For example this is a valid subject with the CN, C, S, L, O and OU defined.

CN = UIX, OU = NAS, O = IGT, L = Reno, S = Nevada, C = US

To specify the subject in C# we also need to provide them into the same format, and set into the PKCS #10 object.

var objDN = new CX500DistinguishedName();
var subjectName =
    "CN = shaunxu.me, OU = ADCS, O = Blog, L = Beijng, S = Beijing, C = CN";
objDN.Encode(subjectName, X500NameFlags.XCN_CERT_NAME_STR_NONE);
objPkcs10.Subject = objDN;

Finally we initialize the CertEnroll COM object by passing the PKCS #10 in and invoke its CreateRequest method to generate the certificate request in base64 format.

var objEnroll = new CX509Enrollment();
objEnroll.InitializeFromRequest(objPkcs10);
var strRequest = objEnroll.CreateRequest(EncodingType.XCN_CRYPT_STRING_BASE64);

So the full code for certificate request generation would be like this.

private
string CreateCertRequestMessage() {
  var objCSPs = new CCspInformations();
  objCSPs.AddAvailableCsps();

  var objPrivateKey = new CX509PrivateKey();
  objPrivateKey.Length = 2048;
  objPrivateKey.KeySpec = X509KeySpec.XCN_AT_SIGNATURE;
  objPrivateKey.KeyUsage = X509PrivateKeyUsageFlags.XCN_NCRYPT_ALLOW_ALL_USAGES;
  objPrivateKey.MachineContext = false;
  objPrivateKey.ExportPolicy =
      X509PrivateKeyExportFlags.XCN_NCRYPT_ALLOW_EXPORT_FLAG;
  objPrivateKey.CspInformations = objCSPs;
  objPrivateKey.Create();

  var objPkcs10 = new CX509CertificateRequestPkcs10();
  objPkcs10.InitializeFromPrivateKey(
      X509CertificateEnrollmentContext.ContextUser, objPrivateKey,
      string.Empty);

  var objExtensionKeyUsage = new CX509ExtensionKeyUsage();
  objExtensionKeyUsage.InitializeEncode(
      CERTENROLLLib.X509KeyUsageFlags.XCN_CERT_DIGITAL_SIGNATURE_KEY_USAGE |
      CERTENROLLLib.X509KeyUsageFlags.XCN_CERT_NON_REPUDIATION_KEY_USAGE |
      CERTENROLLLib.X509KeyUsageFlags.XCN_CERT_KEY_ENCIPHERMENT_KEY_USAGE |
      CERTENROLLLib.X509KeyUsageFlags.XCN_CERT_DATA_ENCIPHERMENT_KEY_USAGE);
  objPkcs10.X509Extensions.Add((CX509Extension)objExtensionKeyUsage);

  var objObjectId = new CObjectId();
  var objObjectIds = new CObjectIds();
  var objX509ExtensionEnhancedKeyUsage = new CX509ExtensionEnhancedKeyUsage();
  objObjectId.InitializeFromValue("1.3.6.1.5.5.7.3.2");
  objObjectIds.Add(objObjectId);
  objX509ExtensionEnhancedKeyUsage.InitializeEncode(objObjectIds);
  objPkcs10.X509Extensions.Add(
      (CX509Extension)objX509ExtensionEnhancedKeyUsage);

  var objDN = new CX500DistinguishedName();
  var subjectName =
      "CN = shaunxu.me, OU = ADCS, O = Blog, L = Beijng, S = Beijing, C = CN";
  objDN.Encode(subjectName, X500NameFlags.XCN_CERT_NAME_STR_NONE);
  objPkcs10.Subject = objDN;

  var objEnroll = new CX509Enrollment();
  objEnroll.InitializeFromRequest(objPkcs10);
  var strRequest =
      objEnroll.CreateRequest(EncodingType.XCN_CRYPT_STRING_BASE64);
  return strRequest;
}

Send Certificate Request to CA

Send the certificate request message we had just generated to a CA would be easy. In fact we can save the message into a text file and copy to the CA server, request the certificate by using the CA manage portal. But if we are going to use C# then we will need to use CertCli COM to send the message, and verify the status by retrieving the disposition status and request ID.

First we will create the object of CertCli and invoke its Submit method by passing the certificate request message and CA address. The CA address should be in the format:

  • [CA_SERVER_IP]\[CA_NAME]
  • [CA_SERVER_NAME]\[CA_NAME]

The CA name can be found by logging on the CA server and navigate to the Active Directory Certificate Service node in Server Manager window. Right click the CA node and select Properties.

The code would be like this.

private
static int SendCertificateRequest(string message) {
  var objCertRequest = new CCertRequest();
  var iDisposition =
      objCertRequest.Submit(CR_IN_BASE64 | CR_IN_FORMATANY, message,
                            string.Empty, @"192.168.56.101\pal-CPAL-CA");
}

The return value of the Submit method indicates the status of the certificate request, normally it would be in the statuses below.

  • 0x03: Issued. This means the certificate had been issued by the CA that we can download and install to the local machine.
  • 0x05: Under submission. This means the request was in pending status, the certificate administrator need to issue it manually.
  • Failed due to some reason. We can use CCertRequest.GetDispositionMessage method to retrieve the failure reason.
switch (iDisposition) {
  case CR_DISP_ISSUED:
    Console.WriteLine("The certificate had been issued.");
    break;
  case CR_DISP_UNDER_SUBMISSION:
    Console.WriteLine("The certificate is still pending.");
    break;
  default:
    Console.WriteLine("The submission failed: " +
                      objCertRequest.GetDispositionMessage());
    Console.WriteLine("Last status: " +
                      objCertRequest.GetLastStatus().ToString());
    break;
}
return objCertRequest.GetRequestId();

Download and Install Certificate

Once the certificate request had been sent, it will be processed by CA request handling module. By default, for standalone CA all certificate requests will be at pending status and wait for the administrator to issue manually. The administrator should go to the CA portal and select the Pending Requests node, right click on the item and click Issue. (The administrator can click Deny if he/she don’t want to send this certificate.)

Then go to the Issued Certificates node we can see the issued certificate available. The certificate couldn’t be downloaded and installed into the machine where it requested unless in this status.

Back to the source to implement the code to download and install the full certificate. First of all, we will utilize the CCertRequest.RetrievePending method to detect the status of our certificate request had sent. If it’s issued then we will download the response, which is in PKCS #7 format, to the local machine by using the method CCertRequest.GetCertificate.

private
static void DownloadAndInstallCert(int requestId) {
  var objCertRequest = new CCertRequest();
  var iDisposition =
      objCertRequest.RetrievePending(requestId, @"192.168.56.101\pal-CPAL-CA");

  if (iDisposition == CR_DISP_ISSUED) {
    var cert = objCertRequest.GetCertificate(CR_OUT_BASE64 | CR_OUT_CHAIN);
  }
}

Then initialize the CertEnroll object from context user certificate store, install the response that we had just retrieved.

var objEnroll = new CX509Enrollment();
objEnroll.Initialize(X509CertificateEnrollmentContext.ContextUser);
objEnroll.InstallResponse(InstallResponseRestrictionFlags.AllowUntrustedRoot,
                          cert, EncodingType.XCN_CRYPT_STRING_BASE64, null);
Console.WriteLine("The certificate had been installed successfully.");

After it downloaded and installed the certificate we can check it’s in the current user certificate store. And from now on, since the certificate was in the store, we can use X509Store and X509Certificate2 class to export and view the attributes such as subject, thumbprint, etc..

The full code is listed below.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using CERTCLIENTLib;
using CERTENROLLLib;

namespace ShaunXu.ADCSviaCSharp {
class Program {
 private
  static string CreateCertRequestMessage() {
    var objCSPs = new CCspInformations();
    objCSPs.AddAvailableCsps();

    var objPrivateKey = new CX509PrivateKey();
    objPrivateKey.Length = 2048;
    objPrivateKey.KeySpec = X509KeySpec.XCN_AT_SIGNATURE;
    objPrivateKey.KeyUsage =
        X509PrivateKeyUsageFlags.XCN_NCRYPT_ALLOW_ALL_USAGES;
    objPrivateKey.MachineContext = false;
    objPrivateKey.ExportPolicy =
        X509PrivateKeyExportFlags.XCN_NCRYPT_ALLOW_EXPORT_FLAG;
    objPrivateKey.CspInformations = objCSPs;
    objPrivateKey.Create();

    var objPkcs10 = new CX509CertificateRequestPkcs10();
    objPkcs10.InitializeFromPrivateKey(
        X509CertificateEnrollmentContext.ContextUser, objPrivateKey,
        string.Empty);

    var objExtensionKeyUsage = new CX509ExtensionKeyUsage();
    objExtensionKeyUsage.InitializeEncode(
        CERTENROLLLib.X509KeyUsageFlags.XCN_CERT_DIGITAL_SIGNATURE_KEY_USAGE |
        CERTENROLLLib.X509KeyUsageFlags.XCN_CERT_NON_REPUDIATION_KEY_USAGE |
        CERTENROLLLib.X509KeyUsageFlags.XCN_CERT_KEY_ENCIPHERMENT_KEY_USAGE |
        CERTENROLLLib.X509KeyUsageFlags.XCN_CERT_DATA_ENCIPHERMENT_KEY_USAGE);
    objPkcs10.X509Extensions.Add((CX509Extension)objExtensionKeyUsage);

    var objObjectId = new CObjectId();
    var objObjectIds = new CObjectIds();
    var objX509ExtensionEnhancedKeyUsage = new CX509ExtensionEnhancedKeyUsage();
    objObjectId.InitializeFromValue("1.3.6.1.5.5.7.3.2");
    objObjectIds.Add(objObjectId);
    objX509ExtensionEnhancedKeyUsage.InitializeEncode(objObjectIds);
    objPkcs10.X509Extensions.Add(
        (CX509Extension)objX509ExtensionEnhancedKeyUsage);

    var objDN = new CX500DistinguishedName();
    var subjectName =
        "CN = shaunxu.me, OU = ADCS, O = Blog, L = Beijng, S = Beijing, C = CN";
    objDN.Encode(subjectName, X500NameFlags.XCN_CERT_NAME_STR_NONE);
    objPkcs10.Subject = objDN;

    var objEnroll = new CX509Enrollment();
    objEnroll.InitializeFromRequest(objPkcs10);
    var strRequest =
        objEnroll.CreateRequest(EncodingType.XCN_CRYPT_STRING_BASE64);
    return strRequest;
  }

 private
  const int CC_DEFAULTCONFIG = 0;
 private
  const int CC_UIPICKCONFIG = 0x1;
 private
  const int CR_IN_BASE64 = 0x1;
 private
  const int CR_IN_FORMATANY = 0;
 private
  const int CR_IN_PKCS10 = 0x100;
 private
  const int CR_DISP_ISSUED = 0x3;
 private
  const int CR_DISP_UNDER_SUBMISSION = 0x5;
 private
  const int CR_OUT_BASE64 = 0x1;
 private
  const int CR_OUT_CHAIN = 0x100;

 private
  static int SendCertificateRequest(string message) {
    var objCertRequest = new CCertRequest();
    var iDisposition =
        objCertRequest.Submit(CR_IN_BASE64 | CR_IN_FORMATANY, message,
                              string.Empty, @"192.168.56.101\pal-CPAL-CA");

    switch (iDisposition) {
      case CR_DISP_ISSUED:
        Console.WriteLine("The certificate had been issued.");
        break;
      case CR_DISP_UNDER_SUBMISSION:
        Console.WriteLine("The certificate is still pending.");
        break;
      default:
        Console.WriteLine("The submission failed: " +
                          objCertRequest.GetDispositionMessage());
        Console.WriteLine("Last status: " +
                          objCertRequest.GetLastStatus().ToString());
        break;
    }
    return objCertRequest.GetRequestId();
  }

 private
  static void DownloadAndInstallCert(int requestId) {
    var objCertRequest = new CCertRequest();
    var iDisposition = objCertRequest.RetrievePending(
        requestId, @"192.168.56.101\pal-CPAL-CA");

    if (iDisposition == CR_DISP_ISSUED) {
      var cert = objCertRequest.GetCertificate(CR_OUT_BASE64 | CR_OUT_CHAIN);
      var objEnroll = new CX509Enrollment();
      objEnroll.Initialize(X509CertificateEnrollmentContext.ContextUser);
      objEnroll.InstallResponse(
          InstallResponseRestrictionFlags.AllowUntrustedRoot, cert,
          EncodingType.XCN_CRYPT_STRING_BASE64, null);
      Console.WriteLine("The certificate had been installed successfully.");
    }
  }

  static void Main(string[] args) {
    Console.WriteLine("Request a new certificate? (y|n)");
    if (Console.ReadLine() == "y") {
      var request = CreateCertRequestMessage();
      var id = SendCertificateRequest(request);
      Console.WriteLine("Request ID: " + id.ToString());
    }

    Console.WriteLine("Download & install certificate? (y|n)");
    if (Console.ReadLine() == "y") {
      Console.WriteLine("Request ID?");
      var id = int.Parse(Console.ReadLine());
      DownloadAndInstallCert(id);
    }
  }
}
}  // namespace ADCSviaCSharp

Enterprise CA and Certificate Template

In the section above we discussed on how to use C# to communicate with AD CS, that is a standalone CA. A standalone CA has some limitation comparing with the enterprise CA. The biggest difference is that, the standalone CA cannot use the certificate templates.

When we implement the certificate request function, we specified everything the certificate needs. And for a CA there’s no way to define what kind of information can be set by request, what policy should the request follow. And there’s no way to define how long the certificate will be valid, which is the validity period, as well. All certificates issued by a standalone CA will have the same validity period, which is defined at the register in CA server. But if we are using enterprise CA, we can define vary rules and validity period in each template.

The enterprise templates are stored in the active directory, which means all CAs in the AD can select which templates they can use. This is a good way to control the certificate issuing permission.

You can verify if a CA is enterprise or not by opening the CA portal. If there’s a sub folder named Certificate Templates it means this is an enterprise CA.

Let’s create a template and specify some rules. Click the Certificate Templates node which under the Active Directory Certificate Service node and select a template named Computer. Right click the template and click Duplicate Template.

You can not create a brand new template. Instead you have to duplicate an existing template.

Select Windows Server 2008 Enterprise version on the popping up windows and specify a template name. In the template properties window we can see that it’s possible to define the validity period of it. All certificates that requested and issued on this template will have the same validity period.

And there are many items we can define as well. For example we can have the “Client Authenticate” in the application policies extension, which we had specified in code in previous sample.

And we can define what kind of value can be set to the subject in certificates. This provides a good way for certificate administrator to control the value of the certificates. For example, if the certificate is to represent a domain user, the subject must be a valid AD user. But in this case we will let the request supply the subject which means no control on CA side.

Once we created the template we also need to issue this template to this CA server, which means it can be received and issued by this CA. Right click the Certificate Templates node and select New > Certificate Template to Issue, and select the template we have just created.

Now the template is ready for use. Then we will change our code to send request to enterprise CA with template specified.

Send Request to Enterprise CA with Template

Send the certificate request to an enterprise CA would be very similar as what we did on a standalone CA. Previously when we generated the key pair we used an empty string on the template name parameter. So now for enterprise CA we will specify which template we are going to use.

var objPkcs10 = new CX509CertificateRequestPkcs10();
objPkcs10.InitializeFromPrivateKey(X509CertificateEnrollmentContext.ContextUser,
                                   objPrivateKey, "ShaunXu");

It’s not allowed to request a certificate without template specified on an enterprise CA. This means we have to set a template. On the other hand, standalone CA does not allow the request related with a template.

Seems that we finished, but if we just execute it will throw an exception to us, said that the file exists when adding some extensions.

The exception message could be a little bit confusing. In fact this is because we defined something which had been defined in the certificate template. If we dig into the source code we can see that the exception occurred when we added the key usage extension.

And if we get back to the CA server and open the template we are using, we can find that the key usage had been defined in the template. This means in the code, or in the certificate request we should not specify it again.

Hence we need to comment the code for adding the key usage, also we need to comment the enhanced key usage part since it had been defined in the template, too. Because we let the request supply the subject name so here we can still specify the subject information in the request. The method for generating request message would be like this.

private
static string CreateCertRequestMessage() {
  var objCSPs = new CCspInformations();
  objCSPs.AddAvailableCsps();

  var objPrivateKey = new CX509PrivateKey();
  objPrivateKey.Length = 2048;
  objPrivateKey.KeySpec = X509KeySpec.XCN_AT_SIGNATURE;
  objPrivateKey.KeyUsage = X509PrivateKeyUsageFlags.XCN_NCRYPT_ALLOW_ALL_USAGES;
  objPrivateKey.MachineContext = false;
  objPrivateKey.ExportPolicy =
      X509PrivateKeyExportFlags.XCN_NCRYPT_ALLOW_EXPORT_FLAG;
  objPrivateKey.CspInformations = objCSPs;
  objPrivateKey.Create();

  var objPkcs10 = new CX509CertificateRequestPkcs10();
  objPkcs10.InitializeFromPrivateKey(
      X509CertificateEnrollmentContext.ContextUser, objPrivateKey, "ShaunXu");

  // var objExtensionKeyUsage = new CX509ExtensionKeyUsage();
  // objExtensionKeyUsage.InitializeEncode(
  //     CERTENROLLLib.X509KeyUsageFlags.XCN_CERT_DIGITAL_SIGNATURE_KEY_USAGE |
  //     CERTENROLLLib.X509KeyUsageFlags.XCN_CERT_NON_REPUDIATION_KEY_USAGE |
  //     CERTENROLLLib.X509KeyUsageFlags.XCN_CERT_KEY_ENCIPHERMENT_KEY_USAGE |
  //     CERTENROLLLib.X509KeyUsageFlags.XCN_CERT_DATA_ENCIPHERMENT_KEY_USAGE);
  // objPkcs10.X509Extensions.Add((CX509Extension)objExtensionKeyUsage);

  // var objObjectId = new CObjectId();
  // var objObjectIds = new CObjectIds();
  // var objX509ExtensionEnhancedKeyUsage = new
  // CX509ExtensionEnhancedKeyUsage();
  // objObjectId.InitializeFromValue("1.3.6.1.5.5.7.3.2");
  // objObjectIds.Add(objObjectId);
  // objX509ExtensionEnhancedKeyUsage.InitializeEncode(objObjectIds);
  // objPkcs10.X509Extensions.Add((CX509Extension)objX509ExtensionEnhancedKeyUsage);

  var objDN = new CX500DistinguishedName();
  var subjectName =
      "CN = entprise.shaunxu.me, OU = ADCS, O = Blog, L = Beijng, S = Beijing, "
      "C = CN";
  objDN.Encode(subjectName, X500NameFlags.XCN_CERT_NAME_STR_NONE);
  objPkcs10.Subject = objDN;

  var objEnroll = new CX509Enrollment();
  objEnroll.InitializeFromRequest(objPkcs10);
  var strRequest =
      objEnroll.CreateRequest(EncodingType.XCN_CRYPT_STRING_BASE64);
  return strRequest;
}

It works well this time and installed the certificate successfully. If we open the certificate store in MMC we can see the new one with the template displayed.

Certificate Renewal

A certificate must have a limited validity period. For example the certificate we had just request before is valid through 2012-01-13 07:21:48 to 20124-01-12 07:21:48.

When the certificate is going to be expired the operation system will send the renew request to the CA server automatically to attempt renew it. But we can ask to renew it by our code.

To send a certificate renewal message we must have this certificate installed in the certificate store. It could be in local machine or current user store. The first step is to find it by using the X509Store.Certificates.Find.

private
static int Renew() {
  X509Certificate2 certificate = null;
  X509Store store = new X509Store(StoreLocation.CurrentUser);
  try {
    store.Open(OpenFlags.ReadWrite);
    certificate = store.Certificates.Find(
        X509FindType.FindByThumbprint,
        "c1555218deed2c6dbe5101178617ef7628388a85", false)[0];
  } catch (Exception ex) {
    Console.WriteLine(ex.ToString());
  } finally {
    store.Close();
  }
}

The certificate renew request is in PKCS #7 format. So in the next step, we will create an object of PKCS #7 and initialize it from the certificate we had just found from the certificate store. When initializing we’d specify that this is a renew request in the parameter. We also need to specify that the new certificate will inherit the validity period and the key pair from the existing one.

var objPkcs7 = new CX509CertificateRequestPkcs7();
objPkcs7.InitializeFromCertificate(
    X509CertificateEnrollmentContext.ContextUser, true,
    Convert.ToBase64String(certificate.RawData),
    EncodingType.XCN_CRYPT_STRING_BASE64,
    X509RequestInheritOptions.InheritPrivateKey& X509RequestInheritOptions
        .InheritValidityPeriodFlag);

Then the following code would be very similar with what we did to send the new request before. Using the CertEnroll to generate the request message and send it out by CertCli, and check the disposition status.

var objEnroll = new CX509Enrollment();
objEnroll.InitializeFromRequest(objPkcs7);
var message = objEnroll.CreateRequest(EncodingType.XCN_CRYPT_STRING_BASE64);

var objCertRequest = new CCertRequest();
var iDisposition =
    objCertRequest.Submit(CR_IN_BASE64 | CR_IN_FORMATANY, message, string.Empty,
                          @"192.168.56.101\pal-CPAL-CA");

switch (iDisposition) {
  case CR_DISP_ISSUED:
    Console.WriteLine("The certificate had been issued.");
    break;
  case CR_DISP_UNDER_SUBMISSION:
    Console.WriteLine("The certificate is still pending.");
    break;
  default:
    Console.WriteLine("The submission failed: " +
                      objCertRequest.GetDispositionMessage());
    Console.WriteLine("Last status: " +
                      objCertRequest.GetLastStatus().ToString());
    break;
}
return objCertRequest.GetRequestId();

When the request had been sent to the CA, based on the request handling policy it will be issued automatically or manually by the administrator. To download and install the renewed certificate would be the same like what we did before, so just use the method that download the new certificate should be fine.

The full code would be like this. Just note that I hard-coded my certificate thumbprint in the code.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using CERTCLIENTLib;
using CERTENROLLLib;
using System.Security.Cryptography.X509Certificates;

namespace ShaunXu.ADCSviaCSharp {
class Program {
 private
  static string CreateCertRequestMessage() {
    var objCSPs = new CCspInformations();
    objCSPs.AddAvailableCsps();

    var objPrivateKey = new CX509PrivateKey();
    objPrivateKey.Length = 2048;
    objPrivateKey.KeySpec = X509KeySpec.XCN_AT_SIGNATURE;
    objPrivateKey.KeyUsage =
        X509PrivateKeyUsageFlags.XCN_NCRYPT_ALLOW_ALL_USAGES;
    objPrivateKey.MachineContext = false;
    objPrivateKey.ExportPolicy =
        X509PrivateKeyExportFlags.XCN_NCRYPT_ALLOW_EXPORT_FLAG;
    objPrivateKey.CspInformations = objCSPs;
    objPrivateKey.Create();

    var objPkcs10 = new CX509CertificateRequestPkcs10();
    objPkcs10.InitializeFromPrivateKey(
        X509CertificateEnrollmentContext.ContextUser, objPrivateKey, "ShaunXu");

    // var objExtensionKeyUsage = new CX509ExtensionKeyUsage();
    // objExtensionKeyUsage.InitializeEncode(
    //     CERTENROLLLib.X509KeyUsageFlags.XCN_CERT_DIGITAL_SIGNATURE_KEY_USAGE
    //     | CERTENROLLLib.X509KeyUsageFlags.XCN_CERT_NON_REPUDIATION_KEY_USAGE
    //     | CERTENROLLLib.X509KeyUsageFlags.XCN_CERT_KEY_ENCIPHERMENT_KEY_USAGE
    //     |
    //     CERTENROLLLib.X509KeyUsageFlags.XCN_CERT_DATA_ENCIPHERMENT_KEY_USAGE);
    objPkcs10.X509Extensions.Add((CX509Extension)objExtensionKeyUsage);

    // var objObjectId = new CObjectId();
    // var objObjectIds = new CObjectIds();
    // var objX509ExtensionEnhancedKeyUsage = new
    // CX509ExtensionEnhancedKeyUsage();
    // objObjectId.InitializeFromValue("1.3.6.1.5.5.7.3.2");
    // objObjectIds.Add(objObjectId);
    // objX509ExtensionEnhancedKeyUsage.InitializeEncode(objObjectIds);
    // objPkcs10.X509Extensions.Add((CX509Extension)objX509ExtensionEnhancedKeyUsage);

    var objDN = new CX500DistinguishedName();
    var subjectName =
        "CN = entprise.shaunxu.me, OU = ADCS, O = Blog, L = Beijng, S = "
        "Beijing, C = CN";
    objDN.Encode(subjectName, X500NameFlags.XCN_CERT_NAME_STR_NONE);
    objPkcs10.Subject = objDN;

    var objEnroll = new CX509Enrollment();
    objEnroll.InitializeFromRequest(objPkcs10);
    var strRequest =
        objEnroll.CreateRequest(EncodingType.XCN_CRYPT_STRING_BASE64);
    return strRequest;
  }

 private
  const int CC_DEFAULTCONFIG = 0;
 private
  const int CC_UIPICKCONFIG = 0x1;
 private
  const int CR_IN_BASE64 = 0x1;
 private
  const int CR_IN_FORMATANY = 0;
 private
  const int CR_IN_PKCS10 = 0x100;
 private
  const int CR_DISP_ISSUED = 0x3;
 private
  const int CR_DISP_UNDER_SUBMISSION = 0x5;
 private
  const int CR_OUT_BASE64 = 0x1;
 private
  const int CR_OUT_CHAIN = 0x100;

 private
  static int SendCertificateRequest(string message) {
    var objCertRequest = new CCertRequest();
    var iDisposition =
        objCertRequest.Submit(CR_IN_BASE64 | CR_IN_FORMATANY, message,
                              string.Empty, @"192.168.56.101\pal-CPAL-CA");

    switch (iDisposition) {
      case CR_DISP_ISSUED:
        Console.WriteLine("The certificate had been issued.");
        break;
      case CR_DISP_UNDER_SUBMISSION:
        Console.WriteLine("The certificate is still pending.");
        break;
      default:
        Console.WriteLine("The submission failed: " +
                          objCertRequest.GetDispositionMessage());
        Console.WriteLine("Last status: " +
                          objCertRequest.GetLastStatus().ToString());
        break;
    }
    return objCertRequest.GetRequestId();
  }

 private
  static void DownloadAndInstallCert(int requestId) {
    var objCertRequest = new CCertRequest();
    var iDisposition = objCertRequest.RetrievePending(
        requestId, @"192.168.56.101\pal-CPAL-CA");

    if (iDisposition == CR_DISP_ISSUED) {
      var cert = objCertRequest.GetCertificate(CR_OUT_BASE64 | CR_OUT_CHAIN);
      var objEnroll = new CX509Enrollment();
      objEnroll.Initialize(X509CertificateEnrollmentContext.ContextUser);
      objEnroll.InstallResponse(
          InstallResponseRestrictionFlags.AllowUntrustedRoot, cert,
          EncodingType.XCN_CRYPT_STRING_BASE64, null);
      Console.WriteLine("The certificate had been installed successfully.");
    }
  }

 private
  static int Renew() {
    X509Certificate2 certificate = null;
    X509Store store = new X509Store(StoreLocation.CurrentUser);
    try {
      store.Open(OpenFlags.ReadWrite);
      certificate = store.Certificates.Find(
          X509FindType.FindByThumbprint,
          "c1555218deed2c6dbe5101178617ef7628388a85", false)[0];
    } catch (Exception ex) {
      Console.WriteLine(ex.ToString());
    } finally {
      store.Close();
    }

    var objPkcs7 = new CX509CertificateRequestPkcs7();
    objPkcs7.InitializeFromCertificate(
        X509CertificateEnrollmentContext.ContextUser, true,
        Convert.ToBase64String(certificate.RawData),
        EncodingType.XCN_CRYPT_STRING_BASE64,
        X509RequestInheritOptions.InheritPrivateKey &
            X509RequestInheritOptions.InheritValidityPeriodFlag);

    var objEnroll = new CX509Enrollment();
    objEnroll.InitializeFromRequest(objPkcs7);
    var message = objEnroll.CreateRequest(EncodingType.XCN_CRYPT_STRING_BASE64);

    var objCertRequest = new CCertRequest();
    var iDisposition =
        objCertRequest.Submit(CR_IN_BASE64 | CR_IN_FORMATANY, message,
                              string.Empty, @"192.168.56.101\pal-CPAL-CA");

    switch (iDisposition) {
      case CR_DISP_ISSUED:
        Console.WriteLine("The certificate had been issued.");
        break;
      case CR_DISP_UNDER_SUBMISSION:
        Console.WriteLine("The certificate is still pending.");
        break;
      default:
        Console.WriteLine("The submission failed: " +
                          objCertRequest.GetDispositionMessage());
        Console.WriteLine("Last status: " +
                          objCertRequest.GetLastStatus().ToString());
        break;
    }
    return objCertRequest.GetRequestId();
  }

  static void Main(string[] args) {
    Console.WriteLine("Request a new certificate? (y|n)");
    if (Console.ReadLine() == "y") {
      var request = CreateCertRequestMessage();
      var id = SendCertificateRequest(request);
      Console.WriteLine("Request ID: " + id.ToString());
    }

    Console.WriteLine("Download & install certificate? (y|n)");
    if (Console.ReadLine() == "y") {
      Console.WriteLine("Request ID?");
      var id = int.Parse(Console.ReadLine());
      DownloadAndInstallCert(id);
    }

    Console.WriteLine("Renew an existing certificate? (y|n)");
    if (Console.ReadLine() == "y") {
      var id = Renew();
      Console.WriteLine("Request ID: " + id.ToString());
    }

    Console.WriteLine("Download & install renewed certificate? (y|n)");
    if (Console.ReadLine() == "y") {
      Console.WriteLine("Request ID?");
      var id = int.Parse(Console.ReadLine());
      DownloadAndInstallCert(id);
    }
  }
}
}  // namespace ADCSviaCSharp

After executed and back to the certificate store we can see the certificate renewed by the CA, which its validity period had been changed from 2012-01-13 07:21:48 – 20124-01-12 7:21:48 to 2012-01-13 07:52:36 – 20124-01-12 07:52:36. The old certificate had been archived by the operation system automatically.

Request Certificate Out of Domain

As I mentioned before, the sample code in this post should be executed in the same server of CA, or at least a server in the CA’s domain. This is the limitation when using CertCli and CertEnroll to communication with CA.

First of all, CA integrated with active directory. By default, only the authenticated user can request certificate. Secondly, if we are using enterprise CA, all templates are being stored in the AD. When the client request a new certificate with template specified, it will try to retrieve the template information from AD.

So before Windows Server 2008 R2 it would be very difficult to communicate to the CA from the client that out of the domain. This is why, in the beginning of this post I mentioned, that I’m working on a WCF web service working as a proxy to let the client (PC, laptop and mobile) connect and request certificates out side from the domain.

But if we have the Windows Server 2008 R2, it introduced a new component of AD CS which called Certificate Enrollment Web Services. Basically it includes two web services that wraps the LDAP invoke and DCOM invoke, so that the client can communicate with them through HTTPS with WS-Trust.

Summary

When I was beginning to work on this task I found there is very little information on the internet about how to communicate with the CA by C#, or even by code. I think this is because, CA is something related with IT Pro that more focused on how to install and configure. IT Pro doesn’t care about the code. Communicating from C# is more related with development but developer doesn’t care about the CA since it’s something about IT infrastructure. So this topic is in the middle of the two worlds – IT and development.

But I think when we move to the cloud computing, the enterprise application, most of them we need to migration the existing AD integrated architecture to certificate-based architecture, which need to replace the existing security, authentication, identification parts.

In this post I introduced a little bit background knowledge about CA, especially the AD CS. I demonstrated how to request, install and renew a certificate to a standalone and an enterprise CA by C# through COM. I also mentioned a little bit about the new Certificate Enrollment Web Service. Thanks to the great post and articles I  referred recently, this and this.

There are still some topics I didn’t cover. For example the online revocation list, SCEP, OCSP, etc.. We need them if we need to build a fully, robust, online certificate solution.

Hope this helps,

Shaun

This article is part of the GWB Archives. Original Author: Shaun Xu

Related Posts