From 861b1ffe26bff7e10054b1223f5d30e25e35eb57 Mon Sep 17 00:00:00 2001 From: enricobuehler Date: Wed, 29 Jul 2026 12:15:53 +0200 Subject: [PATCH] fix(packaging/windows): keep the installer-run scripts ASCII, as the gate requires MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- packaging/windows/build-gamepad-drivers.ps1 | 10 +++---- packaging/windows/build-pf-vdisplay.ps1 | 10 +++---- packaging/windows/make-driver-cert.ps1 | 32 ++++++++++----------- packaging/windows/pack-host-installer.ps1 | 6 ++-- 4 files changed, 29 insertions(+), 29 deletions(-) diff --git a/packaging/windows/build-gamepad-drivers.ps1 b/packaging/windows/build-gamepad-drivers.ps1 index 09024fd5..8ac74d31 100644 --- a/packaging/windows/build-gamepad-drivers.ps1 +++ b/packaging/windows/build-gamepad-drivers.ps1 @@ -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.") } diff --git a/packaging/windows/build-pf-vdisplay.ps1 b/packaging/windows/build-pf-vdisplay.ps1 index 83fa39b7..1acbc679 100644 --- a/packaging/windows/build-pf-vdisplay.ps1 +++ b/packaging/windows/build-pf-vdisplay.ps1 @@ -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.") } diff --git a/packaging/windows/make-driver-cert.ps1 b/packaging/windows/make-driver-cert.ps1 index 7e26d44a..95c7d665 100644 --- a/packaging/windows/make-driver-cert.ps1 +++ b/packaging/windows/make-driver-cert.ps1 @@ -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 '' diff --git a/packaging/windows/pack-host-installer.ps1 b/packaging/windows/pack-host-installer.ps1 index 58e4100e..7766378c 100644 --- a/packaging/windows/pack-host-installer.ps1 +++ b/packaging/windows/pack-host-installer.ps1 @@ -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.") }