Skip to content

Code signing chain of trust: from binary to Apple's root

Every Mach-O on a Mac is signed; every signature chains to an Apple root cert. Here's what the kernel actually verifies, what notarization adds, and how the SEP roots it all.

Published 6 min read
Code signing chain of trustFull chain from a user-installed binary's signature up through the macOS install's Apple Root CA, the sealed system volume, kernelcache, LLB, Boot ROM, and the SEP's hardware fuse.SOFTWARE TRUST CHAINYour binary's signatureCMS over CodeDirectory · entitlementssoftwareDeveloper ID Application certleaf cert with your team identifiersoftwareDeveloper ID Intermediate CAshared by all Developer ID signerssoftwareApple Root CAbaked into macOS install — never fetchedsoftwareSealed System VolumeMerkle hash root signed by Applesystemkernelcache + LLBloaded + verified by iBootfirmwareBoot ROMimmutable · ROM mask programmed at silicon manufacturehardwareEach layer is verified by the layer below before it's allowed to run.AMFI gates thisat exec time.Page hashingverifies pageslazily as theyfault in.The root CA isnot network-fetched.Boot ROM keyis fused at theSEP.

Every Mach-O binary on a modern Mac is signed. The code signing glossary entry covers what's in the signature; the security architecture article covers how signing fits with the other security layers. This article is the chain itself — what certificates exist, how the kernel verifies them, how notarization adds another link, and where the chain ultimately roots.

What's actually in the signature blob

The Mach-O LC_CODE_SIGNATURE load command points at a chunk of trailing bytes that contains a SuperBlob — a header plus an array of slot-typed inner blobs:

  • CodeDirectory — the table of SHA-256 hashes, one per code page, plus identifier, version, team ID, runtime flags. There can be multiple (SHA-1 + SHA-256 alternates for old-OS compatibility).
  • Requirements — Designated Requirement (DR) language describing who the legitimate signer is. The kernel uses this to refuse re-signed binaries.
  • Entitlements — the XML/plist blob granting capabilities.
  • CMS Signature — a PKCS#7 / CMS blob containing the signer's certificate chain and the signed CodeDirectory hash.
apple-oss-distributions/SecurityOSX/libsecurity_codesigning/lib/SecCode.cppThe userspace side of code signing — what codesign(1) operates on.View on GitHub(line )

The CMS signature is the cryptographic anchor. It signs the CodeDirectory's hash with the signer's private key, and the embedded certificate chain proves the public key is trusted.

The certificate chain

A typical Developer ID-signed binary's CMS chain is:

Leaf:          "Developer ID Application: Acme Inc. (TEAMID12)"
Intermediate:  "Developer ID Certification Authority"
Root:          "Apple Root CA"

The leaf cert is what's unique to the developer. The intermediate is shared by all Developer ID signers and chains to Apple's root. The root cert's public key is baked into macOS — it's not fetched, not negotiated, not trusted-on-first-use. It's there at install time, signed into the system itself.

When the kernel validates a signature, it walks this chain bottom-up:

  1. Hash the CodeDirectory locally; verify the CMS signature matches.
  2. Verify the leaf certificate's signature was made by the intermediate's private key.
  3. Verify the intermediate's signature was made by Apple Root CA.
  4. Confirm Apple Root CA matches the baked-in copy.

If any step fails, the binary is rejected — refused to load.

Page hashing — why the signature stays honest at runtime

The CodeDirectory contains a SHA-256 hash of every 4 KB code page. The kernel doesn't verify all of them at exec time (that would slow startup). Instead, it verifies pages lazily, as the MMU pages them in:

  1. Process executes; CPU faults on an unmapped page in the __TEXT segment.
  2. The kernel's page-fault handler reads the page from the file.
  3. AMFI hashes the page and compares against the CodeDirectory.
  4. If matches: PTE installed, execution continues.
  5. If mismatch: page-in fails, process killed.

This is what stops binary patching at rest. An attacker who modifies a single byte in the on-disk binary causes the next page-in to fail.

apple-oss-distributions/SecurityOSX/libsecurity_codesigning/lib/StaticCode.cppStaticCode — the userspace side of static verification. Kernel uses related code.View on GitHub(line )

The Designated Requirement (DR)

