Enrolling For Smartcard Certificates Across Domains


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);

        }

    }

}

Comments (3)

  1. Paul_Kudryavtsev says:

    Hi!

    Thanks for code! But I have a problem with It. When I am trying to request certificate I get a dialog "Insert Smart Card" and for my inserted smart card It writes "The card is being shared by another process.  However, the card is not the one being requested, and cannot be used for the current operation.". What Can cause this? Is this a problem in code or It’s just a hardware problem?

  2. Aleksey says:

    Paul,

    try using different crypto provider, probably the one supplied with your smart card. I do not think anything else could prevent you from enrolling.

  3. Pavel Vomacka says:

    Hello,

    can be specified certificate validity period using NotBefore and NotAfter? I have tried to use CX509CetificateRequestCertificate instead CX509CertificateRequestPkcs10, but without success.. InitializeFromInnerRequestTemplateName throw exception with error code 0x80091004,

    thank you for help

    P.