smart-byte-core
- `wire`: canonical CBOR for any serde type. `from_slice` refuses bytes that
are not the decoded object's own encoding. Serde otherwise accepts an array
of integers for a byte string, a byte string for text, and an array for a
struct, each a second encoding the SAID does not cover.
- Controller AIDs. Controller signatures are checked against the key states
replayed logs establish. One strict Ed25519 rule.
- The envelope: attested provenance, ownership chain and joule cost. Cargo
carries a registered type tag.
smart-byte-keri-witness
- Events are §2 SAIDs over canonical CBOR.
- The default verifier is spec v1. It replays one event at a time and says
which event it refused. It refuses `dip`, which skipped the pre-rotation
check entirely, plus zero thresholds, duplicate keys, rotations after an
empty `n`, and extension members.
- Extension mode, fixed:
- Witness receipts carried their own key, so anyone could receipt in any
witness's name. Witnesses are now named by their keys, and receipts sign
everything they claim.
- Duplicity evidence was gathered from unverified receipts.
- Recovery accepted any keys; it now needs its witnesses' receipts.
- OOBIs were signed by an embedded key; they are now controller signatures.
- Configuration traits were never enforced, so they are gone.
VC, BBS+, CRDT, mDL, lockstep, libp2p and the CLI move to the new envelope.
smart-byte-conformance
- kel_vectors.json: 25 vectors.
- envelope_vectors.json: 18 vectors, checked against the logs' key states.
- The SAID and signature vectors are rebuilt on the real objects, so all the
files describe one set of parties.
- Planted defects in every file are caught on their own vectors.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
354 lines
11 KiB
Rust
354 lines
11 KiB
Rust
//! The parties the pack is built from: three controllers, the key-event logs
|
|
//! that establish their keys, and the smart bytes they issue and transfer.
|
|
//!
|
|
//! * The **issuer** has one key and has never rotated.
|
|
//! * **Alice** has one key, has rotated once, and has anchored the byte she
|
|
//! was issued in an interaction event.
|
|
//! * The **custodian** holds three keys, any two of which sign, and has
|
|
//! rotated once.
|
|
//!
|
|
//! The issuer issues Alice a USD byte; it passes Alice → custodian → Alice →
|
|
//! custodian, and Alice's second transfer is signed with the key she rotated
|
|
//! to. Every key derives from a published seed, so every signature in the
|
|
//! pack can be reproduced: Ed25519 is deterministic.
|
|
|
|
use std::collections::HashMap;
|
|
|
|
use smart_byte_core::sign::Signer as _;
|
|
use smart_byte_core::{Aid, Cargo, Envelope, JouleCost, KeyBook, Said, Signer, SigningKey};
|
|
use smart_byte_keri_witness::events::{
|
|
InceptionEvent, InteractionEvent, KeyEvent, PublicKey, RotationEvent, Threshold,
|
|
finalize_event, per_key_said,
|
|
};
|
|
use smart_byte_keri_witness::{LogVerifier, SignedEvent, VERSION_STRING};
|
|
|
|
/// The seed of RFC 8032 §7.1 test 1: the issuer's first key.
|
|
pub const RFC8032_TEST1_SEED: &str =
|
|
"9d61b19deffd5a60ba844af492ec2cc44449c5697b326919703bac031cae7f60";
|
|
/// The seed of RFC 8032 §7.1 test 2: Alice's first key.
|
|
pub const RFC8032_TEST2_SEED: &str =
|
|
"4ccd089b28ff96da9db6c346ec114e0f5b8a319f35aba624da8cf6ed4fb8a6fb";
|
|
|
|
/// The names the pack gives the three controllers' logs.
|
|
pub const ISSUER_LOG: &str = "kel-001-single-key-inception";
|
|
pub const ALICE_LOG: &str = "kel-002-rotation-then-interaction";
|
|
pub const CUSTODIAN_LOG: &str = "kel-003-two-of-three-rotation";
|
|
|
|
pub fn hex32(s: &str) -> [u8; 32] {
|
|
hex::decode(s).unwrap().try_into().unwrap()
|
|
}
|
|
|
|
/// A key the pack signs with.
|
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
|
pub struct Key {
|
|
pub seed: [u8; 32],
|
|
}
|
|
|
|
impl Key {
|
|
/// The key named `label`. The issuer's and Alice's first keys are RFC
|
|
/// 8032's test keys, so an implementation can check its Ed25519 against
|
|
/// the RFC; every other seed is BLAKE3 of the label.
|
|
pub fn named(label: &str) -> Key {
|
|
let seed = match label {
|
|
"issuer-0" => hex32(RFC8032_TEST1_SEED),
|
|
"alice-0" => hex32(RFC8032_TEST2_SEED),
|
|
_ => Said::hash(format!("smart-byte/conformance/v1 key {label}").as_bytes()).0,
|
|
};
|
|
Key { seed }
|
|
}
|
|
|
|
pub fn signing(&self) -> SigningKey {
|
|
SigningKey::from_bytes(&self.seed)
|
|
}
|
|
|
|
/// The key as an event writes it: `0x01` and the Ed25519 public key.
|
|
pub fn public(&self) -> PublicKey {
|
|
PublicKey::new(0x01, self.signing().verifying_key().as_bytes())
|
|
}
|
|
|
|
/// Its next-key commitment (identity spec §3.4).
|
|
pub fn digest(&self) -> Said {
|
|
per_key_said(&self.public()).unwrap()
|
|
}
|
|
|
|
/// `0x01` and the Ed25519 signature over `message`.
|
|
pub fn sign(&self, message: &[u8]) -> Vec<u8> {
|
|
[&[0x01][..], &self.signing().sign(message).to_bytes()].concat()
|
|
}
|
|
}
|
|
|
|
pub fn keys(labels: &[&str]) -> Vec<Key> {
|
|
labels.iter().map(|l| Key::named(l)).collect()
|
|
}
|
|
|
|
/// One signature on a logged event, with the seed of the key that made it
|
|
/// (`None` for a signature no seed reproduces).
|
|
#[derive(Clone, Debug)]
|
|
pub struct EventSignature {
|
|
pub index: u32,
|
|
pub signature: Vec<u8>,
|
|
pub seed: Option<[u8; 32]>,
|
|
}
|
|
|
|
/// An event as a log carries it: its bytes and the signatures over them.
|
|
#[derive(Clone, Debug)]
|
|
pub struct LoggedEvent {
|
|
pub bytes: Vec<u8>,
|
|
pub signatures: Vec<EventSignature>,
|
|
}
|
|
|
|
impl LoggedEvent {
|
|
/// `event`, signed by `signers` as `(key index, key)`.
|
|
pub fn signed(event: &KeyEvent, signers: &[(u32, Key)]) -> Self {
|
|
Self::signed_bytes(event.to_cbor().unwrap(), signers)
|
|
}
|
|
|
|
/// `bytes` — which need not be an event — signed by `signers`.
|
|
pub fn signed_bytes(bytes: Vec<u8>, signers: &[(u32, Key)]) -> Self {
|
|
let signatures = signers
|
|
.iter()
|
|
.map(|(index, key)| EventSignature {
|
|
index: *index,
|
|
signature: key.sign(&bytes),
|
|
seed: Some(key.seed),
|
|
})
|
|
.collect();
|
|
Self { bytes, signatures }
|
|
}
|
|
|
|
pub fn wire(&self) -> SignedEvent {
|
|
SignedEvent {
|
|
bytes: self.bytes.clone(),
|
|
signatures: self
|
|
.signatures
|
|
.iter()
|
|
.map(|s| (s.index, s.signature.clone()))
|
|
.collect(),
|
|
}
|
|
}
|
|
|
|
/// The event, for one the reference decodes.
|
|
pub fn event(&self) -> KeyEvent {
|
|
KeyEvent::decode(&self.bytes, false).expect("a well-formed event")
|
|
}
|
|
}
|
|
|
|
pub fn wire_log(log: &[LoggedEvent]) -> Vec<SignedEvent> {
|
|
log.iter().map(LoggedEvent::wire).collect()
|
|
}
|
|
|
|
fn public_keys(k: &[Key]) -> Vec<PublicKey> {
|
|
k.iter().map(Key::public).collect()
|
|
}
|
|
|
|
fn digests(n: &[Key]) -> Vec<Said> {
|
|
n.iter().map(Key::digest).collect()
|
|
}
|
|
|
|
/// An inception event, stamped.
|
|
pub fn inception(k: &[Key], kt: u64, n: &[Key], nt: u64) -> KeyEvent {
|
|
finalize_event(KeyEvent::Inception(InceptionEvent {
|
|
v: VERSION_STRING.into(),
|
|
d: Said::default(),
|
|
i: Aid::default(),
|
|
s: 0,
|
|
p: String::new(),
|
|
kt: Threshold(kt),
|
|
k: public_keys(k),
|
|
nt: Threshold(nt),
|
|
n: digests(n),
|
|
a: vec![],
|
|
b: vec![],
|
|
bt: Threshold(0),
|
|
}))
|
|
.unwrap()
|
|
}
|
|
|
|
/// A rotation following `prior`, stamped.
|
|
pub fn rotation(prior: &KeyEvent, k: &[Key], kt: u64, n: &[Key], nt: u64) -> KeyEvent {
|
|
finalize_event(KeyEvent::Rotation(RotationEvent {
|
|
v: VERSION_STRING.into(),
|
|
d: Said::default(),
|
|
i: *prior.controller(),
|
|
s: prior.sequence() + 1,
|
|
p: prior.said(),
|
|
kt: Threshold(kt),
|
|
k: public_keys(k),
|
|
nt: Threshold(nt),
|
|
n: digests(n),
|
|
a: vec![],
|
|
br: vec![],
|
|
ba: vec![],
|
|
bt: Threshold(0),
|
|
}))
|
|
.unwrap()
|
|
}
|
|
|
|
/// An interaction following `prior`, carrying the key fields in force.
|
|
pub fn interaction(
|
|
prior: &KeyEvent,
|
|
k: &[Key],
|
|
kt: u64,
|
|
n: &[Key],
|
|
nt: u64,
|
|
anchors: Vec<Said>,
|
|
) -> KeyEvent {
|
|
finalize_event(KeyEvent::Interaction(InteractionEvent {
|
|
v: VERSION_STRING.into(),
|
|
d: Said::default(),
|
|
i: *prior.controller(),
|
|
s: prior.sequence() + 1,
|
|
p: prior.said(),
|
|
kt: Threshold(kt),
|
|
k: public_keys(k),
|
|
nt: Threshold(nt),
|
|
n: digests(n),
|
|
a: anchors,
|
|
}))
|
|
.unwrap()
|
|
}
|
|
|
|
/// A controller and its log.
|
|
#[derive(Clone, Debug)]
|
|
pub struct Party {
|
|
pub aid: Aid,
|
|
pub log: Vec<LoggedEvent>,
|
|
}
|
|
|
|
impl Party {
|
|
/// A signer under establishment event `establishment`, signing with
|
|
/// `keys` as `(key index, key)`.
|
|
pub fn signer(&self, establishment: u64, keys: &[(u64, Key)]) -> Signer {
|
|
Signer {
|
|
aid: self.aid,
|
|
establishment,
|
|
keys: keys.iter().map(|(i, k)| (*i, k.signing())).collect(),
|
|
}
|
|
}
|
|
}
|
|
|
|
/// The 16-byte nonce named `label`.
|
|
pub fn nonce(label: &str) -> [u8; 16] {
|
|
Said::hash(format!("smart-byte/conformance/v1 nonce {label}").as_bytes()).0[..16]
|
|
.try_into()
|
|
.unwrap()
|
|
}
|
|
|
|
/// The pack's parties and bytes.
|
|
#[derive(Clone, Debug)]
|
|
pub struct World {
|
|
pub issuer: Party,
|
|
pub alice: Party,
|
|
pub custodian: Party,
|
|
/// The USD byte as issued to Alice.
|
|
pub issued: Envelope,
|
|
/// The same byte after three transfers, ending with the custodian.
|
|
pub transferred: Envelope,
|
|
/// An empty opaque byte, issued to the custodian.
|
|
pub opaque: Envelope,
|
|
}
|
|
|
|
impl World {
|
|
pub fn build() -> World {
|
|
let k = |l: &str| Key::named(l);
|
|
|
|
// The issuer: one key, never rotated.
|
|
let issuer_icp = inception(&[k("issuer-0")], 1, &[k("issuer-1")], 1);
|
|
let issuer = Party {
|
|
aid: *issuer_icp.controller(),
|
|
log: vec![LoggedEvent::signed(&issuer_icp, &[(0, k("issuer-0"))])],
|
|
};
|
|
|
|
// Alice: incepted, issued a byte, rotated, then anchored the byte.
|
|
let alice_icp = inception(&[k("alice-0")], 1, &[k("alice-1")], 1);
|
|
let alice_aid = *alice_icp.controller();
|
|
let issued = Envelope::issue(
|
|
&issuer.signer(0, &[(0, k("issuer-0"))]),
|
|
alice_aid,
|
|
nonce("env-001"),
|
|
Cargo::usd_micros(100_000_000),
|
|
JouleCost {
|
|
measured_microjoules: 1200,
|
|
estimated_microjoules: 1500,
|
|
},
|
|
);
|
|
let alice_rot = rotation(&alice_icp, &[k("alice-1")], 1, &[k("alice-2")], 1);
|
|
let alice_ixn = interaction(
|
|
&alice_rot,
|
|
&[k("alice-1")],
|
|
1,
|
|
&[k("alice-2")],
|
|
1,
|
|
vec![issued.identity()],
|
|
);
|
|
let alice = Party {
|
|
aid: alice_aid,
|
|
log: vec![
|
|
LoggedEvent::signed(&alice_icp, &[(0, k("alice-0"))]),
|
|
LoggedEvent::signed(&alice_rot, &[(0, k("alice-1"))]),
|
|
LoggedEvent::signed(&alice_ixn, &[(0, k("alice-1"))]),
|
|
],
|
|
};
|
|
|
|
// The custodian: two of three keys sign, before and after rotating.
|
|
let c = |n: usize| k(&format!("custodian-{n}"));
|
|
let custodian_icp = inception(&[c(0), c(1), c(2)], 2, &[c(3), c(4), c(5)], 2);
|
|
let custodian_rot = rotation(
|
|
&custodian_icp,
|
|
&[c(3), c(4), c(5)],
|
|
2,
|
|
&[c(6), c(7), c(8)],
|
|
2,
|
|
);
|
|
let custodian = Party {
|
|
aid: *custodian_icp.controller(),
|
|
log: vec![
|
|
LoggedEvent::signed(&custodian_icp, &[(0, c(0)), (1, c(1))]),
|
|
LoggedEvent::signed(&custodian_rot, &[(0, c(3)), (2, c(5))]),
|
|
],
|
|
};
|
|
|
|
let transferred = issued
|
|
.transfer(&alice.signer(0, &[(0, k("alice-0"))]), custodian.aid, 10)
|
|
.transfer(&custodian.signer(0, &[(0, c(0)), (2, c(2))]), alice.aid, 11)
|
|
.transfer(&alice.signer(1, &[(0, k("alice-1"))]), custodian.aid, 12);
|
|
|
|
let opaque = Envelope::issue(
|
|
&issuer.signer(0, &[(0, k("issuer-0"))]),
|
|
custodian.aid,
|
|
[0; 16],
|
|
Cargo::opaque(Vec::new()),
|
|
JouleCost::default(),
|
|
);
|
|
|
|
World {
|
|
issuer,
|
|
alice,
|
|
custodian,
|
|
issued,
|
|
transferred,
|
|
opaque,
|
|
}
|
|
}
|
|
|
|
/// The three logs, by the names the pack gives them.
|
|
pub fn logs(&self) -> [(&'static str, &Party); 3] {
|
|
[
|
|
(ISSUER_LOG, &self.issuer),
|
|
(ALICE_LOG, &self.alice),
|
|
(CUSTODIAN_LOG, &self.custodian),
|
|
]
|
|
}
|
|
|
|
/// The key states the three logs establish.
|
|
pub fn key_book(&self) -> KeyBook {
|
|
let mut book = KeyBook::new();
|
|
for (name, party) in self.logs() {
|
|
LogVerifier::new()
|
|
.verify_encoded(&wire_log(&party.log), &HashMap::new())
|
|
.unwrap_or_else(|r| panic!("{name} is refused at event {}: {}", r.index, r.error))
|
|
.add_to(&mut book);
|
|
}
|
|
book
|
|
}
|
|
}
|