Skip to main content
View Markdown ↗

Copy this page

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

On this page

Your first attestation

Intro

This quick tutorial will walk you through a simple example of how CI/lock can be used to record a signed attestation around a build, then verify it against a signed policy. To complete it successfully, you will need the following:

  • Go (1.22 or later is recommended)
  • openssl
  • jq
  • base64 (part of GNU coreutils on Linux, builtin on macOS)

You will also need to have CI/lock installed, which can be achieved by following the Installation guide. Confirm with cilock version before continuing.

A 30-second sample project to follow along:

mkdir demo-cilock && cd demo-cilock
git init -q          # enables the git attestor (it silently skips outside a repo)
cat > main.go <<'EOF'
package main

import "fmt"

func main() { fmt.Println("Hello, cilock!") }
EOF
go mod init github.com/aflock-ai/demo-cilock

1. Create a keypair

For a local walkthrough we sign with a file-based ed25519 key. (In CI you almost always switch to keyless Sigstore signing, covered in the CI quickstart.)

openssl genpkey -algorithm ed25519 -outform PEM -out testkey.pem
openssl pkey -in testkey.pem -pubout > testpub.pem

You now have testkey.pem (private, used to sign) and testpub.pem (public, used to verify).

caution

testkey.pem is a real signing key. Add *.pem and *.key to your .gitignore so it never ends up in source control.

2. Record attestations for a build step

cilock run wraps a command and produces a signed DSSE envelope containing one or more attestations.

cilock run \
  --step build \
  --signer-file-key-path testkey.pem \
  --outfile build.attestation.json \
  --offline \
  -- go build -o myapp ./
FlagShortWhat it does
--step-sNames the step in the supply-chain lifecycle. Required, the policy verifier matches collections by step name.
--signer-file-key-path-kPath to a local PEM private key, for this offline tutorial. Outside it, drop the key and --offline: with no signer flags, cilock run signs keyless through the TestifySec Platform (see Choose a signer).
--outfile-oWhere to write the signed envelope. Omit to write to stdout.
--offlineOpts out of the hosted platform for this local tutorial. This keeps the command fully offline even if you previously ran cilock login.
--workingdir-dDirectory the wrapped command runs in. Defaults to the current directory.
--attestations-aAttestors to record. Pass once per attestor (-a environment -a git) or comma-separated (-a environment,git). Defaults to [environment, git, platform]. See note below.
-- <cmd>Everything after -- is the command CI/lock wraps.

What gets recorded

Three attestors are always recorded regardless of flags:

  • material: SHA-256 digests of every file in the working directory before the step ran.
  • command-run: the wrapped command's argv, exit code, stdout/stderr digests, and process information.
  • product: SHA-256 digests of every file that changed or was added after the step ran.

Three more are recorded by default when -a is omitted:

  • environment: os, hostname, username, env vars (sensitive ones filtered).
  • git: commit SHA, tree hash, branch, and a snapshot of git status.
  • platform: binds the attestation to your hosted-platform tenant/product. Recorded only when a platform session exists (after cilock login); it silently skips when there is none — which is why it does not appear in the output below, since this tutorial passes --offline.

Run cilock attestors list to see the full set with (always run) and (default) markers.

-a replaces the default, it does not extend it

Passing any -a overrides the [environment, git, platform] default. If you want those plus an extra: -a environment -a git -a platform -a secretscan. A single -a secretscan drops environment, git, and platform from the output. The always-run material / command-run / product are unaffected.

A space-separated string inside quotes (-a "environment git") does not work, CI/lock treats the whole value as one attestor name.

3. View the attestation

The output is a DSSE envelope wrapping an in-toto Collection. Quick top-level inspection:

jq '.payloadType, .signatures[0].keyid' build.attestation.json
# "application/vnd.in-toto+json"
# "<64-char hex keyid>"

The keyid is the SHA-256 of testpub.pem, and policy verification later matches against it.

To see the full structured payload:

cat build.attestation.json | jq -r .payload | base64 -d | jq

To narrow to the step name and attestation types:

cat build.attestation.json | jq -r .payload | base64 -d | jq '.predicate.name, (.predicate.attestations | map(.type))'
# "build"
# [
#   "https://aflock.ai/attestations/environment/v0.1",
#   "https://aflock.ai/attestations/git/v0.1",
#   "https://aflock.ai/attestations/material/v0.3",
#   "https://aflock.ai/attestations/command-run/v0.2",
#   "https://aflock.ai/attestations/product/v0.3",
#   "https://aflock.ai/attestations/go-build/v0.1"
# ]

The last one, go-build, was not asked for: CI/lock recognised the wrapped go build and recorded what it built. That is also why build.attestation.json.detection.json and myapp.gobuild.json appeared beside the attestation.

To see the JSON schema of any specific attestor:

cilock attestors schema git

4. Create a policy file

The policy lists which attestations each step must produce and which keys are trusted to sign them. We start with a minimal policy requiring only the always-run trio:

policy.json
{
  "expires": "2030-12-17T23:57:40-05:00",
  "steps": {
    "build": {
      "name": "build",
      "attestations": [
        {"type": "https://aflock.ai/attestations/material/v0.3", "regopolicies": []},
        {"type": "https://aflock.ai/attestations/command-run/v0.2", "regopolicies": []},
        {"type": "https://aflock.ai/attestations/product/v0.3", "regopolicies": []}
      ],
      "functionaries": [{"type": "publickey", "publickeyid": "{{PUBLIC_KEY_ID}}"}]
    }
  },
  "publickeys": {
    "{{PUBLIC_KEY_ID}}": {
      "keyid": "{{PUBLIC_KEY_ID}}",
      "key": "{{B64_PUBLIC_KEY}}"
    }
  }
}

