Update: this blog is no longer active. For new posts and RSS subscriptions, please go to http://saintgimp.org.

In my current work, I have a specific scenario involving smart cards that works (roughly) as follows:

  1. Users have accounts in domain A.
  2. Administrators have accounts in domain B.
  3. Administrators need to run an application in domain B that will allow them to burn smart cards that allow users to access resources in domain A.

It turns out that it’s really hard to find decent documentation and C# sample code for doing this sort of thing.  After much web searching, experimentation, and picking the brains of people much smarter than me, I have some proof-of-concept code that I’d like to share.

The Disclaimer

First, the disclaimer.  This code is proof-of-concept only, not production-ready.  It represents only my current understanding of how things work, and is probably laughably wrong in some respects.  I’m emphatically not a smartcard, certificate, or security expert.  I’ve verified that it works on my machine, but that’s all I can promise.  Corrections welcome!

The Concept

Ok, now that’s out of the way, let’s talk about the concept.  The basic idea here goes like this:

  • The client builds a certificate request for a user in domain A.
  • The client gets a string representation of the request and sends it across the network to the server in the other domain.
  • The server component runs under the credentials of a system account that has the right to enroll on behalf of other users and has a valid enrollment agent certificate.
  • The server wraps the client’s certificate request inside another request, sets the requester name to the subject name of the client request, and signs it with its agent certificate.
  • The server submits the agent request, gets the resulting certificate string, and returns it to the client.
  • The client then saves the certificate to the smartcard.

The code uses the new-ish Certificate Enrollment API (certenroll) that’s available only on Vista+ and Windows Server 2008+.  It won’t run on XP or Server 2003.

The Code

So here it is.  I used the CopySourceAsHTML Visual Studio add-in because it works well in RSS, but the line wrapping is a bit obnoxious.  Oh well.  You’ll need to add references to two COM libraries in order to build this code:

  • CertCli 1.0 Type Library
  • CertEnroll 1.0 Type Library

using System;

using System.Collections.Generic;

using System.Text;

using System.Security.Cryptography.X509Certificates;

using System.Security.Cryptography;

using CERTENROLLLib;

using CERTCLIENTLib;

using System.Text.RegularExpressions;

namespace CertTest

{

    public enum RequestDisposition

    {

        CR_DISP_INCOMPLETE = 0,

        CR_DISP_ERROR = 0x1,

        CR_DISP_DENIED = 0x2,

        CR_DISP_ISSUED = 0x3,

        CR_DISP_ISSUED_OUT_OF_BAND = 0x4,

        CR_DISP_UNDER_SUBMISSION = 0x5,

        CR_DISP_REVOKED = 0x6,

        CCP_DISP_INVALID_SERIALNBR = 0x7,

        CCP_DISP_CONFIG = 0x8,

        CCP_DISP_DB_FAILED = 0x9

    }

    public enum Encoding

    {

        CR_IN_BASE64HEADER = 0x0,

        CR_IN_BASE64 = 0x1,

        CR_IN_BINARY = 0x2,

        CR_IN_ENCODEANY = 0xff,

        CR_OUT_BASE64HEADER = 0x0,

        CR_OUT_BASE64 = 0x1,

        CR_OUT_BINARY = 0x2

    }

    public enum Format

    {

        CR_IN_FORMATANY = 0x0,

        CR_IN_PKCS10 = 0x100,

        CR_IN_KEYGEN = 0x200,

        CR_IN_PKCS7 = 0x300,

        CR_IN_CMC = 0x400

    }

    public enum CertificateConfiguration

    {

        CC_DEFAULTCONFIG = 0x0,

        CC_UIPICKCONFIG = 0x1,

        CC_FIRSTCONFIG = 0x2,

        CC_LOCALCONFIG = 0x3,

        CC_LOCALACTIVECONFIG = 0x4,

        CC_UIPICKCONFIGSKIPLOCALCA = 0x5

    }

    class Program

    {

        static void Main(string[] args)

        {

            // Do this on the client side

            SmartCardCertificateRequest request = new SmartCardCertificateRequest("user");

            string base64EncodedRequestData = request.Base64EncodedRequestData;

            // Do this on the server side

            EnrollmentAgent enrollmentAgent = new EnrollmentAgent();

            string base64EncodedCertificate = enrollmentAgent.GetCertificate(base64EncodedRequestData);

            // Do this on the client side

            request.SaveCertificate(base64EncodedCertificate);

        }

    }

    public class SmartCardCertificateRequest

    {

        IX500DistinguishedName _subjectName;

        IX509PrivateKey _privateKey;

        IX509CertificateRequestPkcs10 _certificateRequest;

        public SmartCardCertificateRequest(string userName)

        {

            BuildSubjectNameFromCommonName(userName);

            BuildPrivateKey();

            BuildCertificateRequest();

        }

        public string Base64EncodedRequestData

        {

            get

            {

                return _certificateRequest.get_RawData(EncodingType.XCN_CRYPT_STRING_BASE64);

            }

        }

        public void SaveCertificate(string base64EncodedCertificate)

