ci(windows): Authenticode-sign HashWallet.exe with the hardware token

The Windows lane already runs on the machine that physically holds the Sectigo
EV code-signing token, but the runner there is NT SERVICE\act_runner, which
cannot use it: SafeNet holds the token login per user and an operator
establishes that login interactively. So the build queues the launcher for the
signing broker that Fleet installs (such-fleet roles/windows_code_signing) and
waits for it to come back signed.

CI never holds the private key -- it is non-exportable on FIPS 140-2 Level 2
hardware -- and never holds the token password. Sectigo allows three
consecutive failed password attempts before the token needs vendor unlocking,
and a job that never supplies a password cannot spend one.

Only HashWallet.exe is signed. The bundled MSVC runtime DLLs are already
Microsoft-signed and must not be re-signed, and the plugin DLLs are loaded by
an already-trusted process; it is the launcher the user double-clicks that
SmartScreen judges.

The signature, its RFC 3161 timestamp and the expected signer are verified on
the exe that actually goes into the zip, and any of the three failing fails the
build. If the broker's circuit breaker is tripped the job says so and stops
rather than shipping an unsigned binary.

Procedure: docs platform/windows-hardware-token-signing.md.
This commit is contained in:
jwinterm
2026-08-31 18:00:18 -04:00
parent 3a60b921f4
commit 8e45de5928
+60
View File
@@ -445,6 +445,66 @@ jobs:
cp -v "$REDIST_DIR/msvcp140.dll" "$REDIST_DIR/vcruntime140.dll" "$REDIST_DIR/vcruntime140_1.dll" \
build/windows/x64/runner/Release/
# ---- Authenticode sign the launcher ----------------------------------
# This workflow already runs on the machine with the EV code-signing
# token in it, but the runner (NT SERVICE\act_runner) cannot use the
# token: SafeNet holds the login per user and an operator establishes it
# interactively. So the exe is queued for the Fleet-installed signing
# broker (such-fleet roles/windows_code_signing), which signs it in the
# operator's session. CI never holds the key or the token password, so it
# cannot spend one of the token's three password attempts.
#
# Only HashWallet.exe is signed. The bundled MSVC runtime DLLs are
# already Microsoft-signed and must not be re-signed, and the plugin DLLs
# are loaded by an already-trusted process -- it is the launcher the user
# double-clicks that SmartScreen judges.
- name: Sign HashWallet.exe with the hardware token
shell: powershell
run: |
$ErrorActionPreference = 'Stop'
$release = "build\windows\x64\runner\Release"
$exe = Join-Path $release 'HashWallet.exe'
if (-not (Test-Path $exe)) { throw "no HashWallet.exe at $exe" }
if (Test-Path 'C:\signing\SIGNING_LOCKED') {
Get-Content 'C:\signing\SIGNING_LOCKED'
throw "The signing broker's circuit breaker is tripped. An operator must clear it."
}
$queued = "hashbags-$env:GITHUB_RUN_ID.exe"
$inPath = "C:\signing\in\$queued"
$outPath = "C:\signing\out\$queued"
Copy-Item $exe $inPath -Force
Set-Content -LiteralPath "$inPath.ready" -Value $env:GITHUB_RUN_ID
Write-Host "queued $queued"
$deadline = (Get-Date).AddMinutes(10)
while ((Get-Date) -lt $deadline) {
if (Test-Path "$outPath.done") { break }
if (Test-Path 'C:\signing\SIGNING_LOCKED') {
Get-Content 'C:\signing\SIGNING_LOCKED'
throw "The broker tripped its circuit breaker while signing this artifact."
}
Start-Sleep -Seconds 5
}
if (-not (Test-Path "$outPath.done")) { throw "Timed out waiting for the signing broker." }
Move-Item $outPath $exe -Force
Remove-Item "$outPath.done" -Force -ErrorAction SilentlyContinue
# Verify on the binary that actually ships. A bad signature or a
# missing timestamp blocks the release.
$sig = Get-AuthenticodeSignature $exe
$sig | Format-List Status, StatusMessage,
@{n='Signer';e={$_.SignerCertificate.Subject}},
@{n='TimeStamper';e={$_.TimeStamperCertificate.Subject}}
if ($sig.Status -ne 'Valid') { throw "Authenticode status is $($sig.Status)" }
if (-not $sig.TimeStamperCertificate) { throw "Signature carries no RFC 3161 timestamp" }
if ($sig.SignerCertificate.Subject -notmatch 'Such Software LLC') {
throw "Unexpected signer: $($sig.SignerCertificate.Subject)"
}
Write-Host "SHA256 $((Get-FileHash $exe -Algorithm SHA256).Hash)"
# ---- Package + upload ------------------------------------------------
- name: List built artifacts (for debug visibility)
run: |