A consumer-facing guide to verifying the artifacts aicr produces (deployment
bundles and recipe-evidence bundles) across public-trust, KMS-key, and
offline deployment shapes. For the exhaustive per-flag reference, see the
CLI reference: aicr verify,
aicr evidence verify, and
aicr trust update.
| Artifact | Produced by | Verified by |
|---|---|---|
| Deployment bundle | aicr bundle |
aicr verify |
| Recipe-evidence bundle | aicr validate --emit-attestation |
aicr evidence verify |
| Embedded recipe catalog | shipped in the aicr binary |
aicr recipe verify-catalog |
This guide focuses on the first two. Catalog verification is a single self-contained command; see its CLI reference entry.
Bundles are unsigned by default; attestation is opt-in via aicr bundle --attest. An unsigned bundle can still have its closed-world inventory
verified, but it cannot reach the higher trust levels described below.
AICR treats a deployment bundle as a closed-world inventory. checksums.txt contains one SHA256 entry for every regular payload file, including recipe.yaml when present. Verification derives the required directories from those paths and rejects every additional file or directory, symlink, and other non-regular filesystem object. Only checksums.txt, attestation/bundle-attestation.sigstore.json, and attestation/aicr-attestation.sigstore.json may exist outside the manifest; they remain part of the verified inventory. Inventory validation accepts valid manifest entries in any order, while AICR generates them sorted by canonical slash-relative path. AICR ZIP and CLI OCI publication revalidate a private snapshot and publish only that inventory. Legacy bundles with incomplete manifests report unknown trust and must be regenerated.
aicr verify computes a trust level for a bundle. The levels are ordered;
each one subsumes the guarantees of the levels beneath it.
| Level | Name | What it guarantees |
|---|---|---|
| 4 | verified |
Closed-world inventory valid, bundle attestation verified, binary attestation verified with identity pinned to NVIDIA CI, and no external data |
| 3 | attested |
Closed-world inventory and bundle attestation valid, but the chain is incomplete — the binary attestation is missing or external --data was used |
| 2 | unverified |
Closed-world inventory valid, but no attestation files exist (the bundle was created without --attest) |
| 1 | unknown |
Inventory is missing, incomplete, or invalid; or an attestation (bundle or binary) is present but fails verification — a present binary attestation that does not verify is a hard failure, not a degraded attested |
The ordering matters for enforcement: verified > attested > unverified >
unknown. A bundle that uses external data can never exceed attested, and a
bundle created without --attest can never exceed unverified, regardless of
anything else. This is why --min-trust-level max (the default) auto-detects
the highest level a given bundle could achieve and verifies against that,
rather than failing a deliberately unsigned bundle.
The default path. Verification is offline and makes no network calls (the one
exception, a KMS URI passed to --key, is covered below).
# Verify a bundle, auto-detecting and enforcing its maximum achievable trust level.
aicr verify ./my-bundleUnder the hood this runs three checks:
- Closed-world inventory: every regular payload file, including
recipe.yamlwhen present, must have a matching entry inchecksums.txt; every additional filesystem entry is rejected except the three allowed inventory metadata files. - Bundle attestation: the bundle's signature is verified against the Sigstore trusted root.
- Binary attestation: the provenance chain is verified with identity pinned to NVIDIA CI.
Manifest entry order does not affect inventory validation. AICR nevertheless
generates checksums.txt in canonical slash-relative path order. Reordering an
already signed manifest changes its bytes and invalidates the existing bundle
attestation.
You can also pin the bundle's creator identity or the CLI version that produced it:
# Require that a specific identity created the bundle.
aicr verify ./my-bundle --require-creator jdoe@company.com
# Require a minimum aicr CLI version (recorded in the attestation predicate).
aicr verify ./my-bundle --cli-version-constraint ">= 0.8.0"By default aicr verify uses --min-trust-level max, which resolves to the
highest level the bundle could achieve and fails if verification falls short.
To require an explicit floor regardless of the bundle's contents, name a level:
# Fail unless the bundle reaches full `verified` trust.
aicr verify ./my-bundle --min-trust-level verified
# Accept `attested` or higher (e.g. when external --data is expected).
aicr verify ./my-bundle --min-trust-level attestedValid values are verified, attested, unverified, unknown, and max.
A bundle whose computed level is below the requested floor exits non-zero.
An incomplete legacy manifest cannot establish a closed-world inventory, so it
reports unknown and the bundle must be regenerated.
Some environments cannot use keyless OIDC signing, so bundles are signed with a
KMS key via aicr bundle --attest --signing-key <kms-uri>. Verify those
bundles with --key, supplying the same KMS URI used to sign. Supported
schemes are awskms://, gcpkms://, azurekms://, and hashivault://.
# Sign with a KMS key, then verify it with the same key.
aicr bundle -r recipe.yaml --attest \
--signing-key gcpkms://projects/p/locations/l/keyRings/r/cryptoKeys/k \
-o ./bundles
aicr verify ./bundles/<bundle-dir> \
--key gcpkms://projects/p/locations/l/keyRings/r/cryptoKeys/kResolving a KMS URI makes network calls to the KMS provider to fetch the public key, so credentials for that provider must be available in the environment.
To verify without granting KMS access, export the public key once and verify against the local PEM file; this part makes no provider calls:
# Export the public key once (requires KMS access at export time).
cosign public-key --key gcpkms://projects/p/locations/l/keyRings/r/cryptoKeys/k > bundle-signer.pub
# Verify anywhere afterward, with no KMS access needed.
aicr verify ./bundles/<bundle-dir> --key ./bundle-signer.pubA local PEM key is read from disk only. The bundle's Rekor transparency-log
entry is also checked by default, but its inclusion proof is embedded in the
bundle and the trusted root falls back to the embedded copy on a cache miss —
so verification runs fully offline without aicr trust update.
Air-gapped and private-deployment sites often run their own Sigstore stack (a
self-hosted Fulcio CA and Rekor log, for example from sigstore/scaffolding)
rather than the public-good infrastructure. aicr bundle --attest signs
against that stack with --fulcio-url / --rekor-url; aicr verify --trust-root is the verify counterpart. Point it at the trusted_root.json
your private Sigstore stack emits:
# Verify a bundle signed against a private Fulcio/Rekor.
aicr verify ./my-bundle --trust-root ./trusted_root.json--trust-root is additive: the supplied root is unioned with AICR's
built-in public-good root, not substituted for it. So a single command verifies
both org-signed bundles (against the private root) and NVIDIA-signed bundles
(against the public-good root); you do not need to switch flags per artifact.
The flag supplies trust anchors only; it does not by itself require a bundle to
be privately signed. To pin a specific signer, keep using --require-creator
(and, for the binary attestation's identity, --certificate-identity-regexp):
# Verify against the org root and require a specific signer identity.
aicr verify ./my-bundle \
--trust-root ./trusted_root.json \
--require-creator ci@myorg.example.com--trust-root composes with --key, so a KMS- or PEM-key-signed bundle whose
signature was logged to a private Rekor verifies with both flags together:
aicr verify ./my-bundle \
--trust-root ./trusted_root.json \
--key ./bundle-signer.pubOnly the bundle attestation consults the private root. The binary
attestation is always produced by NVIDIA's public CI and continues to verify
against the public-good root, regardless of --trust-root. Errors with the
supplied trusted_root.json (missing, unreadable, oversized, or malformed) are
reported as invalid-request failures.
aicr verify is offline by default and does not call out to Sigstore at verify
time; the Rekor inclusion proof is embedded in the bundle, so no live Rekor
call is made. The check needs the Sigstore trusted root, but verification forces the local
cache (WithForceCache) and falls back to the embedded trusted root on a
cache miss — it does not fetch on the verify path, so offline verification works
out of the box. aicr trust update refreshes the cached trust material (it
contacts the TUF CDN once) but is not required for offline verification.
Two scope limits to be aware of, both reflected in the current CLI reference:
- Fully transparency-log-free verification: dropping the Rekor transparency-log check entirely, for true air-gapped use, is not yet supported. It is tracked in #1154.
- Private Sigstore verification:
aicr bundle --attestcan redirect signing to a private Fulcio/Rekor with--fulcio-url/--rekor-url, andaicr verify --trust-rootverifies the resulting bundles against that infrastructure'strusted_root.json. See Privately-Signed Bundle Verification.
Recipe-evidence bundles, produced by aicr validate --emit-attestation, are
verified with aicr evidence verify. When the bundle carries a signature, the
command verifies it against the Sigstore trusted root, recomputes every
manifest-listed payload file's sha256 against manifest.json, and surfaces the predicate's fingerprint, phase
counts, and BOM info.
Bundles are minimized by default: the published snapshot.yaml keeps only
an allowlisted set of fields and the CTRF reports omit per-test stdout/message,
keeping sensitive operational detail (node names, provider instance IDs, the
node label/taint set, OS tuning, raw container logs) out of the published
artifact. The predicate records the applied policy in a redaction block, which
aicr evidence verify surfaces. Minimal bundles self-verify exactly like full
ones — the digests cover whatever bytes shipped. Pass --full to
aicr validate --emit-attestation to publish the raw payloads instead.
The positional argument is auto-detected. Prefer the committed pointer file, which pins the bundle by digest:
# Verify a pointer a contributor committed alongside a recipe change (preferred).
aicr evidence verify recipes/evidence/<recipe>/<src>/<digest>.yaml
# Verify a pushed OCI bundle directly, pinned by digest.
aicr evidence verify ghcr.io/myorg/aicr-evidence@sha256:abc...
# Verify a local bundle directory (self-debug before push).
aicr evidence verify ./out/summary-bundleTo require a specific signer, pin the OIDC issuer and identity:
aicr evidence verify recipes/evidence/<recipe>/<src>/<digest>.yaml \
--expected-issuer https://token.actions.githubusercontent.com \
--expected-identity-regexp '^https://github\.com/myorg/myrepo/\.github/workflows/release\.yaml@refs/tags/.+$'Committed evidence pointers are per-source so two parties can attest to the same recipe without overwriting each other. The on-disk layout is add-only and immutable (ADR-007; issue #1347):
recipes/evidence/<recipe>/<src>/<bundle-digest>.yaml # one immutable pointer per run
recipes/evidence/allowlist.yaml # maintained signer allowlist
Each file is a single-attestation V1 pointer — the same schema that
aicr evidence verify already consumes. The <src> segment is a stable slug
derived from the signer OIDC identity recorded in the pointer — the first 32 hex
characters (128 bits) of sha256(issuer + "\n" + identity). Because the slug is
computed from the signer fields rather than chosen freely, the
Evidence Pointer Contract CI job recomputes the slug from each pointer's own
signer block and rejects any file that does not live under the directory its
signer hashes to.
Where cryptographic trust enters (#1535). The on-disk pointer-contract merge gate is structural: it derives the slug from the signer fields the pointer declares and checks path ownership + allowlist membership, but does not itself verify a signature. Cryptographic trust is enforced after merge, at ingest (
evidence-ingest.yaml), which verifies the signature pinned to the claimed signer and cross-checks the certificate before any result is counted — so a pointer that lied about its signer passes the gate but fails ingest. See #1535 and ADR-007. (This ingest verification is implemented but currently fails closed — the GP2 loader cannot yet parse the canonicalidentityPattern/sourceallowlist; tracked in #1505.)
Consumers discover a recipe's evidence by glob —
recipes/evidence/<recipe>/*/*.yaml — and aggregate across sources; nothing is
ever modified in place.
The allowlist (recipes/evidence/allowlist.yaml) is the trust root. It
pins, per class — first-party, community, partner — the signers that may
contribute corroborating evidence. Community and partner entries are keyed by
the one-way source slug only — the cleartext identity (e.g. a personal
email) is never stored in the allowlist; an optional non-PII label
(e.g. a GitHub handle) is for display and may be omitted. (The committed
pointer and Rekor still carry signer.identity — see the note below — so a
contributor is not anonymous.)
First-party entries pin a tightly-bounded anchored identityPattern whose
issuer/org/repo/workflow segment is literal (a wildcard is permitted only in
the ref) — these are CI workflow URLs, not personal identities. The classes
are disjoint and no two entries overlap, so a verified signer classifies
exactly one way. A verified signer that is not listed is admitted as
reported only — it never counts toward corroboration.
(Only the allowlist is slug-only — its loader rejects a stray identity:
field. Committed pointers (recipes/evidence/<recipe>/<src>/*.yaml) do
carry signer.identity, and keyless Sigstore also records it in the public
Rekor log, so the signer identity is public regardless. Sign with a
non-personal identity — a GitHub users.noreply.github.com address or CI's
ambient OIDC — not a personal email.)
First-party AICR CI (the UAT workflows) signs with the GitHub Actions OIDC
issuer and ingests evidence directly — it does not commit a per-run pointer,
which would churn main nightly. Committed per-source pointers are therefore
the community/partner channel.
- Run
aicr validate --emit-attestationand push the signed bundle to a registry you control. The command prints acopyTohint with the exact per-source destination path. - Open a PR that adds your own pointer file under
recipes/evidence/<recipe>/<src>/— alongside, never replacing, any other party's — plus a slug entry (source:+issuer:, optionallabel:) for your signer in the correct class ofrecipes/evidence/allowlist.yaml. The<src>slug is printed in thecopyTohint; use that same value assource:. - Maintainer review (enforced by
CODEOWNERS) is the trust gate; theEvidence Pointer Contractjob verifies the path-ownership and allowlist invariants before merge.
A tag-only OCI reference is refused by default because tags are
registry-rewritable; pass the pointer file (which carries the digest) or a
digest-pinned ref instead. See the
aicr evidence verify reference for the
full input-detection rules and the --allow-unpinned-tag escape hatch.
Both verifiers emit machine-readable JSON for pipeline gating.
# Bundle verification as JSON.
aicr verify ./my-bundle --format json
# Evidence verification as JSON to a file.
aicr evidence verify recipes/evidence/<recipe>/<src>/<digest>.yaml --format json -o result.jsonaicr evidence verify exits 0 when every check passes and 2 when the
bundle is invalid or recorded validator results show failures. The JSON
output's exit field further distinguishes recorded phase failures (1) from
an invalid bundle (2), so a shell consumer can branch on it. Write the JSON to
a file and read the exit field from it rather than piping aicr evidence verify straight into jq: under set -o pipefail (common in CI) the verifier's
non-zero exit would otherwise propagate through the pipeline and abort the script
before the case runs. The || true keeps that exit from tripping set -e:
aicr evidence verify recipes/evidence/<recipe>/<src>/<digest>.yaml --format json -o result.json || true
case "$(jq '.exit' result.json)" in
0) echo "evidence valid" ;;
1) echo "validator phases failed" ;;
2) echo "bundle invalid" ;;
esacCertificate chain errors / stale trusted root. Sigstore rotates signing keys a few times per year. If verification fails with certificate-chain errors, refresh the local trusted root:
aicr trust update--key cannot reach the KMS provider. A KMS URI requires provider
credentials in the environment. If you only need to verify (not sign), export
the public key once with cosign public-key --key <kms-uri> and pass the local
PEM file to --key instead; see Local PEM Key Verification.
Tag-only OCI reference refused. aicr evidence verify rejects tag-only
references because tags are registry-rewritable. Pass the committed pointer file
(which carries the sha256: digest) or a digest-pinned ref (...@sha256:<hex>).
Trust level lower than expected. A bundle created without --attest caps at
unverified; a bundle built with external --data caps at attested. If you
require a stricter level, re-create the bundle with attestation and without
external data, then verify with --min-trust-level verified. If an older
bundle has an incomplete checksums.txt, regenerate it; listed-digest checks
cannot establish closed-world trust.
Private-Sigstore-signed bundle won't verify. A bundle signed against a
private Fulcio/Rekor needs that infrastructure's trusted root. Pass it with
aicr verify --trust-root ./trusted_root.json. See
Privately-Signed Bundle Verification.