Skip to content

ci(release): sign Windows binaries and MSI via SignPath (#7207) - #7356

Merged
lidge-jun merged 3 commits into
devfrom
ci/windows-code-signing
Oct 19, 2026
Merged

lidge-jun merged 3 commits into
devfrom
ci/windows-code-signing

Conversation

@lidge-jun

@lidge-jun lidge-jun commented Oct 19, 2026 •

Copy link
Copy Markdown
Owner

Summary

Wires Windows Authenticode code signing into the release workflow through SignPath Foundation for #7207, where Kaspersky and VirusTotal flag the unsigned ocx.exe. Signing is layered onto the release so it can never block or fail one. That matters because the release-signing certificate has not been issued yet and the SIGNPATH_SIGNING_POLICY_SLUG variable is still test-signing.

What a run does now:

Configuration Dry run (dry-run: true) Real release
No SIGNPATH_API_TOKEN (forks, unconfigured copies) unsigned, unchanged unsigned, unchanged, ::notice::
Token, but a variable is missing or the policy slug is malformed unsigned, ::warning:: unsigned, ::warning::
test-signing (today) signed with the test cert, verified unsigned, exactly as today, ::notice::; a test certificate never ships
release-signing signed and verified as trusted signed and verified as trusted
SignPath outage, timeout, rejection or failed verification unsigned, ::warning:: unsigned, ::warning::

Standalone (package-standalone, bun-windows-x64). After the smoke test, ocx.exe is uploaded alone (it sits at the zip root, matching windows-standalone-zip), submitted with the official signpath/github-action-submit-signing-request pinned to v3.0 f6d04783…, verified, and swapped in. The zip and its .sha256 are then built from the signed binary.

Desktop MSI (package-desktop, x86_64-pc-windows-msvc). After tauri build and before collect-release-assets, the .msi is uploaded and signed with windows-msi-zip, which deep-signs the app exe, the bundled ocx.exe, and the MSI itself. Authenticode rewrites the MSI, so the Tauri updater signature is regenerated over the signed bytes with tauri signer sign and the same TAURI_SIGNING_PRIVATE_KEY. verify-release then checks that .sig against the final MSI and writes latest.json from it, exactly as before.

Swap safety (scripts/ci/apply-signed-windows-artifact.ps1). Everything that can fail runs on a staged copy first: the SignPath outcome, the returned file, Authenticode verification, the signed ocx.exe --version, and the updater re-signature. Only then are the MSI and its .sig swapped in together, and an error mid-swap restores both originals. A failure therefore never leaves a signed MSI next to a stale .sig, which would otherwise fail verify-release.

Verification (scripts/ci/verify-authenticode.ps1). Built-in tools only: Get-AuthenticodeSignature, msiexec /a to expand the MSI into an administrative image (nothing is installed), and the hosted image's Windows SDK signtool. It checks ocx.exe, the MSI, and every .exe inside the MSI, requiring ocx.exe and at least two executables. Under release-signing it requires Status Valid, a CN=SignPath Foundation signer, a timestamp, and signtool verify /pa /v. Under test-signing, where the self-signed root is untrusted on the runner, it requires every file to carry a hash-valid signature and all files to share one signer. A second signer would mean an already-signed third-party file got re-signed, which the Foundation policy forbids.

Non-publishing test path. A new .github/workflows/windows-signing-test.yml (Windows signing test) is dispatch-only and takes no inputs. It builds ocx.exe and the MSI on windows-latest, signs both with test-signing (hardcoded, so it never spends a release approval), verifies them, links the SignPath request in the job summary, and keeps the test-signed files as 7-day workflow artifacts. Unlike the release path, it goes red on any failure, because a green run is the evidence it exists to produce. It publishes, tags and attaches nothing.

Gating and permissions. The decision lives in scripts/ci/windows-signing-plan.sh so the gates are testable. Both packaging jobs gain actions: read (the SignPath action reads the job and downloads the artifact) next to the existing contents: read; nothing else is broadened. Every job on the signing path runs on GitHub-hosted runners, as SignPath OSS requires. The Windows legs' job timeouts rise to 90 and 105 minutes because a release-signing request can wait up to 3600 s for manual approval; the other legs keep 25 and 45.

Docs. New page Windows Code Signing at docs-site/src/content/docs/reference/code-signing.md. It covers the Code signing policy with the SignPath-required wording (Free code signing provided by SignPath.io, certificate by SignPath Foundation; committers, reviewers and approvers: lidge-jun; the privacy statement), how users verify a signature, what gets signed, the policy table above, and how to flip to release-signing. The README and all 7 locale READMEs gain a "Code signing policy" section and an updated Windows row (the i18n manifest hash is bumped), and the desktop and installation guides link the page.

Decisions made autonomously (HOTL)

  • Fail-open, not fail-closed. The original brief asked a real release with signing configured to fail closed. The maintainer steer overrides that: signing must never block a release. Every signing step is continue-on-error and degrades to unsigned with a warning. Once release-signing is live and proven, a fail-closed switch (for example a SIGNPATH_REQUIRED variable) can be added as a follow-up.
  • Dry runs use the configured policy. After the flip to release-signing, a dry run opens two signing requests that each need approval (or ship unsigned after the wait). The test workflow is the cheap path for exercising signing.
  • Third-party .node keyring addons stay unsigned. SignPath Foundation forbids signing third-party binaries with its certificate, but allows including them unsigned. No artifact-configuration change is proposed for them.

Maintainer actions

  1. Each release-signing request needs manual approval in app.signpath.io. After the flip, each release opens two requests (standalone and MSI). The workflow waits up to an hour per request, then ships unsigned with a warning.
  2. Flip to production once the certificate is issued: set the repo variable SIGNPATH_SIGNING_POLICY_SLUG to release-signing. No code change is needed.
  3. SignPath Foundation conditions to check before the flip (outside this repo, not changed here):
    • The terms require "metadata attributes set and enforced using file metadata restrictions" (product name and version). The current artifact configurations have no metadata restrictions, and the bun-compiled ocx.exe carries no Windows version resource. Proposed XML once that metadata exists (a separate change would pass bun's --windows-title/--windows-version flags):
      <zip-file>
        <pe-file path="ocx.exe" product-name="OpenCodex" product-version="${version}">
          <authenticode-sign/>
        </pe-file>
      </zip-file>
    • The Foundation asks for a "Code signing policy" link on the download/release pages. Release notes are generated from commits, so this PR does not change the release-notes renderer right before the release. Add a footer link to the policy page before the first signed release.
    • Please confirm the privacy wording on the docs page. It quotes the required sentence and also discloses the GitHub and npm update checks.

Refs #7207. Do not close #7207 yet: it stays open until release-signing is live and a signed release has shipped.

Verification

  • bun test tests/ci-workflows/release-windows-signing.test.ts: 21 pass. This covers the decision matrix (bash), workflow gates and step order, the unsigned-artifact names not matching standalone-*/desktop-*, hosted runners, and permissions. It also runs swap behavior under PowerShell 7.6 with a fake verifier and fake bunx: a verified swap regenerates .sig; with no key the stale .sig is removed; a failed request, failed verification or failed re-signature each keep the original MSI and .sig with a warning; -FailOnError goes red; both .ps1 files parse. The pwsh cases skip where pwsh is absent; the hosted Ubuntu image ships it.
  • bun test tests/ci-workflows (Bun 1.4.3): 1575 pass, 8 fail in one parallel run. The 8 (release-helper, skill-ocx chapter) are 2–5 s subprocess tests that timed out under host contention; rerun in isolation they pass 40/40 and 8/8. One pre-existing contract test now selects the release upload by artifact name, since the standalone job gained a second upload-artifact step.
  • tests/test-layout*.test.ts: 27 pass. bun run typecheck, bun run privacy:scan and bun run structure:check pass, as does actionlint on release.yml and windows-signing-test.yml. docs-site bun run build passes (585 pages, internal link check).
  • Full bun run test was not run locally: concurrent worktrees made the parallel run time out on unrelated subprocess tests (above). Full-suite coverage is left to CI.
  • Branch is up to date with origin/dev (merged at 0a995db, fix(sidecar): map provider input overflow to context_length_exceeded #7352; layout maps auto-merged, focused tests 122/122 green after the merge). No force-push.
  • Not run: Windows execution of the PowerShell scripts against real signatures, and a SignPath round-trip. The Windows signing test workflow can only be dispatched once it exists on the default branch (main), so the test-signed run happens after this lands and is promoted. Independent review: gpt-6.1-sol and gemini-4-argon both reported no findings (read-only reviews of the full diff).

Checklist

  • Scope stays focused and avoids unrelated cleanup.
  • Docs or release notes were updated when needed.
  • Security-sensitive changes were reviewed for secrets, auth, and unsafe defaults.

Submit the standalone ocx.exe and the desktop MSI to SignPath Foundation from the
release workflow, verify the returned Authenticode signatures on the Windows runner,
and regenerate the Tauri updater signature over the signed MSI before the assets are
renamed, checksummed and verified.

Signing never blocks a release: missing or partial configuration, SignPath outages,
timeouts, rejections and verification failures all keep the unsigned file with a
warning, and a test-signing policy never signs a real release. A dispatch-only
Windows signing test workflow exercises test-signing end to end without publishing.
@lidge-jun
lidge-jun requested a review from Ingwannu as a code owner October 19, 2026 06:40
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 19, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ⚠️ Failed 2026-10-19T06:46:06.226838Z 2efb5b0 PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@coderabbitai

coderabbitai Bot commented Oct 19, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

Note

Currently processing new changes in this PR. This may take a few minutes, please wait...

⚙️ Run configuration
  • Configuration used: Repository: lidge-jun/opencodex/.coderabbit.yaml
  • Review profile: ASSERTIVE
  • Plan: Advanced
  • Run ID: ff18e4be-cde0-4851-8abc-1171b1b4f4c0
📥 Commits

Reviewing files that changed from the base of the PR and between 0a995db and 2249c1a.

📒 Files selected for processing: 22
  • .github/workflows/release.yml
  • .github/workflows/windows-signing-test.yml
  • README.md
  • docs-site/astro.config.mjs
  • docs-site/src/content/docs/getting-started/installation.md
  • docs-site/src/content/docs/guides/desktop-app.md
  • docs-site/src/content/docs/reference/code-signing.md
  • readme/README.fr.md
  • readme/README.ja.md
  • readme/README.ko.md
  • readme/README.ru.md
  • readme/README.tr.md
  • readme/README.zh-CN.md
  • readme/README.zh-TW.md
  • readme/i18n-manifest.json
  • scripts/ci/apply-signed-windows-artifact.ps1
  • scripts/ci/verify-authenticode.ps1
  • scripts/ci/windows-signing-plan.sh
  • scripts/test-layout/layout.json
  • tests/ci-workflows/release-pipeline-contract.test.ts
  • tests/ci-workflows/release-windows-signing.test.ts
  • tests/fixtures/test-layout-expected.json
+----------------------------------------------------------------+
| KōdoUsagi Gundam is my Japanese cousin. He's a bit more mecha. |
+----------------------------------------------------------------+
  \
   \   (\__/)
       (•ㅅ•)
       /   づ
✨ Finishing Touches
📝 Generate docstrings
  • Commit to this branch
  • Create a new PR
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions github-actions Bot added the chore Maintenance, CI, tests, refactors, or build changes (not a user-facing bug or feature). label Oct 19, 2026
@github-actions

Copy link
Copy Markdown
Contributor

✅ Deterministic PR hygiene checks passed.

@lidge-jun
lidge-jun merged commit 421e4a6 into dev Oct 19, 2026
32 of 34 checks passed
@lidge-jun
lidge-jun deleted the ci/windows-code-signing branch October 19, 2026 06:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

chore Maintenance, CI, tests, refactors, or build changes (not a user-facing bug or feature).

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant