Skip to content

fix(ci): read cosign's attestation output in the framing it writes - #1751

Merged
FelixTJDietrich merged 1 commit into
mainfrom
fix-release-evidence-attestation-framing
Sep 2, 2026
Merged

fix(ci): read cosign's attestation output in the framing it writes#1751
FelixTJDietrich merged 1 commit into
mainfrom
fix-release-evidence-attestation-framing

Conversation

@FelixTJDietrich

Copy link
Copy Markdown
Collaborator

What changed and why

Release run 33691274046 failed in tag-imagesVerify evidence from registry subjects with
Error: Cosign attestation result is malformed, thrown because the parsed
cosign verify-attestation output was not an array. This path had never completed in a real
release — v0.74.0 predates the evidence bundle and every attempt since stopped earlier — so it was
unproven code rather than a regression.

Cosign writes its verification banner to stderr and its result to stdout as a stream of JSON
documents
: verify-attestation prints one DSSE envelope per verified attestation, unwrapped and
newline-delimited. A subject carrying a single SBOM attestation is therefore a bare object, which
the gate rejected. (cosign verify prints an array instead, which is where the assumption came
from; that call site never parses its output.)

  • Read every framing cosign might use — one document, several newline-delimited documents, or an
    array of them — so a cosign upgrade that reframes its output cannot fail a release.
  • Decode each envelope strictly. The old try/catch returned false for an envelope it could not
    read, silently skipping past it; an envelope cosign has already verified cryptographically and
    still cannot be decoded is a broken capture, and now stops the release with a named error.
  • Empty output, or an empty array, is cosign verified nothing rather than a quiet mismatch.
  • The predicate comparison is unchanged, because it was already correct: cosign attest --type spdxjson stores the predicate file verbatim, so the deep equality against <image>-<platform>.spdx.json
    asserts exactly what it means to.

Fixes the v0.75.0 release.

How to test

Reproduced against the real registry with cosign v3.0.6 — the version
setup-release-security-tools installs, not the v2.6.1 the images predate — using the v0.75.0
candidate digests from run 33691274046.

cosign verify-attestation --type spdxjson \
  --certificate-identity 'https://github.com/hephaestus-build/Hephaestus/.github/workflows/release.yml@refs/heads/main' \
  --certificate-oidc-issuer 'https://token.actions.githubusercontent.com' \
  ghcr.io/hephaestus-build/webapp@sha256:ff26dd2004e5d4d07240b7db599570bed0b5bf3fda1cdc1b9ab5e53f5b7977f7

exits 0, prints the banner on stderr and one 1 476 210-byte line on stdout, whose payload
decodes to an in-toto statement (predicateType: https://spdx.dev/Document, subject
ghcr.io/hephaestus-build/webapp@sha256:ff26dd20…). Re-running syft --from registry … --platform linux/amd64 --scope squashed -o spdx-json over the same digest reproduces that predicate key for
key, differing only in creationInfo.created and the documentNamespace UUID — the published
attestation carries the syft document the bundle records, verbatim.

Feeding that captured 1.4 MB stdout to the fixed code parses one document, returns true for the
published predicate and false for the re-generated SBOM: the gate reads the real output and still
bites.

The node:test cases use captured cosign v3.0.6 output as their fixture — real
attest --type spdxjson / verify-attestation round trips over a local registry, one attestation
and two — so the framing is proven by tests rather than by a release. pnpm run format and
pnpm run check pass.

Release impact

No changeset. verify-changesets.yml scopes SHIPPED_PATHS to server, webapp and docker;
this changes scripts/ only, which is repository tooling and not image contents.

Notes for reviewers

verifyIndexSignatures is the other release-only check in this file and does not share the
defect: it never parses cosign's output, only its exit code. It was run for real against all four
published indexes (webapp, application-server, agent-pi, postgres) — cosign verify against
the reusable-docker-build.yml@refs/heads/main identity and gh attestation verify --signer-workflow
both exit 0, and a deliberately wrong --signer-workflow exits 1. It is left unchanged.

Worth knowing for the next release run: because cosign attest already ran against these digests,
a re-run will add a second attestation to each subject and verify-attestation will then print
two newline-delimited envelopes. That is precisely the framing the fallback path handles, and the
TWO_ATTESTATIONS fixture covers it.

Made by Claude Fable 5 in Claude Code.

The release gate read `cosign verify-attestation` as a JSON array. Cosign writes its
banner to stderr and one DSSE envelope per verified attestation to stdout, unwrapped
and newline-delimited, so a subject with a single SBOM attestation is a bare object
and the gate rejected it as malformed. That failed v0.75.0 in `tag-images`, and since
this path had never run to completion in a real release, nothing had ever seen it.

Read every framing cosign might use — one document, several newline-delimited ones, or
an array of them — and decode each envelope strictly instead of skipping past one that
cannot be read, so a broken capture stops a release rather than being silently ignored.
The predicate comparison is unchanged: `cosign attest --type spdxjson` stores the
predicate file verbatim, so the deep equality against the bundle's SBOM is what it
claims to be.

Verified against the real registry with cosign v3.0.6, the version the release installs:
verifying the published webapp subject prints exactly one 1.4 MB envelope whose
predicate is the syft document in the bundle key for key, which the fixed gate accepts
and a re-generated SBOM does not. The two other release-only checks in this file, the
index signature and its GitHub attestation, were run against all four published indexes
and pass unchanged. The `node:test` cases carry captured cosign output as their fixture.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

Next included review available in 37 seconds.

Check out review usage here.

View limit details

Limit details: You’ve used the included review currently available.

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Learn how review limits work.

Review configuration:

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Team

Run ID: e19a444b-9761-4b6b-b2e8-89313f72dc0f

📥 Commits

Reviewing files that changed from the base of the PR and between d296e96 and a0b69fd.

📒 Files selected for processing (2)
  • scripts/verify-release-evidence.test.ts
  • scripts/verify-release-evidence.ts

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.

@github-actions github-actions 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.

Approved automatically: @FelixTJDietrich is listed in the REVIEW_POLICY_MAINTAINERS repository variable, which the repository treats as satisfying the review requirement. See the review policy in docs/contributor/ci-cd.mdx.

@github-actions github-actions Bot added bug Something isn't working test Unit tests, integration tests, test infrastructure labels Sep 2, 2026
@github-project-automation github-project-automation Bot moved this from Backlog to In Review in Hephaestus Sep 2, 2026
@github-actions github-actions Bot added the size:L Pull request diff size, applied automatically by the PR labeler; issues use the Effort field label Sep 2, 2026
@FelixTJDietrich
FelixTJDietrich added this pull request to the merge queue Sep 2, 2026
Merged via the queue into main with commit 7459495 Sep 2, 2026
34 checks passed
@FelixTJDietrich
FelixTJDietrich deleted the fix-release-evidence-attestation-framing branch September 2, 2026 23:21
@github-project-automation github-project-automation Bot moved this from In Review to Done in Hephaestus Sep 2, 2026
@github-actions

github-actions Bot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

📚 Documentation Preview

Preview has been removed (PR closed)

@github-actions

github-actions Bot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

🧩 Storybook Preview

Preview has been removed (PR closed)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working size:L Pull request diff size, applied automatically by the PR labeler; issues use the Effort field test Unit tests, integration tests, test infrastructure

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

1 participant