I originally posted this on my MSDN blog.)
In my current work, I have a specific scenario involving smart cards that works (roughly) as follows:
- Users have accounts in domain A.
- Administrators have accounts in domain B.
- 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);
}
}
}