Skip to content
C.W.K.
Lesson 01 of 04 · published

A Signing Identity Chosen by Measurement

~14 min · code signing, macOS, hardened runtime, designated requirement

Level 0New Tab
0 XP0/44 lessons0/13 achievements
0/100 XP to next level100 XP to go0% complete

A signature is read by other programs

On macOS a code signature is more than a seal on a bundle. Other programs read it and decide things from it. macOS remembers Dad's microphone and camera grants against it, the Keychain decides by it who may open an item, and 1Password's desktop app reads it before it will talk to a browser at all. So the choice of identity that signs Voyager has consequences far outside the build.

The first brick was signed with the family's own self-signed identity, cwk-codesign, the one family Mac apps use so that their grants survive rebuilds. Then the engine gate's first case, W1, met 1Password. Track 2 told the result and track 8 told the debugging. The short version: 1Password's helper accepts a browser only when its signature carries a team identifier under Apple's chain and enforces the hardened runtime, and cwk-codesign has neither. This lesson is the signing that came out of that measurement.

One file knows

Every signing decision lives in scripts/signing.zsh. It is never run on its own. build-app.sh and doctor.sh both source it, so the build and the report cannot disagree about which identity is meant. Its header (first block) gives the reason in one paragraph.

The default is found, not configured. The script lists the Mac's valid code-signing identities, keeps those named Apple Development: …, and takes the first whose certificate names the paid team as its organizational unit. A Mac without one falls back to cwk-codesign, and then the hardened runtime defaults to off, because the family identity is only for builds that never meet 1Password. Two environment variables override the identity and the runtime, so every departure from the default is spelled out on the command line.

IdentityHardened runtimeDesignated requirement1Password
The paid team's Apple Developmenton by defaultbundle id, Apple's chain, the teamaccepted, measured
cwk-codesignoff by defaultbundle id, the certificate's hashrefused, measured

What the designated requirement pins

A designated requirement is the rule a signature must satisfy for macOS to treat a new build as the same app. Privacy grants and Keychain access are stored against it. A rebuild that fails the old requirement is a stranger to macOS: the microphone prompt comes back, and 1Password's consent is gone.

For an Apple-issued identity, codesign's own default requirement pins the certificate's common name. That looks stable until the certificate is renewed, and renewal always comes. So Voyager hands codesign a requirement of its own (second block): the bundle identifier, anchor apple generic for Apple's chain, the team in the leaf certificate's organizational unit, and the extension that marks an Apple development certificate. A renewed certificate from the same team satisfies every clause, so the grants and the consent carry over.

Then the build reads the requirement back out of the signed bundle and checks that the clauses which make it stable are there: the bundle identifier, anchor apple generic and the team. It checks the staged bundle, the copy staged for install, and the installed app, each time. Know what that check is, though. It looks for those clauses inside the string and never compares the whole string, so a requirement that lost the development-certificate marker, or gained a common-name pin beside the team, would still pass. What it refuses is a requirement missing one of the three.

The hardened runtime has a price

The hardened runtime is what 1Password demands, and it is also a set of refusals. One of them: an app under it gets no microphone and no camera unless its entitlements claim them. A browser has to claim both, because any page may ask for either. The third block is the whole entitlements file, and its comment draws the boundary. The claim only lets a page's request reach Voyager. Whether that page gets the device is still decided by the trust zone and by macOS.

The build does not assume the runtime flag took effect. After signing, it reads the signature's details back and stops with HARD_FAIL if the runtime was asked for and is missing, or if the signing authority is not the identity it meant to use.

Rules that fell out of it

  • Never sign Voyager with the family identity to match the family. The house sign says it in those words. Looking like the sibling apps is no reason when the measurement says the program that matters refuses it.
  • Sign where the keychain is open. One Mac in the fleet, reached over ssh, could not sign: its login keychain was locked outside the graphical session, and codesign failed with errSecInternalComponent. That is a fact about that keychain in that session, not a law of ssh, and the house sign records it for that one Mac. The answer was not to fight the keychain. Voyager is built and signed on the workbench and copied to other Macs by the family's deploy script. The copy keeps its signature, so it keeps its grants.
  • The next step waits for a reason. A Developer ID certificate with notarization comes only if a Mac in the fleet or a renewal breaks this arrangement, and that request belongs to Codex Chief Pippa.
Principle: Choose a signing identity by what reads the signature. Its real audience is every program that will decide something from it.

Code

cwkVoyager, scripts/signing.zsh: why this identity and this runtime (verbatim, lines 1-13)·shell
# Which identity signs Voyager, and how. Sourced by build-app.sh and
# doctor.sh; never run on its own.
#
# 1Password's desktop connection accepts a browser only when its signature
# carries a team identifier under Apple's chain and enforces the hardened
# runtime (docs/ENGINE-EVALUATION.md §6 W1, measured 2026-10-03). The
# family's cwk-codesign has neither, so Voyager signs with the paid team's
# Apple Development identity and the hardened runtime by default. The
# designated requirement pins the team, not the certificate, so a renewed
# certificate keeps macOS's grants and 1Password's consent.
#
# VOYAGER_SIGN_IDENTITY names another identity (cwk-codesign for a build that
# never meets 1Password); VOYAGER_HARDENED_RUNTIME=0|1 overrides the runtime.
cwkVoyager, scripts/signing.zsh: the designated requirement that survives renewal (verbatim, lines 41-47)·shell
# The designated requirement a build signed with an Apple-issued identity
# gets: the bundle, Apple's chain, the team. codesign's own default would pin
# the certificate's common name, which a renewal changes.
voyager_designated_requirement() {
  local bundle_identifier="$1"
  print -r -- "designated => identifier \"$bundle_identifier\" and anchor apple generic and certificate leaf[subject.OU] = \"$voyager_team\" and certificate 1[field.1.2.840.113635.100.6.2.1] exists"
}
cwkVoyager, Resources/Voyager.entitlements: the two devices the hardened runtime would otherwise refuse (verbatim, lines 1-13)·text
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
	<!-- The hardened runtime (what 1Password requires of a browser) refuses the
	     microphone and the camera unless the app claims them. Pages ask through
	     WebKit; the trust zone and macOS still decide. -->
	<key>com.apple.security.device.audio-input</key>
	<true/>
	<key>com.apple.security.device.camera</key>
	<true/>
</dict>
</plist>

External links

Exercise

Pick a Mac app you build and sign. Run codesign -dr - on the built bundle and write down every clause of its designated requirement. Mark the clause a certificate renewal would change. Then write the requirement you want, pass it to codesign with --requirements, and add a build step that reads the requirement back from the signed bundle and fails when it is not the shape you chose.
Hint
For an Apple-issued identity the clauses that survive renewal are the bundle identifier, anchor apple generic and the team in certificate leaf[subject.OU]. Match the read-back string against those clauses rather than comparing it for equality: codesign may print the team with or without quotes, and Voyager's check accepts both spellings. A clause match has a blind side, though: it also passes a requirement that carries an extra clause, such as a common-name pin beside the team. If your check must refuse that too, refuse a certificate leaf[subject.CN] clause explicitly.

Progress

Progress is local-only — sign in to sync across devices.
Spotted a bug or have feedback on this page?Report an Issue

Comments 0

🔔 Reply notifications (sign in)
Sign in — Please sign in to comment.

No comments yet — be the first.