Phase 15. Attestation verification
Land the install-time half of the trusted-publishing pipeline whose
publish half landed in phase 11:
fetch the PEP 740 <wheel-url>.provenance attestation bundle for every
resolved wheel, validate its Sigstore + SLSA fields, and gate the
install when --require-attestations is on.
Status
LANDED (pending PR merge). Phase 15 ships the offline policy +
verifier surface that the wheel-install loop can dial in today; the
live sigstore-go crypto verifier, the live HTTPS fetch of
<wheel-url>.provenance, and the mochi pkg install flag wiring are
split into sub-phases 15.1 / 15.2 / 15.3 so the umbrella gate stays
deterministic.
Gate
go test ./package3/python/attest/... -count=1
Covers:
ParseStatementhappy + six error paths (no_type/ nopredicateType/ zero subjects / subject missing name / subject missing sha256 / non-JSON).ParseBundlehappy + four error paths (no payload / nopayloadType/ no signatures / non-JSON).(*Bundle).Statement()payload-type guard, base64 round-trip, nil-safety.(*Statement).BuilderID()extraction (deeprunDetails.builder.id) and nil-safety on every layer.- Every
Policy.Verifybranch: missing bundle, unsupportedmediaType, badpayloadType, statement parse failure, wrong_type, wrongpredicateType, subject not found, digest mismatch, builder rejected, trusted-publisher rejection (with and withoutIdentityExtractor), publisher-extractor error. Policy.Requiredgate (returns error when not OK, returns nil error when not Required even with violations).StaticFetcher(empty ->ErrNoAttestation, non-empty -> round trip).HTTPFetcher.AttestationURL(<url>.provenancehappy + non-.whlrejection).Verifier.Verifyend-to-end withStaticFetcher: happy, missing + not-Required, missing + Required, bad-digest + Required, parse-error + Required, fetch-error + not-Required, fetch-error + Required, no-Fetcher error.phase15_test.goumbrella sentinel: trusted-publisher end-to-end, digest-mismatch flagged, Required gate fails the install.
Files
package3/python/attest/
doc.go # 6-step verification pipeline overview
statement.go # in-toto Statement v1 + SLSA Provenance v1 shapes
bundle.go # Sigstore Bundle + DSSE envelope shapes
report.go # Reason codes + Violation + Report
policy.go # Policy + Verify() + WheelTarget + IdentityExtractor
fetcher.go # Fetcher interface + StaticFetcher + HTTPFetcher.AttestationURL
verifier.go # Verifier glues Fetcher + Policy
statement_test.go # 7 tests
bundle_test.go # 6 tests
report_test.go # 2 tests
policy_test.go # 17 tests
fetcher_test.go # 5 tests
verifier_test.go # 9 tests
phase15_test.go # 1 umbrella sentinel (4 sub-cases)
Sub-phase decomposition
Sub-phases land separately so the umbrella stays offline and deterministic.
15.1. Live sigstore-go crypto verifier
The crypto verifier (X.509 chain validation against Fulcio,
SCT verification, Rekor inclusion-proof check, DSSE signature check
against the certificate's public key) ships as a sigstore-go
shell-out. Today the bundle's verificationMaterial is parsed but not
cryptographically verified; a crypto.Verifier hook in the Policy
struct will surface this once 15.1 lands. The hook signature is
roughly func(b *Bundle) error; non-nil errors will flow through
ReasonSignatureNotVerified. Identity (via Fulcio SAN) will be lifted
out so the IdentityExtractor becomes the production
default rather than a test seam.
15.2. Live PyPI HTTP fetcher
Today HTTPFetcher.AttestationURL returns <wheel-url>.provenance
per PEP 740 but HTTPFetcher.Fetch is a stub returning
ErrNoAttestation. Sub-phase 15.2 wires net/http with:
- caching against the same content-addressed cache the wheel loop uses (so repeated installs are zero-RTT).
- retry-with-backoff on transient 5xx.
- 404 ->
ErrNoAttestation(the wheel publisher has not opted in).
15.3. CLI verbs
The CLI surface:
--require-attestations->Policy.Required = true.- repeated
--allowed-builder=<URI>->Policy.AllowedBuilders. - repeated
--trusted-publisher=<identity>->Policy.TrustedPublishers. - machine-readable JSON output of each
Reportfor telemetry (theReasonconstants exist precisely so the CLI can switch on cause without pattern-matching free-form messages).
Skip count
Phase 15 is install-side and does not feed the wrapper-synthesiser skip counter. The fixture-corpus golden counts are unchanged.
Fixtures
Sub-phase 15.2 will add a fixture under package3/python/attest/testdata/
mirroring the publisher-emitted bundle for one of the 25-package
fixture corpus members so the live HTTP path can run offline against
a recorded transcript. Phase 15 itself stays offline.
Cross-references
- PEP 740 (attestation envelope)
- in-toto Statement v1
- SLSA Provenance v1
- Sigstore Bundle v0.3
- phase 11 for the publish-side that produced the bundle this phase verifies.
- MEP-71 spec § Trusted publishing for the normative design.