Skip to content

fix: terminal install works with curl alone by trusting the repository pin #2080

Description

@qua-hatayama

Summary

The documented terminal install requires gh (logged in) and jq. That is a high barrier for a first-time user who just wants the CLI. Make the default terminal install need nothing beyond curl and a shell — without weakening first-install authenticity — by reusing the digest source the Unity GUI path already trusts: project-runner-pin.json.

Target UX:

# macOS / Linux / Windows Git Bash
curl -fsSL https://raw.githubusercontent.com/hatayama/unity-cli-loop/main/scripts/install.sh | sh
# Windows PowerShell
irm https://raw.githubusercontent.com/hatayama/unity-cli-loop/main/scripts/install.ps1 | iex

Current state

scripts/install.sh itself needs neither gh nor jq. Its dependencies are curl, shasum/sha256sum, and awk:

  • find_latest_asset_url (scripts/install.sh:284) resolves the newest matching release by scanning the releases API with curl + awk.
  • Download and same-origin checksum comparison use curl and shasum/sha256sum.

The single blocker is that a first install hard-fails without a trusted digest list: verify_archive_attestation_manifest (scripts/install.sh:485) exits with Attestation manifest is required when ULOOP_ARCHIVE_MANIFEST is unset (scripts/install.ps1:515 enforces the same). Producing that value is exactly what the README's gh attestation verify + jq block does. So gh/jq are not installer dependencies — they are the only currently documented way to supply one mandatory installer input.

That requirement must not simply be dropped: the .sha256 file lives in the same release as the archive, so an attacker who can replace one can replace both. ULOOP_ARCHIVE_MANIFEST is the authentication of a first install.

Proposal: source the digest manifest from the repository pin

Packages/src/project-runner-pin.json already carries dispatcherReleaseTag and dispatcherArchiveManifest, stamped by release automation and enforced by CI: VerifyDispatcherPinSubjects requires the manifest to match the published release's verified attestation subjects exactly (see docs/project-runner-pin.md, docs/dispatcher-pin-release-order.md). This is precisely why the Unity Install CLI button achieves full verification with zero extra tools.

Make the terminal path symmetrical:

  1. The user fetches install.sh from the git repository (raw.githubusercontent.com, protected main), not from a release asset.
  2. When ULOOP_ARCHIVE_MANIFEST is not supplied, the installer fetches Packages/src/project-runner-pin.json from the same repository ref it was itself fetched from, and uses dispatcherArchiveManifest as the trusted digest list and dispatcherReleaseTag as the version to install.
  3. An explicitly supplied ULOOP_ARCHIVE_MANIFEST (the dispatcher self-update path, and the documented gh attestation verify flow) continues to take precedence unchanged.
  4. Neither present → fail, exactly as today.

Security analysis

  • No new trust model. "Trust the repository contents over HTTPS" is already the accepted baseline of the Unity GUI path (package pin → digests). The terminal path merely becomes symmetrical with it; the system's weakest link does not move.
  • Release-asset compromise is still detected. The digest list comes from the git repository (protected branch + CI verification), not from the release. An attacker who can rewrite release assets — installer, archives, .sha256 — cannot make the repository-sourced manifest match. This is the decisive advantage over stamping digests into the release-asset installer, which that attacker could re-stamp consistently.
  • Deterministic version. The installer stops resolving "latest" by default and installs the pinned dispatcherReleaseTag — the same version the GUI path would install.
  • Attestation flow remains the hardened option. The current gh attestation verify block moves to an optional "verify the installer yourself" subsection for security-conscious users and CI.

Residual gap versus mandatory attestation: sigstore verification chains to the signing workflow run itself, repository trust chains to GitHub's branch protection. That gap already exists in the shipping GUI path, so this change introduces no regression anywhere.

Work involved

  • Installer changes (scripts/install.sh, scripts/install.ps1): pin fetch + parse (awk / native PowerShell JSON), env-var precedence, fail-closed when both sources are absent. Both files are shared release inputs — scripts/stamp-release-inputs.sh and the check-release-triggers CI gate apply (docs/shared-release-inputs.md).
  • Decide the ref the installer reads the pin from: pinning to the installer's own fetched ref (main in the documented command) is the natural choice; a ULOOP_REF override enables installing from a tag.
  • README install section rewrite in both languages: the one-line default up front, gh/jq prerequisites deleted, attestation flow demoted to an optional subsection.
  • Windows parity per docs/windows-compatibility.md. The PowerShell one-liner requires install.ps1 to stay executable when piped through iex (no top-level param block, no $PSScriptRoot dependence, configuration via environment variables only). The script already satisfies all three today — keep it that way and verify the irm | iex form end to end on Windows as part of acceptance.
  • Tests: explicit manifest wins; pin-sourced manifest used when the env var is absent; both absent fails; pin digest mismatch fails; pin missing dispatcherArchiveManifest fails.

Out of scope / follow-up

Homebrew tap and winget/scoop manifests share the same trust-separation structure (digests committed to a separate repository) and would further simplify installs, but they are distribution additions, not prerequisites for this fix.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions