Skip to content

Verify uploaded package attestations using package:sigstore - #9546

Draft
mosuem wants to merge 2 commits into
accept-and-serve-attestationsfrom
verify-uploaded-attestations
Draft

mosuem wants to merge 2 commits into
accept-and-serve-attestationsfrom
verify-uploaded-attestations

Conversation

@mosuem

@mosuem mosuem commented Aug 19, 2026 •

Copy link
Copy Markdown
Member

Note

This PR was generated by an AI coding agent (Jetski) on behalf of @mosuem.

Second PR in the stack for package attestation support (stacked on top of #9545).

  • Adds the sigstore dependency in app/pubspec.yaml.
  • AttestationVerifier (registered as a service, attestationVerifier) verifies the Sigstore bundle uploaded with a package archive over the archive's sha256 digest that publishUploadedBlob already computes:
    • verifies the Fulcio certificate signer identity against the trusted reusable publishing workflows (defaultTrustedSignerWorkflows),
    • parses the in-toto SLSA v1 DSSE provenance statement from the same bundle JSON (BuildProvenance.parseBundle),
    • enforces the provenance policy (checkProvenancePolicy): binds the archive sha256 and subject name, verifies provenance.sourceRepository against the repository: of the uploaded pubspec.yaml, and - when GitHub Actions automated publishing is enabled for the package - also against publishingConfig.githubConfig.repository.
  • Rejects the upload with PackageRejectedException when the attestation is invalid or fails verification. Internal errors (e.g. the verifier not being available) propagate and fail the request with a logged 500 instead of blaming the upload.
  • Persists the verified facts with the version as PackageVersion.provenance (VersionProvenance, a JSON property like publishingConfig): repository and its immutable repository_id / repository_owner_id, workflow path, ref, commit, run URL, the verified signer identity and issuer, and the Rekor log index / integrated time. The package page (Display SLSA provenance level shield badges and Security tab #9550) and later policy checks (e.g. the TOFU lock on the provenance repository, Attestations: lock the provenance repository on first attested upload (TOFU) #9694) use these instead of re-parsing the bundle.
  • withFakeServices registers a FakeAttestationVerifier that replaces only the cryptographic step, so tests exercise the real provenance parsing and policy with hand-crafted bundles; one test verifies a real Sigstore bundle end-to-end.

Deployment (Dockerfile.app): package:sigstore resolves its native library through a native assets mapping, which dart compile jit-snapshot does not produce. tool/build-server-snapshot.sh embeds a mapping pointing at the library's location in the image into the kernel the app-JIT snapshot is created from (the mechanism dart run uses, cf. dart-lang/pub#4888); a rust-build stage compiles libsigstore_ffi.so from the Rust source bundled with the package. dockerfile_app_build_test.dart runs server.jit self-check in the built image, and the frontend performs the same check at startup.

Comment thread app/lib/package/backend.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.

Thanks for porting the DSSE SLSA provenance parser and policy checks!

Note: this branch currently has merge conflicts with #9545 (accept-and-serve-attestations), so GitHub Actions skipped the All tests workflow on the latest push — please rebase onto #9545 so CI runs.

Also, ~550 lines of app/lib/package/attestation_verifier.dart (GitHubRepository, GitHubWorkflowIdentity, ProvenanceSubject, BuildProvenance, checkProvenancePolicy) are duplicated verbatim from dart-lang/pub#4875. Could we move the SLSA provenance parser/types into package:sigstore (or a shared package) so pub and pub.dev don't drift?

Comment thread app/pubspec.yaml
tar: '2.0.2'
api_builder:
sigstore: ^0.1.0

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.

package:sigstore uses a Native Assets build hook (hook/build.dart) to download/load libsigstore_ffi.so via @ffi.Native.

This will currently fail at runtime in the production container (Dockerfile.app):

  1. Stage 1 compiles a JIT snapshot by running dart compile jit-snapshot -o server.jit bin/server.dart --help (Dockerfile.app:45), which exits before package:sigstore is ever touched, and Stage 2 (FROM scratch, Dockerfile.app:55-80) only copies server.jit and .dart_tool/package_config.json — it does not copy .dart_tool/lib/ or libsigstore_ffi.so (or any glibc/C runtime dependencies if dynamically linked).
  2. We should also avoid downloading prebuilt .so binaries from github.com/mosuem/sigstore/releases over HTTP during build hooks in Dockerfile.app — compare how pkg/signature_verifier is built from Rust source in Stage 1 of Dockerfile.app (lines 1–8), and add a test in dockerfile_app_build_test.dart (or startup self-check) that exercises AttestationVerifier inside the built container.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

You're right, and it's actually worse than copying the .so: dart compile jit-snapshot has no native-assets support at all, and dart run <app-jit snapshot> only injects the asset mapping for kernel files, so even with the library in the image @Native would only resolve via its process-symbol fallback (LD_PRELOAD-style), which I don't want to rely on. With the current Dockerfile the server starts fine and then fails at the first SigstoreClient.create() — which, combined with the catch-all you flagged below, would have turned into a 400 for every attested upload. Not great.

I see three options and would like your/the team's opinion before writing more code:

  1. Subprocess binary, like pkg/signature_verifier: build the Rust crate from package:sigstore's rust/ directory (or a tiny CLI around it) in a Docker stage, copy the static binary into the FROM scratch image, and have AttestationVerifier shell out to it. Fits the existing deployment model and keeps verification off the main isolate.
  2. A non-native-assets loading path in package:sigstore (explicit DynamicLibrary.open), with the .so built from source in the Docker build stage (buildMode: checkout, no downloads).
  3. Switching the app to dart build/AOT — I think that's a non-starter given the Isolate.spawnUri of the .dill entrypoints.

My preference is 1. Either way I'll add a startup self-check and a test that runs the verifier inside the built container, as you suggest.

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.

I also think 1 sounds like the path of least resistance
@jonasfj WDYT?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Correction to my message above, after actually testing it: the FFI route works with the JIT snapshot, no dart build and no subprocess needed — the same mechanism as dart-lang/pub#4888.

What dart run does for native assets is (1) run the build hooks and (2) have the frontend server embed a native assets mapping (asset id → library path) into the kernel; the VM keeps that mapping in the isolate group's object store, and that object store is part of an app-JIT snapshot. So the mapping can be embedded into the kernel that --snapshot-kind=app-jit is created from, with the path the library will have in the image. Reproduced locally in the shape of Dockerfile.app:

dart compile kernel -o server.dill bin/server.dart
frontend_server_aot --native-assets-only --native-assets=native_assets.yaml --output-dill=na.dill
cat server.dill na.dill > combined.dill
dart --snapshot-kind=app-jit --snapshot=server.jit combined.dill --help        # training run, sigstore untouched
cd / && dart server.jit …   # no .dart_tool, no sources, no hooks → verifies the real bundle

The same snapshot without the mapping fails with the production error (Couldn't resolve native function … No available native assets).

Pushed as part of this PR:

  • tool/build-server-snapshot.sh does the four steps above; Dockerfile.app uses it instead of dart compile jit-snapshot.
  • A rust-build stage compiles libsigstore_ffi.so from the Rust source bundled with package:sigstore (the Rust twin of the Go stage; nothing is downloaded from GitHub releases) and the final image gets the library at the mapped path plus libgcc_s, which it needs and which is not in the Dart image's /runtime.
  • dockerfile_app_build_test.dart now also runs the built image: server.jit self-check (new hidden command) calls into the native library, and the frontend entrypoint does the same check at startup so a broken image fails to boot instead of failing every attested upload.

Caveat: --native-assets-only and the mapping format are dartdev/flutter-tool plumbing rather than a documented CLI contract — stable in practice, and the SDK is pinned here anyway, but it's a thing to keep an eye on at SDK rolls (the container test covers that).

So I'd drop option 1 (subprocess) — @jonasfj, unless you prefer native code out-of-process on principle. This would also unblock replacing the Go signature_verifier with package:boring later.

Comment thread app/lib/package/attestation_verifier.dart Outdated
Comment thread app/lib/package/attestation_verifier.dart Outdated
Comment thread app/lib/package/backend.dart Outdated
Comment thread app/lib/package/attestation_verifier.dart
@sigurdm

sigurdm commented Oct 12, 2026

Copy link
Copy Markdown
Contributor

verify-uploaded-attestations still forks off accept-and-serve-attestations at c00d8943 (Sep 3), so it predates 0333863c and 62690868, which moved the attestation upload from a second GCS signed-POST policy (attestationUrl + attestationFields, bundle stored as tmp/<guid>.tar.gz.sigstore.json) to the dedicated POST /api/packages/versions/newUploadAttestation/<uploadId> endpoint (bundle stored as tmp/<guid>.attestation.json). The startUpload/publishUploadedBlob plumbing here is wired to the old layout, and #9549, #9552 and #9550 inherit the same base — the GitHub diff hides this since it is computed from the merge-base.

Could you rebase this PR (and the ones stacked on it) onto the current accept-and-serve-attestations head, so the stack matches the protocol the pub client in dart-lang/pub#4876 now speaks?

@mosuem
mosuem force-pushed the verify-uploaded-attestations branch from 943b0a3 to bb506ed Compare October 12, 2026 07:12
@mosuem

mosuem commented Oct 12, 2026

Copy link
Copy Markdown
Member Author

Rebased onto the current accept-and-serve-attestations head (62690868) — squashed into one commit while resolving the conflicts, the four previous commits were iterations of the same change — so this PR now uses the newUploadAttestation endpoint / tmp/<guid>.attestation.json layout. A second commit addresses the inline review comments (see the threads). The stacked PRs (#9549 → #9552 → #9550) still fork off the old base; I'll rebase them onto this branch next.

On the duplication with dart-lang/pub#4875: agreed, and I'd rather not grow two copies. My preference is a small pure-Dart package for the SLSA provenance parser + GitHub identity types + policy helpers (BuildProvenance, GitHubRepository, GitHubWorkflowIdentity, checkProvenancePolicy), rather than putting it into package:sigstore: pub is vendored into the SDK, a dependency without build hooks is much easier to roll there, and package:sigstore can stay a thin binding. I'll do that extraction as its own PR and switch both dart-lang/pub#4875 and this PR over to it — does that order work for you, or would you prefer the extraction to land first?

@mosuem
mosuem force-pushed the verify-uploaded-attestations branch from 08c6c0f to 0ac8f96 Compare October 12, 2026 08:46
@sigurdm

sigurdm commented Oct 12, 2026

Copy link
Copy Markdown
Contributor

On the shared code: I'd keep it in package:sigstore as a separate pure-Dart library (e.g. package:sigstore/provenance.dart, no import of the bindings) rather than a new package. Both pub and pub.dev depend on package:sigstore either way, so the hooks-free-rolling argument doesn't buy anything, and keeping the parser next to the binding that produces the identity strings it parses avoids skew between the two. A separate package only makes sense if something needs the parser without the native binding, and nothing does today.

Order: extraction first, as a package:sigstore release with provenance_test/policy_test moving along with the code, then switch dart-lang/pub#4875 and this PR over before they land. Review here can continue in the meantime; the switch is mechanical. Since the SDK will end up vendoring package:sigstore, this is also the moment to move it under dart-lang with a verified publisher.

Adds `package:sigstore` and an `AttestationVerifier` service that verifies
the Sigstore bundle uploaded with a package archive before the version is
published:

- verifies the Fulcio certificate signer identity against the trusted
  reusable publishing workflows (`defaultTrustedSignerWorkflows`), over the
  archive's sha256 digest that `publishUploadedBlob` already computed
  (`SigstoreClient.verify(..., isDigest: true)`), so the archive is neither
  read into memory nor hashed again,
- parses the in-toto SLSA v1 DSSE provenance statement from the same
  bundle JSON (`BuildProvenance.parseBundle`),
- enforces the provenance policy (`checkProvenancePolicy`): binds the
  archive sha256 and subject name, verifies `provenance.sourceRepository`
  against the `repository:` of the uploaded `pubspec.yaml`, and - when
  GitHub Actions automated publishing is enabled for the package - also
  against `publishingConfig.githubConfig.repository`,
- rejects the upload with `PackageRejectedException` when the attestation
  is invalid or fails verification; internal errors (e.g. the native
  library missing) propagate as a 500 instead of blaming the upload.

`AttestationVerifier` is a registered service (`registerAttestationVerifier`
/ `attestationVerifier`); `withFakeServices` registers a
`FakeAttestationVerifier` that replaces only the cryptographic step and
trusts the builder self-reported in the statement, so tests exercise the
real provenance parsing and policy with hand-crafted bundles. An end-to-end
test verifies a real signed bundle.

Deployment: `package:sigstore` calls into `libsigstore_ffi.so` through
`@Native` bindings, which the VM resolves through a native assets mapping
that `dart compile jit-snapshot` does not produce. `tool/build-server-
snapshot.sh` compiles the server to kernel, compiles the mapping (asset id
-> path of the library in the image) to a kernel component with the
frontend server, concatenates the two and creates the app-JIT snapshot
from the result; the VM keeps the mapping in the snapshot, so no hooks or
package sources are needed at runtime (the mechanism `dart run` uses, cf.
dart-lang/pub#4888). `Dockerfile.app` builds the library from the Rust
source bundled with the package in a `rust-build` stage (no prebuilt
binaries are downloaded) and copies it, with `libgcc_s`, into the final
image. `server.jit self-check` calls into the native library;
`dockerfile_app_build_test.dart` runs it in the built image, and the
frontend entrypoint performs the same check at startup so that a broken
image fails to boot instead of failing every attested upload.
Adds `PackageVersion.provenance` (`VersionProvenance`, a JSON property
like `publishingConfig`), set at upload from the verification result:
repository (`owner/name`), repository id and owner id (from the signed
statement's `internalParameters.github`), workflow path, ref, commit,
run URL, the verified signer identity and issuer, and the Rekor log index
and integrated time.

These are the facts pub.dev verified at upload time, persisted so that
the package page and later policy checks (e.g. a lock on the provenance
repository, #9694) do not have to parse the bundle again.
@mosuem
mosuem force-pushed the verify-uploaded-attestations branch from f6cd346 to 509f358 Compare October 12, 2026 13:06
mosuem added a commit that referenced this pull request Oct 12, 2026
The attestation is no longer a PackageVersionAsset (it lives in the
canonical bucket, see #9545) and the facts pub.dev verified at upload are
persisted on the PackageVersion (#9546). The Security tab is now rendered
from those, instead of re-deriving them from the bundle (including the
ad-hoc ASN.1 string scan of the certificate), and the security page no
longer loads an asset.
@sigurdm

sigurdm commented Oct 13, 2026

Copy link
Copy Markdown
Contributor

Two policy points on upload verification (#9546 / #9552):

  1. Disallow manual publishing (AuthenticatedUser) when uploading with an attestation:
    Right now --with-attestation can be uploaded using a manual dart pub publish user OAuth token, and when GitHub automated publishing isn't enabled on the package, verification only checks that the bundle's source repository matches the uploaded pubspec.yaml's repository: field. That means a stolen user OAuth token can publish a version built and validly signed by github.com/attacker/foo (with repository: https://github.com/attacker/foo in pubspec.yaml).
    Matching PyPI (PEP 740) and npm, we should only accept attestation bundles when the upload itself is authenticated via automated publishing (OIDC), where the package's configured repository is already established.

  2. Auto-enable "Require attestation" (#9552) on first signed upload:
    The design doc specifies: "Uploading a signed package would automatically enable this, to prevent downgrade attacks." In #9552, attestationConfig.isRequired is currently only toggled manually in the admin UI; we should flip it on automatically when a version with a verified attestation is published.

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