Skip to content

Commit 2b1be85

Browse files
authored
fix: enforce closed-world bundle verification (NVIDIA#1758)
Signed-off-by: Mark Chmarny <mark@chmarny.com>
1 parent d89d2c6 commit 2b1be85

82 files changed

Lines changed: 24785 additions & 2331 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

demos/bundle-attestation-demo-slides.html

Lines changed: 21 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -190,8 +190,8 @@ <h2>Generated artifacts have no upstream tag</h2>
190190
</tbody>
191191
</table>
192192
<div class="grid2">
193-
<div class="mini"><h4>Without attestation</h4><p>Bundle is just files. <code class="inl">checksums.txt</code> catches accidental corruption, nothing else.</p></div>
194-
<div class="mini"><h4>With <code class="inl">--attest</code></h4><p>The generated payload files listed in <code class="inl">checksums.txt</code>, including <code class="inl">recipe.yaml</code>, are cryptographically bound to the creator <em>and</em> the binary that produced it. Recursive provenance.</p></div>
193+
<div class="mini"><h4>Without attestation</h4><p><code class="inl">checksums.txt</code> detects accidental payload or tree changes, but provides no signed creator identity or provenance.</p></div>
194+
<div class="mini"><h4>With <code class="inl">--attest</code></h4><p>The complete closed-world payload inventory, including <code class="inl">recipe.yaml</code>, is cryptographically bound to the creator <em>and</em> the binary that produced it. Recursive provenance.</p></div>
195195
</div>
196196
</div>
197197
</section>
@@ -202,7 +202,7 @@ <h2>Generated artifacts have no upstream tag</h2>
202202
<div class="label">02 · Produce</div>
203203
<h2><code class="inl">aicr bundle --attest</code></h2>
204204
<figure class="fig">
205-
<svg class="diagram" viewBox="0 0 860 380" role="img" aria-label="Recipe flows through aicr bundle --attest, producing a bundle directory with attestation/ subdirectory containing two signed Sigstore bundles">
205+
<svg class="diagram" viewBox="0 0 860 380" role="img" aria-label="Recipe flows through aicr bundle --attest, producing a closed-world bundle inventory with checksums.txt and the two permitted attestation files">
206206
<defs>
207207
<marker id="bD1" markerWidth="9" markerHeight="9" refX="7" refY="4.5" orient="auto"><path d="M0,0 L9,4.5 L0,9 Z" fill="#46525f"/></marker>
208208
<marker id="bD1g" markerWidth="9" markerHeight="9" refX="7" refY="4.5" orient="auto"><path d="M0,0 L9,4.5 L0,9 Z" fill="#76b900"/></marker>
@@ -233,7 +233,8 @@ <h2><code class="inl">aicr bundle --attest</code></h2>
233233
<text x="46" y="214">recipe.yaml &#183; deploy.sh &#183; README.md</text>
234234
<text x="46" y="234">&lt;component&gt;/values.yaml &#183; ...</text>
235235
<text x="46" y="266" fill="#9aa7b4">checksums.txt</text>
236-
<text x="46" y="282" fill="#6b7785" font-size="11">(SHA256 of every generated payload file)</text>
236+
<text x="46" y="282" fill="#6b7785" font-size="11">closed-world SHA256 inventory; covers recipe.yaml</text>
237+
<text x="46" y="300" fill="#6b7785" font-size="11">additional filesystem entries are rejected</text>
237238
</g>
238239
<rect class="node-g" x="432" y="170" width="398" height="186" rx="9"/>
239240
<text class="ttl-g nlabel" x="448" y="194">attestation/</text>
@@ -300,7 +301,9 @@ <h2>Bundle &harr; binary &harr; NVIDIA CI</h2>
300301
<span class="t info">Why both halves matter</span>
301302
The bundle signature alone proves a creator made a bundle. The chain to the binary
302303
attestation proves the bundle was built by an attested NVIDIA-CI-released AICR CLI —
303-
not by a fork, an alpha build, or a tampered binary.
304+
not by a fork, an alpha build, or a tampered binary. AICR generates manifest entries
305+
sorted, but validates valid entries in any order; reordering a signed manifest changes its
306+
signed bytes and invalidates this chain.
304307
</div>
305308
</div>
306309
</section>
@@ -322,8 +325,9 @@ <h2><code class="inl">aicr verify</code> — clean pass</h2>
322325

323326
<span class="g">Bundle verification: PASSED</span></pre>
324327
</div>
325-
<p>Five gates ran: file integrity, bundle signature, bundle predicate, binary attestation
326-
chain, identity pin. Any failure short-circuits to a lower trust level.</p>
328+
<p>Five gates ran: closed-world inventory, bundle signature, bundle predicate, binary
329+
attestation chain, identity pin. Additional files or directories, symlinks, other
330+
non-regular objects, and failed attestations report <code class="inl">unknown</code>.</p>
327331
</div>
328332
</section>
329333

@@ -336,16 +340,17 @@ <h2>Four trust levels</h2>
336340
<thead><tr><th>Level</th><th>Name</th><th>Criteria</th></tr></thead>
337341
<tbody>
338342
<tr><td class="ok">4</td><td><code>verified</code></td><td>Checksums &nbsp;+ &nbsp;bundle attestation &nbsp;+ &nbsp;binary attestation pinned to NVIDIA CI</td></tr>
339-
<tr><td class="ok">3</td><td><code>attested</code></td><td>Bundle attestation verified; binary attestation missing or external data used (a <em>failed</em> binary attestation reports attested but exits nonzero)</td></tr>
340-
<tr><td class="warn">2</td><td><code>unverified</code></td><td>Checksums valid, no <code class="inl">--attest</code> was passed</td></tr>
341-
<tr><td class="bad">1</td><td><code>unknown</code></td><td>Missing/invalid <code class="inl">checksums.txt</code>, or bundle attestation fails verification</td></tr>
343+
<tr><td class="ok">3</td><td><code>attested</code></td><td>Bundle attestation verified; binary attestation missing, or external data used</td></tr>
344+
<tr><td class="warn">2</td><td><code>unverified</code></td><td>Closed-world checksum inventory valid, no <code class="inl">--attest</code> was passed</td></tr>
345+
<tr><td class="bad">1</td><td><code>unknown</code></td><td>Missing, invalid, or incomplete manifest; unexpected filesystem entry; or failed attestation</td></tr>
342346
</tbody>
343347
</table>
344348
<div class="note">
345349
<span class="t info">Why levels, not pass/fail</span>
346350
Different bundles live under different threats. CI-built production bundles must be
347351
<code class="inl">verified</code>; an on-call engineer&rsquo;s emergency hotfix bundle might only
348-
reach <code class="inl">attested</code>. Operators pick the floor per environment.
352+
reach <code class="inl">attested</code>. Legacy bundles with incomplete manifests remain
353+
<code class="inl">unknown</code> and must be regenerated. Operators pick the floor per environment.
349354
</div>
350355
</div>
351356
</section>
@@ -366,7 +371,7 @@ <h2>Policy from one flag</h2>
366371
<span class="c"># Constrain the CLI version that produced the bundle.</span>
367372
<span class="p">$</span> aicr verify ./my-bundle --cli-version-constraint ">= 1.0.0"
368373

369-
<span class="c"># CI: keep the verify exit code (a failed binary attestation reports attested but exits nonzero)</span>
374+
<span class="c"># CI: keep the verify exit code; failed integrity or attestation reports unknown</span>
370375
<span class="p">$</span> set -o pipefail; aicr verify ./my-bundle --format json | jq '.trustLevel, .trustReason'</pre>
371376
</div>
372377
<div class="grid2">
@@ -394,9 +399,10 @@ <h2>Tamper-evident</h2>
394399
<span class="r">Bundle verification: FAILED (non-zero exit)</span></pre>
395400
</div>
396401
<p>The signature&rsquo;s subject is the digest of <code class="inl">checksums.txt</code>, and
397-
<code class="inl">checksums.txt</code> pins every file it lists. Mutating a listed file
398-
breaks the checksum; mutating <code class="inl">checksums.txt</code> to match breaks the
399-
signature.</p>
402+
<code class="inl">checksums.txt</code> pins every regular payload file. Mutating a listed
403+
file breaks the checksum; mutating or reordering <code class="inl">checksums.txt</code>
404+
breaks the signature. Adding any unlisted file, directory, symlink, or other
405+
non-regular object fails exact-tree verification.</p>
400406
</div>
401407
</section>
402408

demos/bundle-attestation-demo.sh

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,11 @@
2020
# Verify — aicr verify (with default + min-trust-level + tamper paths)
2121
#
2222
# When --attest is passed, the CLI signs `checksums.txt` with SLSA Build
23-
# Provenance v1 via Sigstore keyless OIDC (Fulcio cert + Rekor log entry),
24-
# and copies the binary's own SLSA attestation into the bundle so the
25-
# verifier can walk the full chain back to NVIDIA CI.
23+
# Provenance v1 via Sigstore keyless OIDC (Fulcio cert + Rekor log entry).
24+
# That manifest inventories every regular payload file, including recipe.yaml;
25+
# closed-world verification also rejects any additional filesystem entry. The
26+
# CLI copies its own SLSA attestation into the bundle so the verifier can walk
27+
# the full chain back to NVIDIA CI.
2628
#
2729
# Signing requires Fulcio + Rekor egress and an OIDC source — either ambient
2830
# GitHub Actions OIDC, or an interactive browser sign-in. On hosts where
@@ -201,7 +203,8 @@ fi # end SKIP_ATTEST guard
201203
# --- inspect the bundle -------------------------------------------------------
202204

203205
banner "Inspect the bundle layout"
204-
note "Content files + checksums.txt + attestation/ holding the two signed predicates."
206+
note "Closed-world payload inventory + checksums.txt + the two permitted attestation files."
207+
note "Only checksums.txt and the two attestation JSON files may sit outside the manifest."
205208
run ls -R "$BUNDLE"
206209

207210
banner "Inspect the bundle attestation predicate"
@@ -214,8 +217,10 @@ run bash -c "jq '{ subject_count: (.dsseEnvelope.payload | @base64d | fromjson |
214217
# --- verify -------------------------------------------------------------------
215218

216219
banner "Verify — default (auto-detect maximum trust level)"
217-
note "Five gates: checksums → bundle signature → bundle predicate → binary attestation chain → identity pin."
218-
note "The reported trust level is the highest level every gate passed at."
220+
note "Five gates: closed-world inventory → bundle signature → bundle predicate → binary attestation chain → identity pin."
221+
note "Extra files, directories, symlinks, or other non-regular objects fail verification."
222+
note "Legacy bundles with incomplete manifests report unknown trust and must be regenerated."
223+
note "The trust level reflects the verified chain and is capped at attested when external data was used."
219224
pause "Press Enter to run aicr verify (default)"
220225
run "$AICR" verify "$BUNDLE"
221226

@@ -233,8 +238,9 @@ run bash -c "set -o pipefail; '$AICR' verify '$BUNDLE' --format json | jq '{ tru
233238
# --- tamper -------------------------------------------------------------------
234239

235240
banner "Tamper-evident: mutate a content file, verify fails"
236-
note "The signature's subject is the digest of checksums.txt, which pins every generated payload file, including recipe.yaml."
237-
note "Mutating any file listed in checksums.txt breaks its checksum; mutating checksums.txt breaks the signature."
241+
note "The signature's subject is the digest of checksums.txt, which pins the complete payload inventory, including recipe.yaml."
242+
note "Mutating a listed file breaks its checksum; mutating or reordering checksums.txt breaks the signature."
243+
note "Adding an unlisted filesystem entry also fails the exact-tree check."
238244
# Pick a file the bundle is guaranteed to contain. README.md is always present.
239245
TAMPER_TARGET="$BUNDLE/README.md"
240246
if [ ! -f "$TAMPER_TARGET" ]; then

demos/bundle-attestation.md

Lines changed: 38 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,14 @@
22

33
Bundle attestation provides cryptographic proof of **who** created a deployment
44
bundle and **which AICR CLI** built it. When `aicr bundle --attest` runs, the
5-
CLI signs the bundle's `checksums.txt` (which inventories the deployment
6-
payload, including `recipe.yaml`; the attestation files are verified separately)
7-
using [Sigstore](https://www.sigstore.dev/) and
8-
generates SLSA Build Provenance v1 metadata. Anyone can later verify the
9-
bundle with `aicr verify` to confirm:
10-
11-
* The generated payload files listed in `checksums.txt`, including `recipe.yaml`, haven't been tampered with.
5+
CLI signs the bundle's `checksums.txt` using
6+
[Sigstore](https://www.sigstore.dev/) and generates SLSA Build Provenance v1
7+
metadata. The manifest is a closed-world inventory of the deployment payload,
8+
including `recipe.yaml`; the attestation files are verified separately as part
9+
of the same inventory. Anyone can later verify the bundle with `aicr verify` to
10+
confirm:
11+
12+
* Every required payload file is present and unchanged, and no additional filesystem entry is present.
1213
* It was created by a trusted identity.
1314
* It was built by an attested NVIDIA-CI-released AICR CLI.
1415

@@ -88,8 +89,8 @@ permissions.
8889

8990
```text
9091
my-bundle/
91-
├── checksums.txt # SHA256 of every generated payload file
92-
├── recipe.yaml # canonical post-resolution recipe
92+
├── checksums.txt # closed-world SHA256 payload inventory
93+
├── recipe.yaml # canonical recipe; covered by checksums.txt
9394
├── deploy.sh # automation script
9495
├── README.md # deployment guide
9596
├── 001-<component>/ # per-component folder (NNN-prefixed)
@@ -101,13 +102,19 @@ my-bundle/
101102
└── aicr-attestation.sigstore.json # binary SLSA attestation (copied in)
102103
```
103104

105+
`checksums.txt` covers every regular payload file shown above; the two
106+
attestation files are separately verified metadata outside the manifest. Any
107+
other unlisted file, directory, symlink, or special object fails verification.
108+
See [Artifact Verification](../docs/user/artifact-verification.md#what-can-be-verified)
109+
for the complete inventory contract and manifest-order rules.
110+
104111
The two attestations together form the chain that makes `verified` reachable:
105112

106113
* **`bundle-attestation.sigstore.json`** — Sigstore Bundle (DSSE + Fulcio cert
107114
+ Rekor inclusion proof). Its in-toto subject is the SHA256 of
108-
`checksums.txt`, so signing this one file transitively pins every generated
109-
payload file that `checksums.txt` lists, including `recipe.yaml`. The signer
110-
identity is the creator's OIDC identity.
115+
`checksums.txt`, so signing this one file transitively pins the complete
116+
closed-world payload inventory, including `recipe.yaml`. The signer identity
117+
is the creator's OIDC identity.
111118
* **`aicr-attestation.sigstore.json`** — the SLSA Build Provenance attestation
112119
*of the AICR CLI binary that produced the bundle*, copied in at bundle time.
113120
Its signer identity is NVIDIA CI (`https://github.com/NVIDIA/aicr/.github/workflows/on-tag.yaml@...`).
@@ -142,13 +149,15 @@ Bundle verification: PASSED
142149

143150
Five gates run, top to bottom:
144151

145-
1. **Checksums** — every generated payload file listed in `checksums.txt`, including `recipe.yaml`, is hashed and compared.
152+
1. **Closed-world inventory** — every regular payload file, including `recipe.yaml`, is hashed and compared; additional files, directories, symlinks, and other non-regular objects are rejected.
146153
2. **Bundle signature** — the Sigstore Bundle is verified against the trusted root.
147154
3. **Bundle predicate** — the in-toto subject is checked against the actual `checksums.txt` digest.
148155
4. **Binary attestation chain**`aicr-attestation.sigstore.json` is verified and its subject is checked against the CLI binary digest claimed in the bundle predicate.
149156
5. **Identity pin** — the binary attestation's signer is pinned to NVIDIA CI workflows.
150157

151-
Any gate failing short-circuits to a lower trust level (see table below).
158+
An invalid inventory or failed attestation reports `unknown` trust. An absent
159+
attestation can still reach `unverified` when the closed-world inventory is
160+
valid (see the table below).
152161

153162
## 6. Policy enforcement
154163

@@ -161,10 +170,13 @@ aicr verify ./my-bundle --min-trust-level attested
161170

162171
| Level | Name | Criteria |
163172
|-------|------|----------|
164-
| **4** | `verified` | Checksums + bundle attestation + binary attestation pinned to NVIDIA CI |
165-
| **3** | `attested` | Bundle attestation verified; binary attestation missing/unverified, or external data used. A *failed* binary attestation also reports attested but exits nonzero (#1550) |
166-
| **2** | `unverified` | Checksums valid, `--attest` was not used |
167-
| **1** | `unknown` | Missing/invalid `checksums.txt`, or bundle attestation fails verification |
173+
| **4** | `verified` | Closed-world inventory + bundle attestation + binary attestation pinned to NVIDIA CI |
174+
| **3** | `attested` | Closed-world inventory + bundle attestation verified; binary attestation missing, or external data used |
175+
| **2** | `unverified` | Closed-world checksum inventory valid; `--attest` was not used |
176+
| **1** | `unknown` | Missing, invalid, or incomplete `checksums.txt`; unexpected filesystem entries; or bundle/binary attestation verification failure |
177+
178+
Legacy bundles with incomplete manifests report `unknown` trust and must be
179+
regenerated before deployment.
168180

169181
Pick the floor per environment. Production bundles must be `verified`; an
170182
emergency hotfix bundle built off-CI might only be required to reach
@@ -207,8 +219,8 @@ aicr verify ./my-bundle --format json | jq '{ trustLevel, bundleCreator, toolVer
207219
Branching in a pipeline:
208220

209221
```shell
210-
# Capture the JSON and the verify exit code separately: a binary-attestation
211-
# that *fails* verification still reports trustLevel=attested but exits nonzero.
222+
# Capture the JSON and the verify exit code separately: failed integrity or
223+
# attestation reports trustLevel=unknown and exits nonzero.
212224
if out=$(aicr verify ./my-bundle --format json); then rc=0; else rc=$?; fi
213225
trust=$(printf '%s' "$out" | jq -r .trustLevel)
214226
if [ "$rc" -ne 0 ]; then echo "fail — verify exited $rc" ; exit 1 ; fi
@@ -221,8 +233,8 @@ esac
221233

222234
## 8. Tamper demo
223235

224-
The signed manifest hash pins every file listed in `checksums.txt`. Mutating
225-
a listed file breaks verification:
236+
The signed manifest hash pins every regular payload file in the closed-world
237+
inventory. Mutating a listed file breaks verification:
226238

227239
```shell
228240
# Component dirs are numbered NNN-<component>/; tamper the first one's values
@@ -241,6 +253,10 @@ Editing `checksums.txt` to match the new hash defeats the checksum gate but
241253
breaks the bundle signature gate — the signed subject is the digest of
242254
`checksums.txt` itself, which now doesn't match the signed value.
243255

256+
Adding an unlisted file, directory, symlink, or other non-regular object also
257+
fails `aicr verify`: exact-tree validation rejects entries outside the verified
258+
inventory before any deployment gate can pass.
259+
244260
## Troubleshooting
245261

246262
**"sigstore verification failed — trusted root may be stale"** — Sigstore

0 commit comments

Comments
 (0)