Skip to content

Verify OCI blob content against its descriptor - #688

Open
Philip Lombardi (plombardi89) wants to merge 1 commit into
mainfrom
oci-blob-digest-verification
Open

Verify OCI blob content against its descriptor#688
Philip Lombardi (plombardi89) wants to merge 1 commit into
mainfrom
oci-blob-digest-verification

Conversation

@plombardi89

Copy link
Copy Markdown
Collaborator

Blobs fetched from an OCI registry were never checked against their descriptor
digest.

oras-go's Repository.Fetch calls verifyContentDigest, which compares only
the Docker-Content-Digest response header against the expected digest, and
returns success when the registry omits that header:

digestStr := resp.Header.Get(headerDockerContentDigest)
if len(digestStr) == 0 {
    return nil          // no header, no verification
}

The bytes themselves are never hashed. Content of the declared size but the
wrong bytes was accepted, and a registry that omits the header got no integrity
check at all.

Impact

This affects every oci:// artifact source reaching
pkg/agent/internal/ociartifact.Open, including offline bootstrap bundles
resolved through bootstrapartifacts. Those are binaries the agent installs
into the node root filesystem, so the content is executed.

Some callers layer their own protection: DownloadWithSHA256Verification hashes
the stream, and Kubernetes binaries, CoreDNS and container image archives fetch
a sibling .sha256. Callers using Open or ReadAll directly, and the
components fetched without a checksum sidecar, had none.

Fix

Wrap the fetched body so the stream is hashed as it is read, and check the
digest once the content is complete, using oras-go's content.VerifyReader.

The failure is reported through Read, not Close. Callers reliably check
read errors and frequently ignore the error from Close, so reporting a
mismatch there would let corrupt content through unnoticed. A digest mismatch
therefore cannot be mistaken for a clean end of stream, which is pinned by a
test.

Tests

Five cases on the wrapper, including the one oras-go does not catch on its
own: content of exactly the declared size with substituted bytes. Also covers
truncated and overlong streams, and asserts that the failure arrives via Read
while Close stays clean.

How this was found

While evaluating whether a checksum sidecar could be dropped for oci://
sources on the grounds that OCI blobs are content-addressed. They are addressed
by digest, but nothing was enforcing it.

Split out of #687 because it is a correctness issue in a shared code path,
independent of the Azure Container Linux work it was found during.

Blobs fetched from an OCI registry were never checked against their descriptor
digest. oras-go's Repository.Fetch only compares the Docker-Content-Digest
response header, and returns success when the registry omits that header
entirely, so the bytes themselves were never hashed. Content of the declared
size but the wrong bytes was accepted.

This affects every oci:// artifact source, including offline bootstrap bundles,
and it silently weakened the integrity story for anything distributed that way.

Wrap the fetched body so the stream is hashed as it is read and the digest is
checked once the content is complete.

The failure is reported through Read rather than Close. Callers reliably check
read errors and frequently ignore the error from Close, so reporting it there
would let corrupt content through unnoticed. A mismatch therefore cannot be
mistaken for a clean end of stream, which is pinned by a test.

Found while evaluating whether a checksum sidecar could be dropped for oci://
sources on the grounds that OCI blobs are content-addressed. They are, but
nothing was enforcing it.
@plombardi89
Philip Lombardi (plombardi89) requested a review from a team August 31, 2026 18:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant