Skip to content

Verify package attestations and enforce provenance policies in pub get/upgrade - #4875

Draft
mosuem wants to merge 10 commits into
publish-with-attestationfrom
verify-sigstore-attestation
Draft

mosuem wants to merge 10 commits into
publish-with-attestationfrom
verify-sigstore-attestation

Conversation

@mosuem

@mosuem mosuem commented Aug 19, 2026 •

Copy link
Copy Markdown
Member

Client-side Ingestion & Cryptographic Verification on pub get / upgrade

When downloading packages in HostedSource, queries the repository for GET /api/packages/<pkg>/versions/<ver>/attestation and runs PubAttestationVerifier against the cached or embedded Sigstore trusted_root.json:

  1. Lockfile & Downgrade Prevention: ProvenancePolicy.enforcePolicy() blocks downgrades from signed to unsigned packages and detects repository switches across versions.
  2. Archive SHA-256 Digest: Verifies downloaded .tar.gz SHA-256 hash matches the in-toto subject digest in the attestation.
  3. Package Name & Version: Verifies in-toto subject name matches the requested <package>-<version>.tar.gz.
  4. DSSE Envelope & PAE Signatures: Validates signatures over DSSE Pre-Authentication Encoding.
  5. Fulcio Root CA Verification: Validates leaf certificate against trusted_root.json Fulcio Root CAs.
  6. Rekor Transparency Log Proof: Verifies Rekor inclusion proof against active public keys in trusted_root.json.
  7. GitHub Actions OIDC Issuer: Enforces https://token.actions.githubusercontent.com issuer identity.
  8. Signer Workflow & Git Ref: Validates signer workflow ID, branch/tag ref, and commit SHA against SLSA provenance.
  9. Package Repository Binding: Verifies certificate repository matches pubspec.yaml's declared repository.

Tests

  • test/get/hosted/attestation_verification_test.dart
  • test/sigstore/verifier_test.dart
  • test/sigstore/trusted_root_test.dart
  • test/sigstore/lockfile_policy_test.dart

@mosuem
mosuem force-pushed the verify-sigstore-attestation branch from ea5aaba to c218fa1 Compare August 19, 2026 15:19
@mosuem mosuem changed the title Support package attestations in client publish and get/upgrade Verify package attestations and enforce provenance policies in pub get/upgrade Aug 19, 2026
@mosuem
mosuem changed the base branch from master to publish-with-attestation August 19, 2026 15:20
@mosuem
mosuem force-pushed the verify-sigstore-attestation branch 3 times, most recently from 79adede to 47dde5e Compare August 24, 2026 12:18
@mosuem
mosuem force-pushed the verify-sigstore-attestation branch from 47dde5e to 28c09b4 Compare August 24, 2026 12:22
@mosuem
mosuem force-pushed the verify-sigstore-attestation branch from 28c09b4 to dc37a88 Compare August 24, 2026 12:40
@mosuem
mosuem force-pushed the verify-sigstore-attestation branch 3 times, most recently from b1dc5ea to 2d3e6e9 Compare August 24, 2026 12:59
@mosuem
mosuem force-pushed the verify-sigstore-attestation branch from 2d3e6e9 to e397ce2 Compare August 24, 2026 13:04
@mosuem
mosuem force-pushed the verify-sigstore-attestation branch 2 times, most recently from d3c6c4a to e0c3456 Compare August 24, 2026 15:05
@mosuem
mosuem force-pushed the verify-sigstore-attestation branch from e0c3456 to c33ac01 Compare August 24, 2026 15:40
@mosuem
mosuem force-pushed the verify-sigstore-attestation branch from c33ac01 to d6008e4 Compare August 24, 2026 15:46
@mosuem
mosuem force-pushed the verify-sigstore-attestation branch 2 times, most recently from 547c547 to 2f0bd9e Compare August 24, 2026 15:53
@mosuem
mosuem force-pushed the verify-sigstore-attestation branch from 70d774f to 90f3bf1 Compare August 27, 2026 12:21
@mosuem
mosuem force-pushed the verify-sigstore-attestation branch from 90f3bf1 to 95f8ad3 Compare August 27, 2026 12:37
@mosuem
mosuem force-pushed the verify-sigstore-attestation branch 2 times, most recently from c4ff175 to e61d51f Compare August 28, 2026 11:14
@mosuem
mosuem force-pushed the verify-sigstore-attestation branch from e61d51f to 8761f5d Compare August 28, 2026 12:13
@mosuem
mosuem force-pushed the verify-sigstore-attestation branch from 8761f5d to 5e81745 Compare August 28, 2026 12:49
@mosuem
mosuem force-pushed the verify-sigstore-attestation branch from 5e81745 to c41eb61 Compare August 31, 2026 13:58
Comment thread lib/src/source/hosted.dart Outdated

