Skip to content

feat(evidence): aicr evidence verify (directory input)#879

Merged
mchmarny merged 10 commits into
NVIDIA:mainfrom
njhensley:feat/evidence-verify-dir
May 14, 2026
Merged

feat(evidence): aicr evidence verify (directory input)#879
mchmarny merged 10 commits into
NVIDIA:mainfrom
njhensley:feat/evidence-verify-dir

Conversation

@njhensley
Copy link
Copy Markdown
Member

@njhensley njhensley commented May 14, 2026

Summary

First slice of aicr evidence verify (ADR-007 PR-B): offline verification of an unpacked bundle directory produced by aicr validate --emit-attestation. Ships the receiving end of the bundle integrity chain plus a Markdown/JSON report.

Motivation / Context

ADR-007 ships a recipe-evidence v1 bundle so contributors on hardware AICR maintainers can't reach can hand reviewers a signed, verifiable artifact. The emitter landed in a prior PR; this is the start of the consumer. Shipped as a vertical slice so each PR adds one capability an end user can invoke independently. This PR gives contributors directory self-debug — aicr evidence verify ./out/summary-bundle after aicr validate --emit-attestation ./out.

Signature verification, inline constraint replay, OCI pull, and pointer files follow in three subsequent PRs.

Fixes: N/A (#753 closes with PR 4 of this series)
Related: #753, ADR-007

Type of Change

  • New feature (non-breaking change that adds functionality)

Component(s) Affected

  • CLI (cmd/aicr, pkg/cli)
  • Other: pkg/evidence/verifier (new package)

Implementation Notes

Four steps run: materialize-bundlepredicate-parsemanifest-hash-checkrender-summary.

Trust property shipped — the receiving end of the bundle integrity chain. Every file in the bundle is sha256-verified against manifest.json, and sha256(manifest.json) is verified against predicate.Manifest.Digest read from the bundled in-toto Statement (statement.intoto.json).

The Statement file is unsigned at this slice — both sides of the manifest-digest comparison come from the same untrusted bundle directory, so a tamperer who rewrites manifest.json and the unsigned Statement in lockstep passes verification. The check catches accidental corruption and lazy tampering, which is the right contract for the contributor-self-debug case PR 1 targets.

How PR 3 closes the chain. When signature verification lands, predicate extraction switches to the DSSE payload inside attestation.intoto.jsonl (a Sigstore Bundle with a Fulcio-issued cert and Rekor inclusion proof). At that point, predicate.Manifest.Digest is cryptographically anchored to an OIDC identity at sign-time, and PR 1's manifest-hash check extends that anchor to every bundled file. PR 4 (OCI pull) adds redundancy via content-addressed registry storage.

ADR-007 §"Verifier steps" enumerates twelve steps. This implementation runs four, collapsing the redundant per-field digest checks the manifest-hash chain already covers and folding display of signed fields (fingerprint, phase counts, BOM) into the renderer. The package doc records the mapping.

Non-directory inputs (pointer-like .yaml, OCI refs) are rejected with a clear "not yet supported" message rather than silently failing — keeps the contract visible in the error surface.

Output flags. Standard project pattern: --output/-o selects the destination (stdout when empty, a file otherwise); --format/-t selects text (Markdown) or json. Format applies regardless of destination; no double-rendering.

Exit codes. OS exit codes are 0 (valid) and 2 (anything else, including recorded phase failures), per pkg/errors/exitcode.go's code-to-OS mapping. The 0/1/2 distinction from ADR-007 is preserved in the structured output — VerifyResult.Exit field in JSON / Markdown carries 1 for the informational phase-failure case so JSON consumers can branch. Shell consumers can branch via jq '.exit'.

Testing

go test ./pkg/evidence/verifier/... ./pkg/cli/...
golangci-lint -c .golangci.yaml run ./pkg/evidence/verifier/... ./pkg/cli/...
./tools/check-docs-mdx
./tools/check-docs-sidebar

All green locally (lint 0 issues; tests pass).

Tests cover: happy-path verify of a synthesized bundle, tampered-file → exit 2, stray-file → exit 2, manifest-digest mismatch → exit 2, manifest schemaVersion rejection, traversal-entry rejection, ctx cancellation in inventory, predicate-parse failure attributed to its own step, auto-detect rejects unsupported input forms with clear errors, -o writes to file with empty stdout, -o -t json writes JSON to file, -t json emits the steps array on stdout.

Risk Assessment

  • Low — additive new subcommand under aicr evidence; no existing behavior changes; fully covered by unit tests; the trust property (self-consistency on an unsigned Statement) is partial-by-design and explicitly described in the package doc.

Rollout notes: N/A. Users who don't invoke aicr evidence verify see no change.

Checklist

  • Tests pass locally
  • Linter passes
  • I did not skip/disable tests to make CI green
  • I added tests for new functionality
  • I updated docs (docs/user/cli-reference.md)
  • Changes follow existing patterns
  • Commits are cryptographically signed

njhensley added 2 commits May 13, 2026 18:17
Adds the first slice of the evidence verifier: an offline
`aicr evidence verify <dir>` command that recomputes every file's
sha256 against the bundle's manifest.json (bound to the predicate's
manifest.digest) and renders a Markdown or JSON summary of the signed
predicate (recipe, fingerprint, phase counts, BOM info).

