{"title":"Notarizing And Stapling A Mac App For Direct Download","html_url":"https://deallocatedobjects.com/posts/notarizing-and-stapling-a-mac-app-for-direct-download","source_url":"","markdown_content":"Every Mac app I sell from my own site goes out the same way. It's signed with Developer ID, notarized, stapled, put in a DMG, and then the DMG gets signed, notarized and stapled too. The steps are well known. The order is where people get bitten, along with which file each step applies to.\n\nHere's the sequence my release scripts run, with the flags and the reason behind each one.\n\n### Signing With Hardened Runtime And A Timestamp\n\nBefore the notary service accepts an upload, every executable in it has to be signed with Developer ID, have the hardened runtime turned on and carry a secure timestamp. The debug-only `com.apple.security.get-task-allow` entitlement gets it rejected.\n\nIf Xcode does the signing, `ENABLE_HARDENED_RUNTIME: YES` in the target's settings turns on the hardened runtime, and exporting the archive with the `developer-id` method handles the Developer ID signature and the timestamp:\n\n```xml title=\"ExportOptions.plist\"\n<key>method</key>\n<string>developer-id</string>\n<key>teamID</key>\n<string>TEAMID</string>\n```\n\n```bash\nxcodebuild -exportArchive -archivePath build/MyApp.xcarchive \\\n  -exportOptionsPlist build/ExportOptions.plist -exportPath build/export\n```\n\nThe export re-signs every nested framework, XPC service and helper app with your Developer ID certificate, the runtime flag and a timestamp. After it finishes, my scripts make sure the debug entitlement didn't slip through, because it's quicker to catch that before uploading than to read about it in a notary log:\n\n```bash\nif codesign -d --entitlements - \"$app\" 2>/dev/null | grep -q \"get-task-allow\"; then\n  echo \"ERROR: exported app carries com.apple.security.get-task-allow\" >&2; exit 1\nfi\n```\n\n### Inside Out, Without --deep\n\nSometimes the signing has to happen outside Xcode, for instance when the build needs provisioning profiles and entitlements generated at release time. Then you sign each piece yourself, starting from the deepest code and working outward.\n\nOrder matters because a bundle's signature records the signatures of the code nested inside it. Sign the app first and a framework second, and the app's seal no longer matches what's on disk.\n\nThe tempting shortcut is `codesign --deep`. `man codesign` marks it \"DEPRECATED for signing as of macOS 13.0\", and the warning right under that is the real reason to skip it: \"All signing options will be applied, in turn, to all nested content. This is almost never what you want.\" Pass `--entitlements` along with `--deep` and your app's entitlements end up on Sparkle's XPC services too. Sparkle's docs warn against `--deep` for exactly this reason: its downloader service is signed with entitlements of its own. It also only finds nested code in a fixed list of locations (`Contents/Frameworks`, `Contents/XPCServices`, `Contents/Helpers`, `Contents/Library/LoginItems` and a few more), and anything elsewhere goes unsigned.\n\nHere's the inside-out version for an app that embeds Sparkle 2, in the order Sparkle's own docs give:\n\n```bash title=\"sign.sh\"\nIDENTITY=\"Developer ID Application: Your Name (TEAMID)\"\ncs=(codesign --force --timestamp --options runtime --sign \"$IDENTITY\")\nfw=\"MyApp.app/Contents/Frameworks/Sparkle.framework\"\nfwv=\"$fw/Versions/$(readlink \"$fw/Versions/Current\")\"\n\n\"${cs[@]}\" \"$fwv/XPCServices/Installer.xpc\"\n\"${cs[@]}\" --preserve-metadata=entitlements \"$fwv/XPCServices/Downloader.xpc\"\n\"${cs[@]}\" \"$fwv/Autoupdate\"\n\"${cs[@]}\" \"$fwv/Updater.app\"\n\"${cs[@]}\" \"$fw\"\n\"${cs[@]}\" --entitlements MyApp.entitlements MyApp.app\n```\n\n`--preserve-metadata=entitlements` keeps the entitlements Sparkle already gave its downloader service, and the app's own entitlements only go on the last line. `--deep` is still the right flag when verifying, which comes up at the end.\n\n### Notarizing With A Stored Profile\n\n`notarytool` reads credentials from the keychain, so the password never goes in a script. Store them once:\n\n```bash\nxcrun notarytool store-credentials notary-profile \\\n  --apple-id you@example.com --team-id TEAMID\n```\n\nLeave out `--password` and it prompts for an app-specific password, then checks the credentials with Apple before saving them (`--validate` is the default).\n\n`notarytool submit` takes a file, so the app goes up as a zip. `ditto -c -k --keepParent` makes a PKZip archive with `MyApp.app` as its top-level folder:\n\n```bash title=\"release.sh\"\nnotarize() { # path to a .zip or .dmg\n  local out id status\n  out=$(xcrun notarytool submit \"$1\" --keychain-profile notary-profile --wait 2>&1)\n  status=$(echo \"$out\" | awk '/^ *status:/{s=$2} END{print s}')\n  if [ \"$status\" != \"Accepted\" ]; then\n    id=$(echo \"$out\" | awk '/^ *id:/{print $2; exit}')\n    echo \"ERROR: notarization of $(basename \"$1\") was not accepted.\" >&2\n    [ -n \"$id\" ] && xcrun notarytool log \"$id\" --keychain-profile notary-profile >&2\n    exit 1\n  fi\n}\n\nditto -c -k --keepParent build/export/MyApp.app build/MyApp-notarize.zip\nnotarize build/MyApp-notarize.zip\n```\n\n`--wait` keeps the command running until the service finishes, so the script can move on to stapling. The function reads the `status:` line and stops on anything other than `Accepted`. When a submission fails, `notarytool log` prints the service's JSON report, which names each file that failed and why, for example a nested binary with no secure timestamp.\n\n### Staple The App Before It Goes In The DMG\n\nNotarization creates a ticket on Apple's side. Without a stapled ticket, Gatekeeper has to look it up online the first time someone opens your app. Stapling attaches the ticket to the file so the check works with no network.\n\nEach stapled file only carries its own ticket. The DMG's ticket stays with the DMG. When someone drags the app to Applications, the copy is still quarantined, Gatekeeper checks it again at first launch, and it needs a ticket of its own. So the app gets stapled first, while it's still a plain folder on disk:\n\n```bash\nxcrun stapler staple build/export/MyApp.app\nxcrun stapler validate build/export/MyApp.app\n```\n\nThe zip you uploaded is only a container for the upload. `stapler` lists the formats it supports as \"UDIF disk images, code-signed executable bundles, and signed 'flat' installer packages\", so the ticket goes on the `.app` and the zip gets thrown away. In a bundle the ticket lands in `Contents/CodeResources`, which is why stapling has to happen before the app goes into a compressed, read-only DMG.\n\n### Then The DMG Gets The Same Treatment\n\nThe DMG is the first thing Gatekeeper checks after a download, so it needs its own signature, notarization and ticket. A disk image isn't executable code, so my scripts sign it with `--timestamp` and no runtime flag:\n\n```bash\nmkdir build/dmg-src && ditto build/export/MyApp.app build/dmg-src/MyApp.app\ncreate-dmg --volname MyApp --app-drop-link 450 190 build/MyApp.dmg build/dmg-src\ncodesign --force --timestamp --sign \"$IDENTITY\" build/MyApp.dmg\nnotarize build/MyApp.dmg\nxcrun stapler staple build/MyApp.dmg\n```\n\nStapling writes into the DMG, so anything that hashes or signs the final file has to come after it. My scripts generate Sparkle's EdDSA signature for the appcast right after stapling, from the finished DMG.\n\n### Checking What People Download\n\nMy release scripts end with checks that ask the same questions Gatekeeper will. For the app:\n\n```bash\nspctl -a -vvv -t exec MyApp.app\nxcrun stapler validate MyApp.app\ncodesign --verify --deep --strict --verbose=2 MyApp.app\ncodesign -dvv MyApp.app 2>&1 | grep -E \"flags|Timestamp|Notarization\"\n```\n\nA good result says `accepted` with `source=Notarized Developer ID`, then \"The validate action worked!\", then `valid on disk` and `satisfies its Designated Requirement`. Here `--deep` checks every nested piece in full, where the default is only a shallow pass. The last line should show `flags=0x10000(runtime)`, a `Timestamp=` line and `Notarization Ticket=stapled`.\n\nThe DMG needs a different assessment type. The default `execute` type rejects it with \"the code is valid but does not seem to be an app\". `-t open` on its own comes back `rejected` with `source=Insufficient Context`. `--context` isn't in `man spctl`, but this is the form that comes back `accepted`:\n\n```bash\nspctl -a -vvv -t open --context context:primary-signature MyApp.dmg\nxcrun stapler validate MyApp.dmg\n```\n\nOn my Mac, `-t install` also accepts a signed DMG, though `man spctl` describes that type as the one for installer packages, so my scripts stick to `-t open`. An unsigned DMG fails both with `source=no usable signature`.\n\nLast, check the app as the buyer gets it, from inside the mounted DMG:\n\n```bash\nhdiutil attach -nobrowse -quiet build/MyApp.dmg\nxcrun stapler validate /Volumes/MyApp/MyApp.app\nspctl -a -vvv /Volumes/MyApp/MyApp.app\nhdiutil detach /Volumes/MyApp -quiet\n```\n\nIf the app was stapled before packaging, `stapler validate` passes here too. If it fails, the app in the DMG has no ticket of its own, and Gatekeeper can't confirm it was notarized when someone opens it for the first time without a network connection.","html_content":"<p>Every Mac app I sell from my own site goes out the same way. It’s signed with Developer ID, notarized, stapled, put in a DMG, and then the DMG gets signed, notarized and stapled too. The steps are well known. The order is where people get bitten, along with which file each step applies to.</p>\n<p>Here’s the sequence my release scripts run, with the flags and the reason behind each one.</p>\n<h2 id=\"signing-with-hardened-runtime-and-a-timestamp\" data-level=\"3\">Signing With Hardened Runtime And A Timestamp</h2>\n<p>Before the notary service accepts an upload, every executable in it has to be signed with Developer ID, have the hardened runtime turned on and carry a secure timestamp. The debug-only <code>com.apple.security.get-task-allow</code> entitlement gets it rejected.</p>\n<p>If Xcode does the signing, <code>ENABLE_HARDENED_RUNTIME: YES</code> in the target’s settings turns on the hardened runtime, and exporting the archive with the <code>developer-id</code> method handles the Developer ID signature and the timestamp:</p>\n<figure class=\"code\" data-lang=\"xml\"><figcaption><span class=\"file\">ExportOptions.plist</span><span class=\"lang\">XML</span></figcaption><pre class=\"shiki shiki-themes github-light github-dark\" style=\"--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e\" tabindex=\"0\"><code><span class=\"line\"><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">&#x3C;</span><span style=\"--shiki-light:#22863A;--shiki-dark:#85E89D\">key</span><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">>method&#x3C;/</span><span style=\"--shiki-light:#22863A;--shiki-dark:#85E89D\">key</span><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">></span></span>\n<span class=\"line\"><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">&#x3C;</span><span style=\"--shiki-light:#22863A;--shiki-dark:#85E89D\">string</span><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">>developer-id&#x3C;/</span><span style=\"--shiki-light:#22863A;--shiki-dark:#85E89D\">string</span><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">></span></span>\n<span class=\"line\"><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">&#x3C;</span><span style=\"--shiki-light:#22863A;--shiki-dark:#85E89D\">key</span><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">>teamID&#x3C;/</span><span style=\"--shiki-light:#22863A;--shiki-dark:#85E89D\">key</span><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">></span></span>\n<span class=\"line\"><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">&#x3C;</span><span style=\"--shiki-light:#22863A;--shiki-dark:#85E89D\">string</span><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">>TEAMID&#x3C;/</span><span style=\"--shiki-light:#22863A;--shiki-dark:#85E89D\">string</span><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">></span></span></code></pre></figure><figure class=\"code\" data-lang=\"bash\"><figcaption><span class=\"lang\">Shell</span></figcaption><pre class=\"shiki shiki-themes github-light github-dark\" style=\"--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e\" tabindex=\"0\"><code><span class=\"line\"><span style=\"--shiki-light:#6F42C1;--shiki-dark:#B392F0\">xcodebuild</span><span style=\"--shiki-light:#005CC5;--shiki-dark:#79B8FF\"> -exportArchive</span><span style=\"--shiki-light:#005CC5;--shiki-dark:#79B8FF\"> -archivePath</span><span style=\"--shiki-light:#032F62;--shiki-dark:#9ECBFF\"> build/MyApp.xcarchive</span><span style=\"--shiki-light:#005CC5;--shiki-dark:#79B8FF\"> \\</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#005CC5;--shiki-dark:#79B8FF\">  -exportOptionsPlist</span><span style=\"--shiki-light:#032F62;--shiki-dark:#9ECBFF\"> build/ExportOptions.plist</span><span style=\"--shiki-light:#005CC5;--shiki-dark:#79B8FF\"> -exportPath</span><span style=\"--shiki-light:#032F62;--shiki-dark:#9ECBFF\"> build/export</span></span></code></pre></figure><p>The export re-signs every nested framework, XPC service and helper app with your Developer ID certificate, the runtime flag and a timestamp. After it finishes, my scripts make sure the debug entitlement didn’t slip through, because it’s quicker to catch that before uploading than to read about it in a notary log:</p>\n<figure class=\"code\" data-lang=\"bash\"><figcaption><span class=\"lang\">Shell</span></figcaption><pre class=\"shiki shiki-themes github-light github-dark\" style=\"--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e\" tabindex=\"0\"><code><span class=\"line\"><span style=\"--shiki-light:#D73A49;--shiki-dark:#F97583\">if</span><span style=\"--shiki-light:#6F42C1;--shiki-dark:#B392F0\"> codesign</span><span style=\"--shiki-light:#005CC5;--shiki-dark:#79B8FF\"> -d</span><span style=\"--shiki-light:#005CC5;--shiki-dark:#79B8FF\"> --entitlements</span><span style=\"--shiki-light:#032F62;--shiki-dark:#9ECBFF\"> -</span><span style=\"--shiki-light:#032F62;--shiki-dark:#9ECBFF\"> \"</span><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">$app</span><span style=\"--shiki-light:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"--shiki-light:#D73A49;--shiki-dark:#F97583\"> 2></span><span style=\"--shiki-light:#032F62;--shiki-dark:#9ECBFF\">/dev/null</span><span style=\"--shiki-light:#D73A49;--shiki-dark:#F97583\"> |</span><span style=\"--shiki-light:#6F42C1;--shiki-dark:#B392F0\"> grep</span><span style=\"--shiki-light:#005CC5;--shiki-dark:#79B8FF\"> -q</span><span style=\"--shiki-light:#032F62;--shiki-dark:#9ECBFF\"> \"get-task-allow\"</span><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">; </span><span style=\"--shiki-light:#D73A49;--shiki-dark:#F97583\">then</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#005CC5;--shiki-dark:#79B8FF\">  echo</span><span style=\"--shiki-light:#032F62;--shiki-dark:#9ECBFF\"> \"ERROR: exported app carries com.apple.security.get-task-allow\"</span><span style=\"--shiki-light:#D73A49;--shiki-dark:#F97583\"> >&#x26;2</span><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">; </span><span style=\"--shiki-light:#005CC5;--shiki-dark:#79B8FF\">exit</span><span style=\"--shiki-light:#005CC5;--shiki-dark:#79B8FF\"> 1</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#D73A49;--shiki-dark:#F97583\">fi</span></span></code></pre></figure><h2 id=\"inside-out-without-deep\" data-level=\"3\">Inside Out, Without --deep</h2>\n<p>Sometimes the signing has to happen outside Xcode, for instance when the build needs provisioning profiles and entitlements generated at release time. Then you sign each piece yourself, starting from the deepest code and working outward.</p>\n<p>Order matters because a bundle’s signature records the signatures of the code nested inside it. Sign the app first and a framework second, and the app’s seal no longer matches what’s on disk.</p>\n<p>The tempting shortcut is <code>codesign --deep</code>. <code>man codesign</code> marks it “DEPRECATED for signing as of macOS 13.0”, and the warning right under that is the real reason to skip it: “All signing options will be applied, in turn, to all nested content. This is almost never what you want.” Pass <code>--entitlements</code> along with <code>--deep</code> and your app’s entitlements end up on Sparkle’s XPC services too. Sparkle’s docs warn against <code>--deep</code> for exactly this reason: its downloader service is signed with entitlements of its own. It also only finds nested code in a fixed list of locations (<code>Contents/Frameworks</code>, <code>Contents/XPCServices</code>, <code>Contents/Helpers</code>, <code>Contents/Library/LoginItems</code> and a few more), and anything elsewhere goes unsigned.</p>\n<p>Here’s the inside-out version for an app that embeds Sparkle 2, in the order Sparkle’s own docs give:</p>\n<figure class=\"code\" data-lang=\"bash\"><figcaption><span class=\"file\">sign.sh</span><span class=\"lang\">Shell</span></figcaption><pre class=\"shiki shiki-themes github-light github-dark\" style=\"--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e\" tabindex=\"0\"><code><span class=\"line\"><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">IDENTITY</span><span style=\"--shiki-light:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"--shiki-light:#032F62;--shiki-dark:#9ECBFF\">\"Developer ID Application: Your Name (TEAMID)\"</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">cs</span><span style=\"--shiki-light:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"--shiki-light:#032F62;--shiki-dark:#9ECBFF\">codesign</span><span style=\"--shiki-light:#032F62;--shiki-dark:#9ECBFF\"> --force</span><span style=\"--shiki-light:#032F62;--shiki-dark:#9ECBFF\"> --timestamp</span><span style=\"--shiki-light:#032F62;--shiki-dark:#9ECBFF\"> --options</span><span style=\"--shiki-light:#032F62;--shiki-dark:#9ECBFF\"> runtime</span><span style=\"--shiki-light:#032F62;--shiki-dark:#9ECBFF\"> --sign</span><span style=\"--shiki-light:#032F62;--shiki-dark:#9ECBFF\"> \"</span><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">$IDENTITY</span><span style=\"--shiki-light:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">fw</span><span style=\"--shiki-light:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"--shiki-light:#032F62;--shiki-dark:#9ECBFF\">\"MyApp.app/Contents/Frameworks/Sparkle.framework\"</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">fwv</span><span style=\"--shiki-light:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"--shiki-light:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">$fw</span><span style=\"--shiki-light:#032F62;--shiki-dark:#9ECBFF\">/Versions/$(</span><span style=\"--shiki-light:#6F42C1;--shiki-dark:#B392F0\">readlink</span><span style=\"--shiki-light:#032F62;--shiki-dark:#9ECBFF\"> \"</span><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">$fw</span><span style=\"--shiki-light:#032F62;--shiki-dark:#9ECBFF\">/Versions/Current\")\"</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"--shiki-light:#6F42C1;--shiki-dark:#B392F0\">\"${</span><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">cs</span><span style=\"--shiki-light:#6F42C1;--shiki-dark:#B392F0\">[</span><span style=\"--shiki-light:#D73A49;--shiki-dark:#F97583\">@</span><span style=\"--shiki-light:#6F42C1;--shiki-dark:#B392F0\">]}\"</span><span style=\"--shiki-light:#032F62;--shiki-dark:#9ECBFF\"> \"</span><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">$fwv</span><span style=\"--shiki-light:#032F62;--shiki-dark:#9ECBFF\">/XPCServices/Installer.xpc\"</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#6F42C1;--shiki-dark:#B392F0\">\"${</span><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">cs</span><span style=\"--shiki-light:#6F42C1;--shiki-dark:#B392F0\">[</span><span style=\"--shiki-light:#D73A49;--shiki-dark:#F97583\">@</span><span style=\"--shiki-light:#6F42C1;--shiki-dark:#B392F0\">]}\"</span><span style=\"--shiki-light:#005CC5;--shiki-dark:#79B8FF\"> --preserve-metadata=entitlements</span><span style=\"--shiki-light:#032F62;--shiki-dark:#9ECBFF\"> \"</span><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">$fwv</span><span style=\"--shiki-light:#032F62;--shiki-dark:#9ECBFF\">/XPCServices/Downloader.xpc\"</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#6F42C1;--shiki-dark:#B392F0\">\"${</span><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">cs</span><span style=\"--shiki-light:#6F42C1;--shiki-dark:#B392F0\">[</span><span style=\"--shiki-light:#D73A49;--shiki-dark:#F97583\">@</span><span style=\"--shiki-light:#6F42C1;--shiki-dark:#B392F0\">]}\"</span><span style=\"--shiki-light:#032F62;--shiki-dark:#9ECBFF\"> \"</span><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">$fwv</span><span style=\"--shiki-light:#032F62;--shiki-dark:#9ECBFF\">/Autoupdate\"</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#6F42C1;--shiki-dark:#B392F0\">\"${</span><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">cs</span><span style=\"--shiki-light:#6F42C1;--shiki-dark:#B392F0\">[</span><span style=\"--shiki-light:#D73A49;--shiki-dark:#F97583\">@</span><span style=\"--shiki-light:#6F42C1;--shiki-dark:#B392F0\">]}\"</span><span style=\"--shiki-light:#032F62;--shiki-dark:#9ECBFF\"> \"</span><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">$fwv</span><span style=\"--shiki-light:#032F62;--shiki-dark:#9ECBFF\">/Updater.app\"</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#6F42C1;--shiki-dark:#B392F0\">\"${</span><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">cs</span><span style=\"--shiki-light:#6F42C1;--shiki-dark:#B392F0\">[</span><span style=\"--shiki-light:#D73A49;--shiki-dark:#F97583\">@</span><span style=\"--shiki-light:#6F42C1;--shiki-dark:#B392F0\">]}\"</span><span style=\"--shiki-light:#032F62;--shiki-dark:#9ECBFF\"> \"</span><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">$fw</span><span style=\"--shiki-light:#032F62;--shiki-dark:#9ECBFF\">\"</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#6F42C1;--shiki-dark:#B392F0\">\"${</span><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">cs</span><span style=\"--shiki-light:#6F42C1;--shiki-dark:#B392F0\">[</span><span style=\"--shiki-light:#D73A49;--shiki-dark:#F97583\">@</span><span style=\"--shiki-light:#6F42C1;--shiki-dark:#B392F0\">]}\"</span><span style=\"--shiki-light:#005CC5;--shiki-dark:#79B8FF\"> --entitlements</span><span style=\"--shiki-light:#032F62;--shiki-dark:#9ECBFF\"> MyApp.entitlements</span><span style=\"--shiki-light:#032F62;--shiki-dark:#9ECBFF\"> MyApp.app</span></span></code></pre></figure><p><code>--preserve-metadata=entitlements</code> keeps the entitlements Sparkle already gave its downloader service, and the app’s own entitlements only go on the last line. <code>--deep</code> is still the right flag when verifying, which comes up at the end.</p>\n<h2 id=\"notarizing-with-a-stored-profile\" data-level=\"3\">Notarizing With A Stored Profile</h2>\n<p><code>notarytool</code> reads credentials from the keychain, so the password never goes in a script. Store them once:</p>\n<figure class=\"code\" data-lang=\"bash\"><figcaption><span class=\"lang\">Shell</span></figcaption><pre class=\"shiki shiki-themes github-light github-dark\" style=\"--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e\" tabindex=\"0\"><code><span class=\"line\"><span style=\"--shiki-light:#6F42C1;--shiki-dark:#B392F0\">xcrun</span><span style=\"--shiki-light:#032F62;--shiki-dark:#9ECBFF\"> notarytool</span><span style=\"--shiki-light:#032F62;--shiki-dark:#9ECBFF\"> store-credentials</span><span style=\"--shiki-light:#032F62;--shiki-dark:#9ECBFF\"> notary-profile</span><span style=\"--shiki-light:#005CC5;--shiki-dark:#79B8FF\"> \\</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#005CC5;--shiki-dark:#79B8FF\">  --apple-id</span><span style=\"--shiki-light:#032F62;--shiki-dark:#9ECBFF\"> you@example.com</span><span style=\"--shiki-light:#005CC5;--shiki-dark:#79B8FF\"> --team-id</span><span style=\"--shiki-light:#032F62;--shiki-dark:#9ECBFF\"> TEAMID</span></span></code></pre></figure><p>Leave out <code>--password</code> and it prompts for an app-specific password, then checks the credentials with Apple before saving them (<code>--validate</code> is the default).</p>\n<p><code>notarytool submit</code> takes a file, so the app goes up as a zip. <code>ditto -c -k --keepParent</code> makes a PKZip archive with <code>MyApp.app</code> as its top-level folder:</p>\n<figure class=\"code\" data-lang=\"bash\"><figcaption><span class=\"file\">release.sh</span><span class=\"lang\">Shell</span></figcaption><pre class=\"shiki shiki-themes github-light github-dark\" style=\"--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e\" tabindex=\"0\"><code><span class=\"line\"><span style=\"--shiki-light:#6F42C1;--shiki-dark:#B392F0\">notarize</span><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">() { </span><span style=\"--shiki-light:#6A737D;--shiki-dark:#6A737D\"># path to a .zip or .dmg</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#D73A49;--shiki-dark:#F97583\">  local</span><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\"> out id status</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">  out</span><span style=\"--shiki-light:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">$(</span><span style=\"--shiki-light:#6F42C1;--shiki-dark:#B392F0\">xcrun</span><span style=\"--shiki-light:#032F62;--shiki-dark:#9ECBFF\"> notarytool</span><span style=\"--shiki-light:#032F62;--shiki-dark:#9ECBFF\"> submit</span><span style=\"--shiki-light:#032F62;--shiki-dark:#9ECBFF\"> \"</span><span style=\"--shiki-light:#005CC5;--shiki-dark:#79B8FF\">$1</span><span style=\"--shiki-light:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"--shiki-light:#005CC5;--shiki-dark:#79B8FF\"> --keychain-profile</span><span style=\"--shiki-light:#032F62;--shiki-dark:#9ECBFF\"> notary-profile</span><span style=\"--shiki-light:#005CC5;--shiki-dark:#79B8FF\"> --wait</span><span style=\"--shiki-light:#D73A49;--shiki-dark:#F97583\"> 2>&#x26;1</span><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">  status</span><span style=\"--shiki-light:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">$(</span><span style=\"--shiki-light:#005CC5;--shiki-dark:#79B8FF\">echo</span><span style=\"--shiki-light:#032F62;--shiki-dark:#9ECBFF\"> \"</span><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">$out</span><span style=\"--shiki-light:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"--shiki-light:#D73A49;--shiki-dark:#F97583\"> |</span><span style=\"--shiki-light:#6F42C1;--shiki-dark:#B392F0\"> awk</span><span style=\"--shiki-light:#032F62;--shiki-dark:#9ECBFF\"> '/^ *status:/{s=$2} END{print s}'</span><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#D73A49;--shiki-dark:#F97583\">  if</span><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\"> [ </span><span style=\"--shiki-light:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">$status</span><span style=\"--shiki-light:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"--shiki-light:#D73A49;--shiki-dark:#F97583\"> !=</span><span style=\"--shiki-light:#032F62;--shiki-dark:#9ECBFF\"> \"Accepted\"</span><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\"> ]; </span><span style=\"--shiki-light:#D73A49;--shiki-dark:#F97583\">then</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">    id</span><span style=\"--shiki-light:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">$(</span><span style=\"--shiki-light:#005CC5;--shiki-dark:#79B8FF\">echo</span><span style=\"--shiki-light:#032F62;--shiki-dark:#9ECBFF\"> \"</span><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">$out</span><span style=\"--shiki-light:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"--shiki-light:#D73A49;--shiki-dark:#F97583\"> |</span><span style=\"--shiki-light:#6F42C1;--shiki-dark:#B392F0\"> awk</span><span style=\"--shiki-light:#032F62;--shiki-dark:#9ECBFF\"> '/^ *id:/{print $2; exit}'</span><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#005CC5;--shiki-dark:#79B8FF\">    echo</span><span style=\"--shiki-light:#032F62;--shiki-dark:#9ECBFF\"> \"ERROR: notarization of $(</span><span style=\"--shiki-light:#6F42C1;--shiki-dark:#B392F0\">basename</span><span style=\"--shiki-light:#032F62;--shiki-dark:#9ECBFF\"> \"</span><span style=\"--shiki-light:#005CC5;--shiki-dark:#79B8FF\">$1</span><span style=\"--shiki-light:#032F62;--shiki-dark:#9ECBFF\">\") was not accepted.\"</span><span style=\"--shiki-light:#D73A49;--shiki-dark:#F97583\"> >&#x26;2</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">    [ </span><span style=\"--shiki-light:#D73A49;--shiki-dark:#F97583\">-n</span><span style=\"--shiki-light:#032F62;--shiki-dark:#9ECBFF\"> \"</span><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">$id</span><span style=\"--shiki-light:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\"> ] &#x26;&#x26; </span><span style=\"--shiki-light:#6F42C1;--shiki-dark:#B392F0\">xcrun</span><span style=\"--shiki-light:#032F62;--shiki-dark:#9ECBFF\"> notarytool</span><span style=\"--shiki-light:#032F62;--shiki-dark:#9ECBFF\"> log</span><span style=\"--shiki-light:#032F62;--shiki-dark:#9ECBFF\"> \"</span><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">$id</span><span style=\"--shiki-light:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"--shiki-light:#005CC5;--shiki-dark:#79B8FF\"> --keychain-profile</span><span style=\"--shiki-light:#032F62;--shiki-dark:#9ECBFF\"> notary-profile</span><span style=\"--shiki-light:#D73A49;--shiki-dark:#F97583\"> >&#x26;2</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#005CC5;--shiki-dark:#79B8FF\">    exit</span><span style=\"--shiki-light:#005CC5;--shiki-dark:#79B8FF\"> 1</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#D73A49;--shiki-dark:#F97583\">  fi</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">}</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"--shiki-light:#6F42C1;--shiki-dark:#B392F0\">ditto</span><span style=\"--shiki-light:#005CC5;--shiki-dark:#79B8FF\"> -c</span><span style=\"--shiki-light:#005CC5;--shiki-dark:#79B8FF\"> -k</span><span style=\"--shiki-light:#005CC5;--shiki-dark:#79B8FF\"> --keepParent</span><span style=\"--shiki-light:#032F62;--shiki-dark:#9ECBFF\"> build/export/MyApp.app</span><span style=\"--shiki-light:#032F62;--shiki-dark:#9ECBFF\"> build/MyApp-notarize.zip</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#6F42C1;--shiki-dark:#B392F0\">notarize</span><span style=\"--shiki-light:#032F62;--shiki-dark:#9ECBFF\"> build/MyApp-notarize.zip</span></span></code></pre></figure><p><code>--wait</code> keeps the command running until the service finishes, so the script can move on to stapling. The function reads the <code>status:</code> line and stops on anything other than <code>Accepted</code>. When a submission fails, <code>notarytool log</code> prints the service’s JSON report, which names each file that failed and why, for example a nested binary with no secure timestamp.</p>\n<h2 id=\"staple-the-app-before-it-goes-in-the-dmg\" data-level=\"3\">Staple The App Before It Goes In The DMG</h2>\n<p>Notarization creates a ticket on Apple’s side. Without a stapled ticket, Gatekeeper has to look it up online the first time someone opens your app. Stapling attaches the ticket to the file so the check works with no network.</p>\n<p>Each stapled file only carries its own ticket. The DMG’s ticket stays with the DMG. When someone drags the app to Applications, the copy is still quarantined, Gatekeeper checks it again at first launch, and it needs a ticket of its own. So the app gets stapled first, while it’s still a plain folder on disk:</p>\n<figure class=\"code\" data-lang=\"bash\"><figcaption><span class=\"lang\">Shell</span></figcaption><pre class=\"shiki shiki-themes github-light github-dark\" style=\"--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e\" tabindex=\"0\"><code><span class=\"line\"><span style=\"--shiki-light:#6F42C1;--shiki-dark:#B392F0\">xcrun</span><span style=\"--shiki-light:#032F62;--shiki-dark:#9ECBFF\"> stapler</span><span style=\"--shiki-light:#032F62;--shiki-dark:#9ECBFF\"> staple</span><span style=\"--shiki-light:#032F62;--shiki-dark:#9ECBFF\"> build/export/MyApp.app</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#6F42C1;--shiki-dark:#B392F0\">xcrun</span><span style=\"--shiki-light:#032F62;--shiki-dark:#9ECBFF\"> stapler</span><span style=\"--shiki-light:#032F62;--shiki-dark:#9ECBFF\"> validate</span><span style=\"--shiki-light:#032F62;--shiki-dark:#9ECBFF\"> build/export/MyApp.app</span></span></code></pre></figure><p>The zip you uploaded is only a container for the upload. <code>stapler</code> lists the formats it supports as “UDIF disk images, code-signed executable bundles, and signed ‘flat’ installer packages”, so the ticket goes on the <code>.app</code> and the zip gets thrown away. In a bundle the ticket lands in <code>Contents/CodeResources</code>, which is why stapling has to happen before the app goes into a compressed, read-only DMG.</p>\n<h2 id=\"then-the-dmg-gets-the-same-treatment\" data-level=\"3\">Then The DMG Gets The Same Treatment</h2>\n<p>The DMG is the first thing Gatekeeper checks after a download, so it needs its own signature, notarization and ticket. A disk image isn’t executable code, so my scripts sign it with <code>--timestamp</code> and no runtime flag:</p>\n<figure class=\"code\" data-lang=\"bash\"><figcaption><span class=\"lang\">Shell</span></figcaption><pre class=\"shiki shiki-themes github-light github-dark\" style=\"--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e\" tabindex=\"0\"><code><span class=\"line\"><span style=\"--shiki-light:#6F42C1;--shiki-dark:#B392F0\">mkdir</span><span style=\"--shiki-light:#032F62;--shiki-dark:#9ECBFF\"> build/dmg-src</span><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\"> &#x26;&#x26; </span><span style=\"--shiki-light:#6F42C1;--shiki-dark:#B392F0\">ditto</span><span style=\"--shiki-light:#032F62;--shiki-dark:#9ECBFF\"> build/export/MyApp.app</span><span style=\"--shiki-light:#032F62;--shiki-dark:#9ECBFF\"> build/dmg-src/MyApp.app</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#6F42C1;--shiki-dark:#B392F0\">create-dmg</span><span style=\"--shiki-light:#005CC5;--shiki-dark:#79B8FF\"> --volname</span><span style=\"--shiki-light:#032F62;--shiki-dark:#9ECBFF\"> MyApp</span><span style=\"--shiki-light:#005CC5;--shiki-dark:#79B8FF\"> --app-drop-link</span><span style=\"--shiki-light:#005CC5;--shiki-dark:#79B8FF\"> 450</span><span style=\"--shiki-light:#005CC5;--shiki-dark:#79B8FF\"> 190</span><span style=\"--shiki-light:#032F62;--shiki-dark:#9ECBFF\"> build/MyApp.dmg</span><span style=\"--shiki-light:#032F62;--shiki-dark:#9ECBFF\"> build/dmg-src</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#6F42C1;--shiki-dark:#B392F0\">codesign</span><span style=\"--shiki-light:#005CC5;--shiki-dark:#79B8FF\"> --force</span><span style=\"--shiki-light:#005CC5;--shiki-dark:#79B8FF\"> --timestamp</span><span style=\"--shiki-light:#005CC5;--shiki-dark:#79B8FF\"> --sign</span><span style=\"--shiki-light:#032F62;--shiki-dark:#9ECBFF\"> \"</span><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">$IDENTITY</span><span style=\"--shiki-light:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"--shiki-light:#032F62;--shiki-dark:#9ECBFF\"> build/MyApp.dmg</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#6F42C1;--shiki-dark:#B392F0\">notarize</span><span style=\"--shiki-light:#032F62;--shiki-dark:#9ECBFF\"> build/MyApp.dmg</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#6F42C1;--shiki-dark:#B392F0\">xcrun</span><span style=\"--shiki-light:#032F62;--shiki-dark:#9ECBFF\"> stapler</span><span style=\"--shiki-light:#032F62;--shiki-dark:#9ECBFF\"> staple</span><span style=\"--shiki-light:#032F62;--shiki-dark:#9ECBFF\"> build/MyApp.dmg</span></span></code></pre></figure><p>Stapling writes into the DMG, so anything that hashes or signs the final file has to come after it. My scripts generate Sparkle’s EdDSA signature for the appcast right after stapling, from the finished DMG.</p>\n<h2 id=\"checking-what-people-download\" data-level=\"3\">Checking What People Download</h2>\n<p>My release scripts end with checks that ask the same questions Gatekeeper will. For the app:</p>\n<figure class=\"code\" data-lang=\"bash\"><figcaption><span class=\"lang\">Shell</span></figcaption><pre class=\"shiki shiki-themes github-light github-dark\" style=\"--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e\" tabindex=\"0\"><code><span class=\"line\"><span style=\"--shiki-light:#6F42C1;--shiki-dark:#B392F0\">spctl</span><span style=\"--shiki-light:#005CC5;--shiki-dark:#79B8FF\"> -a</span><span style=\"--shiki-light:#005CC5;--shiki-dark:#79B8FF\"> -vvv</span><span style=\"--shiki-light:#005CC5;--shiki-dark:#79B8FF\"> -t</span><span style=\"--shiki-light:#032F62;--shiki-dark:#9ECBFF\"> exec</span><span style=\"--shiki-light:#032F62;--shiki-dark:#9ECBFF\"> MyApp.app</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#6F42C1;--shiki-dark:#B392F0\">xcrun</span><span style=\"--shiki-light:#032F62;--shiki-dark:#9ECBFF\"> stapler</span><span style=\"--shiki-light:#032F62;--shiki-dark:#9ECBFF\"> validate</span><span style=\"--shiki-light:#032F62;--shiki-dark:#9ECBFF\"> MyApp.app</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#6F42C1;--shiki-dark:#B392F0\">codesign</span><span style=\"--shiki-light:#005CC5;--shiki-dark:#79B8FF\"> --verify</span><span style=\"--shiki-light:#005CC5;--shiki-dark:#79B8FF\"> --deep</span><span style=\"--shiki-light:#005CC5;--shiki-dark:#79B8FF\"> --strict</span><span style=\"--shiki-light:#005CC5;--shiki-dark:#79B8FF\"> --verbose=2</span><span style=\"--shiki-light:#032F62;--shiki-dark:#9ECBFF\"> MyApp.app</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#6F42C1;--shiki-dark:#B392F0\">codesign</span><span style=\"--shiki-light:#005CC5;--shiki-dark:#79B8FF\"> -dvv</span><span style=\"--shiki-light:#032F62;--shiki-dark:#9ECBFF\"> MyApp.app</span><span style=\"--shiki-light:#D73A49;--shiki-dark:#F97583\"> 2>&#x26;1</span><span style=\"--shiki-light:#D73A49;--shiki-dark:#F97583\"> |</span><span style=\"--shiki-light:#6F42C1;--shiki-dark:#B392F0\"> grep</span><span style=\"--shiki-light:#005CC5;--shiki-dark:#79B8FF\"> -E</span><span style=\"--shiki-light:#032F62;--shiki-dark:#9ECBFF\"> \"flags|Timestamp|Notarization\"</span></span></code></pre></figure><p>A good result says <code>accepted</code> with <code>source=Notarized Developer ID</code>, then “The validate action worked!”, then <code>valid on disk</code> and <code>satisfies its Designated Requirement</code>. Here <code>--deep</code> checks every nested piece in full, where the default is only a shallow pass. The last line should show <code>flags=0x10000(runtime)</code>, a <code>Timestamp=</code> line and <code>Notarization Ticket=stapled</code>.</p>\n<p>The DMG needs a different assessment type. The default <code>execute</code> type rejects it with “the code is valid but does not seem to be an app”. <code>-t open</code> on its own comes back <code>rejected</code> with <code>source=Insufficient Context</code>. <code>--context</code> isn’t in <code>man spctl</code>, but this is the form that comes back <code>accepted</code>:</p>\n<figure class=\"code\" data-lang=\"bash\"><figcaption><span class=\"lang\">Shell</span></figcaption><pre class=\"shiki shiki-themes github-light github-dark\" style=\"--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e\" tabindex=\"0\"><code><span class=\"line\"><span style=\"--shiki-light:#6F42C1;--shiki-dark:#B392F0\">spctl</span><span style=\"--shiki-light:#005CC5;--shiki-dark:#79B8FF\"> -a</span><span style=\"--shiki-light:#005CC5;--shiki-dark:#79B8FF\"> -vvv</span><span style=\"--shiki-light:#005CC5;--shiki-dark:#79B8FF\"> -t</span><span style=\"--shiki-light:#032F62;--shiki-dark:#9ECBFF\"> open</span><span style=\"--shiki-light:#005CC5;--shiki-dark:#79B8FF\"> --context</span><span style=\"--shiki-light:#032F62;--shiki-dark:#9ECBFF\"> context:primary-signature</span><span style=\"--shiki-light:#032F62;--shiki-dark:#9ECBFF\"> MyApp.dmg</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#6F42C1;--shiki-dark:#B392F0\">xcrun</span><span style=\"--shiki-light:#032F62;--shiki-dark:#9ECBFF\"> stapler</span><span style=\"--shiki-light:#032F62;--shiki-dark:#9ECBFF\"> validate</span><span style=\"--shiki-light:#032F62;--shiki-dark:#9ECBFF\"> MyApp.dmg</span></span></code></pre></figure><p>On my Mac, <code>-t install</code> also accepts a signed DMG, though <code>man spctl</code> describes that type as the one for installer packages, so my scripts stick to <code>-t open</code>. An unsigned DMG fails both with <code>source=no usable signature</code>.</p>\n<p>Last, check the app as the buyer gets it, from inside the mounted DMG:</p>\n<figure class=\"code\" data-lang=\"bash\"><figcaption><span class=\"lang\">Shell</span></figcaption><pre class=\"shiki shiki-themes github-light github-dark\" style=\"--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e\" tabindex=\"0\"><code><span class=\"line\"><span style=\"--shiki-light:#6F42C1;--shiki-dark:#B392F0\">hdiutil</span><span style=\"--shiki-light:#032F62;--shiki-dark:#9ECBFF\"> attach</span><span style=\"--shiki-light:#005CC5;--shiki-dark:#79B8FF\"> -nobrowse</span><span style=\"--shiki-light:#005CC5;--shiki-dark:#79B8FF\"> -quiet</span><span style=\"--shiki-light:#032F62;--shiki-dark:#9ECBFF\"> build/MyApp.dmg</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#6F42C1;--shiki-dark:#B392F0\">xcrun</span><span style=\"--shiki-light:#032F62;--shiki-dark:#9ECBFF\"> stapler</span><span style=\"--shiki-light:#032F62;--shiki-dark:#9ECBFF\"> validate</span><span style=\"--shiki-light:#032F62;--shiki-dark:#9ECBFF\"> /Volumes/MyApp/MyApp.app</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#6F42C1;--shiki-dark:#B392F0\">spctl</span><span style=\"--shiki-light:#005CC5;--shiki-dark:#79B8FF\"> -a</span><span style=\"--shiki-light:#005CC5;--shiki-dark:#79B8FF\"> -vvv</span><span style=\"--shiki-light:#032F62;--shiki-dark:#9ECBFF\"> /Volumes/MyApp/MyApp.app</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#6F42C1;--shiki-dark:#B392F0\">hdiutil</span><span style=\"--shiki-light:#032F62;--shiki-dark:#9ECBFF\"> detach</span><span style=\"--shiki-light:#032F62;--shiki-dark:#9ECBFF\"> /Volumes/MyApp</span><span style=\"--shiki-light:#005CC5;--shiki-dark:#79B8FF\"> -quiet</span></span></code></pre></figure><p>If the app was stapled before packaging, <code>stapler validate</code> passes here too. If it fails, the app in the DMG has no ticket of its own, and Gatekeeper can’t confirm it was notarized when someone opens it for the first time without a network connection.</p>","created_at":"2026-11-05T14:00:00.000Z","updated_at":"2026-11-05T14:00:00.000Z"}