From f44317fb3376a0ab465b2552bdab1ba97ce323a8 Mon Sep 17 00:00:00 2001 From: enricobuehler Date: Tue, 16 Jun 2026 09:17:30 +0000 Subject: [PATCH] feat(windows): stable code-signing cert for the MSIX (one-time per-machine trust) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Sign every MSIX build with one STABLE self-signed cert instead of a fresh per-build cert, so the Trusted People import is a one-time, per-machine step that survives upgrades (a fresh cert each build forced a re-import every time). The cert (CN=unom, SHA-1 CD1EFDEE…E941, valid to 2036) lives in the MSIX_CERT_PFX_B64 / MSIX_CERT_PASSWORD Actions secrets; its public half is checked in as packaging/punktfunk-codesign.cer and published next to each .msix. pack-msix.ps1 now always exports the signing cert's public .cer (extracted from a supplied pfx too, not just the ephemeral-generated path) and warns if the cert subject != manifest Publisher (the mismatch Add-AppxPackage would otherwise reject). Documents the path to a publicly-trusted (no-import) cert: swap the two secrets + pass a matching -Publisher. Co-Authored-By: Claude Opus 4.8 --- .../packaging/README.md | 29 ++++++++++----- .../packaging/pack-msix.ps1 | 35 ++++++++++-------- .../packaging/punktfunk-codesign.cer | Bin 0 -> 768 bytes 3 files changed, 39 insertions(+), 25 deletions(-) create mode 100644 crates/punktfunk-client-windows/packaging/punktfunk-codesign.cer diff --git a/crates/punktfunk-client-windows/packaging/README.md b/crates/punktfunk-client-windows/packaging/README.md index f42b598..6e261ed 100644 --- a/crates/punktfunk-client-windows/packaging/README.md +++ b/crates/punktfunk-client-windows/packaging/README.md @@ -37,25 +37,34 @@ MSIX requires a strictly 4-part numeric version. The workflow computes: ## Signing & install -Signing precedence in `pack-msix.ps1`: -1. **`MSIX_CERT_PFX_B64` / `MSIX_CERT_PASSWORD`** Actions secrets — a real (or shared) code-signing - `.pfx` whose **subject DN must equal `-Publisher`** (default `CN=unom`). Drop these in later to - move off self-signed with **no manifest change**; if the cert's subject differs, pass a matching - `-Publisher` (it's stamped into the manifest `Identity`). -2. otherwise an **ephemeral self-signed** cert (subject = `-Publisher`) is generated and its public - `.cer` is published next to the `.msix`. - -To install a self-signed build, trust the cert once, then add the package: +CI signs every build with a **stable self-signed code-signing cert** (`CN=unom`, SHA-1 +`CD1EFDEEEC9743AFC38F56C5AF30C5A3009BE941`, valid to 2036). Its public half is checked in as +[`punktfunk-codesign.cer`](punktfunk-codesign.cer); the private `.pfx` + password live in the +`MSIX_CERT_PFX_B64` / `MSIX_CERT_PASSWORD` Actions secrets. Because it's the *same* cert every build, +trusting it is **one-time, per machine** — once imported, every future build and in-place upgrade is +trusted with no further prompt: ```powershell -Import-Certificate -FilePath .\punktfunk-client-windows__x64.cer -CertStoreLocation Cert:\LocalMachine\TrustedPeople +# once per machine (elevated): trust the publisher +Import-Certificate -FilePath .\punktfunk-codesign.cer -CertStoreLocation Cert:\LocalMachine\TrustedPeople +# then install (and re-run for each upgrade — no re-trust needed) Add-AppxPackage -Path .\punktfunk-client-windows__x64.msix ``` +The matching `.cer` is also published next to each `.msix` in the registry, so it's always at hand. + The MSIX declares a dependency on the Windows App SDK 2.x runtime; install [the App SDK runtime](https://aka.ms/windowsappsdk) if `Add-AppxPackage` reports a missing `Microsoft.WindowsAppRuntime.2` framework. +`pack-msix.ps1` signing precedence: it uses the **`MSIX_CERT_PFX_B64` / `MSIX_CERT_PASSWORD`** secrets +when present (the stable cert above), else generates an *ephemeral* self-signed cert (forks / local +builds without the secrets). Either way it exports the signing cert's public `.cer` for the import. +**To move to a publicly-trusted (no-import) cert** — Azure Artifact Signing or a public OV cert — +replace the two secrets with the new `.pfx`; the cert's subject DN must equal the manifest +`Publisher`, so pass a matching `-Publisher` (it's stamped into the package `Identity`, and changing +it changes the package identity → a one-time reinstall). + ## Building locally On the Windows runner / dev VM (MSVC + Windows SDK present), after a release build: diff --git a/crates/punktfunk-client-windows/packaging/pack-msix.ps1 b/crates/punktfunk-client-windows/packaging/pack-msix.ps1 index dce9ad7..b0cb16b 100644 --- a/crates/punktfunk-client-windows/packaging/pack-msix.ps1 +++ b/crates/punktfunk-client-windows/packaging/pack-msix.ps1 @@ -92,28 +92,37 @@ $msix = Join-Path $OutDir "punktfunk-client-windows_${Version}_x64.msix" & $makeappx pack /o /d $layout /p $msix if ($LASTEXITCODE -ne 0) { throw "makeappx pack failed ($LASTEXITCODE)" } -# --- signing cert --- +# --- signing cert (supplied stable pfx OR ephemeral self-signed) --- $pfxPath = Join-Path $OutDir 'signing.pfx' $cerPath = Join-Path $OutDir "punktfunk-client-windows_${Version}_x64.cer" -$selfSigned = $false if ($PfxBase64) { Write-Host "signing with supplied code-signing cert (MSIX_CERT_PFX_B64)" [IO.File]::WriteAllBytes($pfxPath, [Convert]::FromBase64String($PfxBase64)) } else { Write-Host "no MSIX_CERT_PFX_B64 -> generating an ephemeral self-signed cert (subject $Publisher)" - $selfSigned = $true if (-not $PfxPassword) { $PfxPassword = 'punktfunk' } - $cert = New-SelfSignedCertificate -Type Custom -Subject $Publisher ` + $tmp = New-SelfSignedCertificate -Type Custom -Subject $Publisher ` -KeyUsage DigitalSignature -FriendlyName 'punktfunk MSIX (self-signed)' ` -CertStoreLocation 'Cert:\CurrentUser\My' ` -TextExtension @('2.5.29.37={text}1.3.6.1.5.5.7.3.3', '2.5.29.19={text}') $sec = ConvertTo-SecureString -String $PfxPassword -Force -AsPlainText - Export-PfxCertificate -Cert "Cert:\CurrentUser\My\$($cert.Thumbprint)" -FilePath $pfxPath -Password $sec | Out-Null - Export-Certificate -Cert "Cert:\CurrentUser\My\$($cert.Thumbprint)" -FilePath $cerPath | Out-Null - Remove-Item "Cert:\CurrentUser\My\$($cert.Thumbprint)" -Force + Export-PfxCertificate -Cert "Cert:\CurrentUser\My\$($tmp.Thumbprint)" -FilePath $pfxPath -Password $sec | Out-Null + Remove-Item "Cert:\CurrentUser\My\$($tmp.Thumbprint)" -Force } -# --- sign (timestamp best-effort; a self-signed cert needs no TSA) --- +# Always export the public .cer from the pfx. For a self-signed / private-trust cert it's the file +# users import once (Trusted People) — a STABLE cert (same pfx every build via the secret) means that +# import is a one-time, per-machine step that keeps working across upgrades. For a public-CA cert +# it's just an unused extra (harmless). The manifest Publisher must equal the cert's subject DN. +$pwsec = if ($PfxPassword) { ConvertTo-SecureString -String $PfxPassword -Force -AsPlainText } else { $null } +$pubCert = if ($pwsec) { Get-PfxCertificate -FilePath $pfxPath -Password $pwsec } else { Get-PfxCertificate -FilePath $pfxPath } +Export-Certificate -Cert $pubCert -FilePath $cerPath | Out-Null +Write-Host "signing cert subject=$($pubCert.Subject) thumbprint=$($pubCert.Thumbprint)" +if ($pubCert.Subject -ne $Publisher) { + Write-Warning "cert subject '$($pubCert.Subject)' != manifest Publisher '$Publisher' — Add-AppxPackage will reject the mismatch. Pass -Publisher '$($pubCert.Subject)'." +} + +# --- sign (timestamp best-effort) --- $signArgs = @('sign', '/fd', 'SHA256', '/f', $pfxPath) if ($PfxPassword) { $signArgs += @('/p', $PfxPassword) } & $signtool ($signArgs + @('/tr', 'http://timestamp.digicert.com', '/td', 'SHA256', $msix)) @@ -124,16 +133,12 @@ if ($LASTEXITCODE -ne 0) { } Remove-Item $pfxPath -Force -ErrorAction SilentlyContinue -# if self-signed, the public .cer must travel with the package; otherwise drop it (chain is public) -if (-not $selfSigned) { Remove-Item $cerPath -Force -ErrorAction SilentlyContinue } - Write-Host "" Write-Host "==> MSIX: $msix" -if ($selfSigned) { - Write-Host "==> self-signed; trust before install: Import-Certificate -FilePath '$cerPath' -CertStoreLocation Cert:\LocalMachine\TrustedPeople" -} +Write-Host "==> trust the cert once per machine (then it stays trusted across all future builds):" +Write-Host " Import-Certificate -FilePath '$cerPath' -CertStoreLocation Cert:\LocalMachine\TrustedPeople" # emit paths for the workflow to publish (only under CI, where GITHUB_ENV is set) if ($env:GITHUB_ENV) { "MSIX_PATH=$msix" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8 - if ($selfSigned) { "MSIX_CER_PATH=$cerPath" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8 } + "MSIX_CER_PATH=$cerPath" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8 } diff --git a/crates/punktfunk-client-windows/packaging/punktfunk-codesign.cer b/crates/punktfunk-client-windows/packaging/punktfunk-codesign.cer new file mode 100644 index 0000000000000000000000000000000000000000..499d9ba13682e24d9f29df0171f923970ee7a140 GIT binary patch literal 768 zcmXqLV)|py#Q0hBO}AHmiiz;!LHA(yHmEW+O%!?t+wOB8 zVkFjV?A#i+!1|b`pTw-RgWK{M&T{B$HB9k4HFIM_$3KOSbH12dn!WM<#f0UZ_pDwo zy0M%oVcmE44~ffV)9;9V`phv^>(zp(%IR0DDp$@wEAu+yIoq+9oWVXk&yICiR>$sV z?`n|vWLL6hx`||#@RkA<#dqIpp6z7~o+0sFWOnFT<1G@WRM%PfU8&8!m+Jjz#mOJj z^v<7udP{a*#+^r{lN#3tC9RZ=c(=vitWs`b`)<8Qr?z~!;`;F29f`xoBYiD{k9HL8 z);O>A=~hr;PowJ`mSrx|pO2f{Z!TqGW@KPo9AXe;zy}OxS$;;w|12!bOzaH?!XUmX z3y%R88;3RxK(ZP z)yCg%;$Kw?ANi$Jy5)dub@-aK^MCxfy8EJmbM**vjTlvoo9`uEaAdB>dLbm7AVz%$IUF zs3xZorfpi{ccXi6v#~YL?$=ND7u6~(d6n|;>$2zv6OXRBboucyg=PPfpUv2$HhoGT a!&-68E!;OE=G*QG&STke<@;6BC}jXeCp#|y literal 0 HcmV?d00001