Repository navigation
Conversation
232fa4f to
6061d24
Compare
bdf7ccb to
f3f169e
Compare
f3f169e to
72390ad
Compare
sigurdm
left a comment
There was a problem hiding this comment.
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?
| tar: '2.0.2' | ||
| api_builder: | ||
| sigstore: ^0.1.0 | ||
|
|
There was a problem hiding this comment.
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):
- Stage 1 compiles a JIT snapshot by running
dart compile jit-snapshot -o server.jit bin/server.dart --help(Dockerfile.app:45), which exits beforepackage:sigstoreis ever touched, and Stage 2 (FROM scratch,Dockerfile.app:55-80) only copiesserver.jitand.dart_tool/package_config.json— it does not copy.dart_tool/lib/orlibsigstore_ffi.so(or any glibc/C runtime dependencies if dynamically linked). - We should also avoid downloading prebuilt
.sobinaries fromgithub.com/mosuem/sigstore/releasesover HTTP during build hooks inDockerfile.app— compare howpkg/signature_verifieris built from Rust source in Stage 1 ofDockerfile.app(lines 1–8), and add a test indockerfile_app_build_test.dart(or startup self-check) that exercisesAttestationVerifierinside the built container.
There was a problem hiding this comment.
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:
- Subprocess binary, like
pkg/signature_verifier: build the Rust crate frompackage:sigstore'srust/directory (or a tiny CLI around it) in a Docker stage, copy the static binary into theFROM scratchimage, and haveAttestationVerifiershell out to it. Fits the existing deployment model and keeps verification off the main isolate. - A non-native-assets loading path in
package:sigstore(explicitDynamicLibrary.open), with the.sobuilt from source in the Docker build stage (buildMode: checkout, no downloads). - Switching the app to
dart build/AOT — I think that's a non-starter given theIsolate.spawnUriof the.dillentrypoints.
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.
There was a problem hiding this comment.
I also think 1 sounds like the path of least resistance
@jonasfj WDYT?
There was a problem hiding this comment.
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.shdoes the four steps above;Dockerfile.appuses it instead ofdart compile jit-snapshot.- A
rust-buildstage compileslibsigstore_ffi.sofrom the Rust source bundled withpackage: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 pluslibgcc_s, which it needs and which is not in the Dart image's/runtime. dockerfile_app_build_test.dartnow 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.
|
Could you rebase this PR (and the ones stacked on it) onto the current |
943b0a3 to
bb506ed
Compare
|
Rebased onto the current 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 ( |
08c6c0f to
0ac8f96
Compare
|
On the shared code: I'd keep it in Order: extraction first, as a |
0ac8f96 to
f6cd346
Compare
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.
f6cd346 to
509f358
Compare
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.
|
Two policy points on upload verification (
|
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).
sigstoredependency inapp/pubspec.yaml.AttestationVerifier(registered as a service,attestationVerifier) verifies the Sigstore bundle uploaded with a package archive over the archive's sha256 digest thatpublishUploadedBlobalready computes:defaultTrustedSignerWorkflows),BuildProvenance.parseBundle),checkProvenancePolicy): binds the archive sha256 and subject name, verifiesprovenance.sourceRepositoryagainst therepository:of the uploadedpubspec.yaml, and - when GitHub Actions automated publishing is enabled for the package - also againstpublishingConfig.githubConfig.repository.PackageRejectedExceptionwhen 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.PackageVersion.provenance(VersionProvenance, a JSON property likepublishingConfig): repository and its immutablerepository_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.withFakeServicesregisters aFakeAttestationVerifierthat 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:sigstoreresolves its native library through a native assets mapping, whichdart compile jit-snapshotdoes not produce.tool/build-server-snapshot.shembeds a mapping pointing at the library's location in the image into the kernel the app-JIT snapshot is created from (the mechanismdart runuses, cf. dart-lang/pub#4888); arust-buildstage compileslibsigstore_ffi.sofrom the Rust source bundled with the package.dockerfile_app_build_test.dartrunsserver.jit self-checkin the built image, and the frontend performs the same check at startup.