feat(desktop): sign Windows installers via Azure Trusted Signing - #559
feat(desktop): sign Windows installers via Azure Trusted Signing#559Weegy wants to merge 9 commits into
Conversation
The existing Windows path expects a .p12 in WINDOWS_CSC_LINK_BASE64, which is
a dead end for any new certificate: since 2023-06-01 the CA/Browser Forum
requires every code-signing private key (OV *and* EV) to be generated and kept
in a FIPS 140-2 L2 / EAL4+ HSM, so no CA will issue an exportable file. As a
result Windows installers currently ship unsigned and SmartScreen shows an
'unknown publisher' warning.
Azure Trusted Signing (rebranded Azure Artifact Signing) solves this without a
hardware token, so it works on GitHub-hosted runners. Verified against the
pinned app-builder-lib@25.1.8 schema rather than assumed:
- win.azureSignOptions already exists in 25.1.8 — no electron-builder
upgrade needed
- required fields are endpoint, codeSigningAccountName, certificateProfileName
- electron-builder installs the TrustedSigning PowerShell module itself
(Install-Module -Name TrustedSigning -RequiredVersion 0.4.1) and reads
credentials via Azure EnvironmentCredential, which is why the three
AZURE_* credential names are fixed by Azure rather than chosen here
The three account coordinates are passed on the CLI instead of being written
into electron-builder.yml, so the committed config stays generic — a fork
without these secrets builds unsigned rather than failing against someone
else's Azure account.
Also adds an always-on Windows verification gate, mirroring the macOS one: it
runs whenever signing was configured, so 'secrets present but the installer
came out unsigned' fails the build instead of reaching users. It additionally
requires a trusted timestamp, without which the signature would expire with
the certificate. Uses Get-AuthenticodeSignature rather than signtool.exe to
avoid depending on a Windows SDK path.
The legacy .p12 path is kept for pre-2023 certificates but is now only used
when the Azure secrets are absent.
…ent-install race electron-builder signs every unpacked Windows binary in parallel. Each parallel Invoke-TrustedSigning call independently checks whether its NuGet dependencies (Microsoft.Trusted.Signing.Client, Microsoft.Windows.SDK.BuildTools) are installed, and on the first run none are cached yet — so N concurrent installs race for the same package file and lose with "the process cannot access the file because it is being used by another process". Not an auth/config problem, a bootstrap race. Sign one throwaway file (a copy of notepad.exe) single-threaded right after Azure signing is configured, so the one-time package install finishes before electron-builder's parallel batch starts.
…signing
electron-builder's default NSIS artifact name is
"${productName} Setup ${version}.${ext}" — the space in that filename
breaks Azure Trusted Signing: electron-builder invokes
`Invoke-TrustedSigning -Files <path>` without quoting the path, so
signtool truncates it at the first space and fails with
"SignTool Error: File not found".
Override nsis.artifactName to drop the space entirely rather than patch
electron-builder's quoting internals.
Test-only — will be reverted before merge. Needed so this workflow_dispatch run actually exercises the electron-builder.yml fixes that only exist on this branch.
electron-builder's WindowsSignAzureManager spreads any extra win.azureSignOptions keys straight through to Invoke-TrustedSigning as -<Key> <value>. We only ever passed endpoint/account/cert-profile, so the signature was valid but uncountersigned — Get-AuthenticodeSignature reports no TimeStamperCertificate, which the verification gate correctly rejects: an untimestamped signature stops validating the moment the cert expires. Add timestampRfc3161 (Microsoft's public Trusted/Artifact Signing timestamp authority) and timestampDigest.
This reverts commit 0af2312.
Verified end-to-end — three real bugs found + fixed on this branchAzure Trusted Signing account (
Final verification run produced a Windows installer with
Branch is up to date with |
Problem
Windows installers currently ship unsigned — SmartScreen shows an "unknown publisher" warning. Unlike the macOS defect in #558 the installer still runs, so this is a product-quality issue rather than an outage, but it hurts conversion on every download.
The existing path in the workflow expects a
.p12inWINDOWS_CSC_LINK_BASE64, and that is a dead end for any new certificate: since 2023-06-01 the CA/Browser Forum requires every code-signing signing key — OV and EV — to be generated and held in a FIPS 140-2 Level 2 / EAL4+ HSM. No CA will issue an exportable file any more, so that secret can only ever be filled from a pre-2023 certificate.Approach: Azure Trusted Signing
Azure Trusted Signing (rebranded Azure Artifact Signing in 2026) keeps the signing material in Microsoft's HSM and signs over an API, so it needs no hardware token and works on GitHub-hosted runners. ~$9.99/month, available to verified EU businesses.
Everything below was verified against the pinned
app-builder-lib@25.1.8, not assumed:win.azureSignOptionsin 25.1.8 schemaendpoint,codeSigningAccountName,certificateProfileNameInstall-Module -Name TrustedSigning -RequiredVersion 0.4.1)EnvironmentCredential→ the threeAZURE_*names are fixed by Azure, not chosen hereChanges
.github/workflows/desktop-apps.yml--config.win.azureSignOptions.*flags; fails fast ifAZURE_CLIENT_IDis set but any sibling is empty.github/workflows/desktop-apps.yml.github/workflows/desktop-apps.yml.p12path kept, but only used when the Azure secrets are absent; header documents why it is legacydesktop/README.mdThe three account coordinates are passed on the CLI rather than written into
electron-builder.yml, so the committed config stays generic — a fork without these secrets builds unsigned instead of failing against byte5's Azure account.Verification gate
Mirrors the macOS gate added in #558. It runs whenever signing was configured, so "secrets are present but the installer came out unsigned" fails the build instead of going green and reaching users. It also requires a trusted timestamp — without a countersignature the signature stops validating once the certificate expires. Uses
Get-AuthenticodeSignaturerather thansigntool.exe, so there is no Windows SDK path to locate.That failure mode is not hypothetical: it is exactly how v0.56.0 and v0.57.0 shipped a macOS app that could not be opened at all.
Verified so far
actionlintclean (the two info-levelSC2086hits are the intentional word-splitting on${EB_NOTARIZE:-}/${EB_WIN_SIGN:-})Not yet validated end-to-end
This cannot be tested until the Azure Trusted Signing account exists. Required setup on the byte5 Azure tenant:
AZURE_TENANT_ID,AZURE_CLIENT_ID,AZURE_CLIENT_SECRET,AZURE_SIGN_ENDPOINT,AZURE_SIGN_ACCOUNT,AZURE_SIGN_CERT_PROFILEUntil then this branch is a no-op for Windows: no Azure secrets → no
EB_WIN_SIGNflags → unsigned build, exactly as today. Merging it early is safe; it simply activates once the secrets land.Test plan
azureSignOptionssupport confirmed in the pinned electron-builderDesktop appsdispatched; Windows job signs and the gate passes.exeshows a valid, timestamped signature with the byte5 publisherNeed help on this PR? Tag
@codesmith-botwith what you need. Autofix is disabled.