For the full policy schema (multi-step pipelines, Rego rules per attestor, certificate functionaries, timestamp roots) see the policy schema reference. CI/lock attestation type URIs use the https://aflock.ai/attestations/<name>/v0.1 form, the same shape witness uses but on the aflock.ai namespace. Both ecosystems can verify the other's collections through the witness compat shim.

5. Fill in the key ID and public key

The policy needs the key's ID and the base64-encoded PEM. cilock keyid show prints the ID exactly as cilock verify computes it, on Linux and macOS alike, with no sha256sum or shasum needed. The block checks the result before it touches the policy: an empty key ID would produce a policy that trusts no key.

# The key ID cilock uses for this key, and the base64-encoded public key
KEYID=$(cilock keyid show testpub.pem | awk '{print $1}')
PUBB64=$(base64 < testpub.pem | tr -d '\n')

# Substitute both placeholders only if the key ID is 64 hex characters.
# Writing to a temp file avoids the GNU vs BSD sed -i incompatibility.
if printf '%s' "$KEYID" | grep -Eq '^[0-9a-f]{64}

    
    

    
    
    
    
  

 && [ -n "$PUBB64" ]; then
  sed -e "s/{{PUBLIC_KEY_ID}}/${KEYID}/g" \
      -e "s|{{B64_PUBLIC_KEY}}|${PUBB64}|g" \
      policy.json > policy.json.tmp && mv policy.json.tmp policy.json
  echo "policy.json now trusts key $KEYID"
else
  echo "Could not read the key ID. Check that 'cilock version' works, then run this block again." >&2
fi

The key ID is also what jq '.signatures[0].keyid' build.attestation.json showed in step 3: the build was signed with this key, and the policy now trusts it.

6. Sign the policy

The policy itself is a signed DSSE envelope. Whoever signs the policy controls every gate built on top of it, so keep this key safe.

cilock sign \
  -f policy.json \
  -o policy-signed.json \
  --signer-file-key-path testkey.pem \
  --offline

In a real setup, the policy signing key would be a separate, more strictly controlled key than the per-step attestation key. We reuse testkey.pem here for brevity.

Logged in already?

cilock login enables the hosted platform's keyless Fulcio signer for commands that do not choose another signer. In this tutorial the local file key is intentional, so keep --offline on the local signing commands. You can also run cilock logout before following the offline walkthrough.

7. Verify the build meets the policy

cilock verify checks that every attestation listed in the policy was produced, signed by a trusted functionary, and satisfies any embedded Rego rules.

cilock verify \
  -p policy-signed.json \
  -a build.attestation.json \
  -f myapp \
  -k testpub.pem \
  --offline
FlagShortWhat it does
--policy-pPath to the signed policy.
--attestations-aAttestation envelopes to evaluate against the policy. Repeat for multiple.
--artifactfile-fThe artifact whose subject digest is being verified, CI/lock checks this digest appears in the attestation.
--publickey-kPublic key that signed the policy (not the attestation).

On success the exit code is 0 and the result line reads (the digest is your binary's):

verified: sha256:<64 hex digits> bound to step "build" subject "go-build/v0.1/binary:myapp"

A standards: report follows it. It lists the most a verifier could grant this evidence (here: no SLSA level, and ALPS-0 because a key file, not a platform identity, signed it) and what would raise each level. Those are ceilings, not verified levels. Any other exit code is a failure (an untrusted key, a missing required attestation, a denied Rego rule).

To verify a directory as a subject instead of a file:

cilock verify --directory-path build/output -p policy-signed.json -a build.attestation.json -k testpub.pem

8. See a denial

A policy is only worth something if it says no. Sign the same build with a second key that the policy does not trust, and verify it:

openssl genpkey -algorithm ed25519 -outform PEM -out otherkey.pem
cilock run --step build --signer-file-key-path otherkey.pem \
  --outfile untrusted.attestation.json --offline -- go build -o myapp ./
cilock verify -p policy-signed.json -a untrusted.attestation.json -f myapp -k testpub.pem --offline

cilock verify exits 1, and the reason names the key it was looking for:

collection rejected: "build", Reason: "no verifiers present to validate against collection verifiers
failed to verify envelope: no valid signatures for the provided verifiers found for keyids: <your testpub.pem key ID>

The build, the policy and the binary are the same as in step 7. Only the signer changed, and that is the reason it was refused.

9. Common failures

  • no valid signatures for the provided verifiers found for keyids: the policy does not trust the key that signed the attestation (step 8). Add the public key to publickeys and reference it in the step's functionaries.
  • no collection passed verification for step X: no attestation for step X passed: it was not passed to -a, it was signed by an untrusted key, or it was recorded under another --step name than the one in policy.steps.
  • missing required attestation: the policy lists an attestation type that wasn't produced. Add the attestor to -a in your cilock run.
  • deny: <message>: an embedded Rego rule denied the attestation. The message comes from the rule's deny[msg] output.

10. Where to next

You've just run the full attestation loop: build, sign, write policy, verify. From here:

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