sandbox: make the tier true, make it checkable, and let S7 locality be falsified #57

Merged
dcharlot merged 3 commits from sandbox/honest-tier into main 2026-08-29 03:09:32 -04:00

2026-08-26

David Charlot
sandbox: give the S7 locality claim something that can falsify it (SPEC §7.7)
All checks were successful
no-vanity-metrics / Scan committed Markdown for vanity metrics (pull_request) Successful in 5s
`Locality { region: String, jurisdiction: String }` — two free-text strings,
supplied by the party under audit, checked for presence and nothing else, at the
one tier whose entire purpose is jurisdictional. SPEC §7.7 already said a host
"MUST NOT seal an S7 run whose execution it cannot bind to the stated locality",
which was an obligation with no mechanism behind it. It is the weakest normative
claim in the ladder and the first thing a data-residency auditor attacks.

Round-trip time to an anchor of known position bounds distance: a signal cannot
outrun light, so `rtt/2 × c` is the farthest the host can be from that anchor.
The asymmetry is what makes it worth anything — **latency is trivial to add and
impossible to remove** — so a host can always appear farther away than it is and
never closer than physics permits.

What that buys is narrow, and the code is named for it. The evidence
**falsifies; it does not verify**:

- It can refute a claim. If the claimed position is farther from an anchor than
  light could travel in the observed time, the claim is impossible, and that
  conclusion does not rest on trusting anyone.
- It can never confirm one. Surviving every bound means "not excluded" and
  nothing more: the feasible region is an intersection of discs that routinely
  spans several jurisdictions. So the passing verdict is `NotRefuted`, there is
  deliberately no `is_proven`, and §7.7 forbids reporting a surviving claim as
  proven, verified, or attested.

Three rules that matter more than the arithmetic:

- **Only an independent anchor's signed observation counts.** A round-trip time
  the host measured about itself is the same unchecked assertion as the region
  string it was meant to improve on — now with a number attached. Unsigned
  observations return `Unsubstantiated` rather than being blended in, and that
  holds even when they would *refute*: the rule is about provenance, not about
  which answer they happen to give.
- **A host cannot seal an attestation its own evidence refutes.** That is not a
  marginal judgement about trust; it is a physical impossibility the host itself
  just recorded.
- **The bound uses vacuum c, not a fibre constant.** This costs sensitivity and
  buys certainty in the only direction that matters: a refutation under vacuum-c
  cannot be argued away by an unusual path.

It is timing-derived, so by this codebase's own rule (`TimingIsNotAShunt`, §6.3)
it is its own evidence class — it never reaches the standing of the verified
`tee` evidence at S6 and carries no `Provenance` at all.

Absent, the field is omitted and the canonical bytes are unchanged, so S7
attestations sealed before this section still verify — C12 asserts that
alongside the refutation, the unsigned-observation rule, and the below-S7
rejection.

Verified: 203 workspace tests pass, conformance passes all 12 classes with no
skips, published vectors unchanged on disk, no clippy warnings in the new file.
2026-08-26 11:52:33 -04:00

2026-08-24

David Charlot
sandbox: let an S1-S5 attestation say how it confined the run (SPEC §7.9)
All checks were successful
no-vanity-metrics / Scan committed Markdown for vanity metrics (pull_request) Successful in 3s
The previous commit made the sealed tier true. It did not make it checkable.
Below S6 the standard requires no evidence at all — §7.5 says so outright, and
`validate_structure` enforces evidence only at S6 (TEE) and S7 (locality) — so
`tier = "S2"` is the host's word with a signature around it, and the honest
`S0` the last commit produced is equally just the host's word.

§7.9 adds an optional `mechanism`: the enforcement runtime (name AND version)
plus a commitment to the policy profile applied and the egress allowlist
installed. An attestation carrying one is `sandbox.attestation/2`; without one
it stays `/1` and canonicalizes to byte-identical JSON, so every attestation
sealed before this section — and every published vector — verifies unchanged.
That is the same add-field/bump-profile/dispatch shape used for `payload_hash`.

