feat(packaging/windows): give the drivers one publisher identity, and give the trust back

The drivers had no cryptographic identity at all. Every build minted a fresh
`CN=punktfunk-driver` cert (build-pf-vdisplay.ps1, build-gamepad-drivers.ps1), and
install.rs `trust_cert` adds whatever .cer it finds in the unpacked bundle to
machine Root AND TrustedPublisher. So the signature vouched for nothing an
attacker couldn't restage — replace the bundle, ship your own cert beside your own
driver, install proceeds identically. It was ceremony to make PnP install quietly.

Worse, it leaked. `trust_cert` runs once per driver (install.rs:102 and :155), so
every upgrade added TWO more self-signed root CAs under the same name, and nothing
ever removed them: uninstalling punktfunk left trust behind that the user had no
reason to keep granting.

So: both build scripts now take a stable cert via DRIVER_CERT_PFX_B64 (they already
read the env var — windows-host.yml just never passed it) and fail closed on a v*
tag, same rule as the host and MSIX packers. `driver uninstall` purges every
`CN=punktfunk-driver` cert from both stores, and `driver install` purges before
adding, so an upgrade also collects the historical pile instead of adding to it.

Purge-before-add lives ONLY on the pf-vdisplay install path, not the gamepad one.
The installer runs vdisplay first and gamepad second; purging in both would have
the gamepad leg delete the cert the vdisplay leg just added whenever the two
bundles carry different certs — which is exactly what canary's per-build fallback
produces. Purging by subject rather than thumbprint is deliberate too: it is what
lets one install clean up certs from builds that no longer exist anywhere, and it
needs no parsing of certutil's localized output (this module exists because
locale-parsed PowerShell broke the driver install on a German box).

This does NOT make the driver download authenticated — a self-signed leaf is its
own root, so the installer must trust it for the driver to install at all. What it
buys is a fingerprint we can publish out-of-band so a substituted driver is
detectable, an allowlistable publisher, continuity across releases, and no root
accumulation. Attestation signing remains the real fix; documented as such.

Documented the key-custody trade honestly in packaging/windows/README.md: a stable
key trusted as a machine root on every install is worth stealing in a way a
throwaway never was, with no revocation path. CI secret only.

⚠️ The secrets must exist before the next v* tag or the release will fail — that
is the guard working, and the generation runbook is in that README. The fingerprint
line there is a placeholder until the key is generated.

