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-cilock1. 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.pemYou now have testkey.pem (private, used to sign) and testpub.pem (public, used to verify).
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 ./| Flag | Short | What it does |
|---|---|---|
--step | -s | Names the step in the supply-chain lifecycle. Required, the policy verifier matches collections by step name. |
--signer-file-key-path | -k | Path 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 | -o | Where to write the signed envelope. Omit to write to stdout. |
--offline | Opts out of the hosted platform for this local tutorial. This keeps the command fully offline even if you previously ran cilock login. | |
--workingdir | -d | Directory the wrapped command runs in. Defaults to the current directory. |
--attestations | -a | Attestors 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 ofgit status.platform: binds the attestation to your hosted-platform tenant/product. Recorded only when a platform session exists (aftercilock 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 itPassing 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 | jqTo 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 git4. 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:
{
"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}