All checks were successful
repository checks / lockfiles, advisories, tests in CI, workflow hygiene, spec and section references, public text (push) Successful in 11m9s
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]>
843 lines
44 KiB
Rust
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));
|
|
}
|
|
|