Created
September 20, 2024 21:06
-
-
Save Bill-Stewart/49902112040180899df8028d30abe51c to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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