WARNING: USE OF THE SAMPLE CODE PROVIDED IN THIS ARTICLE IS AT YOUR OWN RISK. Microsoft provides this sample code "as is" without warranty of any kind, either express or implied, including but not limited to the implied warranties of merchantability and/or fitness for a particular purpose.
In this post I would like to talk about how a developer can package a certificate template as a resource in their application to be later installed in a customer environment. This post assumes that you have good understanding of the Enterprise CAs, certificate templates, and Active Directory.
Here is one possible scenario. Let’s say your application uses a certificate for communication between its client and server components. You have specific requirements as to what that certificate should look like. Your application is meant to work in Windows environment so you want to take advantage of Windows CA and autoenrollment and have CA issue certificates for you clients so you don’t have to worry about the certificate enrollment yourself. But how do you configure CA and more importantly certificate template from your code? Before Windows Server 2008 R2 your only solution to those is to provide documentation for your admins on how to do it manually or go directly to AD using LDAP and modify certificate template objects (this option is a hack and not supported by the way).
In Windows Server 2008 R2 and Windows 7 we’ve added an API that will let developers to do the template setup in code in a supported way. As a developer you will actually need a Windows Server 2008 R2 installation in your test environment to create your resource, but your application can run on either Windows Server 2008 R2 or Windows 7. This will become clear as I go through the steps in this post.
First, a little historical perspective to understand why going directly to AD to modify certificate templates is not supported. Certificate templates have been around for a while and were not even customizable originally. When customization was added a lot of rules about what settings make sense were implemented in the certificate template MMC snap-in (certtmpl.msc). For example, you can only select to archive a private key on a certificate template that has encryption purpose. There are many more rules like that. These rules ensured that certificate templates are always “good” and allowed some assumptions to be made in the code that actually used the templates. Hence, we don’t want others to mess with certificate templates outside of our snap-in. However, as the number of applications that used certificates grew, we started receiving request to programmatically create certificate templates.
So how do you do it? At the very high level the process looks like this:
At the development time:
1. Setup a test MS PKI environment including a DC, CA, Certificate Enrollment Policy Service, and Certificate Enrollment Service.
2. Configure a template as desired.
3. Export a template.
4. Include exported template in your application and develop a code that will import it at the execution time.
At the setup execution time
1. Import exported template to your customer’s AD environment.
2. Configure a CA to issue a template.
Most of the work will have to happen during development (more importantly all of the manual work).
Before you can create and export a certificate template, you need to setup a proper environment. You need only one Windows Server 2008 R2 machine for that.
First of all, we need an Enterprise CA and of course a DC because templates are stored in Active Directory. Don’t worry about the names you use for you CA, DC, or domain. None of that stuff will be exported by our API.
Then you need to setup Certificate Enrollment Policy Service and Certificate Enrollment Service. These are new role services for the ADCS server role in Windows Server 2008 R2. To learn more about these role services see this page. The reason we need them is because we want to leverage the XML that they produce to export our template.
Now configure a template that you application needs by duplicating one of the default templates. Don’t bother with the security settings (more on that later) except for making sure that your test user account that you are going to use during export has Read and Enroll permissions on the template. If you need more than one template, configure them as well. Our API can handle exporting/importing of multiple templates.
Once you have configured the template(s), add it for issuance on your CA and remove all other templates. This will ensure that you only export what you need.
You need to write a little code to export a template. This code doesn’t need to be included in the actual application since the application only requires the import. Here is sample code that exports templates to a file:
2: // exports template from a policy server and saves the data
3: // into some file. Defaulting to a user context and kerb auth.
5: static void Export(string policyServerURL, string fileName)
8: // Get template data from policy server
10: IX509EnrollmentPolicyServer policyServer = new CX509EnrollmentPolicyWebServiceClass();
18: policyServer.SetCredential(0, X509EnrollmentAuthFlags.X509AuthKerberos, null, null);
22: // export template data and save it to a file
24: byte exportedData = (byte)policyServer.Export(
25: X509EnrollmentPolicyExportFlags.ExportOIDs | X509EnrollmentPolicyExportFlags.ExportTemplates
27: using(FileStream fs = new FileStream(fileName, FileMode.Create, FileAccess.Write, FileShare.None))
29: fs.Write(exportedData, 0, exportedData.Length);
Although, the APIs are documented in the MSDN, I will add few words on the parameters and the general flow of the code here.
First we create an instance of an object that implements IX509EnrollmentPolicyServer interface and represents a Certificate Enrollment Policy Service. We initialize it with its URL that you can get from the IIS snap-in -> Default Web Site -> properties of an application that has “PolicyProvider_CEP” in its name. The AuthFlags are set based on the authentication option you chose during Certificate Enrollment Policy Service setup. I’ve used Kerberos here as this the easiest one to deal with. The Context parameter is set to ContextUser so make sure you ACL template so that the user account executing this code has access.
Then we call SetCredential() method with a X509EnrollmentAuthFlags.X509AuthKerberos type. No other parameters need to be set as this type uses Windows Integrated authentication.
After that we call LoadPolicy() which actually retrieve the Certificate Enrollment Policy Service end point. We use X509EnrollmentPolicyLoadOption.LoadOptionReload to avoid caching. More on that later.
Finally we export the template data and associated OID objects and write them to a file. Later we can package that file into our application for import at the run time.
A word of caution on caching… If you’re playing with a template by changing its properties and trying to export it, there several caches that you should be aware of. First the cache that the APIs maintain on each client. That cache can be avoided by passing X509EnrollmentPolicyLoadOption.LoadOptionReload option to the LoadPolicy() method call as I have done above. Second cache exists on the Certificate Enrollment Policy Service side. To avoid this cache go to the IIS manager snapin -> Default Web Site -> *CEP* -> Application Setting and create a RetryIntervalMs application setting and set it to something small like 1000. This makes policy service to refresh its cache every second.
There are several tasks that you would need to do at the setup and uninstall time.
During setup process, you want to load the exported template data and import it into your customer’s Active Directory. Here is a sample code that does that:
2: // Read exported template data from the file
4: static void Import(string fileName)
7: // read exported template data from a file
9: byte importedData = null;
10: using(FileStream fs = new FileStream(fileName, FileMode.Open, FileAccess.Read, FileShare.None))
12: importedData = new byte[fs.Length];
13: fs.Read(importedData, 0, importedData.Length);
17: // Initialize policy server interface with exported data instead of connecting
18: // to a real policy server
20: IX509EnrollmentPolicyServer importPolicyServer = new CX509EnrollmentPolicyWebServiceClass();
24: // go through each imported template, set its security descriptor, and
25: // write it to Active Directory
27: IX509CertificateTemplates templates = importPolicyServer.GetTemplates();
28: foreach (IX509CertificateTemplate template in templates)
30: IX509CertificateTemplateWritable writableTemplate = new CX509CertificateTemplateADWritableClass();
32: writableTemplate.set_Property(EnrollmentTemplateProperty.TemplatePropSecurityDescriptor, SDDL);
33: writableTemplate.Commit(CommitTemplateFlags.CommitFlagSaveTemplateGenerateOID, null);
37: private const string SDDL =
38: "O:EA" + //owner is Enterprise Admins
39: "G:EA" + //group is EA as well
40: "D:PAI" + //DACL with SE_DACL_PROTECTED and SE_DACL_AUTO_INHERITED
41: "(OA;;CR;a05b8cc2-17bc-4802-a710-e7c15ab866a2;;DU)" + //autoenroll for Domain Users
42: "(OA;;RPWPCR;0e10c968-78fb-11d2-90d4-00c04f79dc55;;DA)" + //enroll for Domain Admins
43: "(OA;;RPWPCR;0e10c968-78fb-11d2-90d4-00c04f79dc55;;DU)" + //enroll for Domain Users
44: "(OA;;RPWPCR;0e10c968-78fb-11d2-90d4-00c04f79dc55;;EA)" + //enroll for Enterprise Admins
45: "(A;;CCDCLCSWRPWPDTLOSDRCWDWO;;;DA)" + //all access to Domain Admins
46: "(A;;CCDCLCSWRPWPDTLOSDRCWDWO;;;EA)" + //all access to Enterprise Admins
47: "(A;;LCRPLORC;;;AU)"; //read for Authenticated Users
First we read the exported data from the same file we used in the export sample.
Then we create an instance of an object that implements IX509EnrollmentPolicyServer and initialize it with data we have read from the file earlier. This time we don’t actually need to call LoadPolicy() method or set the authentication parameters since we don’t need to retrieve the data from the actual Certificate Enrollment Policy Service endpoint.
Finally, we iterate through the template collection and for each template in the collection we create an instance of the writable template object. These objects can actually we written to the Active Directory.
However, before we actually commit the write operation, we need to set the security descriptor property. This property is not exported (it doesn’t make sense to export it actually since it would be meaningless outside of the test environment we’ve used to create a template) and if you don’t set it you will inherit a default setting that would only allow enrollment for your domain admins. Note that at this time you can’t change other properties besides security descriptor.
When we call Commit() method to complete the write operation, we pass the CommitFlagSaveTemplateGenerateOID flag to tell the API to generate an OID for us similar to what the duplicate action does in the certificate template snap-in.
Note that in order for you to add a certificate template, you need write permissions to the Configuration\Services\Public Key Services AD container which in default setup means Enterprise Admins membership.
If you have created custom application policies (aka EKUs) in your test environment by going to the Extensions tab -> Edit -> Add… -> New…, you would need to install those in the customer environment using certutil.exe since the API at this time doesn’t support importing those. Here is how you would do it:
Certutil.exe -f -oid 126.96.36.199.188.8.131.52.9.0 MyEKU 1033 3
What this command does is registers your EKU in Active Directory. We use -f switch to allow certutil.exe to create new object. Then we provide the OID value and the display name. The 1033 is the language ID that will be used by the certificate UI when it encounters this OID in a certificate. For a list of language IDs see this MSDN page. The last parameter specifies the type of OID object we are creating which is the Application Policy type.
To check that registration has been successful in your code just check the return code from certutil.exe. Manually you can run this command:
C:\>certutil.exe -v -oid 184.108.40.206.220.127.116.11.9.0 System default Language Id:: 409 (1033) 18.104.22.168.22.214.171.124.9.0 – MyEKU pwszName = MyEKU CRYPT_ENHKEY_USAGE_OID_GROUP_ID (7) dwValue = 0 0: 1033,TestEKU2 CertUtil: -oid command completed successfully.
Once you have certificate template setup you can use ICertAadmin2::SetCAProperty() method to add your template to be issued by a CA. Here is a sample code on how to do it:
1: static void AddTemplateToCA(string templateName, string templateOid)
4: // let user to pick a CA
6: ICertConfig2 certConfig = new CCertConfigClass();
7: string caConfig = certConfig.GetConfig(0x1 /* CC_UIPICKCONFIG */);
10: // get current templates that are configured on the CA
12: ICertAdmin2 admin = new CCertAdminClass();
13: StringBuilder sb = new StringBuilder(
14: (string)admin.GetCAProperty(caConfig, 29 /* CA_PROP_TEMPLATES */, 0, 4 /*string*/, 0)
18: // add new template to the list and update the CA
24: object newTemplates = sb.ToString();
25: admin.SetCAProperty(caConfig, 29, 0, 4, ref newTemplates);
This method takes two parameters the template name and template OID. It is obvious where we get the template name (we created it at the development time), but where is the application can get the OID. You can record it right after you wrote the template to AD by calling the GetProperty() method with the EnrollmentTemplateProperty.TemplatePropOID flag.
Now let’s look at what the code does. First, we use ICertConfig::GetConfig() method to prompt a user to select a CA. Then we query for a currently configured list of templates from that CA. Finally, we modify the list by adding our template to the end of it and by setting the list back on the CA. You need to be an admin on the CA for this code to run.
When your application performs uninstall you need a way to delete a template and/or OIDs that you have created during setup.
The OID deletion is simple and can be achieved by this certutil.exe command:
Certutil.exe -oid 126.96.36.199.188.8.131.52.9.0 delete
To remove a template from a CA, you can use ICertConfig2::Next() method to iterate through all enterprise CAs and use ICertAdmin2 interface to find which CA has your template configured. Removing it from a CA is a reverse operation from what we did in the AddTemplateToCA() sample code earlier.
The deletion of a template is a little bit more involved. Here is a sample code to do it:
2: // Delete a template identified by templateCommonName paramter from AD
3: // using a DC specified by dcDnsName parameter
5: static void Delete(string templateCommonName, string dcDnsName)
8: // Get templates from AD
10: IX509EnrollmentPolicyServer adPolicyServer = new CX509EnrollmentPolicyActiveDirectoryClass();
19: IX509CertificateTemplates templates = adPolicyServer.GetTemplates();
22: // go through each template and if a match found delete from AD
24: foreach (IX509CertificateTemplate template in templates)
26: string currentTemplateCommonName =
29: if (0 == String.Compare(currentTemplateCommonName, templateCommonName, true))
31: IX509CertificateTemplateWritable writableTemplate = new CX509CertificateTemplateADWritableClass();
33: writableTemplate.Commit(CommitTemplateFlags.CommitFlagDeleteTemplate, dcDnsName);
This code looks very similar to the import case. First we create IX509EnrollmentPolicyServer instance only this time we use a CX509EnrollmentPolicyActiveDirectoryClass class. Then we get all of the templates currently in AD and go through that collection until we find the one we looking for. Once we find it we delete by calling the Commit() method with the CommitFlagDeleteTemplate flag. You need to have the delete permission on the Configuration\Services\Public Key Services AD container (specifically Certificate Templates container and OID container) for the deletion to succeed.
In my sample I’m specifying a DC name that I want APIs to work with. This is not required. You can just pass null to get a default DC. However, it is a good general practice to use a specific DC to get consistent results. Otherwise different method invocations can actually be performed against different DCs and you may run into problems with AD data not being consistent across DCs you are using.