Windows pki certificate templates

Update 13.05.2024: fixed bugs in script

Hello S-1-1-0, Crypto Guy is on a failboat board again.

Sometimes it is useful to export a certificate template to a file for future use. For example:

Till Windows Server 2008 R2 release there was no supported way to export (or serialize) certificate template and move it out of band between two forests. With Windows Server 2008 R2 there was the only publically described way to transfer templates between two forests: AD CS: Cross-forest Certificate Enrollment with Windows Server 2008 R2. This whitepaper includes a PKISync.ps1 script (the script was written by a man who first time faced PowerShell, he-he) which copies certificate templates along other AD data between two forests. The downside of this approach is that it requires a two-way trust between forests and performs data transfer online.

In addition, Windows Server 2008 R2 introduces Enrollment Web Services. These services implement two communication protocols: MS-XCEP and MS-WSTEP.

Certificate template export

MS-XCEP is used to transfer policy information from policy (CEP) server to client. Policy information contains the following data:

Since, these protocols implement simple (comparing with low-level DCOM communications) XML over HTTPS, there is a way to reuse them manually. What you need in order to write a code that reads certificate templates from Active Directory and convert them to a XCEP-compatible XML format:

MS-CRTD contains information about certificate template structure, field meanings, dependencies and other useful information. From MS-XCEP you will find information about XML structure and XML Schema. Also, a sample XML response will be very helpful. I used these documents and got the following script:

Note: there is no built-in support for certificate templates neither in .NET or PowerShell, therefore the script relies on a PKI.Core.dll which contains a set of underlying APIs for PowerShell PKI Module and exposes a set of classes to work with certificate templates in PowerShell. If you have module installed, just import it to a current PS session. Alternatively, you can download this DLL from PSPKI project home page (in the Downloads section, select PSPKI sources) and use Add-Type cmdlet to load it to current PS session.

##################################################################### # Export-CertificateTemplate.ps1 # Version 1.0 # # Exports certificate templates to a serialized format. # # Vadims Podans (c) 2013 # http://en-us.sysadmins.lv/ ##################################################################### #requires -Version 2.0 function Export-CertificateTemplate   Export-CertificateTemplate $templates c:\temp\templates.dat #> [CmdletBinding()] param( [Parameter(Mandatory = $true)] [PKI.CertificateTemplates.CertificateTemplate[]]$Template, [Parameter(Mandatory = $true)] [IO.FileInfo]$Path ) if ($Template.Count -lt 1) throw "At least one template must be specified in the 'Template' parameter."> $ErrorActionPreference = "Stop" #region enums $HashAlgorithmGroup = 1 $EncryptionAlgorithmGroup = 2 $PublicKeyIdGroup = 3 $SigningAlgorithmIdGroup = 4 $RDNIdGroup = 5 $ExtensionAttributeGroup = 6 $EKUGroup = 7 $CertificatePolicyGroup = 8 $EnrollmentObjectGroup = 9 #endregion #region funcs function Get-OIDid ($OID,$group)  $found = $false :outer for ($i = 0; $i -lt $oids.Count; $i++)  if ($script:oids[$i].Value -eq $OID.Value)  $ID = ++$i $found = $true break outer > > if (!$found)  $script:oids += New-Object psobject -Property @ Value = $OID.Value; Group = $group; Name = $OID.FriendlyName; > $ID = $script:oids.Count > $ID > function Get-Seconds ($str)  [void]("$str" -match "(\d+)\s(\w+)") $period = $matches[1] -as [int] $units = $matches[2] switch ($units)  "hours" $period * 3600> "days" $period * 3600 * 24> "weeks" $period * 3600 * 168> "months" $period * 3600 * 720> "years" $period * 3600 * 8760> > > #endregion $SB = New-Object Text.StringBuilder [void]$SB.Append( @"    8  "@) $script:oids = @() foreach ($temp in $Template)  [void]$SB.Append("") $OID = New-Object Security.Cryptography.Oid $temp.OID.Value, $temp.DisplayName $tempID = Get-OIDid $OID $EnrollmentObjectGroup # validity/renewal $validity = Get-Seconds $temp.Settings.ValidityPeriod $renewal = Get-Seconds $temp.Settings.RenewalPeriod # key usages $KU = if ([int]$temp.Settings.Cryptography.CNGKeyUsage -eq 0)  '' > else  "$([int]$temp.Settings.Cryptography.CNGKeyUsage)" > # private key security $PKS = if ([string]::IsNullOrEmpty($temp.Settings.Cryptography.PrivateKeySecuritySDDL))  '' > else  "$($temp.Settings.Cryptography.PrivateKeySecuritySDDL)" > # public key algorithm $KeyAlgorithm = if ($temp.Settings.Cryptography.KeyAlgorithm.Value -eq "1.2.840.113549.1.1.1")  '' > else  $kalgID = Get-OIDid $temp.Settings.Cryptography.KeyAlgorithm $PublicKeyIdGroup "$kalgID" > # superseded templates $superseded = if ($temp.Settings.SupersededTemplates.Length -eq 0)  '' > else  $str = "" $temp.Settings.SupersededTemplates | ForEach-Object $str += "$_"> $str + "" > # list of CSPs $CSPs = if ($temp.Settings.Cryptography.ProviderList.Count -eq 0)  '' > else  $str = "`n" $temp.Settings.Cryptography.ProviderList | ForEach-Object  $str += "$_`n" > $str + "" > # version [void]($temp.Version -match "(\d+)\.(\d+)") $major = $matches[1] $minor = $matches[2] # hash algorithm $hash = if ($temp.Settings.Cryptography.HashAlgorithm.Value -eq "1.3.14.3.2.26")  '' > else  $hashID = Get-OIDid $temp.Settings.Cryptography.HashAlgorithm $HashAlgorithmGroup "$hashID" > # enrollment agent $RAR = if ($temp.Settings.RegistrationAuthority.SignatureCount -eq 0)  '' > else  $str = @" $($temp.Settings.RegistrationAuthority.SignatureCount) "@ if ([string]::IsNullOrEmpty($temp.Settings.RegistrationAuthority.ApplicationPolicy.Value))  $str += '' > else  $raapID = Get-OIDid $temp.Settings.RegistrationAuthority.ApplicationPolicy $EKUGroup $str += @" $raapID  "@ > if ($temp.Settings.RegistrationAuthority.CertificatePolicies.Count -eq 0)  $str += '' > else  $str += " " $temp.Settings.RegistrationAuthority.CertificatePolicies | ForEach-Object  $raipID = Get-OIDid $_ $CertificatePolicyGroup $str += "$raipID`n" > $str += "`n" > $str += "`n" $str > # key archival $KAS = if (!$temp.Settings.KeyArchivalSettings.KeyArchival)  '' > else  $kasID = Get-OIDid $temp.Settings.KeyArchivalSettings.EncryptionAlgorithm $EncryptionAlgorithmGroup @" $kasID $($temp.Settings.KeyArchivalSettings.KeyLength)  "@ > $sFlags = [Convert]::ToUInt32($("" -f [int]$temp.Settings.SubjectName),16) [void]$SB.Append( @" $tempID 0  $($temp.Name) $($temp.SchemaVersion) $validity $renewal  false false  $($temp.Settings.Cryptography.MinimalKeyLength) $([int]$temp.Settings.Cryptography.KeySpec) $KU $PKS $KeyAlgorithm $CSPs $major $minor  $superseded $([int]$temp.Settings.Cryptography.PrivateKeyOptions) $sFlags $([int]$temp.Settings.EnrollmentOptions) $([int]$temp.Settings.GeneralFlags) $hash $rar $KAS "@) foreach ($ext in $temp.Settings.Extensions)  $extID = Get-OIDid ($ext.Oid) $ExtensionAttributeGroup $critical = $ext.Critical.ToString().ToLower() $value = [Convert]::ToBase64String($ext.RawData) [void]$SB.Append("$extID$critical$value") > [void]$SB.Append("") > [void]$SB.Append("") [void]$SB.Append("") $n = 1 $script:oids | ForEach-Object  [void]$SB.Append(@" $($_.Value) $($_.Group) $n $($_.Name)  "@) $n++ > [void]$SB.Append("") Set-Content -Path $Path -Value $SB.ToString() -Encoding Ascii >

