Skip to content

feat(image): verify OCI image contracts - #16

Merged
jmgilman merged 1 commit into
mainfrom
feat/release-cli-image-verify
Aug 20, 2026
Merged

feat(image): verify OCI image contracts#16
jmgilman merged 1 commit into
mainfrom
feat/release-cli-image-verify

Conversation

@jmgilman

Copy link
Copy Markdown
Contributor

PR 9 of the eleven-PR release-cli program (.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-line jq/tar/sha256sum step Verify 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.goReadLayout: oci-layout presence, the exact index.json bytes and their SHA-256 digest, per-platform manifest/config/layer descriptors, and blob existence at the declared size. Deliberately independent of internal/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.goVerifyLayout, VerifySBOMs, CanonicalDigests. Index schema, media type, exactly two linux platforms, the six org.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>, user 65532, the layer's usr/bin/<binary> entry as a regular file with low-twelve mode bits exactly 0755 and ownership 0:0 whose streamed content is byte-identical to the canonical staged binary, and an SPDX APPLICATION package at <version>-r0 per architecture.
  • internal/cli/image.goimage verify --output --work --binary --version, reading GITHUB_SHA, GITHUB_SERVER_URL, and GITHUB_REPOSITORY for the expected revision and source. It writes <output>/image-digest.txt, which publish-oci-image.yml reads, only after both verifiers succeed. internal/cli/image_test.go had crossed the 1,000-line cap in PR 8 and is split into image_test.go, image_build_test.go, and image_verify_test.go with every assertion carried over unchanged.
  • .github/workflows/go-oci-build.yml — the verify step keeps its id and now runs the CLI, writing image-digest from the envelope with jq -er '.result.index_digest'. Upload authoritative OCI image and every input, output, and permission are byte-identical.
  • No new port (budget stays closed at 13), no new module dependency, no publication change.

Verification

Against the real apko 1.2.37 two-platform layout that PR 8's CLI built on a laptop:

  • image verify passes and reports index_digest sha256:897536744f2c4e90b88b284df4fc647eafb12109135d07fa32d2fc3ca033a2cb, identical to the digest the removed shell verifier had written into image-digest.txt and to an independent shasum -a 256 layout/index.json. Per-platform binary digests equal the canonical projection digests from PR 8.
  • A rebuilt amd64 layer with one flipped byte in usr/bin/release-cli, with manifest and index descriptors updated so every digest and size is internally consistent, fails with linux/amd64 image binary has digest ... expected ... — the tar content is genuinely read.
  • A rebuilt layer whose entrypoint is 04755 with identical content and ownership fails with usr/bin/release-cli has mode 04755, want 0755, and no image-digest.txt is written.
  • Wrong GITHUB_SHA, wrong --version, a tampered canonical staged binary, a missing sboms/, and a wrong --binary each fail with a precise diagnostic; appending a newline to index.json changes the reported digest; success without --json writes zero bytes to stdout; every exit-2 configuration failure happens before any file is opened.
  • mise exec -- moon run root:check green.

Review

Two rounds. Round 1 found one blocking dropped check: checkBinaryHeader compared Mode().Perm(), which masks setuid/setgid/sticky, so a 04755 entrypoint passed while the image runs as user 65532 — 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-up IsRegular-based type check rested on a false premise: archive/tar normalizes the historic NUL typeflag before Next() returns, so that path was dead and the accepted set had silently widened to TypeCont and unknown vendor flags. Now a strict Typeflag != tar.TypeReg with Mode&0o7777 tolerance for producers that write file-type bits — security fix intact, dead test removed. Conformance audited every AGENTS.md rule; its three blocking findings (a tar.TypeRegA claim, a stale doc.go, and --binary validation happening after the roots opened) are all fixed.

Notes

  • The two layout readers (puboci for publish, image for 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.
  • Follow-up, unchanged: mockery's testify template emits no Godoc for generated expecter types.

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.
@jmgilman
jmgilman merged commit 8a5e0a7 into main Aug 20, 2026
2 checks passed
@jmgilman
jmgilman deleted the feat/release-cli-image-verify branch August 20, 2026 02:26
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