Skip to content

Instantly share code, notes, and snippets.

@Bill-Stewart
Created September 20, 2024 21:06
Show Gist options
  • Save Bill-Stewart/49902112040180899df8028d30abe51c to your computer and use it in GitHub Desktop.
Save Bill-Stewart/49902112040180899df8028d30abe51c to your computer and use it in GitHub Desktop.
# Get-X509Value.ps1
#
# Written By Bill Stewart (bstewart AT iname.com)
#
# This script uses 'certutil -scinfo -silent' to get all smart card certificate
# SHA1 hashes (aka "thumbprints") from an inserted smart card. Of these
# certificates, the script selects those certificates that are used for smart
# card logon and have a valid date, and presents a selectable list of
# certificates to the user. After the user selects a certificate, the script
# outputs an object with the following properties:
#
# * Thumbprint - The certificate's thumbprint
# * Issuer = The certificate's issuer
# * Subject - The certificate's subject
# * SerialNumber - The certificate's serial number
# * X509Value - The value to use for the altSecurityIdentities attribute
#
# The X509Value is the "X509IssuerSerialNumber" format recommended for the
# altSecurityIdentities user attribute in Microsoft article KB5014754
# ("Certificate-based authentication changes on Windows domain controllers"):
#
# X509:<I>IssuerName<SR>SerialNumber
#
# The altSecurityIdentities attribute has two requirements for the certificate
# mapping to work correctly:
#
# 1. You must reverse the order of the name elements in the certificate
# issuer's distinguished name, and
# 2. You must reverse the order of the bytes in the certificate's serial
# number.
#
# For example, suppose a certificate has the following properties:
#
# * Issuer: CN=FABRIKAM-CA, DC=fabrikam, DC=local
# * Serial number: 090B080A
#
# For these certificate properties, the altSecurityIdentities attribute would
# be set to the following value:
#
# X509:<I>DC=local,DC=fabrikam,CN=FABRIKAM-CA<SR>0A080B09
#
# Version History
#
# 2024-09-20
# * Initial version.
#requires -version 5.1
#------------------------------------------------------------------------------
# BEGIN: Global variables and objects
#------------------------------------------------------------------------------
# For testing/debugging - run 'certutil -scinfo -silent > certutiloutput.txt'
# (useful in cases where accessing smart card certificates is slow)
$TEST_MODE = $false
# Path for certificate store
$CERTIFICATE_PATH = "Cert:\CurrentUser\My"
# Pathname COM object (implements IADsPathname interface but lacks typelib)
$ADS_SETTYPE_DN = 4
$Pathname = New-Object -ComObject "Pathname" -ErrorAction Stop
#------------------------------------------------------------------------------
# END: Global variables and objects
#------------------------------------------------------------------------------
#------------------------------------------------------------------------------
# BEGIN: Function definitions
#------------------------------------------------------------------------------
# Invokes a method for a COM object that lacks a typelib (Pathname)
function Invoke-Method {
param(
[__ComObject]
$object,
[String]
$method,
$params
)
$result = $object.GetType().InvokeMember($method,"InvokeMethod",$null,$object,$params)
if ( $null -ne $result ) {
$result
}
}
# Outputs a distinguished name in reverse order; e.g.:
# 'CN=C,CN=B,CN=A' is returned as 'CN=A,CN=B,CN=C'
function Get-DNReverse {
param(
[String]
$distinguishedName
)
Invoke-Method $Pathname "Set" ($distinguishedName,$ADS_SETTYPE_DN)
$numElements = Invoke-Method $Pathname "GetNumElements"
if ( $numElements -eq 0 ) {
return
}
$result = Invoke-Method $Pathname "GetElement" ($numElements - 1)
for ( $i = $numElements - 2; $i -ge 0; $i-- ) {
$result += ",{0}" -f (Invoke-Method $Pathname "GetElement" $i)
}
$result
}
# Outputs hexadecimal byte string in reverse byte order; e.g:
# '04030201' is returned as '01020304'
function Get-ReverseByteString {
param(
[String]
$inputString
)
if ( ($inputString.Length % 2) -ne 0 ) {
Write-Error "Byte string has an unknown format." -Category InvalidArgument
return
}
$result = $inputString.Substring($inputString.Length - 2)
for ( $i = $inputString.Length - 4; $i -ge 0; $i -= 2 ) {
$result += $inputString.Substring($i,2)
}
$result
}
# Gets user input (Y/N/Q)
function Get-YNQ {
param(
[String]
$prompt
)
while ( $true ) {
$value = Read-Host $prompt
if ( $value -ne "" ) {
$value = $value.Substring(0,1)
if ( "Y","N","Q" -contains $value ) {
break
}
}
}
$value
}
# Parses output of 'certutil -scinfo -silent' to 1) make sure a smart card is
# inserted and 2) outputs a de-duplicated list of SHA1 certificate hashes
# (thumbprints) on the smart card
function Get-SmartCardCertThumbprints {
$stopwatch = New-Object Diagnostics.Stopwatch
$stopwatch.Start()
while ( $true ) {
Write-Host -NoNewline "Looking for smart card certificates; please wait..."
$inserted = $false
if ( -not $TEST_MODE ) {
$certutilOutput = certutil -scinfo -silent
}
else {
$certutilOutput = Get-Content "certutiloutput.txt" -ErrorAction Stop
}
if ( $null -ne $certutilOutput ) {
$reMatches = [Regex]::Match($certutilOutput,'Status: SCARD_STATE_(\S+)',"IgnoreCase")
if ( $null -ne $reMatches ) {
$inserted = $reMatches.Groups[1].Value -eq "PRESENT"
}
}
if ( $inserted ) {
break
}
else {
Write-Host
$answer = Get-YNQ "Smart card not detected. Try again? [Y/N] "
if ( "N","Q" -contains $answer ) {
return
}
}
$stopwatch.Restart()
}
$stopwatch.Stop()
# Hashtable for filtering out duplicate thumbprints
$thumbprints = @{}
$reMatches = [Regex]::Matches($certutilOutput,'Cert Hash\(sha1\): (\S+)',"IgnoreCase")
foreach ( $reMatch in $reMatches ) {
$thumbprint = $reMatch.Groups[1].Value
if ( -not $thumbprints.Contains($thumbprint) ) {
$thumbprint
$thumbprints.Add($thumbprint,"")
}
}
Write-Host ("done [{0}]" -f $stopwatch.Elapsed)
}
# Use relative column widths for certificate display/selection based on buffer
# size width. -9 is to accommodate the spaces between columns and a few extra
# columns so things look right no matter whether in the console or ISE.
$COL1_WIDTH = 2 # certificate number for selection
$COL4_WIDTH = 10 # date
$COL2_WIDTH = [Math]::Truncate(($Host.UI.RawUI.BufferSize.Width - $COL1_WIDTH - $COL4_WIDTH - 9) / 2) -as [Int]
$COL3_WIDTH = $COL2_WIDTH
# Outputs a string up to a maximum width
function Get-StringMaxWidth {
param(
[String]
$s,
[Int]
$maxWidth
)
if ( $s.Length -gt $maxWidth ) {
"{0}..." -f $s.Substring(0,$maxWidth - 3)
}
else {
$s
}
}
# Outputs a certificate's subject, issuer, and expiration date
# using formatted columns
function Get-CertDescription {
param(
$cert
)
$subject = Get-StringMaxWidth $cert.Subject $COL2_WIDTH
$issuer = Get-StringMaxWidth $cert.Issuer $COL3_WIDTH
$expiration = Get-StringMaxWidth ("{0:MM/dd/yyyy}" -f $cert.NotAfter) $COL4_WIDTH
" {0,-$COL2_WIDTH} {1,-$COL3_WIDTH} {2,-$COL4_WIDTH}" -f $subject,$issuer,$expiration
}
# Outputs a ordered hashtable based on thumbprint values
# key = number; value = thumbprint and description
function Get-CertList {
param(
[String[]]
$thumbprints
)
$result = [Ordered] @{}
for ( $i = 0; $i -lt $thumbprints.Count; $i++ ) {
$cert = Get-Item ("{0}\{1}" -f $CERTIFICATE_PATH,$thumbprints[$i])
$target = [PSCustomObject] @{
"Thumbprint" = $cert.Thumbprint
"Description" = Get-CertDescription $cert
}
$result.Add($i,$target)
}
$result
}
# Displays a list of certificates and prompts for a selection; outputs the
# thumbprint of the selected certificate
function Get-CertChoice {
param(
[String[]]
$thumbprints
)
$certList = Get-CertList $thumbprints
Write-Host ("{0,-$COL1_WIDTH} {1,-$COL2_WIDTH} {2,-$COL3_WIDTH} {3,-$COL4_WIDTH}" -f
"#","Subject","Issuer","Expiration")
Write-Host ("{0,-$COL1_WIDTH} {1,-$COL2_WIDTH} {2,-$COL3_WIDTH} {3,-$COL4_WIDTH}" -f
"--","-------","------","----------")
for ( $i = 0; $i -lt $certList.Count; $i++ ) {
Write-Host ("{0,-$COL1_WIDTH}{1}" -f $i,$certList[$i].Description)
}
Write-Host
$choice = -1
while ( $true ) {
$value = Read-Host -Prompt "Select a certificate number (Q to quit)"
if ( $value -ne "" ) {
if ( $value.Substring(0,1) -eq "Q" ) {
$choice = -1
break
}
$choice = $value -as [Int]
if ( $null -ne $choice ) {
if ( ($choice -ge 0) -and ($choice -le $certList.Count - 1) ) {
break
}
}
Write-Host "Invalid selection"
}
}
if ( $choice -ge 0 ) {
$certList[$choice].Thumbprint
}
}
#------------------------------------------------------------------------------
# END: Function definitions
#------------------------------------------------------------------------------
# Get thumbprints (SHA1 hashes) of smart cart certificates
$Thumbprints = Get-SmartCardCertThumbprints
if ( $null -eq $Thumbprints ) {
return
}
# Certificates must be in current user store
$Certs = $Thumbprints | ForEach-Object {
Get-Item ("{0}\{1}" -f $CERTIFICATE_PATH,$_) -ErrorAction SilentlyContinue
}
if ( $null -eq $Certs ) {
Write-Warning "None of the smart card certificates were found in the current user's certificate store. Please verify that the user running the script is the same user that inserted the smart card."
return
}
# Select only smart card logon certificates with valid dates
$Thumbprints = $Certs |
Where-Object {
($_.EnhancedKeyUsageList.FriendlyName -eq "Smart Card Logon") -and
((Get-Date) -ge $_.NotBefore) -and ((Get-Date) -le ($_.NotAfter))
} | Sort-Object NotAfter -Descending | Select-Object -ExpandProperty Thumbprint
if ( $null -eq $Thumbprints ) {
Write-Warning "No certificates found matching criteria."
return
}
# Show user list of certificates and allow selection
Write-Host
$Thumbprint = Get-CertChoice $Thumbprints
if ( $null -eq $Thumbprint ) {
return
}
# Get the certificate the user selected
$Cert = Get-Item ("{0}\{1}" -f $CERTIFICATE_PATH,$Thumbprint)
[PSCustomObject] @{
"Thumbprint" = $Cert.Thumbprint
"Issuer" = $Cert.Issuer
"Subject" = $Cert.Subject
"SerialNumber" = $Cert.SerialNumber
"X509Value" = "X509:<I>{0}<SR>{1}" -f (Get-DNReverse $Cert.Issuer),(Get-ReverseByteString $Cert.SerialNumber)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment