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.
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 { <# .Synopsis Exports certificate templates to a serialized format. .Description Exports certificate templates to a serialized format. Exported templates can be distributed and imported in another forest. .Parameter Template A collection of certificate templates to export. A collection can be retrieved by running Get-CertificateTemplate that is a part of PSPKI module: https://pspki.codeplex.com .Parameter Path Specifies the path to export. .Example $Templates = Get-CertificateTemplate -Name SmartCardV2, WebServerV3 PS C:\> 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( @" <GetPoliciesResponse xmlns="http://schemas.microsoft.com/windows/pki/2009/01/enrollmentpolicy"> <response> <policyID/> <policyFriendlyName/> <nextUpdateHours>8</nextUpdateHours> <policiesNotChanged a:nil="true" xmlns:a="http://www.w3.org/2001/XMLSchema-instance"/> <policies> "@) $script:oids = @() foreach ($temp in $Template) { [void]$SB.Append("<policy>") $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) { '<keyUsageProperty xmlns:a="http://www.w3.org/2001/XMLSchema-instance" a:nil="true"/>' } else { "<keyUsageProperty>$([int]$temp.Settings.Cryptography.CNGKeyUsage)</keyUsageProperty>" } # private key security $PKS = if ([string]::IsNullOrEmpty($temp.Settings.Cryptography.PrivateKeySecuritySDDL)) { '<permissions xmlns:a="http://www.w3.org/2001/XMLSchema-instance" a:nil="true"/>' } else { "<permissions>$($temp.Settings.Cryptography.PrivateKeySecuritySDDL)</permissions>" } # public key algorithm $KeyAlgorithm = if ($temp.Settings.Cryptography.KeyAlgorithm.Value -eq "1.2.840.113549.1.1.1") { '<algorithmOIDReference xmlns:a="http://www.w3.org/2001/XMLSchema-instance" a:nil="true"/>' } else { $kalgID = Get-OIDid $temp.Settings.Cryptography.KeyAlgorithm $PublicKeyIdGroup "<algorithmOIDReference>$kalgID</algorithmOIDReference>" } # superseded templates $superseded = if ($temp.Settings.SupersededTemplates.Length -eq 0) { '<supersededPolicies xmlns:a="http://www.w3.org/2001/XMLSchema-instance" a:nil="true"/>' } else { $str = "<supersededPolicies>" $temp.Settings.SupersededTemplates | ForEach-Object {$str += "<commonName>$_</commonName>"} $str + "</supersededPolicies>" } # list of CSPs $CSPs = if ($temp.Settings.Cryptography.ProviderList.Count -eq 0) { '<cryptoProviders xmlns:a="http://www.w3.org/2001/XMLSchema-instance" a:nil="true"/>' } else { $str = "<cryptoProviders>`n" $temp.Settings.Cryptography.ProviderList | ForEach-Object { $str += "<provider>$_</provider>`n" } $str + "</cryptoProviders>" } # 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") { '<hashAlgorithmOIDReference xmlns:a="http://www.w3.org/2001/XMLSchema-instance" a:nil="true"/>' } else { $hashID = Get-OIDid $temp.Settings.Cryptography.HashAlgorithm $HashAlgorithmGroup "<hashAlgorithmOIDReference>$hashID</hashAlgorithmOIDReference>" } # enrollment agent $RAR = if ($temp.Settings.RegistrationAuthority.SignatureCount -eq 0) { '<rARequirements xmlns:a="http://www.w3.org/2001/XMLSchema-instance" a:nil="true"/>' } else { $str = @" <rARequirements> <rASignatures>$($temp.Settings.RegistrationAuthority.SignatureCount)</rASignatures> "@ if ([string]::IsNullOrEmpty($temp.Settings.RegistrationAuthority.ApplicationPolicy.Value)) { $str += '<rAEKUs xmlns:a="http://www.w3.org/2001/XMLSchema-instance" a:nil="true"/>' } else { $raapID = Get-OIDid $temp.Settings.RegistrationAuthority.ApplicationPolicy $EKUGroup $str += @" <rAEKUs> <oIDReference>$raapID</oIDReference> </rAEKUs> "@ } if ($temp.Settings.RegistrationAuthority.CertificatePolicies.Count -eq 0) { $str += '<rAPolicies xmlns:a="http://www.w3.org/2001/XMLSchema-instance" a:nil="true"/>' } else { $str += " <rAPolicies>" $temp.Settings.RegistrationAuthority.CertificatePolicies | ForEach-Object { $raipID = Get-OIDid $_ $CertificatePolicyGroup $str += "<oIDReference>$raipID</oIDReference>`n" } $str += "</rAPolicies>`n" } $str += "</rARequirements>`n" $str } # key archival $KAS = if (!$temp.Settings.KeyArchivalSettings.KeyArchival) { '<keyArchivalAttributes xmlns:a="http://www.w3.org/2001/XMLSchema-instance" a:nil="true"/>' } else { $kasID = Get-OIDid $temp.Settings.KeyArchivalSettings.EncryptionAlgorithm $EncryptionAlgorithmGroup @" <keyArchivalAttributes> <symmetricAlgorithmOIDReference>$kasID</symmetricAlgorithmOIDReference> <symmetricAlgorithmKeyLength>$($temp.Settings.KeyArchivalSettings.KeyLength)</symmetricAlgorithmKeyLength> </keyArchivalAttributes> "@ } $sFlags = [Convert]::ToUInt32($("{0:x2}" -f [int]$temp.Settings.SubjectName),16) [void]$SB.Append( @" <policyOIDReference>$tempID</policyOIDReference> <cAs> <cAReference>0</cAReference> </cAs> <attributes> <commonName>$($temp.Name)</commonName> <policySchema>$($temp.SchemaVersion)</policySchema> <certificateValidity> <validityPeriodSeconds>$validity</validityPeriodSeconds> <renewalPeriodSeconds>$renewal</renewalPeriodSeconds> </certificateValidity> <permission> <enroll>false</enroll> <autoEnroll>false</autoEnroll> </permission> <privateKeyAttributes> <minimalKeyLength>$($temp.Settings.Cryptography.MinimalKeyLength)</minimalKeyLength> <keySpec>$([int]$temp.Settings.Cryptography.KeySpec)</keySpec> $KU $PKS $KeyAlgorithm $CSPs </privateKeyAttributes> <revision> <majorRevision>$major</majorRevision> <minorRevision>$minor</minorRevision> </revision> $superseded <privateKeyFlags>$([int]$temp.Settings.Cryptography.PrivateKeyOptions)</privateKeyFlags> <subjectNameFlags>$sFlags</subjectNameFlags> <enrollmentFlags>$([int]$temp.Settings.EnrollmentOptions)</enrollmentFlags> <generalFlags>$([int]$temp.Settings.GeneralFlags)</generalFlags> $hash $rar $KAS <extensions> "@) 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("<extension><oIDReference>$extID</oIDReference><critical>$critical</critical><value>$value</value></extension>") } [void]$SB.Append("</extensions></attributes></policy>") } [void]$SB.Append("</policies></response>") [void]$SB.Append("<oIDs>") $n = 1 $script:oids | ForEach-Object { [void]$SB.Append(@" <oID> <value>$($_.Value)</value> <group>$($_.Group)</group> <oIDReferenceID>$n</oIDReferenceID> <defaultName>$($_.Name)</defaultName> </oID> "@) $n++ } [void]$SB.Append("</oIDs></GetPoliciesResponse>") 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.
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 { <# .Synopsis Imports and registers certificate templates in Active Directory from a file. .Description Imports certificate templates from a file that contains serialized templates. Use Export-CertificateTemplate command to export and serialize certificate templates. If certificate template is successfully imported, it is installed to Active Directory. The command must be run on a Windows 7/Windows Server 2008 R2 or newer OS. Windows Server 2003 and Windows Server 2008 are not supported. Note: the command generates new object identifier (OID) for the template. Existing OID reuse is not supported. .Parameter Path Specifies the path to a file that contains exported certificate templates. .Parameter ServerName Specifies the DNS name of the Active Directory server to which the changes will be applied. If this value is NULL, the changes will be applied to the default domain controller. .Example Import-CertificateTemplate c:\temp\templates.dat #> [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.
HTH
Hi Vadims, I love this code and all the great PKI things you do. I wonder if it would be possible to update the code such that singular dat files are created for each certificate template listed - rather than just one blob. Using your example you'd then have SmartCardV2.dat and WebServerV3.dat, etc? This way gives more flexibility - I think to update / replace / etc. specific templates which is particularly helpful when going through development phases. FYI: I've previously always used LDIFDE to export and import certificate templates: Export: ldifde -m -v -d cn=%Template1%,%LDAP% -f %Template1%.ldf Import: ldifde -i -k -f %Template1%.ldf I doubt LDIFDE is a supported method but it certainly does work - tested through to 2012 R2. It also has the neat advantage that I can edit certain properties (such as issuance policy and CSP) directly in the ldf file. However, I do recognise that PowerShell is the future and want to get everything working that way. Anyway, if you were able to update the PowerShell code to make it do separate dat files great - if not, keep up the good work anyway. Cheers, Chipeater
Few notes here: 1) LDIFDE is a working solution too, however (afaik) it export unnecessary attributes which may not be compatible when transferring certificate templates between forests. 2) you SHOULD NOT modify .ldf or .dat file directly, because there is a non-trivial logic on how different template settings are related between each other. 3) you definitely can export just single template: $Templates = Get-CertificateTemplate -Name SmartCardV2 Export-CertificateTemplate $templates c:\temp\SmartCardV2.dat you just pass a single template to "Template" parameter. or do this: Get-CertificateTemplate | foreach-object {Export-CertificateTemplate -Template $_ -Path c:\temp\($_.name).dat} in this case each template will be saved to a separate file. HTH.
Hi Vadims, I agree that LDIFDE isn't the best solution - though FYI it's the -m switch on export which allows the resulting file to be "Forest-Free". The Get-CertificateTemplate | foreach-object {Export-CertificateTemplate -Template $_ -Path c:\temp\($_.name).dat} code is exactly what I was after - many thanks. I guess this seems obvious to you... but I'm tip toeing in PowerShell. Thanks again, Dave
Ok, you win. LDIFDE looks like a another option. I'm not very familiar with this tool, so I ended up with PS version. Actually, this topic was a part of my MS-XCEP protocol learning. That is, you can use any tool you prefer. In the case if you want to dive to low-level details, this PS example would be helpful.
I am trying to run these on Windows 2012 and I am not getting any output. Also when i run just the file name it doesn't output any help message as expected. I've installed the PSPKI and imported the module, get-CertificateTemplate works just fine. Any ideas what could be wrong here?
This is because you didn't attached the function via dot-sourcing. In the console, you have to run" . path\scriptfile.ps1 and then run the function.
Hi, In windows 2012, there is some issues with your scripts. I have described it in my blog post: http://wawszczak.pr0.pl/en/2014/03/09/eksportimport-szablon%c3%b3w-certyfikat%c3%b3w-z-ad-w-windows-2012/ Regards, Stanislaw Wawszczak
yeah, it is the bug with with superseded templates. However, it is the only issue I see here. I don't understand your claims about function definition and CmdletBinding attribute. Can you elaborate?
i am running under 2012. new to all this power shell stuff. But I created a copy of template. ran the export, did indeed get an xml file that had stuff. the import fails with:
Exception calling "InitializeImport" with "1" argument(s): "CX509EnrollmentPolicyWebService::InitializeImport: The input data was not in the expected format or did not
have the expected value. 0x803d0000 (-2143485952 WS_E_INVALID_FORMAT)"
At C:\temp\Import-CertificateTemplate.ps1:50 char:5
+ $pol.InitializeImport($bytes)
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : NotSpecified: (:) [], MethodInvocationException
+ FullyQualifiedErrorId : ComMethodTargetInvocation
Exception calling "GetTemplates" with "0" argument(s): "CX509EnrollmentPolicyWebService::GetTemplates: Uninitialized object 0x80040007 (-2147221497 OLE_E_BLANK)"
At C:\temp\Import-CertificateTemplate.ps1:51 char:5
+ $templates = $pol.GetTemplates() | ForEach-Object {$_}
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : NotSpecified: (:) [], MethodInvocationException
+ FullyQualifiedErrorId : ComMethodTargetInvocation
Exception calling "Initialize" with "1" argument(s): "Type mismatch. (Exception from HRESULT: 0x80020005 (DISP_E_TYPEMISMATCH))"
At C:\temp\Import-CertificateTemplate.ps1:54 char:9
+ $adwt.Initialize($_)
+ ~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : NotSpecified: (:) [], MethodInvocationException
+ FullyQualifiedErrorId : ComMethodTargetInvocation
Exception calling "Commit" with "2" argument(s): "CertEnroll::CX509CertificateTemplateADWritable::Commit: Uninitialized object 0x80040007 (-2147221497 OLE_E_BLANK)"
At C:\temp\Import-CertificateTemplate.ps1:55 char:9
+ $adwt.Commit(1,$ServerName)
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : NotSpecified: (:) [], MethodInvocationException
+ FullyQualifiedErrorId : ComMethodTargetInvocation
Vadims, I prefer using this over the LDIFDE method when importing to different forests because it's cleaner by not re-using the Template OID or having to edit the LDF file with forest specific RootDSE.
I did notice the Private Key SDDL was not exporting...looks like the ELSE statement in $PKS= section is missing "Cryptography". If i am not mistaken, "$temp.Settings.PrivateKeySecuritySDDL" should read "$temp.Settings.Cryptography.PrivateKeySecuritySDDL". Please verify my thinking is valid. Thanks for all your work here. Regards, Tim.
Tried running this code to export cert templates but nothing happens. Have installed the Public Key Infrastructure PowerShell module but do not see those even loading when i execute your code. Execution poiicy is RemoteSigned.
Only modules I see on my Cert server are:
Directory: C:\Windows\system32\WindowsPowerShell\v1.0\Modules
ModuleType Name ExportedCommands
---------- ---- ----------------
Manifest ADRMS {Update-ADRMS, Uninstall-ADRMS, Install-ADRMS}
Manifest AppLocker {Set-AppLockerPolicy, Get-AppLockerPolicy, Test-AppLockerPolicy, Get-...
Manifest BestPractices {Get-BpaModel, Invoke-BpaModel, Get-BpaResult, Set-BpaResult}
Manifest BitsTransfer {Add-BitsFile, Remove-BitsTransfer, Complete-BitsTransfer, Get-BitsTr...
Manifest CimCmdlets {Get-CimAssociatedInstance, Get-CimClass, Get-CimInstance, Get-CimSes...
Script ISE {New-IseSnippet, Import-IseSnippet, Get-IseSnippet}
Manifest Microsoft.PowerShell.Diagnostics {Get-WinEvent, Get-Counter, Import-Counter, Export-Counter...}
Manifest Microsoft.PowerShell.Host {Start-Transcript, Stop-Transcript}
Manifest Microsoft.PowerShell.Management {Add-Content, Clear-Content, Clear-ItemProperty, Join-Path...}
Manifest Microsoft.PowerShell.Security {Get-Acl, Set-Acl, Get-PfxCertificate, Get-Credential...}
Manifest Microsoft.PowerShell.Utility {Format-List, Format-Custom, Format-Table, Format-Wide...}
Manifest Microsoft.WSMan.Management {Disable-WSManCredSSP, Enable-WSManCredSSP, Get-WSManCredSSP, Set-WSM...
Script PSDiagnostics {Disable-PSTrace, Disable-PSWSManCombinedTrace, Disable-WSManTrace, E...
Binary PSScheduledJob {New-JobTrigger, Add-JobTrigger, Remove-JobTrigger, Get-JobTrigger...}
Manifest PSWorkflow {New-PSWorkflowExecutionOption, New-PSWorkflowSession, nwsn}
Manifest PSWorkflowUtility Invoke-AsWorkflow
Manifest ServerManager {Get-WindowsFeature, Add-WindowsFeature, Remove-WindowsFeature}
Manifest TroubleshootingPack {Get-TroubleshootingPack, Invoke-TroubleshootingPack}
Manifest WebAdministration {Start-WebCommitDelay, Stop-WebCommitDelay, Get-WebConfigurationLock,...
Please advise how to get this code to export our certificate templates
Hi, these commands are not part of the PSPKI module, they are available as a standalone scripts. You can copy/paste the code from this article.
just comment the line
function Export-CertificateTemplate {
and the last curly brace
}
then the script works as expected.
Regards
Hi Vadims,
Quick question, in a non XP/2003 environment, do you still need to copy the template over and/or execute PKISync ? I thought this was only a requirement when you had XP or 2003 based clients.
Regards,
Alex
Template copy between forests (in the case of cross-forest enrollment) is still necessary if you are not using certificate enrollment web services which are supported starting with Windows 7.
Hi Vadims,
not quite sure what's wrong in my lab - I've dot sourced the function, and can see it under dir function:\. No problem getting it to start. When checking the available template with Get-CertificateTemplate, I see a list of all available templates, 'FredLab OCSP Signing Response' amongst others. This is a v4 copy of the OCSP Signing Response Template. When selecting any copied Template (all copied Templates are v4), I receive the error, that 'a Template with that name' could not be found - even though I've copy/pasted it from the output of Get-CertificateTemplate. Any idea what might be going on there?
Thanks & Best Regards,
Fred
Hi Vadims,
In a Windows 2016 environment the import function throws an error "The input data was not in the expected format..."
After investigation it was the export function which causes the problem. The function checks for a number of attributes in SupersededTemplates.
Line:
$superseded = if ($temp.Settings.SupersededTemplates -eq 0) {
Change the line as follow:
$superseded = if ($temp.Settings.SupersededTemplates.Length -eq 0) {
After changing this line, the scripts work as aspected.
Thanks Ben, I fixed the code.
Hi Vadims,
First of all thanks for excelent code.
I just got a question regarding importing certificate templates into AD from xml file produced by Export-CertificateTemplate.
Importing works fine, but my problem is that native ADCS cmdlet Add-CATemplate does not see imported template immediatelly.
Restarting ADCS services does not help as well. Adding template in Certification Authority console works, and after I use console Add-CATemplate starts to see imported template as well. But I'm developing fully scripted solution to install ADCS and configure templates and GUI approach does not work for me.
Is there any way to force ADCS to read templates from AD through some COM object interface?
Thanks.
Is there any way to for
> Importing works fine, but my problem is that native ADCS cmdlet Add-CATemplate does not see imported template immediatelly.
This might be associated with internal cache of built-in Add-CATemplate.
> Importing works fine, but my problem is that native ADCS cmdlet Add-CATemplate does not see imported template immediatelly.
Hey I had the same issue, using certutil -SetCAtemplates +templateName didn't work either. I came across the git repo below and did it using Set-ADObject in the end and it worked without a delay.
https://github.com/GoateePFE/ADCSTemplate/blob/master/ADCSTemplate.psm1
I hope this helps somebody :)
> https://github.com/GoateePFE/ADCSTemplate/blob/master/ADCSTemplate.psm1
I did a review of that module previously and it has serious design issues and may make your configuration unsupported by Microsoft. I would not recommend to use it until issues are solved.
Hi!
I tried to export the certificate templates from one forest and import them in another. The OIDs of the certificate templates were different between the two forests - is this expected?
Jon
> The OIDs of the certificate templates were different between the two forests - is this expected?
I would say that yes. IX509CertificateTemplateADWritable COM interface re-generates template OIDs.
Never mind - I see "Existing OID reuse is not supported" in the Description of the Import- script.
Hi Vadims - Thanks for this post. I have a question on cross forest enrollment.
We have two way trust between the resource (has the root and issuing CA) and account forest. I am trying to copy one template from resource to account forest running pkisync.ps1 but get write error. I can see template created in account forest but its incomplete. The account used has full permissions on Certificate Template and OID containers in AD. I also tried creating the template manually directly on account forest with same name and properties but the server in account forest cant see the template in MMC for enrollment. It is unavailable and error is "The requested certificate template is not supported by this CA. A valid certification...."
The template in resource forest is added to the issuing CA and being used for enrollment by servers in resource forst. When creating manually the oid is different to one in resource forest. Does the oid need to be same between resource and account forest in this case?
It would seem that a couple places that you are testing if($temp.Settings.Cryptography.xxx -eq yyy){}else{} in your else scriptblock you reference $temp.Settings.xxx leaving out the .Cryptography bit. I can see it on lines 104/107 and 110/113.
Hello all,
I'm new in the PKI, i'm trying to get the export script working fine, but 'm having issues.
First, i have installed the PSPKI module using: Install-Module -Name PSPKI -RequiredVersion 4.0.0.
Is there another step after that ? because when i declare the variable $Templates = Get-CertificateTemplate... i ave the fllowing error:
Get-CertificateTemplate: The term Get-CertificateTemplate is not recognized as the name of cmdlet.
Many thanks in advance for your help !
Regards,
You need to import the module using Import-Module cmdlet.
I used the command:
$Templates = Get-CertificateTemplate -Name UserCertificate
.\Export-CertificateTemplate.ps1 $Templates c:\Temp\Templates.dat
But no export is happening.
I can see the certificate using Get-CertificateTemplate -Name UserCertificate.
Many thanks in advance for your help,
Regards,
Hello Folks,
First, Thanks a lot for that great work !
Is the script above compatible with Windows Server 2019 ?
I have installed, imported the module, changhe the variables but it's not exporting the certificate template while i can see them using PS command.
Thanks for your help,
Hey Vadims! Question might be slightly off topic, but Ill try it here:
I create a certificate template `Computer Autoenrollment` and use this for all machines in the domain.
The DC in my domain has 4 certs with Subject: CN=dc.example.com and one of these certs is based on my template `Computer Autoenrollment` the other three are the DC basics, Kerberos etc.
I need to be able to filter out this certificate, and to the best of my knowledge, I would very much love to use a FriendlyName but I am lost as to how I can add this to my certificate template. This value can be static, and preferably free text. Is this something you have done, or would know how to do?
Hi Vadims,
When exporting a template with compatibility settings for Windows Server 2016 CA and Certificate recipient for Windows 10/Windows Server 2016, it appears that these settings are not included in the export. Upon importing, the compatibility mode is always set to Windows Server 2012 for both CA and recipient.
Upon inspecting the output of the Get-CertificateTemplate -Name xxwebserver function, it seems that the UseLegacyProvider value is reflected in the XML as <privateKeyFlags>256</privateKeyFlags>, which is then incorporated into the template attribute msPKI-Private-Key-Flag.
However, when manually creating a template with the following compatibility settings for CA and recipient:
Windows Server 2012 / Windows 8: 67371264
Windows Server 2012 R2/ Windows 8.1: 84214016
Windows Server 2016/ Windows 10: 101056768
the values differ significantly.
Is this discrepancy a bug in the Get-CertificateTemplate function, or is there something I'm overlooking?
This is by design. I'm ignoring most significant 16 bits from private key flags when populating PrivateKeyOptions bit and read them into dedicated compatibility properties. This script was written back in 2013 when these compatibility settings barely existed and it never was updated to use them. It would be nice if you would open an issue in PSPKI repo to add this functionality into PSPKI module in a form of Export-CertificateTemplate function that would export template (or a collection of templates) into [MS-XCEP] format. Maybe a complementary Import-CertificateTemplate can be introduced as well.
Hi there,
I'm trying to import some templates provided by third party (Citrix). All the commands run fine until I try and commit. I'm logged into the Intermediate CA with an elevated domain account (tried on root CA just in case and got the same error). Server 2019.
CertEnroll::CX509CertificateTemplateADWritable::Commit: Access is denied. 0x80070005 (WIN32: 5 ERROR_ACCESS_DENIED)
Not sure where I'm going wrong or what permissions I need to review. Any thoughts?
Post your comment:
Comments: