[GH-ISSUE #1681] [Design] Pin the Hopper publisher identity in the macOS installer #564

Closed
opened 2026-10-15 18:26:11 +00:00 by bsakaguchi · 1 comment
Owner

Originally created by @QuentinMoss on GitHub (Oct 11, 2026).
Original GitHub issue: https://github.com/morluto/rea/issues/1681

Before submitting

  • I searched the open and closed issues and pull requests for a related discussion.
  • I removed credentials, tokens, private paths, and sensitive target data.

Decision needed

Should rea setup --install-hopper on macOS verify who signed the downloaded Hopper app, and if so, how should that publisher identity be owned and kept current?

The proposal: pin an Apple code requirement on Hopper's team ID (one owned constant), report a distinct publisher_mismatch failure, and add a scheduled canary that exercises the real macOS install path so vendor-side changes surface in CI before users hit them.

Context and evidence

All references are to 8c96baab.

Observed

  • The installer fetches release metadata from files-api.php, downloads the DMG, and checks its length and SHA-1 against that same metadata response (MacHopper.ts#L99, #L273-L279).
  • validBundle runs codesign --verify --deep --strict with no -R requirement, then compares CFBundleIdentifier to com.cryptic-apps.hopper-web-4 (#L177-L198). If both pass, the app is copied to ~/Applications and launched with open (#L119-L125).
  • Reproduced locally (macOS, Darwin 27): a one-line C program in an .app with CFBundleIdentifier = com.cryptic-apps.hopper-web-4, ad-hoc signed (codesign -s -), passes both current checks. The same bundle fails codesign --verify --deep --strict -R='anchor apple generic and certificate leaf[subject.OU] = "<TEAM>"'.
  • Linux pins exact artifacts instead: version 6.4.2, per-package hashes, and launcher digests (LinuxHopper.ts#L14-L41).
  • No workflow runs the macOS installer against the real download. real-hopper.yml runs verify:hopper on a self-hosted runner with Hopper preinstalled; the clean-setup lane exists only in real-hopper-linux.yml. scripts/verify-package-setup.mjs checks that setup plans install_hopper but does not apply it. installMacHopper is otherwise covered only by the fake-host tests in MacHopper.test.ts.
  • On 2026-10-10, a plain curl request to the files-api.php URL returned HTTP 403. This was one request with curl's default user agent; the installer sends user-agent: rea-installer and I did not test that.

Derived

  • The SHA-1 comes from the same origin as the DMG, so it detects corruption, not substitution. Trust in the installed app currently rests on TLS to hopperapp.com plus a bundle-ID string, which any signer can set.
  • If the metadata or download response is ever attacker-controlled (vendor server or CDN compromise), REA would install and launch any validly signed or ad-hoc-signed app that uses Hopper's bundle ID.

Inferred

  • Apple team IDs belong to the developer account and persist across certificate renewals and app versions, so a team pin should not need per-release maintenance. That makes it a better fit than Linux-style pins for a path that intentionally tracks the latest release.

I'm treating this as hardening rather than a vulnerability report: exploiting it requires compromising the vendor's distribution, and the code is public. If maintainers would rather handle it as a private advisory, I'm happy to move it there.

Boundary and invariants

  • Owner: src/application/MacHopper.ts, behind the existing MacHopperInstallHost seam. The identity constant is provider-specific and stays out of domain/.
  • One record: a single Hopper publisher identity (bundle ID and team ID) that every check reads, beside the existing BUNDLE_IDENTIFIER.
  • Ordering invariant (already true, must be kept): nothing from the mounted DMG is copied or launched until verification passes.
  • Disclosure: macHopperInstallDisclosure.commands lists the codesign command, so setup --dry-run should show the exact requirement that will be enforced.
  • Failure reason: add publisher_mismatch to MacHopperInstallResult, distinct from bundle, and carry the expected and observed team through SetupHost into the CLI/MCP setup result.
  • Recovery: no override flag. A user blocked by a publisher change installs Hopper manually and points REA at it with the existing HOPPER_LAUNCHER_PATH.

Options and tradeoffs

Option Protection Maintenance Notes
A. Status quo Bundle-ID string only None Accepts ad-hoc and any team.
B. Team-ID code requirement (proposed) Rejects any signer other than Hopper's team One constant; changes only if the publisher account changes Fails closed if the vendor changes team; recovery via HOPPER_LAUNCHER_PATH. Offline and deterministic.
C. Exact artifact pins (Linux model) Strongest A REA release for every Hopper release Gives up tracking latest on macOS.
D. spctl --assess (Gatekeeper/notarization) Requires notarized Developer ID, any team None Accepts any notarized developer; depends on Gatekeeper state and possibly network. Could complement B, not replace it.

Success criteria and open questions

The decision is settled when:

  1. A bundle with Hopper's bundle ID but a different or ad-hoc signer fails with publisher_mismatch and is never copied or launched. This is covered by a fake-host unit test plus a macOS boundary test that uses an ad-hoc-signed fixture app.
  2. A real install of the current Hopper release passes on a clean macOS runner.
  3. A scheduled canary (for example weekly) runs the real macOS install path through verification without launching Hopper, and fails visibly if the team ID, bundle ID, release API, or download behavior changes.

Open questions that could change the choice:

  1. Hopper's team ID. This should be captured from the official artifact in a maintainer-controlled environment (the canary, or the self-hosted Hopper runner) and recorded with its source, not supplied by the reporter.
  2. Canary permission. Is a scheduled download of the Hopper demo from a GitHub-hosted runner acceptable to the vendor in frequency and terms?
  3. Scope. Should the same publisher record also gate detection of an already-installed Hopper, or only installation?
  4. Canary failures. On failure, should the canary only fail the workflow, or also open an issue automatically?

References

Originally created by @QuentinMoss on GitHub (Oct 11, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/1681 ### Before submitting - [x] I searched the open and closed issues and pull requests for a related discussion. - [x] I removed credentials, tokens, private paths, and sensitive target data. ### Decision needed Should `rea setup --install-hopper` on macOS verify **who signed** the downloaded Hopper app, and if so, how should that publisher identity be owned and kept current? The proposal: pin an Apple code requirement on Hopper's team ID (one owned constant), report a distinct `publisher_mismatch` failure, and add a scheduled canary that exercises the real macOS install path so vendor-side changes surface in CI before users hit them. ### Context and evidence All references are to `8c96baab`. **Observed** - The installer fetches release metadata from `files-api.php`, downloads the DMG, and checks its length and SHA-1 against that same metadata response ([MacHopper.ts#L99](https://github.com/morluto/rea/blob/8c96baab80dc258d31ca1632d761fffb11dbaaae/src/application/MacHopper.ts#L99), [#L273-L279](https://github.com/morluto/rea/blob/8c96baab80dc258d31ca1632d761fffb11dbaaae/src/application/MacHopper.ts#L273-L279)). - `validBundle` runs `codesign --verify --deep --strict` with no `-R` requirement, then compares `CFBundleIdentifier` to `com.cryptic-apps.hopper-web-4` ([#L177-L198](https://github.com/morluto/rea/blob/8c96baab80dc258d31ca1632d761fffb11dbaaae/src/application/MacHopper.ts#L177-L198)). If both pass, the app is copied to `~/Applications` and launched with `open` ([#L119-L125](https://github.com/morluto/rea/blob/8c96baab80dc258d31ca1632d761fffb11dbaaae/src/application/MacHopper.ts#L119-L125)). - Reproduced locally (macOS, Darwin 27): a one-line C program in an `.app` with `CFBundleIdentifier = com.cryptic-apps.hopper-web-4`, **ad-hoc signed** (`codesign -s -`), passes both current checks. The same bundle fails `codesign --verify --deep --strict -R='anchor apple generic and certificate leaf[subject.OU] = "<TEAM>"'`. - Linux pins exact artifacts instead: version 6.4.2, per-package hashes, and launcher digests ([LinuxHopper.ts#L14-L41](https://github.com/morluto/rea/blob/8c96baab80dc258d31ca1632d761fffb11dbaaae/src/application/LinuxHopper.ts#L14-L41)). - No workflow runs the macOS installer against the real download. `real-hopper.yml` runs `verify:hopper` on a self-hosted runner with Hopper preinstalled; the clean-setup lane exists only in `real-hopper-linux.yml`. `scripts/verify-package-setup.mjs` checks that setup *plans* `install_hopper` but does not apply it. `installMacHopper` is otherwise covered only by the fake-host tests in `MacHopper.test.ts`. - On 2026-10-10, a plain `curl` request to the `files-api.php` URL returned HTTP 403. This was one request with curl's default user agent; the installer sends `user-agent: rea-installer` and I did not test that. **Derived** - The SHA-1 comes from the same origin as the DMG, so it detects corruption, not substitution. Trust in the installed app currently rests on TLS to `hopperapp.com` plus a bundle-ID string, which any signer can set. - If the metadata or download response is ever attacker-controlled (vendor server or CDN compromise), REA would install and launch any validly signed or ad-hoc-signed app that uses Hopper's bundle ID. **Inferred** - Apple team IDs belong to the developer account and persist across certificate renewals and app versions, so a team pin should not need per-release maintenance. That makes it a better fit than Linux-style pins for a path that intentionally tracks the latest release. I'm treating this as hardening rather than a vulnerability report: exploiting it requires compromising the vendor's distribution, and the code is public. If maintainers would rather handle it as a private advisory, I'm happy to move it there. ### Boundary and invariants - **Owner:** `src/application/MacHopper.ts`, behind the existing `MacHopperInstallHost` seam. The identity constant is provider-specific and stays out of `domain/`. - **One record:** a single Hopper publisher identity (bundle ID and team ID) that every check reads, beside the existing `BUNDLE_IDENTIFIER`. - **Ordering invariant (already true, must be kept):** nothing from the mounted DMG is copied or launched until verification passes. - **Disclosure:** `macHopperInstallDisclosure.commands` lists the `codesign` command, so `setup --dry-run` should show the exact requirement that will be enforced. - **Failure reason:** add `publisher_mismatch` to `MacHopperInstallResult`, distinct from `bundle`, and carry the expected and observed team through `SetupHost` into the CLI/MCP setup result. - **Recovery:** no override flag. A user blocked by a publisher change installs Hopper manually and points REA at it with the existing `HOPPER_LAUNCHER_PATH`. ### Options and tradeoffs | Option | Protection | Maintenance | Notes | |---|---|---|---| | **A. Status quo** | Bundle-ID string only | None | Accepts ad-hoc and any team. | | **B. Team-ID code requirement** (proposed) | Rejects any signer other than Hopper's team | One constant; changes only if the publisher account changes | Fails closed if the vendor changes team; recovery via `HOPPER_LAUNCHER_PATH`. Offline and deterministic. | | **C. Exact artifact pins (Linux model)** | Strongest | A REA release for every Hopper release | Gives up tracking latest on macOS. | | **D. `spctl --assess` (Gatekeeper/notarization)** | Requires notarized Developer ID, any team | None | Accepts any notarized developer; depends on Gatekeeper state and possibly network. Could complement B, not replace it. | ### Success criteria and open questions The decision is settled when: 1. A bundle with Hopper's bundle ID but a different or ad-hoc signer fails with `publisher_mismatch` and is never copied or launched. This is covered by a fake-host unit test plus a macOS boundary test that uses an ad-hoc-signed fixture app. 2. A real install of the current Hopper release passes on a clean macOS runner. 3. A scheduled canary (for example weekly) runs the real macOS install path through verification without launching Hopper, and fails visibly if the team ID, bundle ID, release API, or download behavior changes. Open questions that could change the choice: 1. **Hopper's team ID.** This should be captured from the official artifact in a maintainer-controlled environment (the canary, or the self-hosted Hopper runner) and recorded with its source, not supplied by the reporter. 2. **Canary permission.** Is a scheduled download of the Hopper demo from a GitHub-hosted runner acceptable to the vendor in frequency and terms? 3. **Scope.** Should the same publisher record also gate detection of an already-installed Hopper, or only installation? 4. **Canary failures.** On failure, should the canary only fail the workflow, or also open an issue automatically? ### References - Installer: [`src/application/MacHopper.ts`](https://github.com/morluto/rea/blob/8c96baab80dc258d31ca1632d761fffb11dbaaae/src/application/MacHopper.ts), tests in `src/application/MacHopper.test.ts` - Linux pin model: [`src/application/LinuxHopper.ts`](https://github.com/morluto/rea/blob/8c96baab80dc258d31ca1632d761fffb11dbaaae/src/application/LinuxHopper.ts) - Workflows: `.github/workflows/real-hopper.yml`, `.github/workflows/real-hopper-linux.yml` - Apple: [Code Signing Requirement Language](https://developer.apple.com/library/archive/documentation/Security/Conceptual/CodeSigningGuide/RequirementLang/RequirementLang.html), `man codesign` (`-R` / `--test-requirement`)
Author
Owner

@matbrgz commented on GitHub (Oct 12, 2026):

Security & Installer Provenance Review

Validating Apple code-signing publisher identity on downloaded DMGs during rea setup --install-hopper closes an important supply-chain verification gap:

  • Verification Contract: Verifying the Apple Developer Team ID via codesign -R '= identifier "com.cryptic-apps.hopper-web-4" and anchor apple generic and certificate leaf[subject.OU] = "..."' -v <appPath> guarantees that both the bundle ID and the signing team match Cryptic Apps.
  • Fail-Closed Reporting: If Apple signature verification fails or the Team ID mismatches, emit publisher_verification_failed with the observed subject rather than proceeding with staging.
  • Canary CI: A scheduled monthly/bi-weekly CI canary job downloading the official DMG ensures upstream publisher key rotations are caught proactively.
<!-- gh-comment-id:6134641510 --> @matbrgz commented on GitHub (Oct 12, 2026): ### Security & Installer Provenance Review Validating Apple code-signing publisher identity on downloaded DMGs during `rea setup --install-hopper` closes an important supply-chain verification gap: - **Verification Contract:** Verifying the Apple Developer Team ID via `codesign -R '= identifier "com.cryptic-apps.hopper-web-4" and anchor apple generic and certificate leaf[subject.OU] = "..."' -v <appPath>` guarantees that both the bundle ID and the signing team match Cryptic Apps. - **Fail-Closed Reporting:** If Apple signature verification fails or the Team ID mismatches, emit `publisher_verification_failed` with the observed subject rather than proceeding with staging. - **Canary CI:** A scheduled monthly/bi-weekly CI canary job downloading the official DMG ensures upstream publisher key rotations are caught proactively.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
morluto/rea#564
No description provided.