feat: sign-dry-run.ps1 + CERT LICENSE-FONTS + EmbedBrandFonts default off
Some checks failed
windows-msi / full-msi-unsigned (push) Has been cancelled

Signtool verify/print sign recipe without secrets; PDF brand embed opt-in only.
This commit is contained in:
George Lambert 2026-10-14 00:55:24 -04:00
commit 6090b41f8f
8 changed files with 450 additions and 15 deletions

View file

@ -26,7 +26,7 @@ Repo now includes **authoring/docs scaffolds** for the two commercial gaps that
|-----------------|----------------------------------|
| `Product-Full.wxs` File components match StageFull; candle/light-ready | **Enable** (`enable-printer-cas.ps1`) **+ test** printer deferred CAs on clean VM (DUAL-SHIP VM C); parity with NSIS |
| `build-full-msi.ps1` StageFull + ProductVersion + exit codes | Live CI: **signtool / Trusted Signing** on MSI + EXEs (workflow sign step commented; secret names only) |
| `CI-SIGN-NOTARY.md` GHA/Forgejo draft (secret **names** only) | Live notarytool staple on macOS runners; clean-VM Gatekeeper proof |
| `CI-SIGN-NOTARY.md` GHA/Forgejo draft (secret **names** only) + [`sign-dry-run.ps1`](../platform/windows/scripts/sign-dry-run.ps1) local verify/print | Live CI **signtool** step still commented; live notarytool staple on macOS; clean-VM Gatekeeper proof |
| `SILENT_PROPS.md` msiexec one-liner + exit codes for full MSI | Properties honored by **scheduled** CAs (config write, skip printers/key) |
| HelperVersion path on build-windows / build-pkg / NSIS / build-installer / full MSI | Dual-ship **clean-VM validation** (A/B/C); then execute thin-NSIS exit checklist |
| Commented CA + `InstallExecuteSequence` stubs (`install-printer` schedule) | Intune/SCCM silent smoke with real exit codes; publish 0.3.6 signed blobs + manifest checksums |
@ -39,6 +39,7 @@ Repo now includes **authoring/docs scaffolds** for the two commercial gaps that
| **Printable VM A/B/C sheet** [`VM-DUAL-SHIP-CHECKLIST.md`](../platform/windows/wix/VM-DUAL-SHIP-CHECKLIST.md) | Operator sign-off on clean VMs (C blocked until CAs uncommented) |
| **Offline dual-ship smoke** [`dual-ship-smoke.ps1`](../platform/windows/scripts/dual-ship-smoke.ps1) (prints A/B/C; **VM C blocked until CAs enabled**; `-VerifyArtifacts` → existence + `fill-manifest-sha256.ps1`) | Does **not** replace clean-VM sign-off; no install/sign/upload |
| **Enable printer CAs** [`enable-printer-cas.ps1`](../platform/windows/scripts/enable-printer-cas.ps1) + wix README § Enable printer CAs | Unwraps `DEFERRED_PRINTER_CAS:OFF` in Product-Full for VM C; does **not** build/test/sign; default remains disabled |
| **Authenticode dry-run** [`sign-dry-run.ps1`](../platform/windows/scripts/sign-dry-run.ps1) + [`CI-SIGN-NOTARY.md`](../platform/windows/wix/CI-SIGN-NOTARY.md) | Local verify + print sign cmds only (**no secrets**). Live CI signtool / Trusted Signing still **not** enabled |
**Still use for day-to-day presence (no WiX / no Apple certs):**
Windows `scripts/build-tray-startmenu.ps1` + `install-tray-startup.ps1`; macOS `verae-print menubar --install` / unsigned `build-menubar-app.sh`. Full product install remains NSIS (`installer-windows.nsi`) and macOS `pkg/build-pkg.sh`. Optional: `build-tray-msi.ps1` (WiX tip exit 0); `build-full-msi.ps1` (needs WiX; exit 1 if missing).
@ -79,6 +80,7 @@ Related but out of scope for “done” above (still backlog-adjacent): Linux `.
| **Manifest SHA-256 row helper** | [`scripts/fill-manifest-sha256.ps1`](../platform/windows/scripts/fill-manifest-sha256.ps1) | **Tool** — local `Get-FileHash` → SHA256SUMS / MD rows; does **not** upload or rewrite site files |
| **Offline dual-ship smoke** | [`scripts/dual-ship-smoke.ps1`](../platform/windows/scripts/dual-ship-smoke.ps1) | **Tool** — prints VM A/B/C checklist; notes **C blocked until CAs enabled**; optional local MSI/NSIS existence + hash via fill-manifest; does **not** install or drive VMs |
| **Enable deferred printer CAs** | [`scripts/enable-printer-cas.ps1`](../platform/windows/scripts/enable-printer-cas.ps1) | **Tool** — toggles `DEFERRED_PRINTER_CAS:OFF` / `:ON` markers in Product-Full.wxs (VM C prereq); default disabled |
| **Authenticode dry-run** | [`scripts/sign-dry-run.ps1`](../platform/windows/scripts/sign-dry-run.ps1) | **Tool** — find `signtool`; verify unsigned MSI/EXE; print sign command templates (secret **names** only); exit 0 if no signtool; **does not sign** |
| Shared version helper | [`platform/windows/scripts/common.ps1`](../platform/windows/scripts/common.ps1) `Get-VeraePackageVersion` | Aligns tray/full MSI/NSIS drivers with `HelperVersion` |
| End-user Windows readme | [`platform/windows/README-WINDOWS.txt`](../platform/windows/README-WINDOWS.txt), [`README.txt`](../platform/windows/README.txt) | Documents NSIS flow; states **no elevated MSI** for tray |
| Packaging policy doc | [PACKAGING.md](PACKAGING.md) | “**No elevated MSI**” for tray presence by design |
@ -93,7 +95,7 @@ NSIS product already covers much of the *install experience* (files under Progra
|-----|----------------|----------------|
| **Full-product WiX authoring + StageFull driver** | **Exists** — `Product-Full.wxs` + `build-full-msi.ps1` | MSI binary only on hosts with candle/light; still dual-ship NSIS |
| **MSI output when WiX present** | `wix/out/VeraePrintStamp-windows-<arch>-<version>.msi` via `build-full-msi.ps1` | Not yet published to `dist/` / site downloads; local hash helper `fill-manifest-sha256.ps1` (no upload/CI publish job yet) |
| **No Authenticode signing pipeline** in-repo | SmartScreen / enterprise trust | `signtool` (or Azure Trusted Signing) for EXE/DLL **and** MSI; store certs in CI secrets only — never in git. **Not shipped.** |
| **No Authenticode signing pipeline** in-repo | SmartScreen / enterprise trust | Local dry-run: [`sign-dry-run.ps1`](../platform/windows/scripts/sign-dry-run.ps1). Live: `signtool` (or Azure Trusted Signing) for EXE/DLL **and** MSI; store certs in CI secrets only — never in git. **Live sign not shipped.** |
| **Silent / unattended install contract** | Fleet deploy | Props documented (`SILENT_PROPS.md`); wire scheduled CAs to honor them |
| **Upgrade / repair / ARP metadata** | Clean major-upgrade, Add/Remove Programs | WiX `UpgradeCode` present; migrate ARP fully from NSIS `UNINST_KEY` after dual-ship |
| **Custom actions parity with NSIS** | Printers, scheduled helper, per-user key | Port `install-printer.ps1`, helper registration into deferred/impersonated CAs (stubs commented in Product-Full) |
@ -108,7 +110,7 @@ NSIS product already covers much of the *install experience* (files under Progra
2. ~~Dual-ship runbook + commented printer CA schedule stubs~~ (**done**: [`DUAL-SHIP.md`](../platform/windows/wix/DUAL-SHIP.md); Product-Full commented `InstallExecuteSequence`).
3. ~~CI unsigned full MSI workflow + artifact~~ (**done**: [`.github/workflows/windows-msi.yml`](../.github/workflows/windows-msi.yml); DUAL-SHIP § CI artifact).
4. **Enable** printer/helper deferred CAs (`enable-printer-cas.ps1`) + test on clean VM (DUAL-SHIP VM C); keep NSIS primary until parity. `dual-ship-smoke` blocks C until CAs enabled.
5. Authenticode on binaries + MSI (**not shipped** — enable commented sign step; secret names in workflow + `CI-SIGN-NOTARY.md`).
5. Authenticode on binaries + MSI (**not shipped** — enable commented sign step; secret names in workflow + `CI-SIGN-NOTARY.md`; local dry-run [`sign-dry-run.ps1`](../platform/windows/scripts/sign-dry-run.ps1)).
6. Honor silent props in live CAs + Intune sample command line proof.
7. Publish 0.3.6 artifacts; refresh site `DOWNLOADS-MANIFEST` checksums via `fill-manifest-sha256.ps1` (hash-only; no upload); keep smoke on HelperVersion drift.
8. Deprecate or thin NSIS once MSI is the primary download (**thin-NSIS exit checklist** A–D in DUAL-SHIP.md — all boxes).
@ -183,7 +185,7 @@ NSIS product already covers much of the *install experience* (files under Progra
| Windows **MSI** full product (files + ARP) | **Yes authoring** (`Product-Full.wxs` + `build-full-msi.ps1` StageFull → light; CA schedule stubs commented) | **No commercial** — unsigned; printer CAs not live; NSIS still primary |
| Dual-ship NSIS↔MSI runbook | **Yes** ([`DUAL-SHIP.md`](../platform/windows/wix/DUAL-SHIP.md)) | **Docs only** — clean-VM validation + thin-NSIS exit checklist still unchecked |
| Menubar CI notary example | **Yes** ([`ci-notarize.yml.example`](../platform/macos/menubar/ci-notarize.yml.example)) | **Example only** — unsigned default; notary job `if: false` |
| Windows Authenticode pipeline | **Draft doc** (`wix/CI-SIGN-NOTARY.md`) | **No** — no live signtool job (**do not claim shipped**) |
| Windows Authenticode pipeline | **Draft doc** (`wix/CI-SIGN-NOTARY.md`) + **dry-run** [`sign-dry-run.ps1`](../platform/windows/scripts/sign-dry-run.ps1) | **No** — no live signtool job (**do not claim shipped**) |
| macOS menu bar LaunchAgent | Yes | Yes (dev/ops) |
| macOS unsigned menubar `.app` | Yes | **No** (Gatekeeper) |
| macOS **signed+notarized `.pkg`** script | Yes | **Near** — needs version sync, CI, clean-VM proof |
@ -201,6 +203,7 @@ NSIS product already covers much of the *install experience* (files under Progra
- [`platform/windows/wix/Product-Full.wxs`](../platform/windows/wix/Product-Full.wxs) + [`build-full-msi.ps1`](../platform/windows/scripts/build-full-msi.ps1) — elevated full MSI StageFull + candle/light
- [`platform/windows/wix/DUAL-SHIP.md`](../platform/windows/wix/DUAL-SHIP.md) — NSIS + MSI side-by-side validation runbook
- [`platform/windows/wix/CI-SIGN-NOTARY.md`](../platform/windows/wix/CI-SIGN-NOTARY.md) — CI Authenticode + notarytool sketch (**draft only**)
- [`platform/windows/scripts/sign-dry-run.ps1`](../platform/windows/scripts/sign-dry-run.ps1) — local `signtool` verify + print sign commands (no secrets; exit 0 if no signtool)
- [`platform/windows/wix/SILENT_PROPS.md`](../platform/windows/wix/SILENT_PROPS.md) — msiexec `/qn` property contract
- [`platform/macos/pkg/`](../platform/macos/pkg/) — commercial `.pkg` build
- [`platform/macos/menubar/`](../platform/macos/menubar/) — menu bar + unsigned `.app` + notarize docs/checklist

View file

@ -29,9 +29,12 @@ const (
// Minimal fallback when Node/jsPDF is unavailable.
//
// Fonts: PDF core / library faces only (Times title, Helvetica body).
// Do not claim Fraunces embed — no AddFont/TTF path unless a licensed
// Fraunces file is wired in later (OPEN-ISSUES #7 / TODO-6 I7).
// EmbedBrandFonts defaults false (see EmbedBrandFontsEnabled). Do not claim
// Fraunces embed — no AddFont/TTF path unless EmbedBrandFonts is on and a
// licensed Fraunces file is wired later (OPEN-ISSUES #7).
func buildFallbackGofpdf(in Input, docName, created, scope, merkle, lookup string) ([]byte, error) {
// Opt-in brand embed is not implemented yet; keep core fonts when false (default).
_ = EmbedBrandFontsEnabled(in)
pdf := gofpdf.New("P", "pt", "Letter", "")
pdf.SetCompression(false)
pdf.SetMargins(48, 48, 48)

View file

@ -35,6 +35,28 @@ type Input struct {
VerifyURL string
Channel string
ByteLength int64
// EmbedBrandFonts requests PDF embed of a licensed brand serif (Fraunces)
// when a face file is wired. Default false: core Times/Helvetica only.
// Also enabled via env VERAE_EMBED_BRAND_FONTS=1|true|yes (see EmbedBrandFontsEnabled).
// Today both paths still use standard fonts — no silent proprietary embed.
EmbedBrandFonts bool
}
// EmbedBrandFontsEnabled reports whether brand-font PDF embed is requested.
// Default false. True only when Input.EmbedBrandFonts is set or env
// VERAE_EMBED_BRAND_FONTS is 1/true/yes. Embed still requires a licensed face
// on disk; until that lands, Build keeps Times/Helvetica (OPEN-ISSUES #7).
func EmbedBrandFontsEnabled(in Input) bool {
if in.EmbedBrandFonts {
return true
}
v := strings.TrimSpace(os.Getenv("VERAE_EMBED_BRAND_FONTS"))
switch strings.ToLower(v) {
case "1", "true", "yes", "on":
return true
default:
return false
}
}
// ContentMultihash returns sha2-256 multihash hex (code 0x12, len 0x20 → f1220…)
@ -105,9 +127,13 @@ func LookupURL(sha256, receiptID string) string {
// available; otherwise gofpdf fallback.
//
// Typography: both paths use PDF standard fonts (Times title, Helvetica body).
// Fraunces is the brand serif on web CERT shells only — the issued PDF does
// not embed Fraunces unless a licensed face is explicitly added later.
// EmbedBrandFonts defaults false (env VERAE_EMBED_BRAND_FONTS off). Fraunces is
// the brand serif on web CERT shells; PDF embed stays opt-in and inactive until
// a licensed face is wired — never claim Fraunces in the issued PDF by default.
func Build(in Input) ([]byte, error) {
// EmbedBrandFontsEnabled(in) is consulted when a TTF/OFL path is added;
// until then standard faces only (default false — OPEN-ISSUES #7).
_ = EmbedBrandFontsEnabled(in)
created := in.CreatedAt
if created == "" {
// Do not invent a ledger time — leave blank for the PDF field.

View file

@ -260,9 +260,33 @@ func TestFindBuildScriptNoResearchHardcode(t *testing.T) {
}
}
// TestEmbedBrandFontsDefaultFalse: option/env off → no brand embed request.
func TestEmbedBrandFontsDefaultFalse(t *testing.T) {
t.Setenv("VERAE_EMBED_BRAND_FONTS", "")
if EmbedBrandFontsEnabled(Input{}) {
t.Fatal("EmbedBrandFonts must default false")
}
if EmbedBrandFontsEnabled(Input{EmbedBrandFonts: false}) {
t.Fatal("explicit false must stay false")
}
if !EmbedBrandFontsEnabled(Input{EmbedBrandFonts: true}) {
t.Fatal("Input.EmbedBrandFonts true must enable")
}
t.Setenv("VERAE_EMBED_BRAND_FONTS", "1")
if !EmbedBrandFontsEnabled(Input{}) {
t.Fatal("VERAE_EMBED_BRAND_FONTS=1 must enable")
}
t.Setenv("VERAE_EMBED_BRAND_FONTS", "false")
if EmbedBrandFontsEnabled(Input{}) {
t.Fatal("VERAE_EMBED_BRAND_FONTS=false must not enable")
}
}
// TestBuildUsesSystemFontsNotFraunces: issued PDF uses core Times/Helvetica;
// do not claim Fraunces embed until a licensed face is explicitly added (I7).
// EmbedBrandFonts defaults false — do not claim Fraunces embed until a licensed
// face is explicitly added and the option is on (I7).
func TestBuildUsesSystemFontsNotFraunces(t *testing.T) {
t.Setenv("VERAE_EMBED_BRAND_FONTS", "")
sha := "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
in := Input{
DocumentName: "FontCheck.pdf",
@ -270,6 +294,10 @@ func TestBuildUsesSystemFontsNotFraunces(t *testing.T) {
ReceiptID: "QmFontCheck",
CreatedAt: "2026-10-03T12:00:00Z",
Merkle: "99",
// EmbedBrandFonts: false (zero value) — default
}
if EmbedBrandFontsEnabled(in) {
t.Fatal("default Build input must not request brand font embed")
}
// gofpdf path — deterministic, no Node.
pdf, err := buildFallbackGofpdf(in, "FontCheck.pdf", in.CreatedAt, "GLOBAL", in.Merkle, LookupURL(sha, in.ReceiptID))

View file

@ -0,0 +1,342 @@
#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 commented sign step in .github/workflows/windows-msi.yml (still left)." -ForegroundColor DarkGray
Write-Host " This dry-run does not claim Authenticode shipped." -ForegroundColor DarkGray
exit 0
}

View file

@ -6,6 +6,7 @@
[`DUAL-SHIP.md`](DUAL-SHIP.md) § CI artifact,
[`.github/workflows/windows-msi.yml`](../../../.github/workflows/windows-msi.yml),
[`docs/COMMERCIAL-INSTALLER-BACKLOG.md`](../../../docs/COMMERCIAL-INSTALLER-BACKLOG.md),
[`platform/windows/scripts/sign-dry-run.ps1`](../scripts/sign-dry-run.ps1) — local `signtool verify` + print real sign commands (**no secrets**; exit 0 if no signtool),
[`platform/macos/menubar/NOTARIZE-CHECKLIST.md`](../../macos/menubar/NOTARIZE-CHECKLIST.md),
[`platform/macos/pkg/build-pkg.sh`](../../macos/pkg/build-pkg.sh).
@ -198,6 +199,8 @@ GHA/Forgejo copy-paste sketch (unsigned default, notary job gated off): [`ci-not
**Live:** [`.github/workflows/windows-msi.yml`](../../../.github/workflows/windows-msi.yml) builds an **unsigned** full MSI when WiX is installed on `windows-latest` and uploads the artifact (see DUAL-SHIP § CI artifact).
**Local dry-run (no CI secrets):** [`../scripts/sign-dry-run.ps1`](../scripts/sign-dry-run.ps1) finds `signtool`, runs `verify /pa` on local MSI/EXE when present, and prints copy-paste `signtool sign` lines using the env **names** above only. Missing signtool → exit 0 + Windows SDK tip. Does **not** sign or import a PFX.
**Not done:**
- Enable Authenticode `signtool` / Trusted Signing in that workflow (commented; secret **names** only).

