Skip to content

macOS binaries are unsigned and unnotarized: browser downloads are blocked by Gatekeeper #2523

Description

@adamtheturtle

vws-macos, vuforia-cloud-reco-macos and vumark-macos are ad-hoc signed by PyInstaller, so they run, but they are not signed with a Developer ID certificate and not notarized. Anyone who downloads them from the releases page in a browser gets a com.apple.quarantine attribute and is blocked by Gatekeeper, which is why docs/source/install.rst carries an xattr -d com.apple.quarantine workaround for all three.

Nothing is currently blocked by this, and there are no user reports. The Homebrew tap is unaffected: its formula builds from PyPI source, so Gatekeeper never sees a downloaded binary. The documented curl install is also unaffected, since curl does not set the quarantine attribute.

Copy the literalizer-cli implementation

This was solved in adamtheturtle/literalizer-cli#605, in PR adamtheturtle/literalizer-cli#611. Port that build-macos job rather than reimplementing it. Take:

  • the build-macos steps from .github/workflows/release.yml, which import the certificate with a commit-pinned Apple-Actions/import-codesign-certs, sign, smoke-test, notarize with notarytool, and assess with spctl;
  • bin/entitlements.plist verbatim;
  • the "Repository secrets" section of docs/source/release-process.rst.

Several details in that implementation look removable but are not, and each was arrived at by testing rather than reasoning:

  • The entitlements file is what makes the binary run at all. The hardened runtime, which notarization requires, turns on library validation, and a --onefile binary unpacks and loads its own libpython3.*.dylib at startup, still carrying PyInstaller's ad-hoc signature. Without com.apple.security.cs.disable-library-validation the signed binary dies immediately with "mapping process and mapped file (non-platform) have different Team IDs". That single entitlement is sufficient; the wider sets often recommended for Python apps are not needed. This was confirmed on Python 3.13 and PyInstaller 6.12.0; this repository builds with 3.14 and 6.22.1, which the smoke run below is what actually verifies.
  • There is deliberately no codesign --keychain. The flag silently falls back to the keychain search list when it cannot resolve what it is given — a path that cannot exist still signs successfully — so it implies a guarantee it does not provide.
  • notarytool does not reliably exit non-zero on a rejected submission, hence the status: Accepted check, and the notarytool log call that reports why a rejection happened.
  • The smoke run and the spctl assessment are the only things that catch the two silent failure modes: a signed binary that cannot start, and notarization that did not take effect.

Where this repository differs: three binaries

  • Each binary needs its own codesign call and its own smoke run. A passing vws-macos says nothing about vumark-macos.
  • They do not need three notarization submissions. notarytool accepts a zip containing several Mach-O executables and issues a ticket for each, so one ditto -c -k --sequesterRsrc over all three and one submit --wait covers the set.
  • Each still needs its own spctl --assess --type exec check afterwards.

Stapling

A notarization ticket cannot be stapled to a bare Mach-O executable — stapler only handles .app bundles, .dmg, .pkg and kexts. So Gatekeeper verifies online, and a browser-downloaded binary needs a network connection on first run. That is the accepted tradeoff: shipping a .pkg instead would allow stapling and work offline, but would replace the curl … && chmod +x one-liners with an installer.

Prerequisites

  • DEVELOPER_ID_APP_CERT_P12_BASE64 and DEVELOPER_ID_APP_CERT_PASSWORD — already set on this repository
  • ASC_KEY, ASC_KEY_ID and ASC_ISSUER_ID — not set anywhere yet

Worth deciding before setting those: the existing certificate is a personal Developer ID Application identity, so binaries published under the VWS-Python organization would carry a personal team identifier in their signature, which codesign -dv and Gatekeeper both surface. Reusing it is the cheap option and matches the other projects. The alternative is an organization Apple Developer Program membership and a separate certificate, which is a real cost and a second set of credentials to rotate.

Note that build publishes to PyPI and pushes the tag before build-macos runs, so releasing before the notarization secrets are set leaves a published release with no macOS binary attached at all.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions