Skip to content

Instantly share code, notes, and snippets.

@rmbolger
Last active December 7, 2023 16:30
Show Gist options
  • Save rmbolger/311736ef9ce32ff1b5c6e93b0912e015 to your computer and use it in GitHub Desktop.
Save rmbolger/311736ef9ce32ff1b5c6e93b0912e015 to your computer and use it in GitHub Desktop.
Find copies of Let's Encrypt related chain/root certificates on Windows
#Requires -Version 2.0
<#
.SYNOPSIS
Find copies of Let's Encrypt related chain/root certificates.
.DESCRIPTION
This script searches all certificate stores both for the Local Computer and all active Local User accounts with an HKEY_USERS registry hive loaded.
It does not require running as administrator, but will throw warnings for registry locations it can't read. So it is suggested to run as administrator.
It has been tested on systems as old as Windows 7 SP1 running PowerShell 2.0.
.EXAMPLE
.\Find-LetsEncryptChainCerts.ps1
Display Let's Encrypt related chain/root certificate details and their locations.
.LINK
https://gist.github.com/rmbolger/311736ef9ce32ff1b5c6e93b0912e015
#>
[CmdletBinding()]
param()
# Build a lookup table of cert store names -> cert store "friendly" names
$stores = @{
'AuthRoot' = 'Third-Party Root Certification Authorities'
'CA' = 'Intermediate Certification Authorities'
'ClientAuthIssuer' = 'Client Authentication Issuers'
'Disallowed' = 'Untrusted Certificates'
'FlightRoot' = 'Preview Build Roots'
'MY' = 'Personal'
'Remote Desktop' = 'Remote Desktop'
'REQUEST' = 'Certificate Enrollment Requests'
'ROOT' = 'Trusted Root Certification Authorities'
'SmartCardRoot' = 'Smart Card Trusted Roots'
'TestSignRoot' = 'Test Roots'
'trust' = 'Enterprise Trust'
'TrustedDevices' = 'Trusted Devices'
'TrustedPeople' = 'Trusted People'
'TrustedPublisher' = 'Trusted Publishers'
'WebHosting' = 'Web Hosting'
'Windows Live ID Token Issuer' = 'Windows Live ID Token Issuer'
'WindowsServerUpdateServices' = 'WindowsServerUpdateServices'
}
# Build a lookup table of all the LE ecosystem certs we care about.
# We're going to hard code the details so we don't have to bother
# parsing them at runtime and we can tweak the names a bit
$certs = @{
'DAC9024F54D8F6DF94935FB1732638CA6AD77C13' = @{
Name = 'DST Root CA X3'
IssuedBy = 'DST Root CA X3'
NotBefore = [DateTime]::Parse('2000-09-30T14:12:19Z')
NotAfter = [DateTime]::Parse('2021-09-30T07:01:15Z')
}
'CABD2A79A1076A31F21D253635CB039D4329A5E8' = @{
Name = 'ISRG Root X1 (self-sign)'
IssuedBy = 'ISRG Root X1'
NotBefore = [DateTime]::Parse('2015-06-04T04:04:38Z')
NotAfter = [DateTime]::Parse('2041-02-12T10:14:03Z')
}
'933C6DDEE95C9C41A40F9F50493D82BE03AD87BF' = @{
Name = 'ISRG Root X1 (cross-sign)'
IssuedBy = 'DST Root CA X3'
NotBefore = [DateTime]::Parse('2021-01-20T11:14:03Z')
NotAfter = [DateTime]::Parse('2024-09-30T11:14:03Z')
}
'BDB1B93CD5978D45C6261455F8DB95C75AD153AF' = @{
Name = 'ISRG Root X2 (self-sign)'
IssuedBy = 'ISRG Root X2'
NotBefore = [DateTime]::Parse('2020-09-03T17:00:00Z')
NotAfter = [DateTime]::Parse('2040-09-17T09:00:00Z')
}
'151682F5218C0A511C28F4060A73B9CA78CE9A53' = @{
Name = 'ISRG Root X2 (cross-sign)'
IssuedBy = 'ISRG Root X1'
NotBefore = [DateTime]::Parse('2020-09-03T17:00:00Z')
NotAfter = [DateTime]::Parse('2025-09-15T09:00:00Z')
}
'A053375BFE84E8B748782C7CEE15827A6AF5A405' = @{
Name = 'R3'
IssuedBy = 'ISRG Root X1'
NotBefore = [DateTime]::Parse('2020-09-03T17:00:00Z')
NotAfter = [DateTime]::Parse('2025-09-15T09:00:00Z')
}
'48504E974C0DAC5B5CD476C8202274B24C8C7172' = @{
Name = 'R3 (cross-sign)(retired)'
IssuedBy = 'DST Root CA X3'
NotBefore = [DateTime]::Parse('2020-10-07T12:21:40Z')
NotAfter = [DateTime]::Parse('2021-09-29T12:21:40Z')
}
'4D7F2DE64A8E44394FEC4A7DC8CDC498230DB829' = @{
Name = 'R4 (backup)'
IssuedBy = 'ISRG Root X1'
NotBefore = [DateTime]::Parse('2020-09-03T17:00:00Z')
NotAfter = [DateTime]::Parse('2025-09-15T09:00:00Z')
}
'F99ABE21CDB1823C54C272E2B4904D263E8A8BFF' = @{
Name = 'R4 (cross-sign)(backup)'
IssuedBy = 'DST Root CA X3'
NotBefore = [DateTime]::Parse('2020-10-07T12:21:45Z')
NotAfter = [DateTime]::Parse('2020-10-07T12:21:45Z')
}
'091E8EA1B256A312962AF6C140C0FBF079A407B3' = @{
Name = 'E1'
IssuedBy = 'ISRG Root X2'
NotBefore = [DateTime]::Parse('2020-09-03T17:00:00Z')
NotAfter = [DateTime]::Parse('2025-09-15T09:00:00Z')
}
'9A40DBEB347E861D523A707436225E16D5000133' = @{
Name = 'E2 (backup)'
IssuedBy = 'ISRG Root X2'
NotBefore = [DateTime]::Parse('2020-09-03T17:00:00Z')
NotAfter = [DateTime]::Parse('2025-09-15T09:00:00Z')
}
'E045A5A959F42780FA5BD7623512AF276CF42F20' = @{
Name = 'Let''s Encrypt Authority X1 (retired)'
IssuedBy = 'ISRG Root X1'
NotBefore = [DateTime]::Parse('2015-06-04T05:00:20Z')
NotAfter = [DateTime]::Parse('2020-06-04T05:00:20Z')
}
'3EAE91937EC85D74483FF4B77B07B43E2AF36BF4' = @{
Name = 'Let''s Encrypt Authority X1 (cross-sign)(retired)'
IssuedBy = 'DST Root CA X3'
NotBefore = [DateTime]::Parse('2015-10-19T15:33:36Z')
NotAfter = [DateTime]::Parse('2020-10-19T15:33:36Z')
}
'3A7A7D70C08997BC33161744A5B7C24AD8F67B1B' = @{
Name = 'Let''s Encrypt Authority X2 (retired)'
IssuedBy = 'ISRG Root X1'
NotBefore = [DateTime]::Parse('2015-06-04T05:00:31Z')
NotAfter = [DateTime]::Parse('2020-06-04T05:00:31Z')
}
'02007A05CED36899AA8A03A2CF307F1C0449FC31' = @{
Name = 'Let''s Encrypt Authority X2 (cross-sign)(retired)'
IssuedBy = 'DST Root CA X3'
NotBefore = [DateTime]::Parse('2015-10-19T15:35:01Z')
NotAfter = [DateTime]::Parse('2020-10-19T15:35:01Z')
}
'1B23675354FCAD90119D88075015EA17ADD527D8' = @{
Name = 'Let''s Encrypt Authority X3 (retired)'
IssuedBy = 'ISRG Root X1'
NotBefore = [DateTime]::Parse('2016-10-06T08:43:55Z')
NotAfter = [DateTime]::Parse('2021-10-06T08:43:55Z')
}
'E6A3B45B062D509B3382282D196EFE97D5956CCB' = @{
Name = 'Let''s Encrypt Authority X3 (cross-sign)(retired)'
IssuedBy = 'DST Root CA X3'
NotBefore = [DateTime]::Parse('2016-03-17T09:40:46Z')
NotAfter = [DateTime]::Parse('2021-03-17T09:40:46Z')
}
'77D6B59A84C605DF62B03ECB28DC168C50A93E98' = @{
Name = 'Let''s Encrypt Authority X4 (retired)'
IssuedBy = 'ISRG Root X1'
NotBefore = [DateTime]::Parse('2016-10-06T08:44:34Z')
NotAfter = [DateTime]::Parse('2021-10-06T08:44:34Z')
}
'C05E2471E589A57053F2747EE06A593C513E23A5' = @{
Name = 'Let''s Encrypt Authority X4 (cross-sign)(retired)'
IssuedBy = 'DST Root CA X3'
NotBefore = [DateTime]::Parse('2016-03-17T09:41:02Z')
NotAfter = [DateTime]::Parse('2021-03-17T09:41:02Z')
}
}
$reStore = [regex]'Certificates\\([a-zA-Z ]+)\\Cert'
$reSid = [regex]'HKEY_USERS\\([^\\]+)\\'
function Build-CertDetail {
[CmdletBinding()]
param(
[Parameter(Mandatory=$true)]
[Microsoft.Win32.RegistryKey]$CertKey
)
# can't use PSChildName on PowerShell 2.0 for some reason, so just parse the thumbprint from the path
$thumb = ($CertKey.Name.Split('\'))[-1]
# skip if the thumbprint isn't one we care about
if (-not ($cert = $certs[$thumb])) { return }
# parse the reg path for the store details
if ($CertKey.Name -like 'HKEY_LOCAL_MACHINE*') {
$context = "Local Computer"
} else {
# parse the SID and try to translate into a human-readable username
$sidString = $reSid.Match($CertKey.Name).Groups[1].Value
$sid = New-Object System.Security.Principal.SecurityIdentifier -ArgumentList $sidString
try {
$context = $sid.Translate([System.Security.Principal.NTAccount]).Value
} catch {
$context = $_
}
}
$storeName = $reStore.Match($CertKey.Name).Groups[1].Value
$storeFriendly = $stores[$storeName]
# build a PowerShell 2.0 friendly custom object
$ret = New-Object PSObject -Property @{
Context = $context
Store = $storeFriendly
Name = $cert.Name
#IssuedBy = $cert.IssuedBy
#NotBefore = $cert.NotBefore
#NotAfter = $cert.NotAfter
#Thumbprint = $thumb
}
$ret
}
# grab the HKLM certs
$allCerts = @(Get-ChildItem "HKLM:\SOFTWARE\Microsoft\SystemCertificates\*\Certificates\*")
Write-Verbose "$($allCerts.Count) visible HKLM certs"
# mount a PSDrive for HKEY_USERS if it doesn't already exist
if (-not (Get-PSDrive | Where-Object { $_.Name -eq 'HKU' })) {
Write-Verbose "Mounting HKEY_USERS to check SYSTEM user's hive"
New-PSDrive -Name HKU -PSProvider Registry -Root HKEY_USERS | Out-Null
}
# enumerate the currently loaded user hives
$hiveSIDs = (Get-Item HKU:\).GetSubKeyNames() | Where-Object {
$_ -ne '.DEFAULT' -and $_ -notlike '*_Classes'
} | Sort-Object
# add the HKU certs
$allCerts += @($hiveSIDs | ForEach-Object {
try {
Get-ChildItem "HKU:\$_\Software\Microsoft\SystemCertificates\*\Certificates\*" -ErrorAction Stop
} catch {
Write-Warning "Failed to read registry for $($_.TargetObject)"
}
})
Write-Verbose "$($allCerts.Count) total HKLM + HKU certs"
# process them all at once
$allCerts | ForEach-Object {
Build-CertDetail -CertKey $_
} |
Select-Object Context,Store,Name
@galmok
Copy link

galmok commented Dec 7, 2023

In the friendly name mapping you seem to have swapped ROOT and AuthRoot strings.

@rmbolger
Copy link
Author

rmbolger commented Dec 7, 2023

You're right and I swear I had referenced an MS authoritative doc source for the info at the time. But now I can't find it to save my life. Fixed now.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment