diff --git a/.github/workflows/generator_container_slsa3.yml b/.github/workflows/generator_container_slsa3.yml new file mode 100644 index 0000000000..4d8989cdab --- /dev/null +++ b/.github/workflows/generator_container_slsa3.yml @@ -0,0 +1,144 @@ +# Copyright 2022 SLSA Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +name: SLSA container image provenance + +env: + # Generator + BUILDER_BINARY: generic-generator + BUILDER_RELEASE_BINARY: slsa-generator-generic-linux-amd64 + BUILDER_REPOSITORY: slsa-framework/slsa-github-generator + # Verifier + # NOTE: These VERIFIER_* variables are used for verification of generator + # release binaries when the compile-generator input is false. + VERIFIER_REPOSITORY: slsa-framework/slsa-verifier + VERIFIER_RELEASE_BINARY: slsa-verifier-linux-amd64 + VERIFIER_RELEASE_BINARY_SHA256: f92fc4e571949c796d7709bb3f0814a733124b0155e484fad095b5ca68b4cb21 + VERIFIER_RELEASE: v1.1.1 + # Builder location + BUILDER_DIR: internal/builders + +on: + workflow_call: + secrets: + registry-password: + description: "Password to log in the container registry." + required: true + inputs: + image: + description: "The OCI image name. This must not include a tag or digest." + required: true + type: string + digest: + description: "The OCI image digest. The image digest of the form ':' (e.g. 'sha256:abcdef...')" + required: true + type: string + registry-username: + description: "Username to log into the container registry." + required: true + type: string + compile-generator: + description: "Build the generator from source. This increases build time by ~2m." + required: false + type: boolean + default: false + +jobs: + # detect-env detects the reusable workflow's repository and ref for use later + # in the workflow. + detect-env: + outputs: + repository: ${{ steps.detect.outputs.repository }} + ref: ${{ steps.detect.outputs.ref }} + runs-on: ubuntu-latest + permissions: + id-token: write # Needed to detect the current reusable repository and ref. + steps: + - name: Detect the generator ref + id: detect + uses: slsa-framework/slsa-github-generator/.github/actions/detect-workflow@49e648aa7f5f4f88513b6cd54f6b189516184e6b + + # generator builds the generator binary and runs it to generate SLSA + # provenance. + # + # If `compile-generator` is true then the generator is compiled + # from source at the ref detected by `detect-env`. + # + # If `compile-generator` is false, then the generator binary is downloaded + # with the release at the ref detected by `detect-env`. This must be a tag + # reference. + generator: + runs-on: ubuntu-latest + needs: [detect-env] + permissions: + # id-token:write is needed to create an OCID token for keyless signing. + id-token: write + # actions permissions are needed to read info on the workflow and + # workflow run. + actions: read + # packages:write permissions are needed to login and upload attestations. + packages: write + steps: + - name: Generate builder + uses: slsa-framework/slsa-github-generator/.github/actions/generate-builder@49e648aa7f5f4f88513b6cd54f6b189516184e6b + with: + repository: "${{ needs.detect-env.outputs.repository }}" + ref: "${{ needs.detect-env.outputs.ref }}" + go-version: 1.18 + binary: "${{ env.BUILDER_BINARY }}" + compile-builder: "${{ inputs.compile-generator }}" + # NOTE: We are using the generic generator. + directory: "${{ env.BUILDER_DIR }}/generic" + + - uses: sigstore/cosign-installer@7e0881f8fe90b25e305bbf0309761e9314607e25 # v2.4.0 + - name: Login + env: + UNTRUSTED_IMAGE: "${{ inputs.image }}" + UNTRUSTED_USERNAME: "${{ inputs.registry-username }}" + UNTRUSTED_PASSWORD: "${{ secrets.registry-password }}" + run: | + set -euo pipefail + + # NOTE: Some docker images are of the form / + # Here we get the first part and check if it has a '.' or ':' + # character in it to see if it's a domain name. + # See: https://stackoverflow.com/questions/37861791/how-are-docker-image-names-parsed#37867949 + untrusted_registry="docker.io" + # NOTE: Do not fail the script if grep does not match. + maybe_domain=$(echo "$UNTRUSTED_IMAGE" | cut -f1 -d "/" | { grep -E "\.|:" || true; }) + if [ "$maybe_domain" != "" ]; then + untrusted_registry="$maybe_domain" + fi + + echo "login to $untrusted_registry" + cosign login "$untrusted_registry" -u "$UNTRUSTED_USERNAME" -p "$UNTRUSTED_PASSWORD" + + - name: Create and sign provenance + id: sign-prov + shell: bash + env: + UNTRUSTED_IMAGE: "${{ inputs.image }}" + UNTRUSTED_DIGEST: "${{ inputs.digest }}" + GITHUB_CONTEXT: "${{ toJSON(github) }}" + run: | + set -euo pipefail + + # Generate a predicate only. + predicate_name="predicate.json" + ./"$BUILDER_BINARY" attest --signature="" --predicate="$predicate_name" + + COSIGN_EXPERIMENTAL=1 cosign attest --predicate="$predicate_name" \ + --type slsaprovenance \ + --force \ + "${UNTRUSTED_IMAGE}@${UNTRUSTED_DIGEST}" diff --git a/internal/builders/container/README.md b/internal/builders/container/README.md new file mode 100644 index 0000000000..05094e4eaf --- /dev/null +++ b/internal/builders/container/README.md @@ -0,0 +1,255 @@ +# Generation of SLSA3+ provenance for container images + +This document explains how to generate SLSA provenance for container images. + +This can be done by adding an additional step to your existing Github Actions +workflow to call a [reusable +workflow](https://docs.github.com/en/actions/using-workflows/reusing-workflows) +to generate generic SLSA provenance. We'll call this workflow the "container +workflow" from now on. + +The container workflow differs from ecosystem specific builders (like the [Go +builder](../go)) which build the artifacts as well as generate provenance. This +project simply generates provenance as a separate step in an existing workflow. + +--- + +- [Project Status](#project-status) +- [Benefits of Provenance](#benefits-of-provenance) +- [Generating Provenance](#generating-provenance) + - [Getting Started](#getting-started) + - [Supported Triggers](#supported-triggers) + - [Workflow Inputs](#workflow-inputs) + - [Provenance Format](#provenance-format) + - [Provenance Example](#provenance-example) + +--- + +## Project Status + +This workflow is currently under active development. The API could change while +approaching an initial release. + +## Benefits of Provenance + +Using the container workflow will generate a non-forgeable attestation to the +container image using the identity of the GitHub workflow. This can be used +to create a positive attestation to a container image coming from your +repository. + +That means that once your users verify the image they have downloaded they +can be sure that the image was created by your repository's workflow and +hasn't been tampered with. + +## Generating Provenance + +The container workflow uses a Github Actions reusable workflow to generate the +provenance. + +### Getting Started + +To get started, you will need to add some steps to your current workflow. We +will assume you have an existing Github Actions workflow to build your project. + +```yaml +provenance: + needs: [build] + permissions: + actions: read # for detecting the Github Actions environment. + id-token: write # for creating OCID tokens for signing. + packages: write # for uploading attestations. + if: startsWith(github.ref, 'refs/tags/') + # TODO(https://github.com/slsa-framework/slsa-github-generator/issues/492): Use a tagged release once we have one. + uses: slsa-framework/slsa-github-generator/.github/workflows/generator_container_slsa3.yml@main + with: + image: ${{ needs.build.outputs.tag }} + registry-username: ${{ github.actor }} + # TODO(https://github.com/slsa-framework/slsa-github-generator/issues/492): Remove after GA release. + compile-generator: true + secrets: + registry-password: ${{ secrets.GITHUB_TOKEN }} +``` + +Here's an example of what it might look like all together. + +```yaml +env: + IMAGE_REGISTRY: ghcr.io + IMAGE_NAME: ${{ github.repository }} + +jobs: + # This step builds our image, pushes it, and outputs the repo hash digest. + build: + permissions: + contents: read + packages: write + outputs: + image: ${{ steps.image.outputs.image }} + runs-on: ubuntu-latest + steps: + - name: Checkout the repository + uses: actions/checkout@2541b1294d2704b0964813337f33b291d3f8596b # v2.3.4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@dc7b9719a96d48369863986a06765841d7ea23f6 # v2.0.0 + + - name: Authenticate Docker + uses: docker/login-action@49ed152c8eca782a232dede0303416e8f356c37b # v2.0.0 + with: + registry: ${{ env.IMAGE_REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract metadata (tags, labels) for Docker + id: meta + uses: docker/metadata-action@69f6fc9d46f2f8bf0d5491e4aabe0bb8c6a4678a # v4.0.1 + with: + images: ${{ env.IMAGE_REGISTRY }}/${{ env.IMAGE_NAME }} + + - name: Build and push Docker image + uses: docker/build-push-action@e551b19e49efd4e98792db7592c17c09b89db8d8 # v3.0.0 + id: build + with: + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + + - name: Output image + id: image + run: | + # NOTE: Set the image as an output because the `env` context is not + # available to the inputs of a reusable workflow call. + image_name="${IMAGE_REGISTRY}/${IMAGE_NAME}" + echo "::set-output name=image::$image_name" + + # This step calls the container workflow to generate provenance and push it to + # the container registry. + provenance: + needs: [build] + permissions: + actions: read # for detecting the Github Actions environment. + id-token: write # for creating OCID tokens for signing. + packages: write # for uploading attestations. + if: startsWith(github.ref, 'refs/tags/') + uses: slsa-framework/slsa-github-generator/.github/workflows/generator_container_slsa3.yml@main + with: + image: ${{ needs.build.outputs.image }} + registry-username: ${{ github.actor }} + # TODO(https://github.com/slsa-framework/slsa-github-generator/issues/492): Remove after GA release. + compile-generator: true + secrets: + registry-password: ${{ secrets.GITHUB_TOKEN }} +``` + +### Supported Triggers + +The following [GitHub trigger events](https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows) are fully supported and tested: + +- `schedule` +- `push` (including new tags) +- `release` +- Manual run via `workflow_dispatch` + +However, in practice, most triggers should work with the exception of +`pull_request`. If you would like support for `pull_request`, please tell us +about your use case on [issue #358](https://github.com/slsa-framework/slsa-github-generator/issues/358). If +you have an issue in all other triggers please submit a [new +issue](https://github.com/slsa-framework/slsa-github-generator/issues/new/choose). + +### Workflow Inputs + +The [container workflow](https://github.com/slsa-framework/slsa-github-generator/blob/main/.github/workflows/generator_container_slsa3.yml) accepts the following inputs: + +Inputs: + +| Name | Required | Description | +| ------------------- | -------- | --------------------------------------------------------------------------------------------------- | +| `image` | yes | The OCI image name. This must not include a tag or digest. | +| `digest` | yes | The OCI image digest. The image digest of the form ':' (e.g. 'sha256:abcdef...') | +| `registry-username` | yes | Username to log into the container registry. | +| `compile-generator` | false | Whether to build the generator from source. This increases build time by ~2m. | + +Secrets: + +| Name | Required | Description | +| ------------------- | -------- | ------------------------------------------ | +| `registry-password` | yes | Password to log in the container registry. | + +### Provenance Format + +The project generates SLSA provenance with the following values. + +| Name | Value | Description | +| ---------------------------- | -------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `buildType` | `"https://github.com/slsa-framework/slsa-github-generator@v1"` | Identifies a generic GitHub Actions build. | +| `metadata.buildInvocationID` | `"[run_id]-[run_attempt]"` | The GitHub Actions [`run_id`](https://docs.github.com/en/actions/learn-github-actions/contexts#github-context) does not update when a workflow is re-run. Run attempt is added to make the build invocation ID unique. | + +### Provenance Example + +The following is an example of the generated proveanance. Provenance is +generated as an [in-toto](https://in-toto.io/) statement with a SLSA predicate. + +```json +{ + "_type": "https://in-toto.io/Statement/v0.1", + "predicateType": "https://slsa.dev/provenance/v0.2", + "subject": [ + { + "name": "ghcr.io/ianlewis/actions-test", + "digest": { + "sha256": "8ae83e5b11e4cc8257f5f4d1023081ba1c72e8e60e8ed6cacd0d53a4ca2d142b" + } + }, + ], + "predicate": { + "builder": { + "id": "https://github.com/slsa-framework/slsa-github-generator/.github/workflows/generator_container_slsa3.yml@refs/tags/v1.1.1" + }, + "buildType": "https://github.com/slsa-framework/slsa-github-generator@v1", + "invocation": { + "configSource": { + "uri": "git+https://github.com/ianlewis/actions-test@refs/heads/main.git", + "digest": { + "sha1": "e491e4b2ce5bc76fb103729b61b04d3c46d8a192" + }, + "entryPoint": ".github/workflows/generic-container.yml" + }, + "parameters": {}, + "environment": { + "github_actor": "ianlewis", + "github_actor_id": "49289", + "github_base_ref": "", + "github_event_name": "push", + "github_event_payload": {...}, + "github_head_ref": "", + "github_ref": "refs/tags/v0.0.9", + "github_ref_type": "tag", + "github_repository_id": "474793590", + "github_repository_owner": "ianlewis", + "github_repository_owner_id": "49289", + "github_run_attempt": "1", + "github_run_id": "2556669934", + "github_run_number": "12", + "github_sha1": "e491e4b2ce5bc76fb103729b61b04d3c46d8a192" + } + }, + "metadata": { + "buildInvocationID": "2556669934-1", + "completeness": { + "parameters": true, + "environment": false, + "materials": false + }, + "reproducible": false + }, + "materials": [ + { + "uri": "git+https://github.com/ianlewis/actions-test@refs/tags/v0.0.9", + "digest": { + "sha1": "e491e4b2ce5bc76fb103729b61b04d3c46d8a192" + } + } + ] + } +} +``` diff --git a/internal/builders/generic/attest.go b/internal/builders/generic/attest.go index 12845d4154..8e4762bc59 100644 --- a/internal/builders/generic/attest.go +++ b/internal/builders/generic/attest.go @@ -158,11 +158,16 @@ run in the context of a Github Actions workflow.`, ghContext, err := github.GetWorkflowContext() check(err) - parsedSubjects, err := parseSubjects(subjects) - check(err) + var parsedSubjects []intoto.Subject + // We don't actually care about the subjects if we aren't writing an attestation. + if attPath != "" { + var err error + parsedSubjects, err = parseSubjects(subjects) + check(err) - if len(parsedSubjects) == 0 { - check(errors.New("expected at least one subject")) + if len(parsedSubjects) == 0 { + check(errors.New("expected at least one subject")) + } } ctx := context.Background()