        {

            _privateKey.set_Certificate(EncodingType.XCN_CRYPT_STRING_BASE64, base64EncodedCertificate);

        }

        private void BuildSubjectNameFromCommonName(string commonName)

        {

            _subjectName = new CX500DistinguishedName();

            _subjectName.Encode("CN=" + commonName, X500NameFlags.XCN_CERT_NAME_STR_NONE);

        }

        private void BuildPrivateKey()

        {

            _privateKey = new CX509PrivateKey();

            _privateKey.Pin = "0000";

            _privateKey.ProviderName = "Microsoft Base Smart Card Crypto Provider";

            _privateKey.KeySpec = X509KeySpec.XCN_AT_SIGNATURE;

            _privateKey.Length = 1024;

            _privateKey.Silent = true;

        }

        private void BuildCertificateRequest()

        {

            _certificateRequest = new CX509CertificateRequestPkcs10();

            _certificateRequest.InitializeFromPrivateKey(X509CertificateEnrollmentContext.ContextUser, (CX509PrivateKey)_privateKey, null);

            _certificateRequest.Subject = (CX500DistinguishedName)_subjectName;

            _certificateRequest.Encode();

        }

    }

    public class EnrollmentAgent

    {

        private readonly string _certificateTemplateName = "MyTemplate";

        private readonly Regex _commonNameRegularExpression = new Regex("CN=(.+?)(?:[,/]|$)", RegexOptions.Compiled);

        public string GetCertificate(string base64EncodedRequestData)

        {

            IX509CertificateRequestPkcs10 userRequest = new CX509CertificateRequestPkcs10();

            userRequest.InitializeDecode(base64EncodedRequestData, EncodingType.XCN_CRYPT_STRING_BASE64);

            IX509CertificateRequestCmc agentRequest = BuildAgentRequest(userRequest);

            string certificate = Enroll(agentRequest);

            return certificate;

        }

        private IX509CertificateRequestCmc BuildAgentRequest(IX509CertificateRequestPkcs10 userRequest)

        {

            IX509CertificateRequestCmc agentRequest = new CX509CertificateRequestCmc();

            agentRequest.InitializeFromInnerRequestTemplateName(userRequest, _certificateTemplateName);

            agentRequest.RequesterName = GetCommonNameFromDistinguishedName(userRequest.Subject);

            agentRequest.SignerCertificates.Add((CSignerCertificate)GetSignerCertificate());

            agentRequest.Encode();

            return agentRequest;

        }

        private string GetCommonNameFromDistinguishedName(IX500DistinguishedName distinguishedName)

        {

            MatchCollection matches = _commonNameRegularExpression.Matches(distinguishedName.Name);

            if (matches.Count > 0)

            {

                return matches[0].Groups[1].Value;

            }

            else

            {

                throw new Exception("There is no common name defined in the distinguished name '" + distinguishedName.Name + "'");

            }

        }

        private ISignerCertificate GetSignerCertificate()

        {

            ISignerCertificate signerCertificate = new CSignerCertificate();

            signerCertificate.Silent = true;

            signerCertificate.Initialize(false, X509PrivateKeyVerify.VerifyNone, EncodingType.XCN_CRYPT_STRING_BASE64, GetBase64EncodedEnrollmentAgentCertificate());

            return signerCertificate;

        }

        private string GetBase64EncodedEnrollmentAgentCertificate()

        {

            X509Store store = new X509Store(StoreLocation.CurrentUser);

            store.Open(OpenFlags.ReadOnly);

            X509Certificate2Collection enrollmentCertificates = store.Certificates.Find(X509FindType.FindByTemplateName, "EnrollmentAgent", true);

            if (enrollmentCertificates.Count > 0)

            {

                X509Certificate2 enrollmentCertificate = enrollmentCertificates[0];

                byte[] rawBytes = enrollmentCertificate.GetRawCertData();

                return Convert.ToBase64String(rawBytes);

            }

            else

            {

                throw new Exception("The service account does not have an enrollment agent certificate available.");

            }

        }

        private string Enroll(IX509CertificateRequestCmc agentRequest)

        {

            ICertRequest2 requestService = new CCertRequestClass();

            string base64EncodedRequest = agentRequest.get_RawData(EncodingType.XCN_CRYPT_STRING_BASE64);

            RequestDisposition disposition = (RequestDisposition)requestService.Submit((int)Encoding.CR_IN_BASE64 | (int)Format.CR_IN_FORMATANY, base64EncodedRequest, null, GetCAConfiguration());

            if (disposition == RequestDisposition.CR_DISP_ISSUED)

            {

                string base64EncodedCertificate = requestService.GetCertificate((int)Encoding.CR_OUT_BASE64);

                return base64EncodedCertificate;

            }

            else

            {

                string message = string.Format("Failed to get a certificate for the request.  {0}", requestService.GetDispositionMessage());

                throw new Exception(message);

            }

        }

        private string GetCAConfiguration()

        {

            CCertConfigClass certificateConfiguration = new CCertConfigClass();

            return certificateConfiguration.GetConfig((int)CertificateConfiguration.CC_DEFAULTCONFIG);

        }

    }

}