This document explains how to generate SLSA provenance for projects for which there is no language or ecosystem specific builder available.
This can be done by adding an additional step to your existing Github Actions workflow to call a reusable workflow to generate generic SLSA provenance. We'll call this workflow the "generic workflow" from now on.
The generic workflow differs from ecosystem specific builders (like the Go builder) which build the artifacts as well as generate provenance. This project simply generates provenance as a separate step in an existing workflow.
Using the generic workflow will generate a non-forgeable attestation to the artifacts' digests using the identity of the GitHub workflow. This can be used to create a positive attestation to a software artifact coming from your repository.
That means that once your users verify the artifacts they have downloaded they can be sure that the artifacts were created by your repository's workflow and haven't been tampered with.
The generic workflow uses a GitHub Actions reusable workflow to generate the provenance.
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.
Add a step to your workflow after you have built your project to generate a sha256 hash of your artifacts and base64 encode it.
Assuming you have a binary called binary-linux-amd64
you can use the
sha256sum
and base64
commands to create the digest. Here we use the -w0
to
output the encoded data on one line and make it easier to use as a GitHub Actions
output:
$ sha256sum artifact1 artifact2 ... | base64 -w0
This workflow expects the base64-subjects
input to decode to a string conforming to the expected output of the sha256sum
command. Specifically, the decoded output is expected to be comprised of a hash value followed by a space followed by the artifact name.
After you have encoded your digest, add a new job to call the reusable workflow.
provenance:
permissions:
actions: read # Needed for detection of GitHub Actions environment.
id-token: write # Needed for provenance signing and ID
contents: read # Needed for API access
uses: slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@v1.2.0
with:
base64-subjects: "${{ needs.build.outputs.hashes }}"
Note: Make sure that you reference the generator with a semantic version of the form @vX.Y.Z
. The build will fail
if you reference it via a shorter tag like @vX.Y
or @vX
.
Here's an example of what it might look like all together.
jobs:
# This step builds our artifacts, uploads them to the workflow run, and
# outputs their digest.
build:
outputs:
hashes: ${{ steps.hash.outputs.hashes }}
runs-on: ubuntu-latest
steps:
- name: Build artifacts
run: |
# These are some amazing artifacts.
echo "foo" > artifact1
echo "bar" > artifact2
- name: Generate hashes
shell: bash
id: hash
run: |
# sha256sum generates sha256 hash for all artifacts.
# base64 -w0 encodes to base64 and outputs on a single line.
# sha256sum artifact1 artifact2 ... | base64 -w0
echo "::set-output name=hashes::$(sha256sum artifact1 artifact2 | base64 -w0)"
- name: Upload artifact1
uses: actions/upload-artifact@3cea5372237819ed00197afe530f5a7ea3e805c8 # tag=v3.1.0
with:
name: artifact1
path: artifact1
if-no-files-found: error
retention-days: 5
- name: Upload artifact2
uses: actions/upload-artifact@3cea5372237819ed00197afe530f5a7ea3e805c8 # tag=v3.1.0
with:
name: artifact2
path: artifact2
if-no-files-found: error
retention-days: 5
# This step calls the generic workflow to generate provenance.
provenance:
needs: [build]
permissions:
actions: read
id-token: write
contents: read
uses: slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@v1.2.0
with:
base64-subjects: "${{ needs.build.outputs.hashes }}"
# Set a custom name for the provenance attestation.
attestation-name: "artifacts.intoto.jsonl"
# Upload provenance to a new release
upload-assets: true
# This step uploads our artifacts to the tagged GitHub release.
release:
needs: [build, provenance]
runs-on: ubuntu-latest
if: startsWith(github.ref, 'refs/tags/')
steps:
- name: Download artifact1
uses: actions/download-artifact@fb598a63ae348fa914e94cd0ff38f362e927b741 # tag=v2.1.0
with:
name: artifact1
- name: Download artifact2
uses: actions/download-artifact@fb598a63ae348fa914e94cd0ff38f362e927b741 # tag=v2.1.0
with:
name: artifact2
- name: Upload assets
uses: softprops/action-gh-release@1e07f4398721186383de40550babbdf2b84acfc5 # v0.1.14
with:
files: |
artifact1
artifact2
The following GitHub trigger events 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. If
you have an issue with any other triggers please submit a new
issue.
The generic workflow accepts the following inputs:
Name | Required | Default | Description |
---|---|---|---|
base64-subjects |
yes | Artifact(s) for which to generate provenance, formatted the same as the output of sha256sum (SHA256 NAME\n[...]) and base64 encoded. The encoded value should decode to, for example: 90f3f7d6c862883ab9d856563a81ea6466eb1123b55bff11198b4ed0030cac86 foo.zip |
|
upload-assets |
no | false | If true provenance is uploaded to a GitHub release for new tags. |
attestation-name |
no | "attestation.intoto.jsonl" | The artifact name of the signed provenance. The file must have the intoto.jsonl extension. |
The generic workflow produces the following outputs:
Name | Description |
---|---|
attestation-name |
The artifact name of the signed provenance |
The project generates SLSA provenance with the following values.
Name | Value | Description |
---|---|---|
buildType |
"https://github.com/slsa-framework/slsa-github-generator/generic@v1" |
Identifies a generic GitHub Actions build. |
metadata.buildInvocationID |
"[run_id]-[run_attempt]" |
The GitHub Actions run_id does not update when a workflow is re-run. Run attempt is added to make the build invocation ID unique. |
The following is an example of the generated provenance. Provenance is generated as an in-toto statement with a SLSA predicate.
{
"_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_generic_slsa3.yml@refs/tags/v1.2.0"
},
"buildType": "https://github.com/slsa-framework/slsa-github-generator/generic@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"
}
}
]
}
}
This section explains how to generate non-forgeable SLSA provenance with existing build systems.
If you use GoReleaser to generate your build, you can easily generate SLSA3 provenance by updating your existing workflow with the steps indicated in the workflow below:
- Declare an
outputs
for the GoReleaser job:
jobs:
goreleaser:
outputs:
hashes: ${{ steps.hash.outputs.hashes }}
- Add an
id: run-goreleaser
field to your goreleaser step:
steps:
[...]
- name: Run GoReleaser
id: run-goreleaser
uses: goreleaser/goreleaser-action@b953231f81b8dfd023c58e0854a721e35037f28b # tag=v3
- Add a step to generate the provenance subjects as shown below:
- name: Generate subject
id: hash
env:
ARTIFACTS: "${{ steps.run-goreleaser.outputs.artifacts }}"
run: |
set -euo pipefail
checksum_file=$(echo "$ARTIFACTS" | jq -r '.[] | select (.type=="Checksum") | .path')
echo "::set-output name=hashes::$(cat $checksum_file | base64 -w0)"
- Call the generic workflow to generate provenance by declaring the job below:
provenance:
needs: [goreleaser]
permissions:
actions: read # To read the workflow path.
id-token: write # To sign the provenance.
contents: write # To add assets to a release.
uses: slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@v1.2.0
with:
base64-subjects: "${{ needs.goreleaser.outputs.hashes }}"
upload-assets: true # upload to a new release
All in all, it will look as the following:
jobs:
goreleaser:
outputs:
hashes: ${{ steps.hash.outputs.hashes }}
steps:
- name: Checkout repository
uses: actions/checkout@2541b1294d2704b0964813337f33b291d3f8596b # tag=v3
- name: Run GoReleaser
id: run-goreleaser
uses: goreleaser/goreleaser-action@b953231f81b8dfd023c58e0854a721e35037f28b # tag=v3
- name: Generate subject
id: hash
env:
ARTIFACTS: "${{ steps.run-goreleaser.outputs.artifacts }}"
run: |
set -euo pipefail
checksum_file=$(echo "$ARTIFACTS" | jq -r '.[] | select (.type=="Checksum") | .path')
echo "::set-output name=hashes::$(cat $checksum_file | base64 -w0)"
provenance:
needs: [goreleaser]
permissions:
actions: read # To read the workflow path.
id-token: write # To sign the provenance.
contents: write # To add assets to a release.
uses: slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@v1.2.0
with:
base64-subjects: "${{ needs.goreleaser.outputs.hashes }}"
upload-assets: true # upload to a new release
If you use Bazel to generate your artifacts, you can easily generate SLSA3 provenance by updating your existing workflow with steps indicated in the workflow below:
- Declare an
outputs
for the hashes:
jobs:
build:
outputs:
hashes: ${{ steps.hash.outputs.hashes }}
- Build your project and copy the binaries from
bazel-bin
path (i.e., Bazel sandbox) to the root of the repository for easier reference (this makes it easier to upload these to the release too!):
steps:
[...]
- name: Build using bazel
run: |
# Your normal build workflow targets here
bazel build //path/to/target_binary //path/to_another/binary
# Copy the binaries.
cp bazel-bin/path/to/target_binary .
cp bazel-bin/path/to/another/binary .
- Add a step to generate the provenance subjects as shown below. Update the sha256 sum arguments to include all binaries that you generate provenance for:
- name: Generate subject
id: hash
run: |
set -euo pipefail
sha256sum target_binary binary > checksums
echo "::set-output name=hashes::$(cat checksums | base64 -w0)"
- Call the generic workflow to generate provenance by declaring the job below:
provenance:
needs: [build]
permissions:
actions: read # To read the workflow path.
id-token: write # To sign the provenance.
contents: write # To add assets to a release.
uses: slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@v1.2.0
with:
base64-subjects: "${{ needs.build.outputs.hashes }}"
upload-assets: true # Optional: Upload to a new release
All in all, it will look as the following:
jobs:
build:
outputs:
hashes: ${{ steps.hash.outputs.hashes }}
steps:
- name: Checkout repository
uses: actions/checkout@2541b1294d2704b0964813337f33b291d3f8596b # tag=v3
- name: Build using bazel
run: |
# Your normal build workflow targets here
bazel build //path/to/target_binary //path/to_another/binary
# Copy the binaries.
cp bazel-bin/path/to/target_binary .
cp bazel-bin/path/to/another/binary .
- name: Generate subject
id: hash
run: |
set -euo pipefail
sha256sum target_binary binary > checksums
echo "::set-output name=hashes::$(cat checksums | base64 -w0)"
provenance:
needs: [build]
permissions:
actions: read # To read the workflow path.
id-token: write # To sign the provenance.
contents: write # To add assets to a release.
uses: slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@v1.2.0
with:
base64-subjects: "${{ needs.build.outputs.hashes }}"
upload-assets: true # Optional: Upload to a new release
If you develop with Java and use Maven or Gradle, you can easily generate SLSA3 provenance by updating your existing workflow with the steps indicated in the workflow below:
- Declare an
outputs
for the artifacts generated by the build and their hashes:
jobs:
build:
outputs:
artifacts: ${{ steps.build.outputs.artifacts }}
hashes: ${{ steps.hash.outputs.hashes }}
- Add an
id: build
field to your maven build step; and save the location of the maven output files for easier reference:
steps:
[...]
- name: Build using maven
id: build
run: |
# Your normal build workflow targets here
mvn clean package
# Save the location of the maven output files for easier reference
ARTIFACT_PATTERN=./target/$(mvn help:evaluate -Dexpression=project.artifactId -q -DforceStdout)-$(mvn help:evaluate -Dexpression=project.version -q -DforceStdout)*.jar
echo "::set-output name=artifact_pattern::$ARTIFACT_PATTERN"
- Add a step to generate the provenance subjects as shown below. Update the sha256 sum arguments to include all binaries that you generate provenance for:
- name: Generate subject
id: hash
run: |
echo "::set-output name=hashes::$(sha256sum ${{ steps.build.outputs.artifact_pattern }} | base64 -w0)"
- Call the generic workflow to generate provenance by declaring the job below:
provenance:
needs: [build]
permissions:
actions: read # To read the workflow path.
id-token: write # To sign the provenance.
contents: write # To add assets to a release.
uses: slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@v1.2.0
with:
base64-subjects: "${{ needs.build.outputs.hashes }}"
upload-assets: true # Optional: Upload to a new release
All in all, it will look as the following:
jobs:
build:
outputs:
artifacts: ${{ steps.build.outputs.artifacts }}
hashes: ${{ steps.hash.outputs.hashes }}
steps:
- name: Checkout repository
uses: actions/checkout@2541b1294d2704b0964813337f33b291d3f8596b # tag=v3
- name: Build using maven
id: build
run: |
# Your normal build workflow targets here
mvn clean package
# Save the location of the maven output files for easier reference
ARTIFACT_PATTERN=./target/$(mvn help:evaluate -Dexpression=project.artifactId -q -DforceStdout)-$(mvn help:evaluate -Dexpression=project.version -q -DforceStdout)*.jar
echo "::set-output name=artifact_pattern::$ARTIFACT_PATTERN"
- name: Generate subject
id: hash
run: |
echo "::set-output name=hashes::$(sha256sum ${{ steps.build.outputs.artifact_pattern }} | base64 -w0)"
- name: Upload build artifacts
uses: actions/upload-artifact@3cea5372237819ed00197afe530f5a7ea3e805c8 # tag=v3
with:
name: maven-build-outputs
path: ${{ steps.build.outputs.artifact_pattern }}
if-no-files-found: error
provenance:
needs: [build]
permissions:
actions: read # To read the workflow path.
id-token: write # To sign the provenance.
contents: write # To add assets to a release.
uses: slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@v1.2.0
with:
base64-subjects: "${{ needs.build.outputs.hashes }}"
upload-assets: true # Optional: Upload to a new release
- Declare an
outputs
for the artifacts generated by the build and their hashes:
jobs:
build:
outputs:
hashes: ${{ steps.hash.outputs.hashes }}
- Add an
id: build
field to your gradle build ste:
steps:
[...]
- name: Build using gradle
id: build
run: |
# Your normal build workflow targets here
./gradlew clean build
- Add a step to generate the provenance subjects as shown below. Update the sha256 sum arguments to include all binaries that you generate provenance for. (This build assumes build artifacts are saved in ./build/libs).
- name: Generate subject
id: hash
run: |
echo "::set-output name=hashes::$(sha256sum ./build/libs/* | base64 -w0)"
- Call the generic workflow to generate provenance by declaring the job below:
provenance:
needs: [build]
permissions:
actions: read
id-token: write
contents: read
uses: slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@v1.2.0
with:
base64-subjects: "${{ needs.build.outputs.hashes }}"
upload-assets: true # Optional: Upload to a new release
All in all, it will look as the following:
jobs:
build:
outputs:
hashes: ${{ steps.hash.outputs.hashes }}
steps:
- name: Checkout repository
uses: actions/checkout@2541b1294d2704b0964813337f33b291d3f8596b # tag=v3
- name: Build using gradle
id: build
run: |
# Your normal build workflow targets here
./gradlew clean build
- name: Generate subject
id: hash
run: |
echo "::set-output name=hashes::$(sha256sum ./build/libs/* | base64 -w0)"
- name: Upload build artifacts
uses: actions/upload-artifact@3cea5372237819ed00197afe530f5a7ea3e805c8 # tag=v3
with:
name: gradle-build-outputs
path: ./build/libs/
if-no-files-found: error
provenance:
needs: [build]
permissions:
actions: read
id-token: write
contents: read
uses: slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@v1.2.0
with:
base64-subjects: "${{ needs.build.outputs.hashes }}"
upload-assets: true # Optional: Upload to a new release
If you use Cargo to generate your artifacts, you can easily generate SLSA3 provenance by updating your existing workflow with the steps indicated in the workflow below:
- Declare an
outputs
for the hashes:
jobs:
build:
outputs:
hashes: ${{ steps.hash.outputs.hashes }}
- Build your binaries. Then add a step to generate the provenance subjects as shown below. Update the sha256 sum arguments to include all binaries that you generate provenance for:
steps:
[...]
- name: Build using cargo
run: |
# Your normal build workflow targets here.
cargo build --release
cp target/release/target_binary .
# Generate the subject.
- name: Generate subject
id: hash
run: |
set -euo pipefail
echo "::set-output name=hashes::$(sha256sum target_binary | base64 -w0)"
- Call the generic workflow to generate provenance by declaring the job below:
provenance:
needs: [build]
permissions:
actions: read # To read the workflow path.
id-token: write # To sign the provenance.
contents: write # To add assets to a release.
uses: slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@v1.2.0
with:
base64-subjects: "${{ needs.build.outputs.hashes }}"
upload-assets: true # Optional: Upload to a new release
All in all, it will look as the following:
jobs:
build:
outputs:
hashes: ${{ steps.hash.outputs.hashes }}
steps:
- name: Checkout repository
uses: actions/checkout@2541b1294d2704b0964813337f33b291d3f8596b # tag=v3
- name: Build using cargo
run: |
# Your normal build workflow targets here.
cargo build --release
cp target/release/target_binary .
# Generate the subject.
- name: Generate subject
id: hash
run: |
set -euo pipefail
echo "::set-output name=hashes::$(sha256sum target_binary | base64 -w0)"
provenance:
needs: [build]
permissions:
actions: read # To read the workflow path.
id-token: write # To sign the provenance.
contents: write # To add assets to a release.
uses: slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@v1.2.0
with:
base64-subjects: "${{ needs.build.outputs.hashes }}"
upload-assets: true # Optional: Upload to a new release
If you use Haskell (either via
cabal
or
stack
) to generate your
artifacts, you can easily generate SLSA3 provenance by updating your existing
workflow with the steps indicated in the workflow below.
- Declare an
outputs
for the hashes:
jobs:
build:
outputs:
hashes: ${{ steps.hash.outputs.hashes }}
- Build your binaries. Then add a step to generate the provenance subjects as shown below. Update the sha256 sum arguments to include all binaries that you generate provenance for:
steps:
[...]
- name: Build using Haskell
run: |
# Your normal build workflow targets here.
cabal build # or stack build
# Copy the binary to the root directory for easier reference
# For Cabal, use the following command
cp $(cabal list-bin .) .
# For Stack, use the following command instead
# cp $(stack path --local-install-root)/bin/target_binary .
# Generate the subject.
- name: Generate subject
id: hash
run: |
set -euo pipefail
echo "::set-output name=hashes::$(sha256sum target_binary | base64 -w0)"
- Call the generic workflow to generate provenance by declaring the job below:
provenance:
needs: [build]
permissions:
actions: read # To read the workflow path.
id-token: write # To sign the provenance.
contents: write # To add assets to a release.
uses: slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@v1.2.0
with:
base64-subjects: "${{ needs.build.outputs.hashes }}"
upload-assets: true # Optional: Upload to a new release
All in all, it will look as the following:
jobs:
build:
outputs:
hashes: ${{ steps.hash.outputs.hashes }}
steps:
- name: Checkout repository
uses: actions/checkout@2541b1294d2704b0964813337f33b291d3f8596b # tag=v3
- name: Setup Haskell
uses: haskell/actions/setup@745062a754c3c4b70b87cb93937ad443096cc94d # tag=v1
- name: Build using Haskell
run: |
# Your normal build workflow targets here.
cabal build # or stack build
# Copy the binary to the root directory for easier reference
# For Cabal, use the following command
cp $(cabal list-bin .) .
# For Stack, use the following command instead
# cp $(stack path --local-install-root)/bin/target_binary .
# Generate the subject.
- name: Generate subject
id: hash
run: |
set -euo pipefail
echo "::set-output name=hashes::$(sha256sum target_binary | base64 -w0)"
provenance:
needs: [build]
permissions:
actions: read # To read the workflow path.
id-token: write # To sign the provenance.
contents: write # To add assets to a release.
uses: slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@v1.2.0
with:
base64-subjects: "${{ needs.build.outputs.hashes }}"
upload-assets: true # Optional: Upload to a new release