Skip to content

feat(sdk): expose verification and signing on pkg/client/v1 - #2218

Merged
mchmarny merged 10 commits into
mainfrom
feat/sdk-verification-surface
Aug 17, 2026
Merged

feat(sdk): expose verification and signing on pkg/client/v1#2218
mchmarny merged 10 commits into
mainfrom
feat/sdk-verification-surface

Conversation

@mchmarny

@mchmarny mchmarny commented Aug 17, 2026

Copy link
Copy Markdown
Member

Summary

Adds the verification and signing surface to pkg/client/v1, so an integrator can check every artifact AICR produces without importing pkg/bundler/verifier, pkg/evidence/verifier, or pkg/recipe/catalog. Routes six CLI commands and pkg/server/signing.go through it.

Motivation / Context

The facade could produce signed artifacts but not check one. That capped #1149's acceptance bar — "every artifact has retrievable provenance and a documented verification path" — at CLI users only, and left every verification workflow reaching around the stability contract.

One finding reshaped the design: #1567 is already closed, so config.VerifySpec{Policy, Trust} exists and pkg/cli/bundle_verify.go already resolves verify settings from it. The "trust configuration is the hard part" blocker in the issue body is stale — the options shape was a committed schema to mirror, not a design to invent.

Fixes: #2024
Related: #2016, #1149, #1567, #2026

