//! One receipt countersigned post-quantum, and a steward's keys attested by //! the witnesses that follow its seals (log-seal §4.2, §5.7). use ed25519_dalek::SigningKey; use jcp_grant::issue; use jcp_logseal::attest::{attest_stating, attested_keys, countersign, read_key_attestation, verify_countersigned, AttestedKeys}; use jcp_logseal::{ keys_for, prove_consistency, seal_log, seal_log_committing, verify_following, HybridKey, Policy, SealError, StewardKeys, Trust, Witness, Witnessing, }; use jcp_receipt::ReceiptLog; use jcp_runtime::{Price, Runtime}; use jcp_schema::{Capability, ContentId, Did, Energy, Frame, FrameKind, Funds, Grant, Trust as FrameTrust}; 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()) } fn steward_key() -> HybridKey { HybridKey::new(sk(7), [9; 32]) } fn keys_of(k: &HybridKey) -> StewardKeys { StewardKeys { ed: k.ed_public(), pq: k.pq_public().to_vec() } } /// A log of `n` decisions; from `turn` on its receipts are under `then`. fn log(n: usize, turn: Option<(usize, 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: [5; 16], }, ) .unwrap(); let mut rt = Runtime::new(); let mut log = ReceiptLog::new(sk(7), STEWARD); let mut turn = turn; for i in 0..n { if turn.as_ref().is_some_and(|(at, _)| *at == i) { log.rekey(turn.take().expect("one change").1); } 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!("attested:{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() } fn witness(seed: u8, key_id: ContentId) -> Witness { let mut w = Witness::new(HybridKey::new(sk(40 + seed), [40 + seed; 32])); w.watch(STEWARD, key_id); w } #[test] fn a_receipt_countersigned_stands_post_quantum_before_any_seal_covers_it() { let key = steward_key(); let entries = log(5, None); // the last receipt, after a seal of the first four let _seal = seal_log(&entries[..4], STEWARD, &key, None).unwrap(); let c = countersign(&entries[4], STEWARD, &key).unwrap(); assert_eq!(verify_countersigned(&entries[4], &c, STEWARD, &keys_of(&key)), Ok(())); assert_eq!(verify_countersigned(&entries[3], &c, STEWARD, &keys_of(&key)), Err(SealError::ProofInvalid)); // the receipt key alone, beside a post-quantum key not the steward's, is not the steward's countersignature let half = HybridKey::new(sk(7), [12; 32]); let by_half = countersign(&entries[4], STEWARD, &half).unwrap(); assert_eq!(verify_countersigned(&entries[4], &by_half, STEWARD, &keys_of(&key)), Err(SealError::WrongKey)); // a member a countersignature does not define, its signatures otherwise intact let mut record: serde_json::Value = jcp_schema::from_canonical_cbor(&c).unwrap(); record.as_object_mut().unwrap().insert("note".into(), serde_json::json!("extra")); let padded = jcp_receipt::jcr1::canonical_cbor(&record).unwrap(); assert!(matches!(verify_countersigned(&entries[4], &padded, STEWARD, &keys_of(&key)), Err(SealError::Malformed(_)))); // a bit changed in either signature let mut bent = c.clone(); let at = bent.len() - 10; bent[at] ^= 1; assert!(verify_countersigned(&entries[4], &bent, STEWARD, &keys_of(&key)).is_err()); } #[test] fn witnesses_attest_the_keys_of_the_seals_they_cosigned() { let key = steward_key(); let id = keys_of(&key).id(); let entries = log(9, None); let s1 = seal_log(&entries[..4], STEWARD, &key, None).unwrap(); let s2 = seal_log(&entries[..7], STEWARD, &key, Some(&s1)).unwrap(); let path = prove_consistency(&entries, 4, 7).unwrap(); let mut ws: Vec = (1..=3).map(|i| witness(i, id)).collect(); for w in &mut ws { w.cosign(STEWARD, std::slice::from_ref(&s1), &[], 100).unwrap(); w.cosign(STEWARD, &[s1.clone(), s2.clone()], &path, 200).unwrap(); } let trusted: Vec = ws.iter().map(|w| w.keys().id()).collect(); let attestations: Vec> = ws.iter().map(|w| w.attest_keys(STEWARD, 300).unwrap()).collect(); let a = read_key_attestation(&attestations[0], STEWARD).unwrap(); assert_eq!((a.key_id, a.seal, a.count, a.time, a.witness), (id, ContentId::of_bytes(&s2), 7, 300, trusted[0])); let rule = Witnessing { witnesses: &trusted, quorum: 2, not_before: 250 }; let got = attested_keys(STEWARD, &attestations, &rule).unwrap(); assert_eq!(got, AttestedKeys { key_id: id, witnesses: { let mut t = trusted.clone(); t.sort(); t }, seal: ContentId::of_bytes(&s2), count: 7 }); // a reader given no steward key id takes the keys from the seal the witnesses name and reads on let keys = keys_for(&s2, &got.key_id).unwrap(); let reading = verify_following(&entries[..7], &[s1.clone(), s2.clone()], STEWARD, &keys, Trust::SealsAlone, Policy::RequireSealed, None).unwrap(); assert_eq!(reading.standing.sealed, 7); // witnesses that last saw different seals: the reader starts from the newest one named let behind = attest_stating(STEWARD, &id, &ContentId::of_bytes(&s1), 4, 300, &HybridKey::new(sk(43), [43; 32])).unwrap(); let mixed = [attestations[0].clone(), attestations[1].clone(), behind]; let got = attested_keys(STEWARD, &mixed, &rule).unwrap(); assert_eq!((got.seal, got.count, got.witnesses.len()), (ContentId::of_bytes(&s2), 7, 3)); // one trusted attestation is not a quorum; untrusted, stale, altered and other stewards' ones do not count assert_eq!(attested_keys(STEWARD, &attestations[..1], &rule), Err(SealError::KeysUnattested { have: 1, need: 2 })); assert_eq!( attested_keys(STEWARD, &attestations, &Witnessing { witnesses: &trusted[..1], quorum: 2, not_before: 0 }), Err(SealError::KeysUnattested { have: 1, need: 2 }) ); assert_eq!( attested_keys(STEWARD, &attestations, &Witnessing { witnesses: &trusted, quorum: 2, not_before: 301 }), Err(SealError::KeysUnattested { have: 0, need: 2 }) ); let mut altered = attestations.clone(); for a in altered.iter_mut().take(2) { let at = a.len() - 10; a[at] ^= 1; } assert_eq!(attested_keys(STEWARD, &altered, &rule), Err(SealError::KeysUnattested { have: 1, need: 2 })); assert_eq!(attested_keys("did:example:other", &attestations, &rule), Err(SealError::KeysUnattested { have: 0, need: 2 })); // a witness that attests other keys later: its latest attestation is the one that counts let liar = HybridKey::new(sk(41), [41; 32]); let other_id = keys_of(&HybridKey::new(sk(8), [10; 32])).id(); let lie = attest_stating(STEWARD, &other_id, &ContentId::of_bytes(&s2), 7, 310, &liar).unwrap(); let with_lie = [attestations.clone(), vec![lie.clone()]].concat(); assert_eq!(attested_keys(STEWARD, &with_lie, &rule).map(|k| k.key_id), Ok(id), "two of three still agree"); assert_eq!(attested_keys(STEWARD, &with_lie, &Witnessing { quorum: 3, ..rule }), Err(SealError::KeysUnattested { have: 2, need: 3 })); // a quorum of no more than half: two witnesses, two key ids, each with one assert_eq!(attested_keys(STEWARD, &[attestations[1].clone(), lie], &Witnessing { quorum: 1, ..rule }), Err(SealError::KeysDisputed)); } #[test] fn an_attestation_follows_the_steward_through_a_change_of_keys() { let old = steward_key(); let new = HybridKey::new(sk(17), [19; 32]); let entries = log(9, Some((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 path = prove_consistency(&entries, 5, 9).unwrap(); let mut ws: Vec = (1..=3).map(|i| witness(i, keys_of(&old).id())).collect(); for w in &mut ws { w.cosign(STEWARD, std::slice::from_ref(&first), &[], 100).unwrap(); } let trusted: Vec = ws.iter().map(|w| w.keys().id()).collect(); let rule = Witnessing { witnesses: &trusted, quorum: 2, not_before: 0 }; let before: Vec> = ws.iter().map(|w| w.attest_keys(STEWARD, 150).unwrap()).collect(); assert_eq!(attested_keys(STEWARD, &before, &rule).map(|k| k.key_id), Ok(keys_of(&old).id())); for w in &mut ws { w.cosign(STEWARD, &[first.clone(), second.clone()], &path, 200).unwrap(); } let after: Vec> = ws.iter().map(|w| w.attest_keys(STEWARD, 250).unwrap()).collect(); let got = attested_keys(STEWARD, &[before, after].concat(), &rule).unwrap(); assert_eq!((got.key_id, got.seal, got.count), (keys_of(&new).id(), ContentId::of_bytes(&second), 9)); assert_eq!(keys_for(&second, &got.key_id), Ok(keys_of(&new))); } #[test] fn a_witness_attests_only_keys_it_has_seen_sign_a_seal() { let w = witness(1, keys_of(&steward_key()).id()); assert_eq!(w.attest_keys(STEWARD, 1), Err(SealError::Unfollowed)); assert_eq!(w.attest_keys("did:example:unwatched", 1), Err(SealError::WrongSteward)); }