From 477b4fc379e65c12af957f88e5a15aa4c610a6aa Mon Sep 17 00:00:00 2001 From: Ralf Anton Beier Date: Mon, 7 Sep 2026 14:42:31 +0200 Subject: [PATCH 1/2] =?UTF-8?q?guard:=20varve=20rotated=20the=20rolling=20?= =?UTF-8?q?trust=20root=20=E2=80=94=20taking=20the=20new=20realms=20file?= =?UTF-8?q?=20would=20orphan=20our=20pin,=20and=20the=20diff=20looks=20lik?= =?UTF-8?q?e=20a=20no-op=20(AFD-112)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit varve v0.32.1 is NOT the consumer-API release we asked for on varve#130. Its own notes: "There is no code change from v0.32.0 — no behaviour, no CLI, no format." It exists to publish a rotated rolling trust root, 4e771dc6... -> 7d3b892e... Our correct action is to do nothing, and that is varve's own instruction. Layers 2026.08.0..2026.09.1 are signed by the OLD root; 2026.09.2 will be the first signed by the new one. We pin 2026.08.4 and carry the OLD root — the correct pairing. Verified rather than assumed: `varve verify` reports "layer 2026.08.4 ... verified: signature OK, 11 tool(s) match their signed digests". It exits 1 only on PATH shadowing, which is the advisory class AFD-111 just separated out — the two checkers agree. Also ran varve's own falsification test from the release notes: the published rolling.pub and varve-realms.toml name the same root. AGREE. THE HAZARD. The published varve-realms.toml is byte-identical to ours except the trust-root line — measured, the diff is empty once that line is masked. So "vendor the new realms file" reads in review as a one-line no-op and produces: error: manifest signature verification failed: ... No valid signatures This repo runs an autonomous 4h loop that bumps pins on evidence; it bumped rivet and spar to the pin earlier today. A rule that lives only in a release note is a rule that loop will not see. tools/varve/check-realm.sh makes it executable and refuses a realm/pin pairing whose root cannot verify the pinned layer, in EITHER direction, plus an unrecognised root. --self-test runs five rows and BOTH failure modes are observed failing, including the trap itself. The layer comparison is numeric, not lexical: 2026.09.10 must sort ABOVE the 2026.09.2 boundary, and a string compare puts it below. varve-core 0.32.1 against our four asks — two landed: (a) held-payload kind + locate: LANDED. kind::PayloadKind (8 variants, is_dispatchable() true only for Tool, UnknownKind for forward-compat). Locate exists at library level via store::payload_rel_path and Store::find_anywhere, reaching kinds the CLI has no export verb for. (b) the crate can say `verified` at all: LANDED. reverify::verify_installed() re-verifies an installed layer against the trust root, and verify::PinnedKeyVerifier is a public constructible ManifestVerifier. So a reader can make a real verified claim instead of the weaker sentence we promised on varve#130. (c) crate/format version self-report: NOT landed. No pub fn version, no FORMAT_VERSION. (d) query by type/field: NOT landed beyond discover::find_pin. AND THE CATALOGUE-READER PREMISE IS STILL NOT MET — checked, not assumed. No layer carries a hardware-catalogue payload. The pinned layer's only held payloads are rivet-sdlc/0.34.0 and spar-aadl/0.40.0, single .crate files we consume neither of. Building the reader now would be building against nothing — the same stale-premise error as AFD-107, caught this time before writing code rather than after. Co-Authored-By: Claude Opus 4.8 --- .github/workflows/ci.yml | 9 +++++ artifacts/findings.yaml | 69 ++++++++++++++++++++++++++++++++ tools/varve/check-realm.sh | 80 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 158 insertions(+) create mode 100755 tools/varve/check-realm.sh diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bd081e4..ce3c878 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -127,6 +127,15 @@ jobs: sh -c 'exit 3' | cat || rc=$? [ "$rc" = "3" ] || { echo "pipefail is NOT active: a failing left-hand side reported $rc — piped gates can report green while failing"; exit 1; } echo "pipefail active: a failing left-hand side propagates (rc=$rc)" + # varve's rolling trust root rotated in v0.32.1. The published varve-realms.toml is + # byte-identical to ours EXCEPT the trust-root line, so vendoring it reads as a + # one-line no-op diff and would orphan our pin (`No valid signatures`) until layer + # 2026.09.2 exists. This loop bumps pins on evidence; a rule that lives only in a + # release note is one it will not see. + - name: varve realm/pin agree (trust root can verify the pinned layer) + run: | + tools/varve/check-realm.sh --self-test + tools/varve/check-realm.sh - name: rivet sync + validate working-directory: jess run: | diff --git a/artifacts/findings.yaml b/artifacts/findings.yaml index 7919819..6d7f227 100644 --- a/artifacts/findings.yaml +++ b/artifacts/findings.yaml @@ -6261,3 +6261,72 @@ artifacts: detected-by: clean-room verification of AFD-109/AFD-110 plus a retrospective check-drift run, 2026-09-07 severity: critical triage-status: confirmed + + - id: AFD-112 + type: ai-found-defect + title: "varve v0.32.1 rotated the rolling trust root and the correct consumer action is DO NOTHING — but the new realms file differs from ours by one line, so taking it reads as a no-op diff and would orphan the pin; guarded" + status: open + description: |- + 2026-09-07. varve v0.32.1 (published 11:36) is NOT the consumer-API release jess asked for on + varve#130. Its own notes: "There is no code change from v0.32.0 — no behaviour, no CLI, no + format." It exists to publish a rotated rolling trust root, 4e771dc6... -> 7d3b892e... + + *** JESS'S CORRECT ACTION IS TO DO NOTHING, AND THAT IS VARVE'S OWN INSTRUCTION *** + Layers 2026.08.0 .. 2026.09.1 are signed by the OLD root; 2026.09.2 will be the first signed + by the new one. jess pins layer 2026.08.4 and carries the OLD root, which is the CORRECT + pairing. Verified rather than assumed: `varve verify` reports "layer 2026.08.4 ... verified: + signature OK, 11 tool(s) match their signed digests". (It exits 1 only on REQ-SHADOW-001 PATH + shadowing of 7 tools — the ADVISORY class AFD-111 just separated from blocking drift, so the + two checkers agree.) + Ran varve's own falsification test from the release notes: the published rolling.pub and the + published varve-realms.toml name the same root — AGREE. + + *** THE HAZARD, AND WHY IT WARRANTS EXECUTABLE GUARDING *** + The published varve-realms.toml is BYTE-IDENTICAL to jess's except the trust-root line + (measured: diff is empty once that line is masked). So "vendor the new realms file" reads in + review as a one-line no-op, and produces: + error: manifest signature verification failed: ... No valid signatures + This repo runs an autonomous 4h loop that bumps pins on evidence — it bumped rivet and spar to + the pin in AFD-111 the same day. A rule that exists only in a release note is a rule that loop + will not see. tools/varve/check-realm.sh makes it executable: it refuses a realm/pin pairing + whose root cannot verify the pinned layer, in EITHER direction, plus an unrecognised root. + --self-test exercises five rows and BOTH failure modes are OBSERVED failing, including the + trap itself. The layer comparison is numeric, not lexical: 2026.09.10 must sort ABOVE the + 2026.09.2 boundary and a string compare puts it below. + + *** varve-core 0.32.1 AGAINST JESS'S FOUR ASKS: TWO LANDED *** + (a) generic held-payload kind + locate -> LANDED. `kind::PayloadKind` has 8 variants + (Tool/Crate/Wit/ZephyrModule/Sdk/WasmComponent/Vsix/Layer), `is_dispatchable()` true only + for Tool, and `UnknownKind` for forward compatibility. Its doc restates jess's own + argument: "Every other kind is held, not dispatched — its identity is (name, version, + platform)". Locate exists at the LIBRARY level via `store::payload_rel_path` and + `Store::find_anywhere`, which reach kinds the CLI has no export verb for (there are + export-cargo/-vsix/-bazel/-crates-vendor/-bazel-distdir, but none for wit/sdk/zephyr). + That is exactly the "consume the crate instead of parsing a command" the operator asked + for. + (b) the crate must be able to say `verified` at ALL -> LANDED. `reverify::verify_installed( + store, layer, verifier, platform) -> Result` re-verifies an + installed layer against the trust root and returns the number of binaries checked, and + `verify::PinnedKeyVerifier` is a PUBLIC constructible impl of the `ManifestVerifier` + trait. So a jess reader can make a real verified claim instead of the weaker sentence + jess promised on varve#130 ("layer resolved, not re-verified at read time"). + (c) crate/format version self-report -> NOT LANDED. No `pub fn version`, no FORMAT_VERSION + constant; the only version is the Cargo package version, which a consumer reads for its + OWN build, not for the crate's contract. This was the operator's framing: "with device + version x and varve crate y, and if varve changes to z we have one way to detect". + (d) query by type/field -> NOT LANDED beyond `discover::find_pin`. + + *** THE CATALOGUE-READER PREMISE IS STILL NOT MET — CHECKED, NOT ASSUMED *** + The plan recorded on varve#130 was to build jess's catalogue reader against `resolve()`. No + layer carries a hardware-catalogue payload. The pinned layer's only held payloads are + `rivet-sdlc/0.34.0` (966,866 B) and `spar-aadl/0.40.0` (4,673,917 B), single .crate files that + jess consumes NEITHER of today. Building "consume the catalogue from varve" now would be + building against nothing — the same stale-premise error as AFD-107, checked this time before + writing code rather than after. + Incidental confirmation: those held payload versions are spar 0.40.0 and rivet 0.34.0, the + exact versions AFD-111 aligned CI to. + tags: [varve, trust-root, supply-chain, varve-core, premise-check, afd-111, varve-130] + fields: + detected-by: assessing varve v0.32.1 and varve-core 0.32.1 against jess's four consumer asks, 2026-09-07 + severity: major + triage-status: confirmed diff --git a/tools/varve/check-realm.sh b/tools/varve/check-realm.sh new file mode 100755 index 0000000..b23a086 --- /dev/null +++ b/tools/varve/check-realm.sh @@ -0,0 +1,80 @@ +#!/usr/bin/env bash +# Refuse the ONE combination varve v0.32.1's rotation makes silently fatal: +# a trust root that cannot verify the layer this project is pinned to. +# +# WHY THIS EXISTS. On 2026-09-07 varve rotated the rolling trust root +# (4e771dc6... -> 7d3b892e...). Layers 2026.08.0 .. 2026.09.1 are signed by the OLD +# root; 2026.09.2 will be the first signed by the NEW one. varve's own guidance to a +# consumer pinned below that boundary is: DO NOTHING. +# +# The hazard is that doing the wrong thing looks trivial. The published +# varve-realms.toml is BYTE-IDENTICAL to the one in this repo except for the +# trust-root line — verified, `diff` is empty once that line is masked. So "vendor the +# new realms file" reads as a one-line no-op diff in review, and produces: +# +# error: manifest signature verification failed: ... No valid signatures +# +# This repo runs an autonomous loop that bumps pins on evidence. A rule that lives only +# in a release note is a rule the loop will not see. This is that rule, executable. +set -uo pipefail +ROOT="$(cd "$(dirname "$0")/../.." && pwd -P)" +REALMS="${REALMS:-$ROOT/varve-realms.toml}" +PIN="${PIN:-$ROOT/varve.toml}" + +OLD_ROOT=4e771dc62a08be89e3450f8cd807da58ff70af4a4e124ebf2d2b71684cfd9973 +NEW_ROOT=7d3b892e6a33c70043becc708e08042e1cef0d54dd5ae6f23d7d4c68de1da1a0 +# The boundary is a varve FACT, not a jess preference: the first layer signed by the +# new root. Layers sort lexically here because they are zero-padded YYYY.MM.N. +FIRST_NEW_ROOT_LAYER=2026.09.2 + +verdict() { # $1=root $2=layer -> prints OK/FAIL reason, returns 0/1 + local root="$1" layer="$2" era + if [ "$root" = "$OLD_ROOT" ]; then era=old + elif [ "$root" = "$NEW_ROOT" ]; then era=new + else echo "UNKNOWN trust root $root — neither the pre- nor post-rotation value"; return 1; fi + # String compare is wrong across a component boundary (2026.09.10 vs 2026.09.2), so + # compare the numeric triple. + local a b; a=$(printf '%s' "$layer" | awk -F. '{printf "%04d%02d%03d",$1,$2,$3}') + b=$(printf '%s' "$FIRST_NEW_ROOT_LAYER" | awk -F. '{printf "%04d%02d%03d",$1,$2,$3}') + if [ "$a" -ge "$b" ]; then + [ "$era" = new ] && { echo "OK: layer $layer is post-rotation and the realm carries the NEW root"; return 0; } + echo "FAIL: layer $layer is signed by the NEW root, but this realm carries the OLD one — it cannot verify"; return 1 + else + [ "$era" = old ] && { echo "OK: layer $layer is pre-rotation and the realm carries the OLD root"; return 0; } + echo "FAIL: this realm carries the NEW trust root, but layer $layer was signed by the OLD one. + varve: 'Every layer published from 2026.08.0 through 2026.09.1 was signed by the old + root and does not verify against the new one.' Taking the realms file WITHOUT moving + the pin to >= $FIRST_NEW_ROOT_LAYER leaves this project pinned to a layer its own realm + cannot verify. Until that layer exists, the correct action is to do NOTHING."; return 1 + fi +} + +if [ "${1:-}" = "--self-test" ]; then + ok=0 + # Every row must be OBSERVED to give its stated verdict — including the two failures. + # A guard whose failing cases were never executed is the vacuity this campaign keeps + # finding in checkers rather than in code. + for row in "$OLD_ROOT|2026.08.4|0|pre-rotation pin, old root (jess today)" \ + "$NEW_ROOT|2026.09.2|0|post-rotation pin, new root" \ + "$NEW_ROOT|2026.08.4|1|THE TRAP: new realms file, pin not moved" \ + "$OLD_ROOT|2026.09.2|1|pin moved past the boundary, realm not updated" \ + "deadbeef|2026.08.4|1|unrecognised root"; do + IFS='|' read -r r l want label < %s\n" "$label" "$(echo "$out" | head -1)" + else ok=1; printf " [FAIL] %-42s want rc=%s got rc=%s\n" "$label" "$want" "$got"; fi + done + echo "SELF-TEST: $([ $ok = 0 ] && echo PASS || echo FAIL)"; exit $ok +fi + +root=$(sed -n 's/^trust-root = "\(.*\)"/\1/p' "$REALMS" | head -1) +layer=$(sed -n 's/^layer[[:space:]]*=[[:space:]]*"\(.*\)"/\1/p' "$PIN" | head -1) +[ -n "$root" ] || { echo "no trust-root in $REALMS" >&2; exit 2; } +[ -n "$layer" ] || { echo "no layer in $PIN" >&2; exit 2; } +echo "realm trust-root: $root" +echo "pinned layer: $layer" +out=$(verdict "$root" "$layer"); rc=$? +echo "$out" +exit $rc From 4a673483edc093c05f2e27cb55a5a2a3644a49c0 Mon Sep 17 00:00:00 2001 From: Ralf Anton Beier Date: Mon, 7 Sep 2026 14:51:25 +0200 Subject: [PATCH 2/2] =?UTF-8?q?fix:=20the=20realm=20guard=20needs=20workin?= =?UTF-8?q?g-directory:=20jess=20=E2=80=94=20this=20job=20checks=20out=20t?= =?UTF-8?q?o=20a=20subdirectory?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The step ran at the workspace root and exited 127 with 'No such file or directory', which reads like a missing script rather than a wrong cwd. CI caught it, which is what the gate is for. Co-Authored-By: Claude Opus 4.8 --- .github/workflows/ci.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ce3c878..85afa59 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -133,6 +133,10 @@ jobs: # 2026.09.2 exists. This loop bumps pins on evidence; a rule that lives only in a # release note is one it will not see. - name: varve realm/pin agree (trust root can verify the pinned layer) + # This job checks out to `path: jess`, so every step that touches the repo needs + # working-directory. Omitting it cost a red run: exit 127, "No such file or + # directory", which reads like a missing script rather than a wrong cwd. + working-directory: jess run: | tools/varve/check-realm.sh --self-test tools/varve/check-realm.sh