Although, the code is large, it is quite straightforward and tests certificate template properties, composes XML and saves it to a file. I didn’t bothered myself with XML formatting for brevity and it is not relevant in our case.

Certificate template import

Ok, we exported the template. How we can restore it or import it to another AD forest? Now we need to take a look at CertEnroll COM interfaces:

IX509CertificateTemplateWritable interface has a Initialize method that accepts a pointer to a IX509CertificateTemplate interface. However IX509CertificateTemplate do not contains any methods that could be used to instantiate a certificate template and implements the only read-only property that contains certificate template. Moreover, there is no appropriate public COM class for this interface. In other words, a way from nowhere to nowhere.

After careful research of related interfaces, I noticed that IX509EnrollmentPolicyServer interface (which has appropriate COM class) which implements GetTemplates method. This method returns a pointer (or pointers) to IX509CertificateTemplate interface. Luckily, there is a InitializeImport method that accepts an array of certificate templates. Since we have only XML file and the way how CryptoAPI COM interfaces works, I tried the most logical solution: read the file to a byte array and pass this array to a method and, BINGO, I got it working! I successfully initialized IX509EnrollmentPolicyServer interface object, retrieved a pointer to a IX509CertificateTemplate interface, instantiated IX509CertificateTemplateWritable interface, . PROFIT. 111oneone, hurrah!

To avoid long descriptions, I provide a small (comparing with Export-CertificateTemplate) function that illustrates all said above:

Note: in order to import certificate templates, you must run Windows 7 or Windows Server 2008 R2 at a minimum. Previous OS do not support these interfaces (as XCEP was first implemented only in mentioned OS versions)

##################################################################### # Import-CertificateTemplate.ps1 # Version 1.0 # # Imports and registers certificate templates in Active Directory from a file. # # Note: this function supports only Windows 7/Windows Server 2008 R2 and newer systems. # Vadims Podans (c) 2013 # http://en-us.sysadmins.lv/ ##################################################################### #requires -Version 2.0 function Import-CertificateTemplate   [CmdletBinding()] param( [Parameter(Mandatory = $true)] [IO.FileInfo]$Path, [Alias('DNSName','DC','DomainController','DomainControllerName','ComputerName')] [string]$ServerName ) if ( [Environment]::OSVersion.Version.Major -lt 6 -or [Environment]::OSVersion.Version.Major -eq 6 -and [Environment]::OSVersion.Version.Minor -lt 1 ) throw New-Object PlatformNotSupportedException> $bytes = [IO.File]::ReadAllBytes($Path) $pol = New-Object -ComObject X509Enrollment.CX509EnrollmentPolicyWebService $pol.InitializeImport($bytes) $templates = $pol.GetTemplates() | ForEach-Object $_> $templates | ForEach-Object  $adwt = New-Object -ComObject X509Enrollment.CX509CertificateTemplateADWritable $adwt.Initialize($_) $adwt.Commit(1,$ServerName) > >

The code do not implements any error handling, so errors may occur. To avoid errors and other unpredictable results, consider the following common restrictions:

These scripts are great examples that demonstrate new techniques in advanced PKI/ADCS management with PowerShell. Please, test them in a test environment and add error handling before you will use them in a production environment.