Some checks failed
repository checks / lockfiles, advisories, tests in CI, workflow hygiene, spec and section references, public text (push) Failing after 19m56s
A receipt written after the last seal stood on Ed25519 until the next seal. A steward may now countersign one at once: its hybrid key over a record naming the receipt's content id (jcp:receipt-countersignature, log-seal §5.7). A holder of the Ed25519 key alone cannot make one. A reader needed each steward's key id carried to it by hand (§4). A witness that has cosigned a steward's seal now attests the keys it last saw sign one, with that seal and its count, under its own hybrid key (§4.2). A reader trusting a set of witnesses takes the steward's key id from a quorum of their latest attestations and starts reading from the newest seal they name; fewer than a quorum is keys_unattested, two key ids each with one keys_disputed, and a witness that has cosigned nothing refuses (witness_unfollowed). The pack gains 8 countersigned receipts and 13 attested key cases; the Python second reader decides all of them with its own ML-DSA-65 and notices each of its 13 new rules removed. 13 seeded faults in the Rust code, each caught. C33 and SPEC's summary of it say so. Co-Authored-By: Claude Opus 5.5 <[email protected]>
195 lines
9.6 KiB
Rust
195 lines
9.6 KiB
Rust
//! 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<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: [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<Witness> = (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<ContentId> = ws.iter().map(|w| w.keys().id()).collect();
|
|
let attestations: Vec<Vec<u8>> = 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<Witness> = (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<ContentId> = ws.iter().map(|w| w.keys().id()).collect();
|
|
let rule = Witnessing { witnesses: &trusted, quorum: 2, not_before: 0 };
|
|
let before: Vec<Vec<u8>> = 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<Vec<u8>> = 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));
|
|
}
|