Why must nested code be signed before the code that contains it, what does “nested code is modified or invalid” mean, and why is --deep discouraged for releases?
Predict first
A bundle with a nested dylib is signed in different orders. What does codesign --verify report?
// === main.swift ===
print("hi")
// === lib.swift ===
public func f() -> Int { 1 }
// === run.sh ===
swiftc main.swift -o app 2>/dev/null; swiftc -emit-library lib.swift -o libfoo.dylib 2>/dev/null
make() {
rm -rf App.app; mkdir -p App.app/Contents/MacOS App.app/Contents/Frameworks
cp app App.app/Contents/MacOS/App; cp libfoo.dylib App.app/Contents/Frameworks/
printf '<?xml version="1.0" encoding="UTF-8"?><plist version="1.0"><dict><key>CFBundleIdentifier</key><string>com.example.app</string><key>CFBundleExecutable</key><string>App</string><key>CFBundlePackageType</key><string>APPL</string></dict></plist>' > App.app/Contents/Info.plist
}
check() { codesign --verify --strict --deep App.app 2>&1 | tail -1; }
make; codesign -s - -f App.app 2>/dev/null; codesign -s - -f App.app/Contents/Frameworks/libfoo.dylib 2>/dev/null
echo "outer first, inner second: $(check)"
make; codesign -s - -f App.app/Contents/Frameworks/libfoo.dylib 2>/dev/null; codesign -s - -f App.app 2>/dev/null
check; echo "inner first, outer second: exit $?"
make; codesign -s - -f --deep App.app 2>/dev/null
check; echo "--deep: exit $?"
- Outer then inner fails with “nested code is modified or invalid”; inner then outer passes; --deep passes
- All three orders pass
- Inner then outer fails
Mental model. Sealing envelopes: you seal the small envelope first and put it in the big one. Reopening the small one after the big one is sealed leaves the big seal broken.
A bundle’s signature seals hashes of everything inside it, including the signatures of nested frameworks, extensions, helpers and the watch app. Signing is therefore inside-out: nested code first, then the code that contains it, last the app. If you sign the app and then re-sign a framework (or any file inside the bundle changes), the outer seal no longer matches and verification reports nested code is modified or invalid. The check signs a small bundle both ways: outer then inner fails with that message, inner then outer verifies (exit 0), and --deep also produces a valid result because it walks the nested code for you.
The reason people avoid --deep for releases is control: it applies one identity and one set of options to everything, while real apps need different entitlements per extension, the hardened runtime on helpers and preserved metadata, and a one-line mistake is hard to see in its output. Apple’s guidance has long been to sign each component explicitly. Xcode does the ordering when it builds and exports; the order bites when you add a custom script phase or CI step that touches the bundle after signing (stripping architectures from a vendored framework, swapping a plist, injecting a crash-reporter configuration). Do such work before the signing step, or sign again afterwards from the innermost out. Notarisation and App Store validation both reject incorrectly nested signatures. The check ran on a macOS bundle with an ad hoc identity; iOS bundles follow the same rule but need a real certificate and profile to install.
The outer seal covers the inner signature, so changing the inner one afterwards breaks it.
Common traps
- Modifying the bundle after signing. A script that edits the Info.plist or strips a framework in a late build phase silently breaks the seal.
- Reaching for
--deepto silence an error. It can hide which component is wrongly signed and applies the same entitlements everywhere.
They might follow up with
- How do you check which part is bad? Run
codesign --verify --verbose=4on each nested component from the inside. - Where do extensions fit? They are nested bundles under
PlugInsand are signed before the app, each with its own entitlements and profile.
Related questions
- 25.6.2 What is inside a code signature: the code directory, hashes, the designated requirement and the team ID, and…
- 6.8.3 Why is an app extension a separate process, and what does that mean for how you build it?
- 25.2.3 How do you design a release pipeline: hermetic builds, secrets, caching, artefact promotion and the order of…
← What is inside a code signature: the code directory, hashes, the… How does a CI job authenticate to the App Store Connect API: API… →