SLSA Attester
slsa-attester generates signed SLSA attestations as
in-toto statements built on the official SLSA predicate protos:
- Build provenance, in the v1 and v0.2 predicates.
- Verification summary attestations (VSAs), v1.
- Provenance for GitHub Actions runs, with a watcher that observes a
run from a job of its own, waits for the other jobs, and attests the
artifacts they produced. This is the successor to the attestation half of
slsa-github-generator.
It is a command line tool and a Go library. What it produces verifies with
the SLSA Verifier: the two
tools share the algorithm:digest syntax for subjects, so a digest written
at attest time reads back verbatim at verify time.
Installing
Download a binary from the
releases for Linux
(amd64, arm64), macOS (arm64) or Windows (amd64), or build from source:
go install github.com/slsa-framework/attester@latest
Every release ships its own provenance, attestations.intoto.jsonl, generated by
the attester itself (see Releases of this repository);
releases up to v0.1.0 published it as attestations.jsonl.
To verify a download before trusting it:
slsa-verifier build attestations.intoto.jsonl slsa-attester-v0.1.0-linux-amd64 \
--param expected_source:github.com/slsa-framework/attester \
--param 'trusted_builders:[https://github.com/slsa-framework/attester/.github/workflows/release.yaml@refs/tags/v0.1.0]' \
--signer 'sigstore::https://token.actions.githubusercontent.com::https://github.com/slsa-framework/actions/.github/workflows/attest_actions.yml@refs/heads/main' \
--require-signatures
The slsa-framework/actions
repository does this for you in GitHub Actions.
Attesting GitHub Actions runs
slsa-attester watch observes a workflow run, waits for its jobs to finish
and attests the artifacts they produced, as provenance of the
watcher build type
(https://slsa.dev/buildtypes/watcher/v1). The provenance names the
workflow that ran as the builder, records the ref, event, repository and
inputs the run was triggered with, and the commit it built.
From inside the run
The easiest way is the reusable workflow in slsa-framework/actions, added
as one more job to the workflow being attested:
jobs:
build:
runs-on: ubuntu-latest
steps:
# ... build and upload your artifacts ...
provenance:
permissions:
id-token: write # sigstore keyless signing
actions: read # read the run's jobs and artifacts
contents: read
uses: slsa-framework/actions/.github/workflows/attest_actions.yml@main
The job waits for every other job of the run, downloads their workflow
artifacts, hashes them and signs the provenance. Because the signing
happens inside the reusable workflow, the sigstore certificate identifies
slsa-framework/actions, a builder identity independent of the workflow
being attested, the way slsa-github-generator worked. The attestation is
uploaded as a workflow artifact (slsa-attestations by default).
The workflow's inputs choose what is attested:
| Input |
Meaning |
watch-jobs |
Only these jobs are waited for (default: every other job) |
collect-artifacts, expand-artifacts |
Attest the run's workflow artifacts, unpacking archives to one subject per file (both default on) |
artifacts-filter |
Globs on artifact and asset names; only matches are attested |
release |
Also attest the assets of this release tag |
sboms |
Collector sources of SBOMs whose top-level elements are attested |
subjects, checksums, dependencies |
Extra subjects as algorithm:digest, a sha256sum listing, and extra resolved dependencies |
version, verify, build-from-source |
Which attester runs, and whether its release is verified against its provenance first |
The attester must have a job of its own. Every step that runs earlier in
a job can tamper with the attester, and every step in the job shares the
OIDC identity the attestation is signed with. The watcher inspects its own
job and refuses to run beside any other step. A hostile step that already
ran could defeat that check too, which is why verifiers pin the signing
identity rather than trust the check.
From outside the run
Any run can be watched by its spec, with a GITHUB_TOKEN in the
environment that can read it. The watcher waits for the whole run and
attests when it completes:
slsa-attester watch github://slsa-framework/attester/123456789 --release v0.1.0 -o provenance.json
Outside the attested run the ref is resolved through the API, telling tags
from branches; the workflow's inputs can only be recorded as the defaults
the workflow declares, since the platform does not expose the values a run
was given after the fact.
Where subjects come from
| Source |
Flag |
Digest computed by |
| Workflow artifacts of the run |
--collect-artifacts (default) |
the attester, which downloads and hashes them |
| Release assets of a tag |
--release <tag> |
the platform, which reports the digest it computed at upload; assets without one are downloaded and hashed |
| Top-level elements of an SBOM |
--sbom <source> |
asserted by the SBOM; elements without hashes are rejected |
Checksum files (sha256sum output) |
--checksums <file> |
asserted by the caller |
| Explicit digests |
-s algorithm:digest |
asserted by the caller |
SBOMs are located with the
carabiner collector, so a
source can be a directory (fs:sboms/), a release (release:owner/repo@v1.0.0)
or anything else a collector driver reaches, and are read with protobom,
so SPDX and CycloneDX documents work, bare or wrapped in an attestation.
Attesting builds
slsa-attester build writes build provenance over subjects given as files,
which are hashed, or as digests:
slsa-attester build --builder-id=https://builder.example/ci \
--build-type=https://builder.example/buildtypes/container/v1 \
--invocation-id=run-42 -o provenance.json bin/app
Flags use the latest (v1) vocabulary. Selecting the older predicate with
--predicate-version v0.2 maps them onto the older field names
(--invocation-id becomes metadata.buildInvocationId, resolved
dependencies become materials), and a flag that exists in only one version
is refused when targeting the other.
A whole predicate can be passed as a base with --predicate, as JSON or
@file. It is parsed strictly against the official SLSA proto of the
selected version, and the content flags are merged onto it:
slsa-attester build --predicate @predicate.json --finished-on 2026-10-05T12:00:00Z bin/app
Resource descriptors (--resolved-dependency, --byproduct,
--builder-dependency) take JSON, @file, or the shorthand
name=…,uri=…,sha256=….
Attesting verifications
slsa-attester vsa writes a verification summary attestation, the record
a verifier leaves of what it verified and to which level:
slsa-attester vsa --verifier-id=https://verifier.example \
--resource-uri=pkg:oci/app@sha256:9f86d0… \
--verification-result=PASSED --verified-level=SLSA_BUILD_LEVEL_3 \
--policy-uri=https://policies.example/release.json \
-s sha256:9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08
--input-attestation records the attestations the verdict was based on,
and --dependency-level the levels of the resource's dependencies.
Signing
Every command signs by default, with sigstore: ambient credentials are
used where they exist (GitHub Actions, GitLab CI, GCP), with the browser
flow as the fallback. The result is a sigstore bundle, recorded in Rekor
and timestamped. --sigstore-instance github signs against GitHub's
sigstore instance instead of the public one.
With a key the result is a DSSE envelope; without signing, a bare
statement:
slsa-attester build --signing-key key.pem ... # DSSE envelope; the passphrase comes from $SIGNING_KEY_PASSPHRASE
slsa-attester build --sign=false ... # unsigned statement
Keys may be PEM (PKCS#8, PKCS#1, SEC1) or OpenPGP. A SPIFFE identity signs
with --signing-backend spiffe.
Verifying
Attestations verify with the SLSA Verifier. The provenance's builder.id
is a claim; the verifier binds it to the signing identity. For provenance
generated through the reusable workflow, the certificate proves which
workflow's run the attester observed, so the builder is proven rather than
claimed:
slsa-verifier build attestations.intoto.jsonl your-artifact \
--param expected_source:github.com/your-org/your-repo \
--param 'trusted_builders:[https://github.com/your-org/your-repo/.github/workflows/release.yml@refs/tags/v1.0.0]' \
--signer 'sigstore::https://token.actions.githubusercontent.com::https://github.com/slsa-framework/actions/.github/workflows/attest_actions.yml@refs/heads/main' \
--require-signatures --level 3
The release tag is held by the @ref on the trusted builder. The
verify/build action in slsa-framework/actions runs the same check in a
workflow.
Using the library
The attest package exposes the same functionality through one canonical
option set; the per-version mapping and the compatibility checks happen
inside:
import "github.com/slsa-framework/attester/attest"
writer := &attest.Writer{}
err := writer.Attest(attest.SlsaProvenanceV1, []string{"bin/app"},
attest.WithBuilderID("https://builder.example/ci"),
attest.WithInvocationID("run-42"),
attest.WithWriter(out),
)
Attest takes the format (SlsaProvenanceV1, SlsaProvenanceV02,
VsaV1) and the subject files; AttestSlsaProvenanceV1,
AttestSlsaProvenanceV02 and AttestVSAV1 are the same with the format
fixed. Subjects known only by digest come in with WithSubjects, a base
predicate with WithPredicate, and a Signer with WithSigner; without
one the statement is written unsigned.
Releases of this repository
Each release is attested by the attester itself: the release workflow has
one job that builds and publishes with goreleaser, and a second, dedicated
job that runs the reusable workflow to watch the first and attest the
release assets. The provenance is signed with the slsa-framework/actions
identity, so the command under Installing verifies it.
Contributing
The SLSA attester is released under the terms of the Apache 2.0 license.
Patches and issues are welcome in the repository.