Skip to content

Verify bare binaries in macos_verify_code_signing - #782

Draft
mokagio wants to merge 8 commits into
trunkfrom
mokagio/macos-verify-bare-binaries
Draft

mokagio wants to merge 8 commits into
trunkfrom
mokagio/macos-verify-bare-binaries

Conversation

@mokagio

@mokagio mokagio commented Nov 4, 2026 •

Copy link
Copy Markdown
Contributor

What does it do?

Lets macos_verify_code_signing verify bare binaries, identified by having no file extension, so CLIs can stop hand-rolling this check.

@iangmaia asked why beeper/cli had its own lane instead of using the action; the answer was that the action only took .app and .dmg.

It also adds a verify_hardened_runtime option, off by default, for app bundles and bare binaries.
Notarization requires the hardened runtime, so verify_notarization already covers it implicitly, but a caller verifying before submission has to turn that off.
beeper/cli#64 still keeps its own check for this; once this lands it can drop it.

Gotchas

  • Gatekeeper is assessed with --type open --context context:primary-signature, not the --type execute used for app bundles.
    --type execute rejects every bare binary, notarized or not, with the code is valid but does not seem to be an app.
  • A bare binary can't be stapled, so there is no stapler check and the Gatekeeper result relies on an online lookup that can lag a fresh submission.
    Pass verify_notarization: false when verifying right after signing.
  • verify_hardened_runtime skips disk images instead of failing, so a list mixing an app with its .dmg still works.

How to test

The specs stub the shell commands, so I also ran the action on a notarized beeper arm64 binary from Buildkite:

Binary Result
As built, expected authority Passes (Notarized Developer ID)
Different expected authority Fails: not signed by the authority
Ad-hoc signed Fails: wrong authority, and rejected by Gatekeeper
Signature removed Fails: not signed at all
One byte changed Fails: signature not valid

verify_hardened_runtime, run directly against ad-hoc signed copies of /usr/bin/true, passes with --options runtime (flags=0x10002(adhoc,runtime)) and fails without it.

Re-signing with Developer ID without notarizing makes spctl itself report rejected, source=Unnotarized Developer ID.
I ran that command directly, not through the action.

Checklist before requesting a review

  • Run bundle exec rubocop to test for code style violations and recommendations.
  • Add Unit Tests (aka specs/*_spec.rb) if applicable.
  • Run bundle exec rspec to run the whole test suite and ensure all your tests pass. Only this action's spec ran locally; the full suite is left to CI.
  • Make sure you added an entry in the CHANGELOG.md file to describe your changes under the appropriate existing ### subsection of the existing ## Trunk section.

CLIs ship bare Mach-O binaries, so `beeper/cli` and the Day One CLI each hand-roll this verification because the action only accepted `.app` and `.dmg`.
Raised in beeper/cli#56 (comment).

---

Generated with the help of Claude Code (Opus 5.5), https://code.claude.com
`spctl --type execute` rejects every bare binary, notarized or not, with `the code is valid but does not seem to be an app`.
The stubbed specs could not catch this; running against a real binary did.

Checked on a notarized `beeper` binary from Buildkite: `--type open --context context:primary-signature` reports `Notarized Developer ID`, and rejects a Developer ID re-signed copy as `Unnotarized Developer ID`.

---

Generated with the help of Claude Code (Opus 5.5), https://code.claude.com
@mokagio mokagio self-assigned this Nov 4, 2026
@dangermattic

dangermattic commented Nov 4, 2026 •

Copy link
Copy Markdown
Collaborator
1 Message
📖 This PR is still a Draft: some checks will be skipped.

Generated by 🚫 Danger

---

Generated with the help of Claude Code (Opus 5.5), https://code.claude.com
Comment on lines +23 to +24
when ''
verify_binary(path: path, expected_authority: params[:expected_authority], verify_notarization: params[:verify_notarization])

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suppose this could backfire if someone generates a package without an extension, but that seems like a perverse case. And the spctl check may (haven't checked) fail because of the mismatch. Seems like an edge case not yet worth covering at this stage.

The method doc comment now covers only what affects the caller.
Why `spctl` runs with `--type open` is an implementation detail, so it sits inline.

---

Generated with the help of Claude Code (Opus 5.5), https://code.claude.com
A caller that verifies before notarizing passes `verify_notarization: false`, so nothing asserted the hardened runtime that notarization will later demand.
beeper/cli#64 kept its own check for exactly this.

The option defaults to `false` so existing callers are unaffected.
Disk images are skipped with a warning rather than failed, because a list mixing an app with its `.dmg` is a normal call.

---

Generated with the help of Claude Code (Opus 5.5), https://code.claude.com
---

Generated with the help of Claude Code (Opus 5.5), https://code.claude.com
mokagio added a commit to beeper/cli that referenced this pull request Nov 6, 2026
The `macos_verify_code_signing` action gained a `verify_hardened_runtime` option in wordpress-mobile/release-toolkit#782, so the lane no longer needs its own `codesign --display` check.

---

Generated with the help of Claude Code (Opus 5.5), https://code.claude.com
A correctly configured call that lists an app with its `.dmg` should not print a warning.
The docs now state outright that notarization implies the hardened runtime, which is why the option is for verifying before submission.

---

Generated with the help of Claude Code (Opus 5.5), https://code.claude.com
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants