<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Supplychain · XB Field Notes</title><link>https://www.xbcatm.com/tags/supplychain/</link><description>Illustrated field notes on HTTP, DNS, TLS, Caddy, SSH, releases, and recovery.</description><generator>Hugo</generator><language>en-us</language><atom:link href="https://www.xbcatm.com/tags/supplychain/index.xml" rel="self" type="application/rss+xml"/><item><title>Signed images prove who built them</title><link>https://www.xbcatm.com/posts/signed-images-prove-who-built-them/</link><pubDate>Thu, 21 Oct 2027 00:00:00 +0000</pubDate><guid>https://www.xbcatm.com/posts/signed-images-prove-who-built-them/</guid><description>A registry digest plus a signature ties an artifact to the key that signed it. Verification then asks who produced the bytes before running them.</description><content:encoded>&lt;p&gt;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.&lt;/p&gt;
&lt;p&gt;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.&lt;/p&gt;
&lt;h2 id="digests-are-the-identity-tags-are-pointers"&gt;Digests are the identity, tags are pointers&lt;/h2&gt;
&lt;p&gt;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, &lt;code&gt;openssl dgst -verify&lt;/code&gt; reports success; flipping a single byte of the artifact changes the digest, and the same verification returns failure.&lt;/p&gt;
&lt;p&gt;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.&lt;/p&gt;
&lt;h2 id="signing-a-tag-binds-nothing"&gt;Signing a tag binds nothing&lt;/h2&gt;
&lt;p&gt;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.&lt;/p&gt;
&lt;p&gt;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 &amp;ldquo;this identity built it&amp;rdquo;. Distributing and rotating that anchor is the security boundary, and it lives outside the signature entirely.&lt;/p&gt;
&lt;h2 id="verification-is-the-control-not-the-signature"&gt;Verification is the control, not the signature&lt;/h2&gt;
&lt;p&gt;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.&lt;/p&gt;
&lt;p&gt;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.&lt;/p&gt;
&lt;h2 id="when-it-breaks"&gt;When it breaks&lt;/h2&gt;
&lt;p&gt;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.&lt;/p&gt;
&lt;p&gt;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.&lt;/p&gt;
&lt;h2 id="check-it-quickly"&gt;Check it quickly&lt;/h2&gt;
&lt;p&gt;Verify the digest, then confirm the tag still resolves to it:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;cosign verify --key cosign.pub registry.example.com/app@sha256:9ab5a134...
cosign tree registry.example.com/app@sha256:9ab5a134...
skopeo inspect --format &amp;#39;{{.Digest}}&amp;#39; docker://registry.example.com/app:latest
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;code&gt;cosign verify&lt;/code&gt; must print the verified identity and exit 0, and &lt;code&gt;cosign tree&lt;/code&gt; should list the signatures and attestations attached to that digest. &lt;code&gt;skopeo inspect&lt;/code&gt; must print the same digest you verified. A wrong result is verify failing with no matching signatures, or verify succeeding while &lt;code&gt;skopeo&lt;/code&gt; reports a different digest — meaning the tag now points at bytes that were never signed.&lt;/p&gt;
&lt;h2 id="sources"&gt;Sources&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://docs.sigstore.dev/cosign/verifying/verify/"&gt;Sigstore: verifying signatures with cosign&lt;/a&gt;, key-based and keyless verification over a digest, checked 2026-09-16.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://slsa.dev/spec/v1.0/"&gt;SLSA v1.0 specification&lt;/a&gt;, provenance, digests and what a signature is claimed to attest, checked 2026-09-16.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.sigstore.dev/policy-controller/overview/"&gt;Sigstore policy-controller&lt;/a&gt;, enforcing signature and identity policy at admission, checked 2026-09-16.&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item></channel></rss>