Skip to content
CommonLayer
Documentation menu
On this page

Verifying artifacts

This guide shows how to verify a CommonLayer container image or Helm chart yourself before you deploy it: resolve the digest, verify the signature against the project's registered identity, verify the SBOM and provenance attestations, and compare with what the platform recorded. You do not have to trust the platform's verdict; the commands below check the same evidence independently with cosign.

Status. No CommonLayer artifact has been published to registry.commonlayer.org yet (see the implementation status). The commands match what the platform's verification-instructions endpoint generates (crates/commonlayer-artifacts/src/domain/instructions.rs) and what the project template's release workflow produces. What has actually run:

  • Key-signed artifacts, locally. The publish drill (WP-70g) promoted an nginx image and chart signed with a cosign key pair, then ran cosign verify and cosign verify-attestation for the SPDX, CycloneDX and SLSA v1 attestations against the CommonLayer registry, with cosign 3.1.4. The exact commands are in what the publish drill ran.
  • Keyless artifacts: never. No keyless signature (Fulcio certificate, Rekor entry, GitHub Actions identity) has been made or verified by this project, and no provenance produced by GitHub's attestation action has been through the pipeline. The keyless commands below follow cosign's documented flags and the platform's generated instructions; they have not been run against a CommonLayer artifact.

Tools

  • cosign 3.x (the repository pins 3.1.4 in scripts/dev/install-tools.sh), jq, and either Docker with Buildx or Helm 3.8+.
  • Network access to the registry and, for keyless signatures, to Sigstore's public transparency log and trust root (cosign fetches them).

1. Work with digests, never tags

Tags such as 1.30.6, 1.30, stable and latest are mutable pointers. The tag <version>-r<revision> (for example 1.30.6-r1) is immutable on CommonLayer, but you should still pin and verify the digest.

REF=registry.commonlayer.org/nginx
DIGEST=$(docker buildx imagetools inspect "$REF:1.30.6-r1" --format '{{json .Manifest}}' | jq -r .digest)
echo "$REF@$DIGEST"

The digest is the image index (multi-platform). List its platform manifests:

docker buildx imagetools inspect "$REF@$DIGEST" --format '{{json .Manifest}}' \
  | jq -r '.manifests[] | select(.platform.os != "unknown") | "\(.platform.os)/\(.platform.architecture) \(.digest)"'

(Entries with platform unknown/unknown are attestation manifests, not images.)

2. Ask the platform for the exact commands

The platform stores the signing identity that was in force when it verified the digest and generates the commands from it:

API=https://commonlayer.org/api/v1          # the deployment's API base
curl -fsS "$API/digests/$DIGEST/verify-instructions" \
  | jq -r '.commands[] | "# \(.title)\n\(.command)\n"'

The response also contains notes on what the commands prove. The following steps explain each command.

3. Verify the signature

Projects built from the template sign keyless in GitHub Actions. The identity is the workflow path in the project's repository, written in commonlayer.yaml as a glob, which the platform turns into an anchored regular expression (ADR-0026). For a project whose manifest declares https://github.com/acme/nginx/.github/workflows/*@refs/tags/v*:

cosign verify \
  --certificate-oidc-issuer https://token.actions.githubusercontent.com \
  --certificate-identity-regexp '^https://github\.com/acme/nginx/\.github/workflows/.*@refs/tags/v.*$' \
  "$REF@$DIGEST"

