feat(image): verify OCI image contracts - #16
Merged
Conversation
Replace the last deep shell verifier in go-oci-build.yml (OB-18 to OB-21, 112 lines of jq, tar and sha256sum) with `release-cli image verify`. The index digest is SHA-256 over the exact index.json bytes, never over re-marshaled JSON. The layer is streamed: the entrypoint's tar entry must be a regular file whose low twelve mode bits are exactly 0755, owned 0/0, with content byte-identical to the canonical staged binary. Review caught that comparing only the permission bits let a setuid entrypoint pass while the image runs as user 65532; the raw mode is now compared. The CLI writes image-digest.txt, which the publisher workflow reads. Verified against the real apko 1.2.37 layout: the digest matches what the removed shell verifier recorded, and a rebuilt layer with one flipped byte fails.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
PR 9 of the eleven-PR
release-cliprogram (.journal/002/PLAN.md), on top of PR 8 (e235a28). Replaces the last deep shell verifier — inventory items OB-18 through OB-21, the 112-linejq/tar/sha256sumstepVerify authoritative OCI image— with exact-byte Go parsing. PR 8 deliberately kept that step as an independent migration oracle; this PR retires it.Scope
internal/stage/image/layout.go—ReadLayout:oci-layoutpresence, the exactindex.jsonbytes and their SHA-256 digest, per-platform manifest/config/layer descriptors, and blob existence at the declared size. Deliberately independent ofinternal/stage/puboci's publish-time reader: the two verify different things at different trust boundaries, and independence is the point of this slice.internal/stage/image/verify.go—VerifyLayout,VerifySBOMs,CanonicalDigests. Index schema, media type, exactly twolinuxplatforms, the sixorg.opencontainers.image.*annotations with nonempty description/licenses/title and revision/source/version matching the expected values, manifest annotations and config labels equal to the index annotations, exactly one layer, config architecture and OS, entrypoint/usr/bin/<binary>, user65532, the layer'susr/bin/<binary>entry as a regular file with low-twelve mode bits exactly0755and ownership0:0whose streamed content is byte-identical to the canonical staged binary, and an SPDXAPPLICATIONpackage at<version>-r0per architecture.internal/cli/image.go—image verify --output --work --binary --version, readingGITHUB_SHA,GITHUB_SERVER_URL, andGITHUB_REPOSITORYfor the expected revision and source. It writes<output>/image-digest.txt, whichpublish-oci-image.ymlreads, only after both verifiers succeed.internal/cli/image_test.gohad crossed the 1,000-line cap in PR 8 and is split intoimage_test.go,image_build_test.go, andimage_verify_test.gowith every assertion carried over unchanged..github/workflows/go-oci-build.yml— theverifystep keeps its id and now runs the CLI, writingimage-digestfrom the envelope withjq -er '.result.index_digest'.Upload authoritative OCI imageand every input, output, and permission are byte-identical.Verification
Against the real apko 1.2.37 two-platform layout that PR 8's CLI built on a laptop:
image verifypasses and reportsindex_digest sha256:897536744f2c4e90b88b284df4fc647eafb12109135d07fa32d2fc3ca033a2cb, identical to the digest the removed shell verifier had written intoimage-digest.txtand to an independentshasum -a 256 layout/index.json. Per-platform binary digests equal the canonical projection digests from PR 8.usr/bin/release-cli, with manifest and index descriptors updated so every digest and size is internally consistent, fails withlinux/amd64 image binary has digest ... expected ...— the tar content is genuinely read.04755with identical content and ownership fails withusr/bin/release-cli has mode 04755, want 0755, and noimage-digest.txtis written.GITHUB_SHA, wrong--version, a tampered canonical staged binary, a missingsboms/, and a wrong--binaryeach fail with a precise diagnostic; appending a newline toindex.jsonchanges the reported digest; success without--jsonwrites zero bytes to stdout; every exit-2 configuration failure happens before any file is opened.mise exec -- moon run root:checkgreen.Review
Two rounds. Round 1 found one blocking dropped check:
checkBinaryHeadercomparedMode().Perm(), which masks setuid/setgid/sticky, so a04755entrypoint passed while the image runs as user65532— a privilege-escalation primitive the removed shell script caught by string-comparing-rwxr-xr-x. Fixed by comparing the raw tar mode. Round 2 re-verified by mutation (all mode variants rejected,Perm()regression caught) and found that the follow-upIsRegular-based type check rested on a false premise:archive/tarnormalizes the historic NUL typeflag beforeNext()returns, so that path was dead and the accepted set had silently widened toTypeContand unknown vendor flags. Now a strictTypeflag != tar.TypeRegwithMode&0o7777tolerance for producers that write file-type bits — security fix intact, dead test removed. Conformance audited everyAGENTS.mdrule; its three blocking findings (atar.TypeRegAclaim, a staledoc.go, and--binaryvalidation happening after the roots opened) are all fixed.Notes
pubocifor publish,imagefor build-time verification) overlap in roughly 60 lines of index parsing. Kept separate deliberately; the consolidation trigger is a third consumer needing on-disk layout parsing.