Skip to content

fix(dispatcher): Verify dispatcher archive attestation before installer runs#1673

Merged
hatayama merged 2 commits into
v3-betafrom
feat/attestation-verify-archive
Jul 10, 2026
Merged

fix(dispatcher): Verify dispatcher archive attestation before installer runs#1673
hatayama merged 2 commits into
v3-betafrom
feat/attestation-verify-archive

Conversation

@hatayama

@hatayama hatayama commented Jul 10, 2026

Copy link
Copy Markdown
Owner

Summary

  • Dispatcher self-update now binds the installed archive to a Sigstore attestation, not just the same-origin .sha256.
  • Archive attestation is verified before the archive is trusted, layering on top of the existing installer-script check (B2) and runner-archive check (B3).

User Impact

  • Before: uloop update and the auto-update path trusted the archive when its bytes matched the sibling .sha256. An attacker who could tamper with a release asset could rewrite the archive and its .sha256 together and pass verification.
  • After: the dispatcher resolves the target release tag, fetches the Sigstore bundle, and hands a verified digest manifest to install.sh / install.ps1. The installer scripts refuse to extract an archive whose sha256 does not match the manifest entry for its filename.
  • The README curl | sh bootstrap path is unchanged: when ULOOP_ARCHIVE_MANIFEST is unset, the installer scripts continue to rely on the .sha256 file (bootstrap trust remains rooted in TLS + repo ownership, as documented in the security TODO).

Changes

  • cli/dispatcher/internal/attestation/verifier.go: add VerifySubjects which runs full Sigstore verification (signature, transparency log, integrated timestamps, SCT, cert identity, source-repo digest binding) and returns the verified subject-digest map, without a caller-supplied artifact digest.
  • cli/dispatcher/internal/dispatcher/attestation_release.go: paginated /releases enumeration with dispatcher-v + channel filtering that mirrors install.sh's selector so dispatcher-side and script-side resolution converge on the same tag; fetchAttestationSubjectManifest builds a sha256sum-style manifest from the verified statement.
  • cli/dispatcher/internal/dispatcher/update.go & run_dispatcher.go: pre-resolve latest → concrete target version before CommandForOS; append ULOOP_ARCHIVE_MANIFEST env to the installer command after the installer script's own attestation passes.
  • scripts/install.sh & scripts/install.ps1: consume ULOOP_ARCHIVE_MANIFEST; fail closed when the manifest is set but has no entry for the asset or the digest mismatches; skip when the env is unset so first-time installs still work.
  • Tests: unit tests for the channel filter, the resolver, and the shell manifest verification (scripts/test-install-archive-manifest.sh).

Verification

  • scripts/check-go-cli.sh — pass (fmt, vet, lint, tests, dist rebuild)
  • sh scripts/test-install-archive-manifest.sh — pass (matched digest, mismatched digest, missing entry, unset env)

Review in cubic

…manifest

Dispatcher self-update flows now resolve the target release tag before
running the installer, fetch the attestation bundle for that tag, and pass
a Sigstore-verified digest manifest to install.sh / install.ps1 via
ULOOP_ARCHIVE_MANIFEST. The installer scripts enforce the manifest entry
for the archive they downloaded, so a compromised same-origin .sha256 file
alone can no longer bless a swapped archive.

The manifest step layers on top of the existing installer-script attestation
check without changing the README curl|iex bootstrap path: when the env is
unset, install.sh / install.ps1 fall back to the .sha256 comparison only.

Add VerifySubjects to the attestation package to extract verified subject
digests from a bundle without pinning to a caller-supplied artifact digest.
Add resolveDispatcherLatestReleaseTag with GitHub /releases pagination and
channel filtering that mirrors install.sh's rules so dispatcher-side and
script-side resolution converge on the same tag.
@coderabbitai

coderabbitai Bot commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 0fb7963e-56c5-425d-8b6d-d3dd9677a761

📥 Commits

Reviewing files that changed from the base of the PR and between a4c5c21 and 2ccfb3d.

📒 Files selected for processing (1)
  • scripts/test-install-release-filter.sh

📝 Walkthrough

Walkthrough

The dispatcher now resolves release targets, verifies Sigstore attestation subjects, passes a generated archive manifest to the installer, and validates downloaded archives against both checksum files and optional attestation manifests.

Changes

Attested Dispatcher Updates

