A container image reference is a mutable pointer; a digest is immutable. Signing ties a digest to a key, so a verifier can ask who produced these exact bytes before anything runs them. That is the question registries and manifests cannot answer, because they tell you where bytes came from, not who made them.

The tension is that a signature is only a control when something verifies it, and verification is only meaningful when the trust root is managed. Signing is a few lines in a pipeline; deciding which keys count, and what happens when one is retired, is the actual engineering.

Digests are the identity, tags are pointers

The digest is a hash over the exact manifest bytes, so any change to the image produces a different digest. Signing that digest binds the signature to content rather than to a name. With an EC P-256 keypair, a real SHA-256 digest and a cosign-shaped payload, openssl dgst -verify reports success; flipping a single byte of the artifact changes the digest, and the same verification returns failure.

Tags behave differently. Three tags pointing at the signed digest all verify, because the tag was never part of the signed payload. That is the design: tags are pointers to digests, and re-pointing one is a normal registry operation. Move a tag to a digest that was never signed and verification against the resolved digest fails — but only if the verifier resolves the tag first.

Signing a tag binds nothing

Sign the string app:v2 instead of a digest and verification succeeds forever, because the string never changes. Point that tag at a completely different image and the signature still verifies: it is a true statement about a string that says nothing about bytes. This is why a signature must be over the digest, and why verification has to resolve the tag to a digest before checking anything.

The trust root is the other half. A signature proves that whoever holds the private key produced it; the public key, or a Fulcio-issued certificate paired with a Rekor log entry, is what turns that into “this identity built it”. Distributing and rotating that anchor is the security boundary, and it lives outside the signature entirely.

Verification is the control, not the signature

A pipeline that signs and then deploys without verifying has bought decoration. The measured contrast is blunt: a tampered artifact is blocked when the deploy step verifies, and the same artifact deploys cleanly when the step only records provenance — with the provenance record now naming the tampered digest. Enforcement belongs at admission, or in a gate before deploy, so an unsigned image never reaches a node.

Revocation is the part with no answer inside the signature. A signature carries no expiry and no revocation list: it verifies under its key forever. Rotating keys does not revoke anything — the old signature is rejected under the new key and still accepted under the old one — so anyone holding a retired key keeps producing artifacts that pass. The fix is a trust root with short-lived certificates and a transparency log, plus policy that pins identity rather than merely checking that some signature exists.

When it breaks

The classic failure is an unsigned image that a policy engine never checked. The pipeline signs, the deploy step pulls by tag and runs, and the gap stays invisible until someone pushes different bytes under the same tag. The audit trail then claims provenance for an image nobody verified, which is worse than having no provenance at all.

The second failure is a signature that verifies but was made by a key you no longer trust. Because there is no revocation inside the signature, a leaked or retired key keeps signing acceptable artifacts, and the blast radius is every artifact that key ever signed, retroactively. Pin the signing identity in policy, keep certificates short-lived, and record signatures in a transparency log so an unexpected key appears as a log entry rather than as a silent pass.

Check it quickly

Verify the digest, then confirm the tag still resolves to it:

cosign verify --key cosign.pub registry.example.com/app@sha256:9ab5a134...
cosign tree registry.example.com/app@sha256:9ab5a134...
skopeo inspect --format '{{.Digest}}' docker://registry.example.com/app:latest

cosign verify must print the verified identity and exit 0, and cosign tree should list the signatures and attestations attached to that digest. skopeo inspect must print the same digest you verified. A wrong result is verify failing with no matching signatures, or verify succeeding while skopeo reports a different digest — meaning the tag now points at bytes that were never signed.

Sources