View file

@ -0,0 +1,27 @@
# Font license note (printit CERT)
This directory does **not** ship Fraunces (or JetBrains Mono) binaries.
## Operator responsibility
The **operator** (site deploy owner) must place **SIL Open Font License 1.1 (OFL)** Fraunces themselves if they want self-hosted brand type:
1. Obtain Fraunces under OFL from upstream (e.g. google/fonts `ofl/fraunces` or underscript-type/fraunces).
2. Drop planned woff2 filenames here (see `README.md`).
3. Place the upstream **`OFL.txt`** beside those binaries (keep copyright notices).
4. Do **not** vendor proprietary faces or strip OFL notices.
Until those files exist on the deploy host, `certificate.html` falls back to the Google Fonts CDN (or system serif). Git keeps only placeholders (`.gitkeep`, docs, example fetch script) — no woff2 in-repo by default.
Optional mono: JetBrains Mono is also OFL; same operator drop-in rules.
## PDF certificates
Issued PDFs (`internal/pdfcert`) use core Times/Helvetica by default.
`EmbedBrandFonts` is **off** unless explicitly enabled — and even then requires a licensed face on disk (not provided here). See package comments.
## See also
- [`README.md`](./README.md) — planned filenames, local-first probe, fetch example
- [`fetch-fonts.example.sh`](./fetch-fonts.example.sh) — commented upstream URLs only
- OPEN-ISSUES #7 (CERT Fraunces pixel residual)