@sigurdm sigurdm 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.

The Sep 24 rewrite addresses all three points from my earlier comment — thanks. Two policy-level problems remain before this can go in, plus a handful of smaller things inline.

What the client check proves. As implemented, a passing verification means: the bundle was signed by dart-lang/setup-dart/.github/workflows/publish.yml, and the archive was built from the GitHub repository that the archive's own pubspec names. It does not establish that this repository is the one authorized for the package — that binding lives on pub.dev at publish time, and on the client a switch is a warning (and only on machines with a marker). I think that is fine for v1, but the PR description ("Package Repository Binding") and the spec text should say exactly this.

Trigger field. slsa_level only appears on the pub-dev side in dart-lang/pub-dev#9550; it should be split out of the badge PR into #9546 so the API change can land with verification.

Also note the branch is one commit behind publish-with-attestation (789afc51), so doc/repository-spec-v2.md will conflict on rebase.

Comment on lines +1639 to +1644
final previousProvenance = packageProvenanceFromCache(description, cache);
// Attestations are fetched when the repository says there is one, but also
// when we have seen one before: the repository must not be able to skip
// verification by simply not reporting `slsa_level`.
final expectAttestation =
versionInfo.slsaLevel != null || previousProvenance != null;

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.

The marker is per package, not per version, so once any attested version has been downloaded on a machine, every unattested version of that package becomes uninstallable there: dart pub downgrade (which most dart-lang repos run in CI), a second project pinning a pre-attestation version, pub cache repair. And it only happens on machines that have the marker, so a dev box and fresh CI behave differently. The test covers the newer-version case only.

The marker needs a version dimension — e.g. record the lowest attested version seen and only require attestations for versions above it — and the lockfile provenance of the version being downloaded should feed into expectAttestation too (see below); that is the cross-machine signal CI can actually use.

Comment on lines +76 to +84
if (previousLockedProvenance != null && currentProvenance == null) {
throw PackageProvenanceException('''
Package "$packageName" version $version is not signed with provenance.
However, previously locked versions of this package were signed by:
${previousLockedProvenance.repository}

Downgrading from a signed package to an unsigned package is prohibited.
''');
}

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.

Nothing here is about the lockfile or about downgrading — the caller passes the cache marker, and the condition fires for older versions just as well as newer ones. The parameter name and the message should describe what is actually compared, and the message needs to tell the user what to do (which there currently is no way to do).

Comment on lines +2216 to +2225
/// Returns a description with [newProvenance], if it is not `null`.
///
/// A known provenance is never cleared: that would silently drop the
/// repository binding of an already verified package.
ResolvedHostedDescription withProvenance(String? newProvenance) =>
ResolvedHostedDescription(
description,
sha256: sha256,
provenance: newProvenance ?? provenance,
);

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.

This is the wrong way round: if the download just performed was not verified (repository stopped reporting slsa_level, no marker on this machine), keeping the old value makes pubspec.lock assert a binding this download did not establish — and hides the change from code review. Either clear it, or better, treat a lockfile provenance as "attestation expected" in _downloadAndExtract so this situation fails like the marker case does.

