# Set up a supported level

Source: https://www.testifysec.com/docs/cilock/guides/reach-a-supported-level

The exact CI/lock configuration, and the verify command that proves it, for every Supported cell in the support matrix.

Each Supported cell in the [support matrix](https://www.testifysec.com/docs/concepts/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 |

 

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](https://www.testifysec.com/docs/concepts/support-matrix#sm-planned-isolated-provenance).

 

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.

 

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

 

```bash
# 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.**

 

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

 

**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.

 

.github/workflows/build.yml

```yaml
permissions:
  id-token: write
  contents: read

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: aflock-ai/cilock-action@v1.0.4
        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"`) 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, set the Fulcio and timestamp inputs shown in the [CI quickstart](https://www.testifysec.com/docs/cilock/getting-started/quickstart-ci#signing-with-the-public-sigstore-fulcio-optional). 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](https://www.testifysec.com/docs/cilock/guides/reach-a-supported-level#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

```yaml
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](https://www.testifysec.com/docs/cilock/getting-started/installation#2-prebuilt-binary). 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.

 

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](https://www.testifysec.com/docs/concepts/support-matrix#sm-planned-platform-fulcio-ci) here. For public Sigstore, each job requests a token with audience `sigstore` and passes it in.

 

**Prerequisites:** `cilock` on the worker ([Installation](https://www.testifysec.com/docs/cilock/getting-started/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:

 

```bash
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, 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.

 

```bash
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`, 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.

 

```bash
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](https://www.testifysec.com/docs/pushgate/setup#first-signed-push).

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