Use the regular expression from step 2 rather than writing your own: an identity that is too broad (for example ^https://github.com/acme/) accepts signatures from any workflow and ref of that organization.

Projects that register a public key instead (security.signing.key in the manifest):

curl -fsS "$API/digests/$DIGEST/verify-instructions" | jq -r '.commands[0].command' | sh   # writes commonlayer-signing.pub
cosign verify --key commonlayer-signing.pub "$REF@$DIGEST"

Read the first command before piping it to a shell; it is a cat > commonlayer-signing.pub heredoc with the project's registered key. In production the platform requires a transparency-log entry for every signature, so the plain command above is what you run against a published artifact.

What a valid signature proves depends on how the artifact was signed, and the platform's instructions say which applies (notes in the response):

  • Keyless: which repository, workflow and ref produced the digest, and that the signing event is in the public transparency log.
  • Key: that the holder of the project's registered key signed the digest; not which build produced it. The provenance attestation (step 5) is the evidence for that.

Either way a signature says nothing by itself about whether the content is secure, and it is only as strong as the project's control over its release tags or its key.

4. Verify the SBOM attestations

The template attaches SPDX and CycloneDX SBOMs as signed attestations to each platform digest (not to the index). For each platform digest PDIGEST from step 1:

cosign verify-attestation --type spdxjson \
  --certificate-oidc-issuer https://token.actions.githubusercontent.com \
  --certificate-identity-regexp '<regexp from step 2>' \
  "$REF@$PDIGEST" | jq -r '.payload' | base64 -d | jq '.predicate' > sbom.spdx.json

cosign verify-attestation --type cyclonedx \
  --certificate-oidc-issuer https://token.actions.githubusercontent.com \
  --certificate-identity-regexp '<regexp from step 2>' \
  "$REF@$PDIGEST" | jq -r '.payload' | base64 -d | jq '.predicate' > sbom.cdx.json

The decoded statement's subject[].digest.sha256 must equal PDIGEST.

What the SBOM lists. Besides the packages syft finds, CommonLayer SBOMs contain the software the project built from source (for example nginx, Valkey, PostgreSQL) and CommonLayer's own helpers, declared by the project with purls and CPEs; such entries carry the property commonlayer:declared (CycloneDX) or a sourceInfo naming the declaration (SPDX). If you scan the SBOM yourself: Grype matches the declared components through their CPEs; Trivy does not evaluate them, so a clean Trivy result says nothing about that software (ADR-0039).

grype sbom:sbom.cdx.json          # matches declared components by CPE
trivy sbom sbom.cdx.json          # OS packages only; declared components are not evaluated

The platform also stores the SBOMs it verified. Download them by the index digest and platform:

curl -fsS "$API/digests/$DIGEST/sboms/spdx-json?platform=linux/amd64" -o platform-sbom.spdx.json
curl -fsS "$API/digests/$DIGEST/sboms/cyclonedx-json?platform=linux/arm64" -o platform-sbom.cdx.json

A stored copy is convenient; the attestation verified with cosign is the evidence.

5. Verify the provenance

The template records SLSA v1 build provenance with GitHub's artifact attestation (actions/attest-build-provenance, Sigstore-signed, pushed to the registry next to the image). The platform's instructions include:

cosign verify-attestation --type slsaprovenance1 \
  --certificate-oidc-issuer https://token.actions.githubusercontent.com \
  --certificate-identity-regexp '<regexp from step 2>' \
  "$REF@$DIGEST"

GitHub documents its own verifier for these attestations, which checks them against the source registry where the workflow pushed them:

gh attestation verify oci://ghcr.io/<owner>/<repo>@$DIGEST --owner <owner>

Not yet verified. GitHub stores these attestations in a newer bundle format through the OCI referrers API, while the platform discovers referrers through the tag schema (ADR-0027) and verifies with cosign verify-attestation. Whether the cosign command above succeeds against such an attestation, and whether the attestation is mirrored into the CommonLayer registry, has not been tested: the template's release workflow has never run (threat model open item O11). The publish drill's provenance is different: an SLSA v1 statement in the shape of the GitHub action's, written and signed by the drill script itself with the drill's key. It shows that the platform parses, checks and preserves provenance through mirroring; it does not show that a build service attested a build. If the cosign command fails for an artifact whose platform record shows provenance.present = pass, report it.

What provenance establishes: which workflow run, in which repository, at which commit, built the digest, as attested by GitHub's signing identity. It does not establish that the build was hermetic or reproducible, and CommonLayer does not claim a SLSA level.

6. Compare with the platform's record

curl -fsS "$API/digests/$DIGEST" | jq '{state, checks: [.verification.checks[]? | {id, result}]}'

Every check of the verification policy should be pass, waived (with its recorded reason) or not_applicable. Scan results and vulnerabilities per digest: GET $API/digests/$DIGEST/vulnerabilities and …/scans; the project's OpenVEX statements: GET $API/digests/$DIGEST/vex.

The exact response fields are defined in the OpenAPI document (REST API); adjust the jq filter if your deployment's version differs.

Helm charts

Charts are OCI artifacts under oci://registry.commonlayer.org/charts/<chart>.

helm pull oci://registry.commonlayer.org/charts/nginx --version 0.1.0
# Pulled: registry.commonlayer.org/charts/nginx:0.1.0
# Digest: sha256:…
CHART_DIGEST=sha256:…    # from the output above

cosign verify \
  --certificate-oidc-issuer https://token.actions.githubusercontent.com \
  --certificate-identity-regexp '<regexp from step 2>' \
  "registry.commonlayer.org/charts/nginx@$CHART_DIGEST"

Released charts default to an image digest (the platform's chart.images_pinned check requires that it is a promoted CommonLayer image). Check it and verify that image as above:

helm show values oci://registry.commonlayer.org/charts/nginx --version 0.1.0 | grep -A6 '^image:'

Install by version and keep the digest pinned; image.digest: "" is the documented way to opt out of digest pinning, which you should not do in production.

What the publish drill ran

These are the consumer commands of scripts/dev/e2e-publish.sh as it ran in WP-70g against the drill's CommonLayer registry (localhost:5003, plain HTTP) with cosign 3.1.4. REF is localhost:5003/nginx@sha256:27c7… (a single-platform image, so the index and the platform digest are the same), API is the drill backend's http://localhost:18080/api/v1 and API_URL the same host without /api/v1 (the token realm is outside it).

# anonymous pull by digest through the registry's token flow
docker pull "$REF"

# what an anonymous token may do: the drill asked for pull,push and got pull only
curl -fsS "$API_URL/registry/token?service=commonlayer-registry&scope=repository:nginx:pull,push" \
  | jq '.token | split(".")[1] | gsub("-";"+") | gsub("_";"/")
        | . + ("=" * ((4 - length % 4) % 4)) | @base64d | fromjson | .access'

# signature and attestations, key-signed, without a transparency-log entry
VERIFY="--key cosign.pub --insecure-ignore-tlog=true --allow-http-registry=true --allow-insecure-registry=true"
cosign verify $VERIFY "$REF"
cosign verify-attestation $VERIFY --type slsaprovenance1 "$REF"
cosign verify-attestation $VERIFY --type spdxjson "$REF"
cosign verify-attestation $VERIFY --type cyclonedx "$REF"

# the stored SBOM, the verify instructions and the chart
curl -fsS -OJ "$API/digests/<digest>/sboms/spdx-json"
curl -fsS "$API/digests/<digest>/verify-instructions"
helm pull oci://localhost:5003/charts/nginx --version 0.1.0 --plain-http

What each extra flag means, and why you do not use it against a published artifact:

FlagWhy the drill needed itProduction
--insecure-ignore-tlog=truethe drill signs without uploading to Rekor (--tlog-upload=false), because the sandbox cannot reach Sigstore's public infrastructurenever: the platform requires a transparency-log entry and so should you
--allow-http-registry=true, --allow-insecure-registry=true, --plain-httpthe local registries speak plain HTTPnever

Results: every command exited 0 in both final runs; the decoded provenance names the promoted digest as its subject; the SBOM download has Content-Disposition: attachment and 14 SPDX packages; the verify instructions name cosign verify --key and the digest; the chart's default image is localhost:5003/nginx@<the promoted digest>. An unsigned image submitted afterwards was rejected, and the registry answered 404 for its digest.

In CI

A minimal admission step for your own pipeline:

set -eu
cosign verify --certificate-oidc-issuer https://token.actions.githubusercontent.com \
  --certificate-identity-regexp "$IDENTITY_REGEXP" "$REF@$DIGEST" > /dev/null
for p in $(docker buildx imagetools inspect "$REF@$DIGEST" --format '{{json .Manifest}}' \
           | jq -r '.manifests[] | select(.platform.os != "unknown") | .digest'); do
  cosign verify-attestation --type spdxjson --certificate-oidc-issuer https://token.actions.githubusercontent.com \
    --certificate-identity-regexp "$IDENTITY_REGEXP" "$REF@$p" > /dev/null
done
echo "verified $REF@$DIGEST"

This snippet verifies a keyless identity and has not been run against a CommonLayer artifact (none is published). Kubernetes admission controllers that run cosign policies (for example Sigstore's policy controller or Kyverno) can enforce the same identity; CommonLayer does not ship policies for them.