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 glab to verify the integrity of artifacts. More information available in "Attestation verification via glab CLI".
  • Add two new fields to the list endpoint for attestations. iid was previously missing from the list endpoint and download_url is related to this MR.

References

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:

image

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)

Edited by Sam Roque-Worcel

Merge request reports

Loading