Type of Change

  • Bug fix (non-breaking change that fixes an issue)
  • New feature (non-breaking change that adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Documentation update
  • Refactoring (no functional changes)
  • Build/CI/tooling

Component(s) Affected

  • CLI (cmd/aicr, pkg/cli)
  • API server (cmd/aicrd, pkg/server)
  • Recipe engine / data (pkg/recipe)
  • Bundlers (pkg/bundler, pkg/component/*)
  • Collectors / snapshotter (pkg/collector, pkg/snapshotter)
  • Validator (pkg/validator)
  • Core libraries (pkg/errors, pkg/k8s)
  • Docs/examples (docs/, examples/)
  • Other: SDK facade (pkg/client/v1), API-diff tooling (tools/api-diff*)

Implementation Notes

Surface. Client-bound where the operation binds to the Client's catalog or needs a home for a future WithConfig (#2026): VerifyBundle, VerifyEvidence, VerifyCatalog, RecipeDigest, PublishEvidence, SignCatalog. Package-level for the stateless primitives with no AICRConfig counterpart: VerifyBinaryAttestation, ValidateIdentityPattern, TrustLevels, RenderEvidenceJSON, RenderEvidenceMarkdown.

MinTrustLevel empty means "max", not "no check." This is a deliberate departure from verifier.Policy, where empty skips the trust gate entirely. A caller who never considered the trust floor should get the strict default; lowering it stays an explicit act. Pinned by a test that proves empty agrees with explicit "max" and is not equivalent to a disabled check.

A failed policy is data, not an error. VerifyBundle returns the full Report alongside PolicyFailure so callers can render why a bundle fell short. A non-nil error means verification could not run at all. Same split for VerifyEvidence: an invalid bundle is a verdict on Exit, not an error.

Two new transparent aliases, joining the five recorded under #2019: BundleVerifyReport and EvidenceVerification. The evidence result reaches into the pointer, the in-toto predicate, the per-step records, the signer claims, and the failure cause. Owning translated copies means five more nested types tracking a still-evolving predicate shape, for no consumer benefit — callers read these through := and never name the type. The EvidenceExit* constants are re-exported so verdict branching needs no import.

Timeouts. defaults.VerifyOperationTimeout (5m) bounds the four verify methods, where none existed before. The two signing methods are deliberately uncapped: keyless OIDC can block on a human completing a browser or device-code flow, so a fixed cap would regress an interactive run that works today.

CLI behavior is unchanged. The rewired commands use a new embeddedClient() rather than the config-aware recipeClientFromCmd. None of them defines --data, and routing them through the config-aware constructor would let a spec.recipe.data entry in an unrelated AICRConfig alter — or fail — an artifact verification that never reads the catalog. verify-catalog/sign-catalog target the embedded catalog specifically, which is what ships signed as a release asset.

Deliberately out of scope: aicr evidence sign. Not in the issue's table, and it orchestrates six primitives across two packages including in-place pointer relocation that rewrites files. Wrapping it means either exposing five more primitives or inventing a coarse method around filesystem side effects. Recorded on #2024 per #2025's "record the answer per command" rule.

Tooling. The alias contract gate caught both new aliases on the first run, which is the gate working. tools/api-diff registers them; tools/api-diff_test.sh needed its mock closure output kept in sorted order (the gate compares against an LC_ALL=C-sorted contract without re-sorting) plus the two target packages added to its synthetic fixture repos; tools/api-diff-closure/main_test.go had a hardcoded expectation.

Testing

make qualify   # exit 0

Full gate green, including api-diff (all additions, no incompatible changes since v0.19.0) and the 100+ tools/api-diff_test.sh shell assertions.

New tests: pkg/client/v1/verify_test.go and sign_test.go cover the guard matrix for every entry point, a fixture-backed VerifyBundle happy path built through checksum.GenerateChecksums, the MinTrustLevel default semantics, the policy-failure-is-data contract, digest determinism, and the re-exported verdict constants. TestStability_Verification and TestStability_Signing pin the surface.

SignCatalog's success path is intentionally not unit tested: it needs a real OIDC token, and driving it without one falls through to the interactive browser flow and would hang CI. The goreleaser release hook exercises it on every tagged build.

Coverage deltas:

  • pkg/client/v1: 83.2% → 82.9% (-0.3%)
  • pkg/cli: 74.9% → 74.8% (-0.1%)
  • pkg/server: 82.3% → 82.3% (0.0%)

No new exported function is at 0% coverage.

Risk Assessment

  • Low — Isolated change, well-tested, easy to revert
  • Medium — Touches multiple components or has broader impact
  • High — Breaking change, affects critical paths, or complex rollout

Purely additive on the facade; the risk is the CLI/server rewiring, where six commands and the server's startup self-check now route through new code. Mitigated by keeping the adapters thin and behavior-preserving — every flag/config precedence rule, error code, and output format is unchanged, and the existing CLI and server test suites pass untouched.

Rollout notes: The facade additions need no migration — pkg/bundler/verifier, pkg/evidence/verifier, and pkg/recipe/catalog remain importable; this adds a supported path without removing one. Two behavior changes do need calling out:

  1. ValidateIdentityPattern is stricter. A certificate-identity pattern must now begin with https://github.com/NVIDIA/aicr/ (a leading ^ is allowed) and must not use top-level alternation. Previously it only had to contain the repository somewhere, which let ^https://github\.com/NVIDIA/aicr/.*|.*$ and (good|https://github.com/attacker/x/.*) through — both reduce the gate to "any GitHub Actions workflow in any repository". This affects --certificate-identity-regexp, --identity-pattern, AICR_BINARY_ATTESTATION_IDENTITY_REGEXP, and spec.verify.trust.certificateIdentityRegexp. All ten patterns in-tree (release default, four e2e workflows, four UAT workflows) still validate; a test asserts that. A pattern that merely contained the prefix now fails closed at validation time with a message naming what to anchor to.

  2. recipe sign-catalog drops --fulcio-url and --rekor-url, along with their AICR_FULCIO_URL / AICR_REKOR_URL env sources. Both could only produce a signature recipe verify-catalog cannot verify. The command is Hidden/CI-only and the goreleaser hook passes only --signing-config and --output, so the release path is unaffected. Two consequences for anyone scripting the hidden command: an explicit --rekor-url now gets urfave's flag provided but not defined rather than a descriptive error, and an ambient AICR_REKOR_URL (set for bundle --attest) is now ignored by sign-catalog instead of silently retargeting the log — the latter is the point, since that ambient pickup was producing unverifiable catalogs.

Checklist

  • Tests pass locally (make test with -race)
  • Linter passes (make lint)
  • I did not skip/disable tests to make CI green
  • I added/updated tests for new functionality
  • I updated docs if user-facing behavior changed
  • Changes follow existing patterns in the codebase
  • Commits are cryptographically signed (git commit -S)

The facade could produce signed artifacts but not check one, so an
integrator embedding AICR had to import pkg/bundler/verifier,
pkg/evidence/verifier, and pkg/recipe/catalog to establish trust —
packages carrying no stability guarantee.

Adds the consumer-side surface (VerifyBundle, VerifyEvidence,
VerifyCatalog, RecipeDigest, plus the stateless VerifyBinaryAttestation,
ValidateIdentityPattern, TrustLevels, and evidence renderers) and the
producer-side counterparts (PublishEvidence, SignCatalog).

BundleVerifyOptions mirrors config.VerifySpec field-for-field so the
AICRConfig binding is mechanical. MinTrustLevel departs from
verifier.Policy: empty means "max", not "no check", so a caller who
never considered the trust floor still gets a gate.

Verification methods are bounded by defaults.VerifyOperationTimeout;
the signing methods deliberately are not, since keyless OIDC can block
on a human completing a browser or device-code flow.

Routes six CLI commands and pkg/server/signing.go through the facade.
pkg/server non-test code no longer imports pkg/bundler/verifier.

`aicr evidence sign` stays CLI-only: it orchestrates six primitives
across two packages including in-place pointer relocation, so wrapping
it would mean exposing five more primitives or inventing a coarse
method around filesystem side effects.

Fixes: #2024
Signed-off-by: Mark Chmarny <mark@chmarny.com>
@mchmarny mchmarny self-assigned this Aug 17, 2026
@mchmarny mchmarny added the theme/supply-chain SLSA, SBOM, Sigstore, and provenance verification label Aug 17, 2026
@coderabbitai

This comment was marked as resolved.

coderabbitai[bot]

This comment was marked as resolved.

@github-actions

github-actions Bot commented Aug 17, 2026

Copy link
Copy Markdown
Contributor

Coverage Report ✅

Metric Value
Coverage 83.0%
Threshold 80%
Status Pass
Coverage Badge
![Coverage](https://img.shields.io/badge/coverage-83.0%25-brightgreen)

Merging this branch changes the coverage (2 decrease, 1 increase)

Impacted Packages Coverage Δ 🤖
github.com/NVIDIA/aicr/pkg/bundler/verifier 76.02% (+0.11%) 👍
github.com/NVIDIA/aicr/pkg/cli 74.79% (-0.07%) 👎
github.com/NVIDIA/aicr/pkg/client/v1 83.03% (-0.15%) 👎
github.com/NVIDIA/aicr/pkg/config 93.53% (ø)
github.com/NVIDIA/aicr/pkg/defaults 100.00% (ø)
github.com/NVIDIA/aicr/pkg/recipe/catalog 85.25% (ø)
github.com/NVIDIA/aicr/pkg/server 82.29% (ø)

Coverage by file

Changed files (no unit tests)

Changed File Coverage Δ Total Covered Missed 🤖
github.com/NVIDIA/aicr/pkg/bundler/verifier/verifier.go 74.36% (+0.19%) 312 (+10) 232 (+8) 80 (+2) 👍
github.com/NVIDIA/aicr/pkg/cli/bundle.go 79.05% (ø) 444 351 93
github.com/NVIDIA/aicr/pkg/cli/bundle_verify.go 88.06% (+0.22%) 67 (-7) 59 (-6) 8 (-1) 👍
github.com/NVIDIA/aicr/pkg/cli/evidence_digest.go 76.47% (-0.45%) 17 (+4) 13 (+3) 4 (+1) 👎
github.com/NVIDIA/aicr/pkg/cli/evidence_publish.go 85.00% (-2.50%) 20 (+4) 17 (+3) 3 (+1) 👎
github.com/NVIDIA/aicr/pkg/cli/evidence_verify.go 79.59% (+0.05%) 49 (+5) 39 (+4) 10 (+1) 👍
github.com/NVIDIA/aicr/pkg/cli/recipe_sign_catalog.go 5.88% (+0.62%) 17 (-2) 1 16 (-2) 👍
github.com/NVIDIA/aicr/pkg/cli/recipe_verify_catalog.go 21.43% (-8.57%) 14 (+4) 3 11 (+4) 👎
github.com/NVIDIA/aicr/pkg/cli/root.go 84.96% (-0.31%) 133 (+4) 113 (+3) 20 (+1) 👎
github.com/NVIDIA/aicr/pkg/cli/validate_evidence.go 75.00% (ø) 16 12 4
github.com/NVIDIA/aicr/pkg/client/v1/aicr.go 83.55% (ø) 462 386 76
github.com/NVIDIA/aicr/pkg/client/v1/sign.go 60.00% (+60.00%) 45 (+45) 27 (+27) 18 (+18) 🌟
github.com/NVIDIA/aicr/pkg/client/v1/verify.go 90.99% (+90.99%) 111 (+111) 101 (+101) 10 (+10) 🌟
github.com/NVIDIA/aicr/pkg/config/config.go 0.00% (ø) 0 0 0
github.com/NVIDIA/aicr/pkg/defaults/timeouts.go 0.00% (ø) 0 0 0
github.com/NVIDIA/aicr/pkg/recipe/catalog/verify.go 80.00% (ø) 20 16 4
github.com/NVIDIA/aicr/pkg/server/signing.go 71.11% (ø) 90 64 26

Please note that the "Total", "Covered", and "Missed" counts above refer to code statements instead of lines of code. The value in brackets refers to the test coverage of that file in the old version of the code.

Two review findings on the verification surface.

ValidateIdentityPattern only required the NVIDIA/aicr repository prefix
as a substring, so a pattern could carry it and still match everything:

    ^https://github\.com/NVIDIA/aicr/.*|.*$

That contains the prefix, compiles, and is anchored at the prefix, yet
its second branch matches any string. The identity matcher pins only the
OIDC issuer beyond this pattern, so such a value silently degraded
binary and catalog verification to "any GitHub Actions workflow in any
repository".

Adds two layers, neither of which subsumes the other. Structural:
reject top-level alternation, parsed via regexp/syntax rather than
split on "|" so separators inside groups and character classes are not
misread. This covers branches too narrow for any fixed canary set to
catch, such as one attacker-controlled repository. Behavioral: reject
any pattern that still matches a foreign-identity canary, which covers
widening a structural read cannot see, such as an empty branch inside a
group. Fixed in pkg/bundler/verifier so the CLI, server, and catalog
paths are all covered, not just the facade re-export.

SignCatalog accepted private Fulcio and key-based signing while
VerifyCatalog verifies only keyless GitHub OIDC certificates against
the public-good root with a required transparency-log entry. An
integrator could sign a catalog successfully and then find the
documented counterpart unable to verify it. SignCatalog now rejects
SigningKey, FulcioURL, and DisableTLogUpload before any signing work
runs; RekorURL and SigningConfigPath still pass through because
verification handles both. The undocumented `recipe sign-catalog
--fulcio-url` flag is removed for the same reason -- it could only
produce an unverifiable artifact. The release path uses neither.

The bundle pair is unaffected: `bundle --fulcio-url` is symmetric with
`verify --trust-root`, which is exactly what the catalog pair lacked.

Signed-off-by: Mark Chmarny <mark@chmarny.com>
@mchmarny

Copy link
Copy Markdown
Member Author

Both findings confirmed and fixed in c242a4d. make qualify green.

1 — identity pinning was bypassable

Confirmed. ValidateIdentityPattern did strings.Contains(pattern, "://github.com/NVIDIA/aicr/") plus a compile check, so your example passes validation and matches anything via its second branch. Since the matcher pins only the OIDC issuer beyond this pattern, that degraded the gate to any GitHub Actions workflow in any repository, silently.

One correction to the prescribed fix: structural anchoring alone does not close it. Your example is already anchored at the repo prefix and still bypasses — ^ constrains where the first branch starts, not what the other branches accept. So the fix is two layers, neither subsuming the other:

  • Structural — reject top-level alternation, parsed via regexp/syntax rather than split on | so separators inside groups and character classes are not misread. This catches branches too narrow for any fixed canary set: https://github.com/NVIDIA/aicr/.*|^https://github\.com/evil/repo/.* matches no generic canary, because the attacker picks the repo name.
  • Behavioral — reject any pattern that still matches a foreign-identity canary. This catches widening a structural read cannot see: (https://github.com/NVIDIA/aicr/|) leaves the root op a capture group while making the whole pattern match anything.

Fixed in pkg/bundler/verifier, not just the facade re-export, so the CLI, the server's startup self-check, and recipe verify-catalog are all covered — that function's callers all had the same hole. pkg/recipe/catalog/verify.go even carries a comment claiming it prevents exactly this.

Test cases added for all six bypasses you'd expect: trailing/leading alternation, alternation to one foreign repo, empty branch in a group, trailing empty branch, and inline flags plus alternation. Both legitimate in-tree overrides (the release default and server-kms-e2e.yaml) still validate.

Note this is a tightening of existing behavior, not only new surface — a previously-accepted pattern using top-level alternation will now be rejected. That is the intent, and no in-tree pattern is affected.

2 — sign/verify asymmetry on the catalog

Confirmed. VerifyCatalogrecipecat.Verifyverifier.VerifyBinaryAttestation with a nil trust source: public-good root only, GitHub OIDC issuer pinned, transparency-log entry required. SignCatalog accepted the full ResolveOptions, including KMS and private Fulcio.

Took the restrict-signing branch of your suggestion rather than widening verification. Widening means extending pkg/recipe/catalog.VerifyOptions and threading a trust source and key identity through — a substantially larger change to a package this PR does not otherwise touch, to serve a use case nobody has asked for. The catalog is a release artifact NVIDIA signs, not something consumers re-sign privately.

SignCatalog now rejects three settings with ErrCodeInvalidRequest before any signing work runs:

Setting Why
SigningKey A key-signed catalog has no verification path at all.
FulcioURL A private CA's certificate does not chain to the public-good root.
DisableTLogUpload Verification requires a transparency-log entry.

RekorURL and SigningConfigPath pass through — they select which public-good log and signing config to use, which verification handles. The release path uses --signing-config, so it is unaffected; there is a test asserting neither is swept up by the rejection.

Also removed the undocumented recipe sign-catalog --fulcio-url flag, which could only ever produce an artifact verify-catalog refuses. Nothing in the release or the test suite used it.

On the round-trip test: a real one needs an OIDC token, and driving SignCatalog without one falls through to the interactive browser flow and would hang CI. What is tested is the invariant itself — the rejection happens before the attester resolves, so it is observable without a token — in both directions: asymmetric modes rejected, symmetric ones not.

Worth noting for the broader review: the bundle pair was already symmetric — bundle --fulcio-urlverify --trust-root — which is precisely what the catalog pair lacked. Client.VerifyBundle exposes TrustRoot and Key, so no equivalent restriction is needed there.

@github-actions

Copy link
Copy Markdown
Contributor

@github-actions

Copy link
Copy Markdown
Contributor

Recipe evidence check

No leaf overlays affected by this PR.

This gate is warning-only and never blocks merge.

coderabbitai[bot]

This comment was marked as resolved.

Review round two on the verification surface.

The previous fix rejected top-level alternation, but syntax.Parse returns
OpCapture for a grouped pattern, so a nested alternation slipped past
both layers:

    (https://github.com/NVIDIA/aicr/.*|https://github.com/attacker/isolated/.*)

The root op is a capture rather than an alternation, and the foreign
branch names a repository no fixed canary set can enumerate. Confirmed
as accepted by the code as pushed.

Rather than recursing to classify which alternations can match before
the prefix -- fragile with nesting, inline flags, and repetition -- the
substring test becomes a prefix test. A pattern must BEGIN with
https://github.com/NVIDIA/aicr/ (one optional leading ^, escaped-dot
form accepted) and its root must not be an alternation. Those two are
sound rather than heuristic: a pattern starting with a literal whose
root is a concatenation can only match strings starting with that
literal. Alternatives placed after the prefix stay valid, since every
branch is already behind the pin. The canary layer is kept for widening
a structural read cannot see, such as an empty branch inside a group.

This tightens existing behavior: a pattern that merely contains the
prefix somewhere is now rejected. No in-tree pattern is affected, and
unanchored spellings still work because the leading ^ stays optional.

SignCatalog also now rejects RekorURL. The earlier reasoning -- that the
flag may name the public-good v1 URL, which verifies -- failed open,
because that is indistinguishable from a private log by URL alone. The
CLI `recipe sign-catalog --rekor-url` flag is removed for the same
reason --fulcio-url was. The residual gap is documented rather than
implied: SigningConfigPath still passes through because the release
requires it, and a signing config can name private endpoints.

Removes a test that reached the interactive OIDC flow. SigningConfigPath
and RekorURL passed the symmetry guard, so SignCatalog resolved an
attester, found no ambient token, and opened a browser -- which hangs a
headless CI run. Canceling the context does not help, since
ResolveAttesterLazy ignores its ctx parameter. The both-directions
assertion moved onto rejectUnverifiableCatalogSigning directly, which is
why that function was extracted.

Also fixes an unescaped table-cell pipe, a stale option-type name in a
doc comment, a nil-result dereference in SignCatalog, and a timeout
claim in the integrator guide that contradicted the signing exception.

Signed-off-by: Mark Chmarny <mark@chmarny.com>
coderabbitai[bot]

This comment was marked as resolved.

Review round three, all documentation and comment accuracy.

The identity-pattern rule changed to "must begin with the repository
prefix" but several descriptions still said "must contain", which is the
worse direction to be stale in: a reader follows the older rule and only
finds out at validation time. Two were flagged (the api-server prose and
the AICRConfig YAML example); a sweep found seven more in Go doc
comments, two of which are --help strings, and one of which is
VerifyTrustSpec.CertificateIdentityRegexp -- the committed-config path,
so the likeliest to be copied into version control before failing.

SignCatalog's rejected-setting count said three in three places while
the list beneath had four; RekorURL was added without updating them. The
counts now read four and the field comment names all four, so the next
addition cannot leave a bare number to drift.

The go-library section asserted that SignCatalog accepts only what
VerifyCatalog can verify, then withdrew that guarantee three paragraphs
later for SigningConfigPath. The claim is now scoped up front and the
exception gets its own paragraph: a signing config can still produce a
catalog VerifyCatalog rejects. Validating the loaded config against the
public-good endpoints is noted as the principled fix, deliberately not
taken here -- it needs a maintained list of public-good endpoints, and
the release path depends on --signing-config working.

Also excludes MakeBundle from the uncapped-context statement, since its
cap is opt-in via BundleOptions.Timeout and only absent at the default.

Signed-off-by: Mark Chmarny <mark@chmarny.com>
coderabbitai[bot]

This comment was marked as resolved.

mchmarny and others added 2 commits August 17, 2026 15:10
The CLI E2E chainsaw suite failed on a88c794: bundle-attestation-ci
asserted the rejection message contained "must contain", which the
begin-with rule reworded. The rejection itself was correct (exit 2) --
only the asserted substring was stale.

Local `make qualify` did not catch it. tools/e2e runs the same chainsaw
directory, but skips the attestation tests when no binary attestation is
present, which is the case locally and not in CI.

Rather than swap one prose substring for another, the step now asserts
what actually matters and fails closed:

  - captures the exit status instead of `|| true`, so a regression that
    ACCEPTS a widened pattern fails the test rather than passing it
    silently -- the dangerous direction for a negative test
  - covers both rejection paths, the missing prefix ('.*') and the
    top-level alternation, which previously had no e2e coverage
  - matches on the repository path rather than the wording, so the next
    rewording does not break it again

That assertion depends on every rejection naming the repository, which
was not true: the alternation message described the rule without saying
what to anchor to, and the CLI surfaces only the message, not the
structured context. It now names the prefix and shows the accepted
form. A unit test pins the invariant so the e2e assertion stays honest.

Verified separately that all ten identity patterns in-tree -- the
release default, the server/vault/ministack/scaffolding e2e workflows,
and the four UAT workflows -- still validate under the new rule.

Signed-off-by: Mark Chmarny <mark@chmarny.com>
@mchmarny
mchmarny marked this pull request as ready for review August 17, 2026 22:55
@mchmarny
mchmarny requested a review from a team as a code owner August 17, 2026 22:55
@mchmarny
mchmarny enabled auto-merge (squash) August 17, 2026 23:05
njhensley
njhensley previously approved these changes Aug 17, 2026

@njhensley njhensley left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Approve with comments — 0 🔴 · 0 🟠 · 1 🟡 · 6 🔵

Well-engineered, thoroughly-documented additive SDK surface. The security crux — the 3-layer certificate-identity confinement (HasPrefix anchor + reject root OpAlternate + foreignIdentityCanaries sweep) — survived an adversarial bypass battery with no findings. Sign/verify symmetry, inflight accounting, timeout gating, and CLI behavior-preservation all verified sound. Everything remaining is documentation accuracy plus one intentional CLI flag removal on a Hidden CI-only command.

Inline comments below. The only 🟡 is F1 — a stale exported godoc that contradicts the security contract this PR just hardened; worth fixing before merge, the rest are optional nits.

Verified sound (examined, not defects): identity confinement (adversarial battery, no bypass) · default TrustedRepositoryPattern passes its own new validation · sign/verify symmetry covers all 4 dangerous fields · IgnoreTLog-requires-Key gating · inflight accounting (no leak/double-count/Close race) · MinTrustLevel empty→"max" · no --data/config regression · all 6 rewired commands Close() exactly once · api-diff + stability tests pin every new symbol/field/constant.

Comment thread pkg/client/v1/verify.go
Comment thread pkg/cli/recipe_sign_catalog.go
Comment thread pkg/client/v1/verify.go Outdated
Comment thread pkg/defaults/timeouts.go Outdated
Comment thread pkg/client/v1/verify.go
Comment thread pkg/client/v1/sign.go
Comment thread pkg/cli/bundle_verify.go
@mchmarny
mchmarny disabled auto-merge August 17, 2026 23:38
mchmarny and others added 2 commits August 17, 2026 16:38
Seven review findings, all documentation and comment accuracy on the
verification surface.

BinaryAttestationVerifyOptions.IdentityRegexp still described the old
"must contain" rule. It was the last one left and the one that mattered
most: exported and stability-pinned, so unlike the CLI usage strings an
out-of-tree consumer has no other source of truth. A substring pattern
built from that godoc is now rejected at runtime.

TrustedIdentityPattern claimed an override could verify a fork. Under
the begin-with rule a fork's certificate identity can never satisfy it,
so the clause described pre-hardening behavior. Now says the override
exists to pin a different workflow within NVIDIA/aicr, and that
verifying a fork is not possible and not intended.

VerifyOperationTimeout was framed as a fallback for deadline-less
callers, but the four verify methods wrap unconditionally, so
context.WithTimeout caps a caller who deliberately allowed longer. The
other facade methods all wrap unconditionally too, and making verify
behave differently would trade a doc bug for an inconsistency while
dropping protection for the unbounded-context case it exists for. So
the constant now states it is an unconditional ceiling, and the sharp
consequence is documented on VerifyEvidence: a cap breach returns an
error, NOT EvidenceExitIncomplete, so a gate distinguishing "could not
check this" from "checked it and it failed" must treat a
context-deadline error as the former.

TrustLevels disclosed that "max" is absent without saying what to do
about it. A validator built from the list rejects only the documented
default, so it passes every test where a level was set explicitly. Now
states the list is not the accepted set for MinTrustLevel input.

PublishEvidence reads c.version without the lock while SignCatalog and
RecipeDigest snapshot it under one. Not a race -- version is immutable
post-construction and MergeReports reads it the same way. The asymmetry
is that those two take the lock for the DataProvider, which Close DOES
clear, and pick up version while already holding it. Recorded inline,
including the hazard a bare lock would not convey: do not copy the
pattern for a field Close mutates.

VerifyBundle and VerifyEvidence now state that the recipe catalog is
not consulted, so any open Client will do and a hot-path caller can
reuse one instead of constructing per call.

Also marks the PR breaking and expands the rollout notes to cover the
stricter pattern validation and the sign-catalog flag removal --
including that dropping the AICR_FULCIO_URL / AICR_REKOR_URL env
sources makes an ambient AICR_REKOR_URL ignored rather than silently
retargeting the log.

Signed-off-by: Mark Chmarny <mark@chmarny.com>

@njhensley njhensley left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Re-review — Approve. Confirmed the two follow-up commits (a88c794, c2aba76) resolve all seven findings from the prior round, and did so as docs/comments only — no logic changed, no regressions, nothing new introduced.

  • F1/F3 IdentityRegexp and TrustedIdentityPattern godoc now match ValidateIdentityPattern exactly (begin-with https://github.com/NVIDIA/aicr/, optional leading ^, no top-level alternation; the impossible "verify a fork" clause dropped). Verified against verifier.go:279.
  • F4 VerifyOperationTimeout reworded to an unconditional ceiling and now documents the error-vs-Incomplete edge, called out on VerifyEvidence.
  • F5 TrustLevels() godoc now directs input-validation to also accept max and the empty string.
  • F6 PublishEvidence carries a precise rationale for the lock-free version read, with a warning not to copy the pattern for a Close-mutated field.
  • F7 VerifyBundle/VerifyEvidence document that any open Client is reusable on the hot path.
  • F2 correctly unchanged (intentional CLI-flag removal).

Nice, thorough follow-through.

@mchmarny
mchmarny enabled auto-merge (squash) August 17, 2026 23:43
@mchmarny
mchmarny merged commit 39d5867 into main Aug 17, 2026
74 checks passed
@mchmarny
mchmarny deleted the feat/sdk-verification-surface branch August 17, 2026 23:50
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.

sdk: expose the verification surface on the facade

2 participants