fix(packaging/windows): keep the installer-run scripts ASCII, as the gate requires
ci / docs-site (push) Successful in 1m7s
android / android (push) Canceled after 1m38s
apple / swift (push) Canceled after 0s
apple / screenshots (push) Canceled after 0s
arch / build-publish (push) Canceled after 2m0s
ci / web (push) Successful in 2m6s
ci / rust (push) Canceled after 2m13s
ci / rust-arm64 (push) Canceled after 2m13s
ci / bench (push) Canceled after 1m51s
deb / build-publish (push) Canceled after 58s
deb / build-publish-host (push) Canceled after 41s
deb / build-publish-client-arm64 (push) Canceled after 14s
decky / build-publish (push) Canceled after 22s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Canceled after 16s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Canceled after 5s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Canceled after 9s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Canceled after 9s
docker / build-push (ci, ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Canceled after 6s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Canceled after 6s
docker / build-push-arm64cross (push) Canceled after 0s
docker / deploy-docs (push) Canceled after 0s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Canceled after 14s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Canceled after 11s
windows-host / package (push) Canceled after 0s
windows-host / winget-source (push) Canceled after 0s

windows-host went red at the locale-safety gate: I wrote em-dashes and box-drawing
characters into four scripts under packaging/windows/, and that gate exists
precisely to stop that. Windows PowerShell 5.1 reads a BOM-less .ps1 in the active
ANSI codepage, so a non-ASCII byte mis-decodes on a German box and the script dies
with "unterminated string" — which is how the pf-vdisplay driver install once
failed silently in the field. The whole reason the install logic moved into the
compiled host exe was this exact hazard, and I reintroduced it in the comments.

Substituted to ASCII across all four (- for em-dash and the box-drawing rules).
No logic touched. The gate's own check now passes locally, all four still parse on
the runner, and make-driver-cert.ps1 -TestOnly still runs end to end.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-29 12:15:53 +02:00
co-authored by Claude Opus 5
parent f8cf3a0cf2
commit 861b1ffe26
4 changed files with 29 additions and 29 deletions
+5 -5
View File
@@ -35,8 +35,8 @@ $ErrorActionPreference = 'Stop'
$ProgressPreference = 'SilentlyContinue'
$PSNativeCommandUseErrorActionPreference = $false
# The decoded signing key must not outlive this script. It is a STABLE key now trusted as a
# machine root on every box that installs punktfunk so a .pfx left behind in a build directory is
# The decoded signing key must not outlive this script. It is a STABLE key now - trusted as a
# machine root on every box that installs punktfunk - so a .pfx left behind in a build directory is
# a standing credential on a machine that runs build jobs, not the throwaway it used to be. A
# script-scope trap covers the failure paths; Remove-SigningPfx is also called on the way out.
$script:ShredPfx = $null
@@ -49,7 +49,7 @@ function Remove-SigningPfx {
# `break` is for explicitness, not correctness: measured on the runner, a bare trap and
# trap+break behave identically here (exit 1, no resumption) for `throw` at script scope,
# `throw` inside a function, and a cmdlet error under EAP=Stop. Kept because it states the
# intent shred, then re-throw instead of relying on a default that is easy to misread.
# intent - shred, then re-throw - instead of relying on a default that is easy to misread.
trap { Remove-SigningPfx; break }
$DriversDir = (Resolve-Path $DriversDir).Path
@@ -106,7 +106,7 @@ 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
# 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.)
@@ -123,7 +123,7 @@ if ($CertPfxB64) {
$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 " +
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.")
}
+5 -5
View File
@@ -37,8 +37,8 @@ $ErrorActionPreference = 'Stop'
$ProgressPreference = 'SilentlyContinue'
$PSNativeCommandUseErrorActionPreference = $false
# The decoded signing key must not outlive this script. It is a STABLE key now trusted as a
# machine root on every box that installs punktfunk so a .pfx left behind in a build directory is
# The decoded signing key must not outlive this script. It is a STABLE key now - trusted as a
# machine root on every box that installs punktfunk - so a .pfx left behind in a build directory is
# a standing credential on a machine that runs build jobs, not the throwaway it used to be. A
# script-scope trap covers the failure paths; Remove-SigningPfx is also called on the way out.
$script:ShredPfx = $null
@@ -51,7 +51,7 @@ function Remove-SigningPfx {
# `break` is for explicitness, not correctness: measured on the runner, a bare trap and
# trap+break behave identically here (exit 1, no resumption) for `throw` at script scope,
# `throw` inside a function, and a cmdlet error under EAP=Stop. Kept because it states the
# intent shred, then re-throw instead of relying on a default that is easy to misread.
# intent - shred, then re-throw - instead of relying on a default that is easy to misread.
trap { Remove-SigningPfx; break }
$DriversDir = (Resolve-Path $DriversDir).Path
@@ -103,7 +103,7 @@ 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
# 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.)
@@ -120,7 +120,7 @@ if ($CertPfxB64) {
$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 " +
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.")
}
+16 -16
View File
@@ -47,26 +47,26 @@ if (-not $OutDir) {
}
New-Item -ItemType Directory -Force -Path $OutDir | Out-Null
# ── 1. key + self-signed cert, in memory ──────────────────────────────────────────────────────
# -- 1. key + self-signed cert, in memory ------------------------------------------------------
# RSA 3072 / SHA-256. The drivers were previously signed with 2048; nothing interoperates with this
# cert except our own installer (we are our own trust anchor), so there is no compatibility reason
# to stay at 2048 and the signtool self-test below proves 3072 is consumable.
# to stay at 2048 - and the signtool self-test below proves 3072 is consumable.
$rsa = [System.Security.Cryptography.RSA]::Create(3072)
$req = [System.Security.Cryptography.X509Certificates.CertificateRequest]::new(
$subject, $rsa,
[System.Security.Cryptography.HashAlgorithmName]::SHA256,
[System.Security.Cryptography.RSASignaturePadding]::Pkcs1)
# KeyUsage: DigitalSignature, CRITICAL matches the existing certs.
# KeyUsage: DigitalSignature, CRITICAL - matches the existing certs.
$req.CertificateExtensions.Add(
[System.Security.Cryptography.X509Certificates.X509KeyUsageExtension]::new(
[System.Security.Cryptography.X509Certificates.X509KeyUsageFlags]::DigitalSignature, $true))
# EKU: code signing (1.3.6.1.5.5.7.3.3), NON-critical matches the existing certs.
# EKU: code signing (1.3.6.1.5.5.7.3.3), NON-critical - matches the existing certs.
$oids = [System.Security.Cryptography.OidCollection]::new()
$oids.Add([System.Security.Cryptography.Oid]::new('1.3.6.1.5.5.7.3.3')) | Out-Null
$req.CertificateExtensions.Add(
[System.Security.Cryptography.X509Certificates.X509EnhancedKeyUsageExtension]::new($oids, $false))
# SubjectKeyIdentifier matches. Deliberately NO basicConstraints: the shipping certs carry none,
# SubjectKeyIdentifier - matches. Deliberately NO basicConstraints: the shipping certs carry none,
# and this is the one place to not get creative, since a chain-building difference would surface as
# a failed driver install on a user's machine rather than as an error here.
$req.CertificateExtensions.Add(
@@ -75,20 +75,20 @@ $req.CertificateExtensions.Add(
$now = [DateTimeOffset]::UtcNow.AddMinutes(-5) # backdate slightly: clock skew must not make it not-yet-valid
$cert = $req.CreateSelfSigned($now, $now.AddYears($years))
# ── 2. export ─────────────────────────────────────────────────────────────────────────────────
# -- 2. export ---------------------------------------------------------------------------------
# RandomNumberGenerator, not Get-Random: Get-Random is System.Random and has no business generating
# the passphrase on a signing key.
$rng = [System.Security.Cryptography.RandomNumberGenerator]::Create()
$bytes = [byte[]]::new(24); $rng.GetBytes($bytes)
$pw = [Convert]::ToBase64String($bytes)
# .NET's own PKCS#12 writer avoids the OpenSSL 3 trap where the default AES-256/PBKDF2 encryption
# .NET's own PKCS#12 writer - avoids the OpenSSL 3 trap where the default AES-256/PBKDF2 encryption
# produces a .pfx that Windows CryptoAPI cannot read.
$pfxBytes = $cert.Export([System.Security.Cryptography.X509Certificates.X509ContentType]::Pfx, $pw)
$pfxPath = Join-Path $OutDir 'driver.pfx'
[IO.File]::WriteAllBytes($pfxPath, $pfxBytes)
# ── 3. self-test: can signtool actually sign with it? ─────────────────────────────────────────
# -- 3. self-test: can signtool actually sign with it? -----------------------------------------
function Find-SdkTool([string]$name) {
$root = 'C:\Program Files (x86)\Windows Kits\10\bin'
Get-ChildItem -Path $root -Recurse -Filter $name -EA SilentlyContinue |
@@ -104,18 +104,18 @@ if ($signtool) {
$out = & $signtool sign /fd SHA256 /f $pfxPath /p $pw $scratch 2>&1 | Out-String
if ($LASTEXITCODE -eq 0) {
# Assert the signature is present and carries our subject. /pa chain trust FAILS until the
# cert is in the machine's trust stores expected, and not what this checks.
# cert is in the machine's trust stores - expected, and not what this checks.
$v = & $signtool verify /pa /v $scratch 2>&1 | Out-String
$selftest = if ($v -match 'punktfunk-driver') { 'PASS (signtool signed; signature carries CN=punktfunk-driver)' }
else { 'PASS (signtool signed)' }
}
elseif ($out -match '0x80090010') {
# NTE_PERM. Not a bad certificate a network logon (SSH) has no key container, so signtool
# NTE_PERM. Not a bad certificate - a network logon (SSH) has no key container, so signtool
# cannot import the .pfx to sign with it. The KEY ABOVE IS STILL VALID: generating it needs
# no container, only consuming it does. Re-run at an interactive logon (console/RDP) to
# exercise this, or just let the canary CI build be the proof the runner signs under a
# exercise this, or just let the canary CI build be the proof - the runner signs under a
# real logon, which is how the MSIX cert already works.
$selftest = 'SKIPPED (NTE_PERM 0x80090010 no key container on this logon; run at a console/RDP session, or verify via a canary build)'
$selftest = 'SKIPPED (NTE_PERM 0x80090010 - no key container on this logon; run at a console/RDP session, or verify via a canary build)'
}
else {
throw "SELF-TEST FAILED: signtool could not sign with the generated .pfx (exit $LASTEXITCODE)`n$out"
@@ -123,11 +123,11 @@ if ($signtool) {
Remove-Item $scratch -Force -EA SilentlyContinue
}
# ── 4. report ─────────────────────────────────────────────────────────────────────────────────
# -- 4. report ---------------------------------------------------------------------------------
if ($TestOnly) {
Remove-Item $OutDir -Recurse -Force
Write-Output ''
Write-Output "TEST ONLY nothing kept."
Write-Output "TEST ONLY - nothing kept."
Write-Output " thumbprint would have been : $($cert.Thumbprint)"
Write-Output " key size : $($cert.PublicKey.GetRSAPublicKey().KeySize) bits"
Write-Output " not after : $($cert.NotAfter.ToString('yyyy-MM-dd'))"
@@ -144,14 +144,14 @@ $pwPath = Join-Path $OutDir 'DRIVER_CERT_PASSWORD.txt'
Write-Output ''
Write-Output '================ punktfunk driver signing cert ================'
Write-Output ''
Write-Output " THUMBPRINT (public this is the only value to share):"
Write-Output " THUMBPRINT (public - this is the only value to share):"
Write-Output " $($cert.Thumbprint)"
Write-Output ''
Write-Output " key size : $($cert.PublicKey.GetRSAPublicKey().KeySize) bits"
Write-Output " valid until : $($cert.NotAfter.ToString('yyyy-MM-dd'))"
Write-Output " self-test : $selftest"
Write-Output ''
Write-Output ' SECRETS do not paste these into chat or a terminal. Open the files:'
Write-Output ' SECRETS - do not paste these into chat or a terminal. Open the files:'
Write-Output " DRIVER_CERT_PFX_B64 -> $b64Path"
Write-Output " DRIVER_CERT_PASSWORD -> $pwPath"
Write-Output ''
+3 -3
View File
@@ -78,13 +78,13 @@ Write-Host "ISCC: $iscc"
# FAIL CLOSED on a real release. The ephemeral fallback below exists so canary/CI/dev builds keep
# working without the secret, but it is a per-build throwaway cert: nobody can pin it, and an
# installer signed with one is indistinguishable from one signed by an attacker. Silently falling
# back on a tag build would ship exactly that to users under the release's name so on refs/tags/v*
# back on a tag build would ship exactly that to users under the release's name - so on refs/tags/v*
# a missing MSIX_CERT_PFX_B64 (or -NoSign) is a build failure, not a downgrade. ('auto' resolves from
# GITHUB_REF so a workflow can't forget to opt in; -RequireSignedCert true/false overrides.)
$requireCert = if ($RequireSignedCert -eq 'auto') { $env:GITHUB_REF -like 'refs/tags/v*' }
else { [Convert]::ToBoolean($RequireSignedCert) }
if ($NoSign -and $requireCert) {
throw "release build ($env:GITHUB_REF) with -NoSign refusing to publish an unsigned installer."
throw "release build ($env:GITHUB_REF) with -NoSign - refusing to publish an unsigned installer."
}
$pfxPath = Join-Path $OutDir 'signing.pfx'
$cerPath = Join-Path $OutDir "punktfunk-host-windows_${Version}.cer"
@@ -97,7 +97,7 @@ if (-not $NoSign) {
[IO.File]::WriteAllBytes($pfxPath, [Convert]::FromBase64String($PfxBase64))
}
elseif ($requireCert) {
throw ("release build ($env:GITHUB_REF) with no MSIX_CERT_PFX_B64 refusing to fall back to " +
throw ("release build ($env:GITHUB_REF) with no MSIX_CERT_PFX_B64 - refusing to fall back to " +
"an ephemeral self-signed cert. Restore the MSIX_CERT_PFX_B64 / MSIX_CERT_PASSWORD " +
"repo secrets, or pass -RequireSignedCert false if this really is a test build.")
}