//! `wai-keys` end to end (jwp-receipts §7): a key log built from operator-supplied seeds //! anchors, a revocation appended to it still anchors and moves an anchor forward, a //! rotation by the committed key anchors, one changed byte refuses the log, a published //! test seed is refused without the flag that says the log is a test log, a seed is never //! printed, a status and a module grant come out signed, and the DID document it emits is //! consistent with the log while one with a key missing is not. //! //! Seeds are the corpus's published one-byte seeds, so every run passes //! `--published-test-seed`, except the one that checks the refusal. use ed25519_dalek::SigningKey; use std::path::PathBuf; use std::process::{Command, Output}; use wai::key_log::{read_log, KeyStatus, ModuleGrant}; const BIN: &str = env!("CARGO_BIN_EXE_wai-keys"); const ORIGIN: &str = "https://publisher.example"; fn pk(seed: u8) -> String { SigningKey::from_bytes(&[seed; 32]).verifying_key().to_bytes().iter().map(|b| format!("{b:02x}")).collect() } struct Dir(PathBuf); impl Drop for Dir { fn drop(&mut self) { let _ = std::fs::remove_dir_all(&self.0); } } impl Dir { fn new(tag: &str) -> Dir { let d = std::env::temp_dir().join(format!("wai-keys-cli-{tag}-{}", std::process::id())); let _ = std::fs::remove_dir_all(&d); std::fs::create_dir_all(&d).unwrap(); Dir(d) } fn file(&self, name: &str, contents: &str) -> String { let p = self.0.join(name); std::fs::write(&p, contents).unwrap(); p.to_string_lossy().into_owned() } fn seed(&self, seed: u8) -> String { self.file(&format!("seed-{seed:02x}"), &format!("{}\n", format!("{seed:02x}").repeat(32))) } } fn run(args: &[&str]) -> Output { Command::new(BIN).args(args).output().expect("run wai-keys") } fn stdout(o: &Output) -> String { String::from_utf8_lossy(&o.stdout).into_owned() } fn ok(o: Output) -> String { assert!(o.status.success(), "wai-keys failed: {}{}", stdout(&o), String::from_utf8_lossy(&o.stderr)); stdout(&o) } fn never_prints(o: &Output, seed: u8) { let s = format!("{seed:02x}").repeat(32); assert!(!stdout(o).contains(&s) && !String::from_utf8_lossy(&o.stderr).contains(&s), "seed {seed:#x} printed"); } fn init(d: &Dir) -> String { let root = d.seed(0x61); let o = run(&[ "init", "--origin", ORIGIN, "--root-seed-file", &root, "--next-root-key", &pk(0x62), "--issued-at", "2026-09-01T00:00:00Z", "--key", &format!("id=d1,key={},may_sign=delivery|receipt", pk(0x63)), "--key", &format!("id=modd,key={},may_sign=module/decoder", pk(0x6b)), "--grant-max-s", "43200", "--status-max-age-s", "3600", "--published-test-seed", ]); never_prints(&o, 0x61); d.file("log.json", &ok(o)) } fn verify(log: &str, extra: &[&str]) -> Output { let mut a = vec!["verify", log, "--origin", ORIGIN]; a.extend_from_slice(extra); run(&a) } #[test] fn init_append_rotate_and_verify() { let d = Dir::new("chain"); let log = init(&d); let first = ok(verify(&log, &[])); assert!(first.starts_with("anchored revision=") && first.contains("anchor=fetched"), "{first}"); assert!(first.trim_end().ends_with("note=published_test_key"), "{first}"); let anchor0 = first.split_whitespace().nth(1).unwrap().trim_start_matches("revision=").to_owned(); // A revocation and a revoked module, appended under the same root. let root = d.seed(0x61); let digest = "ab".repeat(32); let o = run(&[ "append", &log, "--root-seed-file", &root, "--issued-at", "2026-09-02T00:00:00Z", "--revoke", "d1@2026-09-01T12:00:00Z", "--revoke-module", &digest, "--add", &format!("id=d2,key={},may_sign=delivery", pk(0x66)), "--published-test-seed", ]); never_prints(&o, 0x61); let log2 = d.file("log2.json", &ok(o)); let revs = read_log(ORIGIN, &std::fs::read_to_string(&log2).unwrap()).unwrap(); assert_eq!(revs.len(), 2); assert_eq!(revs[1].keys[0].compromised_since.as_deref(), Some("2026-09-01T12:00:00Z")); assert_eq!(revs[1].revoked_modules.len(), 1); // Anchored afresh, and as a later retrieval that moves an anchor forward. assert!(ok(verify(&log2, &["--pinned"])).contains("anchor=pinned")); let fwd = ok(verify(&log2, &["--anchor", &anchor0, "--held", &log])); assert!(fwd.starts_with(&format!("anchored revision={}", hex(&revs[1].revision_hash()))), "{fwd}"); // An older log retrieved after the newer one was anchored is a rollback. let anchor1 = hex(&revs[1].revision_hash()); let back = verify(&log, &["--anchor", &anchor1, "--held", &log2]); assert_eq!(back.status.code(), Some(1)); assert_eq!(stdout(&back).trim(), "not_anchored reason=key_log_rolled_back"); // A rotation, signed by the committed key, naming its own successor. let committed = d.seed(0x62); let log3 = d.file( "log3.json", &ok(run(&[ "append", &log2, "--rotate-with-seed-file", &committed, "--next-root-key", &pk(0x68), "--issued-at", "2026-09-03T00:00:00Z", "--published-test-seed", ])), ); assert!(ok(verify(&log3, &[])).starts_with("anchored")); // The root in use cannot change the commitment without rotating. let o = run(&["append", &log2, "--root-seed-file", &root, "--next-root-key", &pk(0x68), "--issued-at", "2026-09-03T00:00:00Z", "--published-test-seed"]); assert_eq!(o.status.code(), Some(2)); // A status for the head, and a module grant. let st = ok(run(&["status", &log3, "--root-seed-file", &committed, "--issued-at", "2026-09-03T01:00:00Z"])); let st = KeyStatus::from_json(st.trim()).expect("a status"); let revs3 = read_log(ORIGIN, &std::fs::read_to_string(&log3).unwrap()).unwrap(); assert!(st.verifies_under(revs3.last().unwrap())); let module = d.file("module.wasm", "\0asm a module"); let g = ok(run(&[ "grant", "--origin", ORIGIN, "--seed-file", &d.seed(0x6b), "--key-id", "modd", "--module-file", &module, "--form", "decoder", "--capability", "wai.neural.int_synth", "--not-before", "2026-09-03T00:00:00Z", "--not-after", "2026-09-03T12:00:00Z", "--published-test-seed", ])); let g = ModuleGrant::from_json(g.trim()).expect("a grant that verifies"); assert_eq!(g.module, wai::key_log::module_digest(b"\0asm a module")); assert_eq!(g.signer_id, "did:web:publisher.example#modd"); } fn hex(b: &[u8]) -> String { b.iter().map(|x| format!("{x:02x}")).collect() } #[test] fn a_changed_byte_is_refused() { let d = Dir::new("tamper"); let log = init(&d); let text = std::fs::read_to_string(&log).unwrap(); let i = text.find("\"sig\":\"").unwrap() + 7; let mut b = text.into_bytes(); b[i] = if b[i] == b'0' { b'1' } else { b'0' }; let bad = d.file("bad.json", std::str::from_utf8(&b).unwrap()); let o = verify(&bad, &[]); assert_eq!(o.status.code(), Some(1)); assert_eq!(stdout(&o).trim(), "not_anchored reason=key_log_signature_invalid"); } #[test] fn a_published_test_seed_is_refused_without_the_flag() { let d = Dir::new("published"); let root = d.seed(0x61); let o = run(&[ "init", "--origin", ORIGIN, "--root-seed-file", &root, "--next-root-key", &pk(0x62), "--issued-at", "2026-09-01T00:00:00Z", "--key", &format!("id=d1,key={},may_sign=delivery", pk(0x63)), ]); assert_eq!(o.status.code(), Some(2)); assert!(String::from_utf8_lossy(&o.stderr).contains("refusing a published test key")); assert!(o.stdout.is_empty()); never_prints(&o, 0x61); // Each command that puts a key into a log or signs with one refuses a published key // without the flag: an append that adds one, a rotation to one, and a grant. let log = init(&d); let refused = |o: Output| { assert_eq!(o.status.code(), Some(2), "{}", String::from_utf8_lossy(&o.stderr)); assert!(String::from_utf8_lossy(&o.stderr).contains("refusing a published test key")); assert!(o.stdout.is_empty()); }; let add = format!("id=d2,key={},may_sign=delivery", pk(0x66)); refused(run(&["append", &log, "--root-seed-file", &root, "--issued-at", "2026-09-02T00:00:00Z", "--add", &add])); let committed = d.seed(0x62); refused(run(&[ "append", &log, "--rotate-with-seed-file", &committed, "--next-root-key", &pk(0x68), "--issued-at", "2026-09-02T00:00:00Z", ])); let module = d.file("module.wasm", "\0asm a module"); refused(run(&[ "grant", "--origin", ORIGIN, "--seed-file", &d.seed(0x6b), "--key-id", "modd", "--module-file", &module, "--form", "decoder", "--capability", "wai.neural.int_synth", "--not-before", "2026-09-03T00:00:00Z", "--not-after", "2026-09-03T12:00:00Z", ])); } /// A later retrieval checked against a pinned anchor: the record stays pinned and keeps /// the revision that was pinned (§7.3), which `--held-pinned` lets an operator check. #[test] fn a_pinned_anchor_stays_pinned_on_a_forward_move() { let d = Dir::new("heldpinned"); let log = init(&d); let root = d.seed(0x61); let add = format!("id=d2,key={},may_sign=delivery", pk(0x66)); let log2 = d.file( "log2.json", &ok(run(&["append", &log, "--root-seed-file", &root, "--issued-at", "2026-09-02T00:00:00Z", "--add", &add, "--published-test-seed"])), ); let revs = read_log(ORIGIN, &std::fs::read_to_string(&log2).unwrap()).unwrap(); let (r0, r1) = (hex(&revs[0].revision_hash()), hex(&revs[1].revision_hash())); let pinned = ok(verify(&log2, &["--anchor", &r0, "--held", &log, "--held-pinned"])); assert_eq!(pinned.trim(), format!("anchored revision={r1} anchor=pinned pinned_revision={r0} note=published_test_key")); let fetched = ok(verify(&log2, &["--anchor", &r0, "--held", &log])); assert_eq!(fetched.trim(), format!("anchored revision={r1} anchor=fetched note=published_test_key")); // The flag describes an anchor already held, and needs one. assert_eq!(verify(&log2, &["--held-pinned"]).status.code(), Some(2)); } #[test] fn did_json_is_consistent_and_a_difference_is_reported() { let d = Dir::new("did"); let log = init(&d); let doc = ok(run(&["did-json", &log])); assert!(doc.contains("\"id\": \"did:web:publisher.example\"") && doc.contains("did:web:publisher.example#d1"), "{doc}"); let did = d.file("did.json", &doc); assert!(ok(run(&["check-did", &log, &did])).starts_with("consistent")); let mut v: serde_json::Value = serde_json::from_str(&doc).unwrap(); v["verificationMethod"].as_array_mut().unwrap().retain(|m| m["id"] != "did:web:publisher.example#d1"); let missing = d.file("did-missing.json", &v.to_string()); let o = run(&["check-did", &log, &missing]); assert_eq!(o.status.code(), Some(1)); assert!(stdout(&o).contains("did:web:publisher.example#d1: active in the key log, missing from the DID document"), "{}", stdout(&o)); } #[test] fn the_seed_file_must_be_a_seed() { let d = Dir::new("seedfile"); let bad = d.file("seed", "not a seed"); let o = run(&["init", "--origin", ORIGIN, "--root-seed-file", &bad, "--next-root-key", &pk(0x62), "--issued-at", "2026-09-01T00:00:00Z"]); assert_eq!(o.status.code(), Some(2)); assert!(!String::from_utf8_lossy(&o.stderr).contains("not a seed")); }