How to verify file signatures with Cosign
Use Cosign to verify non-container software artifacts
For the complete documentation index, see llms.txt.
Cosign verifies keyless signatures against the public Sigstore infrastructure, which an air-gapped environment can’t reach. You can still verify signatures and attestations there. You need to carry two things across the air gap ahead of time: the Sigstore trust root, and the signatures themselves.
This guide uses Chainguard Containers as the example artifact, but the technique applies to any container image signed with Cosign.
A keyless verification normally makes three kinds of network request. Only the first blocks you in a disconnected environment:
tuf-repo-cdn.sigstore.dev. Cosign refreshes this metadata on every verification, so an air-gapped run fails unless you supply the trust root from a file.The last point matters: you keep transparency-log verification in an air-gapped environment. You don’t need --insecure-ignore-tlog, and you shouldn’t use it, because it discards a check that still works.
cgr.dev and the public Sigstore infrastructure.Run this on the connected machine. cosign initialize downloads the current TUF metadata and writes it to a local cache:
cosign initializeCopy the trust root out of that cache:
cp ~/.sigstore/root/tuf-repo-cdn.sigstore.dev/targets/trusted_root.json .The file is about 7 KB. It holds the Fulcio certificate authorities, the Rekor transparency log keys, the certificate transparency log keys, and the timestamp authority certificates that Cosign checks a signature against.
Note: Copying the whole
~/.sigstoredirectory into the air-gapped environment doesn’t work. Cosign refreshes its TUF metadata over the network on every verification, so a copied cache still fails. Pass the trust root as a file with--trusted-rootinstead.
A Cosign signature is a separate artifact in the registry, stored under a tag derived from the image digest. Copying an image alone leaves the signature behind, and verification inside the air gap then fails with no signatures found.
To see what’s attached to an image, run cosign tree on the connected machine:
cosign tree cgr.dev/chainguard/go:latestThe output lists the signature and attestation tags attached to the image:
📦 Supply Chain Security Related artifacts for an image: cgr.dev/chainguard/go:latest
└── 💾 Attestations for an image tag: cgr.dev/chainguard/go:sha256-6be282d9e6dc....att
├── 🍒 sha256:475e95ecb3900fbf479c31b79527c8d1a8f85445dd44caaf21310fb8cd56fdad
├── 🍒 sha256:ae3c33f8f466481133b4826e165f09cdd11bb76a93f6a6af3429e41c887b0109
└── 🍒 sha256:4f3c43079eed9885ebe77b787604e98ebf0f8020f61002efb70f5fda091ed27b
└── 🔐 Signatures for an image tag: cgr.dev/chainguard/go:sha256-6be282d9e6dc....sig
└── 🍒 sha256:f0e210a4dfafc1188c7a9ab97b3e01082a7a9ebbbb98174292bedc8ddca71b61Choose one of the following two transports.
Use cosign copy, which carries the image, its signatures, and its attestations together. For a multi-architecture image it also copies the per-platform signatures, which matters if the air-gapped environment pulls a single architecture:
cosign copy cgr.dev/chainguard/go:latest registry.internal/chainguard/go:latestConfirm the signature arrived before you rely on the mirror:
cosign tree registry.internal/chainguard/go:latestNote: General-purpose copy tools don’t carry Cosign signatures for Chainguard Containers. Chainguard stores signatures and attestations under
.sigand.atttags rather than in the OCI referrers graph, so tools that walk referrers report success and copy the image bytes alone. Usecosign copy, or copy the.sigand.atttags explicitly.
If the air-gapped environment has no registry, cosign save writes the image and its attached artifacts to an OCI layout on disk:
cosign save cgr.dev/chainguard/go:latest --dir ./transferMove the transfer directory and trusted_root.json across the air gap together. On the other side you can verify the directory in place, or load it into a registry with cosign load.
Pass the trust root with --trusted-root. Everything else matches a connected verification.
cosign verify \
--trusted-root ./trusted_root.json \
--certificate-oidc-issuer=https://token.actions.githubusercontent.com \
--certificate-identity=https://github.com/chainguard-images/images/.github/workflows/release.yaml@refs/heads/main \
registry.internal/chainguard/go:latestA successful run reports the offline transparency-log check:
Verification for registry.internal/chainguard/go:latest --
The following checks were performed on each of these signatures:
- The cosign claims were validated
- Existence of the claims in the transparency log was verified offline
- The code-signing certificate was verified using trusted certificate authority certificatesNote: The verified output still reports a
docker-referenceofcgr.dev/chainguard/go, even though you verified a mirrored copy. This is expected. The signature covers the image digest, not the registry it’s served from.
Point Cosign at the directory with --local-image:
cosign verify \
--local-image \
--trusted-root ./trusted_root.json \
--certificate-oidc-issuer=https://token.actions.githubusercontent.com \
--certificate-identity=https://github.com/chainguard-images/images/.github/workflows/release.yaml@refs/heads/main \
./transfercosign verify-attestation works the same way. This example checks the SPDX SBOM attestation:
cosign verify-attestation \
--trusted-root ./trusted_root.json \
--type https://spdx.dev/Document \
--certificate-oidc-issuer=https://token.actions.githubusercontent.com \
--certificate-identity=https://github.com/chainguard-images/images/.github/workflows/release.yaml@refs/heads/main \
registry.internal/chainguard/go:latestImages in your organization’s registry are signed by your organization’s image-syncer and custom-image-builder identities rather than by Chainguard’s public signing identity. Resolve those identifiers on the connected machine, because chainctl needs to reach the Chainguard control plane:
PARENT=your-organization
IMAGE_SYNCER=$(chainctl iam account-associations describe $PARENT -o json | jq -r '.[].chainguard.service_bindings.CATALOG_SYNCER')
CUSTOM_IMAGE_BUILDER=$(chainctl iam account-associations describe $PARENT -o json | jq -r '.[].chainguard.service_bindings.APKO_BUILDER')Record both values and carry them across the air gap with the trust root. Verification then uses them in place of the public identity:
cosign verify \
--trusted-root ./trusted_root.json \
--certificate-oidc-issuer=https://issuer.enforce.dev \
--certificate-identity-regexp="https://issuer.enforce.dev/(${IMAGE_SYNCER}|${CUSTOM_IMAGE_BUILDER})" \
registry.internal/chainguard/go:latestFor more on these identities, see Verifying Chainguard Containers and metadata signatures with Cosign.
Some registry layouts keep signatures apart from the images they sign. Set COSIGN_REPOSITORY to tell Cosign where to look:
export COSIGN_REPOSITORY=registry.internal/signaturesWithout it, Cosign looks beside the image and reports no signatures found.
Sigstore rotates the keys and certificate authorities in the trust root from time to time. A trust root you exported months ago can fail to verify a signature made after a rotation.
Re-export trusted_root.json from a connected machine on the same schedule you use to refresh images, and move the two together. Treat the trust root as part of every transfer rather than as one-time setup.
Some organizations verify Chainguard signatures on the connected side, then re-sign the images with an internally managed key before import. Inside the air gap, admission policies check only that internal key.
That’s a decision about trust domains and key custody, not a technical requirement. The verification described earlier works with no outbound connectivity, so you can also keep verifying Chainguard’s signatures directly. If you do re-sign, see How to sign a container with Cosign.
| Message | Cause |
|---|---|
tuf: failed to download 13.root.json | Cosign tried to refresh TUF metadata over the network. Pass --trusted-root. |
no signatures found | The signature wasn’t copied with the image, or it lives in another repository. Check with cosign tree and set COSIGN_REPOSITORY if needed. |
none of the expected identities matched what was in the certificate | The --certificate-identity or --certificate-oidc-issuer value doesn’t match the signer. The error lists the subject that was found. |
Flag --offline has been deprecated | Remove --offline. Supplying --trusted-root covers this case. |
if any flags in the group [local-image new-bundle-format] are set none of the others can be | Remove --new-bundle-format. Older guides pair it with --local-image, which Cosign v3.0.3 rejects. |
For background on how Cosign verification works, read An introduction to Cosign. To verify Chainguard Containers in a connected environment, see Verifying Chainguard Containers and metadata signatures with Cosign. For mirroring Chainguard Containers into an internal registry, see the pull-through guides.
Last updated: 2026-09-08 00:00