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 verifyandcosign verify-attestationfor 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:
| Flag | Why the drill needed it | Production |
|---|---|---|
--insecure-ignore-tlog=true | the drill signs without uploading to Rekor (--tlog-upload=false), because the sandbox cannot reach Sigstore's public infrastructure | never: the platform requires a transparency-log entry and so should you |
--allow-http-registry=true, --allow-insecure-registry=true, --plain-http | the local registries speak plain HTTP | never |
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.