From ff17ebce667df12323a0676033a923f54806d800 Mon Sep 17 00:00:00 2001 From: Kirill Klimuk Date: Fri, 25 Sep 2026 10:56:54 -0700 Subject: [PATCH] fix: sign and verify macOS release binaries before upload --- .github/workflows/ci.yml | 17 +++++++++--- .github/workflows/release.yml | 15 ++++++++++- CLAUDE.md | 1 + CONTRIBUTING.md | 16 +++++++++-- README.md | 2 ++ SECURITY.md | 5 ++++ scripts/CLAUDE.md | 1 + scripts/macos-entitlements.plist | 18 +++++++++++++ scripts/sign-macos-binary.sh | 14 ++++++++++ skills/docx-cli/references/troubleshooting.md | 27 +++++++++++++++++++ 10 files changed, 110 insertions(+), 6 deletions(-) create mode 100644 scripts/macos-entitlements.plist create mode 100644 scripts/sign-macos-binary.sh diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6db563d..64ad9d2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -71,8 +71,12 @@ jobs: - run: DOCX_LO_ALL=1 bun run test:integration # full fixture sweep in CI build-binary: - name: Smoke-build compiled binary - runs-on: ubuntu-latest + name: Smoke-build compiled binary (${{ matrix.os }}) + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, macos-15-intel, macos-latest] steps: - uses: actions/checkout@v6 - uses: oven-sh/setup-bun@v2 @@ -83,5 +87,12 @@ jobs: restore-keys: bun-${{ runner.os }}- - run: bun install --frozen-lockfile - run: bun run build:binary + - name: Sign and verify macOS binary + if: runner.os == 'macOS' + run: sh scripts/sign-macos-binary.sh ./dist/docx - name: Verify binary runs - run: ./dist/docx --version + run: | + ./dist/docx --version + ./dist/docx --help > /dev/null + ./dist/docx read tests/fixtures/markdown-import.docx > /dev/null + ./dist/docx validate tests/fixtures/markdown-import.docx diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 68af6e4..f15e63a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -18,7 +18,7 @@ jobs: include: - { os: ubuntu-latest, target: bun-linux-x64, name: docx-linux-x64 } - { os: ubuntu-latest, target: bun-linux-arm64, name: docx-linux-arm64 } - - { os: macos-latest, target: bun-darwin-x64, name: docx-darwin-x64 } + - { os: macos-15-intel, target: bun-darwin-x64, name: docx-darwin-x64 } - { os: macos-latest, target: bun-darwin-arm64, name: docx-darwin-arm64 } - { os: windows-latest, target: bun-windows-x64, name: docx-windows-x64.exe } steps: @@ -32,6 +32,17 @@ jobs: - run: bun install --frozen-lockfile - name: Compile run: bun build --compile --target=${{ matrix.target }} --outfile=${{ matrix.name }} src/index.ts + # Sign after compilation (and any future binary rewriting), before upload. + - name: Sign and verify macOS artifact + if: runner.os == 'macOS' + run: sh scripts/sign-macos-binary.sh "./${{ matrix.name }}" + - name: Smoke-test macOS artifact natively + if: runner.os == 'macOS' + run: | + ./${{ matrix.name }} --version + ./${{ matrix.name }} --help > /dev/null + ./${{ matrix.name }} read tests/fixtures/markdown-import.docx > /dev/null + ./${{ matrix.name }} validate tests/fixtures/markdown-import.docx - name: Smoke-test render on Linux artifact if: matrix.os == 'ubuntu-latest' && matrix.target == 'bun-linux-x64' run: | @@ -67,6 +78,8 @@ jobs: # the logic, published two ways. See the installer invariant in CLAUDE.md. run: cp skills/docx-cli/scripts/install.sh ./artifacts/ - name: Generate SHA256SUMS for the release assets + # Hash the final signed bytes downloaded from the build jobs. Never modify + # a binary after this step. # Published as a release asset so install.sh / bootstrap.sh can verify the # downloaded binary's integrity, and so install.sh itself can be verified # before it is run (supply-chain hardening). diff --git a/CLAUDE.md b/CLAUDE.md index 796aef2..29ef110 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -94,6 +94,7 @@ When a feature's natural brute-force path already works for weak agents (verifie - `bun run build` → `dist/index.js` — bundled JS that npm publishes (runs under Bun). **Required**: path aliases (`@core/*`) and JSX runtime resolution don't work when consumed from `node_modules`; the bundle pre-resolves everything. Never ship raw `src/`. - `bun run build:binary` → `dist/docx` — standalone executable for GitHub Releases. +- macOS CI/release builds run `sh scripts/sign-macos-binary.sh BINARY` after all binary modifications, then native version/help/read/validate smoke checks. Signing and strict verification must precede upload; release checksums cover the final signed bytes. ## Docs layout diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c1dca77..ac44ac5 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -9,7 +9,8 @@ bun run check # biome + knip + tsc bun run test:unit # core + cli tests (fast) bun run test:integration # LibreOffice round-trip (needs `soffice` on PATH) bun test # everything -bun run build # produce dist/docx via bun build --compile +bun run build # produce dist/index.js (npm bundle) +bun run build:binary # produce dist/docx via bun build --compile ``` ### LibreOffice (for integration tests) @@ -102,6 +103,17 @@ GitHub Actions (`.github/workflows/ci.yml`) runs four jobs on push to `main` and | `check` | `biome check . && knip-bun && tsc --noEmit` | | `unit-tests` | `bun run test:unit` (core + cli, fast) | | `integration-tests` | Installs LibreOffice, runs `bun run test:integration` | -| `build-binary` | Smoke-builds via `bun build --compile` and runs `--version` | +| `build-binary` | Builds on Linux and native Intel/ARM macOS; signs/verifies macOS binaries; runs version/help, read, and schema-validation smoke checks | `.github/workflows/release.yml` triggers on `v*` tags, matrix-builds the five binaries, and uploads them to a GitHub Release via [`softprops/action-gh-release`](https://github.com/softprops/action-gh-release). + +macOS release binaries are ad-hoc signed with `sh scripts/sign-macos-binary.sh BINARY` +after compilation and any other binary modifications. The script supplies Bun's +JavaScriptCore entitlements and requires strict signature verification to pass. +Both macOS release jobs run the artifact natively before upload. `SHA256SUMS` is +then generated from the downloaded, signed artifacts; never modify binaries after +signing or checksum generation. The same signing and smoke checks run on PRs. +To reproduce locally on macOS, run `bun run build:binary`, then +`sh scripts/sign-macos-binary.sh ./dist/docx` and the `build-binary` smoke commands +in `.github/workflows/ci.yml`. These checks cover the runner's macOS version; +compatibility with a newer macOS release needs a separate run on that release. diff --git a/README.md b/README.md index ee1f393..dc0d155 100644 --- a/README.md +++ b/README.md @@ -55,6 +55,8 @@ shasum -a 256 -c SHA256SUMS --ignore-missing # or: sha256sum -c … sh install.sh ``` +macOS artifacts built by the current release workflow are ad-hoc signed and signature-verified before checksumming; they are not Developer ID-signed or notarized. + Honors `PREFIX` (default `$HOME/.local/bin`) and `VERSION` (default `latest`). Pre-built for linux/x64, linux/arm64, darwin/x64, darwin/arm64, windows/x64. Once installed, **`docx upgrade`** updates a standalone binary — it replaces the binary wherever it already lives (`PREFIX` does not apply), running the same installer embedded in the binary at build time rather than fetched, so the download stays pinned to a release tag and SHA-256-verified. `--to v0.23.0` pins a version, `--dry-run` reports what would change. On an npm/bun install it tells you to use the package manager instead. diff --git a/SECURITY.md b/SECURITY.md index 86c7d83..0fad8c2 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -24,6 +24,11 @@ On a standalone-binary install, run **`docx upgrade`** — it replaces the binar Every release publishes a `SHA256SUMS` manifest alongside the prebuilt binaries. There is one installer — `install.sh` — published two ways: as a release asset, and inside the skill folder where `scripts/bootstrap.sh` delegates to it. It downloads only release **assets** (never a script), tries `sha256sum`, `shasum`, then `openssl`, and **verifies the binary against the manifest**, aborting on a mismatch or a missing manifest. +macOS binaries built by the current release workflow are ad-hoc signed and +strictly signature-verified before upload; `SHA256SUMS` covers those final signed +bytes. Ad-hoc signing provides executable integrity, not Developer ID identity +or Apple notarization. + The one behavioral difference is a parameter, not a second code path — what to do on a machine with no checksum tool at all: | | Invoked by | No checksum tool available | diff --git a/scripts/CLAUDE.md b/scripts/CLAUDE.md index 94b3fde..2209c08 100644 --- a/scripts/CLAUDE.md +++ b/scripts/CLAUDE.md @@ -3,6 +3,7 @@ - [move.ts](move.ts) — move a TS file and auto-update imports (TS LanguageService-driven). - [escape-check.ts](escape-check.ts), [fxp-smoke.ts](fxp-smoke.ts), [jsx-smoke.tsx](jsx-smoke.tsx) — ad-hoc probes for XML escaping, fast-xml-parser behavior, and the JSX runtime. - [word-redlines.sh](word-redlines.sh) — AppleScript oracle that drives Microsoft Word to produce ground-truth track-changes XML (referenced from [src/core/track-changes/replace.tsx](../src/core/track-changes/replace.tsx) and [src/core/notes/CLAUDE.md](../src/core/notes/CLAUDE.md)). +- [sign-macos-binary.sh](sign-macos-binary.sh) — final ad-hoc signing and strict verification for macOS CI/release executables; [macos-entitlements.plist](macos-entitlements.plist) carries Bun's documented JavaScriptCore permissions. Run only after all binary modifications. - `data/` — sample `.docx` files used for one-off inspection (not test fixtures; those live in [tests/fixtures/](../tests/fixtures/)). Fixture builders moved to [tests/fixtures/setup/](../tests/fixtures/setup/CLAUDE.md). diff --git a/scripts/macos-entitlements.plist b/scripts/macos-entitlements.plist new file mode 100644 index 0000000..ee2d9fe --- /dev/null +++ b/scripts/macos-entitlements.plist @@ -0,0 +1,18 @@ + + + + + + com.apple.security.cs.allow-jit + + com.apple.security.cs.allow-unsigned-executable-memory + + com.apple.security.cs.disable-executable-page-protection + + com.apple.security.cs.allow-dyld-environment-variables + + com.apple.security.cs.disable-library-validation + + + diff --git a/scripts/sign-macos-binary.sh b/scripts/sign-macos-binary.sh new file mode 100644 index 0000000..193dbb6 --- /dev/null +++ b/scripts/sign-macos-binary.sh @@ -0,0 +1,14 @@ +#!/bin/sh +# Run after all binary modifications; upload/hash only the resulting signed bytes. +set -eu + +if [ "$#" -ne 1 ]; then + echo "Usage: sh scripts/sign-macos-binary.sh BINARY" >&2 + exit 2 +fi + +script_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) +# Explicit entitlements retain Bun's JavaScriptCore JIT permissions, even when +# the compiler's original signature is absent or invalid. No signing key needed. +codesign --force --sign - --entitlements "$script_dir/macos-entitlements.plist" "$1" +codesign --verify --strict --verbose=2 "$1" diff --git a/skills/docx-cli/references/troubleshooting.md b/skills/docx-cli/references/troubleshooting.md index 5facf72..1901ab3 100644 --- a/skills/docx-cli/references/troubleshooting.md +++ b/skills/docx-cli/references/troubleshooting.md @@ -22,6 +22,33 @@ It installs to `~/.local/bin/docx` by default. Make sure that directory is on your `PATH` (`export PATH="$HOME/.local/bin:$PATH"`). Set `PREFIX=/usr/local/bin` before the install to choose another location. +## macOS kills the standalone binary at startup + +An invalid code signature can cause a startup kill (reported for v0.25.0 on +macOS 27). Verify the actual standalone executable, not a version-manager shim: + +```sh +# For a mise-managed installation: +codesign --verify --strict --verbose=2 "$(mise which docx)" +# For a standalone binary directly on PATH (not a shim): +codesign --verify --strict --verbose=2 "$(command -v docx)" +``` + +With Bun installed, the package is a fallback. Run its explicit path so an +older mise installation or shim earlier on PATH cannot shadow it: + +```sh +bun add -g bun-docx +"$(bun pm bin -g)/docx" --version +``` + +Use that explicit path for subsequent commands, or adjust PATH and check +`command -v docx` before using the bare command. A binary that cannot start +cannot run `docx upgrade`; reinstall externally when a fixed release is available. +The updated release workflow ad-hoc signs and verifies macOS binaries before +generating `SHA256SUMS`; existing release assets are unchanged. Ad-hoc signing +does not provide Developer ID identity or Apple notarization. + ## `docx render` fails or hangs `render` is the **only** command that needs an external app. Everything else