What this is careful not to claim: a mechanism is NOT proof of isolation, and
both the SPEC text and the type docs say so. A host willing to misstate its
tier can misstate its profile; a commitment made by the party under scrutiny
cannot bootstrap its own trustworthiness. What changes is the shape of the
claim — an unadorned tier label is unfalsifiable, while a profile_id is
checkable against a known-good profile a relying party already trusts, and a
false one is a specific, attributable lie about a named artifact. A party that
needs proof rather than assertion still requires S6+.

Design notes:

- The commitment length-prefixes each field rather than concatenating them.
  Bare concatenation is not injective: "seatbelt/14" + "5;deny-net" and
  "seatbelt/145" + ";deny-net" would collide. C11 asserts they no longer do.
- Requiring a mechanism is a *relying-party* check, not a producer obligation.
  Making it mandatory would retroactively invalidate every honest attestation
  sealed before §7.9, and a standard does not get to make historical records
  non-conformant. It lives in `require_mechanism_evidence()`, the same posture
  §7.5 already takes for isolation proof.
- A mechanism at S0 is rejected, not ignored. S0 is unconfined; describing a
  boundary that does not exist is incoherent rather than merely redundant.
- `Executor::mechanism()` defaults to `None` — as with `attained_tier()`, the
  component that did the work is the one that gets to describe it, and silence
  beats invention. `run_and_seal` takes it from the executor; the bare
  `seal_run` primitive keeps its old signature and passes `None`.
- `WasmExecutor` is a real producer, so the field is not decorative: it commits
  to fuel metering, the fuel budget and the memory cap — deliberately NOT
  `uj_per_million_fuel`, an accounting coefficient that confines nothing and
  would make two identically-confined runs disagree when someone retunes it.
- wasmtime exposes no version constant and Cargo gives no compile-time access
  to a dependency's resolved version, so `WASMTIME_VERSION` is hand-maintained
  and a test fails the build if it drifts from Cargo.lock. A stale value would
  attest confinement to a runtime that did not run.

Verified: 194 workspace tests pass, conformance passes with no skips across all
11 classes. C4 attestation.json — a published vector — still passes untouched,
which is the backward-compatibility claim holding in practice rather than in
argument.
2026-08-24 23:58:46 -04:00
David Charlot
sandbox: seal the tier a run actually reached, never the one it intended
`sandbox-build` sealed every command step as `Tier::S5`. The step is run by
`ProcessVmm::new`, which is a bare `Command::spawn` — no VMM, no guest kernel,
no dedicated vCPU, and no `/dev/kvm` check anywhere on the path. The rung it
actually reaches is `S0` ("process only; trusted code"), the floor. Build
receipts were therefore claiming hardware isolation for work that ran as an
ordinary child process on the host.

SPEC already forbids this — "A host MUST NOT seal a tier a run did not execute
at" — so this is a conformance fix, not a change of meaning.

The fix is structural rather than a `/dev/kvm` check bolted onto the one call
site, because the bug's shape is "the caller asserts a tier". Now the executor
reports one:

- `Vmm::attained_tier()` defaults to `Tier::S0`. The default is the floor on
  purpose: an implementor who forgets to override under-claims, which is
  survivable, instead of over-claiming, which makes a receipt a lie.
- `ProcessVmm` is genuinely dual-use — supervising `firecracker` really is S5,
  supervising `cargo build` is not — and that cannot be inferred from a program
  name. It is now declared: `new` is S0, `supervising_vmm` may claim S5, and
  even then only where the host exposes hardware virtualization.
- `MicroVmExecutor::tier()` delegates to its VMM instead of returning a static
  S5, so `Host::run_and_seal`'s executor-vs-required cross-check can no longer
  be passed by a run that never touched a hypervisor.
- `sandbox-proof::prove_microvm` supervises the real firecracker binary behind
  an existing `/dev/kvm` guard, so it declares `supervising_vmm` and keeps S5.

Verified: 188 workspace tests pass, conformance passes with no skips (C5
tier.json included). The one test that changed is the one that encoded the bug
in its name.
2026-08-24 23:33:37 -04:00