Notifio is an Electron desktop app that watches rental search pages and alerts you when a new listing appears. Yesterday I diffed its Info.plist against the stock Electron one and found that 21 of its 32 keys had never been touched by us. One of the keys that had been touched was ElectronAsarIntegrity, a SHA-256 of the archive holding all of our compiled code.
A hash in a metadata file is only worth something if something protects the metadata file. So I went and asked the signature.
codesign -dvvv release/mac-arm64/Notifio.app
Executable=.../Notifio.app/Contents/MacOS/Notifio
Identifier=Electron
Format=app bundle with Mach-O thin (arm64)
CodeDirectory v=20400 size=392 flags=0x20002(adhoc,linker-signed) hashes=9+0
CDHash=7d6ae325aa761148157f02805fe951d5269a3807
Signature=adhoc
Info.plist=not bound
TeamIdentifier=not set
Sealed Resources=none
Internal requirements=none
Seven lines of that output are the whole story of what a Mac user can and cannot verify about our app.
Identifier=Electron
The code-signing identifier is not app.notifio. It is Electron.
That is not electron-builder failing to rename something it renames elsewhere. It is the signature that Apple's linker applies to every arm64 binary at build time, on Electron's build machine, months ago, and it names the binary that was built there. Nothing in our pipeline re-signs it, so the identity in the signature is the identity of the upstream project.
The same pattern runs through the whole bundle. Here is every code bundle inside Contents/Frameworks, the name on disk against the name in its signature:
on disk signing identifier
Notifio Helper.app Electron Helper
Notifio Helper (GPU).app Electron Helper (GPU)
Notifio Helper (Plugin).app Electron Helper (Plugin)
Notifio Helper (Renderer).app Electron Helper (Renderer)
Electron Framework.framework Electron Framework
Mantle.framework Mantle
ReactiveObjC.framework ReactiveObjC
Squirrel.framework Squirrel
Five bundles were renamed for the user's benefit, in Activity Monitor and in the process list, and all five still identify as Electron to the operating system's security machinery. Both halves of that are correct in their own terms, which is exactly why it is worth writing down: the name a person reads and the name a policy matches on are different strings, and only one of them was updated.
As an aside, three of those frameworks are Squirrel and its two dependencies, 752 KB of the Mac auto-update stack, in an app where reinstalling is the update mechanism. Electron bundles them unconditionally.
The launcher is not ours in any sense
shasum -a 256 \
release/mac-arm64/Notifio.app/Contents/MacOS/Notifio \
node_modules/.pnpm/electron@43.1.1/node_modules/electron/dist/Electron.app/Contents/MacOS/Electron
997d6e26011b89344f98626987155158f13f492148f79bc00f24d841ae693b60 Notifio
997d6e26011b89344f98626987155158f13f492148f79bc00f24d841ae693b60 Electron
Identical. Both 33,968 bytes. The CDHash printed in the signature above is the same on both as well, which follows: our main executable is the stock Electron launcher with a different filename, and a rename does not change a file's bytes.
This is how Electron works and it is fine. The 33 KB launcher's job is to load the 274 MB framework next to it, which reads app.asar, which is our product. What it means for verification is that the only thing in the bundle with a signature anybody could ever trust is a file that contains none of our code.
Three lines that say nothing is sealed
Info.plist=not bound
Sealed Resources=none
Internal requirements=none
A real signature covers a _CodeSignature/CodeResources file listing every resource and its hash, and it binds Info.plist into the signed code directory. This one does neither, because a linker-applied ad-hoc signature covers the executable it was applied to and nothing else.
Which answers the question I started with. The SHA-256 of app.asar sitting in our Info.plist is not protected by anything. Anyone who can write to the bundle can change app.asar, recompute the hash, write it into the plist, and the app will launch and verify its own integrity against the number the attacker just typed. Electron's asar integrity feature is explicit that it depends on the plist being signed. Ours is not signed, so the feature is decorative.
To be clear about the threat model: someone who can write into /Applications/Notifio.app on your Mac has already won several rounds before this one. The point is narrower. We ship an integrity mechanism, that mechanism has a prerequisite, we do not meet the prerequisite, and nothing in the build says so.
Gatekeeper cannot even form an opinion
codesign --verify --deep --strict -vv release/mac-arm64/Notifio.app
spctl -a -vv release/mac-arm64/Notifio.app
Both answer the same way:
Notifio.app: code has no resources but signature indicates they must be present
Not "rejected", not "unidentified developer", but a refusal to assess. The structure the signature promises is absent, so the check stops before it gets to policy.
What a user downloading the Mac DMG sees is the quarantine dialog: macOS cannot verify the developer, open anyway or move to bin. RELEASES.md has known this for months, in a parenthesis about re-installing:
(Unsigned builds re-trigger the Gatekeeper prompt)
And here is the gap I did not expect to find. The download page walks a new user through four numbered steps: Download, Install and launch, Activate, Start monitoring. Step 2 reads "Open the installer and launch Notifio." Nothing on that page, and none of the six questions on the help page, mentions the dialog that stands between step 1 and step 2 on every Mac we ship to. The one document that admits the warning exists is a build doc in the repository that no customer will ever read.
What there is to verify, counted
A user who wants to check that the file they downloaded is the file we built has these options:
Developer ID signature none
Notarisation ticket none
Published SHA-256 none, on any of our 38 pages
Signature on the plist not bound
asar integrity hash present, unprotected
I grepped every page of the live site for sha256, checksum, signature and notarised. Zero hits. So the honest summary is that the integrity story for the Mac build is one hash that protects itself, and that is a thing worth knowing about your own release pipeline even when the fix is "pay Apple 99 dollars a year", which is the actual fix and is on the list.
Two things that do not need the Developer Programme and will go out first: a sha256sum printed next to each download button, and one sentence on the download page telling Mac users what they are about to see and why. A scary dialog with no explanation is worse than a scary dialog with one.
Top comments (0)