//! What a log seal is for, shown against the thing it defends against: a //! forger who holds the steward's Ed25519 key. In these tests that forger is //! the test itself, which is the honest way to model a day on which the key //! can be computed from the public one. use ed25519_dalek::SigningKey; use jcp_grant::issue; use jcp_logseal::{ consistency_path, consistent, cosign_stating, inclusion_path, keys_for, merkle_root, prove_consistency, prove_receipt, prove_fork, read_cosignature, read_seal, root_from_path, verify_equivocation, verify_fork, Divergence, Fork, seal_log, seal_log_committing, seal_stating, verify_cosignatures, verify_extending, verify_following, verify_hybrid, verify_receipt, verify_sealed_log, verify_witnessed, HybridKey, Pin, Policy, ReceiptProof, SealError, Standing, Statement, StewardKeys, Trust, Witness, Witnessing, PQ_KEY_LEN, PQ_SIG_LEN, }; use jcp_receipt::{verify_log, ReceiptLog}; use jcp_runtime::{Price, Runtime}; use jcp_schema::{Capability, ContentId, Did, Energy, Frame, FrameKind, Funds, Grant, Trust as FrameTrust}; use serde_json::Value; const STEWARD: &str = "did:example:sealed"; fn sk(s: u8) -> SigningKey { SigningKey::from_bytes(&[s; 32]) } fn did(s: u8) -> Did { Did::from_verifying_key(&sk(s).verifying_key()) } /// The steward's sealing key: its receipt key (7) beside a post-quantum one. fn steward_key() -> HybridKey { HybridKey::new(sk(7), [9; 32]) } /// A log of `n` priced decisions under the steward's key. `story` varies what /// was decided, so two logs of one length differ in every receipt. fn log(n: usize, story: &str) -> Vec> { let sg = issue( &sk(1), Grant { issuer: did(1), subject: did(2), capability: Capability::new(ContentId::of_bytes(b"sealed"), ["pay"]), budget_uj: 1_000_000, budget_funds: Some(Funds::new("USD", 1_000_000)), caveats: Vec::new(), expiry: 0, parent: None, delegate_depth: None, nonce: [3; 16], }, ) .unwrap(); let mut rt = Runtime::new(); let mut log = ReceiptLog::new(sk(7), STEWARD); for i in 0..n { let frame = Frame { kind: FrameKind::ToolCall, trust: FrameTrust::Control, grant_id: sg.grant.id(), action: "pay".into(), resource: ContentId::of_bytes(b"sealed"), payload_hash: ContentId::of_bytes(format!("{story}:{i}").as_bytes()), model: None, }; let d = rt.decide_priced(&frame, &sg, did(2), Energy::measured(10 + i as u64), &Price::new("USD", 100), 10); log.append(&frame, &sg.grant, &d); } log.entries().to_vec() } fn ed() -> [u8; 32] { steward_key().ed_public() } fn read(entries: &[Vec], seals: &[Vec], trust: Trust, policy: Policy) -> Result { let key = steward_key(); verify_sealed_log(entries, seals, STEWARD, &key.ed_public(), key.pq_public(), trust, policy) } /// Re-encode a seal with one member changed, signatures left as they were. fn with_member(seal: &[u8], member: &str, change: impl FnOnce(Option) -> Option) -> Vec { let mut v: Value = jcp_schema::from_canonical_cbor(seal).unwrap(); let record = v.as_object_mut().unwrap(); if let Some(new) = change(record.remove(member)) { record.insert(member.to_string(), new); } jcp_receipt::jcr1::canonical_cbor(&v).unwrap() } fn flip_last_hex(v: Option) -> Option { let mut s = v.unwrap().as_str().unwrap().to_string(); let last = s.pop().unwrap(); s.push(if last == '0' { '1' } else { '0' }); Some(Value::String(s)) } #[test] fn a_sealed_log_stands_under_both_readings() { let key = steward_key(); let entries = log(6, "honest"); let seal = seal_log(&entries, STEWARD, &key, None).unwrap(); let want = Standing { entries: 6, sealed: 6 }; assert_eq!(read(&entries, std::slice::from_ref(&seal), Trust::ReceiptsAndSeals, Policy::RequireSealed), Ok(want)); assert_eq!(read(&entries, std::slice::from_ref(&seal), Trust::SealsAlone, Policy::RequireSealed), Ok(want)); // the seal reads back as what it states, and is the same bytes every time let s = read_seal(&seal, STEWARD, &key.ed_public(), key.pq_public()).unwrap(); assert_eq!((s.count, s.head, s.prev), (6, ContentId::of_bytes(&entries[5]), None)); assert_eq!(seal_log(&entries, STEWARD, &key, None).unwrap(), seal); } /// **The point.** Someone who holds the steward's Ed25519 key writes a /// different history of the same length. Every receipt in it verifies; a /// reader with receipts alone cannot tell it from the real one. A reader /// with the seal can, under either reading, because the forged log does not /// reach the sealed head — and that rests on the hash and the post-quantum /// signature, neither of which the forger can redo. #[test] fn a_forged_history_under_the_stewards_own_key_is_refused_by_the_seal() { let key = steward_key(); let honest = log(6, "honest"); let seal = seal_log(&honest, STEWARD, &key, None).unwrap(); let forged = log(6, "forged"); assert_eq!(verify_log(&forged, STEWARD, &ed()), Ok(6), "receipts alone accept the forgery"); for trust in [Trust::ReceiptsAndSeals, Trust::SealsAlone] { assert_eq!(read(&forged, std::slice::from_ref(&seal), trust, Policy::ReportUnsealed), Err(SealError::HeadMismatch(0))); } // Nor can the forger seal the forgery. With the Ed25519 key and a // post-quantum key of its own, the seal names a key the reader does not // hold for this steward… let forgers_key = HybridKey::new(sk(7), [66; 32]); let own_seal = seal_log(&forged, STEWARD, &forgers_key, None).unwrap(); assert_eq!(read(&forged, std::slice::from_ref(&own_seal), Trust::SealsAlone, Policy::ReportUnsealed), Err(SealError::WrongKey)); // …and naming the steward's real post-quantum key over its own // signature fails that half: the Ed25519 half is valid, and not enough. let real_pq = hex::encode(key.pq_public()); let relabelled = with_member(&own_seal, "pq_key", |_| Some(Value::String(real_pq))); assert!(matches!( read(&forged, &[relabelled], Trust::SealsAlone, Policy::ReportUnsealed), Err(SealError::BadSignature | SealError::PqSignatureInvalid) )); } /// A hash chain alone cannot show that a log was cut short: every prefix of /// a valid chain is a valid chain. A seal can. #[test] fn a_log_cut_short_of_its_seal_is_detected() { let key = steward_key(); let entries = log(6, "honest"); let seal = seal_log(&entries, STEWARD, &key, None).unwrap(); let cut = &entries[..4]; assert_eq!(verify_log(cut, STEWARD, &ed()), Ok(4), "the prefix is a valid chain"); assert_eq!( read(cut, &[seal], Trust::ReceiptsAndSeals, Policy::ReportUnsealed), Err(SealError::Truncated { sealed: 6, have: 4 }) ); } #[test] fn seals_form_a_chain_and_none_can_be_dropped_or_reordered() { let key = steward_key(); let entries = log(9, "honest"); let first = seal_log(&entries[..3], STEWARD, &key, None).unwrap(); let second = seal_log(&entries[..6], STEWARD, &key, Some(&first)).unwrap(); let third = seal_log(&entries, STEWARD, &key, Some(&second)).unwrap(); let all = [first.clone(), second.clone(), third.clone()]; assert_eq!(read(&entries, &all, Trust::SealsAlone, Policy::RequireSealed), Ok(Standing { entries: 9, sealed: 9 })); let dropped = [first.clone(), third.clone()]; assert_eq!(read(&entries, &dropped, Trust::SealsAlone, Policy::ReportUnsealed), Err(SealError::SealChainBroken(1))); let reordered = [second.clone(), first.clone(), third]; assert_eq!(read(&entries, &reordered, Trust::SealsAlone, Policy::ReportUnsealed), Err(SealError::SealChainBroken(0))); // a later seal that covers no more than the one before it is not a later seal // (no sealer writes one: it is refused at sealing, so this one is signed as stated) assert!(matches!(seal_log(&entries[..3], STEWARD, &key, Some(&first)), Err(SealError::Malformed(_)))); let ids: Vec = entries.iter().map(|e| ContentId::of_bytes(e)).collect(); let again = Statement { count: 3, head: ids[2], root: merkle_root(&ids[..3]), prev: Some(ContentId::of_bytes(&first)), next_key: None, }; let stalled = seal_stating(STEWARD, &again, &key).unwrap(); assert_eq!( read(&entries, &[first, stalled], Trust::SealsAlone, Policy::ReportUnsealed), Err(SealError::SealChainBroken(1)) ); } /// A reader shown seals up to some point cannot tell from them that later /// ones exist. One that keeps the last seal it accepted can. #[test] fn a_reader_that_remembers_its_last_seal_cannot_be_shown_less() { let key = steward_key(); let entries = log(9, "honest"); let first = seal_log(&entries[..3], STEWARD, &key, None).unwrap(); let second = seal_log(&entries[..6], STEWARD, &key, Some(&first)).unwrap(); let third = seal_log(&entries, STEWARD, &key, Some(&second)).unwrap(); let reading = |log: &[Vec], seals: &[Vec], pin: Option<&Pin>| { verify_extending(log, seals, STEWARD, &key.ed_public(), key.pq_public(), Trust::SealsAlone, Policy::ReportUnsealed, pin) }; // a first reading, of the log as it stood at the second seal let (standing, pin) = reading(&entries[..6], &[first.clone(), second.clone()], None).unwrap(); assert_eq!(standing, Standing { entries: 6, sealed: 6 }); let pin = pin.expect("a reading that accepted a seal keeps it"); assert_eq!(pin.count, 6); // shown less than it has already accepted: refused, though what is // shown is a valid log under valid seals assert_eq!(reading(&entries[..3], std::slice::from_ref(&first), None).map(|(s, _)| s.sealed), Ok(3)); assert_eq!(reading(&entries[..3], std::slice::from_ref(&first), Some(&pin)), Err(SealError::Withheld)); assert_eq!(reading(&entries, &[], Some(&pin)), Err(SealError::Withheld)); // shown more: accepted, and the pin moves forward let (standing, next) = reading(&entries, &[first.clone(), second.clone(), third.clone()], Some(&pin)).unwrap(); assert_eq!((standing.sealed, next.unwrap().count), (9, 9)); // and it need not be shown what it already accepted: the seals may begin // at the pinned one let (standing, next) = reading(&entries, &[second.clone(), third.clone()], Some(&pin)).unwrap(); assert_eq!((standing.sealed, next.unwrap().count), (9, 9)); // which a reader with no pin, or another pin, does not allow assert_eq!(reading(&entries, &[second.clone(), third.clone()], None), Err(SealError::SealChainBroken(0))); // another chain of seals over the same log, forking after the first: // valid in itself, and not an extension of what this reader accepted let fork = seal_log(&entries[..5], STEWARD, &key, Some(&first)).unwrap(); assert_eq!(reading(&entries, &[first, fork], Some(&pin)), Err(SealError::Withheld)); } #[test] fn receipts_after_the_last_seal_are_reported_or_refused_as_the_reader_chooses() { let key = steward_key(); let entries = log(8, "honest"); let seal = seal_log(&entries[..5], STEWARD, &key, None).unwrap(); for trust in [Trust::ReceiptsAndSeals, Trust::SealsAlone] { assert_eq!( read(&entries, std::slice::from_ref(&seal), trust, Policy::ReportUnsealed), Ok(Standing { entries: 8, sealed: 5 }) ); assert_eq!( read(&entries, std::slice::from_ref(&seal), trust, Policy::RequireSealed), Err(SealError::Unsealed { unsealed: 3 }) ); } // with no seal at all, a reader that requires one has nothing to stand on assert_eq!(read(&entries, &[], Trust::SealsAlone, Policy::RequireSealed), Err(SealError::Unsealed { unsealed: 8 })); assert_eq!(read(&entries, &[], Trust::ReceiptsAndSeals, Policy::ReportUnsealed), Ok(Standing { entries: 8, sealed: 0 })); } /// The two readings differ in exactly one place: a receipt whose own /// signature is wrong. Held to its signature it is refused; held to the seal /// it cannot occur in a log that reaches the sealed head, because changing a /// signature byte changes the receipt's content id. #[test] fn a_receipt_altered_under_a_seal_is_refused_by_both_readings() { let key = steward_key(); let entries = log(5, "honest"); let seal = seal_log(&entries, STEWARD, &key, None).unwrap(); let mut altered = entries.clone(); let last = altered[2].len() - 1; altered[2][last] ^= 1; // one bit of receipt 2's signature assert_eq!( read(&altered, std::slice::from_ref(&seal), Trust::ReceiptsAndSeals, Policy::ReportUnsealed), Err(SealError::BrokenLink(2)) ); // without the signature check the damage shows at the next link assert_eq!(read(&altered, &[seal], Trust::SealsAlone, Policy::ReportUnsealed), Err(SealError::BrokenLink(3))); } #[test] fn a_seal_that_is_not_exactly_a_seal_is_refused() { let key = steward_key(); let entries = log(4, "honest"); let seal = seal_log(&entries, STEWARD, &key, None).unwrap(); let refuse = |bytes: Vec| read_seal(&bytes, STEWARD, &key.ed_public(), key.pq_public()).unwrap_err(); let code = |bytes: Vec| refuse(bytes).code(); // either half altered, and each is named assert_eq!(refuse(with_member(&seal, "sig", flip_last_hex)), SealError::BadSignature); assert_eq!(refuse(with_member(&seal, "pq_sig", flip_last_hex)), SealError::PqSignatureInvalid); // what it states, altered: the signatures are over the old statement assert_eq!(refuse(with_member(&seal, "count", |_| Some(Value::from(3)))), SealError::BadSignature); assert_eq!(refuse(with_member(&seal, "head", flip_last_hex)), SealError::BadSignature); assert_eq!(code(with_member(&seal, "note", |_| Some(Value::from("extra")))), "seal_malformed"); assert_eq!(code(with_member(&seal, "pq_sig", |_| None)), "seal_malformed"); assert_eq!(code(with_member(&seal, "count", |_| Some(Value::from(0)))), "seal_malformed"); assert_eq!(code(with_member(&seal, "kind", |_| Some(Value::from("jcp-receipt")))), "seal_malformed"); assert_eq!( code(with_member(&seal, "sig", |v| Some(Value::String(v.unwrap().as_str().unwrap().to_uppercase())))), "seal_malformed" ); assert_eq!(code(with_member(&seal, "pq_suite", |_| Some(Value::from("id-MLDSA44-Ed25519-SHA512")))), "pq_suite_unknown"); assert_eq!(code(with_member(&seal, "steward", |_| Some(Value::from("did:example:other")))), "seal_wrong_steward"); let mut trailing = seal.clone(); trailing.push(0); assert_eq!(code(trailing), "seal_malformed"); // a reader holding another steward's keys let other = HybridKey::new(sk(8), [9; 32]); assert_eq!(read_seal(&seal, STEWARD, &other.ed_public(), other.pq_public()), Err(SealError::WrongKey)); // an empty log has nothing to seal assert!(matches!(seal_log(&[], STEWARD, &key, None), Err(SealError::Malformed(_)))); } /// The suite is the composite as its specification publishes it: the test /// case of `draft-ietf-lamps-pq-composite-sigs-19` Appendix E for /// `id-MLDSA65-Ed25519-SHA512`, vendored with the family's hybrid-signature /// corpus, verifies here. Its public key is the ML-DSA-65 key followed by /// the Ed25519 key, and its signature the ML-DSA-65 signature followed by /// the Ed25519 one. #[test] fn the_published_composite_test_case_verifies() { let path = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")) .join("../../../../wai/pq-conformance/draft-composite-sigs-19.json"); let case: Value = serde_json::from_slice(&std::fs::read(path).expect("the vendored test case")).unwrap(); let bytes = |k: &str| hex::decode(case[k].as_str().unwrap()).unwrap(); let (pk, m, s) = (bytes("pk"), bytes("m"), bytes("s")); assert_eq!((pk.len(), s.len()), (PQ_KEY_LEN + 32, PQ_SIG_LEN + 64)); let ed_key: [u8; 32] = pk[PQ_KEY_LEN..].try_into().unwrap(); let ed_sig: [u8; 64] = s[PQ_SIG_LEN..].try_into().unwrap(); assert_eq!(verify_hybrid(&ed_key, &pk[..PQ_KEY_LEN], &m, &ed_sig, &s[..PQ_SIG_LEN]), Ok(())); // and not over another message let mut other = m.clone(); other[0] ^= 1; assert_eq!(verify_hybrid(&ed_key, &pk[..PQ_KEY_LEN], &other, &ed_sig, &s[..PQ_SIG_LEN]), Err(SealError::BadSignature)); } /// The tree, for every size and every leaf up to 70: each leaf's path /// reaches the root, and reaches it from nowhere else — not from another /// position, not with a sibling changed, not with a hash added or dropped. #[test] fn every_leaf_of_every_small_tree_proves_and_nothing_else_does() { let ids: Vec = (0..70u32).map(|i| ContentId::of_bytes(&i.to_le_bytes())).collect(); for n in 1..=ids.len() { let tree = &ids[..n]; let root = merkle_root(tree); for i in 0..n { let path = inclusion_path(tree, i); let at = |index: usize, path: &[ContentId]| root_from_path(&tree[i], index as u64, n as u64, path); assert_eq!(at(i, &path), Some(root), "leaf {i} of {n}"); // another leaf's id does not reach it by this path if n > 1 { let other = &tree[(i + 1) % n]; assert_ne!(root_from_path(other, i as u64, n as u64, &path), Some(root), "leaf {i} of {n}: another id"); // nor does this id at another position assert_ne!(at((i + 1) % n, &path), Some(root), "leaf {i} of {n}: another position"); let mut changed = path.clone(); changed[0].0[0] ^= 1; assert_ne!(at(i, &changed), Some(root), "leaf {i} of {n}: a changed sibling"); assert_eq!(at(i, &path[..path.len() - 1]), None, "leaf {i} of {n}: a short path"); } let mut long = path.clone(); long.push(root); assert_eq!(at(i, &long), None, "leaf {i} of {n}: a long path"); // (A path does not by itself fix the size of its tree: leaf 0 // of three and of four climb the same way. The size is the // seal's `count`, which the seal's signatures cover.) } } // a leaf is never a node: the root of two ids is not the leaf hash of anything of that shape assert_ne!(merkle_root(&ids[..1]), ids[0]); } /// **One receipt, shown alone.** With its proof and a seal it stands, under /// either reading, with no other receipt of its log — and a receipt from /// another history, or shown at another place, does not. #[test] fn a_single_receipt_stands_on_a_proof_and_a_seal() { let key = steward_key(); let entries = log(13, "honest"); let seal = seal_log(&entries, STEWARD, &key, None).unwrap(); let check = |envelope: &[u8], proof: &ReceiptProof, trust: Trust| { verify_receipt(envelope, proof, &seal, STEWARD, &key.ed_public(), key.pq_public(), trust).map(|s| s.count) }; for i in 0..13u64 { let proof = prove_receipt(&entries, 13, i).unwrap(); assert!(proof.path.len() <= 4, "a proof for 13 receipts is at most four hashes"); for trust in [Trust::ReceiptsAndSeals, Trust::SealsAlone] { assert_eq!(check(&entries[i as usize], &proof, trust), Ok(13), "receipt {i}"); } } let proof = prove_receipt(&entries, 13, 5).unwrap(); // the forger's receipt 5: validly signed under the steward's own key let forged = log(13, "forged"); assert_eq!(check(&forged[5], &proof, Trust::ReceiptsAndSeals), Err(SealError::ProofInvalid)); assert_eq!(check(&forged[5], &proof, Trust::SealsAlone), Err(SealError::ProofInvalid)); // the honest receipt 6 under receipt 5's proof, and receipt 5 claimed at 6 assert_eq!(check(&entries[6], &proof, Trust::SealsAlone), Err(SealError::ProofInvalid)); let moved = ReceiptProof { index: 6, path: proof.path.clone() }; assert_eq!(check(&entries[5], &moved, Trust::SealsAlone), Err(SealError::ProofInvalid)); // a receipt the seal does not cover has no proof against it assert_eq!(prove_receipt(&entries, 13, 13), Err(SealError::ProofInvalid)); let early = seal_log(&entries[..8], STEWARD, &key, None).unwrap(); let late = prove_receipt(&entries, 13, 10).unwrap(); assert_eq!( verify_receipt(&entries[10], &late, &early, STEWARD, &key.ed_public(), key.pq_public(), Trust::SealsAlone), Err(SealError::ProofInvalid) ); // a receipt whose signature is damaged: refused where signatures are held // to, and — its bytes having changed — by the proof where they are not let mut damaged = entries[5].clone(); let last = damaged.len() - 1; damaged[last] ^= 1; assert_eq!(check(&damaged, &proof, Trust::ReceiptsAndSeals), Err(SealError::ProofInvalid)); assert_eq!(check(&damaged, &proof, Trust::SealsAlone), Err(SealError::ProofInvalid)); } /// A seal whose head is right and whose root is not is validly signed and /// states something false. No correct sealer writes one; a reader refuses it. #[test] fn a_seal_stating_a_root_its_receipts_do_not_have_is_refused() { let key = steward_key(); let entries = log(6, "honest"); let ids: Vec = entries.iter().map(|e| ContentId::of_bytes(e)).collect(); let lying = Statement { count: 6, head: ids[5], root: merkle_root(&ids[..5]), prev: None, next_key: None }; let seal = seal_stating(STEWARD, &lying, &key).unwrap(); assert!(read_seal(&seal, STEWARD, &key.ed_public(), key.pq_public()).is_ok(), "it is a valid seal"); assert_eq!( read(&entries, std::slice::from_ref(&seal), Trust::SealsAlone, Policy::ReportUnsealed), Err(SealError::RootMismatch(0)) ); } /// One 32-byte value fixes both of a steward's keys, and a reader holding it /// takes the keys from a seal. #[test] fn a_key_id_fixes_both_keys() { let key = steward_key(); let entries = log(4, "honest"); let seal = seal_log(&entries, STEWARD, &key, None).unwrap(); let keys = StewardKeys { ed: key.ed_public(), pq: key.pq_public().to_vec() }; let id = keys.id(); assert_eq!(keys_for(&seal, &id), Ok(keys.clone())); // either key different is another id let other_pq = HybridKey::new(sk(7), [66; 32]); let other_ed = HybridKey::new(sk(8), [9; 32]); for other in [&other_pq, &other_ed] { assert_ne!(StewardKeys { ed: other.ed_public(), pq: other.pq_public().to_vec() }.id(), id); let theirs = seal_log(&log_under(other, 4), STEWARD, other, None); if let Ok(theirs) = theirs { assert_eq!(keys_for(&theirs, &id), Err(SealError::WrongKey)); } } // the forger's seal over its own history, under the real Ed25519 key let forged = log(4, "forged"); let forgers = seal_log(&forged, STEWARD, &other_pq, None).unwrap(); assert_eq!(keys_for(&forgers, &id), Err(SealError::WrongKey)); } /// A log under another steward key, for the case above. fn log_under(_key: &HybridKey, n: usize) -> Vec> { log(n, "honest") } /// A log of `n` decisions whose first `turn` receipts are sealed under one /// receipt key and the rest under another: a steward that changed its keys. fn rotated_log(n: usize, turn: usize, then: SigningKey) -> Vec> { let sg = issue( &sk(1), Grant { issuer: did(1), subject: did(2), capability: Capability::new(ContentId::of_bytes(b"sealed"), ["pay"]), budget_uj: 1_000_000, budget_funds: Some(Funds::new("USD", 1_000_000)), caveats: Vec::new(), expiry: 0, parent: None, delegate_depth: None, nonce: [4; 16], }, ) .unwrap(); let mut rt = Runtime::new(); let mut log = ReceiptLog::new(sk(7), STEWARD); let mut then = Some(then); for i in 0..n { if i == turn { log.rekey(then.take().expect("one change of key")); } let frame = Frame { kind: FrameKind::ToolCall, trust: FrameTrust::Control, grant_id: sg.grant.id(), action: "pay".into(), resource: ContentId::of_bytes(b"sealed"), payload_hash: ContentId::of_bytes(format!("rotated:{i}").as_bytes()), model: None, }; let d = rt.decide_priced(&frame, &sg, did(2), Energy::measured(10), &Price::new("USD", 100), 10); log.append(&frame, &sg.grant, &d); } log.entries().to_vec() } /// **A steward changes its keys.** It says so first, under the keys a reader /// already holds; the next seal is under the new ones, and a reader follows /// it there and ends holding them. Keys nothing committed to are refused, /// however validly they sign. #[test] fn a_reader_follows_a_committed_change_of_keys_and_no_other() { let old = steward_key(); let new = HybridKey::new(sk(17), [19; 32]); let keys_of = |k: &HybridKey| StewardKeys { ed: k.ed_public(), pq: k.pq_public().to_vec() }; let entries = rotated_log(9, 5, sk(17)); let reading = |seals: &[Vec], trust: Trust| { verify_following(&entries, seals, STEWARD, &keys_of(&old), trust, Policy::RequireSealed, None) }; let first = seal_log_committing(&entries[..5], STEWARD, &old, None, &keys_of(&new).id()).unwrap(); let second = seal_log(&entries, STEWARD, &new, Some(&first)).unwrap(); for trust in [Trust::ReceiptsAndSeals, Trust::SealsAlone] { let r = reading(&[first.clone(), second.clone()], trust).unwrap(); assert_eq!(r.standing, Standing { entries: 9, sealed: 9 }); assert_eq!(r.keys, keys_of(&new), "the reader ends holding the new keys"); } // the same change with no commitment before it: the new keys are just other keys let silent = seal_log(&entries[..5], STEWARD, &old, None).unwrap(); let uninvited = seal_log(&entries, STEWARD, &new, Some(&silent)).unwrap(); assert_eq!(reading(&[silent, uninvited], Trust::SealsAlone), Err(SealError::WrongKey)); // a commitment to one set of keys, and a seal under another let other = HybridKey::new(sk(17), [66; 32]); let ids: Vec = entries.iter().map(|e| ContentId::of_bytes(e)).collect(); let stated = Statement { count: 9, head: ids[8], root: merkle_root(&ids), prev: Some(ContentId::of_bytes(&first)), next_key: None }; let impostor = seal_stating(STEWARD, &stated, &other).unwrap(); assert_eq!(reading(&[first.clone(), impostor], Trust::SealsAlone), Err(SealError::WrongKey)); // after the change the old keys sign nothing more: a later seal under them is refused let longer = rotated_log(11, 5, sk(17)); let ids: Vec = longer.iter().map(|e| ContentId::of_bytes(e)).collect(); let back = Statement { count: 11, head: ids[10], root: merkle_root(&ids), prev: Some(ContentId::of_bytes(&second)), next_key: None }; let relapse = seal_stating(STEWARD, &back, &old).unwrap(); assert_eq!( verify_following(&longer, &[first.clone(), second.clone(), relapse], STEWARD, &keys_of(&old), Trust::SealsAlone, Policy::ReportUnsealed, None), Err(SealError::WrongKey) ); // Held to their own signatures, receipts are held to the keys of the seal // that first covers them. A log whose seals changed keys and whose // receipts did not is refused there, and stands where only seals are held to. let unchanged = log(9, "honest"); let ids: Vec = unchanged.iter().map(|e| ContentId::of_bytes(e)).collect(); let one = seal_log_committing(&unchanged[..5], STEWARD, &old, None, &keys_of(&new).id()).unwrap(); let all = Statement { count: 9, head: ids[8], root: merkle_root(&ids), prev: Some(ContentId::of_bytes(&one)), next_key: None }; let two = seal_stating(STEWARD, &all, &new).unwrap(); let read_unchanged = |trust| verify_following(&unchanged, &[one.clone(), two.clone()], STEWARD, &keys_of(&old), trust, Policy::RequireSealed, None); assert_eq!(read_unchanged(Trust::ReceiptsAndSeals), Err(SealError::BrokenLink(5))); assert_eq!(read_unchanged(Trust::SealsAlone).map(|r| r.standing.sealed), Ok(9)); // and the sealer itself refuses to seal receipts its key did not sign assert_eq!(seal_log(&unchanged, STEWARD, &new, Some(&one)), Err(SealError::BrokenLink(5))); } // ---- one tree extending another, and the witnesses that hold to it -------- fn ids_of(n: usize, story: &str) -> Vec { (0..n).map(|i| ContentId::of_bytes(format!("{story}:{i}").as_bytes())).collect() } /// Every earlier tree is consistent with every later tree it begins, by the /// path drawn for it — and by no altered path, other sizes, or another tree. #[test] fn every_tree_extends_every_tree_it_begins_and_nothing_else_does() { let ids = ids_of(40, "grow"); let other = ids_of(40, "fork"); for n in 1..=ids.len() { let new_root = merkle_root(&ids[..n]); for m in 1..=n { let old_root = merkle_root(&ids[..m]); let path = consistency_path(&ids[..n], m); assert!(consistent(m as u64, &old_root, n as u64, &new_root, &path), "{m} → {n}"); // no hash of the path can change for i in 0..path.len() { let mut bad = path.clone(); bad[i] = ContentId::of_bytes(b"not this"); assert!(!consistent(m as u64, &old_root, n as u64, &new_root, &bad), "{m} → {n}, hash {i}"); } // nor its length if !path.is_empty() { assert!(!consistent(m as u64, &old_root, n as u64, &new_root, &path[..path.len() - 1]), "{m} → {n} short"); } let mut long = path.clone(); long.push(new_root); assert!(!consistent(m as u64, &old_root, n as u64, &new_root, &long), "{m} → {n} long"); // (The sizes are not tested the same way: a root is opaque, so a // path can fit a tree of another stated size too. What a path // shows is where the earlier tree sits in the later one; the // sizes are what the seals sign.) // a tree that parts from this one before m is consistent with neither let mut forked = ids[..n].to_vec(); forked[m - 1] = other[m - 1]; assert!(!consistent(m as u64, &old_root, n as u64, &merkle_root(&forked), &consistency_path(&forked, m)), "{m} → {n} forked"); } // a path read against a later tree larger than the one it climbs // ends below the stated tree's top, and does not hold for m in 1..n { let path = consistency_path(&ids[..n], m); let at = |count: usize| consistent(m as u64, &merkle_root(&ids[..m]), count as u64, &new_root, &path); assert!(!at(2 * n), "{m} → {n} read as {}", 2 * n); } // and nothing extends a larger tree, or none assert!(!consistent(n as u64 + 1, &new_root, n as u64, &new_root, &[])); assert!(!consistent(0, &new_root, n as u64, &new_root, &[])); } } fn witness(seed: u8) -> Witness { let mut w = Witness::new(HybridKey::new(sk(40 + seed), [40 + seed; 32])); w.watch(STEWARD, keys_of(&steward_key()).id()); w } fn keys_of(k: &HybridKey) -> StewardKeys { StewardKeys { ed: k.ed_public(), pq: k.pq_public().to_vec() } } /// **A witness cosigns one line of seals.** It cosigns a seal that extends /// the one it last cosigned, and again the same one later; it will not /// cosign one before it, one on another line, or one whose log was /// rewritten under the steward's own key. #[test] fn a_witness_cosigns_only_seals_that_extend_the_one_it_last_cosigned() { let key = steward_key(); let entries = log(9, "honest"); let s1 = seal_log(&entries[..4], STEWARD, &key, None).unwrap(); let s2 = seal_log(&entries[..7], STEWARD, &key, Some(&s1)).unwrap(); let s3 = seal_log(&entries, STEWARD, &key, Some(&s2)).unwrap(); let mut w = witness(1); // first contact: the witness vouches from the seal it is shown let c1 = w.cosign(STEWARD, std::slice::from_ref(&s1), &[], 100).unwrap(); assert_eq!(read_cosignature(&c1, STEWARD).unwrap().seal, ContentId::of_bytes(&s1)); // a later seal, with the path from the one it cosigned let path = prove_consistency(&entries, 4, 7).unwrap(); w.cosign(STEWARD, &[s1.clone(), s2.clone()], &path, 200).unwrap(); assert_eq!(w.pin(STEWARD).map(|p| p.count), Some(7)); // the same seal again, later: a fresh cosignature, nothing new vouched for let again = w.cosign(STEWARD, std::slice::from_ref(&s2), &[], 250).unwrap(); assert_eq!(read_cosignature(&again, STEWARD).unwrap().time, 250); // behind its pin: refused, and nothing changes assert_eq!(w.cosign(STEWARD, std::slice::from_ref(&s1), &[], 300), Err(SealError::Withheld)); // a path that does not show s3 extends s2 let wrong = prove_consistency(&entries, 4, 9).unwrap(); assert_eq!(w.cosign(STEWARD, &[s2.clone(), s3.clone()], &wrong, 300), Err(SealError::Inconsistent)); assert_eq!(w.pin(STEWARD).map(|p| p.count), Some(7), "a refusal changes nothing"); // the steward, holding its own keys, rewrites the log behind s2 and seals // the rewrite as following s2: the chain of seals holds, the trees do not let rewritten = log(11, "rewritten"); let forged = seal_log(&rewritten, STEWARD, &key, Some(&s2)).unwrap(); let path = prove_consistency(&rewritten, 7, 11).unwrap(); assert_eq!(w.cosign(STEWARD, &[s2.clone(), forged], &path, 300), Err(SealError::Inconsistent)); // another line of seals from s1: the witness's pin is not on it let other = log(9, "other"); let fork = seal_log(&other, STEWARD, &key, Some(&s1)).unwrap(); assert_eq!(w.cosign(STEWARD, &[s1.clone(), fork], &prove_consistency(&other, 4, 9).unwrap(), 300), Err(SealError::Withheld)); // the honest next seal still goes through let path = prove_consistency(&entries, 7, 9).unwrap(); w.cosign(STEWARD, &[s1, s2, s3], &path, 400).unwrap(); assert_eq!(w.pin(STEWARD).map(|p| p.count), Some(9)); // and a steward it does not watch is refused assert_eq!(w.cosign("did:example:other", &[], &[], 400), Err(SealError::WrongSteward)); } /// **A first reading can no longer be shown less.** A reader with no pin, /// requiring two of three witnesses to have cosigned the last seal recently, /// is refused the log cut back to an earlier seal — and the steward cannot /// make that seal recent again, because every witness that cosigned the /// later one refuses it. #[test] fn a_first_reading_that_requires_witnesses_cannot_be_shown_an_earlier_seal_as_the_last() { let key = steward_key(); let entries = log(9, "honest"); let s1 = seal_log(&entries[..5], STEWARD, &key, None).unwrap(); let s2 = seal_log(&entries, STEWARD, &key, Some(&s1)).unwrap(); let mut ws = [witness(1), witness(2), witness(3)]; let trusted: Vec = ws.iter().map(|w| w.keys().id()).collect(); let mut on_s1 = Vec::new(); for w in &mut ws { on_s1.push(w.cosign(STEWARD, std::slice::from_ref(&s1), &[], 100).unwrap()); } let path = prove_consistency(&entries, 5, 9).unwrap(); let mut on_s2 = Vec::new(); for w in &mut ws { on_s2.push(w.cosign(STEWARD, &[s1.clone(), s2.clone()], &path, 200).unwrap()); } let rule = Witnessing { witnesses: &trusted, quorum: 2, not_before: 150 }; let read = |entries: &[Vec], seals: &[Vec], cosigs: &[Vec]| { verify_witnessed(entries, seals, cosigs, STEWARD, &keys_of(&key), Trust::SealsAlone, Policy::ReportUnsealed, None, &rule) }; let (reading, witnessed) = read(&entries, &[s1.clone(), s2.clone()], &on_s2).unwrap(); assert_eq!((reading.standing.sealed, witnessed.witnesses.len(), witnessed.as_of), (9, 3, 200)); // cut back to s1: a valid log under a valid seal, and stale assert_eq!(read(&entries[..5], std::slice::from_ref(&s1), &on_s1), Err(SealError::Unwitnessed { have: 0, need: 2 })); // and no witness will cosign s1 again for w in &mut ws { assert_eq!(w.cosign(STEWARD, std::slice::from_ref(&s1), &[], 300), Err(SealError::Withheld)); } // s2's cosignatures do not stand for s1 assert_eq!(read(&entries[..5], std::slice::from_ref(&s1), &on_s2), Err(SealError::Unwitnessed { have: 0, need: 2 })); // a reader that does not ask for recency is shown s1 as it always could be let lax = Witnessing { witnesses: &trusted, quorum: 2, not_before: 0 }; assert!(verify_witnessed(&entries[..5], &[s1], &on_s1, STEWARD, &keys_of(&key), Trust::SealsAlone, Policy::ReportUnsealed, None, &lax).is_ok()); } /// A reader counts a witness once, and only a witness it trusts, cosigning /// this seal for this steward, recently enough, with both signatures good. /// A cosignature that fails any of that is passed over, not fatal. #[test] fn only_good_recent_cosignatures_from_trusted_witnesses_count_and_each_witness_once() { let key = steward_key(); let entries = log(4, "honest"); let seal = seal_log(&entries, STEWARD, &key, None).unwrap(); let id = ContentId::of_bytes(&seal); let (a, b, stranger) = (HybridKey::new(sk(51), [51; 32]), HybridKey::new(sk(52), [52; 32]), HybridKey::new(sk(53), [53; 32])); let id_of = |k: &HybridKey| jcp_logseal::WitnessKeys { ed: k.ed_public(), pq: k.pq_public().to_vec() }.id(); let trusted = [id_of(&a), id_of(&b)]; let rule = Witnessing { witnesses: &trusted, quorum: 2, not_before: 100 }; let count = |cosigs: &[Vec]| verify_cosignatures(&seal, STEWARD, cosigs, &rule); let good_a = cosign_stating(STEWARD, &id, 120, &a).unwrap(); let good_b = cosign_stating(STEWARD, &id, 110, &b).unwrap(); let w = count(&[good_a.clone(), good_b.clone()]).unwrap(); assert_eq!((w.witnesses.len(), w.as_of), (2, 110), "as of the later of the two earliest that make the quorum"); let twice = [good_a.clone(), cosign_stating(STEWARD, &id, 130, &a).unwrap()]; assert_eq!(count(&twice), Err(SealError::Unwitnessed { have: 1, need: 2 }), "one witness twice is one"); let cases: [(&str, Vec); 6] = [ ("an untrusted witness", cosign_stating(STEWARD, &id, 120, &stranger).unwrap()), ("another seal", cosign_stating(STEWARD, &ContentId::of_bytes(b"other"), 120, &b).unwrap()), ("another steward", cosign_stating("did:example:other", &id, 120, &b).unwrap()), ("too old", cosign_stating(STEWARD, &id, 99, &b).unwrap()), ("a changed time", with_member(&good_b, "time", |_| Some(Value::from(200u64)))), ("a changed post-quantum half", with_member(&good_b, "pq_sig", flip_last_hex)), ]; for (what, bad) in cases { assert_eq!(count(&[good_a.clone(), bad]), Err(SealError::Unwitnessed { have: 1, need: 2 }), "{what}"); } assert_eq!(read_cosignature(&with_member(&good_b, "sig", flip_last_hex), STEWARD), Err(SealError::BadSignature)); assert_eq!(read_cosignature(&with_member(&good_b, "pq_sig", flip_last_hex), STEWARD), Err(SealError::PqSignatureInvalid)); assert!(matches!(read_cosignature(&with_member(&good_b, "extra", |_| Some(Value::from(1u64))), STEWARD), Err(SealError::Malformed(_)))); } /// A witness follows a steward through a change of keys its seals committed /// to, as a reader does, and refuses keys nothing committed to. #[test] fn a_witness_follows_a_committed_change_of_keys() { let old = steward_key(); let new = HybridKey::new(sk(17), [19; 32]); let entries = rotated_log(9, 5, sk(17)); let first = seal_log_committing(&entries[..5], STEWARD, &old, None, &keys_of(&new).id()).unwrap(); let second = seal_log(&entries, STEWARD, &new, Some(&first)).unwrap(); let mut w = witness(4); w.cosign(STEWARD, std::slice::from_ref(&first), &[], 10).unwrap(); let path = prove_consistency(&entries, 5, 9).unwrap(); w.cosign(STEWARD, &[first.clone(), second.clone()], &path, 20).unwrap(); // from then on the new keys are the steward's: the second seal alone is enough w.cosign(STEWARD, &[second], &[], 30).unwrap(); let mut v = witness(5); let silent = seal_log(&entries[..5], STEWARD, &old, None).unwrap(); v.cosign(STEWARD, std::slice::from_ref(&silent), &[], 10).unwrap(); let uninvited = seal_log(&entries, STEWARD, &new, Some(&silent)).unwrap(); assert_eq!(v.cosign(STEWARD, &[silent, uninvited], &path, 20), Err(SealError::WrongKey)); } /// **A steward that shows two readers two histories can be shown to have.** /// Two seals over logs neither of which begins with the other: of one count, /// their roots differ; of two counts, the larger log has another receipt at /// the smaller seal's last place, shown with its path. #[test] fn a_fork_is_shown_by_two_seals_and_at_most_one_path() { let key = steward_key(); let ids = [keys_of(&key).id()]; let honest = log(9, "honest"); let forged = log(9, "forged"); let honest_9 = seal_log(&honest, STEWARD, &key, None).unwrap(); let forged_9 = seal_log(&forged, STEWARD, &key, None).unwrap(); let forged_6 = seal_log(&forged[..6], STEWARD, &key, None).unwrap(); let honest_6 = seal_log(&honest[..6], STEWARD, &key, None).unwrap(); // one count, two roots let same = Fork { a: honest_9.clone(), b: forged_9.clone(), divergence: None }; assert_eq!(verify_fork(&same, STEWARD, &ids).unwrap().counts, [9, 9]); // two counts: built by a reader holding the larger log let parted = prove_fork(&forged_6, &honest_9, &honest).expect("the logs part at the sixth receipt"); assert_eq!(verify_fork(&parted, STEWARD, &ids).unwrap().counts, [6, 9]); // a prefix of the same log is not a fork, however it is offered assert_eq!(prove_fork(&honest_6, &honest_9, &honest), None); let lie = Fork { a: honest_6.clone(), b: honest_9.clone(), divergence: Some(Divergence { leaf: ContentId::of_bytes(&forged[5]), path: parted.divergence.clone().unwrap().path }), }; assert_eq!(verify_fork(&lie, STEWARD, &ids), Err(SealError::NotAFork), "a leaf that is not the larger tree's"); // the larger tree's true leaf, with its true path: it is the smaller // seal's head, so the larger log begins with the smaller let true_leaf = Fork { a: honest_6.clone(), b: honest_9.clone(), divergence: parted.divergence.clone() }; assert_eq!(verify_fork(&true_leaf, STEWARD, &ids), Err(SealError::NotAFork), "a prefix with its true leaf and path"); let no_path = Fork { divergence: None, ..parted.clone() }; assert_eq!(verify_fork(&no_path, STEWARD, &ids), Err(SealError::NotAFork)); assert_eq!(verify_fork(&Fork { a: honest_9.clone(), b: honest_9.clone(), divergence: None }, STEWARD, &ids), Err(SealError::NotAFork)); // a seal under keys that are not the steward's proves nothing about it let other = seal_log(&forged, STEWARD, &HybridKey::new(sk(7), [66; 32]), None).unwrap(); assert_eq!(verify_fork(&Fork { a: honest_9, b: other, divergence: None }, STEWARD, &ids), Err(SealError::WrongKey)); } /// **A witness that cosigned both sides of a fork has broken its rule**, and /// its two cosignatures with the fork show it to anyone. #[test] fn a_witness_that_cosigned_both_sides_of_a_fork_is_shown_to_have() { let key = steward_key(); let ids = [keys_of(&key).id()]; let honest = log(9, "honest"); let forged = log(9, "forged"); let a = seal_log(&forged[..6], STEWARD, &key, None).unwrap(); let b = seal_log(&honest, STEWARD, &key, None).unwrap(); let fork = prove_fork(&a, &b, &honest).unwrap(); let (bent, other) = (HybridKey::new(sk(60), [60; 32]), HybridKey::new(sk(61), [61; 32])); let cosign = |seal: &[u8], k: &HybridKey| cosign_stating(STEWARD, &ContentId::of_bytes(seal), 100, k).unwrap(); let witness = jcp_logseal::WitnessKeys { ed: bent.ed_public(), pq: bent.pq_public().to_vec() }.id(); assert_eq!(verify_equivocation(&fork, &cosign(&a, &bent), &cosign(&b, &bent), STEWARD, &ids), Ok(witness)); // two witnesses, one side each: each kept its rule assert_eq!(verify_equivocation(&fork, &cosign(&a, &bent), &cosign(&b, &other), STEWARD, &ids), Err(SealError::NotAnEquivocation)); // cosignatures of other seals than the fork's assert_eq!(verify_equivocation(&fork, &cosign(&b, &bent), &cosign(&a, &bent), STEWARD, &ids), Err(SealError::NotAnEquivocation)); // and an honest witness never writes the second: it refuses the other line let mut w = Witness::new(bent.clone()); w.watch(STEWARD, ids[0]); w.cosign(STEWARD, std::slice::from_ref(&b), &[], 100).unwrap(); assert_eq!(w.cosign(STEWARD, std::slice::from_ref(&a), &[], 200), Err(SealError::Withheld)); }