Directory input only; signature verification, inline constraint
replay, OCI pull, and pointer-file support ship in follow-up PRs.

Exit codes:
  0  bundle valid; every check passed
  1  bundle valid; recorded validator results show failures
  2  bundle invalid (manifest hash mismatch or predicate malformed)
Tighten the verifier slice now that the surrounding scaffolding is in:

- Drop the unused verifier.OutputFormat type and VerifyOptions field;
  the CLI already validates the format string and dispatches on it.
- Reuse attestation.HashFileSHA256 in inventory.go instead of an
  inlined sha256/io.Copy block.
- Map bundle-invalid to ErrCodeInvalidRequest (was ErrCodeUnauthorized);
  the failure is structural, not an auth denial.
- Rewrite forward-looking "first slice / PR 3 / PR 4 / ADR-007 §..."
  references in package, type, and function comments to describe the
  current behavior instead of an ordering that will rot.
- Collapse the CLI exit-code table to two rows (0 and 2); the library
  API still distinguishes 1 (recorded phase failures) via
  VerifyResult.Exit, called out in the help and docs.
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 14, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

This pull request introduces a new aicr evidence verify CLI subcommand and a verifier package that performs offline verification of recipe-evidence v1 bundle directories: input detection, bundle materialization (summary-bundle support), manifest inventory/hash checks (SHA-256 and optional size), in-toto predicate parsing (fingerprint and phase counts), per-step results with distinct exit codes, deterministic JSON and PR-style Markdown rendering, CLI wiring and docs, and accompanying unit/CLI tests.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • NVIDIA/aicr#873: Produces the summary-bundle layout and unsigned statement/manifest artifacts consumed and verified by this PR.

Suggested labels

area/tests, enhancement

Suggested reviewers

  • mchmarny
🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat(evidence): aicr evidence verify (directory input)' clearly summarizes the main change: adding a new CLI subcommand for evidence verification that supports directory input, which is the primary focus of this PR.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Comment @coderabbitai help to get the list of available commands and usage tips.

The new `aicr evidence verify` command added a third `Name: "format"`
StringFlag declaration, pushing goconst over its 3-occurrence threshold
and failing CI. Hoist the literal into the existing pkg/cli/consts.go
flag-name block and update the three flagged sites (and the two
cmd.String("format") reads in the same files).
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/user/cli-reference.md`:
- Around line 1899-1924: Add blank lines before and after the fenced code block
under "Synopsis" (the ```shell block), before the table that follows
"**Flags:**", and before the fenced code block under "**Examples:**" so that
each fence/table is separated by at least one empty line; update the markdown
around the "Synopsis" code fence, the "**Flags:**" table, and the
"**Examples:**" code fence to satisfy MD031/MD058 spacing rules.

In `@pkg/evidence/verifier/verify.go`:
- Around line 79-87: Change CheckInventory to accept a context.Context (e.g.,
func CheckInventory(ctx context.Context, mat Manifest) (...)) and update its
internal loops (the hashing loop over manifest entries) to regularly check
ctx.Done() and return early with ctx.Err() (or a wrapped error) when cancelled;
update every caller (including the call here that currently invokes
CheckInventory(mat)) to pass the ctx from the surrounding verification function
(the same ctx used for other steps) so cancellation propagates, and ensure the
returned error case in verify.go still records the failure via record(r,
stepInventory, ...) and sets r.Exit appropriately.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Enterprise

Run ID: e924e9eb-23eb-4039-9af9-3a4d266af140

📥 Commits

Reviewing files that changed from the base of the PR and between e83b2ea and 15b1116.

