Skip to main content
View Markdown ↗

Copy this page

Select and copy the Markdown below, then paste it into your LLM.

On this page

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 full run summary says so. In CI the summary is one line by default, so pass --summary full (or set CILOCK_SUMMARY=full) to see that guidance; see Run summary. 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:

LevelThe policy requires
SLSA Build L1the step's collection contains SLSA provenance (https://slsa.dev/provenance/v1)
SLSA Build L2Build 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 0the collection contains command-run and git for the exact command, result and commit
ALPS 1ALPS 0, and the signer is a platform-issued workload or agent identity under the TestifySec Platform Fulcio CA, with an RFC 3161 timestamp

CI/lock's slsa attestor emits predicateType https://slsa.dev/provenance/v1, the SLSA v1 type, so SLSA verifiers recognize the provenance. The support matrix lists the Build level each environment reaches.

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 that signed your evidence. Use the pair that matches how the evidence was signed, never a mix.

TestifySec Platform roots, for evidence signed by the platform Fulcio and timestamped by the platform TSA. That covers every keyless section below except the public Sigstore alternative. They are published beside each CI/lock release:

curl -fsS https://cilock.dev/dl/v4.6.2/fulcio-roots.pem -o ca.pem
curl -fsS https://cilock.dev/dl/v4.6.2/tsa-chain.pem -o tsa.pem

Public Sigstore roots, only for evidence you signed with the public Sigstore alternative:

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

Then turn the chain into policy roots:


# 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 --offline
cilock verify -f myapp -p policy.signed.json -k policy-key.pub -a build.bundle.json --offline

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, L1, L2 and ALPS 0, 1.

Prerequisites: a workflow with id-token: write. Nothing else: CI/lock signs keyless against the TestifySec Platform Fulcio with the job's GitHub OIDC token and timestamps against the platform TSA.

The job runs the plain CLI. Evidence from cilock-action v1.0.4 keeps no file list, so the cilock verify -f below cannot pass on it (what v1.0.4 evidence cannot do).

.github/workflows/build.yml
permissions:
  id-token: write
  contents: read

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      # Outside the checkout, so the download is not a material.
      - name: Install cilock
        working-directory: ${{ runner.temp }}
        run: |
          VERSION=v4.6.2
          ARCHIVE="cilock-${VERSION#v}-linux-amd64.tar.gz"
          curl -fsSLO "https://cilock.dev/dl/${VERSION}/${ARCHIVE}"
          curl -fsSLO "https://cilock.dev/dl/${VERSION}/${ARCHIVE}.sha256"
          # Check only the archive's line of the sidecar; a mismatch stops the job.
          grep " ${ARCHIVE}\$" "${ARCHIVE}.sha256" > "${ARCHIVE}.line"
          sha256sum --strict -c "${ARCHIVE}.line"
          tar xzf "${ARCHIVE}" cilock
          sudo install -m 0755 cilock /usr/local/bin/cilock
      - name: Build with evidence
        run: |
          cilock run --step build -a environment,git,github,slsa \
            --enable-archivista=false -o build.bundle.json \
            -- go build -o myapp ./
      - 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") under extensions when the certificate carries that extension. Check with jq -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, use the flags in the public Sigstore alternative. That gives up ALPS 1, which needs a platform-issued identity.

GitHub-hosted Windows runners

Levels: SLSA Build L0, L1, L2 and ALPS 0, 1, as for GitHub Actions.

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.

.github/workflows/build-windows.yml
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, L1 and ALPS 0. Build L2 depends on the environment; the support matrix has the verdict for each.

On GitLab.com, Buildkite and CircleCI, CI/lock signs keyless against the TestifySec Platform Fulcio with the job's own OIDC token and timestamps against the platform TSA. You pass no signer flags. The support matrix still lists ALPS 1 for these three as planned, and this page does not claim it. Kubernetes has no identity the platform Fulcio accepts, so pods use the public Sigstore alternative.

Prerequisites: cilock on the worker (Installation), the job's OIDC token with audience sigstore, and no cilock login session or enrolled agent on the worker. Either of those would sign as itself instead of as the job.

PlatformHow CI/lock gets the tokenSigner identity (uris) in the certificate
GitLab.comthe job declares id_tokens: { SIGSTORE_ID_TOKEN: { aud: sigstore } }; without it, CI/lock refuses the run before the buildhttps://gitlab.com/<group>/<project>//.gitlab-ci.yml@refs/heads/<branch>
BuildkiteCI/lock runs buildkite-agent oidc request-token --audience sigstorehttps://buildkite.com/<org>/<pipeline>
CircleCICI/lock runs circleci run oidc get --claims '{"aud":"sigstore"}'https://circleci.com/api/v2/projects/<project-id>/pipeline-definitions/<definition-id>

Then run the build under CI/lock:

cilock run --step build -a environment,git,slsa \
  -o build.bundle.json -- go build -o myapp ./

On GitLab, add gitlab to the -a list so the provenance names the pipeline. This signs and writes build.bundle.json. It does not upload the evidence: storing it in the platform needs its own upload identity (Store attestations). Self-managed GitLab is not covered here; the platform Fulcio accepts only https://gitlab.com tokens, so use a signing key.

Verify with the verification at the top of the page, using the TestifySec Platform 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, or your CircleCI organization's https://oidc.circleci.com/org/<org-id>). Once supported, every Build L2 cell here rests on interpretation I1. On CircleCI the certificate does not say whether a cloud machine or a self-hosted runner ran the job.

Alternative: public Sigstore

Use this for Kubernetes, or when your verifiers trust only the public Sigstore roots. It signs and timestamps entirely in the public Sigstore trust domain: --offline turns off every platform default (Fulcio, TSA, evidence upload and the platform attestor), and the run names its own Fulcio and TSA. Pass the token in yourself:

PlatformHow the job gets the tokenSigner identity (uris) in the certificate
GitLab.comid_tokens: { SIGSTORE_ID_TOKEN: { aud: sigstore } } on the job, then $SIGSTORE_ID_TOKENas above
Buildkitebuildkite-agent oidc request-token --audience sigstoreas above
CircleCIcircleci run oidc get --claims '{"aud":"sigstore"}'as above
Kubernetesa projected service-account token with audience: sigstore, read from its mount pathhttps://kubernetes.io/namespaces/<namespace>/serviceaccounts/<name>
cilock run --step build -a environment,git,slsa \
  --offline \
  --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 Kubernetes, use --signer-fulcio-token-path with the token's mount path instead of --signer-fulcio-token. Verify with the public Sigstore roots in step 2, and pin the issuer as above (on Kubernetes, your cluster's issuer). Evidence signed this way is not under the TestifySec Platform CA, so it cannot reach ALPS 1. The Kubernetes Build L2 cell, once supported, also rests on interpretation I2.

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, L1 and ALPS 0.

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 \
  --offline \
  --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, which is SLSA Build L1.

An enrolled agent on a workstation

Covers a developer workstation (macOS or Linux), with or without a TPM. Levels: SLSA Build L0, L1 and ALPS 0, 1. 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, L1 and ALPS 0, 1) is set up in the Pushgate documentation: first signed push.

Reference generated from the product documentation. Match commands and support details to your installed release.