From 31e3cfa8d1f67a7068cced9495d5efc4f70880ef Mon Sep 17 00:00:00 2001 From: Transaction Science Date: Wed, 21 Oct 2026 20:30:24 -0400 Subject: [PATCH] sandbox-conformance: hold the committed pack to what the builders emit MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The file-backed checks prove each committed vector file is valid: the reference recomputes its content ids, envelopes and signatures. Nothing proved the files were the current output of the builders. When Attestation gained `mechanism`, its serde form started emitting `"mechanism": null`, so gen_vectors and the published attestation.json and negative.json parted. Every check stayed green, because a missing field defaults on read. - sandbox-attest: an absent `mechanism` is omitted from the serialized form, as it is from the signed JCS core (§7.9). The published pack is then exactly what the builders emit, and its bytes do not change. The signed core was never affected; it is built by hand. - sandbox-conformance: - vectors::rendered() lists every committed file with the exact bytes gen_vectors writes. gen_vectors now writes from it. - New test the_committed_pack_is_what_the_builders_emit: the rendered set is FILES, and each file is byte-identical to the committed one. Mutation testing: 2 mutants, both killed (`mechanism` serialized as null again; a file left out of the rendered set). Verified on the build host: sandbox-rs 435 tests (--locked); every conformance class; running gen_vectors changes nothing in git; clippy -D warnings clean on sandbox-conformance and sandbox-attest. Repo checks pass. Co-Authored-By: Claude Opus 5.5 --- .../crates/sandbox-attest/src/lib.rs | 4 +- .../src/bin/gen_vectors.rs | 46 ++----------------- .../crates/sandbox-conformance/src/lib.rs | 19 ++++++++ .../crates/sandbox-conformance/src/vectors.rs | 19 ++++++++ 4 files changed, 44 insertions(+), 44 deletions(-) diff --git a/sandbox/sandbox-rs/crates/sandbox-attest/src/lib.rs b/sandbox/sandbox-rs/crates/sandbox-attest/src/lib.rs index b1b21c507..9cc2780b4 100644 --- a/sandbox/sandbox-rs/crates/sandbox-attest/src/lib.rs +++ b/sandbox/sandbox-rs/crates/sandbox-attest/src/lib.rs @@ -258,7 +258,9 @@ pub struct Attestation { pub audit_prev: Option, /// How confinement was established (SPEC §7.9). Meaningful at S1–S5, where /// the tier is otherwise host-asserted with no in-band evidence; MUST be - /// absent at S0, which is unconfined by definition. + /// absent at S0, which is unconfined by definition. Omitted from the + /// serialized form when absent, as from the signed core (§7.9). + #[serde(default, skip_serializing_if = "Option::is_none")] pub mechanism: Option, /// Evidence bounding where the run could **not** have executed (SPEC §7.7). /// diff --git a/sandbox/sandbox-rs/crates/sandbox-conformance/src/bin/gen_vectors.rs b/sandbox/sandbox-rs/crates/sandbox-conformance/src/bin/gen_vectors.rs index c5f94db58..a72d122a3 100644 --- a/sandbox/sandbox-rs/crates/sandbox-conformance/src/bin/gen_vectors.rs +++ b/sandbox/sandbox-rs/crates/sandbox-conformance/src/bin/gen_vectors.rs @@ -12,51 +12,11 @@ use sandbox_conformance::vectors; fn main() -> std::io::Result<()> { let dir = vectors::vectors_dir(); std::fs::create_dir_all(&dir)?; - - let write = |name: &str, json: String| -> std::io::Result<()> { + for (name, bytes) in vectors::rendered() { let path = dir.join(name); - std::fs::write(&path, json + "\n")?; + std::fs::write(&path, bytes)?; println!("wrote {}", path.display()); - Ok(()) - }; - - write( - "content_addressing.json", - serde_json::to_string_pretty(&vectors::content_vectors()).unwrap(), - )?; - write( - "capability.json", - serde_json::to_string_pretty(&vectors::capability_vectors()).unwrap(), - )?; - write( - "receipt.json", - serde_json::to_string_pretty(&vectors::receipt_vector()).unwrap(), - )?; - write( - "attestation.json", - serde_json::to_string_pretty(&vectors::attestation_vector()).unwrap(), - )?; - write( - "tier.json", - serde_json::to_string_pretty(&vectors::tier_vectors()).unwrap(), - )?; - write( - "durable.json", - serde_json::to_string_pretty(&vectors::durable_vector()).unwrap(), - )?; - write( - "negative.json", - serde_json::to_string_pretty(&vectors::negative_pack()).unwrap(), - )?; - write( - "confidential.json", - serde_json::to_string_pretty(&vectors::confidential_pack()).unwrap(), - )?; - write( - "flow.json", - serde_json::to_string_pretty(&vectors::flow_vectors()).unwrap(), - )?; - + } println!("\nRegenerated conformance vectors. Verify with `cargo test -p sandbox-conformance`."); Ok(()) } diff --git a/sandbox/sandbox-rs/crates/sandbox-conformance/src/lib.rs b/sandbox/sandbox-rs/crates/sandbox-conformance/src/lib.rs index a56ebb02f..7f0cd372f 100644 --- a/sandbox/sandbox-rs/crates/sandbox-conformance/src/lib.rs +++ b/sandbox/sandbox-rs/crates/sandbox-conformance/src/lib.rs @@ -714,6 +714,25 @@ mod tests { assert_eq!(report.results.len(), 13); } + /// The committed pack is exactly what the builders emit, file for file + /// and byte for byte. The file-backed checks prove each committed file is + /// valid; this proves it is the current one. Without it, a change to a + /// builder or to a type's serialized shape left the published pack behind, + /// and nothing noticed: `"mechanism": null` sat unpublished in the + /// attestation and negative packs for a release. + #[test] + fn the_committed_pack_is_what_the_builders_emit() { + use vectors::*; + let rendered = rendered(); + let names: Vec<&str> = rendered.iter().map(|(n, _)| *n).collect(); + let listed: Vec<&str> = FILES.iter().map(|(n, _)| *n).collect(); + assert_eq!(names, listed, "the rendered set is the listed set"); + for (name, bytes) in rendered { + let committed = std::fs::read_to_string(vectors_dir().join(name)).unwrap(); + assert!(committed == bytes, "{name} differs from what the builders emit: run gen_vectors and commit the result"); + } + } + #[test] fn vector_builders_round_trip_in_memory() { // The reference's own vector builders must satisfy their own checkers diff --git a/sandbox/sandbox-rs/crates/sandbox-conformance/src/vectors.rs b/sandbox/sandbox-rs/crates/sandbox-conformance/src/vectors.rs index ac0e383b9..95ce0b1dd 100644 --- a/sandbox/sandbox-rs/crates/sandbox-conformance/src/vectors.rs +++ b/sandbox/sandbox-rs/crates/sandbox-conformance/src/vectors.rs @@ -997,6 +997,25 @@ pub fn vectors_dir() -> std::path::PathBuf { .join("../../../conformance/v1") } +/// Every committed vector file, by name, as the `gen_vectors` binary writes +/// it: the pretty JSON of its builder and a final newline. A test holds the +/// committed pack to this, so the builders and the published files cannot +/// part. +pub fn rendered() -> Vec<(&'static str, String)> { + let pretty = |v: serde_json::Result| v.expect("a vector serializes") + "\n"; + vec![ + ("content_addressing.json", pretty(serde_json::to_string_pretty(&content_vectors()))), + ("capability.json", pretty(serde_json::to_string_pretty(&capability_vectors()))), + ("receipt.json", pretty(serde_json::to_string_pretty(&receipt_vector()))), + ("attestation.json", pretty(serde_json::to_string_pretty(&attestation_vector()))), + ("tier.json", pretty(serde_json::to_string_pretty(&tier_vectors()))), + ("durable.json", pretty(serde_json::to_string_pretty(&durable_vector()))), + ("negative.json", pretty(serde_json::to_string_pretty(&negative_pack()))), + ("confidential.json", pretty(serde_json::to_string_pretty(&confidential_pack()))), + ("flow.json", pretty(serde_json::to_string_pretty(&flow_vectors()))), + ] +} + pub const FILES: [(&str, &str); 9] = [ ("content_addressing.json", "C1"), ("capability.json", "C2"), -- 2.54.0 (Apple Git-157)