📒 Files selected for processing (16)
  • docs/user/cli-reference.md
  • pkg/cli/evidence.go
  • pkg/cli/evidence_verify.go
  • pkg/cli/evidence_verify_test.go
  • pkg/cli/root.go
  • pkg/evidence/verifier/doc.go
  • pkg/evidence/verifier/fetch.go
  • pkg/evidence/verifier/fetch_test.go
  • pkg/evidence/verifier/input.go
  • pkg/evidence/verifier/input_test.go
  • pkg/evidence/verifier/inventory.go
  • pkg/evidence/verifier/report.go
  • pkg/evidence/verifier/testbundle_test.go
  • pkg/evidence/verifier/types.go
  • pkg/evidence/verifier/verify.go
  • pkg/evidence/verifier/verify_test.go

Comment thread docs/user/cli-reference.md
Comment thread pkg/evidence/verifier/verify.go Outdated
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@pkg/cli/evidence_verify.go`:
- Around line 89-99: Replace the bare error returns in the evidence verification
flow with errors.PropagateOrWrap to preserve error codes: where
verifier.Verify(ctx, opts) currently returns err, wrap/propagate that error
before returning; where writeVerifyOutput(cmd.Root().Writer, format, result) is
called, use errors.PropagateOrWrap on its returned error; and where
verifier.WriteMarkdown(opts.MarkdownPath, result) returns an error, also
wrap/propagate it. Use the errors.PropagateOrWrap function so existing coded
errors are preserved and otherwise supply a fallback code when wrapping; update
the return statements in the Verify flow and in writeVerifyOutput (and any bare
returns in that function) to use errors.PropagateOrWrap.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Enterprise

Run ID: 414c35f7-f409-4de8-b10f-29804a5b6da2

📥 Commits

Reviewing files that changed from the base of the PR and between 15b1116 and d42e2ad.

📒 Files selected for processing (4)
  • pkg/cli/bundle_verify.go
  • pkg/cli/consts.go
  • pkg/cli/evidence_verify.go
  • pkg/cli/root.go

Comment thread pkg/cli/evidence_verify.go Outdated
Plumb the verify context into CheckInventory and findExtras so a large
bundle or a hostile manifest with many entries can be canceled
mid-walk. Checks ctx.Err() between files in the manifest loop and
between filesystem entries in findExtras.
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@pkg/evidence/verifier/inventory.go`:
- Line 106: The direct return of attestation.HashFileSHA256(full) must capture
its error and wrap it with pkg/errors.PropagateOrWrap to add context; replace
the single return with calling attestation.HashFileSHA256(full), check the
returned err, and return errors.PropagateOrWrap(err, errors.EINTERNAL, "hashing
file "+full) (or another appropriate exported error code constant) so any
existing structured error is preserved while adding context; reference
attestation.HashFileSHA256 and errors.PropagateOrWrap when locating the change.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Enterprise

Run ID: 033bcfe7-33f2-4641-a6b1-589e5c01581a

📥 Commits

Reviewing files that changed from the base of the PR and between d42e2ad and e8b1914.

📒 Files selected for processing (3)
  • pkg/evidence/verifier/inventory.go
  • pkg/evidence/verifier/verify.go
  • pkg/evidence/verifier/verify_test.go

Comment thread pkg/evidence/verifier/inventory.go Outdated
CheckInventory's per-file hash step was returning the raw error from
attestation.HashFileSHA256, which loses the bundle-relative path in the
reported mismatch row. Wrap via errors.PropagateOrWrap so any inner
StructuredError code is preserved while the message gains the offending
file name.
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@pkg/evidence/verifier/inventory.go`:
- Around line 91-92: The hashFile function must reject non-local relative paths
from manifest.json before joining into bundleDir: call filepath.IsLocal(rel) at
the top of hashFile (or equivalent validation) and return a clear error if it
returns false, preventing traversal like "../../../etc/passwd"; then proceed to
compute full := filepath.Join(bundleDir, filepath.FromSlash(rel)) only for
validated local paths so the code path in hashFile uses a safe rel value.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Enterprise

Run ID: 482d676c-068a-44a9-9996-e03a14cf402c

📥 Commits

Reviewing files that changed from the base of the PR and between e8b1914 and 61a1243.

📒 Files selected for processing (1)
  • pkg/evidence/verifier/inventory.go

Comment thread pkg/evidence/verifier/inventory.go Outdated
A hostile bundle could include a manifest.json with path entries like
"../../../etc/passwd"; the verifier would happily stat and hash files
outside the bundle directory before failing the digest check. Gate
hashFile on filepath.IsLocal so traversal entries are refused before
any filesystem access.
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@pkg/evidence/verifier/inventory.go`:
- Around line 160-162: The error wrapping of walkErr in the WalkDir handling
should preserve context cancellation semantics: inspect walkErr (or ctx.Err())
before returning in the block that currently does "return nil,
errors.Wrap(errors.ErrCodeInternal, ...)" and if the cause is context.Canceled
or context.DeadlineExceeded return/wrap with errors.ErrCodeUnavailable instead
of errors.ErrCodeInternal; update the code path around the WalkDir return
(referencing walkErr and the WalkDir call in inventory.go) so cancellation
errors are translated the same way as in CheckInventory (lines handling
ctx.Err()).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Enterprise

Run ID: d556f212-27be-4b3c-b0fe-2da136e44358

📥 Commits

Reviewing files that changed from the base of the PR and between 61a1243 and e10641c.

📒 Files selected for processing (2)
  • pkg/evidence/verifier/inventory.go
  • pkg/evidence/verifier/verify_test.go

Comment thread pkg/evidence/verifier/inventory.go
Copy link
Copy Markdown
Member

@mchmarny mchmarny left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice clean structure — three-step pipeline, good separation between library and CLI, traversal guard via filepath.IsLocal, sensible test coverage including the stray-file and traversal cases. CI is green.

One blocker on the trust property this slice is meant to ship: the manifest-digest binding from manifest.jsonpredicate.Manifest.Digest is never actually checked. The inventory step recomputes per-file hashes against manifest.json, but manifest.json itself is trusted as-is, so if someone wanted to fake a bundle file and also rewrote manifest.json to match will passe verification with Exit=0. Until signature verification lands in a follow-up, this single digest comparison is what closes the loop — see the inline comment on inventory.go for the suggested fix and a regression test.

The other inline comments are smaller: a step-naming clarity issue (predicate parse failures surface as Step 2 / manifest-hash-check), a wording mismatch around exit-code branching, and a few nits (dead MarkdownPath field, ignored ctx, unvalidated SchemaVersion).

Blocking on the manifest-digest check; everything else can be follow-up.

Comment thread pkg/evidence/verifier/inventory.go
Comment thread pkg/cli/evidence_verify.go
Comment thread pkg/evidence/verifier/verify.go
Comment thread pkg/evidence/verifier/types.go Outdated
Comment thread pkg/evidence/verifier/fetch.go Outdated
Comment thread pkg/evidence/verifier/inventory.go
njhensley added 2 commits May 14, 2026 08:36
Tighten the integrity chain and the CLI surface in one pass.

Verifier (predicate → manifest → files binding):
- Promote predicate parsing to its own step (was folded into inventory)
  so a malformed Statement reports as predicate-parse failure, not as
  manifest-hash-check failure.
- CheckInventory now requires expectedManifestDigest (the predicate's
  Manifest.Digest) and verifies sha256(manifest.json) matches it
  before walking entries. Without this gate, a tampered bundle could
  rewrite manifest.json to match its own contents and pass file-by-file
  checks; this binds the unsigned manifest to the (eventually signed)
  predicate.
- Reject unsupported manifest schemaVersion (verifier accepts 1.0.x).
- MaterializeBundle now honors ctx cancellation for symmetry with the
  rest of the pipeline.

CLI (unified output handling):
- Replace --output-markdown with the standard --output/-o flag, and
  add the -t alias on --format. -o writes whichever format -t selects;
  empty -o keeps stdout behavior. No more double-rendering when a path
  is supplied.
- Remove verifier.WriteMarkdown — superseded by the CLI's unified
  output path; the library returns the result, callers decide where
  the bytes go.

Docs and tests updated for the new flag shape, the four-step pipeline,
and the new failure modes (tampered manifest digest, unsupported
schema, predicate-parse failure surfaced on its own step).
findExtras returns context.Canceled / DeadlineExceeded from the
filepath.WalkDir callback, but the post-walk wrap was unconditionally
labeling them ErrCodeInternal. That makes a normal cancellation look
like a verifier bug. Detect the context-error case and wrap as
ErrCodeUnavailable, matching the cancellation handling in the
manifest entry loop above.
@mchmarny mchmarny enabled auto-merge (squash) May 14, 2026 15:46
@mchmarny mchmarny merged commit 922cd13 into NVIDIA:main May 14, 2026
34 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants