feat: PUBLISH-DIGESTS.md + commented Authenticode CI paths + EmbedBrandFonts TTF
Some checks failed
windows-msi / full-msi-unsigned (push) Has been cancelled
Some checks failed
windows-msi / full-msi-unsigned (push) Has been cancelled
windows-msi-sign.yml.example; gofpdf register when EmbedBrandFonts and fonts present.
This commit is contained in:
parent
9f0881ccfc
commit
3140e6c76f
13 changed files with 607 additions and 121 deletions
164
.github/workflows/windows-msi-sign.yml.example
vendored
Normal file
164
.github/workflows/windows-msi-sign.yml.example
vendored
Normal file
|
|
@ -0,0 +1,164 @@
|
|||
# EXAMPLE ONLY — not a live workflow.
|
||||
# Copy into .github/workflows/windows-msi-sign.yml (or .forgejo/workflows/) and enable
|
||||
# only when org secret store holds the names below. Default product CI remains
|
||||
# windows-msi.yml (unsigned full MSI).
|
||||
#
|
||||
# Secret / var NAMES only — never commit PFX bytes, passwords, thumbprints as values,
|
||||
# or Azure client secrets. See platform/windows/wix/CI-SIGN-NOTARY.md.
|
||||
#
|
||||
# GitHub Actions: secrets.* / vars.* as below.
|
||||
# Forgejo Actions: same secret names; map runs-on to your Windows label; keep names identical.
|
||||
#
|
||||
# After sign + upload: docs/PUBLISH-DIGESTS.md + fill-manifest-sha256.ps1 (hash only; no upload).
|
||||
#
|
||||
# name: windows-msi-sign
|
||||
# on:
|
||||
# workflow_dispatch:
|
||||
# # push:
|
||||
# # tags: ["printstamp-v*"]
|
||||
|
||||
name: windows-msi-sign-example
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
# id-token: write # uncomment if using Azure OIDC → Trusted Signing (no client secret)
|
||||
|
||||
jobs:
|
||||
# Live default stays unsigned — see windows-msi.yml.
|
||||
# This example job is gated off until secrets exist.
|
||||
full-msi-sign:
|
||||
if: ${{ false }} # flip only after secrets exist; do not claim SmartScreen until clean-VM proof
|
||||
runs-on: windows-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
|
||||
- name: Install WiX Toolset v3
|
||||
shell: pwsh
|
||||
run: |
|
||||
choco install wixtoolset --version 3.14.1 -y --no-progress
|
||||
$wixBin = "${env:ProgramFiles(x86)}\WiX Toolset v3.14\bin"
|
||||
if (-not (Test-Path -LiteralPath (Join-Path $wixBin "candle.exe"))) {
|
||||
$found = Get-ChildItem -Path "${env:ProgramFiles(x86)}" -Filter "candle.exe" -Recurse -ErrorAction SilentlyContinue |
|
||||
Where-Object { $_.FullName -match 'WiX Toolset' } |
|
||||
Select-Object -First 1
|
||||
if ($found) { $wixBin = $found.Directory.FullName }
|
||||
}
|
||||
if (-not (Test-Path -LiteralPath (Join-Path $wixBin "candle.exe"))) {
|
||||
Write-Error "WiX candle.exe not found after install."
|
||||
}
|
||||
"WIX_BIN=$wixBin" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8
|
||||
"PATH=$wixBin;$env:PATH" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8
|
||||
|
||||
- name: Build full MSI (pre-sign)
|
||||
shell: pwsh
|
||||
run: |
|
||||
powershell -NoProfile -ExecutionPolicy Bypass -File platform\windows\scripts\build-full-msi.ps1 -Arch amd64
|
||||
$msi = Get-ChildItem -Path platform\windows\wix\out -Filter "VeraePrintStamp-windows-*.msi" |
|
||||
Sort-Object LastWriteTime -Descending | Select-Object -First 1
|
||||
if (-not $msi) { Write-Error "No MSI under platform/windows/wix/out" }
|
||||
"MSI_PATH=$($msi.FullName)" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8
|
||||
"MSI_NAME=$($msi.Name)" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8
|
||||
$stage = "platform\windows\wix\StageFull\bin"
|
||||
"STAGE_BIN=$stage" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8
|
||||
|
||||
# --- Path A: PFX via secrets (signtool) ---
|
||||
# Secret / var names (placeholders — values never in git):
|
||||
# secrets.WINDOWS_CERT_PFX_BASE64
|
||||
# secrets.WINDOWS_CERT_PFX_PASSWORD
|
||||
# secrets.WINDOWS_CERT_THUMBPRINT # optional if /sha1 preferred after import
|
||||
# vars.WINDOWS_SIGN_TIMESTAMP_URL # RFC 3161 TSA
|
||||
#
|
||||
# - name: Import code-signing PFX
|
||||
# shell: pwsh
|
||||
# env:
|
||||
# WINDOWS_CERT_PFX_BASE64: ${{ secrets.WINDOWS_CERT_PFX_BASE64 }}
|
||||
# WINDOWS_CERT_PFX_PASSWORD: ${{ secrets.WINDOWS_CERT_PFX_PASSWORD }}
|
||||
# run: |
|
||||
# $pfx = Join-Path $env:RUNNER_TEMP "codesign.pfx"
|
||||
# [IO.File]::WriteAllBytes($pfx, [Convert]::FromBase64String($env:WINDOWS_CERT_PFX_BASE64))
|
||||
# $pwd = ConvertTo-SecureString $env:WINDOWS_CERT_PFX_PASSWORD -AsPlainText -Force
|
||||
# Import-PfxCertificate -FilePath $pfx -CertStoreLocation Cert:\CurrentUser\My -Password $pwd | Out-Null
|
||||
# Remove-Item -LiteralPath $pfx -Force -ErrorAction SilentlyContinue
|
||||
# # Never Upload-Artifact the PFX; never Write-Host password or base64.
|
||||
#
|
||||
# - name: signtool Authenticode (EXEs + MSI)
|
||||
# shell: pwsh
|
||||
# env:
|
||||
# WINDOWS_SIGN_TIMESTAMP_URL: ${{ vars.WINDOWS_SIGN_TIMESTAMP_URL }}
|
||||
# WINDOWS_CERT_THUMBPRINT: ${{ secrets.WINDOWS_CERT_THUMBPRINT }}
|
||||
# run: |
|
||||
# $tsa = $env:WINDOWS_SIGN_TIMESTAMP_URL
|
||||
# if (-not $tsa) { $tsa = "https://timestamp.digicert.com" }
|
||||
# $signtool = (Get-Command signtool.exe -ErrorAction Stop).Source
|
||||
# $targets = @(
|
||||
# (Join-Path $env:STAGE_BIN "verae-print.exe"),
|
||||
# (Join-Path $env:STAGE_BIN "verae-printhelper.exe"),
|
||||
# $env:MSI_PATH
|
||||
# ) | Where-Object { $_ -and (Test-Path -LiteralPath $_) }
|
||||
# foreach ($t in $targets) {
|
||||
# if ($env:WINDOWS_CERT_THUMBPRINT) {
|
||||
# & $signtool sign /fd SHA256 /tr $tsa /td SHA256 /sha1 $env:WINDOWS_CERT_THUMBPRINT $t
|
||||
# } else {
|
||||
# & $signtool sign /fd SHA256 /tr $tsa /td SHA256 /a $t
|
||||
# }
|
||||
# if ($LASTEXITCODE -ne 0) { Write-Error "signtool sign failed: $t" }
|
||||
# & $signtool verify /pa $t
|
||||
# if ($LASTEXITCODE -ne 0) { Write-Error "signtool verify failed: $t" }
|
||||
# }
|
||||
|
||||
# --- Path B: Azure Trusted Signing (prefer over PFX when available) ---
|
||||
# Secret / var names (placeholders — values never in git):
|
||||
# secrets.WINDOWS_TRUSTED_SIGNING_TENANT_ID # or OIDC; skip client secret
|
||||
# secrets.WINDOWS_TRUSTED_SIGNING_CLIENT_ID
|
||||
# secrets.WINDOWS_TRUSTED_SIGNING_CLIENT_SECRET # omit when id-token: write + federated creds
|
||||
# vars.WINDOWS_TRUSTED_SIGNING_ENDPOINT # e.g. https://cus.codesigning.azure.net
|
||||
# vars.WINDOWS_TRUSTED_SIGNING_ACCOUNT
|
||||
# vars.WINDOWS_TRUSTED_SIGNING_CERTIFICATE_PROFILE
|
||||
# vars.WINDOWS_SIGN_TIMESTAMP_URL # optional; action may default TSA
|
||||
#
|
||||
# - name: Azure Trusted Signing (EXEs + MSI)
|
||||
# # uses: azure/trusted-signing-action@v0 # pin a reviewed tag when enabling
|
||||
# # with:
|
||||
# # azure-tenant-id: ${{ secrets.WINDOWS_TRUSTED_SIGNING_TENANT_ID }}
|
||||
# # azure-client-id: ${{ secrets.WINDOWS_TRUSTED_SIGNING_CLIENT_ID }}
|
||||
# # azure-client-secret: ${{ secrets.WINDOWS_TRUSTED_SIGNING_CLIENT_SECRET }}
|
||||
# # endpoint: ${{ vars.WINDOWS_TRUSTED_SIGNING_ENDPOINT }}
|
||||
# # trusted-signing-account-name: ${{ vars.WINDOWS_TRUSTED_SIGNING_ACCOUNT }}
|
||||
# # certificate-profile-name: ${{ vars.WINDOWS_TRUSTED_SIGNING_CERTIFICATE_PROFILE }}
|
||||
# # files-folder: ${{ env.STAGE_BIN }}
|
||||
# # files-folder-filter: exe
|
||||
# # files-folder-recurse: true
|
||||
# # file-digest: SHA256
|
||||
# # timestamp-rfc3161: ${{ vars.WINDOWS_SIGN_TIMESTAMP_URL }}
|
||||
# # timestamp-digest: SHA256
|
||||
# shell: pwsh
|
||||
# run: |
|
||||
# Write-Error "Trusted Signing path not enabled — wire action + secret names above."
|
||||
|
||||
# - name: Upload signed MSI artifact
|
||||
# uses: actions/upload-artifact@v4
|
||||
# with:
|
||||
# name: VeraePrintStamp-windows-msi-signed
|
||||
# path: platform/windows/wix/out/VeraePrintStamp-windows-*.msi
|
||||
# if-no-files-found: error
|
||||
# retention-days: 14
|
||||
|
||||
# Operator after download: upload CDN blob → fill-manifest-sha256.ps1 → paste digests.
|
||||
# See docs/PUBLISH-DIGESTS.md (does not upload; secret values never land in manifests).
|
||||
|
||||
- name: Placeholder (example not enabled)
|
||||
shell: pwsh
|
||||
run: |
|
||||
Write-Host "windows-msi-sign.yml.example — sign job gated if: false."
|
||||
Write-Host "Live unsigned build: .github/workflows/windows-msi.yml"
|
||||
Write-Host "Local dry-run: platform/windows/scripts/sign-dry-run.ps1"
|
||||
Write-Host "Digests after sign+upload: docs/PUBLISH-DIGESTS.md"
|
||||
73
.github/workflows/windows-msi.yml
vendored
73
.github/workflows/windows-msi.yml
vendored
|
|
@ -79,22 +79,81 @@ jobs:
|
|||
|
||||
# --- Future Authenticode (NOT enabled) ---
|
||||
# Wire only after certs exist in the org secret store. Never commit PFX or passwords.
|
||||
# Full copy-paste example (gated if: false): .github/workflows/windows-msi-sign.yml.example
|
||||
# Local dry-run (no secrets): platform/windows/scripts/sign-dry-run.ps1
|
||||
# After sign + CDN upload: docs/PUBLISH-DIGESTS.md + fill-manifest-sha256.ps1
|
||||
# Secret / var names (placeholders only — see CI-SIGN-NOTARY.md):
|
||||
# secrets.WINDOWS_CERT_PFX_BASE64
|
||||
# secrets.WINDOWS_CERT_PFX_PASSWORD
|
||||
# secrets.WINDOWS_CERT_THUMBPRINT # optional if cert pre-installed
|
||||
# vars.WINDOWS_SIGN_TIMESTAMP_URL # RFC 3161 TSA
|
||||
# secrets.WINDOWS_TRUSTED_SIGNING_* # optional Azure Trusted Signing instead of PFX
|
||||
# Steps (when live): import PFX → signtool sign EXE + MSI → never upload PFX as artifact.
|
||||
# - name: Sign MSI + EXEs (Authenticode)
|
||||
# secrets.WINDOWS_CERT_THUMBPRINT # optional /sha1 after import
|
||||
# vars.WINDOWS_SIGN_TIMESTAMP_URL # RFC 3161 TSA
|
||||
# secrets.WINDOWS_TRUSTED_SIGNING_TENANT_ID # Path B: Azure Trusted Signing
|
||||
# secrets.WINDOWS_TRUSTED_SIGNING_CLIENT_ID
|
||||
# secrets.WINDOWS_TRUSTED_SIGNING_CLIENT_SECRET # omit when OIDC id-token: write
|
||||
# vars.WINDOWS_TRUSTED_SIGNING_ENDPOINT
|
||||
# vars.WINDOWS_TRUSTED_SIGNING_ACCOUNT
|
||||
# vars.WINDOWS_TRUSTED_SIGNING_CERTIFICATE_PROFILE
|
||||
#
|
||||
# --- Path A: PFX → signtool (commented; secret NAMES only) ---
|
||||
# - name: Import code-signing PFX
|
||||
# if: ${{ false }}
|
||||
# shell: pwsh
|
||||
# env:
|
||||
# WINDOWS_CERT_PFX_BASE64: ${{ secrets.WINDOWS_CERT_PFX_BASE64 }}
|
||||
# WINDOWS_CERT_PFX_PASSWORD: ${{ secrets.WINDOWS_CERT_PFX_PASSWORD }}
|
||||
# WINDOWS_SIGN_TIMESTAMP_URL: ${{ vars.WINDOWS_SIGN_TIMESTAMP_URL }}
|
||||
# run: |
|
||||
# Write-Error "Sign step not enabled — unsigned artifact only."
|
||||
# $pfx = Join-Path $env:RUNNER_TEMP "codesign.pfx"
|
||||
# [IO.File]::WriteAllBytes($pfx, [Convert]::FromBase64String($env:WINDOWS_CERT_PFX_BASE64))
|
||||
# $pwd = ConvertTo-SecureString $env:WINDOWS_CERT_PFX_PASSWORD -AsPlainText -Force
|
||||
# Import-PfxCertificate -FilePath $pfx -CertStoreLocation Cert:\CurrentUser\My -Password $pwd | Out-Null
|
||||
# Remove-Item -LiteralPath $pfx -Force -ErrorAction SilentlyContinue
|
||||
# # Never Upload-Artifact the PFX; never echo password or base64.
|
||||
#
|
||||
# - name: signtool Authenticode (EXEs + MSI)
|
||||
# if: ${{ false }}
|
||||
# shell: pwsh
|
||||
# env:
|
||||
# WINDOWS_SIGN_TIMESTAMP_URL: ${{ vars.WINDOWS_SIGN_TIMESTAMP_URL }}
|
||||
# WINDOWS_CERT_THUMBPRINT: ${{ secrets.WINDOWS_CERT_THUMBPRINT }}
|
||||
# run: |
|
||||
# $tsa = $env:WINDOWS_SIGN_TIMESTAMP_URL
|
||||
# if (-not $tsa) { $tsa = "https://timestamp.digicert.com" }
|
||||
# $signtool = (Get-Command signtool.exe -ErrorAction Stop).Source
|
||||
# $stage = "platform\windows\wix\StageFull\bin"
|
||||
# $targets = @(
|
||||
# (Join-Path $stage "verae-print.exe"),
|
||||
# (Join-Path $stage "verae-printhelper.exe"),
|
||||
# $env:MSI_PATH
|
||||
# ) | Where-Object { $_ -and (Test-Path -LiteralPath $_) }
|
||||
# foreach ($t in $targets) {
|
||||
# if ($env:WINDOWS_CERT_THUMBPRINT) {
|
||||
# & $signtool sign /fd SHA256 /tr $tsa /td SHA256 /sha1 $env:WINDOWS_CERT_THUMBPRINT $t
|
||||
# } else {
|
||||
# & $signtool sign /fd SHA256 /tr $tsa /td SHA256 /a $t
|
||||
# }
|
||||
# if ($LASTEXITCODE -ne 0) { Write-Error "signtool sign failed: $t" }
|
||||
# & $signtool verify /pa $t
|
||||
# if ($LASTEXITCODE -ne 0) { Write-Error "signtool verify failed: $t" }
|
||||
# }
|
||||
#
|
||||
# --- Path B: Azure Trusted Signing (prefer over PFX when available) ---
|
||||
# - name: Azure Trusted Signing (EXEs + MSI)
|
||||
# if: ${{ false }}
|
||||
# # uses: azure/trusted-signing-action@v0 # pin a reviewed tag when enabling
|
||||
# # with:
|
||||
# # azure-tenant-id: ${{ secrets.WINDOWS_TRUSTED_SIGNING_TENANT_ID }}
|
||||
# # azure-client-id: ${{ secrets.WINDOWS_TRUSTED_SIGNING_CLIENT_ID }}
|
||||
# # azure-client-secret: ${{ secrets.WINDOWS_TRUSTED_SIGNING_CLIENT_SECRET }}
|
||||
# # endpoint: ${{ vars.WINDOWS_TRUSTED_SIGNING_ENDPOINT }}
|
||||
# # trusted-signing-account-name: ${{ vars.WINDOWS_TRUSTED_SIGNING_ACCOUNT }}
|
||||
# # certificate-profile-name: ${{ vars.WINDOWS_TRUSTED_SIGNING_CERTIFICATE_PROFILE }}
|
||||
# # files: ${{ env.MSI_PATH }}
|
||||
# # file-digest: SHA256
|
||||
# # timestamp-rfc3161: ${{ vars.WINDOWS_SIGN_TIMESTAMP_URL }}
|
||||
# # timestamp-digest: SHA256
|
||||
# shell: pwsh
|
||||
# run: |
|
||||
# Write-Error "Trusted Signing not enabled — unsigned artifact only. See windows-msi-sign.yml.example."
|
||||
|
||||
- name: Upload unsigned MSI artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
|
|
|
|||
|
|
@ -19,24 +19,27 @@ Repo now includes **authoring/docs scaffolds** for the two commercial gaps that
|
|||
| **Menubar CI notary sketch** | [`platform/macos/menubar/ci-notarize.yml.example`](../platform/macos/menubar/ci-notarize.yml.example) | GHA/Forgejo **example** workflow: default **unsigned** `build-menubar-app.sh` + artifact; commented notarytool job gated `if: false`; secret **names** only (`APPLE_API_KEY_*`, `APPLE_CODESIGN_IDENTITY`, …). | Not a live workflow under `.github/`; no staple; not Gatekeeper-proven. |
|
||||
| **Menubar notary dry-run** | [`platform/macos/menubar/notary-dry-run.sh`](../platform/macos/menubar/notary-dry-run.sh) | Local helper: if `notarytool` present print codesign/`ditto`/submit/staple recipe with env **names** only; else Xcode CLT tip (exit 0). Links [`ci-notarize.yml.example`](../platform/macos/menubar/ci-notarize.yml.example) + this backlog. **Never submits** unless `NOTARY_SUBMIT=1`. | Not live CI; does not codesign by default; not Gatekeeper-proven. |
|
||||
| **Full elevated MSI (StageFull + light)** | [`Product-Full.wxs`](../platform/windows/wix/Product-Full.wxs) + [`scripts/build-full-msi.ps1`](../platform/windows/scripts/build-full-msi.ps1) (+ outline, [`SILENT_PROPS.md`](../platform/windows/wix/SILENT_PROPS.md), [`CI-SIGN-NOTARY.md`](../platform/windows/wix/CI-SIGN-NOTARY.md), [`DUAL-SHIP.md`](../platform/windows/wix/DUAL-SHIP.md)) | Elevated WiX v3 product: perMachine, Feature/ComponentGroups for **bin** (`verae-print.exe` required), **scripts**, **docs**, Start Menu; silent public props; distinct UpgradeCode. `build-full-msi.ps1` creates `StageFull/{bin,scripts}`, copies or Go-builds EXEs, copies scripts/LICENSE/README, stamps **ProductVersion** from `Get-VeraePackageVersion`, runs candle/light → `VeraePrintStamp-windows-*-<version>.msi`. WiX missing → **exit 1** + clear tip. Version defaults on build-windows / build-pkg / NSIS / build-installer aligned to HelperVersion. Commented deferred CA + `InstallExecuteSequence` stubs for `install-printer.ps1` (no binaries required). Dual-ship runbook documents NSIS vs MSI clean-VM gates + **thin-NSIS exit checklist**. | **Not** Authenticode-signed; printer CAs still **commented** (not scheduled/tested); properties not yet honored by live CAs; not Intune-proven; NSIS still primary full install; CI builds **unsigned** MSI only (sign step not live). |
|
||||
| **CI unsigned full MSI** | [`.github/workflows/windows-msi.yml`](../.github/workflows/windows-msi.yml) + DUAL-SHIP § CI artifact | `windows-latest`: install WiX v3 → `build-full-msi.ps1` → upload artifact `VeraePrintStamp-windows-msi-unsigned`. Future Authenticode secret **names** only in workflow comments / CI-SIGN-NOTARY. | No live `signtool`; no certs in git; not a production download channel. |
|
||||
| **CI unsigned full MSI** | [`.github/workflows/windows-msi.yml`](../.github/workflows/windows-msi.yml) + DUAL-SHIP § CI artifact | `windows-latest`: install WiX v3 → `build-full-msi.ps1` → upload artifact `VeraePrintStamp-windows-msi-unsigned`. Commented **signtool** + **Trusted Signing** steps (secret **names** only). | No live `signtool`; no certs in git; not a production download channel. |
|
||||
| **CI sign example** | [`.github/workflows/windows-msi-sign.yml.example`](../.github/workflows/windows-msi-sign.yml.example) | GHA/Forgejo **example**: build + Path A PFX/`signtool` + Path B Azure Trusted Signing; all gated `if: false`; secret **names** only. | Not a live workflow; no certs; does not claim SmartScreen. |
|
||||
| **Publish digests runbook** | [`docs/PUBLISH-DIGESTS.md`](PUBLISH-DIGESTS.md) | Operator order: build → sign → upload → `fill-manifest-sha256.ps1` → paste TBD. | Does not upload or enable live sign. |
|
||||
|
||||
### Issue 1 — what landed vs still open
|
||||
|
||||
| Done (advanced) | Still left (Issue 1 commercial) |
|
||||
|-----------------|----------------------------------|
|
||||
| `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) + [`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 |
|
||||
| `build-full-msi.ps1` StageFull + ProductVersion + exit codes | **Enable** live CI Authenticode: wire org secrets + flip `windows-msi-sign.yml.example` / uncomment `windows-msi.yml` sign steps |
|
||||
| `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** / Trusted Signing still **not** running; 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 |
|
||||
| [`DUAL-SHIP.md`](../platform/windows/wix/DUAL-SHIP.md) NSIS+MSI + CI artifact + **thin-NSIS exit checklist** | Site primary download still NSIS; thin/demote NSIS only after exit checklist A–D |
|
||||
| Site product string → **0.3.6**; manifest header notes HelperVersion vs last-published 0.3.2 | Do **not** claim Authenticode / notary / dual-ship / thin-NSIS complete |
|
||||
| [`.github/workflows/windows-msi.yml`](../.github/workflows/windows-msi.yml) unsigned MSI on `windows-latest` + artifact upload | Wire org secrets and enable sign step; publish signed assets to site |
|
||||
| [`.github/workflows/windows-msi.yml`](../.github/workflows/windows-msi.yml) unsigned MSI + **commented** signtool / Trusted Signing (secret names) | Org secrets + enable sign; publish signed assets to site |
|
||||
| [`windows-msi-sign.yml.example`](../.github/workflows/windows-msi-sign.yml.example) full sign sketch (`if: false`) | Promote example → live workflow when certs exist |
|
||||
| [`ci-notarize.yml.example`](../platform/macos/menubar/ci-notarize.yml.example) menubar GHA/Forgejo sketch (unsigned default; secret names) | Promote example → live workflow; automate `build-menubar-app.sh` sign when `SIGNING_ENV` set |
|
||||
| **0.3.6 Windows NSIS+MSI placeholder rows** ([`DOWNLOADS-MANIFEST.txt`](../site/printit.verae.ai/DOWNLOADS-MANIFEST.txt) + [`DOWNLOADS-MANIFEST.md`](../site/printit.verae.ai/DOWNLOADS-MANIFEST.md); sha256 **TBD**) | Build/upload signed 0.3.6 blobs; replace TBD with real digests |
|
||||
| [`fill-manifest-sha256.ps1`](../platform/windows/scripts/fill-manifest-sha256.ps1) local hash → DOWNLOADS-MANIFEST rows (linked from DUAL-SHIP + manifest.md; **no upload**) | Operator still must upload first, then paste digests over TBD |
|
||||
| [`fill-manifest-sha256.ps1`](../platform/windows/scripts/fill-manifest-sha256.ps1) + [`PUBLISH-DIGESTS.md`](PUBLISH-DIGESTS.md) (hash → paste; **no upload**) | Operator still must upload first, then paste digests over TBD |
|
||||
| **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 |
|
||||
|
|
@ -103,7 +106,7 @@ NSIS product already covers much of the *install experience* (files under Progra
|
|||
| **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) |
|
||||
| **Per-user vs per-machine story** | Tray is user-level today; MSI is typically machine | Machine full MSI + optional per-user tray MSI (both authored; product decision open) |
|
||||
| **Version drift in build scripts** | **Largely fixed** — HelperVersion path on build-windows, build-full-msi, build-pkg, build-installer, NSIS fallbacks **0.3.6**; site index product string **0.3.6** | Keep smoke failing on new hard-coded older defaults; `DOWNLOADS-MANIFEST` still checksums **last-published 0.3.2** blobs until release upload |
|
||||
| **CI job that builds + signs MSI** | Reproducible release | **Unsigned build live** (`.github/workflows/windows-msi.yml`); signtool / Trusted Signing still **not live** (secret names only) |
|
||||
| **CI job that builds + signs MSI** | Reproducible release | **Unsigned build live** (`windows-msi.yml`); sign **example** + commented Path A/B (`windows-msi-sign.yml.example`); signtool / Trusted Signing still **not live** |
|
||||
| **MSIX / Print Support App** (later) | Modern print path in original spec | Explicitly **deferred** unless enterprise asks; not blocking first MSI |
|
||||
|
||||
### Windows backlog order (suggested)
|
||||
|
|
@ -112,9 +115,9 @@ 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`; local dry-run [`sign-dry-run.ps1`](../platform/windows/scripts/sign-dry-run.ps1)).
|
||||
5. Authenticode on binaries + MSI (**not shipped** — enable [`windows-msi-sign.yml.example`](../.github/workflows/windows-msi-sign.yml.example) / commented steps in `windows-msi.yml`; secret names in `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.
|
||||
7. Publish 0.3.6 artifacts; refresh site `DOWNLOADS-MANIFEST` per [`PUBLISH-DIGESTS.md`](PUBLISH-DIGESTS.md) + `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).
|
||||
|
||||
---
|
||||
|
|
@ -189,7 +192,8 @@ NSIS product already covers much of the *install experience* (files under Progra
|
|||
| 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` |
|
||||
| Menubar notary dry-run | **Yes** ([`notary-dry-run.sh`](../platform/macos/menubar/notary-dry-run.sh)) | **Scaffold** — print recipe / tip; submit gated `NOTARY_SUBMIT=1` only |
|
||||
| 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**) |
|
||||
| Windows Authenticode pipeline | **Draft** (`wix/CI-SIGN-NOTARY.md`) + **dry-run** [`sign-dry-run.ps1`](../platform/windows/scripts/sign-dry-run.ps1) + commented steps in [`windows-msi.yml`](../.github/workflows/windows-msi.yml) + [`windows-msi-sign.yml.example`](../.github/workflows/windows-msi-sign.yml.example) | **No** — no live signtool job (**do not claim shipped**) |
|
||||
| Publish digests runbook | **Yes** ([`PUBLISH-DIGESTS.md`](PUBLISH-DIGESTS.md) + `fill-manifest-sha256.ps1`) | **Docs/tool** — operator still uploads + pastes TBD |
|
||||
| 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 |
|
||||
|
|
@ -207,6 +211,8 @@ 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**)
|
||||
- [`.github/workflows/windows-msi-sign.yml.example`](../.github/workflows/windows-msi-sign.yml.example) — GHA sign example (signtool + Trusted Signing; `if: false`; secret names only)
|
||||
- [`docs/PUBLISH-DIGESTS.md`](PUBLISH-DIGESTS.md) — fill DOWNLOADS-MANIFEST after sign via `fill-manifest-sha256.ps1`
|
||||
- [`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
|
||||
|
|
|
|||
87
docs/PUBLISH-DIGESTS.md
Normal file
87
docs/PUBLISH-DIGESTS.md
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
# Publish digests — DOWNLOADS-MANIFEST after sign
|
||||
|
||||
Operator runbook for filling [`site/printit.verae.ai/DOWNLOADS-MANIFEST.txt`](../site/printit.verae.ai/DOWNLOADS-MANIFEST.txt) and the human table in [`DOWNLOADS-MANIFEST.md`](../site/printit.verae.ai/DOWNLOADS-MANIFEST.md) **after** Windows (and other) release blobs are built, signed when required, and uploaded to the CDN.
|
||||
|
||||
**Helper:** [`platform/windows/scripts/fill-manifest-sha256.ps1`](../platform/windows/scripts/fill-manifest-sha256.ps1)
|
||||
**Does not:** upload, sign, rewrite site files, or touch CI secrets.
|
||||
**Related:** [DUAL-SHIP.md](../platform/windows/wix/DUAL-SHIP.md) · [CI-SIGN-NOTARY.md](../platform/windows/wix/CI-SIGN-NOTARY.md) · [COMMERCIAL-INSTALLER-BACKLOG.md](COMMERCIAL-INSTALLER-BACKLOG.md) · [`.github/workflows/windows-msi-sign.yml.example`](../.github/workflows/windows-msi-sign.yml.example)
|
||||
|
||||
---
|
||||
|
||||
## Order (fail closed)
|
||||
|
||||
1. **Build** — NSIS `*-setup.exe` and/or full MSI via `build-full-msi.ps1` (CI unsigned: `windows-msi.yml`).
|
||||
2. **Sign** — Authenticode on PE + MSI when shipping Windows trust (live CI still left; local dry-run: `sign-dry-run.ps1`; example workflow: `windows-msi-sign.yml.example`).
|
||||
3. **Upload** — put the **same bytes** that customers will download on the CDN / release channel under the reserved leaf names (e.g. `VeraePrintStamp-windows-amd64-0.3.6.msi`).
|
||||
4. **Hash** — run `fill-manifest-sha256.ps1` on the uploaded (or pre-upload local copy that matches CDN) files.
|
||||
5. **Paste** — replace `TBD` rows in `DOWNLOADS-MANIFEST.txt` and the sha256 column in `DOWNLOADS-MANIFEST.md`.
|
||||
6. **Verify** — re-download from the public URL and confirm `Get-FileHash` / `shasum -a 256` matches the pasted line.
|
||||
|
||||
Do **not** paste digests for unsigned-only CI smoke MSIs into production manifest rows. Do **not** fill TBD before upload (mirror checks break).
|
||||
|
||||
---
|
||||
|
||||
## fill-manifest-sha256.ps1
|
||||
|
||||
```powershell
|
||||
# From repo root (Windows). Paths = the signed blobs you just uploaded (or identical local copies).
|
||||
powershell -NoProfile -ExecutionPolicy Bypass -File platform\windows\scripts\fill-manifest-sha256.ps1 `
|
||||
-Path .\dist\VeraePrintStamp-windows-amd64-0.3.6-setup.exe,
|
||||
.\platform\windows\wix\out\VeraePrintStamp-windows-amd64-0.3.6.msi `
|
||||
-Format both
|
||||
```
|
||||
|
||||
| Flag | Effect |
|
||||
|------|--------|
|
||||
| `-Path` | One or more MSI / setup.exe / bare helper EXE paths |
|
||||
| `-Format txt` | SHA256SUMS lines only (default) — paste into `DOWNLOADS-MANIFEST.txt` |
|
||||
| `-Format md` | Markdown table rows for `DOWNLOADS-MANIFEST.md` |
|
||||
| `-Format both` | txt, blank line, then md |
|
||||
| `-AsName <leaf>` | Override leaf name when local build name ≠ reserved CDN filename (single path) |
|
||||
|
||||
Output shape (txt):
|
||||
|
||||
```text
|
||||
<lowercase-hex-sha256> <leaf-name>
|
||||
```
|
||||
|
||||
Two spaces between digest and name (same as existing `.txt` / `shasum -a 256`).
|
||||
|
||||
POSIX equivalent if the script is unavailable:
|
||||
|
||||
```bash
|
||||
shasum -a 256 VeraePrintStamp-windows-amd64-0.3.6-setup.exe \
|
||||
VeraePrintStamp-windows-amd64-0.3.6.msi
|
||||
# lowercase hex; paste over TBD
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 0.3.6 Windows dual-ship placeholders
|
||||
|
||||
Reserved rows (still **TBD** until signed publish):
|
||||
|
||||
| Leaf | Channel |
|
||||
|------|---------|
|
||||
| `VeraePrintStamp-windows-amd64-0.3.6-setup.exe` | NSIS primary (until thin-NSIS exit A–D) |
|
||||
| `VeraePrintStamp-windows-arm64-0.3.6-setup.exe` | NSIS arm64 |
|
||||
| `VeraePrintStamp-windows-amd64-0.3.6.msi` | Full MSI dual-ship |
|
||||
| `VeraePrintStamp-windows-arm64-0.3.6.msi` | Full MSI arm64 |
|
||||
| `verae-print-windows-amd64-0.3.6.exe` | Optional bare helper |
|
||||
|
||||
After a successful hash run, replace each matching `TBD <leaf>` line in `.txt` and the **TBD** cell in `.md`. Leave other series (live 0.3.2 / 0.1.2) unchanged unless those blobs are re-uploaded.
|
||||
|
||||
---
|
||||
|
||||
## What this does **not** claim
|
||||
|
||||
- Authenticode or SmartScreen green (needs live sign + clean-VM proof).
|
||||
- MSI as site primary download (thin-NSIS checklist still open).
|
||||
- Automatic site deploy from CI (no publish job rewrites manifests).
|
||||
- macOS notary staple (separate: `NOTARIZE-CHECKLIST.md` / `ci-notarize.yml.example`).
|
||||
|
||||
---
|
||||
|
||||
## License
|
||||
|
||||
UNLICENSED © Verae — operator docs only.
|
||||
|
|
@ -3,6 +3,7 @@ package pdfcert
|
|||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
|
|
@ -13,6 +14,9 @@ import (
|
|||
qrcode "github.com/skip2/go-qrcode"
|
||||
)
|
||||
|
||||
// brandTitleFamily is the gofpdf family name when a brand TTF/OTF registers.
|
||||
const brandTitleFamily = "VeraeBrand"
|
||||
|
||||
// Brand tokens (CERT-01 / BrandingBook): plum, gold, ink, muted, ok.
|
||||
const (
|
||||
plumR, plumG, plumB = 83, 27, 89 // #531B59
|
||||
|
|
@ -28,13 +32,10 @@ const (
|
|||
|
||||
// Minimal fallback when Node/jsPDF is unavailable.
|
||||
//
|
||||
// Fonts: PDF core / library faces only (Times title, Helvetica body).
|
||||
// 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).
|
||||
// Fonts: PDF core faces by default (Times title, Helvetica body).
|
||||
// When EmbedBrandFonts is on and BrandFontsDir has a .ttf/.otf, attempt
|
||||
// gofpdf AddUTF8FontFromBytes for the title; on failure log and keep Times.
|
||||
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)
|
||||
|
|
@ -43,6 +44,8 @@ func buildFallbackGofpdf(in Input, docName, created, scope, merkle, lookup strin
|
|||
pageW, pageH := pdf.GetPageSize()
|
||||
margin := 48.0
|
||||
|
||||
titleFamily := tryRegisterBrandTitleFont(pdf, in)
|
||||
|
||||
// CERT-01 double frame: plum outer, gold inner
|
||||
pdf.SetDrawColor(plumR, plumG, plumB)
|
||||
pdf.SetLineWidth(1.5)
|
||||
|
|
@ -64,9 +67,13 @@ func buildFallbackGofpdf(in Input, docName, created, scope, merkle, lookup strin
|
|||
pdf.SetXY(margin, 92)
|
||||
pdf.CellFormat(pageW-margin*2, 11, "TIMESTAMP CERTIFICATE", "", 0, "C", false, 0, "")
|
||||
|
||||
// Title — Times core serif stand-in for CERT-01 Fraunces (no embed unless present)
|
||||
// Title — brand face when registered; else Times core serif stand-in
|
||||
pdf.SetTextColor(inkR, inkG, inkB)
|
||||
pdf.SetFont("Times", "B", 22)
|
||||
if titleFamily != "" {
|
||||
pdf.SetFont(titleFamily, "B", 22)
|
||||
} else {
|
||||
pdf.SetFont("Times", "B", 22)
|
||||
}
|
||||
pdf.SetXY(margin, 106)
|
||||
pdf.CellFormat(pageW-margin*2, 24, "Verae Timestamp Certificate", "", 0, "C", false, 0, "")
|
||||
|
||||
|
|
@ -206,6 +213,43 @@ func buildFallbackGofpdf(in Input, docName, created, scope, merkle, lookup strin
|
|||
return buf.Bytes(), nil
|
||||
}
|
||||
|
||||
// tryRegisterBrandTitleFont attempts gofpdf UTF-8 embed of a licensed brand
|
||||
// serif when EmbedBrandFonts is on and BrandFontsDir has .ttf/.otf.
|
||||
// Returns brandTitleFamily on success, "" on skip/failure (caller keeps Times).
|
||||
// Never errors the PDF build — registration failure is a logged no-op.
|
||||
func tryRegisterBrandTitleFont(pdf *gofpdf.Fpdf, in Input) string {
|
||||
if pdf == nil || !EmbedBrandFontsEnabled(in) {
|
||||
return ""
|
||||
}
|
||||
dir := BrandFontsDir()
|
||||
face := findBrandFaceFile(dir)
|
||||
if face == "" {
|
||||
log.Printf("pdfcert: EmbedBrandFonts on but no .ttf/.otf in %q — keeping Times", dir)
|
||||
return ""
|
||||
}
|
||||
ext := strings.ToLower(filepath.Ext(face))
|
||||
// gofpdf AddUTF8FontFromBytes parses TrueType tables; OTF-CFF often fails.
|
||||
b, err := os.ReadFile(face)
|
||||
if err != nil {
|
||||
log.Printf("pdfcert: brand font read %s: %v — keeping Times", face, err)
|
||||
return ""
|
||||
}
|
||||
pdf.AddUTF8FontFromBytes(brandTitleFamily, "B", b)
|
||||
if err := pdf.Error(); err != nil {
|
||||
log.Printf("pdfcert: gofpdf could not register brand font %s (%s): %v — keeping Times", face, ext, err)
|
||||
pdf.ClearError()
|
||||
return ""
|
||||
}
|
||||
// Regular style alias so SetFont(family, "", …) would also work if needed.
|
||||
pdf.AddUTF8FontFromBytes(brandTitleFamily, "", b)
|
||||
if err := pdf.Error(); err != nil {
|
||||
pdf.ClearError()
|
||||
// Bold alone is enough for the title.
|
||||
}
|
||||
log.Printf("pdfcert: registered brand title font from %s", face)
|
||||
return brandTitleFamily
|
||||
}
|
||||
|
||||
// finalBannerTitle uses statuslang.Label("final", block) — BrandingBook chip text.
|
||||
// Certificates are issued only for evidence states (IsEvidence); hold ≠ evidence.
|
||||
func finalBannerTitle(merkle string) string {
|
||||
|
|
|
|||
|
|
@ -35,17 +35,17 @@ 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 requests PDF embed of a licensed brand serif when a
|
||||
// .ttf/.otf is present under BrandFontsDir(). Default false: core
|
||||
// Times/Helvetica only. Also via VERAE_EMBED_BRAND_FONTS=1|true|yes.
|
||||
// gofpdf path attempts AddUTF8FontFromBytes; failure is a logged no-op.
|
||||
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).
|
||||
// VERAE_EMBED_BRAND_FONTS is 1/true/yes. When enabled, gofpdf may register a
|
||||
// .ttf/.otf from BrandFontsDir(); failure is a logged no-op (core fonts).
|
||||
func EmbedBrandFontsEnabled(in Input) bool {
|
||||
if in.EmbedBrandFonts {
|
||||
return true
|
||||
|
|
@ -59,6 +59,81 @@ func EmbedBrandFontsEnabled(in Input) bool {
|
|||
}
|
||||
}
|
||||
|
||||
// BrandFontsDir is the directory scanned for .ttf/.otf when EmbedBrandFonts is on.
|
||||
// Override with VERAE_BRAND_FONTS_DIR; else site/printit.verae.ai/fonts from the
|
||||
// module tree. Empty when neither is available (embed no-ops with a log).
|
||||
func BrandFontsDir() string {
|
||||
if d := strings.TrimSpace(os.Getenv("VERAE_BRAND_FONTS_DIR")); d != "" {
|
||||
if st, err := os.Stat(d); err == nil && st.IsDir() {
|
||||
return d
|
||||
}
|
||||
}
|
||||
var candidates []string
|
||||
if wd, err := os.Getwd(); err == nil {
|
||||
dir := wd
|
||||
for i := 0; i < 8; i++ {
|
||||
candidates = append(candidates, filepath.Join(dir, "site", "printit.verae.ai", "fonts"))
|
||||
if st, se := os.Stat(filepath.Join(dir, "go.mod")); se == nil && !st.IsDir() {
|
||||
break
|
||||
}
|
||||
parent := filepath.Dir(dir)
|
||||
if parent == dir {
|
||||
break
|
||||
}
|
||||
dir = parent
|
||||
}
|
||||
}
|
||||
candidates = append(candidates, filepath.Join("site", "printit.verae.ai", "fonts"))
|
||||
for _, c := range candidates {
|
||||
if st, err := os.Stat(c); err == nil && st.IsDir() {
|
||||
abs, _ := filepath.Abs(c)
|
||||
return abs
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// findBrandFaceFile returns the preferred .ttf/.otf under dir (Fraunces-named
|
||||
// first, then any ttf, then otf). Empty when none present.
|
||||
func findBrandFaceFile(dir string) string {
|
||||
if dir == "" {
|
||||
return ""
|
||||
}
|
||||
ents, err := os.ReadDir(dir)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
var frauncesTTF, anyTTF, frauncesOTF, anyOTF string
|
||||
for _, e := range ents {
|
||||
if e.IsDir() {
|
||||
continue
|
||||
}
|
||||
name := e.Name()
|
||||
low := strings.ToLower(name)
|
||||
path := filepath.Join(dir, name)
|
||||
switch {
|
||||
case strings.HasSuffix(low, ".ttf"):
|
||||
if strings.Contains(low, "fraunces") && frauncesTTF == "" {
|
||||
frauncesTTF = path
|
||||
} else if anyTTF == "" {
|
||||
anyTTF = path
|
||||
}
|
||||
case strings.HasSuffix(low, ".otf"):
|
||||
if strings.Contains(low, "fraunces") && frauncesOTF == "" {
|
||||
frauncesOTF = path
|
||||
} else if anyOTF == "" {
|
||||
anyOTF = path
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, p := range []string{frauncesTTF, anyTTF, frauncesOTF, anyOTF} {
|
||||
if p != "" {
|
||||
return p
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// ContentMultihash returns sha2-256 multihash hex (code 0x12, len 0x20 → f1220…)
|
||||
// when sha256 is a 64-char hex digest; otherwise "" (caller omits the field).
|
||||
func ContentMultihash(sha256 string) string {
|
||||
|
|
@ -126,14 +201,11 @@ func LookupURL(sha256, receiptID string) string {
|
|||
// evidence copy). Prefers platform/registration-pdf/build.mjs when Node is
|
||||
// available; otherwise gofpdf fallback.
|
||||
//
|
||||
// Typography: both paths use PDF standard fonts (Times title, Helvetica body).
|
||||
// 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.
|
||||
// Typography: default PDF standard fonts (Times title, Helvetica body).
|
||||
// EmbedBrandFonts defaults false (env VERAE_EMBED_BRAND_FONTS off). When on,
|
||||
// gofpdf fallback may register a licensed .ttf/.otf from BrandFontsDir();
|
||||
// failure is a logged no-op. Node path stays core faces (OPEN-ISSUES #7).
|
||||
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.
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package pdfcert
|
|||
import (
|
||||
"bytes"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
|
@ -282,9 +283,8 @@ func TestEmbedBrandFontsDefaultFalse(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
// TestBuildUsesSystemFontsNotFraunces: issued PDF uses core Times/Helvetica;
|
||||
// EmbedBrandFonts defaults false — do not claim Fraunces embed until a licensed
|
||||
// face is explicitly added and the option is on (I7).
|
||||
// TestBuildUsesSystemFontsNotFraunces: issued PDF uses core Times/Helvetica when
|
||||
// EmbedBrandFonts defaults false (OPEN-ISSUES #7).
|
||||
func TestBuildUsesSystemFontsNotFraunces(t *testing.T) {
|
||||
t.Setenv("VERAE_EMBED_BRAND_FONTS", "")
|
||||
sha := "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
|
||||
|
|
@ -353,3 +353,117 @@ func TestBuildUsesSystemFontsNotFraunces(t *testing.T) {
|
|||
t.Fatal("build.mjs should keep times/helvetica standard faces")
|
||||
}
|
||||
}
|
||||
|
||||
// TestEmbedBrandFontsTrueNoFaceKeepsTimes: opt-in with empty fonts dir is a no-op.
|
||||
func TestEmbedBrandFontsTrueNoFaceKeepsTimes(t *testing.T) {
|
||||
empty := t.TempDir()
|
||||
t.Setenv("VERAE_BRAND_FONTS_DIR", empty)
|
||||
t.Setenv("VERAE_EMBED_BRAND_FONTS", "1")
|
||||
sha := "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
|
||||
in := Input{
|
||||
DocumentName: "NoFace.pdf",
|
||||
SHA256: sha,
|
||||
ReceiptID: "QmNoFace",
|
||||
CreatedAt: "2026-10-03T12:00:00Z",
|
||||
Merkle: "99",
|
||||
EmbedBrandFonts: true,
|
||||
}
|
||||
if !EmbedBrandFontsEnabled(in) {
|
||||
t.Fatal("expected embed request enabled")
|
||||
}
|
||||
if findBrandFaceFile(empty) != "" {
|
||||
t.Fatal("empty dir must yield no face")
|
||||
}
|
||||
pdf, err := buildFallbackGofpdf(in, "NoFace.pdf", in.CreatedAt, "GLOBAL", in.Merkle, LookupURL(sha, in.ReceiptID))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
s := string(pdf)
|
||||
if !strings.Contains(s, "Times-Bold") && !strings.Contains(s, "/BaseFont /Times") && !strings.Contains(s, "Times-Roman") {
|
||||
t.Fatal("empty brand fonts dir must keep Times title")
|
||||
}
|
||||
if strings.Contains(s, brandTitleFamily) {
|
||||
t.Fatal("must not name VeraeBrand family without a registered face")
|
||||
}
|
||||
}
|
||||
|
||||
// TestTryRegisterBrandFontWithTTF: when EmbedBrandFonts is on and a .ttf is
|
||||
// present, gofpdf registers it for the title (or no-ops cleanly if unsupported).
|
||||
func TestTryRegisterBrandFontWithTTF(t *testing.T) {
|
||||
ttf := findTestTTF(t)
|
||||
if ttf == "" {
|
||||
t.Skip("no sample .ttf available for gofpdf register smoke")
|
||||
}
|
||||
dir := t.TempDir()
|
||||
dst := filepath.Join(dir, "sample.ttf")
|
||||
b, err := os.ReadFile(ttf)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(dst, b, 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Setenv("VERAE_BRAND_FONTS_DIR", dir)
|
||||
t.Setenv("VERAE_EMBED_BRAND_FONTS", "")
|
||||
sha := "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
|
||||
in := Input{
|
||||
DocumentName: "BrandFace.pdf",
|
||||
SHA256: sha,
|
||||
ReceiptID: "QmBrand",
|
||||
CreatedAt: "2026-10-03T12:00:00Z",
|
||||
Merkle: "99",
|
||||
EmbedBrandFonts: true,
|
||||
}
|
||||
if got := findBrandFaceFile(dir); got != dst {
|
||||
t.Fatalf("findBrandFaceFile: got %q want %q", got, dst)
|
||||
}
|
||||
pdf, err := buildFallbackGofpdf(in, "BrandFace.pdf", in.CreatedAt, "GLOBAL", in.Merkle, LookupURL(sha, in.ReceiptID))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(pdf) < 1000 || !bytes.HasPrefix(pdf, []byte("%PDF")) {
|
||||
t.Fatal("expected valid PDF after brand-font attempt")
|
||||
}
|
||||
// Default false still skips even when a ttf sits on disk.
|
||||
t.Setenv("VERAE_EMBED_BRAND_FONTS", "")
|
||||
off := Input{SHA256: sha, ReceiptID: "QmOff", Merkle: "1"}
|
||||
if EmbedBrandFontsEnabled(off) {
|
||||
t.Fatal("default must stay false with ttf present")
|
||||
}
|
||||
core, err := buildFallbackGofpdf(off, "Off.pdf", "2026-10-03T12:00:00Z", "GLOBAL", "1", LookupURL(sha, "QmOff"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
cs := string(core)
|
||||
if !strings.Contains(cs, "Times-Bold") && !strings.Contains(cs, "/BaseFont /Times") {
|
||||
t.Fatal("EmbedBrandFonts false must keep Times even if ttf exists")
|
||||
}
|
||||
}
|
||||
|
||||
func findTestTTF(t *testing.T) string {
|
||||
t.Helper()
|
||||
candidates := []string{
|
||||
"/Library/Fonts/Arial Unicode.ttf",
|
||||
"/System/Library/Fonts/Supplemental/Arial.ttf",
|
||||
"/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf",
|
||||
"/usr/share/fonts/truetype/liberation/LiberationSerif-Bold.ttf",
|
||||
}
|
||||
// gofpdf module sample faces (dev machines with module cache).
|
||||
if gopath := os.Getenv("GOPATH"); gopath != "" {
|
||||
candidates = append(candidates,
|
||||
filepath.Join(gopath, "pkg/mod/github.com/phpdave11/gofpdf@v1.4.2/font/DejaVuSansCondensed.ttf"),
|
||||
)
|
||||
}
|
||||
home, _ := os.UserHomeDir()
|
||||
if home != "" {
|
||||
candidates = append(candidates,
|
||||
filepath.Join(home, "go/pkg/mod/github.com/phpdave11/gofpdf@v1.4.2/font/DejaVuSansCondensed.ttf"),
|
||||
)
|
||||
}
|
||||
for _, c := range candidates {
|
||||
if st, err := os.Stat(c); err == nil && st.Size() > 1000 {
|
||||
return c
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
|
|
|||
|
|
@ -335,7 +335,8 @@ end {
|
|||
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 " Live CI: enable windows-msi-sign.yml.example / commented steps in windows-msi.yml (still left)." -ForegroundColor DarkGray
|
||||
Write-Host " After sign+upload: docs/PUBLISH-DIGESTS.md + fill-manifest-sha256.ps1" -ForegroundColor DarkGray
|
||||
Write-Host " This dry-run does not claim Authenticode shipped." -ForegroundColor DarkGray
|
||||
|
||||
exit 0
|
||||
|
|
|
|||
|
|
@ -1,10 +1,12 @@
|
|||
# CI — Authenticode (Windows) + notarytool (macOS)
|
||||
|
||||
**Status:** unsigned MSI CI is live (`.github/workflows/windows-msi.yml`); **sign/notary steps remain draft**. No secrets in git.
|
||||
**Status:** unsigned MSI CI is live (`.github/workflows/windows-msi.yml`); **sign/notary steps remain draft** (commented secret **names** + example workflow). No secrets in git.
|
||||
**Audience:** release engineers wiring GitHub Actions or Forgejo Actions.
|
||||
**Related:** [`Product-Full.wxs`](Product-Full.wxs), [`SILENT_PROPS.md`](SILENT_PROPS.md),
|
||||
[`DUAL-SHIP.md`](DUAL-SHIP.md) § CI artifact,
|
||||
[`.github/workflows/windows-msi.yml`](../../../.github/workflows/windows-msi.yml),
|
||||
[`.github/workflows/windows-msi.yml`](../../../.github/workflows/windows-msi.yml) — unsigned build + commented signtool / Trusted Signing,
|
||||
[`.github/workflows/windows-msi-sign.yml.example`](../../../.github/workflows/windows-msi-sign.yml.example) — full sign sketch (`if: false`; secret **names** only),
|
||||
[`docs/PUBLISH-DIGESTS.md`](../../../docs/PUBLISH-DIGESTS.md) — fill DOWNLOADS-MANIFEST after sign via `fill-manifest-sha256.ps1`,
|
||||
[`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/notary-dry-run.sh`](../../macos/menubar/notary-dry-run.sh) — local notarytool recipe / tip (**no secrets**; submit only if `NOTARY_SUBMIT=1`),
|
||||
|
|
@ -62,77 +64,10 @@ Match names already used in operator `signing.env` where possible; map in the wo
|
|||
|
||||
## GitHub Actions sketch (Windows MSI + sign)
|
||||
|
||||
```yaml
|
||||
# .github/workflows/windows-msi-sign.yml — DRAFT, not enabled
|
||||
name: windows-msi-sign
|
||||
on:
|
||||
workflow_dispatch:
|
||||
# push:
|
||||
# tags: ["printstamp-v*"]
|
||||
**Canonical example file:** [`.github/workflows/windows-msi-sign.yml.example`](../../../.github/workflows/windows-msi-sign.yml.example)
|
||||
(Path A: PFX + `signtool`; Path B: Azure Trusted Signing; job `if: false`; secret **names** only.)
|
||||
|
||||
jobs:
|
||||
msi:
|
||||
runs-on: windows-latest
|
||||
permissions:
|
||||
contents: read
|
||||
# id-token: write # if using Azure OIDC Trusted Signing
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version-file: apps/PrintToVeraeTimeStamp/go.mod
|
||||
# adjust path if workflow lives inside the app repo root
|
||||
|
||||
- name: Build Windows binaries
|
||||
shell: pwsh
|
||||
working-directory: apps/PrintToVeraeTimeStamp # or repo root
|
||||
run: |
|
||||
# Prefer existing build-windows.ps1 / cross-build; stage under platform/windows/wix/StageFull
|
||||
# go build → StageFull\bin\verae-print.exe + verae-printhelper.exe
|
||||
# copy scripts + LICENSE + README-WINDOWS.txt into StageFull
|
||||
Write-Host "TODO: wire build-windows.ps1 staging for Product-Full.wxs"
|
||||
|
||||
- name: Install WiX toolset
|
||||
shell: pwsh
|
||||
run: |
|
||||
# choco install wixtoolset --version 3.14.x OR use preinstalled image
|
||||
Write-Host "TODO: ensure candle/light on PATH"
|
||||
|
||||
- name: candle + light Product-Full
|
||||
shell: pwsh
|
||||
run: |
|
||||
# candle -dProductVersion=… -dStageDir=… Product-Full.wxs
|
||||
# light → out\VeraePrintStamp-windows-amd64-<ver>.msi
|
||||
# Expect failure until StageFull complete — see Product-Full.wxs header
|
||||
Write-Host "TODO: invoke candle/light"
|
||||
|
||||
- name: Import code-signing cert
|
||||
shell: pwsh
|
||||
env:
|
||||
WINDOWS_CERT_PFX_BASE64: ${{ secrets.WINDOWS_CERT_PFX_BASE64 }}
|
||||
WINDOWS_CERT_PFX_PASSWORD: ${{ secrets.WINDOWS_CERT_PFX_PASSWORD }}
|
||||
run: |
|
||||
# $bytes = [Convert]::FromBase64String($env:WINDOWS_CERT_PFX_BASE64)
|
||||
# [IO.File]::WriteAllBytes("$env:RUNNER_TEMP\codesign.pfx", $bytes)
|
||||
# Import-PfxCertificate -FilePath ... -CertStoreLocation Cert:\CurrentUser\My -Password ...
|
||||
Write-Host "TODO: import PFX; never Upload-Artifact the PFX"
|
||||
|
||||
- name: signtool Authenticode
|
||||
shell: cmd
|
||||
env:
|
||||
WINDOWS_SIGN_TIMESTAMP_URL: ${{ vars.WINDOWS_SIGN_TIMESTAMP_URL }}
|
||||
run: |
|
||||
REM signtool sign /fd SHA256 /tr %WINDOWS_SIGN_TIMESTAMP_URL% /td SHA256 /a ^
|
||||
REM path\to\verae-print.exe path\to\verae-printhelper.exe path\to\*.msi
|
||||
echo TODO: signtool after binaries + MSI exist
|
||||
|
||||
- name: Checksums
|
||||
shell: pwsh
|
||||
run: |
|
||||
# Get-FileHash … SHA256 | Out-File SHA256SUMS
|
||||
Write-Host "TODO: publish MSI + SHA256SUMS as release assets"
|
||||
```
|
||||
Also mirrored as commented blocks in [`.github/workflows/windows-msi.yml`](../../../.github/workflows/windows-msi.yml) under the unsigned job.
|
||||
|
||||
**Forgejo Actions:** same steps; replace `${{ secrets.* }}` with Forgejo secret syntax (`secrets.WINDOWS_CERT_PFX_BASE64`, etc.) and `runs-on` with your Windows label. Keep secret **names** identical so docs stay portable.
|
||||
|
||||
|
|
@ -192,7 +127,7 @@ GHA/Forgejo copy-paste sketch (unsigned default, notary job gated off): [`ci-not
|
|||
2. **Sign** Windows PE + MSI with `signtool` (or Trusted Signing).
|
||||
3. **Notarize** macOS zip/pkg with `notarytool --wait`.
|
||||
4. **Staple** macOS artifacts; `stapler validate`.
|
||||
5. **Checksum** + upload release assets; update `site/printit.verae.ai/DOWNLOADS-MANIFEST.txt` out-of-band (`platform/windows/scripts/fill-manifest-sha256.ps1` prints rows only — does not upload) or via a docs job.
|
||||
5. **Checksum** + upload release assets; update `site/printit.verae.ai/DOWNLOADS-MANIFEST.txt` out-of-band — see [`docs/PUBLISH-DIGESTS.md`](../../../docs/PUBLISH-DIGESTS.md) (`fill-manifest-sha256.ps1` prints rows only — does not upload).
|
||||
6. Never upload raw PFX / `.p8` as workflow artifacts.
|
||||
|
||||
---
|
||||
|
|
@ -207,11 +142,12 @@ GHA/Forgejo copy-paste sketch (unsigned default, notary job gated off): [`ci-not
|
|||
|
||||
**Not done:**
|
||||
|
||||
- Enable Authenticode `signtool` / Trusted Signing in that workflow (commented; secret **names** only).
|
||||
- **Enable** Authenticode `signtool` / Trusted Signing (example + comments exist; live job still off).
|
||||
- Provide real certs, Team IDs, or TSA URLs.
|
||||
- Replace NSIS as primary Windows download.
|
||||
- Automate menubar notarization inside `build-menubar-app.sh` (still checklist + backlog).
|
||||
- Claim SmartScreen reputation or Gatekeeper green without clean-VM proof.
|
||||
- Paste real 0.3.6 digests into DOWNLOADS-MANIFEST (runbook: PUBLISH-DIGESTS.md).
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ Human-readable companion to [`DOWNLOADS-MANIFEST.txt`](DOWNLOADS-MANIFEST.txt) (
|
|||
**Last-published CDN blobs:** mostly **0.3.2** (Windows amd64 NSIS / macOS / Linux) or **0.1.2** (Windows arm64, `.deb`, bare `.zip`).
|
||||
**Rule:** do not replace a checksum line until the matching artifact is built, signed (when required), uploaded, and the digest verified. `TBD` is intentional — not a shipping claim.
|
||||
|
||||
Related: [DUAL-SHIP.md](../../platform/windows/wix/DUAL-SHIP.md) · [VM-DUAL-SHIP-CHECKLIST.md](../../platform/windows/wix/VM-DUAL-SHIP-CHECKLIST.md) · [COMMERCIAL-INSTALLER-BACKLOG.md](../../docs/COMMERCIAL-INSTALLER-BACKLOG.md) · [`fill-manifest-sha256.ps1`](../../platform/windows/scripts/fill-manifest-sha256.ps1) (local hash → manifest row; **does not upload**).
|
||||
Related: [DUAL-SHIP.md](../../platform/windows/wix/DUAL-SHIP.md) · [VM-DUAL-SHIP-CHECKLIST.md](../../platform/windows/wix/VM-DUAL-SHIP-CHECKLIST.md) · [COMMERCIAL-INSTALLER-BACKLOG.md](../../docs/COMMERCIAL-INSTALLER-BACKLOG.md) · [PUBLISH-DIGESTS.md](../../docs/PUBLISH-DIGESTS.md) (after sign → hash → paste) · [`fill-manifest-sha256.ps1`](../../platform/windows/scripts/fill-manifest-sha256.ps1) (local hash → manifest row; **does not upload**).
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -46,7 +46,8 @@ TBD VeraePrintStamp-windows-arm64-0.3.6.msi
|
|||
# Optional companion EXE (parity with prior 0.3.x bare helper uploads):
|
||||
TBD verae-print-windows-amd64-0.3.6.exe
|
||||
# See also DOWNLOADS-MANIFEST.md (human table) and VM-DUAL-SHIP-CHECKLIST.md.
|
||||
# Fill digests: platform/windows/scripts/fill-manifest-sha256.ps1 -Path <msi|setup.exe> (prints rows; does not upload).
|
||||
# Fill digests after sign+upload: docs/PUBLISH-DIGESTS.md
|
||||
# platform/windows/scripts/fill-manifest-sha256.ps1 -Path <msi|setup.exe> (prints rows; does not upload).
|
||||
56cb72d8d65c5d2e1292b0a05f235ded0a267e66530229a53980887fc5de1b72 verae-print-windows-amd64-0.3.1.exe
|
||||
aa2e7f1843ccbd4d466c66e92a5460f0e3d499e6af0dcba2f93cbd49bc2012e5 verae-print-windows-amd64-0.3.2.exe
|
||||
079482b38ae3b49b268863209e309bb35072a3ec136f076f0ec5062d67a7c26e verae-print_0.1.2_amd64.deb
|
||||
|
|
|
|||
|
|
@ -18,7 +18,8 @@ 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.
|
||||
`EmbedBrandFonts` is **off** unless explicitly enabled (`Input` or `VERAE_EMBED_BRAND_FONTS`).
|
||||
When on, gofpdf may register a licensed `.ttf`/`.otf` from `VERAE_BRAND_FONTS_DIR` or this directory; unsupported faces log and keep Times (not provided here).
|
||||
|
||||
## See also
|
||||
|
||||
|
|
|
|||
|
|
@ -74,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`) | Core Times/Helvetica; **`EmbedBrandFonts` default false** (env `VERAE_EMBED_BRAND_FONTS`) | N/A until opt-in + licensed face |
|
||||
| Issued PDF (`internal/pdfcert`) | Core Times/Helvetica; **`EmbedBrandFonts` default false** (env `VERAE_EMBED_BRAND_FONTS`); opt-in gofpdf `AddUTF8FontFromBytes` for `.ttf`/`.otf` under `VERAE_BRAND_FONTS_DIR` or this dir | Times on fail / no face (logged no-op) |
|
||||
|
||||
Design source of truth: `verae-2026-design/ui-designs/src/html/CERT-01.html`
|
||||
(tokens `--font-serif: 'Fraunces'`; design-kit `src/fonts/` ships Cantarell +
|
||||
|
|
@ -113,7 +113,7 @@ operator uncomments a pinned-commit block after license review.
|
|||
|
||||
| Item | Owner / later |
|
||||
|------|----------------|
|
||||
| PDF CERT Fraunces embed | **EmbedBrandFonts default false** (option + `VERAE_EMBED_BRAND_FONTS`); Times/Helvetica until enabled + licensed face |
|
||||
| PDF CERT Fraunces embed | **EmbedBrandFonts default false**; when on, gofpdf registers first `.ttf`/`.otf` in `VERAE_BRAND_FONTS_DIR` or this dir (fail → log + Times). Node `build.mjs` stays core faces |
|
||||
| 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) |
|
||||
|
|
@ -128,8 +128,9 @@ operator uncomments a pinned-commit block after license review.
|
|||
| Local-first `@font-face` else CDN | **Done** — `certificate.html` probes `fonts/*.woff2` |
|
||||
| Drop-in docs (this README) | **Done** |
|
||||
| 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 |
|
||||
| `EmbedBrandFonts` default false | **Done** — opt-in env/option |
|
||||
| PDF gofpdf ttf/otf register attempt | **Done** — opt-in; no-op + log when missing/unsupported |
|
||||
| Pixel match / OFL face in git | **Not started** — operator drop-in; do not claim full match |
|
||||
|
||||
## Contributing
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue