ENGINEERING

How to audit the 2023 Secure Boot certs across a Windows fleet

Scripts you can run right now to find out which machines took the new certificates and which ones didn’t. No agent, no account, nothing leaves the box.

Microsoft’s Secure Boot certificates from 2011 are aging out through 2026, and machines need the 2023 replacements in their firmware to keep trusting boot components. If you run a Windows fleet you’ve probably already got this on a list somewhere.

We shipped a careful way to apply the update back in June. Since then we’ve spent a while on the other half of the problem - proving it landed - and the documented thing everyone reports on has a quiet blind spot. Here’s what’s behind it, and the scripts to check your own machines.

Check one machine right now

Run this as administrator. It reads only - nothing is changed, nothing is sent anywhere.

$X509 = [guid]'a5c059a1-94e4-4aa7-87b5-ab155c2bf072'

function Get-DbCerts {
    try { $bytes = (Get-SecureBootUEFI -Name db -ErrorAction Stop).Bytes }
    catch { return $null }          # not UEFI, or not elevated

    $certs = @(); $off = 0
    while ($off + 28 -le $bytes.Length) {
        $type     = [guid]::new([byte[]]$bytes[$off..($off+15)])
        $listSize = [BitConverter]::ToUInt32($bytes, $off+16)
        $hdrSize  = [BitConverter]::ToUInt32($bytes, $off+20)
        $sigSize  = [BitConverter]::ToUInt32($bytes, $off+24)
        if ($listSize -lt 28 -or $listSize -gt ($bytes.Length - $off)) { break }
        if ($type -eq $X509 -and $sigSize -gt 16) {
            $start = $off + 28 + $hdrSize
            for ($i = 0; $i -lt [int](($listSize - 28 - $hdrSize) / $sigSize); $i++) {
                $o = $start + ($i * $sigSize)
                if ($o + $sigSize -gt $bytes.Length) { break }
                try {
                    $c = [Security.Cryptography.X509Certificates.X509Certificate2]::new(
                             [byte[]]$bytes[($o+16)..($o+$sigSize-1)])
                    $certs += [pscustomobject]@{ Subject  = $c.GetNameInfo('SimpleName',$false)
                                                 NotAfter = $c.NotAfter }
                } catch { }
            }
        }
        $off += $listSize
    }
    $certs
}

$bios  = Get-CimInstance Win32_BIOS
$certs = Get-DbCerts

Write-Host "`n  $env:COMPUTERNAME"
Write-Host "  BIOS $($bios.SMBIOSBIOSVersion)  ($($bios.ReleaseDate.ToString('yyyy-MM-dd')), $([int]((Get-Date) - $bios.ReleaseDate).TotalDays) days old)`n"

if ($null -eq $certs) { Write-Host "  COULD NOT READ db - run elevated on a UEFI machine." -ForegroundColor Yellow; return }

foreach ($c in ($certs | Sort-Object NotAfter)) {
    $days = [int]($c.NotAfter - (Get-Date)).TotalDays
    $col  = if ($days -lt 0) { 'Red' } elseif ($days -lt 180) { 'Yellow' } else { 'DarkGray' }
    Write-Host ("  {0,-45} {1:yyyy-MM-dd}  {2}" -f $c.Subject, $c.NotAfter,
        $(if ($days -lt 0) { "EXPIRED $([Math]::Abs($days))d ago" } else { "${days}d" })) -ForegroundColor $col
}

$has2023 = [bool]($certs | Where-Object { $_.Subject -like '*Windows UEFI CA 2023*' })
if ($has2023) { Write-Host "`n  PASS - Windows UEFI CA 2023 is in the firmware." -ForegroundColor Green }
else { Write-Host "`n  ACTION - Windows UEFI CA 2023 is NOT in the firmware. Update BIOS, then re-run." -ForegroundColor Red }

$svc = Get-ItemProperty 'HKLM:\SYSTEM\CurrentControlSet\Control\SecureBoot\Servicing' -EA SilentlyContinue
if ($svc.UEFICA2023Status) {
    Write-Host "  Windows servicing reports: $($svc.UEFICA2023Status)" -ForegroundColor DarkGray
    if ($svc.UEFICA2023Status -eq 'Updated' -and -not $has2023) {
        Write-Host "  ^ servicing says Updated but the cert isn't there - firmware refused it." -ForegroundColor Red
    }
}

