Files
waggle-os/app/scripts/sign-windows-pilot.ps1
Oleg Maslov b20b138fe4 moving
2026-09-02 10:14:22 +02:00

203 lines
7.7 KiB
PowerShell

<#
.SYNOPSIS
Self-sign cert generation + signtool wrapper for the Wave-1 Egzakta-internal pilot build.
.DESCRIPTION
Implements docs/code-signing-pilot-and-launch.md §1.1 (Windows self-sign).
Two modes:
-Mode Setup Generate self-signed cert (idempotent — reuses existing cert
by subject if it exists), export to .pfx, write thumbprint
to app/src-tauri/.thumbprint.txt.
-Mode Sign Sign the artefact at -ArtifactPath using the cert produced
by Setup. Wraps signtool.exe.
Password resolution order (Setup): env WAGGLE_PILOT_PFX_PASSWORD; otherwise
Read-Host -AsSecureString prompt. Setup writes the .pfx to %USERPROFILE%
so it never lands inside the repo working tree.
NOT for public Day-0 signing — that uses a real EV Authenticode cert per §2.
.PARAMETER Mode
Setup or Sign. Setup is idempotent — safe to re-run.
.PARAMETER ArtifactPath
Required when -Mode Sign. Path to the .msi or .exe to sign.
.PARAMETER Subject
Cert subject. Default: "CN=Egzakta Internal Pilot, O=Egzakta Group, C=RS".
Override only if rotating cert identity.
.PARAMETER PfxPath
Where to write the exported .pfx. Default:
$env:USERPROFILE\waggle-pilot-codesign.pfx
.PARAMETER ThumbprintFile
Where to write the captured thumbprint for downstream consumption by
apply-signing-config.mjs. Default: app/src-tauri/.thumbprint.txt
(relative to repo root, resolved via this script's location).
.PARAMETER TimestampUrl
RFC3161 timestamp server. Default: http://timestamp.digicert.com.
.EXAMPLE
PS> $env:WAGGLE_PILOT_PFX_PASSWORD = "your-strong-pw"
PS> .\sign-windows-pilot.ps1 -Mode Setup
Generates cert (or reuses existing), writes thumbprint to .thumbprint.txt.
.EXAMPLE
PS> .\sign-windows-pilot.ps1 -Mode Sign -ArtifactPath .\target\release\bundle\msi\Waggle_0.2.0_x64_en-US.msi
Signs the MSI using the cert from Setup.
.NOTES
Last updated: LAUNCH-06 (Phase 2 Step 4).
#>
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[ValidateSet('Setup', 'Sign')]
[string]$Mode,
[Parameter()]
[string]$ArtifactPath,
[Parameter()]
[string]$Subject = 'CN=Egzakta Internal Pilot, O=Egzakta Group, C=RS',
[Parameter()]
[string]$PfxPath = (Join-Path $env:USERPROFILE 'waggle-pilot-codesign.pfx'),
[Parameter()]
[string]$ThumbprintFile,
[Parameter()]
[string]$TimestampUrl = 'http://timestamp.digicert.com'
)
$ErrorActionPreference = 'Stop'
# ─── Resolve repo-root paths ────────────────────────────────────────────────
$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
$AppDir = Split-Path -Parent $ScriptDir # ...\waggle-os\app
$RepoRoot = Split-Path -Parent $AppDir # ...\waggle-os
if (-not $ThumbprintFile) {
$ThumbprintFile = Join-Path $AppDir 'src-tauri\.thumbprint.txt'
}
# ─── Helpers ────────────────────────────────────────────────────────────────
function Resolve-Password {
if ($env:WAGGLE_PILOT_PFX_PASSWORD) {
return ConvertTo-SecureString -String $env:WAGGLE_PILOT_PFX_PASSWORD -Force -AsPlainText
}
Write-Host 'WAGGLE_PILOT_PFX_PASSWORD not set in env — prompting.' -ForegroundColor Yellow
return Read-Host -Prompt 'Enter password to protect the .pfx export' -AsSecureString
}
function Find-Signtool {
# Prefer signtool from latest installed Windows SDK; fall back to PATH.
$candidates = @(
'C:\Program Files (x86)\Windows Kits\10\bin\10.0.22621.0\x64\signtool.exe',
'C:\Program Files (x86)\Windows Kits\10\bin\10.0.22000.0\x64\signtool.exe',
'C:\Program Files (x86)\Windows Kits\10\bin\10.0.19041.0\x64\signtool.exe'
)
foreach ($candidate in $candidates) {
if (Test-Path $candidate) { return $candidate }
}
$fromPath = Get-Command signtool.exe -ErrorAction SilentlyContinue
if ($fromPath) { return $fromPath.Source }
throw 'signtool.exe not found. Install Windows 10 SDK or add signtool to PATH.'
}
# ─── Mode: Setup ────────────────────────────────────────────────────────────
if ($Mode -eq 'Setup') {
Write-Host "[setup] subject: $Subject"
Write-Host "[setup] pfx path: $PfxPath"
Write-Host "[setup] thumbprint out: $ThumbprintFile"
# Reuse existing cert by subject if present (idempotency).
$existing = Get-ChildItem 'Cert:\CurrentUser\My' |
Where-Object { $_.Subject -eq $Subject -and $_.HasPrivateKey } |
Sort-Object NotAfter -Descending |
Select-Object -First 1
if ($existing -and $existing.NotAfter -gt (Get-Date)) {
Write-Host "[setup] reusing existing cert (NotAfter $($existing.NotAfter))" -ForegroundColor Green
$cert = $existing
} else {
Write-Host '[setup] generating new self-signed code-signing cert' -ForegroundColor Cyan
$cert = New-SelfSignedCertificate `
-Type CodeSigningCert `
-Subject $Subject `
-KeyUsage DigitalSignature `
-KeySpec Signature `
-KeyAlgorithm RSA -KeyLength 2048 `
-NotAfter (Get-Date).AddYears(2) `
-CertStoreLocation 'Cert:\CurrentUser\My' `
-TextExtension @('2.5.29.37={text}1.3.6.1.5.5.7.3.3', '2.5.29.19={text}')
}
# Export .pfx (always — re-export is harmless and refreshes the file).
$pwd = Resolve-Password
Export-PfxCertificate -Cert $cert -FilePath $PfxPath -Password $pwd | Out-Null
Write-Host "[setup] exported .pfx -> $PfxPath" -ForegroundColor Green
# Write thumbprint where apply-signing-config.mjs expects it.
$thumbprintDir = Split-Path -Parent $ThumbprintFile
if (-not (Test-Path $thumbprintDir)) {
New-Item -ItemType Directory -Force -Path $thumbprintDir | Out-Null
}
Set-Content -Path $ThumbprintFile -Value $cert.Thumbprint -Encoding ascii -NoNewline
Write-Host "[setup] thumbprint -> $ThumbprintFile" -ForegroundColor Green
Write-Host ''
Write-Host 'Next:' -ForegroundColor Cyan
Write-Host ' 1. npm run tauri:build:win:pilot-signed'
Write-Host ' 2. .\scripts\sign-windows-pilot.ps1 -Mode Sign -ArtifactPath <path-to-msi> # optional'
return
}
# ─── Mode: Sign ─────────────────────────────────────────────────────────────
if ($Mode -eq 'Sign') {
if (-not $ArtifactPath) {
throw '-ArtifactPath required when -Mode Sign'
}
if (-not (Test-Path $ArtifactPath)) {
throw "Artifact not found: $ArtifactPath"
}
if (-not (Test-Path $PfxPath)) {
throw "PFX not found at $PfxPath. Run -Mode Setup first."
}
$signtool = Find-Signtool
$pwd = Resolve-Password
$plainPwd = [System.Net.NetworkCredential]::new('', $pwd).Password
Write-Host "[sign] signtool: $signtool"
Write-Host "[sign] artifact: $ArtifactPath"
& $signtool sign `
/f $PfxPath `
/p $plainPwd `
/tr $TimestampUrl `
/td sha256 /fd sha256 `
$ArtifactPath
if ($LASTEXITCODE -ne 0) {
throw "signtool failed with exit code $LASTEXITCODE"
}
Write-Host '[sign] verifying signature' -ForegroundColor Cyan
& $signtool verify /pa /v $ArtifactPath
if ($LASTEXITCODE -ne 0) {
throw "signtool verify failed with exit code $LASTEXITCODE"
}
Write-Host '[sign] OK' -ForegroundColor Green
return
}