Last active
December 7, 2023 16:30
-
-
Save rmbolger/311736ef9ce32ff1b5c6e93b0912e015 to your computer and use it in GitHub Desktop.
Find copies of Let's Encrypt related chain/root certificates on Windows
This file contains 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
#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 |
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
In the friendly name mapping you seem to have swapped ROOT and AuthRoot strings.