Skip to content

verify: accept GitHub release immutability + Sigstore attestation as runtime-download trust anchor#887

Open
potiuk wants to merge 2 commits into
mainfrom
verify-trusted-download-provenance
Open

verify: accept GitHub release immutability + Sigstore attestation as runtime-download trust anchor#887
potiuk wants to merge 2 commits into
mainfrom
verify-trusted-download-provenance

Conversation

@potiuk
Copy link
Copy Markdown
Member

@potiuk potiuk commented May 24, 2026

Summary

  • Adds TRUSTED_DOWNLOAD_PROVENANCE declarative config + verify_trusted_download_provenance() to utils/verify_action_build/security.py. When an action downloads binaries at runtime without an in-source checksum check, an entry in the config asserts the trust anchor is at GitHub/Sigstore level (immutable release + SLSA attestation).
  • At scan time the pipeline confirms both halves (release.immutable=true + gh attestation verify against a small SLSA-attested asset, typically a .sbom.json from actions/attest-build-provenance). Config entry alone is not enough.
  • Reuses the existing _verify_via_gh_attestation() helper that backs the in-tree-binary check.
  • Bootstrap entry: golangci/golangci-lint-action per maintainer @ldez's confirmation in installBin: download golangci-lint release binary without checksum/signature verification golangci/golangci-lint-action#1396 (releases since v2.12.2 immutable + SLSA-attested; action itself immutable since v9.2.1).

Why

golangci-lint-action's installBin() downloads the golangci-lint binary via tc.downloadTool(assetURL) with no in-source checksum — the verify pipeline correctly flagged this on every recent dependabot bump. The maintainer noted the trust anchor is GitHub release immutability + Sigstore attestation, and pointed out that an in-source checksum re-derived from the same publishing source adds no meaningful layer. This patch teaches the verify pipeline to actually verify that alternative trust anchor at scan time, rather than just suppressing the finding.

Behaviour

  • Actions without a config entry: unchanged.
  • Actions with an entry, runtime check passes: unverified-download failures reclassified as warnings; banner + rationale + scan-time evidence printed.
  • Actions with an entry, runtime check fails (release not immutable, or no SLSA attestation): failures stay failures; failure reason printed.

Test plan

  • Unit: verify_trusted_download_provenance('golangci', 'golangci-lint-action')passed=True, reason mentions immutable + attested
  • Unit: verify_trusted_download_provenance('some', 'other-action')passed=False, empty reason
  • Integration: analyze_binary_downloads('golangci', 'golangci-lint-action', '<v9.2.1 SHA>') returns (1 warning, 0 failures) — previously-failing tc.downloadTool finding reclassified
  • Regression: analyze_binary_downloads('opentofu', 'setup-opentofu', '<v2.0.1 SHA>') still returns (0 warnings, 1 failure) — unchanged for actions without trust config
  • After merge: next golangci-lint-action dependabot bump's verify check passes cleanly

Out of scope

opentofu/setup-opentofu: releases are not GitHub-immutable yet and use cosign keyless signatures rather than actions/attest — different verification primitive (cosign verify-blob with OIDC identity regex). The upstream fix is opentofu/setup-opentofu#117. A cosign-based path here is possible but adds a CLI dependency and is deferred.

🤖 Generated with Claude Code

…runtime-download trust anchor

When an action downloads a binary at runtime via tc.downloadTool /
fetch / etc. without an in-source checksum check, the verify
pipeline currently fails with "unverified binary download(s) detected
(no checksum/signature check in file)". For some upstreams that
finding is intractable on our side: the action's design delegates
trust to the publishing process, not to an inline check.

This patch adds a declarative per-action escape hatch. An entry in
the new TRUSTED_DOWNLOAD_PROVENANCE dict asserts that the action's
runtime downloads are anchored at the GitHub / Sigstore layer: the
configured release_repo must publish immutable releases AND emit
Sigstore attestations via actions/attest-build-provenance. Both
halves are verified at scan time before any failure is reclassified
— the config entry alone is not enough.

The runtime check:
  1. GET releases/latest of the configured release_repo; confirm
     release.immutable is true.
  2. Download one small SLSA-attested asset (preferring .sbom.json
     which actions/attest emits alongside binaries, ~340KB) and run
     `gh attestation verify` against it (reuses the existing
     _verify_via_gh_attestation helper used by the in-tree-binary
     check).

On pass, all unverified-download failures for the action are
reclassified as warnings with the rationale appended. On fail (when
the config exists but the runtime check doesn't confirm both halves),
failures stay failures and the reason is printed so the auditor
sees why the escape hatch didn't apply.

Bootstrap entry: golangci/golangci-lint-action, with rationale
linking golangci/golangci-lint-action#1396
where the maintainer confirmed golangci-lint releases since v2.12.2
are immutable + actions/attest-signed, and the action itself is
immutable since v9.2.1.

Generated-by: Claude Code (Claude Opus 4.7)
Companion to the previous commit that introduced
TRUSTED_DOWNLOAD_PROVENANCE for actions whose runtime downloads are
anchored at the GitHub release / Sigstore layer rather than via an
in-source checksum.

README: add a paragraph after the "Pre-compiled native binaries
shipped in-tree" bullet explaining the escape hatch — what an entry
asserts, what the runtime check confirms (immutable release + valid
Sigstore attestation on an attested asset), and that the config alone
is not enough.

Tests:
  * TestVerifyTrustedDownloadProvenance — happy path, missing config
    (returns no-opinion), network failure on release metadata, non-
    immutable release, no asset downloadable, `gh attestation verify`
    failure.
  * TestFetchReleaseAssetBytes — asset-preference ordering picks the
    cheapest valid probe (.sbom.json over .tar.gz); smallest-asset
    fallback when no preference matches; empty assets returns (None,
    None).
  * TestAnalyzeBinaryDownloadsTrustedDownloadEscapeHatch — the call-
    site branch: passing provenance reclassifies failures as warnings;
    failing provenance preserves failures; no config entry skips the
    branch entirely (verify_trusted_download_provenance never called).

12 new tests; full test_security.py (144 tests) passes.

Generated-by: Claude Code (Claude Opus 4.7)
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