PrintToVeraeTimeStamp/platform/windows/scripts/sign-dry-run.ps1
George Lambert 3140e6c76f
Some checks failed
windows-msi / full-msi-unsigned (push) Has been cancelled
feat: PUBLISH-DIGESTS.md + commented Authenticode CI paths + EmbedBrandFonts TTF
windows-msi-sign.yml.example; gofpdf register when EmbedBrandFonts and fonts present.
2026-10-14 01:24:11 -04:00

343 lines
13 KiB
PowerShell

#Requires -Version 5.1
<#
.SYNOPSIS
Authenticode dry-run: locate signtool, report unsigned MSI/EXE, print real sign commands (no secrets).
.DESCRIPTION
Operator helper for Issue 1 commercial signing. Does NOT import certs, does NOT sign,
and never prints PFX passwords or base64 blobs.
- If signtool is missing: print install tip and exit 0 (scaffold-friendly; CI can still
gate live sign separately).
- If signtool is present: for each found MSI/EXE, run `signtool verify /pa` and report
unsigned vs already signed; then print copy-paste `signtool sign` lines using env-name
placeholders only (see CI-SIGN-NOTARY.md).
Related:
platform/windows/wix/CI-SIGN-NOTARY.md
docs/COMMERCIAL-INSTALLER-BACKLOG.md
.EXITCODES
0 Always on success path (including "no signtool" tip, no artifacts found, or verify done).
1 Terminating error (bad args, unreadable path).
.PARAMETER Path
Explicit artifact paths (MSI, setup.exe, bare helper EXE). Also accepts leftover positionals.
.PARAMETER Version
Expected HelperVersion stamp when auto-searching (default: Get-VeraePackageVersion / 0.3.6).
.PARAMETER Arch
Artifact arch segment for auto-search (default: amd64).
.PARAMETER SearchRoot
Extra directory prepended to the default search list (dist/, wix/out, StageFull/bin, …).
.PARAMETER SkipVerify
Do not run signtool verify; only print presence + sign command templates.
.PARAMETER TimestampUrl
Placeholder TSA URL printed in sign templates (default: env WINDOWS_SIGN_TIMESTAMP_URL
or https://timestamp.digicert.com). Not contacted by this script.
.EXAMPLE
powershell -NoProfile -ExecutionPolicy Bypass -File sign-dry-run.ps1
.EXAMPLE
.\sign-dry-run.ps1 -Path .\platform\windows\wix\out\VeraePrintStamp-windows-amd64-0.3.6.msi
.EXAMPLE
.\sign-dry-run.ps1 -SkipVerify -SearchRoot D:\release\out
#>
param(
[Parameter(ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)]
[Alias("FullName", "PSPath")]
[string[]]$Path = @(),
[string]$Version = "",
[string]$Arch = "amd64",
[string]$SearchRoot = "",
[switch]$SkipVerify,
[string]$TimestampUrl = ""
)
begin {
$ErrorActionPreference = "Stop"
$collected = New-Object System.Collections.Generic.List[string]
}
process {
foreach ($p in @($Path)) {
if ($null -eq $p) { continue }
$s = "$p".Trim()
if (-not $s) { continue }
if ($s -match '^Microsoft\.PowerShell\.Core\\FileSystem::(.+)$') {
$s = $Matches[1]
}
[void]$collected.Add($s)
}
}
end {
if ($args -and $args.Count -gt 0) {
foreach ($a in $args) {
$s = "$a".Trim()
if ($s) { [void]$collected.Add($s) }
}
}
$common = Join-Path $PSScriptRoot "common.ps1"
if (Test-Path -LiteralPath $common) { . $common }
$RepoRoot = (Resolve-Path (Join-Path $PSScriptRoot "..\..\..")).Path
$WinDir = (Resolve-Path (Join-Path $PSScriptRoot "..")).Path
$WixDir = Join-Path $WinDir "wix"
$StageFullBin = Join-Path $WixDir "StageFull\bin"
if (-not $Version) {
if (Get-Command Get-VeraePackageVersion -ErrorAction SilentlyContinue) {
$Version = Get-VeraePackageVersion
} elseif ($env:VERSION -and $env:VERSION.Trim()) {
$Version = $env:VERSION.Trim()
} else {
$Version = "0.3.6"
}
}
if (-not $TimestampUrl) {
if ($env:WINDOWS_SIGN_TIMESTAMP_URL -and $env:WINDOWS_SIGN_TIMESTAMP_URL.Trim()) {
$TimestampUrl = $env:WINDOWS_SIGN_TIMESTAMP_URL.Trim()
} else {
$TimestampUrl = "https://timestamp.digicert.com"
}
}
function Write-Section([string]$Title) {
Write-Host ""
Write-Host ("==== {0} ====" -f $Title) -ForegroundColor Cyan
}
function Find-SignTool {
$cmd = Get-Command signtool.exe -ErrorAction SilentlyContinue
if ($cmd -and $cmd.Source) { return $cmd.Source }
$roots = New-Object System.Collections.Generic.List[string]
if ($env:WindowsSdkDir) { [void]$roots.Add($env:WindowsSdkDir) }
$pf86 = [Environment]::GetEnvironmentVariable("ProgramFiles(x86)")
if ($pf86) {
[void]$roots.Add((Join-Path $pf86 "Windows Kits\10\bin"))
}
$pf = [Environment]::GetEnvironmentVariable("ProgramFiles")
if ($pf) {
[void]$roots.Add((Join-Path $pf "Windows Kits\10\bin"))
}
$candidates = New-Object System.Collections.Generic.List[string]
foreach ($r in $roots) {
if (-not $r) { continue }
if (-not (Test-Path -LiteralPath $r)) { continue }
# Prefer x64 under newest SDK version folder
Get-ChildItem -LiteralPath $r -Directory -ErrorAction SilentlyContinue |
Sort-Object Name -Descending |
ForEach-Object {
foreach ($archDir in @("x64", "x86")) {
$p = Join-Path $_.FullName (Join-Path $archDir "signtool.exe")
if (Test-Path -LiteralPath $p) { [void]$candidates.Add($p) }
}
}
$direct = Join-Path $r "signtool.exe"
if (Test-Path -LiteralPath $direct) { [void]$candidates.Add($direct) }
}
if ($candidates.Count -gt 0) { return $candidates[0] }
return $null
}
function Find-DefaultArtifacts {
$nsisLeaf = "VeraePrintStamp-windows-$Arch-$Version-setup.exe"
$msiLeaf = "VeraePrintStamp-windows-$Arch-$Version.msi"
$searchDirs = @(
(Join-Path $RepoRoot "dist"),
(Join-Path $RepoRoot "dist\windows"),
(Join-Path $WixDir "out"),
(Join-Path $WinDir "out"),
(Join-Path $RepoRoot "out"),
$StageFullBin
)
if ($SearchRoot) { $searchDirs = @($SearchRoot) + $searchDirs }
$found = New-Object System.Collections.Generic.List[string]
foreach ($d in $searchDirs) {
if (-not $d) { continue }
if (-not (Test-Path -LiteralPath $d)) { continue }
foreach ($leaf in @($msiLeaf, $nsisLeaf)) {
$exact = Join-Path $d $leaf
if (Test-Path -LiteralPath $exact) {
$full = (Get-Item -LiteralPath $exact).FullName
if (-not ($found -contains $full)) { [void]$found.Add($full) }
}
}
Get-ChildItem -LiteralPath $d -File -ErrorAction SilentlyContinue |
Where-Object {
($_.Extension -ieq ".msi" -and $_.Name -like "VeraePrintStamp-windows-*.msi" -and $_.Name -notlike "*Tray*") -or
($_.Name -like "VeraePrintStamp-windows-*-setup.exe") -or
($_.Name -ieq "verae-print.exe") -or
($_.Name -ieq "verae-printhelper.exe") -or
($_.Name -like "verae-print-windows-*.exe")
} |
ForEach-Object {
$full = $_.FullName
if (-not ($found -contains $full)) { [void]$found.Add($full) }
}
}
return ,$found.ToArray()
}
Write-Host "sign-dry-run — does NOT sign, import certs, or print secrets." -ForegroundColor DarkGray
Write-Host ("HelperVersion expect: {0} arch: {1}" -f $Version, $Arch)
Write-Host ("Repo: {0}" -f $RepoRoot)
Write-Host "Docs: platform/windows/wix/CI-SIGN-NOTARY.md"
Write-Host " docs/COMMERCIAL-INSTALLER-BACKLOG.md"
Write-Section "signtool"
$signtool = Find-SignTool
if (-not $signtool) {
Write-Host " signtool.exe: NOT FOUND" -ForegroundColor Yellow
Write-Host ""
Write-Host " Tip: install Windows 10/11 SDK (Signing Tools) or Visual Studio Build Tools," -ForegroundColor DarkGray
Write-Host " then ensure signtool is on PATH, e.g.:" -ForegroundColor DarkGray
Write-Host ' %ProgramFiles(x86)%\Windows Kits\10\bin\<ver>\x64\signtool.exe' -ForegroundColor DarkGray
Write-Host " Live CI still uses secret names only — see CI-SIGN-NOTARY.md (sign step not enabled)." -ForegroundColor DarkGray
Write-Host " Exiting 0 (dry-run scaffold; no signtool required)." -ForegroundColor DarkGray
exit 0
}
Write-Host (" signtool.exe: {0}" -f $signtool) -ForegroundColor Green
# Collect artifacts
$artifacts = New-Object System.Collections.Generic.List[string]
foreach ($raw in $collected) {
if (-not (Test-Path -LiteralPath $raw)) {
Write-Host (" WARN: path not found (skipped): {0}" -f $raw) -ForegroundColor Yellow
continue
}
$item = Get-Item -LiteralPath $raw
if ($item.PSIsContainer) {
Write-Host (" WARN: directory skipped (need file): {0}" -f $raw) -ForegroundColor Yellow
continue
}
$ext = $item.Extension.ToLowerInvariant()
if ($ext -notin @(".msi", ".exe", ".dll", ".cab")) {
Write-Host (" WARN: unsupported extension (skipped): {0}" -f $item.FullName) -ForegroundColor Yellow
continue
}
[void]$artifacts.Add($item.FullName)
}
if ($artifacts.Count -eq 0) {
$auto = Find-DefaultArtifacts
foreach ($a in $auto) { [void]$artifacts.Add($a) }
}
Write-Section "Artifacts"
if ($artifacts.Count -eq 0) {
Write-Host " No MSI/EXE found under dist/, wix/out, StageFull/bin, or -Path." -ForegroundColor Yellow
Write-Host " Build first: platform\windows\build-windows.ps1 then scripts\build-full-msi.ps1" -ForegroundColor DarkGray
Write-Host " Continuing with sign command templates only." -ForegroundColor DarkGray
} else {
foreach ($a in $artifacts) {
$len = (Get-Item -LiteralPath $a).Length
Write-Host (" {0} ({1:N0} bytes)" -f $a, $len)
}
}
if (-not $SkipVerify -and $artifacts.Count -gt 0) {
Write-Section "Verify (signtool verify /pa)"
Write-Host " Expect UNSIGNED until Authenticode is live in CI." -ForegroundColor DarkGray
foreach ($a in $artifacts) {
$psi = New-Object System.Diagnostics.ProcessStartInfo
$psi.FileName = $signtool
$psi.Arguments = "verify /pa `"$a`""
$psi.RedirectStandardOutput = $true
$psi.RedirectStandardError = $true
$psi.UseShellExecute = $false
$psi.CreateNoWindow = $true
$proc = New-Object System.Diagnostics.Process
$proc.StartInfo = $psi
[void]$proc.Start()
$stdout = $proc.StandardOutput.ReadToEnd()
$stderr = $proc.StandardError.ReadToEnd()
$proc.WaitForExit()
$code = $proc.ExitCode
$leaf = Split-Path -Leaf $a
if ($code -eq 0) {
Write-Host (" SIGNED {0}" -f $leaf) -ForegroundColor Green
if ($stdout) {
($stdout -split "`r?`n" | Where-Object { $_.Trim() } | Select-Object -First 3) |
ForEach-Object { Write-Host (" {0}" -f $_) -ForegroundColor DarkGray }
}
} else {
Write-Host (" UNSIGNED {0} (verify exit {1})" -f $leaf, $code) -ForegroundColor Yellow
$errLine = ($stderr -split "`r?`n" | Where-Object { $_.Trim() } | Select-Object -First 1)
if (-not $errLine) {
$errLine = ($stdout -split "`r?`n" | Where-Object { $_.Trim() } | Select-Object -First 1)
}
if ($errLine) {
Write-Host (" {0}" -f $errLine.Trim()) -ForegroundColor DarkGray
}
}
}
} elseif ($SkipVerify) {
Write-Section "Verify"
Write-Host " Skipped (-SkipVerify)." -ForegroundColor DarkGray
}
Write-Section "Real sign commands (placeholders — no secrets)"
Write-Host " Env names only (never commit values). See CI-SIGN-NOTARY.md." -ForegroundColor DarkGray
Write-Host " WINDOWS_CERT_PFX_BASE64 / WINDOWS_CERT_PFX_PASSWORD — CI import PFX" -ForegroundColor DarkGray
Write-Host " WINDOWS_CERT_THUMBPRINT — optional pre-installed cert" -ForegroundColor DarkGray
Write-Host " WINDOWS_SIGN_TIMESTAMP_URL — RFC 3161 TSA" -ForegroundColor DarkGray
Write-Host " WINDOWS_TRUSTED_SIGNING_* — optional Azure Trusted Signing" -ForegroundColor DarkGray
Write-Host ""
Write-Host " # 1) Import PFX once per runner (PowerShell) — do NOT echo password or base64:" -ForegroundColor Cyan
Write-Host ' $pfx = Join-Path $env:RUNNER_TEMP "codesign.pfx"'
Write-Host ' [IO.File]::WriteAllBytes($pfx, [Convert]::FromBase64String($env:WINDOWS_CERT_PFX_BASE64))'
Write-Host ' $pwd = ConvertTo-SecureString $env:WINDOWS_CERT_PFX_PASSWORD -AsPlainText -Force'
Write-Host ' Import-PfxCertificate -FilePath $pfx -CertStoreLocation Cert:\CurrentUser\My -Password $pwd | Out-Null'
Write-Host ' Remove-Item -LiteralPath $pfx -Force -ErrorAction SilentlyContinue'
Write-Host ""
Write-Host " # 2) Sign each PE/MSI (cmd / PowerShell). Prefer /sha1 thumbprint when known:" -ForegroundColor Cyan
$tsa = $TimestampUrl
$targets = @()
if ($artifacts.Count -gt 0) {
$targets = @($artifacts)
} else {
$targets = @(
"path\to\verae-print.exe",
"path\to\verae-printhelper.exe",
"path\to\VeraePrintStamp-windows-$Arch-$Version.msi",
"path\to\VeraePrintStamp-windows-$Arch-$Version-setup.exe"
)
}
foreach ($t in $targets) {
Write-Host (' "{0}" sign /fd SHA256 /tr {1} /td SHA256 /a "{2}"' -f $signtool, $tsa, $t)
Write-Host (' # or: "{0}" sign /fd SHA256 /tr {1} /td SHA256 /sha1 %WINDOWS_CERT_THUMBPRINT% "{2}"' -f $signtool, $tsa, $t)
}
Write-Host ""
Write-Host " # 3) Re-verify after sign:" -ForegroundColor Cyan
foreach ($t in $targets) {
Write-Host (' "{0}" verify /pa "{1}"' -f $signtool, $t)
}
Write-Host ""
Write-Host " # 4) Hash for DOWNLOADS-MANIFEST (after upload), no sign:" -ForegroundColor Cyan
Write-Host " powershell -NoProfile -ExecutionPolicy Bypass -File platform\windows\scripts\fill-manifest-sha256.ps1 -Path <signed-msi>,<signed-setup.exe>"
Write-Host ""
Write-Host " Live CI: enable windows-msi-sign.yml.example / commented steps in windows-msi.yml (still left)." -ForegroundColor DarkGray
Write-Host " After sign+upload: docs/PUBLISH-DIGESTS.md + fill-manifest-sha256.ps1" -ForegroundColor DarkGray
Write-Host " This dry-run does not claim Authenticode shipped." -ForegroundColor DarkGray
exit 0
}