Layer / File(s) Summary
Sigstore subject verification
cli/dispatcher/internal/attestation/verifier.go, cli/dispatcher/internal/attestation/verifier_test.go
Adds VerifySubjects validation and bundle verification, returning verified SHA-256 subject mappings with happy-path and failure coverage.
Release selection and attestation manifest
cli/dispatcher/internal/dispatcher/attestation_release.go, cli/dispatcher/internal/dispatcher/attestation_release_test.go
Selects stable or beta dispatcher releases, resolves implicit targets, fetches release data, and generates verified archive manifests.
Update target and manifest propagation
cli/dispatcher/internal/dispatcher/run_dispatcher.go, cli/dispatcher/internal/dispatcher/update.go, cli/dispatcher/internal/dispatcher/*_test.go
Routes updates through target resolution and supplies the attestation manifest to the launched update process.
Installer checksum and manifest enforcement
scripts/install.sh, scripts/test-install-archive-manifest.sh, scripts/test-install-release-filter.sh
Computes archive hashes directly, validates checksum files, optionally enforces attestation manifest entries, and tests pass, mismatch, missing, and unset cases.
Shared input stamp
cli/dispatcher/shared-inputs-stamp.json
Updates the recorded shared-inputs hash.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant UpdateRequest
  participant Dispatcher
  participant GitHub
  participant Sigstore
  participant Installer
  UpdateRequest->>Dispatcher: request update
  Dispatcher->>GitHub: resolve release tag
  GitHub-->>Dispatcher: release metadata and bundle
  Dispatcher->>Sigstore: verify attestation subjects
  Sigstore-->>Dispatcher: verified archive manifest
  Dispatcher->>Installer: launch with manifest environment
  Installer->>Installer: verify checksum and manifest
Loading

Possibly related PRs

  • hatayama/unity-cli-loop#1190: Updates installer retrieval from resolved release tags, connecting to this PR’s installer attestation verification.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 9.09% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main dispatcher attestation verification change.
Description check ✅ Passed The description matches the changeset and accurately describes the new attestation-based update flow.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/attestation-verify-archive

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

…n filter test

install.sh switched from `sha256sum -c` (verify from a manifest file) to
`sha256sum <path>` so the same digest string can be reused by both the
same-origin .sha256 check and the new attestation-manifest cross-check. The
mock in test-install-release-filter.sh only recognized the -c form and was
failing every run with "unexpected sha256sum arguments".

Emit "fakehash  <path>" for the single-file form so the mock digest still
matches the fixtures' .sha256 content, and keep the unrecognized-shape
branch loud so a future accidental change to install.sh still fails the
test.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
cli/dispatcher/internal/dispatcher/attestation_release_test.go (1)

110-126: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Cover second-page release selection.

The suite only serves page 1, leaving the new pagination contract untested. Add 100 nonmatching entries on page 1 and a matching dispatcher release on page 2.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cli/dispatcher/internal/dispatcher/attestation_release_test.go` around lines
110 - 126, Extend installReleaseListServer and its tests to exercise pagination:
provide 100 nonmatching githubReleaseListEntry values for page 1 and return a
matching dispatcher release when the request query parameter page is "2", while
preserving empty responses for later pages. Add or update the relevant
release-selection test to assert that the matching page-2 release is selected.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@cli/dispatcher/internal/dispatcher/update.go`:
- Around line 42-50: Validate the platform with update.CommandForOS before
calling resolveUpdateTargetVersionFunc, so unsupported runtime.GOOS values
return the deterministic classified error without network access. Preserve the
resolved options for supported platforms, and add a regression test covering an
offline unsupported-platform invocation.

---

Nitpick comments:
In `@cli/dispatcher/internal/dispatcher/attestation_release_test.go`:
- Around line 110-126: Extend installReleaseListServer and its tests to exercise
pagination: provide 100 nonmatching githubReleaseListEntry values for page 1 and
return a matching dispatcher release when the request query parameter page is
"2", while preserving empty responses for later pages. Add or update the
relevant release-selection test to assert that the matching page-2 release is
selected.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: fd18e95c-743e-42d8-88f9-35f1e71c0765

📥 Commits

Reviewing files that changed from the base of the PR and between fa3313d and a4c5c21.

⛔ Files ignored due to path filters (1)
  • scripts/install.ps1 is excluded by none and included by none
📒 Files selected for processing (11)
  • cli/dispatcher/internal/attestation/verifier.go
  • cli/dispatcher/internal/attestation/verifier_test.go
  • cli/dispatcher/internal/dispatcher/attestation_release.go
  • cli/dispatcher/internal/dispatcher/attestation_release_test.go
  • cli/dispatcher/internal/dispatcher/dispatcher_bootstrap_routing_test.go
  • cli/dispatcher/internal/dispatcher/run_dispatcher.go
  • cli/dispatcher/internal/dispatcher/update.go
  • cli/dispatcher/internal/dispatcher/update_test.go
  • cli/dispatcher/shared-inputs-stamp.json
  • scripts/install.sh
  • scripts/test-install-archive-manifest.sh

Comment on lines +42 to +50
resolvedOptions, err := resolveUpdateTargetVersionFunc(ctx, update.Options{
CurrentVersion: dispatcherVersion,
TargetVersion: options.targetVersion,
})
if err != nil {
clierrors.WriteClassifiedError(stderr, err, clierrors.ErrorContext{Command: clicore.UpdateCommandName})
return true, 1
}
updateCommand, err := update.CommandForOS(runtime.GOOS, resolvedOptions)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Reject unsupported platforms before resolving releases.

Line 42 performs GitHub resolution before CommandForOS validates the platform. An offline Linux invocation now reports a network error instead of the deterministic unsupported-platform error. Validate runtime.GOOS first and add a regression test.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cli/dispatcher/internal/dispatcher/update.go` around lines 42 - 50, Validate
the platform with update.CommandForOS before calling
resolveUpdateTargetVersionFunc, so unsupported runtime.GOOS values return the
deterministic classified error without network access. Preserve the resolved
options for supported platforms, and add a regression test covering an offline
unsupported-platform invocation.

@hatayama
hatayama merged commit 1a49534 into v3-beta Jul 10, 2026
9 checks passed
@hatayama
hatayama deleted the feat/attestation-verify-archive branch July 10, 2026 05:01
@github-actions github-actions Bot mentioned this pull request Jul 11, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant