Skip to content

About

Record, sign, store and verify build evidence in an OCI registry with cilock

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

3 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

cilock-attach-demo

Record what happened at each step of a build, sign it, store it next to the thing you built in an OCI registry, and let anyone who downloads it check it.

build step ─► cilock records inputs, command, outputs ─► signs it
           ─► stores the record beside the artifact in the registry
           ─► a downloader pulls both ─► cilock verify ─► PASSED / REFUSED

Two paths, same idea:

Path Artifact Needs a platform? Time
A. Binary (start here) a plain executable, built in six steps No — runs fully offline, registry optional ~10 min
B. Container a Docker image Yes — the TestifySec platform publishes the record for you ~20 min

The registry used below is JFrog Artifactory, but anything OCI-compliant works (ghcr, Docker Hub, ECR, Harbor).


Before you start

1. Tools

Tool Used for Install (macOS) Tested with
cilock records, signs, uploads, verifies curl -fsSL https://cilock.dev/install.sh | sh 4.4.0, 4.5.0
go builds the sample app brew install go 1.26
openssl, jq, python3 keys, JSON preinstalled / brew install jq jq 1.7.1
syft SBOM step brew install syft 1.23.1
trivy vulnerability-scan step brew install trivy 0.62.0
oras pushes/pulls the binary + evidence brew install oras 1.2.3
crane reads the registry (container path) brew install crane 0.20.3
docker + buildx builds the image (container path) Docker Desktop 29.6

Path A needs the first five; add oras to store in a registry. Path B needs all.

2. A registry you can push to (JFrog trial, 5 minutes)

  1. Start a trial at jfrog.com. You get https://<name>.jfrog.io.
  2. Repository: trials usually come with a Docker repository already (for example docker-trial); check Artifactory → Artifacts. If not: Administration → Repositories → Create a Repository → Local → Docker, and note the key.
  3. Token: avatar → Edit Profile → Generate an Identity Token. Copy it; it is shown once.
  4. Check it works:
    docker login <name>.jfrog.io -u <your-email>      # paste the token as the password

3. The TestifySec platform (Path B, and uploads in Path A)

  1. An account at platform.testifysec.com (you are the owner of your tenant).
  2. Log cilock in, and create a product for this repo:
    cilock login
    cilock use --product cilock-attach-demo      # creates it if missing
  3. Path B only — in the platform UI, Settings → Connectors → Container Registries → Add: host <name>.jfrog.io, repository key, username, the token from step 2.3, tick Docker build, leave Require policy pass off for now.

4. Your local settings

git clone <this repo> && cd cilock-attach-demo
cp demo/.env.local.example demo/.env.local && chmod 600 demo/.env.local
# edit demo/.env.local: REGISTRY_HOST, REPO_KEY, REGISTRY_USER, REGISTRY_PASS

demo/.env.local, the signing key (.keys/), generated policies and all build output are gitignored. Nothing secret is committed.

5. Preflight

cilock version && go version && syft version | head -1 && trivy --version | head -1
cilock doctor                                   # only needed for uploads / Path B
source demo/.env.local && oras login "$REGISTRY_HOST" -u "$REGISTRY_USER" --password-stdin <<<"$REGISTRY_PASS"
go test ./...                                   # the sample app: prints a greeting

Path A — a binary, six steps, verified offline

A1. Attest the build

OFFLINE=1 ./demo/10-binary-attest.sh    # signs locally, nothing uploaded
./demo/10-binary-attest.sh              # same, and uploads each record to the platform

The first run creates a throwaway signing key in .keys/. Six steps, each wrapped in cilock run — the build commands themselves do not change:

step command extra records
deps go mod verify
test go test -v ./...
build go build -o dist/hello . SLSA provenance, Go build info
sbom syft dist/hello -o spdx-json=… SBOM (SPDX)
scan trivy fs --format json … vulnerability scan
package tar czf dist/hello.tar.gz …

Every step records the git commit, what it read, what it wrote, and the exact command with its exit code. One signed envelope per step lands in evidence/binary/, next to a material-inventory file per step.

A2. Make the rule from what ran, then check

./demo/11-binary-verify.sh

cilock policy from-bundles reads the six envelopes and writes the policy: one entry per step, the key that signed it, and the step-to-step links it found by matching file digests. Then three checks:

case expected
the binary you built exit 0, PASSED
the same binary with one byte appended exit 1 — not the file that was built
the right binary, but the test step's record removed exit 1 — a step was skipped

A3. Store in the registry, verify like a downloader

./demo/12-binary-store.sh v1

Pushes two things into $REGISTRY_HOST/$REPO_KEY/hello:

hello:v1                           the binary
hello:sha256-<binary digest>.att   the six envelopes + their inventories

Then it pulls the binary into a clean folder, hashes it, derives the evidence tag from that hash, pulls the evidence and verifies the downloaded copy. PASSED, with nothing but the registry. The evidence tag is the binary's own fingerprint, so no lookup service is needed to find it.


Path B — a container image, published by the platform

Needs sections 3 and 4 above. The image-path scripts detect a hosted platform and tell you which UI click to make where a local platform would be scripted.

./demo/01-publish.sh v1        # build + push the image, record + sign + upload;
                               # waits for the PLATFORM to write sha256-<digest>.att
./demo/04-inspect.sh v1        # peel the .att open: media type, signer, what's inside,
                               # and that the signed payload names this image digest
./demo/02-verify.sh v1         # pull it back with crane, verify offline with cilock
./demo/03-policy-gate.sh v2    # turn "Require policy pass" on; one image gets its
                               # record published, the other is refused (fail-closed)

What happens in 01-publish.sh: cilock wraps docker buildx build --push, signs, and uploads to the platform. The platform matches the image subject to your connector and writes the record to <repo>:sha256-<image digest>.att using the credential stored on the connector — not your docker login. In JFrog, "Deployed By" on the .att is that credential's owner.

02-verify.sh path A (platform-side verify) and 03-policy-gate.sh need a policy bound to the product. On a local TestifySec development platform, 00-setup.sh creates everything (TestifySec engineers only: it drives the platform's GraphQL API through the jade tool). On the hosted platform, create the policy in Test Plans.

How the platform picks the registry target

From the record's subjects, never guessed:

record flavour image ref image digest
cilock / witness …/imagereference subject …/imagedigest subject
cosign / SLSA subject.name subject.digest.sha256

Target: <repo>:sha256-<digest>.att, one layer of type application/vnd.dsse.envelope.v1+json — the cosign layout, so cosign-aware tools find it.

When the platform does not publish

A record is published only if all of these hold:

  1. it names an image (reference + digest) — plain files are skipped today;
  2. that digest equals the registry's manifest digest;
  3. the image repository is under the connector's host and repository key;
  4. (auto-sync) its type matches the connector's filter;
  5. the registry accepts the stored credential;
  6. (gate on) it passed the policy bound to its product.

Troubleshooting

Real errors met while building this demo, and what fixed them.

You see Why Fix
attestation too large … over the 4MiB limit, mostly command-run cilock stores the command's console output inside the record send noisy output to a file: cilock run … -- sh -c 'make > build.log 2>&1' — the log is then recorded by digest
upload 401: Invalid API credential from GitHub Actions after cilock trust github owner/repo your account issues OIDC tokens with immutable IDs (repo:owner@123/repo@456:…); the trusted pattern repo:owner/repo:* never matches read the real prefix: gh api repos/<owner>/<repo>/actions/oidc/customization/sub, then cilock trust --issuer https://token.actions.githubusercontent.com --subject '<sub_claim_prefix>:*' --verify
cilock bundle create -s sha256:<hex> writes 0 envelopes subject digests are stored as bare hex pass the digest without the sha256: prefix
bundle create returns hundreds of envelopes the default walk (depth 5) follows shared subjects into other builds --max-depth 1
bundle create 401 in CI it reads with a cilock login token or --archivista-headers only mint the workflow OIDC token for <platform>/archivista and pass --archivista-headers "Authorization: Bearer $TOKEN"
downloaded verify fails at a step with no products the step's inventory file did not travel with its envelope push *-inventory.json alongside *.att.json (the store script does)
trivy reports a HIGH "private key" it scanned .keys/ the scan step skips .keys, evidence, download, release

Layout

main.go, main_test.go        the sample app
Dockerfile                   multi-stage build on Chainguard images
demo/.env.local.example      settings template → demo/.env.local (gitignored)
demo/env.sh                  shared config + helpers, sourced by every script
demo/10-binary-attest.sh     Path A: six attested build steps
demo/11-binary-verify.sh     Path A: policy from the evidence; verify green / tampered / skipped
demo/12-binary-store.sh      Path A: binary + evidence into the registry; verify the download
demo/00-setup.sh             Path B, local dev platform only: key, policy, product, connector
demo/01-publish.sh           Path B: build → record → platform publishes the .att
demo/02-verify.sh            Path B: pull from the registry → verify
demo/03-policy-gate.sh       Path B: fail-closed publish gate
demo/04-inspect.sh           Path B: what is inside the .att
demo/README.md               the 60-second explanation, how to read the output

Known limits

  • The platform publishes image records to the registry automatically; for plain binaries the push is a pipeline step (oras push) today.
  • The signing key here is a local file for simplicity. In CI, cilock signs keylessly with the workflow's identity; KMS and Vault signers are also supported.

About

Record, sign, store and verify build evidence in an OCI registry with cilock

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages