Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
248 changes: 248 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,248 @@
# SLSA Attester

`slsa-attester` generates signed [SLSA](https://slsa.dev) 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](https://github.com/slsa-framework/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](https://github.com/slsa-framework/attester/releases) for Linux
(amd64, arm64), macOS (arm64) or Windows (amd64), or build from source:

```sh
go install github.com/slsa-framework/attester@latest
```

Every release ships its own provenance, `attestations.jsonl`, generated by
the attester itself (see [Releases of this repository](#releases-of-this-repository)).
To verify a download before trusting it:

```sh
slsa-verifier build attestations.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`](https://github.com/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](docs/buildtypes/watcher/v1.md)
(`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:

```yaml
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:

```sh
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](https://github.com/carabiner-dev/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:

```sh
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:

```sh
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:

```sh
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:

```sh
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:

```sh
slsa-verifier build attestations.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:

```go
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](#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.
4 changes: 2 additions & 2 deletions attest/impl_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,9 @@ import (
"path/filepath"
"testing"

"github.com/carabiner-dev/signer"
signeroptions "github.com/carabiner-dev/signer/options"
intoto "github.com/in-toto/attestation/go/v1"
"github.com/policylabs/signer"
signeroptions "github.com/policylabs/signer/options"
sdsse "github.com/sigstore/protobuf-specs/gen/pb-go/dsse"
"google.golang.org/protobuf/types/known/structpb"
)
Expand Down
6 changes: 3 additions & 3 deletions attest/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,17 +9,17 @@ import (
"os"
"time"

"github.com/carabiner-dev/signer"
signeroptions "github.com/carabiner-dev/signer/options"
intoto "github.com/in-toto/attestation/go/v1"
"github.com/policylabs/signer"
signeroptions "github.com/policylabs/signer/options"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/types/known/structpb"
"google.golang.org/protobuf/types/known/timestamppb"
)

// Signer signs a serialized in-toto statement, returning the signed artifact
// (a sigstore bundle or a DSSE envelope). It is satisfied by *signer.Signer
// from github.com/carabiner-dev/signer.
// from github.com/policylabs/signer.
type Signer interface {
SignStatement(data []byte, funcs ...signeroptions.SignOptFn) (signer.SignedArtifact, error)
}
Expand Down
33 changes: 17 additions & 16 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,13 @@ module github.com/slsa-framework/attester
go 1.26.0

require (
github.com/carabiner-dev/attestation v0.2.1
github.com/carabiner-dev/collector v0.3.17
github.com/carabiner-dev/command v0.3.2
github.com/carabiner-dev/command v0.3.3
github.com/carabiner-dev/hasher v0.2.4
github.com/carabiner-dev/signer v0.6.2
github.com/google/go-github/v90 v90.0.0
github.com/in-toto/attestation v1.2.0
github.com/policylabs/attestation v0.3.0
github.com/policylabs/collector v0.4.0
github.com/policylabs/signer v0.6.4
github.com/protobom/protobom v0.6.2
github.com/sigstore/protobuf-specs v0.5.2
github.com/slsa-framework/protos v0.0.0-20260905230943-612c99695f3b
Expand Down Expand Up @@ -39,7 +39,7 @@ require (
github.com/AzureAD/microsoft-authentication-library-for-go v1.7.2 // indirect
github.com/CycloneDX/cyclonedx-go v0.12.0 // indirect
github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/ProtonMail/go-crypto v1.4.1 // indirect
github.com/ProtonMail/go-crypto v1.5.2 // indirect
github.com/anchore/go-struct-converter v0.1.0 // indirect
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect
github.com/avast/retry-go/v4 v4.7.0 // indirect
Expand All @@ -59,18 +59,17 @@ require (
github.com/aws/aws-sdk-go-v2/service/sts v1.45.0 // indirect
github.com/aws/smithy-go v1.27.3 // indirect
github.com/blang/semver/v4 v4.0.0 // indirect
github.com/carabiner-dev/attestation v0.2.1 // indirect
github.com/carabiner-dev/deadrop v0.0.0-20260228173914-d95e9ea2877d // indirect
github.com/carabiner-dev/ghrfs v0.3.7 // indirect
github.com/carabiner-dev/github v0.2.3 // indirect
github.com/carabiner-dev/jsonl v0.2.2 // indirect
github.com/carabiner-dev/openeox v1.0.0 // indirect
github.com/carabiner-dev/osv v0.1.2 // indirect
github.com/carabiner-dev/policy v0.5.6 // indirect
github.com/carabiner-dev/predicates v0.5.4 // indirect
github.com/carabiner-dev/sbomfs v0.2.0 // indirect
github.com/carabiner-dev/spdx3 v0.1.0 // indirect
github.com/carabiner-dev/stash v0.0.0-20260914181612-673fcf191e49 // indirect
github.com/carabiner-dev/vcslocator v0.5.0 // indirect
github.com/carabiner-dev/stash v0.0.0-20261004182444-008b3bad7a45 // indirect
github.com/carabiner-dev/vcslocator v0.5.1 // indirect
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
github.com/cenkalti/backoff/v5 v5.0.3 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
Expand Down Expand Up @@ -167,7 +166,7 @@ require (
github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6 // indirect
github.com/olekukonko/errors v1.3.0 // indirect
github.com/olekukonko/ll v0.1.8 // indirect
github.com/olekukonko/tablewriter v1.1.4 // indirect
github.com/olekukonko/tablewriter v1.1.5 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/image-spec v1.1.1 // indirect
github.com/openvex/go-vex v0.2.9 // indirect
Expand All @@ -176,6 +175,8 @@ require (
github.com/pjbgf/sha1cd v0.6.0 // indirect
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/policylabs/policy v0.5.7-0.20261004134513-55e5473eef09 // indirect
github.com/policylabs/predicates v0.6.0 // indirect
github.com/regclient/regclient v0.11.6 // indirect
github.com/ryanuber/go-glob v1.0.0 // indirect
github.com/samber/lo v1.53.0 // indirect
Expand All @@ -199,7 +200,7 @@ require (
github.com/sirupsen/logrus v1.10.2 // indirect
github.com/skeema/knownhosts v1.3.2 // indirect
github.com/spdx/tools-golang v0.5.7 // indirect
github.com/spiffe/go-spiffe/v2 v2.8.1 // indirect
github.com/spiffe/go-spiffe/v2 v2.8.2 // indirect
github.com/syndtr/goleveldb v1.0.1-0.20220721030215-126854af5e6d // indirect
github.com/theupdateframework/go-tuf v0.7.0 // indirect
github.com/theupdateframework/go-tuf/v2 v2.4.2 // indirect
Expand All @@ -222,22 +223,22 @@ require (
go.uber.org/zap v1.28.0 // indirect
go.yaml.in/yaml/v2 v2.4.4 // indirect
go.yaml.in/yaml/v3 v3.0.5 // indirect
golang.org/x/crypto v0.55.0 // indirect
golang.org/x/crypto v0.57.0 // indirect
golang.org/x/mod v0.41.0 // indirect
golang.org/x/net v0.58.0 // indirect
golang.org/x/net v0.59.0 // indirect
golang.org/x/oauth2 v0.36.0 // indirect
golang.org/x/sync v0.23.0 // indirect
golang.org/x/sys v0.48.0 // indirect
golang.org/x/term v0.45.0 // indirect
golang.org/x/term v0.46.0 // indirect
golang.org/x/text v0.42.0 // indirect
golang.org/x/time v0.15.0 // indirect
google.golang.org/api v0.290.0 // indirect
google.golang.org/genproto v0.0.0-20260622175928-b703f567277d // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20260819154853-08b0e4226688 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260819154853-08b0e4226688 // indirect
google.golang.org/grpc v1.83.2 // indirect
google.golang.org/grpc v1.84.0 // indirect
gopkg.in/warnings.v0 v0.1.2 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
k8s.io/klog/v2 v2.140.0 // indirect
sigs.k8s.io/release-utils v0.12.5-0.20260825061901-4f1f891011bb // indirect
sigs.k8s.io/release-utils v0.12.5 // indirect
)
Loading
Loading