View file

@ -9,6 +9,7 @@ validates (woff2 magic `wOF2`), it injects local `@font-face` and **skips**
the Google Fonts CDN. If none are present, it loads CDN Fraunces + JetBrains
Mono. Binaries are **not** committed — only `.gitkeep` keeps the path in git.
Do **not** download or commit proprietary fonts.
Operators place OFL Fraunces themselves — see [`LICENSE-FONTS.md`](./LICENSE-FONTS.md).
## Table of Contents
@ -73,7 +74,7 @@ review. This tree must not ship proprietary font files.
| `certificate.html` title | Local `fonts/*.woff2` if present, else **Fraunces** CDN | Georgia / Times New Roman / serif |
| Mono digests | Local mono woff2 if present, else **JetBrains Mono** CDN | ui-monospace / SF Mono / Menlo |
| UI kit (`verae-ui-kit.css`) | Inter + Material Symbols Rounded CDN | system stacks in `brand/tokens.css` |
| Issued PDF (`internal/pdfcert`) | System / library serif — **not** Fraunces embed | N/A (separate from web) |
| Issued PDF (`internal/pdfcert`) | Core Times/Helvetica; **`EmbedBrandFonts` default false** (env `VERAE_EMBED_BRAND_FONTS`) | N/A until opt-in + licensed face |
Design source of truth: `verae-2026-design/ui-designs/src/html/CERT-01.html`
(tokens `--font-serif: 'Fraunces'`; design-kit `src/fonts/` ships Cantarell +
@ -112,7 +113,7 @@ operator uncomments a pinned-commit block after license review.
| Item | Owner / later |
|------|----------------|
| PDF CERT Fraunces embed | **I7 done:** pdfcert/build.mjs note + test — Times/Helvetica only until embed lands |
| PDF CERT Fraunces embed | **EmbedBrandFonts default false** (option + `VERAE_EMBED_BRAND_FONTS`); Times/Helvetica until enabled + licensed face |
| Inter / Material Symbols self-host for whole site | optional; CERT title is OPEN-ISSUES #7 residual |
| Cantarell from design-kit TTFs | GNOME mock only; not required for CERT-01 web title |
| Pixel-perfect CERT-01.html match | Still later (OPEN-ISSUES Advanced) |
@ -126,7 +127,8 @@ operator uncomments a pinned-commit block after license review.
| Web cert spacing toward CERT-01 | **Closer** — not full pixel |
| Local-first `@font-face` else CDN | **Done** — `certificate.html` probes `fonts/*.woff2` |
| Drop-in docs (this README) | **Done** |
| woff2 binaries + LICENSE in git | **Not started** — operator drop-in only |
| woff2 binaries + OFL in git | **Not started** — operator drop-in only ([LICENSE-FONTS.md](./LICENSE-FONTS.md)) |
| `EmbedBrandFonts` default false | **Done** — opt-in env/option; no embed path yet |
| Pixel match / PDF embed claim | **Not started** — do not claim |
## Contributing
@ -140,6 +142,7 @@ fetch script in CI.
Documentation in this directory: UNLICENSED © Verae LLC.
Font binaries, when added, remain under their upstream licenses (typically
SIL Open Font License 1.1 for Fraunces and JetBrains Mono). Ship `OFL.txt`
beside any binary. This directory does not re-license font files.
Font binaries are **not** shipped. Operators place OFL Fraunces themselves —
see [`LICENSE-FONTS.md`](./LICENSE-FONTS.md). When added, binaries remain under
upstream licenses (typically SIL OFL 1.1). Ship upstream `OFL.txt` beside any
binary. This directory does not re-license font files.