Sign LOCAL_CA SLSA attestations with the project CA
What does this MR do and why?
This MR connects the LOCAL_CA signing material to ProvenancePublisher#cosign_attest_blob. When ATTEST_METHOD resolves to LOCAL_CA, attestations are now signed with the project CA instead of going through Sigstore. The SIGSTORE path is unchanged.
The earlier MRs in this stack added the CA, the leaf certificates, the key import, the trusted root and the offline signing config. This is the step that uses them together.
- Shared validation (hash, blob name) stays at the top.
SIGSTOREthen validates the ID token and runs cosign with the same flags and values as before. Only the order changes:--yesand--bundle, which both methods use, now come right after--hash. cosign does not depend on flag order. LOCAL_CApasses--key,--certificate,--trusted-rootand--signing-configin place of--identity-token,--oidc-issuerand--use-signing-config=false. The key password is only set in the cosign process environment (COSIGN_PASSWORD) and never appears on the command line. The ID token is not passed, as cosign does not use it when signing with a key.LOCAL_CAdoes not send any Sigstore service URLs.--fulcio-url/--rekor-urlare not added, andCOSIGN_FULCIO_URL/COSIGN_REKOR_URLare unset for the cosign process. cosign readsCOSIGN_<FLAG>environment variables as flag values and refuses to sign when service URLs are combined with--signing-config, so without this, LOCAL_CA would fail on any host that exports those variables (GDK with a local Sigstore stack does).- Successful
LOCAL_CAattestations record thesigning_certificate_idof the CA that issued the leaf. Error attestations keep a nil certificate, because most failures happen before a certificate exists. - The shared command body moves into
run_cosign_attest_blob, with two small builders,sigstore_argumentsandlocal_ca_arguments, so each path is easy to follow. The flags both methods use (--yes,--bundle) live in the shared base command.
With the offline signing config, the bundle contains the leaf certificate but no transparency log entry or SCT. Verification therefore needs the project trust root and --insecure-ignore-tlog --insecure-ignore-sct, as agreed in https://gitlab.com/gitlab-org/gitlab/-/work_items/630121. Support for this in glab is tracked in https://gitlab.com/gitlab-org/gitlab/-/work_items/603915.
Who this affects. This is the first MR in the stack that changes runtime behaviour. Everything remains behind the slsa_provenance_statement feature flag (default off, https://gitlab.com/gitlab-org/gitlab/-/work_items/547866), and Ci::Slsa::PublishProvenanceService still only accepts public projects until the next MR. After this MR, a build is signed with the project CA when the flag is enabled for its project and either:
- the instance is self-managed or Dedicated, where
LOCAL_CAis the default, or - the job sets
ATTEST_METHOD: LOCAL_CA.
Until now, these builds were still signed keylessly through Sigstore, while !263571 (merged) already recorded them as local_ca. Any existing local_ca rows therefore contain Sigstore bundles, and this MR resolves that mismatch going forward.
There are no database, API, UI or documentation changes. The documentation will follow with the MR that enables private projects.
Tests: spec/lib/supply_chain/provenance_publisher_spec.rb checks the exact LOCAL_CA command and environment, the persisted row and certificate, and that the ID token is not validated. It also checks that invalid input is rejected before any signing material is created, and covers both attest-blob failures and signing material failures. The artifact and container publisher specs cover LOCAL_CA end to end through publish. The three publisher files have full line and branch coverage.
Manual testing: I ran a pipeline on GDK, using real cosign v3.1.3 and the local Sigstore stack, with a SIGSTORE job and a LOCAL_CA job side by side. Both attestations verified, the LOCAL_CA rows referenced the project CA, and Rekor only recorded an entry for the SIGSTORE artifact. I also ran 38 further checks covering the bundle contents, verification failures, offline signing, each failure path and cleanup of the temporary files.
References
- Related to https://gitlab.com/gitlab-org/gitlab/-/work_items/603913
- Previous MR in this stack (merged): !263845 (merged)
- Earlier MRs: !263571 (merged), !263213 (merged), !261474 (merged), !261090 (merged), !261751 (merged), !262054 (merged)
- Verification approach: https://gitlab.com/gitlab-org/gitlab/-/work_items/630121
- Epic: https://gitlab.com/groups/gitlab-org/-/work_items/22313
- ADR 007: https://handbook.gitlab.com/handbook/engineering/architecture/design-documents/slsa_level_3/decisions/007_attestations_private_dedicated_self_hosted/
Screenshots or screen recordings
Not applicable, as this is a backend-only change.
How to set up and validate locally
This requires cosign on the Sidekiq host and a runner.
-
Enable the feature flag for a public project:
project = Project.find_by_full_path('root/slsa-test') Feature.enable(:slsa_provenance_statement, project) -
Add the following job to the project's
.gitlab-ci.ymland run a pipeline:build-local-ca: stage: build image: alpine:latest variables: ATTEST_BUILD_ARTIFACTS: "true" ATTEST_METHOD: "LOCAL_CA" script: - echo "hello from $CI_JOB_ID" > artifact.txt artifacts: paths: - artifact.txt -
After the job finishes, check the attestation in a Rails console and export the CA and bundle:
attestation = SupplyChain::Attestation.where(project: project).last attestation.slice(:status, :signing_method, :signing_certificate_id) # => {"status"=>"success", "signing_method"=>"local_ca", "signing_certificate_id"=><id of the active CA>} File.write('/tmp/ca.crt.pem', SupplyChain::PostgresCaDriver.new(project).ca_certificate.to_pem) File.binwrite('/tmp/bundle.json', attestation.file.read) -
Download
artifact.txtfrom the job, build a trusted root from the CA, and verify the attestation:GITLAB_URL=http://gdk.test:3000 cosign trusted-root create --fulcio "url=$GITLAB_URL,certificate-chain=/tmp/ca.crt.pem" \ --no-default-fulcio --no-default-rekor --no-default-ctfe --no-default-tsa --out /tmp/trusted-root.json cosign verify-blob-attestation --bundle /tmp/bundle.json --trusted-root /tmp/trusted-root.json \ --certificate-identity "$GITLAB_URL/root/slsa-test//.gitlab-ci.yml@refs/heads/main" \ --certificate-oidc-issuer "$GITLAB_URL" --type slsaprovenance1 \ --insecure-ignore-tlog --insecure-ignore-sct artifact.txt # Verified OK
MR acceptance checklist
Evaluate this MR against the MR acceptance checklist. It helps you analyze changes to reduce risks in quality, performance, reliability, security, and maintainability.