Docker Image attestation with GitHub attestations

This article takes a detailed look at GitHub‘s attestation feature, one of several options for creating and verifying attestations for Docker images and files.

Docker Image signing and attestation series

This article is part of a multi-part series:

Introduction

GitHub attestations (docs) allow cryptographic signing (and verification) of artifacts that you build and publish in your GitHub Actions (GHA) workflows. Any attestation type is supported, with dedicated support for build provenance and SBOMs. You can create attestations for Docker images or regular files.

GitHub did not invent its own format for attestations (as Docker did, see the last part of this series), but made the smart move of building upon the existing Sigstore project, that is, GitHub uses Cosign signatures, specifically the new Cosign Bundle Specification. This makes it possible to leverage existing tools for automated verification, such as the Kubernetes operator Kyverno or Sigstore’s Policy Controller.

Heads up if you are unfamiliar with Sigstore / Cosign

To keep this article concise, I assume you are familiar with Cosign signatures. If not, I highly recommend you read my other article first, which explains Cosign signatures in detail.

Why should you create an attestation in your GHA workflow? Because it allows consumers who download your artifacts to manually or automatically verify that the downloaded artifacts have really been built by a GHA workflow of that repo, and were not replaced by a malicious party afterward. As you might know, a bad actor who got their hands on a Personal Access Token (that can manipulate GitHub release artifacts) could simply replace artifacts generated by a GHA workflow with malicious versions (e.g. binaries with a backdoor), or create new tampered releases.

Limitations of GitHub attestations

GitHub’s attestations only work on projects hosted on github.com! Self-hosted GitHub Enterprise Server is not supported. If your repository is private, you must be on the Enterprise Cloud paid plan. For public repositories, GitHub attestations are free.

If these restrictions bother you, other solutions exist for attesting build provenance, e.g. the SLSA Framework’s provenance generator from the OpenSSF Scorecard ecosystem. That project was started in mid-2022 (thus, it’s more mature than GitHub’s solution that launched in May 2024) and has fewer restrictions.

Build provenance attestation in detail

Creating build provenance for one or more files

