feat: sign-dry-run.ps1 + CERT LICENSE-FONTS + EmbedBrandFonts default off
Some checks failed
windows-msi / full-msi-unsigned (push) Has been cancelled
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:
parent
86809b27eb
commit
6090b41f8f
8 changed files with 450 additions and 15 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
342
platform/windows/scripts/sign-dry-run.ps1
Normal file
342
platform/windows/scripts/sign-dry-run.ps1
Normal 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
|
||||
}
|
||||
|
|
@ -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).
|
||||
|
|
|
|||
27
site/printit.verae.ai/fonts/LICENSE-FONTS.md
Normal file
27
site/printit.verae.ai/fonts/LICENSE-FONTS.md
Normal 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)
|
||||
|
|
@ -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.
|
||||
|
|
|
|||
Loading…
Reference in a new issue