Some checks failed
standards workspaces build + test / jouleclaw-rs (push) Has been cancelled
standards workspaces build + test / aac-rs (push) Has been cancelled
standards workspaces build + test / arl-rs (push) Has been cancelled
standards workspaces build + test / eoc-rs (push) Has been cancelled
standards workspaces build + test / jcp-rs (push) Has been cancelled
standards workspaces build + test / joule-code-rs (push) Has been cancelled
standards workspaces build + test / joule-ui-rs (push) Has been cancelled
standards workspaces build + test / joulecontract-rs (push) Has been cancelled
standards workspaces build + test / jouledesk-rs (push) Has been cancelled
standards workspaces build + test / joulehook-rs (push) Has been cancelled
standards workspaces build + test / map-rs (push) Has been cancelled
standards workspaces build + test / mesh-rs (push) Has been cancelled
standards workspaces build + test / openpay (push) Has been cancelled
standards workspaces build + test / proof-rs (push) Has been cancelled
standards workspaces build + test / sandbox-rs (push) Has been cancelled
standards workspaces build + test / sense-rs (push) Has been cancelled
standards workspaces build + test / smart-byte-rs (push) Has been cancelled
gen-vectors writes proof/conformance/v1/ and nothing read it back -- the crate's
soundness.rs does no file IO -- so the published receipts were never checked
against the implementation that claims to produce them.
The gap is measured. Changing the steward the generator seals under
("did:example:proof-reference" -> "did:example:proof-DRIFTED"), an identity change
in published signed artifacts, leaves soundness.rs green AND the main() harness
reporting "all checks passed". The new drift test is the only thing that notices.
tests/pack.rs checks three separable things:
- Drift: the generator still emits exactly the published files, run into a temp
directory via CARGO_BIN_EXE_gen-vectors so the pack is never touched.
- Signatures, independently: each of the four envelopes is verified from the
pack and RFC 9052 alone, with a hand-built CBOR encoder rather than the
crate's own COSE code -- what a cross-language verifier would have to do --
plus a tampered core and a stranger's key as negatives.
- The cascade's accounting, recomputed across tiers rather than read back one
at a time. Two properties the receipts assert together and none states alone:
the waste recorded at a tier is exactly what the tiers before it consumed
(0, 50, 5050, 505050 against totals 50, 5050, 505050, 50505050), and
escalating all the way to T3 costs MORE than the 50 mJ baseline. The cascade
saves by closing early, not by existing, and that honest limit is now pinned
so it cannot be quietly re-described.
Falsified both ways and restored: breaking T2's wasted_uj in the pack fails the
accounting test naming T2, and the steward change above fails the drift test.
The pack is current -- regenerating into a scratch directory matched both files
byte for byte, so unlike joulehook's and jouledesk's nothing here is stale.
Every vector records closed_by_stub = 1: these verdicts were closed by stand-ins,
and the pack says so rather than hiding it. The test asserts that marking stays.
Co-Authored-By: Claude Opus 5 <[email protected]>
239 lines
9.4 KiB
Rust
239 lines
9.4 KiB
Rust
//! Consume the published `proof.receipt/1` conformance pack.
|
|
//!
|
|
//! `gen-vectors` writes `proof/conformance/v1/` and nothing read it back — the
|
|
//! crate's `soundness.rs` does no file IO, so the published receipts were never
|
|
//! checked against the implementation that claims to produce them.
|
|
//!
|
|
//! Three things are checked here, deliberately separated:
|
|
//!
|
|
//! 1. **Drift** — the generator still emits exactly the published files, run into a
|
|
//! temporary directory so the pack itself is never touched.
|
|
//! 2. **Signatures, independently** — each envelope is verified from the pack and
|
|
//! RFC 9052 alone, with a hand-built CBOR encoder rather than the crate's own
|
|
//! COSE code, which is what a third-party verifier would have to do.
|
|
//! 3. **The cascade's accounting** — the economics the receipts record, recomputed
|
|
//! across tiers rather than read back one at a time.
|
|
|
|
use ed25519_dalek::{Signature, Verifier, VerifyingKey};
|
|
use serde_json::Value;
|
|
use std::path::{Path, PathBuf};
|
|
|
|
fn pack_dir() -> PathBuf {
|
|
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
|
.join("../../../conformance/v1")
|
|
.canonicalize()
|
|
.expect("the conformance pack must exist next to the spec")
|
|
}
|
|
|
|
fn receipts_in(dir: &Path) -> Vec<Value> {
|
|
let raw = std::fs::read_to_string(dir.join("receipts.jsonl")).expect("read receipts.jsonl");
|
|
raw.lines()
|
|
.filter(|l| !l.trim().is_empty())
|
|
.map(|l| serde_json::from_str(l).expect("each line must parse"))
|
|
.collect()
|
|
}
|
|
|
|
fn meta() -> Value {
|
|
serde_json::from_slice(&std::fs::read(pack_dir().join("meta.json")).expect("read meta.json"))
|
|
.expect("meta.json must parse")
|
|
}
|
|
|
|
// --- minimal CBOR (RFC 8949), independent of the crate's own COSE code ----------
|
|
|
|
fn head(major: u8, len: u64) -> Vec<u8> {
|
|
let m = major << 5;
|
|
match len {
|
|
0..=23 => vec![m | len as u8],
|
|
24..=0xff => vec![m | 24, len as u8],
|
|
0x100..=0xffff => {
|
|
let mut v = vec![m | 25];
|
|
v.extend_from_slice(&(len as u16).to_be_bytes());
|
|
v
|
|
}
|
|
_ => {
|
|
let mut v = vec![m | 26];
|
|
v.extend_from_slice(&(len as u32).to_be_bytes());
|
|
v
|
|
}
|
|
}
|
|
}
|
|
fn bstr(b: &[u8]) -> Vec<u8> {
|
|
let mut v = head(2, b.len() as u64);
|
|
v.extend_from_slice(b);
|
|
v
|
|
}
|
|
fn tstr(s: &str) -> Vec<u8> {
|
|
let mut v = head(3, s.len() as u64);
|
|
v.extend_from_slice(s.as_bytes());
|
|
v
|
|
}
|
|
/// `{1: -8, 4: <steward bytes>}` — EdDSA with the steward as `kid`.
|
|
fn protected_header(kid: &str) -> Vec<u8> {
|
|
let mut v = head(5, 2);
|
|
v.push(0x01);
|
|
v.push(0x27); // -8
|
|
v.push(0x04);
|
|
v.extend(bstr(kid.as_bytes()));
|
|
v
|
|
}
|
|
/// RFC 9052 §4.4.
|
|
fn sig_structure(protected: &[u8], payload: &[u8]) -> Vec<u8> {
|
|
let mut v = head(4, 4);
|
|
v.extend(tstr("Signature1"));
|
|
v.extend(bstr(protected));
|
|
v.extend(bstr(&[]));
|
|
v.extend(bstr(payload));
|
|
v
|
|
}
|
|
|
|
/// The generator still emits exactly the published pack.
|
|
///
|
|
/// Run into a temp directory — `gen-vectors` takes an output path, so the
|
|
/// comparison never risks the published files.
|
|
#[test]
|
|
fn the_generator_still_emits_the_published_pack() {
|
|
let tmp = std::env::temp_dir().join(format!("proof-pack-{}", std::process::id()));
|
|
let _ = std::fs::remove_dir_all(&tmp);
|
|
std::fs::create_dir_all(&tmp).expect("create temp dir");
|
|
|
|
let out = std::process::Command::new(env!("CARGO_BIN_EXE_gen-vectors"))
|
|
.arg(&tmp)
|
|
.output()
|
|
.expect("gen-vectors must run");
|
|
assert!(out.status.success(), "gen-vectors failed: {}", String::from_utf8_lossy(&out.stderr));
|
|
|
|
for f in ["meta.json", "receipts.jsonl"] {
|
|
assert_eq!(
|
|
std::fs::read(tmp.join(f)).unwrap(),
|
|
std::fs::read(pack_dir().join(f)).unwrap(),
|
|
"{f} has drifted from what the implementation emits"
|
|
);
|
|
}
|
|
let _ = std::fs::remove_dir_all(&tmp);
|
|
}
|
|
|
|
/// Every published envelope verifies under the published key, checked from the pack
|
|
/// and RFC 9052 alone.
|
|
#[test]
|
|
fn every_published_envelope_verifies_under_the_published_key() {
|
|
let m = meta();
|
|
let steward = m["steward"].as_str().expect("meta names the steward");
|
|
let pubkey: [u8; 32] = hex::decode(m["pubkey_hex"].as_str().unwrap())
|
|
.unwrap()
|
|
.try_into()
|
|
.expect("a 32-byte Ed25519 key");
|
|
let vk = VerifyingKey::from_bytes(&pubkey).unwrap();
|
|
let protected = protected_header(steward);
|
|
|
|
let receipts = receipts_in(&pack_dir());
|
|
assert_eq!(receipts.len(), 4, "the pack records one receipt per cascade tier");
|
|
|
|
for r in &receipts {
|
|
let tier = r["closed_by"].as_str().unwrap();
|
|
let core = hex::decode(r["signed_core_hex"].as_str().unwrap()).unwrap();
|
|
let envelope = hex::decode(r["envelope_hex"].as_str().unwrap()).unwrap();
|
|
let sig = Signature::from_slice(&envelope[envelope.len() - 64..]).unwrap();
|
|
vk.verify(&sig_structure(&protected, &core), &sig)
|
|
.unwrap_or_else(|e| panic!("{tier}: published envelope does not verify: {e}"));
|
|
|
|
// the envelope really does carry the published core, not some other payload
|
|
assert!(
|
|
envelope.windows(core.len()).any(|w| w == core),
|
|
"{tier}: the envelope does not contain its published signed core"
|
|
);
|
|
assert_eq!(r["wire"]["steward"], m["steward"], "{tier}: wire steward disagrees with meta");
|
|
assert_eq!(r["wire"]["closed_by"], r["closed_by"], "{tier}: wire tier disagrees");
|
|
let goal_id = r["wire"]["goal_id"].as_str().expect("wire carries a goal id");
|
|
assert_eq!(goal_id.len(), 64, "{tier}: goal id is not a 32-byte hash");
|
|
assert!(goal_id.bytes().all(|b| b.is_ascii_hexdigit()), "{tier}: goal id is not hex");
|
|
}
|
|
|
|
// Distinct goals must address distinctly, or the receipts would be
|
|
// interchangeable and the pack would prove nothing about which goal closed.
|
|
let goals: Vec<&str> = receipts.iter().map(|r| r["goal"].as_str().unwrap()).collect();
|
|
let ids: Vec<&str> = receipts.iter().map(|r| r["wire"]["goal_id"].as_str().unwrap()).collect();
|
|
for i in 0..receipts.len() {
|
|
for j in i + 1..receipts.len() {
|
|
assert_eq!(
|
|
goals[i] == goals[j],
|
|
ids[i] == ids[j],
|
|
"goal text and goal id disagree about whether {i} and {j} are the same goal"
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// The verification above is not vacuous.
|
|
#[test]
|
|
fn a_tampered_core_or_a_wrong_key_does_not_verify() {
|
|
let m = meta();
|
|
let steward = m["steward"].as_str().unwrap();
|
|
let pubkey: [u8; 32] =
|
|
hex::decode(m["pubkey_hex"].as_str().unwrap()).unwrap().try_into().unwrap();
|
|
let protected = protected_header(steward);
|
|
let r = &receipts_in(&pack_dir())[0];
|
|
let envelope = hex::decode(r["envelope_hex"].as_str().unwrap()).unwrap();
|
|
let sig = Signature::from_slice(&envelope[envelope.len() - 64..]).unwrap();
|
|
|
|
let mut core = hex::decode(r["signed_core_hex"].as_str().unwrap()).unwrap();
|
|
let mid = core.len() / 2;
|
|
core[mid] ^= 0x01;
|
|
assert!(
|
|
VerifyingKey::from_bytes(&pubkey)
|
|
.unwrap()
|
|
.verify(&sig_structure(&protected, &core), &sig)
|
|
.is_err(),
|
|
"a tampered signed core verified"
|
|
);
|
|
|
|
let core = hex::decode(r["signed_core_hex"].as_str().unwrap()).unwrap();
|
|
let stranger = ed25519_dalek::SigningKey::from_bytes(&[0x42; 32]).verifying_key();
|
|
assert!(
|
|
stranger.verify(&sig_structure(&protected, &core), &sig).is_err(),
|
|
"the envelope verified under a key that did not seal it"
|
|
);
|
|
}
|
|
|
|
/// The cascade's accounting, recomputed across the whole pack.
|
|
///
|
|
/// Two properties the receipts assert together and neither states alone: the waste
|
|
/// recorded at a tier is exactly what the tiers before it consumed, and escalating
|
|
/// all the way costs **more** than the baseline it is measured against. The cascade
|
|
/// saves energy by closing early, not by existing.
|
|
#[test]
|
|
fn the_cascade_accounting_holds_across_the_tiers() {
|
|
let receipts = receipts_in(&pack_dir());
|
|
let tiers: Vec<&str> = receipts.iter().map(|r| r["closed_by"].as_str().unwrap()).collect();
|
|
assert_eq!(tiers, ["T0", "T1", "T2", "T3"], "the pack must cover every cascade tier");
|
|
|
|
let total = |r: &Value| r["wire"]["total_uj"].as_u64().unwrap();
|
|
let wasted = |r: &Value| r["wire"]["wasted_uj"].as_u64().unwrap();
|
|
let baseline = receipts[0]["wire"]["baseline_uj"].as_u64().unwrap();
|
|
|
|
assert_eq!(wasted(&receipts[0]), 0, "closing at T0 wastes nothing");
|
|
for i in 1..receipts.len() {
|
|
assert_eq!(
|
|
wasted(&receipts[i]),
|
|
total(&receipts[i - 1]),
|
|
"{}: waste must be exactly what the earlier tiers consumed",
|
|
tiers[i]
|
|
);
|
|
assert!(total(&receipts[i]) > total(&receipts[i - 1]), "{}: cost must rise", tiers[i]);
|
|
}
|
|
|
|
for r in &receipts[..3] {
|
|
assert!(total(r) < baseline, "closing early must beat the baseline");
|
|
}
|
|
assert!(
|
|
total(&receipts[3]) > baseline,
|
|
"escalating to T3 costs more than the baseline — if this ever passes, the \
|
|
honest limit of the cascade has been quietly re-described"
|
|
);
|
|
|
|
// Every verdict here was closed by a stand-in, and the pack says so rather than
|
|
// hiding it. That marking is the property worth holding onto.
|
|
for (r, tier) in receipts.iter().zip(&tiers) {
|
|
assert_eq!(r["wire"]["verdict"], "proved", "{tier}");
|
|
assert_eq!(r["wire"]["closed_by_stub"], 1, "{tier}: a stub close must be recorded as one");
|
|
}
|
|
}
|