Here’s what it printed on a machine in our lab:

  WKSTN-04
  BIOS 2202  (2020-07-13, 2258 days old)
  Secure Boot: OFF

  Microsoft Corporation UEFI CA 2011            2026-06-27  EXPIRED 83d ago
  Microsoft Windows Production PCA 2011         2026-10-19  31d
  ASUSTeK MotherBoard SW Key Certificate        2031-12-26  1925d
  ASUSTeK Notebook SW Key Certificate           2031-12-26  1925d
  Canonical Ltd. Master Certificate Authority   2042-04-11  5684d

  ACTION - Windows UEFI CA 2023 is NOT in the firmware.
  Windows servicing reports: NotStarted

A BIOS from July 2020, no 2023 certificate, and the Windows bootloader signer has a month left on it. That machine’s a fine desktop and it’s been patched the whole time.

Why the registry check isn’t enough

Windows keeps its own record of the update:

HKLM\SYSTEM\CurrentControlSet\Control\SecureBoot\Servicing
    UEFICA2023Status : REG_SZ    NotStarted | InProgress | Updated
    UEFICA2023Error  : REG_DWORD  0 = clean

It’s documented, it’s a registry read, and it’s what most scripts and RMM checks use. Ours included. You get Updated back and you move on.

The catch is that the value is Windows reporting on its own attempt. Windows asks the firmware to take the certificate, and the firmware’s a separate piece of software that can say no. When it does, nothing in that key changes. You still get Updated, and you’ve got a machine showing green on every dashboard you own with none of the certs it needs.

What’s actually in db

The allowed-signature database is a UEFI variable called db. It isn’t a list of names and it isn’t text, which is the first thing that’ll trip you up. It’s a packed sequence of EFI_SIGNATURE_LIST structures:

offset  size  field
0       16    SignatureType        GUID
16       4    SignatureListSize    UINT32   // the whole list, header included
20       4    SignatureHeaderSize  UINT32
24       4    SignatureSize        UINT32   // per entry, owner GUID included
28       n    SignatureHeader
28+n    ...   Signatures[]                  // each: 16-byte owner GUID + payload

When SignatureType is EFI_CERT_X509_GUID each payload is a DER certificate. When it’s EFI_CERT_SHA256_GUID each payload is a 32-byte hash, which is what dbx is mostly full of.

Our first attempt decoded the blob as text and looked for “Microsoft”. It found nothing, on a machine with six certificates in it, because there’s no text in there to find. That’s why the script above walks the structure.

Sweep the whole fleet

Same parse, wrapped for remoting, with the fields you’d actually sort by. Save it as sb-sweep.ps1:

.\sb-sweep.ps1 -ComputerName (Get-Content .\machines.txt) |
    Export-Csv .\secureboot.csv -NoTypeInformation

It returns one row per machine:

ColumnWhy you want it
Has2023CAthe answer. False is your work queue
DbReadableFalse means the check failed, which isn’t the same as a missing cert
BiosVersion / BiosDate / BiosAgeDaysgroup by these and the pattern shows up immediately
OldestMsExpirysorts the fleet by how long you’ve got
ServicingStatuscompare against Has2023CA to find the disagreements
SecureBootOna machine with Secure Boot off is a different conversation

The full script is at the bottom of this post. Once you’ve got the CSV, the query that matters is the cross-tab:

Import-Csv .\secureboot.csv |
    Where-Object { $_.DbReadable -eq 'True' } |
    Group-Object Has2023CA, BiosVersion |
    Sort-Object Name | Format-Table Count, Name -AutoSize

And the one that finds the machines lying to you:

Import-Csv .\secureboot.csv |
    Where-Object { $_.ServicingStatus -eq 'Updated' -and $_.Has2023CA -eq 'False' }

Anything in that second list has a firmware problem, not a patching problem.

Two machines, same afternoon

Both fully patched, both current Windows, measured about an hour apart.

HP EliteOne 800 G5 - BIOS R11 Ver. 02.26.00, released 2026-05-03 (138 days old)

Windows UEFI CA 2023                          2035-06-13
Microsoft UEFI CA 2023                        2038-06-13
Microsoft Option ROM UEFI CA 2023             2038-10-26
HP UEFI Secure Boot DB 2017                   2033-01-16
Microsoft Windows Production PCA 2011          2026-10-19
Microsoft Corporation UEFI CA 2011            2026-06-27   (expired)

