-
Notifications
You must be signed in to change notification settings - Fork 0
Verify release attestation offline against published bundle asset #421
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
996194d
9534c09
fd28783
19a35c3
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -106,10 +106,18 @@ jobs: | |
| run: sha256sum coop-*.tar.gz > SHA256SUMS | ||
|
|
||
| - name: Attest build provenance | ||
| id: attest | ||
| uses: actions/attest-build-provenance@0f67c3f4856b2e3261c31976d6725780e5e4c373 # v4.1.1 | ||
| with: | ||
| subject-path: "coop-*.tar.gz" | ||
|
|
||
| # Normalize to one bundle per line so the published asset has a stable | ||
| # shape regardless of whether the action emits a single bundle or several. | ||
| - name: Normalize attestation bundle | ||
| env: | ||
| BUNDLE_PATH: ${{ steps.attest.outputs.bundle-path }} | ||
| run: jq -c . "$BUNDLE_PATH" > attestations.jsonl | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
That is worse for clients than the asset being absent, because neither client checks the bundle it got. The release-notes step three below already models the guard: |
||
|
|
||
| - name: Extract release notes from CHANGELOG | ||
| env: | ||
| TAG: ${{ github.ref_name }} | ||
|
|
@@ -128,4 +136,4 @@ jobs: | |
| env: | ||
| GH_TOKEN: ${{ github.token }} | ||
| TAG: ${{ github.ref_name }} | ||
| run: gh release create "$TAG" --title "$TAG" --notes-file RELEASE_NOTES.md coop-*.tar.gz SHA256SUMS | ||
| run: gh release create "$TAG" --title "$TAG" --notes-file RELEASE_NOTES.md coop-*.tar.gz SHA256SUMS attestations.jsonl | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -130,12 +130,28 @@ attestation via [`actions/attest-build-provenance`](https://github.com/actions/a | |
| The attestation proves the artifact was built from this repository by the | ||
| tagged release workflow. | ||
|
|
||
| Both `install.sh` and `coop update` run this verification automatically | ||
| when the [GitHub CLI](https://cli.github.com/) is installed. Without `gh`, | ||
| they fall back to checksum verification against the release's `SHA256SUMS` | ||
| and print a note explaining what was and wasn't verified. | ||
| Both `install.sh` and `coop update` run this verification automatically when | ||
| the [GitHub CLI](https://cli.github.com/) is installed. Both verify offline | ||
| against the `attestations.jsonl` bundle published with the release, so | ||
| verification makes no GitHub API call and needs no authentication. Releases | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This over-claims relative to That matters for exactly the SSO-less user this PR targets, whose |
||
| published before that asset existed are verified through the GitHub | ||
| attestations API instead, which does require a credential authorized for the | ||
| `trailofbits` org. Without `gh`, both fall back to checksum verification | ||
| against the release's `SHA256SUMS` and print a note explaining what was and | ||
| wasn't verified. | ||
|
|
||
| To verify a downloaded tarball manually, download `attestations.jsonl` from the | ||
| same release and pass `--bundle`: | ||
|
|
||
| To verify a downloaded tarball manually: | ||
| ```sh | ||
| gh attestation verify coop-<version>-<triple>.tar.gz --repo trailofbits/coop \ | ||
| --bundle attestations.jsonl | ||
| ``` | ||
|
|
||
| That needs no GitHub credential. Dropping `--bundle` makes `gh` fetch the | ||
| attestation from the API instead, which requires one — and fails with | ||
| `HTTP 403: Resource protected by organization SAML enforcement` if your token | ||
| carries no SSO session for the org: | ||
|
|
||
| ```sh | ||
| gh attestation verify coop-<version>-<triple>.tar.gz --repo trailofbits/coop | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -92,11 +92,23 @@ CI can't run the full VM integration suite or the extra-toolchain checks | |
| This triggers `release.yml`. | ||
|
|
||
| 9. **Verify the published release.** On the GitHub release page confirm: | ||
| - three `coop-vX.Y.Z-<target>.tar.gz` artifacts plus `SHA256SUMS`, | ||
| - three `coop-vX.Y.Z-<target>.tar.gz` artifacts plus `SHA256SUMS` and | ||
| `attestations.jsonl`, | ||
| - the build-provenance attestation is attached, | ||
| - the notes match the `## vX.Y.Z` CHANGELOG section. | ||
|
|
||
| Then smoke-test the install path (`install.sh`) against the new tag. | ||
| Then smoke-test the install path with credentials stripped, so the offline | ||
| bundle verification is exercised as an external user sees it. Pin `VERSION` | ||
| to the tag you just pushed rather than relying on "latest": | ||
|
|
||
| ```bash | ||
| env -u GH_TOKEN -u GITHUB_TOKEN GH_CONFIG_DIR="$(mktemp -d)" \ | ||
| VERSION=vX.Y.Z INSTALL_DIR="$(mktemp -d)" bash install.sh | ||
| ``` | ||
|
|
||
| The run must print `Attestation verified offline against attestations.jsonl`. | ||
| A "No `attestations.jsonl` published" line instead means the asset is missing | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The diagnosis states one cause as the only cause. A maintainer following this step after a transient failure would conclude the release asset is missing — a costly wrong conclusion given releases are immutable and the remedy is a version bump. Suggest: "means the bundle could not be downloaded — confirm the asset on the release page (step 9's first bullet) before concluding it is missing." |
||
| and verification fell back to the credential-requiring API path. | ||
|
|
||
| ## If the tag run fails | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -212,11 +212,19 @@ Self-update (`update.rs`) must preserve, in order: | |
| 3. **Mandatory checksum.** The `SHA256SUMS` asset must be present (install is | ||
| refused otherwise) and every downloaded tarball is verified against it | ||
| (`verify_sha256`, constant-size `Sha256Hash` compare). | ||
| 4. **Best-effort attestation.** `gh attestation verify --repo trailofbits/coop` | ||
| (Sigstore provenance). Skipped with a logged note if `gh` is absent, and | ||
| skipped entirely when `COOP_UPDATE_API_BASE_URL` is overridden (test mode). | ||
| So provenance is *not* guaranteed on hosts without `gh` — checksum is the | ||
| floor. | ||
| 4. **Best-effort attestation.** `gh attestation verify --repo trailofbits/coop | ||
| --bundle attestations.jsonl` (Sigstore provenance), against the bundle asset | ||
| downloaded from the same release. `--bundle` makes verification offline: no | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Two corrections to this paragraph, in the document that is the checklist future reviewers apply. "offline" overstates what
Refs: https://cli.github.com/manual/gh_attestation_verify · https://docs.github.com/en/actions/security-for-github-actions/using-artifact-attestations/verifying-attestations-offline |
||
| attestations-API call, so no GitHub credential — but it does *not* weaken the | ||
| check, because the bundle is signed and `--repo` still pins the signer | ||
| identity, so a substituted or tampered bundle fails. A release that publishes | ||
| no bundle asset — or a bundle whose download fails — falls back to the API | ||
| path (credential required); `update.rs` and `install.sh` treat those two | ||
| cases identically. A bundle that downloads but fails to verify is refused | ||
| outright, not retried through the API. Skipped with a logged note if `gh` is | ||
| absent, and skipped entirely when `COOP_UPDATE_API_BASE_URL` is overridden | ||
| (test mode). So provenance is *not* guaranteed on hosts without `gh` — | ||
| checksum is the floor. | ||
| 5. Extraction with `tar -xzf --no-same-owner --no-same-permissions` (path-escape | ||
| safe), then an atomic `rename`-over-self. | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -10,6 +10,7 @@ set -euo pipefail | |
|
|
||
| REPO="trailofbits/coop" | ||
| BINARY="coop" | ||
| BUNDLE="attestations.jsonl" | ||
| INSTALL_DIR="${INSTALL_DIR:-${HOME}/.local/bin}" | ||
|
|
||
| # --- helpers ---------------------------------------------------------------- | ||
|
|
@@ -112,17 +113,56 @@ verify_checksum() { | |
|
|
||
| verify_attestation() { | ||
| local file="$1" | ||
| if has gh; then | ||
| info "Verifying attestation..." | ||
| gh attestation verify "$file" --repo "$REPO" \ | ||
| || die "Attestation verification failed for $(basename "$file") — refusing to install" | ||
| else | ||
| if ! has gh; then | ||
| info "Note: \`gh\` not installed — skipped cryptographic attestation verification." | ||
| info "The download was verified against the published \`SHA256SUMS\` checksum, which" | ||
| info "is the same assurance level as most \`curl | bash\` installers. For end-to-end" | ||
| info "Sigstore verification, install \`gh\` (https://cli.github.com) and re-run, or" | ||
| info "verify manually: \`gh attestation verify <tarball> --repo ${REPO}\`." | ||
| info "verify manually: \`gh attestation verify <tarball> --repo ${REPO} \\" | ||
| info " --bundle ${BUNDLE}\` against the ${BUNDLE} asset from the same release." | ||
| return 0 | ||
| fi | ||
|
|
||
| info "Verifying attestation..." | ||
|
evandowning marked this conversation as resolved.
|
||
| # Prefer the bundle published with the release: `gh attestation verify` | ||
| # without --bundle queries the GitHub attestations API, and gh always | ||
| # attaches its stored token, so a token lacking an SSO session for the org | ||
| # 403s on public data. The --repo identity check is enforced either way. | ||
| # The probe is silenced: a missing bundle is expected on older releases, so | ||
| # curl's bare "404" would read as a hard error. | ||
| if download_asset "$BUNDLE" "${TMPDIR}/${BUNDLE}" > /dev/null 2>&1; then | ||
| # A failure here is not retried through the API. That is no stricter on | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The enumeration omits the security-relevant cause. Worth adding integrity failure to the list, or trimming the block to the one durable why: not retried through the API, because switching transports on a bundle that downloaded but will not verify would mask the failure. |
||
| # integrity — a substituted bundle fails here and would then verify | ||
| # correctly against the genuine attestation — but a bundle that | ||
| # downloaded and will not verify means a broken download or a `gh` that | ||
| # cannot read it, and switching transports would hide both. | ||
| gh attestation verify "$file" --repo "$REPO" --bundle "${TMPDIR}/${BUNDLE}" \ | ||
| || die "Attestation verification failed for $(basename "$file") — refusing to install" | ||
|
evandowning marked this conversation as resolved.
|
||
| info "Attestation verified offline against ${BUNDLE} — no GitHub credential used." | ||
| return 0 | ||
| fi | ||
|
|
||
| # Releases published before the bundle asset existed verify through the | ||
| # API, exactly as every release did before. That needs a credential | ||
| # authorized for the org, so it is the fallback and not the default. | ||
| info "No ${BUNDLE} published for ${VERSION} — verifying through the GitHub API instead." | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This line asserts as fact something the code has just discarded. It runs on every non-zero exit of the silenced probe On a current release plus a transient blip, the user is told the release publishes no provenance bundle, verification silently moves to the credential-requiring transport, and if their token has no SSO session the gate below then prints
|
||
| local out | ||
| if out="$(gh attestation verify "$file" --repo "$REPO" 2>&1)"; then | ||
| info "Attestation verified through the GitHub API." | ||
| return 0 | ||
| fi | ||
| printf '%s\n' "$out" >&2 | ||
| # This path also fails on a network error, a gh too old for the command, or | ||
| # a genuine provenance mismatch, so only explain the credential requirement | ||
| # when gh actually reported one of its symptoms. | ||
| case "$out" in | ||
| *403* | *SAML* | *"gh auth login"*) | ||
| info "${VERSION} predates the ${BUNDLE} asset, so verification used the GitHub API," | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. These three lines go to stdout — Suggest emitting the explanation on stderr too — a |
||
| info "which needs a credential authorized for the org. Installing a release that" | ||
| info "publishes ${BUNDLE} avoids the API entirely." | ||
| ;; | ||
| esac | ||
| die "Attestation verification failed for $(basename "$file") — refusing to install" | ||
| } | ||
|
|
||
| # --- main ------------------------------------------------------------------- | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -8,6 +8,7 @@ | |
| //! to nudge users when a newer release is available. | ||
|
|
||
| use std::env; | ||
| use std::ffi::OsString; | ||
| use std::fs; | ||
| use std::io::IsTerminal as _; | ||
| use std::os::unix::fs::PermissionsExt as _; | ||
|
|
@@ -25,6 +26,8 @@ use crate::sha256_hash::Sha256Hash; | |
|
|
||
| const REPO: &str = "trailofbits/coop"; | ||
| const DEFAULT_API_BASE: &str = "https://api.github.com"; | ||
| /// Release asset holding the Sigstore provenance bundle, published since #421. | ||
| const BUNDLE_ASSET: &str = "attestations.jsonl"; | ||
| const DEFAULT_CHECK_INTERVAL_HOURS: u64 = 24; | ||
|
|
||
| // ── Configuration ──────────────────────────────────────────────────────────── | ||
|
|
@@ -383,7 +386,62 @@ fn verify_sha256(file: &Path, expected: &Sha256Hash) -> Result<()> { | |
|
|
||
| // ── Attestation verification (best-effort) ─────────────────────────────────── | ||
|
|
||
| fn verify_attestation(tarball: &Path) -> Result<()> { | ||
| /// Build the `gh attestation verify` argument list. | ||
| /// | ||
| /// With `bundle`, `gh` reads the Sigstore bundle from disk and makes no API | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is the fourth copy of the same SSO/403 rationale in the diff — it is also in That matters for drift more than for length, because "always attaches its stored token" is a claim about the current |
||
| /// call, so verification needs no GitHub credential. Without it, `gh` fetches | ||
| /// the bundle from the attestations API and always attaches its stored token — | ||
| /// which 403s on public data when that token carries no SSO session for the | ||
| /// org. `--repo` pins the signer identity in both cases. | ||
| fn attestation_verify_args(tarball: &Path, bundle: Option<&Path>) -> Vec<OsString> { | ||
| let mut args: Vec<OsString> = vec![ | ||
| "attestation".into(), | ||
| "verify".into(), | ||
| tarball.as_os_str().to_owned(), | ||
| "--repo".into(), | ||
| REPO.into(), | ||
| ]; | ||
| if let Some(bundle) = bundle { | ||
| args.push("--bundle".into()); | ||
| args.push(bundle.as_os_str().to_owned()); | ||
| } | ||
| args | ||
| } | ||
|
|
||
| /// Download the release's provenance bundle, if it published one. | ||
| /// | ||
| /// `None` means verification falls back to the attestations API, or is skipped | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Most of this records the path taken during review rather than the function's contract. "A download that blips is the same situation as a release that never published the asset, and The first sentence plus " |
||
| /// outright — never that the update fails. A download that blips is the same | ||
| /// situation as a release that never published the asset, and `install.sh` | ||
| /// resolves it the same way. The download is also skipped when | ||
| /// `verify_attestation` would not read the result, so an update whose | ||
| /// attestation step is a no-op does no pointless work. | ||
| fn fetch_attestation_bundle(release: &Release, dir: &Path) -> Option<PathBuf> { | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is the only genuinely new decision logic in the diff — five outcomes (API-base override, It is untestable as written — it probes env, probes |
||
| if api_base_overridden() || !command_exists("gh") { | ||
| return None; | ||
| } | ||
| let Some(asset) = release.find_asset(BUNDLE_ASSET) else { | ||
| tracing::info!( | ||
| "Release {} publishes no {BUNDLE_ASSET} — verifying the attestation through the \ | ||
| GitHub API, which requires a credential authorized for {REPO}.", | ||
| release.tag | ||
| ); | ||
| return None; | ||
| }; | ||
| let dest = dir.join(BUNDLE_ASSET); | ||
| if let Err(err) = download_asset(&release.tag, BUNDLE_ASSET, &asset.url, &dest) { | ||
| tracing::warn!( | ||
| "Failed to download {BUNDLE_ASSET} for release {} ({err:#}) — verifying the \ | ||
| attestation through the GitHub API, which requires a credential authorized for \ | ||
| {REPO}.", | ||
| release.tag | ||
| ); | ||
| return None; | ||
| } | ||
| Some(dest) | ||
| } | ||
|
|
||
| fn verify_attestation(tarball: &Path, bundle: Option<&Path>) -> Result<()> { | ||
| // Skip when the API base is overridden — the local test fixture serves | ||
| // synthetic artifacts that have no provenance in GitHub's attestation | ||
| // API. `warn_if_api_base_overridden` has already surfaced this to the | ||
|
|
@@ -397,21 +455,26 @@ fn verify_attestation(tarball: &Path) -> Result<()> { | |
| The download was verified against the published `SHA256SUMS` checksum, which \ | ||
| is the same assurance level as most `curl | bash` installers. For end-to-end \ | ||
| Sigstore verification, install `gh` (https://cli.github.com) and re-run, or \ | ||
| verify manually: `gh attestation verify <tarball> --repo {}`.", | ||
| REPO | ||
| verify manually: `gh attestation verify <tarball> --repo {REPO} --bundle \ | ||
| {BUNDLE_ASSET}` against the {BUNDLE_ASSET} asset from the same release." | ||
| ); | ||
| return Ok(()); | ||
| } | ||
| Cmd::new("gh") | ||
| .arg("attestation") | ||
| .arg("verify") | ||
| .arg(tarball) | ||
| .arg("--repo") | ||
| .arg(REPO) | ||
| .args(attestation_verify_args(tarball, bundle)) | ||
| .run() | ||
| .with_context(|| { | ||
| let hint = if bundle.is_some() { | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
On the download-failure path (added last round to resolve the Returning the decision once as an enum ( |
||
| String::new() | ||
| } else { | ||
| format!( | ||
| " (verified through the GitHub API because the release publishes no \ | ||
| {BUNDLE_ASSET}; an HTTP 403 here means your GitHub credential has no \ | ||
| SSO session for the org)" | ||
| ) | ||
| }; | ||
| format!( | ||
| "Attestation verification failed for {} — refusing to install", | ||
| "Attestation verification failed for {} — refusing to install{hint}", | ||
| tarball.display() | ||
| ) | ||
| }) | ||
|
|
@@ -576,7 +639,8 @@ fn perform_update(release: &Release, triple: &str) -> Result<()> { | |
| .with_context(|| format!("{tarball_name} not listed in SHA256SUMS"))?; | ||
| verify_sha256(&tarball_path, &expected)?; | ||
|
|
||
| verify_attestation(&tarball_path)?; | ||
| let bundle_path = fetch_attestation_bundle(release, tmp.path()); | ||
| verify_attestation(&tarball_path, bundle_path.as_deref())?; | ||
|
|
||
| // `--no-same-owner --no-same-permissions` ignore embedded uid/mode metadata. | ||
| // `-C <tempdir>` plus modern tar's default refusal of `..`-segmented and absolute | ||
|
|
@@ -924,6 +988,84 @@ mod tests { | |
| verify_sha256(&path, &wrong).unwrap_err(); | ||
| } | ||
|
|
||
| #[test] | ||
| fn attestation_verify_args_pin_repo_and_omit_bundle_when_absent() { | ||
| let args = attestation_verify_args(Path::new("/tmp/coop.tar.gz"), None); | ||
| assert_eq!( | ||
| args, | ||
| ["attestation", "verify", "/tmp/coop.tar.gz", "--repo", REPO] | ||
| ); | ||
| } | ||
|
|
||
| #[test] | ||
| fn attestation_verify_args_append_bundle_when_present() { | ||
| let args = attestation_verify_args( | ||
| Path::new("/tmp/coop.tar.gz"), | ||
| Some(Path::new("/tmp/attestations.jsonl")), | ||
| ); | ||
| assert_eq!( | ||
| args, | ||
| [ | ||
| "attestation", | ||
| "verify", | ||
| "/tmp/coop.tar.gz", | ||
| "--repo", | ||
| REPO, | ||
| "--bundle", | ||
| "/tmp/attestations.jsonl", | ||
| ] | ||
| ); | ||
| } | ||
|
|
||
| #[test] | ||
| fn bundle_asset_is_found_on_a_release_that_publishes_it() { | ||
| let release: Release = serde_json::from_str(&format!( | ||
| r#"{{"tag_name":"v9.9.9","assets":[ | ||
| {{"name":"SHA256SUMS","browser_download_url":"https://example.com/S"}}, | ||
| {{"name":"{BUNDLE_ASSET}","browser_download_url":"https://example.com/B"}} | ||
| ]}}"# | ||
| )) | ||
| .unwrap(); | ||
| assert_eq!( | ||
| release.find_asset(BUNDLE_ASSET).map(|a| a.url.as_str()), | ||
| Some("https://example.com/B") | ||
| ); | ||
|
|
||
| // A release predating the asset must fall back, not match something else. | ||
| let old: Release = serde_json::from_str( | ||
| r#"{"tag_name":"v0.5.4","assets":[ | ||
| {"name":"SHA256SUMS","browser_download_url":"https://example.com/S"} | ||
| ]}"#, | ||
| ) | ||
| .unwrap(); | ||
| assert!(old.find_asset(BUNDLE_ASSET).is_none()); | ||
| } | ||
|
|
||
| /// The asset name is agreed across three files with no compiler link | ||
| /// between them. A rename in one silently degrades `coop update` and | ||
| /// `install.sh` back to the credential-requiring API path. | ||
| /// | ||
| /// `include_str!` is the tripwire, so moving either file breaks this test | ||
| /// as a compile error rather than a named assertion failure. | ||
| #[test] | ||
| fn bundle_asset_name_matches_release_workflow_and_installer() { | ||
| let workflow = include_str!("../.github/workflows/release.yml"); | ||
| let installer = include_str!("../install.sh"); | ||
| // The workflow names the asset twice: the `jq` output redirect that | ||
| // creates it, and the `gh release create` that publishes it. Only the | ||
| // latter makes it reachable by a client, so assert on that line. | ||
| assert!( | ||
| workflow | ||
| .lines() | ||
| .any(|l| l.contains("gh release create") && l.contains(BUNDLE_ASSET)), | ||
| "release.yml no longer publishes {BUNDLE_ASSET} as a release asset" | ||
| ); | ||
| assert!( | ||
| installer.contains(BUNDLE_ASSET), | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This assertion cannot detect what its message claims. The workflow half was tightened in the previous round to require |
||
| "install.sh no longer downloads {BUNDLE_ASSET}" | ||
| ); | ||
| } | ||
|
|
||
| #[test] | ||
| fn parse_sha256sums_returns_first_of_duplicates() { | ||
| let content = concat!( | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The stated rationale describes a shape the pinned action cannot emit, and omits the constraint that actually makes this step necessary.
At
actions/attest-build-provenancev4.1.1 (a wrapper overactions/attestv4.1.1), the action writesbundle-pathto<mkdtemp>/attestation.jsonas exactly oneJSON.stringify(att.bundle)line, and multiplesubject-pathmatches produce a single attestation referencing all three tarballs rather than several bundles — so "regardless of whether the action emits a single bundle or several" is a case that cannot occur, and the one-bundle-per-line normalization is a no-op on content. (A multi-bundle file would also have worked:ghuses any-match semantics across a.jsonl, erroring only whenverifyCount == 0, and sigstore-go matches the artifact digest against every entry ofstatement.Subject.)What the step does accomplish, unremarked, is giving the asset a
.jsonlextension:ghdispatches on extension and rejects anything else withbundle file extension not supported, must be json or jsonl. The published asset name is therefore load-bearing for--bundle, and a future "simplification" to e.g.attestations.txtwould break verification. Worth saying that instead — a plaincpwould do the same work.