open-standards/jcp/jcp-rs/crates/jcp-logseal/tests/seals.rs
Transaction Science acc9e4ca07
All checks were successful
repository checks / lockfiles, advisories, tests in CI, workflow hygiene, spec and section references, public text (push) Successful in 11m9s
Log seals: a steward's fork, and a witness on both sides of one, shown by evidence anyone can check
Witnesses that keep their rule never cosign two lines of seals, and the
spec said a witness that did could be shown to have; nothing showed it.

- verify_fork: two seals of one steward, each read under keys among its
  key ids, whose logs part. Of one count, their roots differ; of two, the
  larger log has, at the smaller seal's last place, a receipt that is not
  the smaller seal's head, shown with its path to the larger root.
- prove_fork: the evidence built by a reader holding the larger log.
- verify_equivocation: a fork with two cosignatures, of its two seals, by
  one witness, which a witness keeping its rule never writes.
- New codes fork_not_shown and equivocation_not_shown; log-seal 5.5, the
  section 6 bullet on witnesses, and C33 in the extension and SPEC.
- log_seal.json: seven forks (one count, two counts, a prefix offered with
  another history's leaf and with its own true leaf, no divergence, one
  seal twice, a foreign key) and three equivocations; verify_seal.py
  decides them on its own with four rules, each noticed when removed.

Seven seeded faults, each caught; the one first missed (a divergence at
the smaller seal's own head) is caught by the prefix-with-its-true-leaf
case added for it.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-11 15:53:27 -05:00

843 lines
44 KiB
Rust

//! 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<Vec<u8>> {
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<u8>], seals: &[Vec<u8>], trust: Trust, policy: Policy) -> Result<Standing, SealError> {
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<Value>) -> Option<Value>) -> Vec<u8> {
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<Value>) -> Option<Value> {
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<ContentId> = 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<u8>], seals: &[Vec<u8>], 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<u8>| read_seal(&bytes, STEWARD, &key.ed_public(), key.pq_public()).unwrap_err();
let code = |bytes: Vec<u8>| 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<ContentId> = (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<ContentId> = 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<Vec<u8>> {
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<Vec<u8>> {
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<u8>], 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<ContentId> = 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<ContentId> = 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<ContentId> = 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<ContentId> {
(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<ContentId> = 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<u8>], seals: &[Vec<u8>], cosigs: &[Vec<u8>]| {
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<u8>]| 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<u8>); 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));
}