RFF device page showing the 2023 Secure Boot CA present in firmware, with the full certificate list and expiry dates

ASUS PRIME B450M-A - BIOS American Megatrends 2202, released 2020-07-13 (2,258 days old)

ASUSTeK MotherBoard SW Key Certificate        2031-12-26
ASUSTeK Notebook SW Key Certificate           2031-12-27
Canonical Ltd. Master Certificate Authority   2042-04-11
Microsoft Windows Production PCA 2011          2026-10-19
Microsoft Corporation UEFI CA 2011            2026-06-27   (expired)

Windows UEFI CA 2023                          MISSING

RFF device page showing the 2023 Secure Boot CA not present in firmware, with an already-expired Microsoft CA

138 days versus 2,258 days. Same patches, same day, and the only thing that differs is how old the firmware is. That’s the whole finding, and it’s why BiosAgeDays is in the sweep output.

The dates, off real hardware

CertificateExpiresOn 2026-09-18
Microsoft Corporation KEK CA 20112026-06-24expired 86 days ago
Microsoft Corporation UEFI CA 20112026-06-27expired 83 days ago
Microsoft Windows Production PCA 20112026-10-1931 days left
Windows UEFI CA 20232035-06-13replaces the PCA above
Microsoft Corporation KEK 2K CA 20232038-03-02replaces the KEK above

Microsoft Windows Production PCA 2011 is the one that signs the Windows bootloader. For the authoritative picture of what expires when and what Microsoft is shipping to replace it, start with the certificate-expiration and CA-update overview and the IT-pro Secure Boot playbook for the certificates expiring in 2026.

Two things that’ll bite you if you automate this

Both of these cost us time, and both are the kind of bug that doesn’t announce itself.

1. db isn’t in the global variable namespace

If you’re calling the Win32 API directly instead of using Get-SecureBootUEFI: PK and KEK live under EFI_GLOBAL_VARIABLE, 8be4df61-93ca-11d2-aa0d-00e098032b8c. db and dbx don’t - they’re under EFI_IMAGE_SECURITY_DATABASE, d719b2cb-3d3a-4596-a3bc-dad00e67656f.

Use the wrong one and GetFirmwareEnvironmentVariableW hands you ERROR_ENVVAR_NOT_FOUND (203). We got that 203 on a box that Get-SecureBootUEFI read 6,322 bytes out of a second later, which is a confusing ten minutes if you’re assuming the machine’s at fault.

2. SeSystemEnvironmentPrivilege is there but switched off

You need that privilege to read firmware variables. LocalSystem holds it, and it’s disabled in the token by default. Having a privilege and having it enabled aren’t the same thing:

OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, &h);
LookupPrivilegeValue(NULL, SE_SYSTEM_ENVIRONMENT_NAME, &luid);
tp.PrivilegeCount = 1; tp.Privileges[0].Luid = luid;
tp.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED;
AdjustTokenPrivileges(h, FALSE, &tp, 0, NULL, NULL);
// AdjustTokenPrivileges returns TRUE even when it assigned nothing.
// GetLastError() == ERROR_NOT_ALL_ASSIGNED is the real answer.

Neither one throws. You get an empty result back and nothing says why, which your code then reports as “this machine has no Secure Boot data” - and that reads to an operator exactly like “this machine has no 2023 certificate”. We’d have shipped a check that came back clean on the entire fleet while reading nothing at all, and there’s no symptom to notice. You’d find out in October.

That’s why DbReadable is a separate column in the sweep. “Couldn’t tell” and “cert is missing” have to look different or you’ve built the same bug again.

What to do with the results

  1. Sweep db across the fleet. The firmware, not the registry value.
  2. Group by BiosVersion and BiosDate. The stale cluster is your queue, and it’ll usually be a handful of models rather than scattered machines.
  3. Update firmware on anything with Has2023CA = False. Vendor tooling - HP Image Assistant, Dell Command Update, Lenovo System Update - is the practical route at scale.
  4. Re-run the sweep. Firmware update and certificate arrival are two separate events, and the second one’s the one that counts.

The fleet that gets caught by this isn’t the unpatched one. Windows Update is somebody’s job everywhere. Firmware usually isn’t anybody’s, and that’s the entire difference between those two machines above.

