Skip to content

Instantly share code, notes, and snippets.

@jborean93
Last active May 26, 2025 00:54
Show Gist options
  • Save jborean93/6f6e99737ada09062e89a1f9c9862bec to your computer and use it in GitHub Desktop.
Save jborean93/6f6e99737ada09062e89a1f9c9862bec to your computer and use it in GitHub Desktop.
Gets the Certificate Template Information from an X509Certificate2 object
# Copyright: (c) 2025, Jordan Borean (@jborean93) <[email protected]>
# MIT License (see LICENSE or https://opensource.org/licenses/MIT)
using namespace System.DirectoryServices
using namespace System.Formats.Asn1
using namespace System.Management.Automation
using namespace System.Numerics
using namespace System.Security.Cryptography.X509Certificates
Function Get-CertificateTemplateInformation {
<#
.SYNOPSIS
Get the certificate template information.
.DESCRIPTION
Gets the certificate template extension information from the extension data and try and retrieve the name from the current domain environment.
.PARAMETER Certificate
The certificate(s) to retrieve the template information for.
.EXAMPLE
Get-ChildItem Cert:\LocalMachine\My | Get-CertificateTemplateInformation
.NOTES
If the certificate does not have the extension Certificate Template Information (1.3.6.1.4.1.311.21.7) then an error will be emitted.
#>
[OutputType("CertificateTemplateInformation")]
[CmdletBinding()]
param (
[Parameter(Mandatory, ValueFromPipeline)]
[X509Certificate2[]]
$Certificate
)
begin {
$CertificateTemplateInformation = '1.3.6.1.4.1.311.21.7'
}
process {
foreach ($cert in $Certificate) {
try {
Write-Verbose -Message "Processing certificate $($cert.Thumbprint)"
$ext = $cert.Extensions | Where-Object { $_.Oid.Value -eq $CertificateTemplateInformation }
if (-not $ext) {
$err = [ErrorRecord]::new(
[ArgumentException]::new(
"Certificate $($cert.Thumbprint) does not have the Certificate Template Information extension present"),
"NoCertTemplateInfoExtension",
[ErrorCategory]::InvalidArgument,
$cert)
$PSCmdlet.WriteError($err)
continue
}
<#
https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-wcce/9da866e5-9ce9-4a83-9064-0d20af8b2ccf
CertificateTemplateOID ::= SEQUENCE {
templateID OBJECT IDENTIFIER,
templateMajorVersion INTEGER (0..4294967295) OPTIONAL,
templateMinorVersion INTEGER (0..4294967295) OPTIONAL
} --#public
#>
Write-Verbose -Message "Reading CertificateTemplateOID extension data for $($cert.Thumbprint)"
$majorVersion = $null
$minorVersion = $nulls
if ('AsnReader' -as [type]) {
# System.Formats.Asn1.* are included in pwsh 7.x but won't be
# present in WinPS unless added to the GAC.
$reader = [AsnReader]::new(
$ext.RawData,
[AsnEncodingRules]::DER).ReadSequence()
$templateId = $reader.ReadObjectIdentifier()
if ($reader.HasData) {
Write-Verbose -Message "Getting templateMajorVersion data"
$majorVersion = [Int64]$reader.ReadInteger()
}
if ($reader.HasData) {
Write-Verbose -Message "Getting templateMinorVersion data"
$minorVersion = [Int64]$reader.ReadInteger()
}
}
else {
# This has very little validation and only supports a
# smaller subset of ASN.1 features. The structures are
# simple enough that we should be able to get away with it.
# This is not every efficient but good enough.
$readerLength = $ext.RawData[1]
$sequence = $ext.RawData[2..($readerLength + 2)]
$templateIdLength = $sequence[1]
$templateIdRaw = $sequence[2..($templateIdLength + 1)]
$templateId1 = $templateIdRaw[0]
$templateId2 = $templateIdRaw[0] % 40
$templateId = [string[]]@(
$(($templateId1 - $templateId2) / 40),
$templateId2
[int64]$currentVal = 0
for ($i = 1; $i -lt $templateIdRaw.Length; $i++) {
$element = $templateIdRaw[$i]
$currentVal = ($currentVal -shl 7) + ($element -band 0x7F)
if ($element -lt 0x80) {
$currentVal
$currentVal = 0
}
}
) -join "."
$sequence = $sequence[($templateIdLength + 2)..($sequence.Length - 1)]
if ($sequence.Length) {
Write-Verbose -Message "Getting templateMajorVersion data"
$majorVersionLength = $sequence[1]
$majorVersionRaw = $sequence[2..($majorVersionLength + 1)]
[Array]::Reverse($majorVersionRaw)
$majorVersion = [int64][BigInteger]::New($majorVersionRaw)
$sequence = $sequence[($majorVersionLength + 2)..($sequence.Length - 1)]
}
if ($sequence.Length) {
Write-Verbose -Message "Getting templateMinorVersion data"
$minorVersionLength = $sequence[1]
$minorVersionRaw = $sequence[2..($minorVersionLength + 1)]
[Array]::Reverse($minorVersionRaw)
$minorVersion = [int64][BigInteger]::New($minorVersionRaw)
$sequence = $sequence[($majorVersionLength + 2)..($sequence.Length - 1)]
}
}
Write-Verbose -Message "Finding the LDAP configuration naming context for current environment"
$configRootDN = ([ADSI]"LDAP://RootDSE").configurationNamingContext
$searchBase = "LDAP://CN=Certificate Templates,CN=Public Key Services,CN=Services,$configRootDN"
$ldapFilter = "(msPKI-Cert-Template-OID=$templateId)"
Write-Verbose -Message "Performing LDAP Get under '$searchBase' with filter '$ldapFilter'"
$searcher = [DirectorySearcher]::new(
$searchBase,
$ldapFilter,
@("cn"),
[SearchScope]::OneLevel)
$templateName = $null
foreach ($result in $searcher.FindAll()) {
Write-Verbose -Message "Processing LDAP result $($result.Path)"
if ($templateName) {
$err = [ErrorRecord]::new(
[Exception]::new(
"Found multiple records for $searchBase with filter $ldapFilter when only expecting 1"),
"FoundMultipleLDAPResultsForTemplate",
[ErrorCategory]::InvalidResult,
$null)
$PSCmdlet.WriteError($err)
continue
}
$templateName = $result.Properties.cn[0]
}
[PSCustomObject]@{
PSTypeName = 'CertificateTemplateInformation'
Name = $templateName
Oid = $templateId
MajorVersion = $majorVersion
MinorVersion = $minorVersion
}
}
catch {
$PSCmdlet.WriteError($_)
}
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment