Set up a supported level
Each Supported cell in the support matrix links to one section of this page. A section gives the prerequisites, the CI/lock configuration, and the check that proves the level. The sections are grouped by where you build, because one configuration serves several levels.
What proves a level
CI/lock records and signs evidence. It does not assign a level; its run summary says so. A verifier assigns the level, and in CI/lock the verifier is a signed policy that cilock verify evaluates. The policy proves a level when it requires these things:
| Level | The policy requires |
|---|---|
| SLSA Build L1 | the step's collection contains SLSA provenance (https://slsa.dev/provenance/v1) |
| SLSA Build L2 | Build L1, and the signer is a root functionary pinned to your build's workload identity (uris and extensions.Issuer) under a certificate authority you trust |
| ALPS 0 | the collection contains command-run and git for the exact command, result and commit |
| ALPS 1 | ALPS 0, and the signer is a platform-issued workload or agent identity under the TestifySec Platform Fulcio CA, with an RFC 3161 timestamp |
SLSA Build L1 and L2 are planned, not supported, today. CI/lock's slsa attestor emits predicateType https://slsa.dev/provenance/v1.0, which is not the SLSA v1 type https://slsa.dev/provenance/v1, so SLSA verifiers do not recognize the provenance. The matrix lists those cells as planned until the attestor emits the v1 type. The sections below keep the slsa attestor in their configuration because those levels will rest on it.
Build L2 on these platforms rests on interpretation I1 of the SLSA spec, and in some places also on I2. I1 counts provenance that a CI/lock step writes on the build platform's own workers, signed with the job's workload identity, as generated by the control plane. GitHub publishes the same reading for its own artifact attestations. I2 treats the organization that operates self-hosted workers as the build platform. Under the strict reading no environment reaches Build L2. SLSA Build L3 needs a signer the build steps cannot reach; that is planned work.
Every section ends with the same verification. You turn the evidence into a starter policy, check it, sign it, and verify the artifact against it.
1. A starter policy from the evidence. It has one step per file, the attestation types found, and the signer. from-bundles names the step after the file (build.bundle.json becomes step build), so keep that file name.
cilock policy from-bundles build.bundle.json -o policy.json # keyless evidence
cilock policy from-bundles -k build-key.pub build.bundle.json -o policy.json # key-signed evidence
jq '.steps.build | {types: [.attestations[].type], signer: .functionaries}' policy.json
2. Keyless evidence only: trust the CA, not the leaf. For a certificate signer, from-bundles records the evidence's own leaf certificate as the trust root. That policy verifies only the file it came from, and it trusts whatever certificate that file carries. Replace it with the certificate authority and timestamp authority you mean to trust:
# Public Sigstore
curl -fsS https://fulcio.sigstore.dev/api/v1/rootCert -o ca.pem
curl -fsS https://timestamp.sigstore.dev/api/v1/timestamp/certchain -o tsa.pem
# ...or the TestifySec Platform, published beside each CI/lock release
curl -fsS https://cilock.dev/dl/v4.5.0/fulcio-roots.pem -o ca.pem
curl -fsS https://cilock.dev/dl/v4.5.0/tsa-chain.pem -o tsa.pem
# A PEM chain as a policy root: the self-signed certificate is the root,
# the others are intermediates.
pem_to_root() {
dir=$(mktemp -d)
awk -v d="$dir" '/BEGIN CERTIFICATE/{n++} n{print > (d "/" n ".pem")}' "$1"
root=""; inter=""
for c in "$dir"/*.pem; do
b64=$(base64 < "$c" | tr -d '\n')
if [ "$(openssl x509 -in "$c" -noout -subject)" = "$(openssl x509 -in "$c" -noout -issuer | sed 's/^issuer=/subject=/')" ]
then root=$b64; else inter="$inter $b64"; fi
done
[ -n "$root" ] || { echo "no self-signed root in $1" >&2; return 1; }
jq -n --arg root "$root" --arg inter "$inter" \
'{certificate: $root, intermediates: ($inter | split(" ") | map(select(. != "")))}'
}
jq --argjson ca "$(pem_to_root ca.pem)" --argjson tsa "$(pem_to_root tsa.pem)" \
'.roots = {ca: $ca} | .timestampauthorities = {tsa: $tsa}
| .steps[].functionaries |= map(if .type == "root" then .certConstraint.roots = ["ca"] else . end)' \
policy.json > policy.ca.json && mv policy.ca.json policy.json
3. Sign the policy with a key you control, then verify the artifact offline.
openssl genpkey -algorithm ed25519 -out policy-key.pem
openssl pkey -in policy-key.pem -pubout -out policy-key.pub
cilock sign -f policy.json -o policy.signed.json --signer-file-key-path policy-key.pem --platform-url ""
cilock verify -f myapp -p policy.signed.json -k policy-key.pub -a build.bundle.json --platform-url ""
cilock verify exits 0 only when every requirement holds.
GitHub Actions
Covers GitHub-hosted runners, self-hosted runners, and a GitHub-hosted job that signs in a reusable workflow. Levels: SLSA Build L0 and ALPS 0, 1. SLSA Build L1 and L2 are planned.
Prerequisites: a workflow with id-token: write. Nothing else: the action signs keyless against the TestifySec Platform Fulcio with the job's GitHub OIDC token and timestamps against the platform TSA.
permissions:
id-token: write
contents: read
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
step: build
command: "go build -o myapp ./"
attestations: environment git github slsa
enable-archivista: false
outfile: build.bundle.json
- uses: actions/upload-artifact@v4
with:
name: build-evidence
path: |
build.bundle.json
myapp
Verify. Download the artifact (gh run download <run-id> -n build-evidence) and run the verification above. In policy.json, the signer must be a root functionary whose uris is your workflow, for example https://github.com/<owner>/<repo>/.github/workflows/build.yml@refs/heads/main, and whose extensions.Issuer is https://token.actions.githubusercontent.com. That pin, under the TestifySec Platform CA and TSA from step 2, is ALPS 1: the platform issued the identity and the evidence carries an RFC 3161 timestamp. The same pin is what Build L2 will require once it is supported.
- A self-hosted runner uses the same workflow with its own
runs-on. Its Build L2 cell, once supported, also rests on interpretation I2. - To tell the two apart, pin the runner type too: add
"RunnerEnvironment": "github-hosted"(or"self-hosted") underextensionswhen the certificate carries that extension. Check withjq -r '.signatures[0].certificate' build.bundle.json | base64 -d | openssl x509 -noout -text. - To chain to the public Sigstore root instead of the platform CA, set the Fulcio and timestamp inputs shown in the CI quickstart. That gives up ALPS 1, which needs a platform-issued identity.
GitHub-hosted Windows runners
Levels: SLSA Build L0 and ALPS 0, 1, as for GitHub Actions. SLSA Build L1 and L2 are planned.
Prerequisites: id-token: write, and cilock.exe. The cilock-action does not run on Windows runners, so the job installs the Windows release directly. On GitHub Actions, CI/lock fetches the job's OIDC token itself, so no signer flags are needed.
permissions:
id-token: write
contents: read
jobs:
build:
runs-on: windows-latest
steps:
- uses: actions/checkout@v4
- name: Install cilock.exe
shell: pwsh
# install-cilock.ps1 is the PowerShell recipe from Installation >
# Windows, committed to your repository. It checks the archive's
# SHA-256 and extracts cilock.exe to cilock-<version>\.
run: ./install-cilock.ps1
- name: Build with evidence
shell: pwsh
run: |
& ".\cilock-$env:CILOCK_VERSION\cilock.exe" run --step build `
-a environment,git,github,slsa -o build.bundle.json `
-- go build -o myapp.exe .
The PowerShell recipe is in Installation. Set CILOCK_VERSION to the version it installs. The Windows build has no omnitrail attestor and no --trace backend, which none of these levels needs. Verify as for GitHub Actions, with -f myapp.exe.
GitLab.com, Buildkite, CircleCI and Kubernetes
Covers GitLab.com hosted runners, Buildkite hosted agents, CircleCI cloud and pods on EKS, GKE or AKS. Levels: SLSA Build L0 and ALPS 0. SLSA Build L1 and L2 are planned.
These platforms issue a workload OIDC token that the public Sigstore Fulcio accepts, and this section signs against public Sigstore. The TestifySec Platform Fulcio also accepts GitLab.com, Buildkite and CircleCI identities, but this page does not document that setup yet, so ALPS 1 is planned here. For public Sigstore, each job requests a token with audience sigstore and passes it in.
Prerequisites: cilock on the worker (Installation), and a token with audience sigstore:
| Platform | How the job gets the token | Signer identity (uris) in the certificate |
|---|---|---|
| GitLab.com | id_tokens: { SIGSTORE_ID_TOKEN: { aud: sigstore } } on the job, then $SIGSTORE_ID_TOKEN | https://gitlab.com/<group>/<project>//.gitlab-ci.yml@refs/heads/<branch> |
| Buildkite | buildkite-agent oidc request-token --audience sigstore | https://buildkite.com/<org>/<pipeline> |
| CircleCI | circleci run oidc get --claims '{"aud":"sigstore"}' | https://circleci.com/api/v2/projects/<project-id>/pipeline-definitions/<definition-id> |
| Kubernetes | a projected service-account token with audience: sigstore, read from its mount path | https://kubernetes.io/namespaces/<namespace>/serviceaccounts/<name> |
Then run the build under CI/lock, signing against public Sigstore:
cilock run --step build -a environment,git,slsa \
--platform-url "" \
--signer-fulcio-url https://fulcio.sigstore.dev \
--signer-fulcio-use-http=false \
--signer-fulcio-token "$SIGSTORE_ID_TOKEN" \
--timestamp-servers https://timestamp.sigstore.dev/api/v1/timestamp \
-o build.bundle.json -- go build -o myapp ./
On GitLab, add gitlab to the -a list so the provenance names the pipeline. On Kubernetes, use --signer-fulcio-token-path with the token's mount path instead of --signer-fulcio-token.
Verify with the verification at the top of the page, using the public Sigstore roots in step 2. The signer must be a root functionary pinned to the identity in the table and to the issuer (https://gitlab.com, https://agent.buildkite.com, your CircleCI organization's https://oidc.circleci.com/org/<org-id>, or your cluster's issuer). Once supported, every Build L2 cell here rests on interpretation I1, and the Kubernetes cell also on I2. On CircleCI the certificate does not say whether a cloud machine or a self-hosted runner ran the job.
Any CI with a signing key
Covers GitLab self-managed, Jenkins, Azure DevOps Microsoft-hosted agents, AWS CodeBuild and Google Cloud Build. Levels: SLSA Build L0 and ALPS 0. SLSA Build L1 is planned.
None of these has an OIDC identity that a Fulcio accepts today, so Build L2 is not available: SLSA L2 needs the platform, not a key you hold, to vouch for the provenance. Build L1 needs only provenance, and a key is enough to make it verifiable once the provenance carries the SLSA v1 type.
Prerequisites: cilock on the worker, and a signing key the job can read from your CI's secret store.
cilock run --step build -a environment,git,slsa \
--platform-url "" \
--signer-file-key-path "$BUILD_KEY_PATH" \
-o build.bundle.json -- go build -o myapp ./
Add jenkins to the -a list on Jenkins, gitlab on GitLab and aws-codebuild on CodeBuild, so the provenance names the pipeline.
Verify. from-bundles needs the public key to pin the signer: cilock policy from-bundles -k build-key.pub build.bundle.json -o policy.json. Skip step 2 and run steps 1 and 3 at the top of the page. The policy lists command-run and git, which is ALPS 0. It also lists the provenance type, https://slsa.dev/provenance/v1.0 today; Build L1 needs https://slsa.dev/provenance/v1.
An enrolled agent on a workstation
Covers a developer workstation (macOS or Linux), with or without a TPM. Levels: SLSA Build L0 and ALPS 0, 1. SLSA Build L1 is planned. A workstation is not a hosted build platform, so Build L2 is not available.
Prerequisites: cilock 4.5 or later, and a person to approve the enrollment with their platform passkey. The agent can start the enrollment; it cannot complete it.
cilock enroll agent --repo <owner>/<repo> # a person approves in the browser
cilock agent status # prints the agent's SPIFFE ID and expiry
cilock run --step build -a environment,git,slsa,alps-evidence \
-o build.bundle.json -- go build -o myapp ./
With an enrolled agent, cilock run signs keyless as the agent, under the TestifySec Platform Fulcio, with a platform timestamp. The identity is time-bound: eight hours by default.
Verify with the verification at the top of the page. The signer must be a root functionary whose uris is the SPIFFE ID that cilock agent status printed. Under the TestifySec Platform roots from step 2, that is ALPS 1. CI/lock observes the agent (alps-evidence), but an observation does not authenticate the agent or prove its isolation.
Pushgate
The Pushgate mint (SLSA Build L0 and ALPS 0, 1; SLSA Build L1 is planned) is set up in the Pushgate documentation: first signed push.