Skip to content

security: gate publication on verified attestations - #496

Open
codeforester wants to merge 1 commit into
mainfrom
security/487-20261028-attestation-publication-gate
Open

codeforester wants to merge 1 commit into
mainfrom
security/487-20261028-attestation-publication-gate

Conversation

@codeforester

Copy link
Copy Markdown
Contributor

Summary

  • add a verification job that runs after provenance attestation and before publication
  • make publication depend on that verification job
  • verify both provenance and SPDX attestations against the exact reviewed source commit and release tag
  • use build outputs rather than ambient GITHUB_SHA/GITHUB_REF in the final release verification

Security rationale

The previous graph allowed publish and attest to run in parallel, so publication was not contingent on a verified attestation. This makes attestation verification an explicit prerequisite while preserving least-privilege permissions.

Validation

  • UV_CACHE_DIR=/private/tmp/base-cli-uv-cache uv run --extra dev pytest -q tests/test_package_workflow.py
  • Ruff, YAML parsing, and git diff --check pass

Relates to #487.

--repo "$GITHUB_REPOSITORY" \
--signer-workflow "github.com/$GITHUB_REPOSITORY/.github/workflows/package.yml" \
--source-digest "$SOURCE_COMMIT" \
--source-ref "refs/tags/$RELEASE_TAG"

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Correctness (breaks a documented flow): this hardcodes --source-ref "refs/tags/$RELEASE_TAG", but RELEASE_TAG is empty for a workflow_dispatch run that doesn't supply release_tag — exactly the documented TestPyPI rehearsal flow in docs/releasing.md (dispatch with publish_target=testpypi, leave release_tag blank, its documented default). build's metadata step leaves release_tag="" since source_ref_type is branch, so this becomes --source-ref "refs/tags/", which can never match the real attested ref (refs/heads/<branch>). Because publish now needs verify_attestations, this permanently breaks the TestPyPI rehearsal path.

publish:
name: Publish reviewed distribution
needs: [build, smoke, provenance]
needs: [build, smoke, provenance, verify_attestations]

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fragility: publish's needs list dropped the direct attest dependency and now only lists verify_attestations; safe today only because verify_attestations itself needs attest — a fragile transitive assumption with no test guarding it. A later edit removing attest from verify_attestations.needs (e.g. someone 'simplifying' it, thinking attestations:read is unrelated to needing the attest job) would silently let publish start before attestations exist, defeating this PR's stated security goal with nothing to catch it.

def test_package_workflow_verifies_attestations_before_publication() -> None:
workflow = (Path(__file__).resolve().parents[1] / ".github/workflows/package.yml").read_text(encoding="utf-8")
jobs = yaml.safe_load(workflow)["jobs"]

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Test gap: this new test only asserts the literal strings '--source-digest "$SOURCE_COMMIT"' and '--source-ref "refs/tags/$RELEASE_TAG"' appear in the YAML text — it never exercises the case where RELEASE_TAG resolves to empty (non-tag workflow_dispatch). The workflow is functionally broken for branch-based dispatch runs (see the refs/tags/"" finding on package.yml) but this test still passes, since it only checks substring presence rather than resolved behavior for all trigger types.

This branch has not been deployed

No deployments
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