Verified: punktfunk-host compiles clean on the windows-amd64 runner with this
install.rs (exit 0, no warnings); both build scripts parse; cargo fmt clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-29 12:12:21 +02:00
co-authored by Claude Opus 5
parent 93902ff60e
commit a49858f194
6 changed files with 141 additions and 0 deletions
+6
View File
@@ -352,6 +352,12 @@ jobs:
env:
MSIX_CERT_PFX_B64: ${{ secrets.MSIX_CERT_PFX_B64 }}
MSIX_CERT_PASSWORD: ${{ secrets.MSIX_CERT_PASSWORD }}
# The DRIVER cert is separate from the host/MSIX one and reaches the two driver build
# scripts through the environment (pack-host-installer.ps1 invokes them, they read
# $env:DRIVER_CERT_PFX_B64 themselves). Without it they sign with a per-build throwaway,
# which the installer then trusts as a machine root — see packaging/windows/README.md.
DRIVER_CERT_PFX_B64: ${{ secrets.DRIVER_CERT_PFX_B64 }}
DRIVER_CERT_PASSWORD: ${{ secrets.DRIVER_CERT_PASSWORD }}
run: |
& packaging/windows/pack-host-installer.ps1 `
-Version $env:HOST_VERSION -TargetDir C:\t\release -OutDir C:\t\out
+8
View File
@@ -73,6 +73,14 @@ us beyond the download itself.
- **Windows installers and MSIX packages** are Authenticode-signed; a release build that cannot
reach its code-signing certificate fails to build rather than falling back to a self-signed one.
Check with `Get-AuthenticodeSignature punktfunk-host-setup-1.2.3.exe`.
- **The Windows drivers** (virtual display, virtual gamepads) are signed with a stable self-signed
certificate, `CN=punktfunk-driver`, whose fingerprint is published in
[`packaging/windows/README.md`](packaging/windows/README.md). The installer has to add it to the
machine's trusted roots for a self-signed driver to install at all, so — unlike the cases above —
this signature does **not** authenticate the download: it gives the drivers a stable publisher
identity you can compare against the published fingerprint, and it is removed again on uninstall.
Verify with `Get-AuthenticodeSignature` on the installed `pf_vdisplay.dll`, or list what is
trusted with `Get-ChildItem Cert:\LocalMachine\Root | ? Subject -like '*punktfunk*'`.
A checksum on its own only tells you the download wasn't corrupted in transit — it says nothing
about who produced the file, since anyone able to replace an artifact can replace its checksum.
@@ -74,6 +74,40 @@ fn driver_install(args: &[String]) -> Result<()> {
Ok(())
}
/// The subject CN both driver-signing certs carry (`build-pf-vdisplay.ps1` /
/// `build-gamepad-drivers.ps1`). certutil matches a CertId against the subject, so this is how we
/// find our own certs again without parsing any localized output — see `purge_driver_certs`.
const DRIVER_CERT_CN: &str = "punktfunk-driver";
/// Remove every `CN=punktfunk-driver` cert this product ever added, from machine `Root` and
/// `TrustedPublisher`.
///
/// Two reasons this has to exist. Uninstall used to leave the certs behind forever, so removing
/// punktfunk left a trusted root CA on the machine — trust we asked for and then never gave back.
/// And before the signing cert was stabilised, every BUILD minted a fresh throwaway cert, so each
/// upgrade added two more roots under the same name; a box that has been upgraded a dozen times is
/// carrying two dozen of them. Purging by subject rather than by thumbprint is what lets one
/// install clean up the whole historical pile.
///
/// Deleting the root does NOT unload an already-installed driver: PnP validates the signature when
/// the package is staged into the driver store, not on every load. So a purge is safe to run before
/// re-adding the current cert.
///
/// Best-effort and silent, like everything else here. `certutil -delstore` deletes one match per
/// call and fails once nothing matches, so loop until it stops succeeding — bounded, because a
/// pathological store must not turn an uninstall into an infinite loop.
fn purge_driver_certs() {
for store in ["Root", "TrustedPublisher"] {
let mut removed = 0;
while removed < 64 && run_quiet("certutil", &["-delstore", store, DRIVER_CERT_CN]) {
removed += 1;
}
if removed > 0 {
println!("removed {removed} stale '{DRIVER_CERT_CN}' cert(s) from {store}");
}
}
}
/// Trust the bundled self-signed driver cert: machine `Root` (so the chain validates) + `TrustedPublisher`
/// (so PnP installs without a prompt).
fn trust_cert(dir: &Path) {
@@ -99,6 +133,12 @@ fn install_pf_vdisplay(dir: &Path) -> Result<()> {
if !inf.exists() {
bail!("no pf_vdisplay.inf in {}", dir.display());
}
// Sweep the old certs before adding the current one. Deliberately only on THIS path and not in
// `install_gamepad`: the installer runs pf-vdisplay first and gamepad second, so one purge here
// clears the pile and both trust_cert calls then add on top. Purging in both would have the
// gamepad leg delete the cert the vdisplay leg just installed whenever the two bundles carry
// different certs — which is exactly what a canary build's per-build fallback certs are.
purge_driver_certs();
trust_cert(dir);
// Create the ROOT device node only if absent (a blind re-create spawns a phantom duplicate, and the
// host binds interface index 0). ALWAYS nefconc (a clean ROOT\DISPLAY node), NEVER devgen (which makes
@@ -213,6 +253,11 @@ fn driver_uninstall(args: &[String]) -> Result<()> {
// Same best-effort contract as install: never abort the (un)installer over a driver.
eprintln!("warning: {what} driver uninstall: {e:#}");
}
// Give back the trust we asked for. Here in the dispatcher rather than in the two uninstall
// bodies so it runs exactly once per invocation, and idempotently when the installer calls both
// legs back to back. Uninstalling punktfunk must not leave a trusted root CA behind — and this
// also collects the historical pile from the era when every build signed with a new cert.
purge_driver_certs();
Ok(())
}
+54
View File
@@ -144,6 +144,60 @@ fresh install uses the generated random console password — read it from
> punktfunk-planning: `windows-build-and-packaging.md` (internal planning repo) for the toolchain
> + signing details.
## Driver signing (`DRIVER_CERT_PFX_B64`)
Our three UMDF drivers are signed with a **stable self-signed code-signing cert**, subject
`CN=punktfunk-driver`, supplied to `build-pf-vdisplay.ps1` / `build-gamepad-drivers.ps1` as the
`DRIVER_CERT_PFX_B64` + `DRIVER_CERT_PASSWORD` Actions secrets. On a `v*` tag build a missing cert
is a **hard failure** (`-RequireSignedCert`, default `auto` off `GITHUB_REF`); canary and local
builds still fall back to a per-build throwaway.
**Current fingerprint (SHA-1 thumbprint):** `<fill in after generating — see below>`
Why stable matters here. The installer trusts the `.cer` that ships in the bundle
(`certutil -addstore -f Root` + `TrustedPublisher`, `crates/punktfunk-host/src/windows/install.rs`),
which is unavoidable for a self-signed cert — a self-signed leaf is its own root, so the chain only
validates if the root is present. That means the signature does **not** authenticate the download:
anyone who can alter the bundle can put their own cert next to their own driver. What a stable cert
buys is everything downstream of that: one anchor imported once instead of two more roots per
upgrade, a fingerprint we can publish out-of-band so a substituted driver is *detectable*, a
publisher an admin can allowlist, and continuity across releases. `driver install` purges stale
`CN=punktfunk-driver` certs before adding the current one, and `driver uninstall` removes them
entirely — including the pile left by the per-build-cert era.
> ⚠️ **The private key is now worth stealing.** It is trusted as a machine **root** on every
> punktfunk box, with code-signing EKU and no practical revocation path (nobody removes a stale
> root, and self-signed roots aren't in any CRL users honour). Keep it in the CI secret and nowhere
> else — not on a dev laptop. This is the trade for stability, and the reason attestation signing
> (which chains to Microsoft and needs no root import at all) remains the real fix.
Generating it — **run this yourself**, on a trusted Windows box, elevated:
```powershell
# 1. Create the cert. 10-year lifetime: an expired driver cert breaks INSTALLS on new machines, and
# rotating means every user picks up a new root anyway, so churn buys nothing here.
$c = New-SelfSignedCertificate -Type CodeSigningCert -Subject 'CN=punktfunk-driver' `
-CertStoreLocation Cert:\CurrentUser\My -KeyExportPolicy Exportable `
-NotAfter (Get-Date).AddYears(10)
$c.Thumbprint # <- publish this: paste into the "Current fingerprint" line above + SECURITY.md
# 2. Export it, protected by a strong random password.
$pw = -join ((48..57) + (65..90) + (97..122) | Get-Random -Count 32 | % { [char]$_ })
$sec = ConvertTo-SecureString $pw -AsPlainText -Force
Export-PfxCertificate -Cert "Cert:\CurrentUser\My\$($c.Thumbprint)" -FilePath .\driver.pfx -Password $sec | Out-Null
[Convert]::ToBase64String([IO.File]::ReadAllBytes('.\driver.pfx')) | Set-Clipboard # -> DRIVER_CERT_PFX_B64
$pw # -> DRIVER_CERT_PASSWORD
# 3. Add BOTH as Gitea Actions secrets (org level on `unom`, like RPM_GPG_PRIVATE_KEY, so any repo
# that builds drivers inherits them), then destroy the local copies:
Remove-Item .\driver.pfx -Force
Remove-Item "Cert:\CurrentUser\My\$($c.Thumbprint)" -Force
```
Keep an offline backup of the .pfx + password somewhere you'd keep a signing key. Losing it means
the next release ships a cert nobody has trusted before, and every user's installer adds a second
root — recoverable, but only by re-running the install.
## Dev iteration on the test box (driver)
Two helpers wrap the painful manual steps of iterating on the pf-vdisplay driver against a live host
@@ -27,6 +27,8 @@ param(
[string]$DriverVer,
[string]$CertPfxB64 = $env:DRIVER_CERT_PFX_B64,
[string]$CertPassword = $env:DRIVER_CERT_PASSWORD,
# 'auto' (default) = required iff this is a v* tag build; 'true'/'false' to force. See below.
[ValidateSet('auto', 'true', 'false')][string]$RequireSignedCert = 'auto',
[switch]$SkipBuild
)
$ErrorActionPreference = 'Stop'
@@ -86,6 +88,13 @@ foreach ($t in @($signtool, $stampinf, $inf2cat)) {
}
# --- 3. signing cert (supplied stable pfx OR fresh self-signed; shared by both drivers) -------
# FAIL CLOSED on a real release, same rule as the host/MSIX pack scripts. The fallback below mints
# a cert per BUILD, and the installer trusts whatever .cer ships in the bundle — so the signature
# proves nothing about origin, and each upgrade adds another self-signed root CA to the user's
# machine under the same name. That is survivable for canary and dev builds; shipping it in a
# release is not. ('auto' resolves from GITHUB_REF so a new workflow inherits the guard.)
$requireCert = if ($RequireSignedCert -eq 'auto') { $env:GITHUB_REF -like 'refs/tags/v*' }
else { [Convert]::ToBoolean($RequireSignedCert) }
$cleanupCert = $null
if ($CertPfxB64) {
Write-Host '==> signing with supplied driver cert (DRIVER_CERT_PFX_B64)'
@@ -95,6 +104,11 @@ if ($CertPfxB64) {
$signArgs = @('/f', $pfx); if ($CertPassword) { $signArgs += @('/p', $CertPassword) }
$pubForCer = if ($sec) { Get-PfxCertificate -FilePath $pfx -Password $sec } else { Get-PfxCertificate -FilePath $pfx }
}
elseif ($requireCert) {
throw ("release build ($env:GITHUB_REF) with no DRIVER_CERT_PFX_B64 — refusing to sign drivers " +
"with a per-build throwaway cert. Set the DRIVER_CERT_PFX_B64 / DRIVER_CERT_PASSWORD " +
"secrets (packaging/windows/README.md), or pass -RequireSignedCert false for a test build.")
}
else {
Write-Host '==> no DRIVER_CERT_PFX_B64 -> generating a fresh self-signed driver cert (the installer trusts the bundled .cer at install time)'
$cleanupCert = New-SelfSignedCertificate -Type CodeSigningCert -Subject 'CN=punktfunk-driver' `
+14
View File
@@ -29,6 +29,8 @@ param(
[string]$DriverVer, # default: 9.9.MMdd.HHmm (strictly-increasing)
[string]$CertPfxB64 = $env:DRIVER_CERT_PFX_B64, # optional stable driver-signing cert (CI secret)
[string]$CertPassword = $env:DRIVER_CERT_PASSWORD,
# 'auto' (default) = required iff this is a v* tag build; 'true'/'false' to force. See below.
[ValidateSet('auto', 'true', 'false')][string]$RequireSignedCert = 'auto',
[switch]$SkipBuild # reuse an existing target\...\release\pf_vdisplay.dll
)
$ErrorActionPreference = 'Stop'
@@ -83,6 +85,13 @@ foreach ($t in @($signtool, $stampinf, $inf2cat)) {
}
# --- 3. signing cert (supplied stable pfx OR fresh self-signed) -------------------------------
# FAIL CLOSED on a real release, same rule as the host/MSIX pack scripts. The fallback below mints
# a cert per BUILD, and the installer trusts whatever .cer ships in the bundle — so the signature
# proves nothing about origin, and each upgrade adds another self-signed root CA to the user's
# machine under the same name. That is survivable for canary and dev builds; shipping it in a
# release is not. ('auto' resolves from GITHUB_REF so a new workflow inherits the guard.)
$requireCert = if ($RequireSignedCert -eq 'auto') { $env:GITHUB_REF -like 'refs/tags/v*' }
else { [Convert]::ToBoolean($RequireSignedCert) }
$cleanupCert = $null
if ($CertPfxB64) {
Write-Host '==> signing with supplied driver cert (DRIVER_CERT_PFX_B64)'
@@ -92,6 +101,11 @@ if ($CertPfxB64) {
$signArgs = @('/f', $pfx); if ($CertPassword) { $signArgs += @('/p', $CertPassword) }
$pubForCer = if ($sec) { Get-PfxCertificate -FilePath $pfx -Password $sec } else { Get-PfxCertificate -FilePath $pfx }
}
elseif ($requireCert) {
throw ("release build ($env:GITHUB_REF) with no DRIVER_CERT_PFX_B64 — refusing to sign drivers " +
"with a per-build throwaway cert. Set the DRIVER_CERT_PFX_B64 / DRIVER_CERT_PASSWORD " +
"secrets (packaging/windows/README.md), or pass -RequireSignedCert false for a test build.")
}
else {
Write-Host '==> no DRIVER_CERT_PFX_B64 -> generating a fresh self-signed driver cert (the installer trusts the bundled .cer at install time)'
$cleanupCert = New-SelfSignedCertificate -Type CodeSigningCert -Subject 'CN=punktfunk-driver' `