Add attestation download API endpoint, add download_url
Background
The grouppipeline security group is working towards providing users with SLSA Level 3 Provenance Attestations. Quoting from the SLSA documentation, it states that attestations are:
It’s the verifiable information about software artifacts describing where, when, and how something was produced. For higher SLSA levels and more resilient integrity guarantees, provenance requirements are stricter and need a deeper, more technical understanding of the predicate. Describe how an artifact or set of artifacts was produced so that:
- Consumers of the provenance can verify that the artifact was built according to expectations.
- Others can rebuild the artifact, if desired.
As a simplified TL;DR, in the context of GitLab, a provenance statement is a JSON document that correlates the SHA-256 sum of an artifact with the build information. A worker then performs a digital signature, called a provenance attestation, stored as a "Sigstore Bundle" blob. This is a highly sought-after feature, particularly for our GitLab Ultimate customers.
Why this MR?
This merge request addresses two problems identified in the "Polish the MVC end-to-end Attestation workflow" ticket:
- Add a new API endpoint for retrieving the bundle. This bundle will be retrieved by
glabto verify the integrity of artifacts. More information available in "Attestation verification via glab CLI". - Add two new fields to the
listendpoint for attestations. iid was previously missing from the list endpoint and download_url is related to this MR.
References
- Add download_url, iid to Attestation API payload
- Add attestation download API endpoint
- Polish the MVC end-to-end Attestation workflow
- [FF]
slsa_provenance_statement-- Roll out feature flag to publish SLSA provenance statements
How to set up and validate locally
You can create test data as below. You can also do a full end-to-end run of the ProvenanceService locally.
a = SupplyChain::Attestation.new do |a|
a.project_id = 9
tf = Tempfile.new
tf.write("this is the bundle contents")
a.file = tf
a.predicate_kind = :provenance
a.predicate_type = "http://example.com/predicate"
a.subject_digest = "5db1fee4b5703808c48078a76768b155b421b210c0761cd6a5d223f4d99f1eaa"
end
a.save
http http://gdk.test:3000/api/v4/projects/root%2ftest-child-pipeline/attestations/5db1fee4b5703808c48078a76768b155b421b210c0761cd6a5d223f4d99f1eaa
HTTP/1.1 200 OK
[...]
[
{
"build_id": null,
"created_at": "2025-11-12T01:02:26.254Z",
"download_url": "http://gdk.test:3000/api/v4/projects/9/attestations/1/download",
"expire_at": null,
"id": 1,
"iid": 1,
"predicate_kind": "provenance",
"predicate_type": "http://example.com/predicate",
"project_id": 9,
"status": "success",
"subject_digest": "5db1fee4b5703808c48078a76768b155b421b210c0761cd6a5d223f4d99f1eaa",
"updated_at": "2025-11-12T01:02:26.254Z"
},
{
"build_id": null,
"created_at": "2025-11-12T01:09:58.480Z",
"download_url": "http://gdk.test:3000/api/v4/projects/9/attestations/5/download",
"expire_at": null,
"id": 6,
"iid": 5,
"predicate_kind": "sbom",
"predicate_type": "http://example.com/predicate",
"project_id": 9,
"status": "success",
"subject_digest": "5db1fee4b5703808c48078a76768b155b421b210c0761cd6a5d223f4d99f1eaa",
"updated_at": "2025-11-12T21:49:51.016Z"
}
]
http http://gdk.test:3000/api/v4/projects/9/attestations/6/download
HTTP/1.1 200 OK
Cache-Control: max-age=0, private, must-revalidate
Content-Disposition: attachment; filename=sigstore.bundle
Content-Length: 27
Content-Security-Policy: default-src 'none'
Content-Type: application/vnd.dev.sigstore.bundle.v0.3+json
Date: Mon, 17 Nov 2025 01:53:10 GMT
ETag: W/"3844ef756a75f7845a432ee27d70632a"
Nel: {"max_age": 0}
Vary: Origin
X-Content-Type-Options: nosniff
X-Frame-Options: SAMEORIGIN
X-Gitlab-Meta: {"correlation_id":"01KA7R843YW7ZD48QQ1480Y28V","version":"1"}
X-Request-Id: 01KA7R843YW7ZD48QQ1480Y28V
X-Runtime: 2.260871
this is the bundle contents
The image below shows the user navigating directly to the download endpoint:
Database
https://console.postgres.ai/gitlab/gitlab-production-ci/sessions/45571/commands/139696
SELECT "slsa_attestations".* FROM "slsa_attestations" WHERE "slsa_attestations"."project_id" = 9 AND "slsa_attestations"."iid" = 6 ORDER BY "slsa_attestations"."id" ASC LIMIT 2
Limit (cost=0.01..0.02 rows=1 width=154) (actual time=0.032..0.032 rows=0 loops=1)
Buffers: shared hit=3
-> Sort (cost=0.01..0.02 rows=1 width=154) (actual time=0.031..0.031 rows=0 loops=1)
Sort Key: slsa_attestations.id
Sort Method: quicksort Memory: 25kB
Buffers: shared hit=3
-> Seq Scan on public.slsa_attestations (cost=0.00..0.00 rows=1 width=154) (actual time=0.005..0.005 rows=0 loops=1)
Filter: ((slsa_attestations.project_id = 9) AND (slsa_attestations.iid = 6))
Rows Removed by Filter: 0
Settings: work_mem = '100MB', effective_cache_size = '338688MB', random_page_cost = '1.5', jit = 'off', seq_page_cost = '4'
Please note that an index exists. I don't know why the database is not using the plan, it may be that it considers the above faster than using the index.
"index_slsa_attestations_on_project_id_iid" UNIQUE, btree (project_id, iid)
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.
Related to #578751 (closed)