A binary's signature includes a Designated Requirement — a small expression in Apple's Code Requirements Language that says "this binary should be considered legitimate if and only if..."

A typical DR:

identifier "com.acme.MyApp" and anchor apple generic and
certificate leaf[subject.OU] = "TEAMID12"

Read: this app is legitimate if its bundle identifier is com.acme.MyApp, the chain anchors to Apple, and the leaf cert's team OU is TEAMID12.

The kernel uses the DR to detect re-signing: even if someone re-signs your binary with their own Developer ID, the team OU won't match TEAMID12, and the DR check fails — the kernel refuses to honor the binary as "the same" app for purposes of keychain access, TCC grants, etc.

This is also why moving an app between Macs preserves its identity: the DR is signed into the binary and self-verifying.

Notarization — Apple's after-the-fact malware scan

For non-App-Store Developer ID-signed binaries, signing isn't enough — they also need to be notarized:

  1. Developer submits the signed binary to Apple's notarization service.
  2. Apple runs an automated malware scan + integrity checks.
  3. If clean, Apple returns a notarization ticket — a small blob signed by Apple confirming "this signature has passed our scan."
  4. The developer staples the ticket to their distribution (via xcrun stapler staple).

When the binary is first run on a user's Mac, macOS:

  • Checks the binary's signature is valid (chain to Apple root).
  • Checks the notarization ticket — either stapled to the binary, or fetched on-demand from Apple's notarization servers using the binary's signature hash as the lookup key.

Without a valid ticket, Gatekeeper refuses to run the binary. The user gets a "Apple could not verify..." dialog.

The root, ultimately

The root of trust is Apple's "Apple Root CA" certificate, embedded in the macOS install at manufacture. The bedrock physical assumption is:

  • The macOS install image you got from Apple contains the genuine Apple Root CA public key.
  • The hardware (Boot ROM) verifies macOS itself wasn't tampered with, going back further.
  • The Boot ROM key is fused into the SEP at silicon manufacture.

So the chain extends:

your app's signature
  ↓ verified by
Developer ID CA
  ↓ verified by
Apple Root CA (in macOS)
  ↓ verified by
sealed system volume (Merkle hash)
  ↓ verified by
kernelcache signature (iBoot checks)
  ↓ verified by
LLB signature (Boot ROM checks)
  ↓ verified by
hardware fuse (SEP)

The trust ultimately rests on a piece of silicon that Apple physically programs at the factory. Everything else is verified against it.

Library validation — the runtime extension

Code signing alone says "this binary is legitimate." Library validation extends this to dylibs loaded at runtime:

  • A signed process can only load dylibs that are signed by Apple OR by the same team as the process itself.
  • Without library validation, an attacker could craft a malicious dylib and have a victim app load it via DYLD_INSERT_LIBRARIES.

Library validation is on by default for all platform binaries; opt-in for third-party apps via the com.apple.security.cs.disable-library-validation entitlement (which itself requires explicit Apple approval to be honored).

See AMFI for the kernel extension that enforces this on every dlopen.

What surprises newcomers

  • Page hashing is lazy. Verification cost is amortized across the process's lifetime, not paid up front.
  • The DR is signed into the binary. Re-signing without preserving the DR creates a different identity to the kernel.
  • Notarization isn't code review. It's automated scanning. The legal contract is "Apple has scanned this and didn't find malware at scan time."
  • The chain anchors in hardware. Removing any link of the chain in software is detectable; removing the hardware anchor is impossible.

apple-oss-distributions/SecurityOSX/libsecurity_codesigning/lib/SecRequirement.cppThe Code Requirements Language interpreter — what evaluates DR expressions.View on GitHub(line ) apple-oss-distributions/SecurityOSX/libsecurity_codesigning/lib/CSCommon.hThe public types in the code-signing API.View on GitHub(line )

And the security architecture article — where this chain sits among the six layered enforcement systems.

Related

The gauntlet every newly-downloaded app passes through — quarantine xattr, signature check, notarization check, user prompt. Where each decision is made and how to debug it.
From double-click to first window: LaunchServices, launchd, posix_spawn, AMFI, dyld, the shared cache, sandbox profile installation, the runloop. Six subsystems in three seconds.
Inside the macOS sandbox — a Scheme-derived policy language, a compiler in libsandbox, and a kernel evaluator that runs on every controlled syscall.