Skip to content

Sign the macOS thv binary with a Developer ID certificate - #6156

Draft
eleftherias wants to merge 2 commits into
mainfrom
sign-macos-thv-binary
Draft

Sign the macOS thv binary with a Developer ID certificate#6156
eleftherias wants to merge 2 commits into
mainfrom
sign-macos-thv-binary

Conversation

@eleftherias

Copy link
Copy Markdown
Member

Summary

The macOS thv binary ships with only the ad-hoc signature Go's linker emits, so it has no Developer ID and therefore no Team ID:

$ codesign -dv --verbose=4 /opt/homebrew/bin/thv
Signature=adhoc
TeamIdentifier=not set

Organizations that enforce binary allowlisting (Santa and similar) trust binaries by code-signing identity. Without a Team ID, their remaining options are approving each release by CDHash — which changes every build — or allowing execution from a user-writable install path. Both are operationally fragile, so thv tends to get blocked outright in managed macOS environments.

ToolHive Studio is already signed with Developer ID Application: Stacklok, Inc (XMNPBXU9PV), and a Developer ID certificate signs any binary from the team rather than being scoped to one app. This wires the same credential into the CLI release.

  • Sign on the existing Linux runner. GoReleaser's notarize.macos pipe embeds anchore/quill as a library, so no macOS runner is added and release minutes don't change.
  • Sign-only, deliberately. Omitting the inner notarize sub-block keeps Apple's notary service off the release critical path. A Developer ID signature is all Team ID allowlisting needs; notarization can land separately.
  • Inert until credentials exist. A MACOS_SIGN_ENABLED flag gates the pipe. Without the secrets, releases produce ad-hoc binaries exactly as they do today and emit a warning annotation instead of failing.

Fixes #5862

Type of change

  • Bug fix
  • New feature
  • Refactoring (no behavior change)
  • Dependency update
  • Documentation
  • Other (describe): Release pipeline / packaging change

Test plan

  • Unit tests (task test)
  • E2E tests (task test-e2e)
  • Linting (task lint-fix)
  • Manual testing (describe below)

No Go code changes, so task test and task lint-fix don't apply. Verified with GoReleaser directly:

  1. Config validitygoreleaser check against the real .goreleaser.yaml reports valid. The single brews deprecation it flags reproduces identically on HEAD, so it predates this PR.
  2. No-secrets path succeeds — snapshot release with MACOS_SIGN_ENABLED=false and the certificate variables entirely unset: release succeeded. The enabled gate short-circuits before {{ .Env.APPLE_CERTIFICATE }} is templated, so there's no missing-key error. This also proves the templated string false parses as boolean false; had it been truthy, the same run would have failed on an empty certificate.
  3. Signing path runs, and a bad certificate fails loudly — snapshot release with the gate on and a throwaway self-signed .p12: GoReleaser reached sign & notarize macOS binaries and quill failed with unable to find remaining chain certificates. Base64 decode, PKCS#12 parse, and the password all worked; it got as far as chain validation. Confirms an expired or wrong certificate aborts the release rather than silently shipping ad-hoc binaries.
  4. id: toolhive is a no-op — with this repo's project_name and binary, the resolved build ID from artifacts.json is toolhive both with and without the explicit id, at the same path dist/toolhive_darwin_arm64_v8.0/thv. The implicit default was already toolhive.
  5. Both YAML files parse, and the new run block passes bash -n.

Items 2–4 ran against minimal projects replicating the relevant config, because a full six-target ToolHive build exceeded the sandbox timeout. Item 1 ran against the real config.

Does this introduce a user-facing change?

Not on merge — no behavior changes until the signing secrets are configured. Once they are, macOS thv binaries in release archives carry a Developer ID Application: Stacklok, Inc signature and a Team ID that can be allowlisted by code-signing identity. Homebrew installs benefit automatically, since the signature is embedded in the Mach-O and survives archive extraction.

Special notes for reviewers

Two secrets are required before this does anything: APPLE_CERTIFICATE (base64 .p12) and APPLE_CERTIFICATE_PASSWORD. Studio's KEYCHAIN_PASSWORD is not needed — quill reads the .p12 directly with no keychain involved. Since both repos need the same values, org-level secrets are preferable to duplicating them. Recommend validating on a prerelease tag before a stable release.

Why a MACOS_SIGN_ENABLED flag instead of isEnvSet "APPLE_CERTIFICATE". GoReleaser's own docs recommend the isEnvSet idiom, so this looks like a deviation worth fixing — please don't. An absent GitHub secret still sets the environment variable, to the empty string, so isEnvSet would report "configured" and fail the release with an empty certificate. The flag is computed from whether the secret is non-empty.

The section named notarize performs signing. That's GoReleaser's name for the whole feature; sign-only is a mode of it, reached by omitting the inner notarize block. Confusing, but correct.

Notarization is a deliberate follow-up, needing three more secrets (APPLE_API_KEY, APPLE_KEY_ID, APPLE_ISSUER_ID). Worth knowing it wouldn't fully close the Gatekeeper story either way: a notarization ticket cannot be stapled to a bare Mach-O binary, only to bundles and installers, so Gatekeeper would still rely on an online check for a standalone CLI. Full offline Gatekeeper behavior would require shipping a signed .pkg, which is materially larger work.

No verification job. An earlier revision added a macos-latest job asserting codesign --verify on the published artifacts. Dropped: a bad certificate already fails the release (see test 3), a missing secret already warns, the check could only run post-publish so it would alert rather than gate, and it was the only thing forcing a macOS runner into an otherwise all-Linux pipeline. The first release should be spot-checked by hand instead.

Also worth noting: there's currently no CI validation of .goreleaser.yaml at all, so a config typo only surfaces at release time. A goreleaser check step on PRs would be cheap insurance — happy to open that separately.

Generated with Claude Code

The macOS thv binary ships with only the ad-hoc signature Go's linker
emits, so it carries no Team ID. Organizations that enforce binary
allowlisting (Santa and similar) trust binaries by code-signing
identity, leaving them to approve each release by CDHash or allow
execution from a user-writable path. Both are fragile, so thv tends to
get blocked in managed macOS environments.

ToolHive Studio already signs with Developer ID Application: Stacklok,
Inc, and the same certificate covers any binary from the team. Wire it
into the CLI release.

GoReleaser embeds anchore/quill, so signing happens on the existing
Linux runner rather than requiring a macOS one. This is sign-only:
omitting the notarize sub-block keeps Apple's notary service off the
release critical path, and a Developer ID signature is all that Team ID
allowlisting needs. Notarization can follow separately.

Signing stays inert until the secrets are configured. Releases without
them keep producing ad-hoc binaries and log a warning rather than
failing, so this is safe to merge ahead of the credentials.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@github-actions github-actions Bot added the size/XS Extra small PR: < 100 lines changed label Jul 31, 2026
@codecov

codecov Bot commented Jul 31, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 72.62%. Comparing base (b7e824c) to head (c354357).

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #6156      +/-   ##
==========================================
+ Coverage   72.56%   72.62%   +0.06%     
==========================================
  Files         736      736              
  Lines       76331    76331              
==========================================
+ Hits        55391    55438      +47     
+ Misses      17019    16956      -63     
- Partials     3921     3937      +16     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@github-actions github-actions Bot added size/XS Extra small PR: < 100 lines changed and removed size/XS Extra small PR: < 100 lines changed labels Jul 31, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/XS Extra small PR: < 100 lines changed

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Sign and notarize the macOS ToolHive CLI (thv) binary

1 participant