Before we look at how to create build provenance for containers, let’s start with regular files:

  • Somewhere in your GHA workflow, you have a step that generates or builds artifacts, which then exist on the file system of the GHA runner. Suppose this step is called the “build” step. This step might also have uploaded the artifacts (e.g. as GitHub release). This is not a problem. It is fine to create attestations after the artifacts are uploaded since the attestation itself is not part of a GitHub release.
  • Some place after the “build” step, you add a “build provenance” step that uses the actions/attest-build-provenance action. You configure the action to use the file(s) generated by the “build” step. You would typically either set that action’s subject-path parameter (which can be a relative path to a specific file, or a folder-path with a glob star, such as
    ./dist/*”), or you provide the subject-checksums parameter, which is a relative path to a checksums text file containing the checksums and file names separated by space, see e.g. here.
  • The actions/attest-build-provenance action determines a list of subjects, which is an array of tuples (file-path, SHA256-checksum). It then creates a Sigstore attestation for these subjects, which it uploads to GitHub’s attestation API. While doing so, it also creates a Rekor entry (whose ID/URL is shown in the output of the GitHub workflow log). You can inspect this attestation in the web UI of GitHub, see e.g. here. From there, you can also download the attestation as a JSON file.

The attestation looks like this:

{
  "mediaType": "application/vnd.dev.sigstore.bundle.v0.3+json",
  "verificationMaterial": {
    "tlogEntries": [
      {
        "logIndex": "166757272",
        "logId": {
          "keyId": "wNI9atQGlz+VWfO6LRygH4QUfY/8W4RFwiT5i5WRgB0="
        },
        "kindVersion": {
          "kind": "dsse",
          "version": "0.0.1"
        },
        "integratedTime": "1738175154",
        "inclusionPromise": {
          "signedEntryTimestamp": "MEUCIBIkocHFd2FfIAHQ61sj2DD8OsbaPsnfEbUFSBas5um4AiEAsd5w6N7EkXymJ4o4KBJ/TgZy5CFLAcic/zTgyYxXuX8="
        },
        "inclusionProof": {
          "logIndex": "44853010",
          "rootHash": "ZyUhNorXcjOdVI8zMCoXRdDdwX1FqOJKLYA7jucV5YM=",
          "treeSize": "44853011",
          "hashes": [
            "0WgNncx83uh6E1d7iqwMPXM24QYGTN0oS1+/lm/o3GI=",
			...
          ],
          "checkpoint": {
            "envelope": "rekor.sigstore.dev - 1193050959916656506\n44853011\nZyUhNorXcjOdVI8zMCoXRdDdwX1FqOJKLYA7jucV5YM=\n\n— rekor.sigstore.dev wNI9ajBFAiEAnr2IW2iv6S3KC5JYVQQxTQtzxKTQ+zzk22iT5HSBG4kCIBCL0i+rkgyS307shaKC8wf6qHw2eTFgYOd5VxXdy1Lm\n"
          }
        },
        "canonicalizedBody": "eyJhcGlWZXJzaW9uIjoiM..."
      }
    ],
    "timestampVerificationData": {
    },
    "certificate": {
      "rawBytes": "MIIG2zCCBmGgAwIBA..."
    }
  },
  "dsseEnvelope": {
    "payload": "eyJfdHlwZ...",
    "payloadType": "application/vnd.in-toto+json",
    "signatures": [
      {
        "sig": "MEUCIH3ZlePHR6CZWIp1JoSIWuE+NC5uWCjKjaTUxtNlwKDZAiEAl0ePXAgzyrp8GPCk/XkvjIDSfUGf/FCKODrPPfKLbOk="
      }
    ]
  }
}
Code language: JSON / JSON with Comments (json)

From this, we can infer the Rekor log entry (166757272). Decoding the payload provides information about the covered files (and their hashes) and the build provenance itself:

{
  "_type": "https://in-toto.io/Statement/v1",
  "subject": [
    {
      "name": "directory-checksum_1.4.7_darwin_amd64",
      "digest": {
        "sha256": "cdfd3b3eb1184d0d1b2228296327a5da1c0f8f60d26b6c95f28953334088c8a8"
      }
    },
    {
      "name": "directory-checksum_1.4.7_darwin_amd64.sbom.json",
      "digest": {
        "sha256": "4d1156e55c42d6e79214214253db871f2b79d4efde924686ebc60e3ca0d79118"
      }
    },
    ...
  ],
  "predicateType": "https://slsa.dev/provenance/v1",
  "predicate": {
    "buildDefinition": {
      "buildType": "https://actions.github.io/buildtypes/workflow/v1",
      "externalParameters": {
        "workflow": {
          "ref": "refs/tags/v1.4.7",
          "repository": "https://github.com/MShekow/directory-checksum",
          "path": ".github/workflows/cd.yml"
        }
      },
      "internalParameters": {
        "github": {
          "event_name": "push",
          "repository_id": "580718346",
          "repository_owner_id": "10126618",
          "runner_environment": "github-hosted"
        }
      },
      "resolvedDependencies": [
        {
          "uri": "git+https://github.com/MShekow/directory-checksum@refs/tags/v1.4.7",
          "digest": {
            "gitCommit": "71b86925572fb97c96f0de596d6cce18df9cc515"
          }
        }
      ]
    },
    "runDetails": {
      "builder": {
        "id": "https://github.com/MShekow/directory-checksum/.github/workflows/cd.yml@refs/tags/v1.4.7"
      },
      "metadata": {
        "invocationId": "https://github.com/MShekow/directory-checksum/actions/runs/13037865884/attempts/1"
      }
    }
  }
}
Code language: JSON / JSON with Comments (json)

Verifying build provenance for individual files

Suppose you are the consumer of some CLI that an open-source project offers as precompiled binary in their GitHub releases. Suppose that the project has created build provenance as described above.

To verify the build provenance created by the actions/attest-build-provenance action for a specific file, you first need to install the GitHub CLI (gh) and be logged into GitHub with it. GitHub’s hosted runners come with the CLI pre-installed, but you can also install it for various OSes with various package managers.

Next, you download the file (e.g. the open-source CLI) you want to verify to disk, e.g. with curl. You’ll assume that there is a GitHub attestation available for that file. For any project, just open https://github.com/<owner>/<repo>/attestations to check whether any attestations are available.

Next, run “gh attestation verify <path-to-file> --repo <owner>/<repo>”. There are various other CLI arguments documented here.

The GitHub CLI then performs these steps:

  • It computes the SHA256 checksum of the local file
  • It calls https://api.github.com/orgs/<owner>/attestations/sha256:<hash> to retrieve existing attestation signatures for that file’s checksum
  • It verifies that
    • the signature is cryptographically valid (that it was really created by GitHub and not by someone else), which includes contacting the Rekor transparency log
    • the signature has the correct attestation type (here: build provenance, which is the https://slsa.dev/provenance/v1 predicate type, which is the implicit default value)
    • the repository owner and repo name (that you provided as CLI arguments) match the ones from the signature
Loaded 1 attestation from GitHub API

The following policy criteria will be enforced:
- OIDC Issuer must match:................... https://token.actions.githubusercontent.com
- Source Repository Owner URI must match:... https://github.com/MShekow
- Source Repository URI must match:......... https://github.com/MShekow/attestation-experiment
- Predicate type must match:................ https://slsa.dev/provenance/v1
- Subject Alternative Name must match regex: (?i)^https://github.com/MShekow/attestation-experiment/

Verifying attestation 1/1 against the configured Sigstore trust roots
Attempting verification against issuer "sigstore.dev"
SUCCESS - attestation signature verified with "sigstore.dev"Code language: JavaScript (javascript)

Alternatively, you could have used the cosign CLI to do the verification, as also explained in this blog post: you manually download the attestation JSON file from GitHub (e.g. to a file named attestation.json) and then run the command:

cosign verify-blob-attestation --bundle attestation.json \
--new-bundle-format --certificate-oidc-issuer="https://token.actions.githubusercontent.com" \
--certificate-identity-regexp="^https://github.com/<owner>/<repo>/.*" \
<path-to-file>

Creating build provenance for Docker images

Creating build provenance for Docker images works similarly to creating build provenance for files. Here is an example workflow:

name: Docker image build and push
on:
  workflow_dispatch: { }

permissions:
  contents: read
  id-token: write  # needed for attestations
  attestations: write  # needed for attestations
  packages: write  # to push images

jobs:
  build-push-docker-image:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Set up QEMU
        uses: docker/setup-qemu-action@v3

      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3

      - name: Set Docker metadata
        id: meta
        uses: docker/metadata-action@v5
        with:
          images: ghcr.io/${{ github.repository_owner }}/temp-test-image
          tags: |
            type=raw,value=latest

      - name: Login to GitHub Container Registry
        uses: docker/login-action@v3
        with:
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}
          registry: ghcr.io

      - name: Build and push the container
        id: build-and-push
        uses: docker/build-push-action@v6
        with:
          file: ./Dockerfile
          platforms: linux/amd64,linux/arm64
          push: true
          tags:  ${{ steps.meta.outputs.tags }}
          labels: ${{ steps.meta.outputs.labels }}
          annotations: ${{ steps.meta.outputs.annotations }}

      - name: Generate image attestation
        uses: actions/attest-build-provenance@v2
        with:
          subject-name: ghcr.io/${{ github.repository_owner }}/temp-test-image
          subject-digest: ${{ steps.build-and-push.outputs.digest }}
          push-to-registry: trueCode language: YAML (yaml)

After building and pushing the image, we run the actions/attest-build-provenance action, this time providing the fully qualified image name (including the server host), the digest (subject-digest), and that the attestation should also be pushed to the registry.

In contrast to attesting files, Docker images attestations only have one subject: the specific Docker image with its digest (which is a “sha256:…” string).

As for files, the actions/attest-build-provenance action creates a Sigstore bundle for this subject, uploads it to GitHub’s attestation API, and also creates the Rekor entry.

As for files, we can inspect this attestation in GitHub’s web UI. From there, you can also download the attestation as JSON.

In addition, because of the “push-to-registry: true” parameter found in the workflow, the GitHub action also creates and uploads various JSON manifests to the image registry. It creates a new tag named “sha256-<digest>” with this content:

{
  "schemaVersion": 2,
  "mediaType": "application/vnd.oci.image.index.v1+json",
  "manifests": [
    {
      "mediaType": "application/vnd.oci.image.manifest.v1+json",
      "artifactType": "application/vnd.dev.sigstore.bundle.v0.3+json",
      "size": 813,
      "digest": "sha256:851d19398c3b9aa2496581619ca0a5e8572fc4b6dd98cf8746451e5693954e86",
      "annotations": {
        "dev.sigstore.bundle.content": "dsse-envelope",
        "dev.sigstore.bundle.predicateType": "https://slsa.dev/provenance/v1",
        "org.opencontainers.image.created": "2025-01-29T09:50:54.629Z"
      }
    }
  ]
}
Code language: JSON / JSON with Comments (json)

This manifest essentially emulates the referrers API (see my Notation article for details).

Let’s look more closely at the manifest (851d19398c3…):

{
  "schemaVersion": 2,
  "mediaType": "application/vnd.oci.image.manifest.v1+json",
  "artifactType": "application/vnd.dev.sigstore.bundle.v0.3+json",
  "config": {
    "mediaType": "application/vnd.oci.empty.v1+json",
    "size": 2,
    "digest": "sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a"
  },
  "layers": [
    {
      "mediaType": "application/vnd.dev.sigstore.bundle.v0.3+json",
      "size": 10810,
      "digest": "sha256:7c0aad925d45aa66c0690d03dd9e012cc7b99a99e6bd962a2b1b9385aa1aaf7f"
    }
  ],
  "subject": {
    "mediaType": "application/vnd.oci.image.index.v1+json",
    "size": 1609,
    "digest": "sha256:1a42825de44e37ef55937a191ed336160e011e76287b8548964aeec26aba27f9"
  },
  "annotations": {
    "dev.sigstore.bundle.content": "dsse-envelope",
    "dev.sigstore.bundle.predicateType": "https://slsa.dev/provenance/v1",
    "org.opencontainers.image.created": "2025-01-29T09:50:54.629Z"
  }
}
Code language: JSON / JSON with Comments (json)

We see an image manifest with a single layer (7c0aad925d…):

{
  "mediaType": "application/vnd.dev.sigstore.bundle.v0.3+json",
  "dsseEnvelope": {
    "payload": "eyJfdHlwZSI6Imh0dHBzOi8vaW4tdG90by5pby9TdGF0ZW1lbnQvdjEiLCJzdWJqZWN0IjpbeyJuYW1lIjoiZ2hjci5pby9tc2hla293L3RlbXAtdGVzdC1pbWFnZSIsImRpZ2VzdCI6eyJzaGEyNTYiOiIxYTQyODI1ZGU0NGUzN2VmNTU5MzdhMTkxZWQzMzYxNjBlMDExZTc2Mjg3Yjg1NDg5NjRhZWVjMjZhYmEyN2Y5In19XSwicHJlZGljYXRlVHlwZSI6Imh0dHBzOi8vc2xzYS5kZXYvcHJvdmVuYW5jZS92MSIsInByZWRpY2F0ZSI6eyJidWlsZERlZmluaXRpb24iOnsiYnVpbGRUeXBlIjoiaHR0cHM6Ly9hY3Rpb25zLmdpdGh1Yi5pby9idWlsZHR5cGVzL3dvcmtmbG93L3YxIiwiZXh0ZXJuYWxQYXJhbWV0ZXJzIjp7IndvcmtmbG93Ijp7InJlZiI6InJlZnMvaGVhZHMvbWFpbiIsInJlcG9zaXRvcnkiOiJodHRwczovL2dpdGh1Yi5jb20vTVNoZWtvdy9hdHRlc3RhdGlvbi1leHBlcmltZW50IiwicGF0aCI6Ii5naXRodWIvd29ya2Zsb3dzL2RvY2tlci1pbWFnZS55bWwifX0sImludGVybmFsUGFyYW1ldGVycyI6eyJnaXRodWIiOnsiZXZlbnRfbmFtZSI6IndvcmtmbG93X2Rpc3BhdGNoIiwicmVwb3NpdG9yeV9pZCI6IjkyMjUyMTg2NyIsInJlcG9zaXRvcnlfb3duZXJfaWQiOiIxMDEyNjYxOCIsInJ1bm5lcl9lbnZpcm9ubWVudCI6ImdpdGh1Yi1ob3N0ZWQifX0sInJlc29sdmVkRGVwZW5kZW5jaWVzIjpbeyJ1cmkiOiJnaXQraHR0cHM6Ly9naXRodWIuY29tL01TaGVrb3cvYXR0ZXN0YXRpb24tZXhwZXJpbWVudEByZWZzL2hlYWRzL21haW4iLCJkaWdlc3QiOnsiZ2l0Q29tbWl0IjoiZDdkZjM3ZGY5NzU3MDA2YTM3N2IyOWNhMzJmNjU2NDgxMTYzZTIyOSJ9fV19LCJydW5EZXRhaWxzIjp7ImJ1aWxkZXIiOnsiaWQiOiJodHRwczovL2dpdGh1Yi5jb20vTVNoZWtvdy9hdHRlc3RhdGlvbi1leHBlcmltZW50Ly5naXRodWIvd29ya2Zsb3dzL2RvY2tlci1pbWFnZS55bWxAcmVmcy9oZWFkcy9tYWluIn0sIm1ldGFkYXRhIjp7Imludm9jYXRpb25JZCI6Imh0dHBzOi8vZ2l0aHViLmNvbS9NU2hla293L2F0dGVzdGF0aW9uLWV4cGVyaW1lbnQvYWN0aW9ucy9ydW5zLzEzMDI4NjYyMDI1L2F0dGVtcHRzLzEifX19fQ==",
    "payloadType": "application/vnd.in-toto+json",
    "signatures": [
      {
        "keyid": "",
        "sig": "MEUCIA2XLDhcxOBsH452DGXf8cacA6DqZ6Qf0uTNNkr6GFf+AiEAiNNTaojlb9yrAfgeRNW04gBi5jGzWRKPzslcEsgChyg="
      }
    ]
  },
  "verificationMaterial": {
    "certificate": {
      "rawBytes": "MIIHLjCC..."
    },
    "timestampVerificationData": {
      "rfc3161Timestamps": []
    },
    "tlogEntries": [
      {
        "canonicalizedBody": "eyJhcGlWZXJza...",
        "inclusionPromise": {
          "signedEntryTimestamp": "MEUCIAt8GnrGTu1Z03S3dA1djWPnXot3Lklt5cFepWQj4Bf1AiEA0wVg6n//LztTh7I4GJYgtB5LFQVqtkdYz7EB6VGPv1Q="
        },
        "inclusionProof": {
          "checkpoint": {
            "envelope": "rekor.sigstore.dev - 1193050959916656506\n44673854\nVC6U21uXfqW0ATVuvfXm+ufY4Bp9nQB3msJm4pg0eJs=\n\n— rekor.sigstore.dev wNI9ajBEAiAtTCr2sEgFKJqDzMOmmPcQPH/6/2n894mLTGVdYNQoMQIgaq1S43ZTIpXCtcOAU4VAcvnKT+vYiozEwTEaNh0pr/4=\n"
          },
          "hashes": [
            "MTlT+v9OH12WNPwHydz8UIciKUtPCi/TFtUD0JEwsik=",
            "M8kkKAwF1LrZfHTIMg0pYtgxS3uzVmn7uFuDrMgVHEY=",
            ...
          ],
          "logIndex": "44673853",
          "rootHash": "VC6U21uXfqW0ATVuvfXm+ufY4Bp9nQB3msJm4pg0eJs=",
          "treeSize": "44673854"
        },
        "integratedTime": "1738144254",
        "kindVersion": {
          "kind": "dsse",
          "version": "0.0.1"
        },
        "logId": {
          "keyId": "wNI9atQGlz+VWfO6LRygH4QUfY/8W4RFwiT5i5WRgB0="
        },
        "logIndex": "166578115"
      }
    ]
  }
}
Code language: JSON / JSON with Comments (json)

We see a Cosign bundle-spec-formatted manifest which includes the signature, signing certificates, and Rekor log entry information. The most interesting field here is dsseEnvelope → payload which contains the actual build provenance, formatted using SLSA (https://slsa.dev/provenance/v1) in a Statement:

{
  "subject": [
    {
      "name": "ghcr.io/mshekow/temp-test-image",
      "digest": {
        "sha256": "1a42825de44e37ef55937a191ed336160e011e76287b8548964aeec26aba27f9"
      }
    }
  ],
  "_type": "https://in-toto.io/Statement/v1",
  "predicateType": "https://slsa.dev/provenance/v1",
  "predicate": {
    "buildDefinition": {
      "buildType": "https://actions.github.io/buildtypes/workflow/v1",
      "externalParameters": {
        "workflow": {
          "path": ".github/workflows/docker-image.yml",
          "ref": "refs/heads/main",
          "repository": "https://github.com/MShekow/attestation-experiment"
        }
      },
      "internalParameters": {
        "github": {
          "event_name": "workflow_dispatch",
          "repository_id": "922521867",
          "repository_owner_id": "10126618",
          "runner_environment": "github-hosted"
        }
      },
      "resolvedDependencies": [
        {
          "digest": {
            "gitCommit": "d7df37df9757006a377b29ca32f656481163e229"
          },
          "uri": "git+https://github.com/MShekow/attestation-experiment@refs/heads/main"
        }
      ]
    },
    "runDetails": {
      "builder": {
        "id": "https://github.com/MShekow/attestation-experiment/.github/workflows/docker-image.yml@refs/heads/main"
      },
      "metadata": {
        "invocationId": "https://github.com/MShekow/attestation-experiment/actions/runs/13028662025/attempts/1"
      }
    }
  }
}Code language: JSON / JSON with Comments (json)

Verifying build provenance for Docker images

There are several options to check a Docker image’s build provenance:

  • In the terminal (or a CI/CD pipeline), call the GitHub CLI as follows:
    gh attestation verify oci://<image-with-tag-or-digest> --repo <owner>/<repo> [--bundle-from-oci]
  • Kubernetes operators:
    • Kyverno, see docs
    • Sigstore’s Policy controller (with GitHub-specific documentation available here)
    • Ratify might also work, but I did not test it

SBOM attestation in detail

Creating SBOM attestations

Above, we used the actions/attest-build-provenance action to create build provenance attestations. To create SBOM attestations, GitHub offers another official action, actions/attest-sbom. It creates SBOM attestations for files or a Docker image.

For SBOMs, I won’t go into as much detail about how to create them as I did in the above four build provenance-related subsections, because the process is very similar. The only difference is that you must first create the SBOM file, using some tool (e.g. the anchore/sbom-action action). GitHub requires that the SBOM is JSON-formatted, in either the SPDX or CycloneDX format, and that its size does not exceed 16 MB.

The actions/attest-sbom action creates an SBOM attestation in very similar ways as the actions/attest-build-provenance action creates build provenance. If you look at the resulting attestation, the file formats and meta-data fields of build provenance vs. SBOM attestations are almost equal. The core difference in the Statement’s JSON content is:

 Attestation type (a.k.a. Predicate type)Content of the attestation’s Predicate
SLSA build provenancehttps://slsa.dev/provenance/v1Information about the workflow, e.g. the repository URL or Git commit hash
SBOM (CycloneDX)https://cyclonedx.org/bomThe CycloneDX SBOM JSON content
SBOM (SPDX)https://spdx.dev/Document/v2.3The SPDX SBOM JSON content

Verifying SBOM attestations

For automatic or manual verification of SBOM attestations, these options exist:

  • GitHub CLI: irrespective of whether the SBOM attestation is for file(s) or a Docker image, you can use the “gh attestation verify” command, as explained above for build provenance. Make sure to additionally provide the expected SBOM attestation type via the “--predicate-type” CLI argument (see above table), e.g.
    --predicate-type https://cyclonedx.org/bom
  • Kubernetes operators:
    • Kyverno should support the verification of attestations
    • Sigstore Policy controller also supports attestations (see docs)

I don’t think Ratify with OPA Gatekeeper supports the GitHub-generated Cosign bundle-formatted attestations. The documentation only mentions ORAS-attached attestations, whose manifests have a different (incompatible) format.

Attestations of arbitrary type

Apart from build provenance and SBOM attestations, you can also create attestations for any predicate type you desire, using the actions/attest action. Its use is very similar to the actions I presented above. The predicate itself can be any JSON object.

Conclusion

The core benefit I see for GitHub’s attestations is for open-source projects to create build provenance for files (e.g. binaries) that they build using GHA workflows, publishing them via a GitHub release.

I don’t see a strong case for using GitHub attestations for the other combinations, particularly:

  • SBOM attestations for files: instead of having dedicated/separate SBOM attestations (using actions/attest-sbom), I find it more economical to upload generated SBOMs along with the other binaries you publish via a GitHub release. Then, you configure GitHub’s build provenance attestation (actions/attest-build-provenance) to cover both the uploaded binaries and the SBOMs as subjects.
  • Attestations (of any kind) for Docker images: I would instead use the Cosign CLI because its signatures have the widest adoption in the ecosystem (e.g. by Kubernetes operators).

If you consume (download) open-source binaries from GitHub releases, you should check whether the respective maintainers already created attestations. If so, verify the downloaded binaries against the attestation. If the respective maintainers don’t yet provide attestations, offer a PR that adds them. As shown in the above example, a few lines of workflow YAML code are sufficient. In the PR, you could also add documentation explaining how consumers can verify the downloaded binaries.

If attestations exist, verifying them with the GitHub CLI is the easiest approach. However, you must acquire the GitHub CLI in a way that ensures that the CLI has not been tampered with. There are various scenarios:

  • You want to download the binaries of third-party CLIs (that you want to verify) directly in your GHA workflows (e.g. using curl in a run step):
    • If you use GitHub’s hosted runners: the GitHub CLI comes pre-installed with the hosted runners
    • If you use self-hosted runners: use sigstore/cosign-installer action to install the cosign CLI, then use the snippet below to acquire a tamper-proof GitHub CLI
  • You want to curl the third-party CLI binaries in a build-container (e.g. while building a Dockerfile): install the GitHub CLI via a Linux package manager, e.g. in a separate build stage, as shown in the snippet below. You will need a GitHub Personal Access Token (PAT) to use the GitHub CLI. Any PAT will do, it does not need any scopes or permissions.
- name: Download and verify GitHub CLI
  env:
    GH_TOKEN: ${{ github.token }}
  run: |
    set -e
    curl -L -o github-cli.tar.gz https://github.com/cli/cli/releases/download/v2.65.0/gh_2.65.0_linux_amd64.tar.gz
    
    sha256digest=$(shasum -a 256 github-cli.tar.gz | cut -d ' ' -f 1)
    
    curl -L -H "Accept: application/vnd.github+json" -H "Authorization: Bearer $GH_TOKEN" \
      -H "X-GitHub-Api-Version: 2022-11-28" --fail \
      https://api.github.com/orgs/cli/attestations/sha256:$sha256digest | jq '.attestations[0].bundle' > cli-attestation.json
    
    cosign verify-blob-attestation --bundle cli-attestation.json --new-bundle-format \
      --certificate-oidc-issuer="https://token.actions.githubusercontent.com" \
      --certificate-identity-regexp="^https://github.com/cli/cli/.github/workflows/deployment.yml@refs/heads/trunk$" \
      github-cli.tar.gz
    
    # The "gh" CLI is in a "gh_<version>_<os>_<cpu-arch>/bin subdirectory, so we extract it directly
    tar --wildcards --strip-components=2 -xzf github-cli.tar.gz 'gh_*/bin/gh'

- name: Download and verify some CLI
  env:
    GH_TOKEN: ${{ github.token }}
  run: |
    curl -L -o goreleaser_Linux_arm64.tar.gz \
      https://github.com/goreleaser/goreleaser/releases/download/nightly/goreleaser_Linux_arm64.tar.gz

    ./gh attestation verify goreleaser_Linux_arm64.tar.gz --repo goreleaser/goreleaserCode language: YAML (yaml)
# syntax=docker/dockerfile:1
# Build this via: docker build --secret id=github-pat,env=ENV_VAR_NAME_CONTAINING_THE_PAT -t some-image:tag .

FROM redhat/ubi9:latest AS tools
ADD --chmod=755 https://github.com/<owner>/<repo>/releases/download/<version>/<tool-name-and-version> <absolute-target-path-of-binary>
# Install GitHub CLI using instructions from https://github.com/cli/cli/blob/75a23e73eb229ee9dd4e18708c14c4cf646385dd/docs/install_linux.md#dnf4
RUN dnf install -y 'dnf-command(config-manager)' && \
dnf config-manager --add-repo https://cli.github.com/packages/rpm/gh-cli.repo && \
dnf install -y git && \
dnf install -y gh --repo gh-cli
# Attest the binary
RUN --mount=type=secret,id=github-pat,env=GH_TOKEN gh attestation verify <absolute-target-path-of-binary> --repo <owner>/<repo>

FROM whatever:you-want
COPY --from=tools <absolute-target-path-of-binary> /usr/local/bin/<binary-name>Code language: Dockerfile (dockerfile)

Leave a Comment