Repository navigation
Conversation
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
--- Generated with the help of Claude Code (Opus 5.5), https://code.claude.com
| when '' | ||
| verify_binary(path: path, expected_authority: params[:expected_authority], verify_notarization: params[:verify_notarization]) |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
I can see actual cases where this would backfire in the opposite direction, i.e. a binary-only CLI with a name that make it looks like it has an extension. For example my-tool-v1.2.3 would be detected like having an extension .3…
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
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
| return if expected_authority.nil? && !verify_hardened_runtime | ||
|
|
||
| UI.user_error!("#{path} is not signed by '#{expected_authority}':\n#{details}") | ||
| details = sh('codesign', '--display', '--verbose=2', path) |
There was a problem hiding this comment.
Confirmed by my Claude, we should fix this.
| details = sh('codesign', '--display', '--verbose=2', path) | ||
| UI.user_error!("#{path} is not signed by '#{expected_authority}':\n#{details}") unless expected_authority.nil? || details.include?("Authority=#{expected_authority}") | ||
| # The flags list every option the code was signed with, e.g. `flags=0x10002(adhoc,runtime)`. | ||
| UI.user_error!("#{path} is not signed with the hardened runtime:\n#{details}") if verify_hardened_runtime && !details.match?(/flags=0x\h+\([^)]*\bruntime\b/) |
There was a problem hiding this comment.
Confirmed by my Claude, we should fix this
|
|
||
| if signed?(path) | ||
| verify_authority!(path: path, expected_authority: expected_authority) unless expected_authority.nil? | ||
| verify_signature_details!(path: path, expected_authority: expected_authority, verify_hardened_runtime: false) |
There was a problem hiding this comment.
What's the point of passing the verify_hardened_runtime parameter to the function if you're not gonna use its value (besides the conditional printing of the UI.message log on line 49) and instead always pass false here? 🤔
| details = sh('codesign', '--display', '--verbose=2', path) | ||
| UI.user_error!("#{path} is not signed by '#{expected_authority}':\n#{details}") unless expected_authority.nil? || details.include?("Authority=#{expected_authority}") | ||
| # The flags list every option the code was signed with, e.g. `flags=0x10002(adhoc,runtime)`. | ||
| UI.user_error!("#{path} is not signed with the hardened runtime:\n#{details}") if verify_hardened_runtime && !details.match?(/flags=0x\h+\([^)]*\bruntime\b/) |
There was a problem hiding this comment.
Confirmed by my Claude, we should fix this
| return if expected_authority.nil? && !verify_hardened_runtime | ||
|
|
||
| UI.user_error!("#{path} is not signed by '#{expected_authority}':\n#{details}") | ||
| details = sh('codesign', '--display', '--verbose=2', path) |
There was a problem hiding this comment.
Confirmed by my Claude, we should fix this.
| when '' | ||
| verify_binary(path: path, expected_authority: params[:expected_authority], verify_notarization: params[:verify_notarization]) |
There was a problem hiding this comment.
I can see actual cases where this would backfire in the opposite direction, i.e. a binary-only CLI with a name that make it looks like it has an extension. For example my-tool-v1.2.3 would be detected like having an extension .3…

What does it do?
Lets
macos_verify_code_signingverify bare binaries, identified by having no file extension, so CLIs can stop hand-rolling this check.@iangmaia asked why
beeper/clihad its own lane instead of using the action; the answer was that the action only took.appand.dmg.It also adds a
verify_hardened_runtimeoption, off by default, for app bundles and bare binaries.Notarization requires the hardened runtime, so
verify_notarizationalready covers it implicitly, but a caller verifying before submission has to turn that off.beeper/clistill keeps its own check for this; once this lands it can drop it.Gotchas
--type open --context context:primary-signature, not the--type executeused for app bundles.--type executerejects every bare binary, notarized or not, withthe code is valid but does not seem to be an app.staplercheck and the Gatekeeper result relies on an online lookup that can lag a fresh submission.Pass
verify_notarization: falsewhen verifying right after signing.verify_hardened_runtimeskips disk images instead of failing, so a list mixing an app with its.dmgstill works.How to test
The specs stub the shell commands, so I also ran the action on a notarized
beeperarm64 binary from Buildkite:Notarized Developer ID)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
spctlitself reportrejected,source=Unnotarized Developer ID.I ran that command directly, not through the action.
Checklist before requesting a review
bundle exec rubocopto test for code style violations and recommendations.specs/*_spec.rb) if applicable.bundle exec rspecto 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.CHANGELOG.mdfile to describe your changes under the appropriate existing###subsection of the existing## Trunksection.Note
Notice the missing
apps-infra-toolingreview request. More in pbArwn-89X-p2#comment-9038