diff --git a/docs/COMMERCIAL-INSTALLER-BACKLOG.md b/docs/COMMERCIAL-INSTALLER-BACKLOG.md index f5a8cf2..bdaf41d 100644 --- a/docs/COMMERCIAL-INSTALLER-BACKLOG.md +++ b/docs/COMMERCIAL-INSTALLER-BACKLOG.md @@ -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--.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 diff --git a/internal/pdfcert/fallback.go b/internal/pdfcert/fallback.go index 5d49ca7..ccb8adc 100644 --- a/internal/pdfcert/fallback.go +++ b/internal/pdfcert/fallback.go @@ -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) diff --git a/internal/pdfcert/pdfcert.go b/internal/pdfcert/pdfcert.go index d9a2e72..b8329b2 100644 --- a/internal/pdfcert/pdfcert.go +++ b/internal/pdfcert/pdfcert.go @@ -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. diff --git a/internal/pdfcert/pdfcert_test.go b/internal/pdfcert/pdfcert_test.go index 8c4ce68..addfb28 100644 --- a/internal/pdfcert/pdfcert_test.go +++ b/internal/pdfcert/pdfcert_test.go @@ -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)) diff --git a/platform/windows/scripts/sign-dry-run.ps1 b/platform/windows/scripts/sign-dry-run.ps1 new file mode 100644 index 0000000..678b21b --- /dev/null +++ b/platform/windows/scripts/sign-dry-run.ps1 @@ -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\\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 ," + 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 +} diff --git a/platform/windows/wix/CI-SIGN-NOTARY.md b/platform/windows/wix/CI-SIGN-NOTARY.md index 65001fa..06f1444 100644 --- a/platform/windows/wix/CI-SIGN-NOTARY.md +++ b/platform/windows/wix/CI-SIGN-NOTARY.md @@ -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). diff --git a/site/printit.verae.ai/fonts/LICENSE-FONTS.md b/site/printit.verae.ai/fonts/LICENSE-FONTS.md new file mode 100644 index 0000000..65ccd22 --- /dev/null +++ b/site/printit.verae.ai/fonts/LICENSE-FONTS.md @@ -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) diff --git a/site/printit.verae.ai/fonts/README.md b/site/printit.verae.ai/fonts/README.md index 08b4157..643e30c 100644 --- a/site/printit.verae.ai/fonts/README.md +++ b/site/printit.verae.ai/fonts/README.md @@ -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.