stabilizeApp: sign wrapped executables with the bundle's requirement #1

Merged
natsukium merged 1 commit from sign-wrapped-executables into main 2026-09-20 11:44:47 +09:00
Owner

Fixes https://github.com/natsukium/nix-mac-app-identity/issues/1.

Problem

A bundle wrapped with makeBinaryWrapper kept its unstable identity after stabilizeApp. Contents/MacOS/foo is a C stub that execs .foo-wrapped by the absolute store path of the source derivation, and stabilize.sh left binaries inside the bundle untouched. The process the kernel holds, and the one TCC records the grant against, was therefore the unsigned original, which moves on every rebuild. Reproduced on pkgs.flameshot and pkgs.kitty.

Fix

  • The stabilized derivation takes the source's own store name so the two paths are equal in length, which is what an in-place Mach-O rewrite needs. Names over 207 characters are refused at eval time. Identify the output by pname; a mkAppBundle result is now named <App>.app.
  • Store references in text files and in Mach-Os inside a signed bundle are rewritten before signing. Mach-Os outside the bundle, in places rcodesign seals without signing, or in a bundle that is passed through, refuse the build.
  • Every Mach-O under Contents/MacOS is passed to rcodesign with a per-path --binary-identifier / --code-requirements-file scope and verified after signing, since rcodesign does not inherit either into nested Mach-Os. Nested bundles keep their own identity. Directory links, links into nested bundles, and names with : or @ are unsupported.
  • A CFBundleIdentifier naming the source store path is refused.

Tests

20 → 50 checks. A self-signed team certificate (tests/team.pem, fixed signing time) makes the vendor-signature fixtures reproducible in the sandbox.

Verified on the real packages: .flameshot-wrapped, .kitty-wrapped and kitten come out with designated => identifier "<bundle id>", no file in the output names the source path, and the binaries run.

Fixes https://github.com/natsukium/nix-mac-app-identity/issues/1. ## Problem A bundle wrapped with `makeBinaryWrapper` kept its unstable identity after `stabilizeApp`. `Contents/MacOS/foo` is a C stub that execs `.foo-wrapped` by the absolute store path of the *source* derivation, and `stabilize.sh` left binaries inside the bundle untouched. The process the kernel holds, and the one TCC records the grant against, was therefore the unsigned original, which moves on every rebuild. Reproduced on `pkgs.flameshot` and `pkgs.kitty`. ## Fix - The stabilized derivation takes the source's own store name so the two paths are equal in length, which is what an in-place Mach-O rewrite needs. Names over 207 characters are refused at eval time. Identify the output by `pname`; a `mkAppBundle` result is now named `<App>.app`. - Store references in text files and in Mach-Os inside a signed bundle are rewritten before signing. Mach-Os outside the bundle, in places rcodesign seals without signing, or in a bundle that is passed through, refuse the build. - Every Mach-O under `Contents/MacOS` is passed to rcodesign with a per-path `--binary-identifier` / `--code-requirements-file` scope and verified after signing, since rcodesign does not inherit either into nested Mach-Os. Nested bundles keep their own identity. Directory links, links into nested bundles, and names with `:` or `@` are unsupported. - A `CFBundleIdentifier` naming the source store path is refused. ## Tests 20 → 50 checks. A self-signed team certificate (`tests/team.pem`, fixed signing time) makes the vendor-signature fixtures reproducible in the sandbox. Verified on the real packages: `.flameshot-wrapped`, `.kitty-wrapped` and `kitten` come out with `designated => identifier "<bundle id>"`, no file in the output names the source path, and the binaries run.
Sign every Mach-O under Contents/MacOS with the bundle's requirement
Some checks failed
ci / nix flake check (pull_request) Has been cancelled
5a8398f5e5
A bundle wrapped with makeBinaryWrapper kept its unstable identity after
stabilizeApp: Contents/MacOS/foo is a C stub that execs .foo-wrapped by the
absolute store path of the source derivation, and stabilize.sh left binaries
inside the bundle untouched. The process the kernel holds, and therefore the
one TCC records the grant against, was the unsigned original in the source
store path, and each rebuild moved it. pkgs.flameshot and pkgs.kitty are
affected (issue #1).

Two things have to change for the exec'd binary to carry the identity.

The store path inside a Mach-O can only be rewritten in place, since the file
is laid out by offsets, so the stabilized derivation now takes the source's
own store name to make the two paths equal in length. A name longer than 207
characters would be truncated by sanitizeDerivationName and break the
equality, so that is refused at eval time. Store references in text files and
in Mach-Os inside a signed bundle are rewritten before signing; rcodesign
rebuilds the signature of every Mach-O it seals afterwards, so the rewrite is
safe there and nowhere else. Mach-Os outside the bundle, in places rcodesign
seals without signing, or in a bundle that is passed through, refuse the
build instead of shipping an unsigned or mismatched binary.

rcodesign passes neither the identifier nor the requirement down to nested
Mach-Os, giving them a filename-derived identifier and a cdhash requirement.
Every Mach-O found under Contents/MacOS is therefore named to rcodesign with a
per-path --binary-identifier and --code-requirements-file scope, and each is
verified after signing. Nested bundles are detected the way rcodesign detects
them and keep their own identity; links to directories, into nested bundles,
and names using rcodesign's ':' and '@' scope syntax cannot be given the
bundle's identity and are treated as unsupported.

The tests need a vendor-signed fixture, so a self-signed team certificate is
checked in and signed with a fixed signing time to keep the fixtures
reproducible in the sandbox.

Assisted-by: Claude Code:Fable 5.1
natsukium changed title from Sign every Mach-O under Contents/MacOS with the bundle's requirement to fix(stabilizeApp): sign wrapped executables with the bundle's requirement 2026-09-20 11:35:11 +09:00
natsukium force-pushed sign-wrapped-executables from 5a8398f5e5
Some checks failed
ci / nix flake check (pull_request) Has been cancelled
to ecfaf9c6fb
Some checks failed
ci / nix flake check (pull_request) Has been cancelled
2026-09-20 11:35:12 +09:00
Compare
natsukium changed title from fix(stabilizeApp): sign wrapped executables with the bundle's requirement to stabilizeApp: sign wrapped executables with the bundle's requirement 2026-09-20 11:37:08 +09:00
natsukium force-pushed sign-wrapped-executables from ecfaf9c6fb
Some checks failed
ci / nix flake check (pull_request) Has been cancelled
to 3ccf168639
All checks were successful
ci / nix flake check (pull_request) Successful in 1m26s
2026-09-20 11:37:09 +09:00
Compare
natsukium force-pushed sign-wrapped-executables from 3ccf168639
All checks were successful
ci / nix flake check (pull_request) Successful in 1m26s
to fd67f94ab0
All checks were successful
ci / nix flake check (pull_request) Successful in 21s
2026-09-20 11:43:04 +09:00
Compare
natsukium deleted branch sign-wrapped-executables 2026-09-20 11:44:47 +09:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
natsukium/nix-mac-app-identity!1
No description provided.