The full sweep script

Tested on Windows PowerShell 5.1 and PowerShell 7. Read-only.

[CmdletBinding()]
param([string[]] $ComputerName = @($env:COMPUTERNAME), [pscredential] $Credential)

$scan = {
    $X509 = [guid]'a5c059a1-94e4-4aa7-87b5-ab155c2bf072'
    $certs = @(); $readOk = $true
    try {
        $bytes = (Get-SecureBootUEFI -Name db -ErrorAction Stop).Bytes
        $off = 0
        while ($off + 28 -le $bytes.Length) {
            $type     = [guid]::new([byte[]]$bytes[$off..($off+15)])
            $listSize = [BitConverter]::ToUInt32($bytes, $off+16)
            $hdrSize  = [BitConverter]::ToUInt32($bytes, $off+20)
            $sigSize  = [BitConverter]::ToUInt32($bytes, $off+24)
            if ($listSize -lt 28 -or $listSize -gt ($bytes.Length - $off)) { break }
            if ($type -eq $X509 -and $sigSize -gt 16) {
                $start = $off + 28 + $hdrSize
                for ($i = 0; $i -lt [int](($listSize - 28 - $hdrSize) / $sigSize); $i++) {
                    $o = $start + ($i * $sigSize)
                    if ($o + $sigSize -gt $bytes.Length) { break }
                    try {
                        $c = [Security.Cryptography.X509Certificates.X509Certificate2]::new(
                                 [byte[]]$bytes[($o+16)..($o+$sigSize-1)])
                        $certs += [pscustomobject]@{ S = $c.GetNameInfo('SimpleName',$false); E = $c.NotAfter }
                    } catch { }
                }
            }
            $off += $listSize
        }
    } catch { $readOk = $false }

    $bios = Get-CimInstance Win32_BIOS
    $svc  = Get-ItemProperty 'HKLM:\SYSTEM\CurrentControlSet\Control\SecureBoot\Servicing' -EA SilentlyContinue
    $ms   = $certs | Where-Object { $_.S -match 'Microsoft|Windows' }

    # Compute these BEFORE the hashtable. PowerShell 5.1 rejects try/catch as an expression
    # inside a hash literal ("The hash literal was incomplete"); only PS7 allows it.
    $sbOn = $null; try { $sbOn = Confirm-SecureBootUEFI } catch { $sbOn = $null }
    $has2023 = $null
    if ($readOk) { $has2023 = [bool]($certs | Where-Object { $_.S -like '*Windows UEFI CA 2023*' }) }
    $oldestMs = $null
    if ($ms) { $oldestMs = ($ms | Sort-Object E | Select-Object -First 1).E.ToString('yyyy-MM-dd') }
    $biosDate = $null; $biosAge = $null
    if ($bios.ReleaseDate) {
        $biosDate = $bios.ReleaseDate.ToString('yyyy-MM-dd')
        $biosAge  = [int]((Get-Date) - $bios.ReleaseDate).TotalDays
    }

    [pscustomobject]@{
        Computer = $env:COMPUTERNAME; Model = (Get-CimInstance Win32_ComputerSystem).Model
        BiosVersion = $bios.SMBIOSBIOSVersion; BiosDate = $biosDate; BiosAgeDays = $biosAge
        SecureBootOn = $sbOn; DbReadable = $readOk; Has2023CA = $has2023
        OldestMsExpiry = $oldestMs; ServicingStatus = $svc.UEFICA2023Status; CertCount = $certs.Count
    }
}

$splat = @{ ComputerName = $ComputerName; ScriptBlock = $scan; ErrorAction = 'SilentlyContinue' }
if ($Credential) { $splat.Credential = $Credential }

# Local-only runs skip remoting, so this works on a box with WinRM disabled.
if ($ComputerName.Count -eq 1 -and $ComputerName[0] -eq $env:COMPUTERNAME) { & $scan }
else { Invoke-Command @splat }

What we changed in RFF

Our own compliance check reads the firmware now and treats UEFICA2023Status as the secondary source. When the two disagree, it reports the mismatch:

firmware db has NO 2023 CA despite Windows reporting Updated
  - firmware did not take it (check BIOS version)

If the firmware can’t be read at all it falls back to the old behavior instead of failing the machine, for the reason above. The screenshots in this post are that check on the two machines.