feat: support signing and notarizing app bundles - #815
Open
TotallyGamerJet wants to merge 8 commits into
Open
Conversation
Adds a new quill/bundle package that can detect .app bundles, parse Info.plist, and seal bundle contents into a CodeResources plist using the same resource rules Apple's codesign tool embeds. Wires bundle signing into quill.Sign: nested Mach-O binaries are signed in place and sealed by cdhash/requirement, and the main executable is signed with the Info.plist and CodeResources hashes bound into its code directory. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: TotallyGamerJet <TotallyGamerJet@users.noreply.github.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: TotallyGamerJet <TotallyGamerJet@users.noreply.github.com>
Adds integration tests that sign .app bundles (ad-hoc, with a certificate, and with nested binaries) and verify the results with Apple's codesign tool, and updates the sign command help text. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: TotallyGamerJet <TotallyGamerJet@users.noreply.github.com>
Bundles submitted for notarization are zipped with the bundle directory as the top-level entry (matching 'ditto -c -k --keepParent'), preserving symlinks and file modes. IsSigned now reports on bundles by checking the resource seal and the main executable signature. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: TotallyGamerJet <TotallyGamerJet@users.noreply.github.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: TotallyGamerJet <TotallyGamerJet@users.noreply.github.com>
Previously a failure here was silently swallowed (returning nil), which could produce a corrupt binary without any indication. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: TotallyGamerJet <TotallyGamerJet@users.noreply.github.com>
Matches the zip layout produced by 'ditto -c -k --keepParent' exactly. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: TotallyGamerJet <TotallyGamerJet@users.noreply.github.com>
Drops the hand-rolled magic-number sniffing in the bundle package in favor of the existing parser-based detection. IsMachoFile now reports unparseable content as (false, nil) instead of returning the parse error, reserving errors for IO failures -- this also makes the intended 'not a darwin macho executable' message in notary payload preparation reachable for non-Mach-O input. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: TotallyGamerJet <TotallyGamerJet@users.noreply.github.com>
TotallyGamerJet
force-pushed
the
app-bundle-signing
branch
from
July 17, 2026 20:46
5c59fa8 to
007d3a3
Compare
themcfarland
pushed a commit
to themcfarland/mercury
that referenced
this pull request
Aug 15, 2026
…esign Three gaps in the macOS packaging, all visible next to the Windows side. The image was always named Mercury.dmg, with nothing to distinguish 1.9.10 from 1.9.11 in a downloads folder or on a release page -- while Windows has shipped mercury-$(MERCURY_VERSION)-w64-<hash>.zip and Mercury_$(MERCURY_VERSION)_Setup.exe all along. MERCURY_VERSION was already parsed from common/mercury_version.h for exactly that purpose; the .dmg simply never used it. Now Mercury-1.9.11-universal.dmg. The image also held only Mercury.app. There is a universal CLI target, but nothing copied its output in, so a Mac operator wanting a headless station -- a TNC for Winlink/BPQ32, or uucp -- had nothing to install. The GUI is built -tags mercury_embedded with the modem linked in, so the CLI is a genuinely separate artifact. It goes in a "Command Line" folder beside the .app with mercury.ini.example and a README, so a drag-install still copies exactly one thing and nothing extra lands inside the bundle to complicate signing. And there was no signing. Added opt-in signing via rcodesign (github.com/indygreg/apple-platform-rs): the CLI, then the .app bundle, then the image -- in that order, because signing a disk image does NOT sign what is inside it. Notarization is a separate target (macos-notarize-dmg) since it needs the network and Apple's verdict. Missing credentials warn instead of failing, so developer builds are unchanged. rcodesign rather than Apple's codesign or anchore/quill. Quill was tried first and rejected: it signs Mach-O binaries only, and cannot sign bundles or disk images (anchore/quill#815, #550 both open, no CodeResources support in its tree) -- which is everything Gatekeeper actually judges. rcodesign covers Mach-O, bundles, .dmg and .pkg, plus notarize and staple, in pure Rust with no Mac, no Xcode and no keychain, so the signing certificate never has to reach a macOS runner. Note this does not remove macOS from the release entirely: hdiutil builds the image and is Apple-only. What moves off the Mac is signing and notarization. Two pre-existing bugs surfaced while testing this: - macos-universal never invoked internal_deps, so after its own `make clean` the link failed on a missing modem/freedv/libfreedvdata.a. That target could not work on a clean tree; nothing had called it from the dmg path before, so it went unnoticed. - the two universal targets both run `make clean`, which removes `mercury` AND Mercury.app, so they cannot be ordered as prerequisites -- whichever ran second deleted the other's output. The CLI is now built first and parked under a name clean does not match (and deliberately NOT added to clean, which would defeat it), cleared at the start of the recipe so a leftover from a failed run can never be staged as if fresh. Tested on macOS 15.7.7 (x86_64): - image mounts and contains Mercury.app, Applications, and Command Line/{mercury,mercury.ini.example,README.txt} - both binaries report `lipo -archs` = x86_64 arm64 - the CLI runs: "Mercury Version 1.9.11 (git 6dec3b0)" - a Mercury.app signed by rcodesign 0.28.0 ON LINUX passes Apple's own `codesign --verify --deep --strict`: "valid on disk", "satisfies its Designated Requirement", with CodeResources written and the universal Mach-O sealed - the .dmg signed on Linux likewise verifies and still mounts Signed with a self-signed test certificate, so spctl still rejects it -- that is Gatekeeper judging the credential, not the signature. A real Developer ID plus notarization is the remaining gate, and is a credentials matter rather than a code one. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #550
Adds the ability to sign and notarize macOS application bundles (
.appdirectories), in addition to plain Mach-O binaries. The implementation is modeled on apple-codesign (Rust), which implements bundle signing without relying on macOS.How it works
quill sign My.app(and the underlyingquill.Sign) now detects app bundles and:MacOS/,Frameworks/,PlugIns/, etc., per Apple's resource rules) are signed in place with the same signing material.Contents/_CodeSignature/CodeResources: regular files by SHA-1 (files) + SHA-256 (files2) content hashes, symlinks by their target, and nested binaries by their code directory hash (cdhash) and designated requirement text. The rules embedded in the seal are the same version 1/version 2 rule sets Apple'scodesignemits.Info.plistandCodeResourcesbound into code directory special slots 1 and 3, andCFBundleIdentifieras the default signing identity (matchingcodesignbehavior;--identitystill overrides).Notarization also accepts bundles:
quill notarize My.appzips the bundle for submission with the same layout asditto -c -k --keepParent(top-level bundle entry, symlinks and file modes preserved), andIsSignednow reports on bundles by checking the resource seal and main executable.New
quill/bundlepackage holds bundle detection,Info.plistparsing, resource rules, and theCodeResourcesbuilder. One new dependency:howett.net/plist(BSD) for plist parsing/serialization.Verification
CodeResourcescomparison), plus zip payload round-trip tests.codesign -vvv --verify --deep --strict(and debug output assertions), alongside the existing binary signing tests.codesigndeep/strict verification, including validation of a nested helper binary against its sealed requirement, and the notarization zip round-trips losslessly (unzip→ deep verify passes; entry list matchesdittooutput).Limitations (possible follow-ups)
.apps) are not yet supported and fail with a clear error; nested plain Mach-O binaries (dylibs, helpers) are fully supported. This covers the common Go/CGo app layout.Contents/CodeResources, so stapled tickets won't invalidate the seal.Also includes a small standalone fix: a failure while updating superblob offset references was silently swallowed (
return nil), which could produce a corrupt binary with no error.🤖 Generated with Claude Code