Comment thread lib/src/sigstore/verifier.dart Outdated
Comment thread lib/src/sigstore/verifier.dart
Comment thread lib/src/source/hosted.dart Outdated
Comment thread lib/src/source/hosted.dart Outdated
Comment thread doc/repository-spec-v2.md
mosuem added 10 commits October 12, 2026 16:49
Adds loadTrustedRoot() and loadTrustedRootJson() in package:pub to resolve
the Sigstore root of trust from:
1. Explicit override paths passed to the loader.
2. The PUB_SIGSTORE_TRUST_ROOT environment variable.
3. The built Dart SDK distribution at lib/_internal/sigstore/trusted_root.json.
4. The Dart SDK repository checkout at third_party/sigstore/trusted_root.json.
…t/upgrade

- Adds `package:sigstore` dependency from labs monorepo.
- Implements `PubAttestationVerifier` wrapping Sigstore attestation verifier.
- Fetches and cryptographically verifies Sigstore attestations during hosted downloads in `pub get` and `pub upgrade`.
- Enforces lockfile downgrade protection, SLSA provenance, digest match, and certificate roots.
- Adds comprehensive unit and integration tests.
…ed root handling

- `PubAttestationVerifier.verify` takes the archive's sha256 digest (the
  content hash computed while the archive was downloaded) and passes it to
  `package:sigstore` with `isDigest: true`, instead of reading the archive
  back and hashing it twice more.
- The attestation is fetched in its own request with its own retries, after
  the archive download: a failing attestation endpoint no longer re-downloads
  the archive on every attempt, and is reported as such (naming the endpoint,
  and pointing at proxies that do not forward it) instead of as a failure to
  find the package. A `401`/`403` from the endpoint is handled the same way.
- The trusted root is loaded outside of the verification `try`: a missing
  `PUB_SIGSTORE_TRUST_ROOT` file fails as a local configuration error instead
  of being attributed to the attestation; a corrupt cached
  `trusted_root.json` is ignored with a warning and the built-in root is used.
- `requireTaggedSignerRef` stays `false` while the publishing workflow lives
  on a branch; the default and the `job_workflow_sha` TODO now reference the
  tracking issue #4942.
- repository-spec-v2.md: spell out that the attestation must be retrievable
  before the listing advertises it, and that proxies must forward the
  endpoint; drop the `Accept` header the client does not send.
@sigurdm

sigurdm commented Oct 13, 2026

Copy link
Copy Markdown
Contributor

Two follow-ups on HostedSource and the lockfile/cache-marker threads:

  1. Cache entries populated by an older pub client skip attestation verification (lib/src/source/hosted.dart lines 1321–1331):
    In downloadToSystemCache, packageDir is only invalidated when !fileExists(hashPath(id, cache)). If a pre-attestation pub client (Dart >= 2.19) already populated packageDir and hosted-hashes/<server>/<pkg>-<version>.sha256, dirExists(packageDir) is true and downloadToSystemCache skips fetching/verifying the attestation even when the package is attested (!fileExists(provenancePath(id, cache))).
    Since _verifyAttestation now takes archiveSha256 (contentHash) and extractedPath (packageDir), when dirExists(packageDir) is true, an attestation is expected, and !fileExists(provenancePath(id, cache)) (and !cache.isOffline), we can fetch the attestation, run _verifyAttestation against contentHash and packageDir without re-downloading the archive, and write <pkg>-<version>.provenance.

  2. Avoiding provenance in pubspec.lock and dropping the versionless <pkg>.provenance cache marker:
    For the three open threads (packageProvenancePath, lockfile_policy.dart, and ResolvedHostedDescription.provenance) and #4903, I posted a proposal in the design doc (comment link) to declare provenance in the package's pubspec.yaml (bound by sha256 in pubspec.lock) rather than adding a provenance: field to pubspec.lock or a versionless <pkg>.provenance marker in PUB_CACHE. Let's align on that in the doc before reworking those three threads.

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.

2 participants