Skip to main content

auths_keri/
ksn.rs

1//! Key-State Notice (KSN) — a signed snapshot of an identity's current key-state.
2//!
3//! A KSN lets a thin/CI client trust a key-state without replaying the full KEL.
4//! Under `kt=1` with no witnesses (`docs/architecture/multi_device_accepted_risks.md`)
5//! a controller-signed KSN is **trust-on-first-sight only**: it proves "a holder
6//! of the key this state names as current asserts this state" — circular until
7//! Epic D adds witness receipts. It is therefore a *latency optimization*, never
8//! a *trust upgrade*: never authoritative when the full KEL is resolvable, and
9//! never sufficient for a revocation check (revocation is a root-KEL fact). See
10//! `SignedKsn` for the verification rules.
11//!
12//! Wire shape (auths-only — not keripy/keria byte-interop, see Epic 4):
13//! - [`KeyStateNotice`] is the controller-signed body: `{version, t:"ksn", state, dt}`.
14//!   Serialized in struct-declaration order (deterministic via serde_json
15//!   `preserve_order`) — the bytes the controller signs.
16//! - [`SignedKsn`] wraps the body with the detached controller signature and a
17//!   **reserved** `receipts` slot for Epic D witness receipts. The receipts are
18//!   NOT covered by the controller signature (witnesses receipt the signed
19//!   notice after the fact), so populating the slot later does not invalidate it.
20
21use serde::{Deserialize, Serialize};
22
23use crate::events::Event;
24use crate::types::{ConfigTrait, Prefix, Said, Threshold};
25use crate::witness::StoredReceipt;
26use crate::witness::agreement::{AgreementStatus, WitnessAgreement};
27use crate::{CesrKey, KeyState};
28
29/// Current KSN schema version.
30pub const KSN_VERSION: u32 = 1;
31
32/// The `t` discriminator for a Key-State Notice.
33pub const KSN_TYPE: &str = "ksn";
34
35/// The KERI protocol major/minor version a key-state record reports in `vn`.
36/// Matches the `KERI10` wire generation (keripy `KeyStateRecord.vn == [1, 0]`).
37pub const KERI_KEY_STATE_VERSION: [u32; 2] = [1, 0];
38
39/// The latest establishment-event summary carried in a [`KeyStateRecord`] `ee`
40/// field: the sequence and SAID of the most recent `icp`/`rot`/`dip`/`drt`, plus
41/// the witnesses cut (`br`) and added (`ba`) by that event.
42///
43/// Mirrors keripy's `KeyStateRecord.ee` sub-record (`{s, d, br, ba}`).
44#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
45pub struct LatestEstablishmentEvent {
46    /// Sequence number of the latest establishment event, lowercase-hex (`"0"`).
47    pub s: String,
48    /// SAID of the latest establishment event.
49    pub d: Said,
50    /// Witnesses removed by the latest establishment event (rotation cuts).
51    #[serde(default)]
52    pub br: Vec<Prefix>,
53    /// Witnesses added by the latest establishment event (rotation adds).
54    #[serde(default)]
55    pub ba: Vec<Prefix>,
56}
57
58/// A **KERI-conformant key-state notice** — the wire record keripy emits as a
59/// `ksn`/`rpy` reply and persists as `KeyStateRecord`.
60///
61/// This is the byte-interoperable counterpart to the auths-internal
62/// [`KeyStateNotice`]: where `KeyStateNotice` is an auths-only envelope around a
63/// [`KeyState`], `KeyStateRecord` is the canonical KERI shape a peer (keripy,
64/// keriox) produces and consumes — field order and labels
65/// `{vn, i, s, p, d, f, dt, et, kt, k, nt, n, bt, b, c, ee, di}`, sequence
66/// numbers as lowercase hex, thresholds as KERI hex/clause strings.
67///
68/// It is a *parsed* type: holding one means the labels and shapes already
69/// matched the KERI form, so [`KeyStateRecord::into_key_state`] is total. Build
70/// one from an auths KEL with [`KeyStateRecord::from_kel`] (emit a record a peer
71/// can read); accept one from a peer by deserializing then
72/// [`into_key_state`](KeyStateRecord::into_key_state) (consume a keripy KSN).
73#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
74pub struct KeyStateRecord {
75    /// Protocol version `[major, minor]` — `[1, 0]` for the `KERI10` generation.
76    pub vn: [u32; 2],
77    /// Identifier prefix (the AID this state describes).
78    pub i: Prefix,
79    /// Sequence number of the latest event, lowercase-hex.
80    pub s: String,
81    /// SAID of the prior event (empty at inception).
82    pub p: String,
83    /// SAID of the latest event.
84    pub d: Said,
85    /// First-seen ordinal. auths does not maintain a first-seen log separate from
86    /// the KEL, so this mirrors `s` (the latest sequence) — truthful for a
87    /// single-source replay, where first-seen order *is* event order.
88    pub f: String,
89    /// Controller-asserted timestamp (RFC 3339).
90    pub dt: String,
91    /// Latest establishment event type (`icp`/`rot`/`dip`/`drt`).
92    pub et: String,
93    /// Current signing threshold (KERI hex/clause string).
94    pub kt: Threshold,
95    /// Current signing key(s), CESR-encoded.
96    pub k: Vec<CesrKey>,
97    /// Next-key threshold (KERI hex/clause string).
98    pub nt: Threshold,
99    /// Next-key commitment digest(s).
100    pub n: Vec<Said>,
101    /// Backer (witness) threshold (`bt`, hex string).
102    pub bt: Threshold,
103    /// Current backer (witness) list.
104    pub b: Vec<Prefix>,
105    /// Configuration traits.
106    pub c: Vec<ConfigTrait>,
107    /// Latest establishment event summary (`{s, d, br, ba}`).
108    pub ee: LatestEstablishmentEvent,
109    /// Delegator AID (empty string when not delegated).
110    pub di: String,
111}
112
113impl KeyStateRecord {
114    /// Build a KERI key-state record by replaying a validated KEL into its
115    /// current state, stamped at `dt`.
116    ///
117    /// `events` is the full, in-order KEL (inception first); the record's `s`/`d`
118    /// come from the last event and `p`/`ee`/`et` from its latest establishment
119    /// event. Returns `None` only if `events` is empty (no inception to anchor a
120    /// state) — the caller has nothing to notice.
121    ///
122    /// Args:
123    /// * `events`: The replayed KEL, in sequence order.
124    /// * `state`: The resolved current [`KeyState`] (from `replay`).
125    /// * `dt`: An RFC-3339 timestamp (injected `now`).
126    pub fn from_kel(events: &[Event], state: &KeyState, dt: impl Into<String>) -> Option<Self> {
127        let last = events.last()?;
128        let latest_est = events.iter().rev().find(|e| !e.is_interaction())?;
129        Some(Self {
130            vn: KERI_KEY_STATE_VERSION,
131            i: state.prefix.clone(),
132            s: format!("{:x}", state.sequence),
133            p: last
134                .previous()
135                .map(|s| s.as_str().to_string())
136                .unwrap_or_default(),
137            d: state.last_event_said.clone(),
138            f: format!("{:x}", state.sequence),
139            dt: dt.into(),
140            et: establishment_type(latest_est).to_string(),
141            kt: state.threshold.clone(),
142            k: state.current_keys.clone(),
143            nt: state.next_threshold.clone(),
144            n: state.next_commitment.clone(),
145            bt: state.backer_threshold.clone(),
146            b: state.backers.clone(),
147            c: state.config_traits.clone(),
148            ee: LatestEstablishmentEvent {
149                s: format!("{:x}", latest_est.sequence().value()),
150                d: latest_est.said().clone(),
151                br: Vec::new(),
152                ba: Vec::new(),
153            },
154            di: state
155                .delegator
156                .as_ref()
157                .map(|p| p.as_str().to_string())
158                .unwrap_or_default(),
159        })
160    }
161
162    /// The sequence number this record notices (the latest event's `s`, decoded
163    /// from its lowercase-hex wire form).
164    pub fn sequence(&self) -> u128 {
165        u128::from_str_radix(self.s.trim_start_matches("0x"), 16).unwrap_or(0)
166    }
167
168    /// Reject this notice if it is older than a state the verifier already trusts.
169    ///
170    /// A key-state notice is a snapshot; a thin client that has already seen
171    /// sequence `last_seen_seq` (e.g. it holds a fresher witness receipt) must not
172    /// accept a notice that rewinds below it — that is a stale or replayed view of
173    /// the identity. Returns [`KsnError::Stale`] when `self.sequence() <
174    /// last_seen_seq`; equal-or-newer is fine.
175    ///
176    /// Args:
177    /// * `last_seen_seq`: The highest sequence the verifier already trusts.
178    pub fn check_not_stale(&self, last_seen_seq: u128) -> Result<(), KsnError> {
179        let got = self.sequence();
180        if got < last_seen_seq {
181            return Err(KsnError::Stale {
182                got,
183                seen: last_seen_seq,
184            });
185        }
186        Ok(())
187    }
188
189    /// Project this KERI record back to the auths [`KeyState`] the rest of the
190    /// platform reasons over (a thin client ingesting a peer's published state).
191    ///
192    /// Total: a parsed `KeyStateRecord` already carries the labels and shapes a
193    /// `KeyState` needs, so no field can be missing or mistyped here.
194    pub fn into_key_state(self) -> KeyState {
195        let sequence = u128::from_str_radix(self.s.trim_start_matches("0x"), 16).unwrap_or(0);
196        let last_est_seq =
197            u128::from_str_radix(self.ee.s.trim_start_matches("0x"), 16).unwrap_or(0);
198        let delegator = if self.di.is_empty() {
199            None
200        } else {
201            Some(Prefix::new_unchecked(self.di))
202        };
203        KeyState {
204            prefix: self.i,
205            current_keys: self.k,
206            next_commitment: self.n.clone(),
207            sequence,
208            last_event_said: self.d,
209            is_abandoned: self.n.is_empty() && self.et != "icp" && self.et != "dip",
210            threshold: self.kt,
211            next_threshold: self.nt,
212            backers: self.b,
213            backer_threshold: self.bt,
214            config_traits: self.c,
215            is_non_transferable: self.n.is_empty(),
216            delegator,
217            last_establishment_sequence: last_est_seq,
218        }
219    }
220}
221
222/// The KERI `et` value for an establishment event (`icp`/`rot`/`dip`/`drt`).
223/// Callers pass only establishment events (`!is_interaction`); a stray `ixn`
224/// falls through to its own tag rather than panicking.
225fn establishment_type(event: &Event) -> &'static str {
226    match event {
227        Event::Icp(_) => "icp",
228        Event::Rot(_) => "rot",
229        Event::Dip(_) => "dip",
230        Event::Drt(_) => "drt",
231        Event::Ixn(_) => "ixn",
232    }
233}
234
235/// Errors building or verifying a KSN.
236#[derive(Debug, thiserror::Error)]
237#[non_exhaustive]
238pub enum KsnError {
239    /// Serializing the notice to its canonical bytes failed.
240    #[error("KSN serialization failed: {0}")]
241    Serialize(String),
242
243    /// The provided signer returned an error.
244    #[error("KSN signing failed: {0}")]
245    Signer(String),
246
247    /// The notice names no current key (abandoned/empty) — nothing to sign or
248    /// verify against.
249    #[error("KSN names no current key")]
250    NoCurrentKey,
251
252    /// The `t` discriminator was not `"ksn"`.
253    #[error("not a KSN (t = {0:?})")]
254    WrongType(String),
255
256    /// The signature did not verify against the noticed current key.
257    #[error("KSN signature is invalid")]
258    BadSignature,
259
260    /// The current key could not be decoded / its curve is unsupported.
261    #[error("KSN current key is undecodable: {0}")]
262    UndecodableKey(String),
263
264    /// The notice is older than a previously-trusted state for this prefix
265    /// (rollback).
266    #[error("KSN is stale: seq {got} < last-seen {seen}")]
267    Stale {
268        /// The (rejected) notice sequence.
269        got: u128,
270        /// The last-seen sequence for this prefix.
271        seen: u128,
272    },
273}
274
275/// The controller-signed body of a Key-State Notice.
276#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
277pub struct KeyStateNotice {
278    /// Schema version.
279    pub version: u32,
280    /// Message type discriminator (always `"ksn"`).
281    pub t: String,
282    /// The key-state snapshot being noticed (carries `prefix`, `current_keys`,
283    /// `sequence`, `delegator`, thresholds, backers, …).
284    pub state: KeyState,
285    /// Controller-asserted timestamp (RFC 3339). Injected by the caller — never
286    /// `Utc::now()` in core.
287    pub dt: String,
288}
289
290impl KeyStateNotice {
291    /// Build a notice over `state` stamped at `dt`.
292    ///
293    /// Args:
294    /// * `state`: The key-state to notice.
295    /// * `dt`: An RFC-3339 timestamp (injected `now`).
296    pub fn new(state: KeyState, dt: impl Into<String>) -> Self {
297        Self {
298            version: KSN_VERSION,
299            t: KSN_TYPE.to_string(),
300            state,
301            dt: dt.into(),
302        }
303    }
304
305    /// The deterministic canonical bytes the controller signs (struct-order JSON).
306    pub fn canonical_bytes(&self) -> Result<Vec<u8>, KsnError> {
307        serde_json::to_vec(self).map_err(|e| KsnError::Serialize(e.to_string()))
308    }
309
310    /// The current signing key this notice claims, if any.
311    pub fn signing_key(&self) -> Option<&CesrKey> {
312        self.state.current_keys.first()
313    }
314
315    /// The noticed sequence number.
316    pub fn sequence(&self) -> u128 {
317        self.state.sequence
318    }
319
320    /// Whether this notice describes a *delegated* identity (a device). A
321    /// delegated KSN names device state and its delegator (`state.delegator`) but
322    /// is **insufficient for a revocation check** — revocation is anchored in the
323    /// root KEL, not the device's own key-state.
324    pub fn names_delegated_device(&self) -> bool {
325        self.state.delegator.is_some()
326    }
327}
328
329/// A [`KeyStateNotice`] paired with its detached controller signature and the
330/// reserved Epic-D witness-receipt slot.
331#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
332pub struct SignedKsn {
333    /// The signed notice body.
334    pub notice: KeyStateNotice,
335    /// The controller signature over `notice.canonical_bytes()`, hex-encoded for
336    /// JSON.
337    #[serde(with = "hex::serde")]
338    pub signature: Vec<u8>,
339    /// Witness receipts over the noticed establishment event (Epic D), each
340    /// carrying its **witness AID** ([`StoredReceipt`]). NOT covered by the
341    /// controller signature — witnesses receipt the signed notice after the fact,
342    /// so populating this slot never invalidates the controller signature. Empty
343    /// (and omitted) leaves the verdict at trust-on-first-sight.
344    #[serde(default, skip_serializing_if = "Vec::is_empty")]
345    pub receipts: Vec<StoredReceipt>,
346}
347
348impl SignedKsn {
349    /// Build a signed KSN by signing the notice's canonical bytes with `signer`.
350    ///
351    /// `signer` is a closure that returns the detached signature for the given
352    /// bytes — keychain-backed in production, a test key in tests. The reserved
353    /// `receipts` slot starts empty.
354    ///
355    /// Args:
356    /// * `notice`: The notice body to sign.
357    /// * `signer`: Produces the signature over the canonical bytes.
358    ///
359    /// Usage:
360    /// ```ignore
361    /// let signed = SignedKsn::sign_with(notice, |bytes| key_ops_sign(seed, bytes))?;
362    /// ```
363    pub fn sign_with(
364        notice: KeyStateNotice,
365        signer: impl FnOnce(&[u8]) -> Result<Vec<u8>, String>,
366    ) -> Result<Self, KsnError> {
367        if notice.signing_key().is_none() {
368            return Err(KsnError::NoCurrentKey);
369        }
370        let bytes = notice.canonical_bytes()?;
371        let signature = signer(&bytes).map_err(KsnError::Signer)?;
372        Ok(Self {
373            notice,
374            signature,
375            receipts: Vec::new(),
376        })
377    }
378
379    /// Attach witness receipts to a signed notice, admitting only those that are
380    /// cryptographically valid for *this* notice.
381    ///
382    /// A candidate is kept only if it (a) receipts the noticed establishment event
383    /// (`state.last_event_said`), (b) is from a witness in `state.backers`, and
384    /// (c) carries a signature that verifies against that witness's pinned key.
385    /// The slot is outside the controller-signed bytes, so attaching never
386    /// invalidates the controller signature.
387    ///
388    /// Args:
389    /// * `candidates`: Collected receipts to vet and attach.
390    ///
391    /// Usage:
392    /// ```ignore
393    /// let published = signed.with_receipts(collected);
394    /// ```
395    pub fn with_receipts(mut self, candidates: Vec<StoredReceipt>) -> Self {
396        let state = &self.notice.state;
397        let said = state.last_event_said.clone();
398        let valid: Vec<StoredReceipt> = candidates
399            .into_iter()
400            .filter(|r| {
401                r.signed.receipt.d == said
402                    && state.backers.iter().any(|b| b == &r.witness)
403                    && receipt_signature_valid(r)
404            })
405            .collect();
406        self.receipts = valid;
407        self
408    }
409
410    /// Verify a KSN and return its (trust-on-first-sight) verdict.
411    ///
412    /// The full forgery-rejection checklist:
413    /// 1. `t` is `"ksn"` (else [`KsnError::WrongType`]).
414    /// 2. the notice names a current key (else [`KsnError::NoCurrentKey`]).
415    /// 3. that key decodes (curve from its CESR tag; else [`KsnError::UndecodableKey`]).
416    /// 4. the signature verifies over the notice's canonical bytes by that key —
417    ///    i.e. the signer **is** the key the state names as current (self-attested);
418    ///    else [`KsnError::BadSignature`].
419    ///
420    /// This does NOT check freshness — call [`SignedKsn::check_not_stale`] against
421    /// the last-trusted sequence to reject a rollback. A bare KSN is
422    /// trust-on-first-sight: see [`VerifiedKsn`].
423    pub fn verify(&self) -> Result<VerifiedKsn, KsnError> {
424        if self.notice.t != KSN_TYPE {
425            return Err(KsnError::WrongType(self.notice.t.clone()));
426        }
427        let key_cesr = self.notice.signing_key().ok_or(KsnError::NoCurrentKey)?;
428        let key = crate::KeriPublicKey::parse(key_cesr.as_str())
429            .map_err(|e| KsnError::UndecodableKey(e.to_string()))?;
430        let bytes = self.notice.canonical_bytes()?;
431        key.verify_signature(&bytes, &self.signature)
432            .map_err(|_| KsnError::BadSignature)?;
433        Ok(VerifiedKsn {
434            state: self.notice.state.clone(),
435            trust: self.witness_trust(),
436        })
437    }
438
439    /// The witness-quorum trust upgrade over the slot's receipts.
440    ///
441    /// Runs KAWA ([`WitnessAgreement`]) over the receipts that attest the noticed
442    /// establishment event (`state.last_event_said`) from witnesses in
443    /// `state.backers`, deduped by witness AID. M-of-N (`state.backer_threshold`)
444    /// met → [`KsnTrust::Witnessed`]; otherwise [`KsnTrust::TrustOnFirstSight`].
445    /// A `bt=0` / backerless KSN stays trust-on-first-sight.
446    fn witness_trust(&self) -> KsnTrust {
447        let state = &self.notice.state;
448        let required = state.backer_threshold.simple_value().unwrap_or(0) as usize;
449        if state.backers.is_empty() || required == 0 {
450            return KsnTrust::TrustOnFirstSight;
451        }
452
453        let said = &state.last_event_said;
454        let sn = state.sequence as u64;
455        let agreement = WitnessAgreement::new(1);
456        agreement.submit_event(
457            &state.prefix,
458            sn,
459            said,
460            &state.backer_threshold,
461            &state.backers,
462        );
463
464        let mut distinct = std::collections::HashSet::new();
465        for r in &self.receipts {
466            // Only correct-SAID, designated-witness receipts count toward quorum;
467            // KAWA additionally dedupes and ignores non-designated witnesses.
468            if &r.signed.receipt.d == said && state.backers.iter().any(|b| b == &r.witness) {
469                agreement.add_receipt(&state.prefix, sn, said, r.witness.as_str());
470                distinct.insert(r.witness.as_str());
471            }
472        }
473
474        match agreement.status(&state.prefix, sn, said) {
475            AgreementStatus::Accepted => KsnTrust::Witnessed {
476                receipts: distinct.len(),
477                threshold: required,
478            },
479            AgreementStatus::Pending { .. } => KsnTrust::TrustOnFirstSight,
480        }
481    }
482
483    /// Monotonicity guard: reject a notice older than a previously-trusted
484    /// sequence for this prefix (a rollback / replay of stale state).
485    ///
486    /// Args:
487    /// * `last_seen_seq`: The highest sequence already trusted for this prefix.
488    pub fn check_not_stale(&self, last_seen_seq: u128) -> Result<(), KsnError> {
489        let got = self.notice.sequence();
490        if got < last_seen_seq {
491            return Err(KsnError::Stale {
492                got,
493                seen: last_seen_seq,
494            });
495        }
496        Ok(())
497    }
498}
499
500/// Verify a stored receipt's detached signature against its pinned witness key
501/// (curve-correct via the AID's CESR tag). Reused by attach-time vetting.
502fn receipt_signature_valid(stored: &StoredReceipt) -> bool {
503    let Ok(key) = crate::KeriPublicKey::parse(stored.witness.as_str()) else {
504        return false;
505    };
506    let Ok(payload) = serde_json::to_vec(&stored.signed.receipt) else {
507        return false;
508    };
509    key.verify_signature(&payload, &stored.signed.signature)
510        .is_ok()
511}
512
513/// The trust level a verified KSN confers.
514///
515/// Under `kt=1` with no witnesses, a controller-signed KSN is only
516/// trust-on-first-sight — Epic D will add a `Witnessed` level once backer
517/// receipts populate the reserved [`SignedKsn::receipts`] slot.
518#[derive(Debug, Clone, Copy, PartialEq, Eq)]
519#[non_exhaustive]
520pub enum KsnTrust {
521    /// Controller-signed only: proves "a holder of the key this state names as
522    /// current asserts this state" — circular under `kt=1`. A latency
523    /// optimization, never a trust upgrade.
524    TrustOnFirstSight,
525    /// Controller-signed **and** witness-receipted: M-of-N designated witnesses
526    /// (`state.backers`/`backer_threshold`) receipted the noticed establishment
527    /// event. No longer trust-on-first-sight — but still never authoritative over
528    /// a resolvable KEL, and a delegated-device KSN still cannot prove
529    /// non-revocation (a root-KEL fact). See [`VerifiedKsn`].
530    Witnessed {
531        /// Distinct, designated, correct-SAID witness receipts counted.
532        receipts: usize,
533        /// The required backer threshold (`bt`).
534        threshold: usize,
535    },
536}
537
538/// A verified Key-State Notice and the trust it confers.
539#[derive(Debug, Clone, PartialEq, Eq)]
540pub struct VerifiedKsn {
541    /// The verified key-state.
542    pub state: KeyState,
543    /// The trust level (TOFU in v1).
544    pub trust: KsnTrust,
545}
546
547impl VerifiedKsn {
548    /// Whether this KSN may be trusted **over** a resolvable full KEL. Always
549    /// `false`, even when [`KsnTrust::Witnessed`]: when the KEL is available,
550    /// replay it — a KSN (witnessed or not) is only a shortcut for clients that
551    /// cannot. Witnessing changes the trust level consumers gate on, not this
552    /// invariant.
553    pub fn is_authoritative_over_kel(&self) -> bool {
554        false
555    }
556
557    /// Whether this KSN may satisfy a revocation check. Always `false`, even when
558    /// [`KsnTrust::Witnessed`]: revocation is anchored in the root KEL as an
559    /// `ixn` fact, not a device's self-asserted key-state, and witness receipts
560    /// attest the *establishment event*, not non-revocation.
561    pub fn satisfies_revocation_check(&self) -> bool {
562        false
563    }
564}
565
566#[cfg(test)]
567#[allow(clippy::unwrap_used, clippy::expect_used)]
568mod tests {
569    use super::*;
570    use crate::{KeriPublicKey, Prefix, Said, Threshold};
571    use ring::rand::SystemRandom;
572    use ring::signature::{Ed25519KeyPair, KeyPair};
573
574    fn real_keypair() -> Ed25519KeyPair {
575        let rng = SystemRandom::new();
576        let pkcs8 = Ed25519KeyPair::generate_pkcs8(&rng).unwrap();
577        Ed25519KeyPair::from_pkcs8(pkcs8.as_ref()).unwrap()
578    }
579
580    /// A key-state whose current key is `kp`'s public key (CESR-encoded).
581    fn state_for_key(kp: &Ed25519KeyPair, seq: u128) -> KeyState {
582        let cesr = KeriPublicKey::ed25519(kp.public_key().as_ref())
583            .unwrap()
584            .to_qb64()
585            .unwrap();
586        let mut state = key_state(seq, false);
587        state.current_keys = vec![CesrKey::new_unchecked(cesr)];
588        state
589    }
590
591    fn sign_ksn(kp: &Ed25519KeyPair, notice: KeyStateNotice) -> SignedKsn {
592        SignedKsn::sign_with(notice, |bytes| Ok(kp.sign(bytes).as_ref().to_vec())).unwrap()
593    }
594
595    fn key_state(seq: u128, delegated: bool) -> KeyState {
596        let key = KeriPublicKey::ed25519(&[3u8; 32]).unwrap();
597        let mut state = KeyState::from_inception(
598            Prefix::new_unchecked("EksnTestPrefix000000000000000000000000000000".to_string()),
599            vec![CesrKey::new_unchecked(key.to_qb64().unwrap())],
600            vec![Said::new_unchecked(
601                "ENextCommitment0000000000000000000000000000".to_string(),
602            )],
603            Threshold::Simple(1),
604            Threshold::Simple(1),
605            Said::new_unchecked("ELastEvent00000000000000000000000000000000000".to_string()),
606            vec![],
607            Threshold::Simple(0),
608            vec![],
609        );
610        state.sequence = seq;
611        if delegated {
612            state.delegator = Some(Prefix::new_unchecked(
613                "ERootDelegator00000000000000000000000000000".to_string(),
614            ));
615        }
616        state
617    }
618
619    #[test]
620    fn canonical_bytes_is_deterministic() {
621        let notice = KeyStateNotice::new(key_state(2, false), "2026-06-03T00:00:00Z");
622        assert_eq!(
623            notice.canonical_bytes().unwrap(),
624            notice.canonical_bytes().unwrap()
625        );
626    }
627
628    #[test]
629    fn sign_with_and_round_trips() {
630        let notice = KeyStateNotice::new(key_state(0, false), "2026-06-03T00:00:00Z");
631        let signed = SignedKsn::sign_with(notice, |_| Ok(vec![7u8; 64])).unwrap();
632        assert_eq!(signed.signature, vec![7u8; 64]);
633        assert!(signed.receipts.is_empty());
634
635        let json = serde_json::to_string(&signed).unwrap();
636        // The empty reserved slot is omitted on the wire...
637        assert!(!json.contains("receipts"));
638        // ...and round-trips back to an equal value (receipts default to empty).
639        let parsed: SignedKsn = serde_json::from_str(&json).unwrap();
640        assert_eq!(parsed, signed);
641    }
642
643    #[test]
644    fn sign_with_rejects_no_current_key() {
645        let mut state = key_state(0, false);
646        state.current_keys.clear();
647        let notice = KeyStateNotice::new(state, "2026-06-03T00:00:00Z");
648        let err = SignedKsn::sign_with(notice, |_| Ok(vec![0u8; 64])).unwrap_err();
649        assert!(matches!(err, KsnError::NoCurrentKey));
650    }
651
652    #[test]
653    fn signer_error_propagates() {
654        let notice = KeyStateNotice::new(key_state(0, false), "2026-06-03T00:00:00Z");
655        let err = SignedKsn::sign_with(notice, |_| Err("keychain locked".to_string())).unwrap_err();
656        assert!(matches!(err, KsnError::Signer(_)));
657    }
658
659    #[test]
660    fn delegated_device_is_flagged() {
661        assert!(KeyStateNotice::new(key_state(1, true), "t").names_delegated_device());
662        assert!(!KeyStateNotice::new(key_state(1, false), "t").names_delegated_device());
663    }
664
665    #[test]
666    fn verify_accepts_valid_ksn() {
667        let kp = real_keypair();
668        let notice = KeyStateNotice::new(state_for_key(&kp, 1), "2026-06-03T00:00:00Z");
669        let signed = sign_ksn(&kp, notice);
670        let verified = signed.verify().unwrap();
671        assert_eq!(verified.trust, KsnTrust::TrustOnFirstSight);
672        // A KSN is never authoritative over a KEL and never satisfies revocation.
673        assert!(!verified.is_authoritative_over_kel());
674        assert!(!verified.satisfies_revocation_check());
675    }
676
677    #[test]
678    fn verify_rejects_tampered_notice() {
679        let kp = real_keypair();
680        let notice = KeyStateNotice::new(state_for_key(&kp, 1), "2026-06-03T00:00:00Z");
681        let mut signed = sign_ksn(&kp, notice);
682        signed.notice.dt = "2099-01-01T00:00:00Z".to_string(); // mutate after signing
683        assert!(matches!(signed.verify(), Err(KsnError::BadSignature)));
684    }
685
686    #[test]
687    fn verify_rejects_signature_by_non_current_key() {
688        let signer = real_keypair();
689        let other = real_keypair();
690        // The state names `other` as current, but `signer` produced the signature.
691        let notice = KeyStateNotice::new(state_for_key(&other, 1), "2026-06-03T00:00:00Z");
692        let signed = sign_ksn(&signer, notice);
693        assert!(matches!(signed.verify(), Err(KsnError::BadSignature)));
694    }
695
696    #[test]
697    fn verify_rejects_unsigned_or_garbage_signature() {
698        let kp = real_keypair();
699        let notice = KeyStateNotice::new(state_for_key(&kp, 1), "2026-06-03T00:00:00Z");
700        let mut signed = sign_ksn(&kp, notice);
701        signed.signature = vec![0u8; 64]; // a forged / unsigned signature
702        assert!(matches!(signed.verify(), Err(KsnError::BadSignature)));
703    }
704
705    #[test]
706    fn verify_rejects_wrong_type() {
707        let kp = real_keypair();
708        let mut notice = KeyStateNotice::new(state_for_key(&kp, 1), "t");
709        notice.t = "rpy".to_string();
710        let signed = sign_ksn(&kp, notice);
711        assert!(matches!(signed.verify(), Err(KsnError::WrongType(_))));
712    }
713
714    #[test]
715    fn check_not_stale_rejects_rollback() {
716        let kp = real_keypair();
717        let signed = sign_ksn(&kp, KeyStateNotice::new(state_for_key(&kp, 2), "t"));
718        assert!(matches!(
719            signed.check_not_stale(3),
720            Err(KsnError::Stale { .. })
721        ));
722        assert!(signed.check_not_stale(2).is_ok());
723        assert!(signed.check_not_stale(1).is_ok());
724    }
725
726    // ── D.13: KSN witness-hardening ──────────────────────────────────────────
727
728    use crate::witness::{Receipt, ReceiptTag, SignedReceipt};
729    use crate::{KeriSequence, VersionString};
730
731    /// A witness keypair and its CESR AID (`D…`).
732    fn witness_kp_and_aid() -> (Ed25519KeyPair, String) {
733        let kp = real_keypair();
734        let aid = KeriPublicKey::ed25519(kp.public_key().as_ref())
735            .unwrap()
736            .to_qb64()
737            .unwrap();
738        (kp, aid)
739    }
740
741    /// A stored receipt by `witness_kp` (AID `witness_aid`) over `(controller, seq, event_said)`.
742    fn witness_receipt(
743        witness_kp: &Ed25519KeyPair,
744        witness_aid: &str,
745        controller: &str,
746        seq: u128,
747        event_said: &str,
748    ) -> StoredReceipt {
749        let receipt = Receipt {
750            v: VersionString::placeholder(),
751            t: ReceiptTag,
752            d: Said::new_unchecked(event_said.to_string()),
753            i: Prefix::new_unchecked(controller.to_string()),
754            s: KeriSequence::new(seq),
755        };
756        let payload = serde_json::to_vec(&receipt).unwrap();
757        let signature = witness_kp.sign(&payload).as_ref().to_vec();
758        StoredReceipt {
759            signed: SignedReceipt { receipt, signature },
760            witness: Prefix::new_unchecked(witness_aid.to_string()),
761        }
762    }
763
764    /// A controller key-state at `seq` designating `backers` with threshold `bt`.
765    fn witnessed_state(
766        controller_kp: &Ed25519KeyPair,
767        seq: u128,
768        backers: &[&str],
769        bt: u64,
770        delegated: bool,
771    ) -> KeyState {
772        let mut state = state_for_key(controller_kp, seq);
773        state.backers = backers
774            .iter()
775            .map(|a| Prefix::new_unchecked(a.to_string()))
776            .collect();
777        state.backer_threshold = Threshold::Simple(bt);
778        if delegated {
779            state.delegator = Some(Prefix::new_unchecked(
780                "ERootDelegator00000000000000000000000000000".to_string(),
781            ));
782        }
783        state
784    }
785
786    #[test]
787    fn ksn_witnessed_when_quorum_met() {
788        let ckp = real_keypair();
789        let (w1kp, w1) = witness_kp_and_aid();
790        let (w2kp, w2) = witness_kp_and_aid();
791        let state = witnessed_state(&ckp, 1, &[&w1, &w2], 2, false);
792        let controller = state.prefix.as_str().to_string();
793        let said = state.last_event_said.as_str().to_string();
794        let signed = sign_ksn(&ckp, KeyStateNotice::new(state, "t")).with_receipts(vec![
795            witness_receipt(&w1kp, &w1, &controller, 1, &said),
796            witness_receipt(&w2kp, &w2, &controller, 1, &said),
797        ]);
798        assert_eq!(
799            signed.verify().unwrap().trust,
800            KsnTrust::Witnessed {
801                receipts: 2,
802                threshold: 2
803            }
804        );
805    }
806
807    #[test]
808    fn ksn_stays_tofu_under_quorum() {
809        let ckp = real_keypair();
810        let (w1kp, w1) = witness_kp_and_aid();
811        let (_w2kp, w2) = witness_kp_and_aid();
812        let state = witnessed_state(&ckp, 1, &[&w1, &w2], 2, false);
813        let controller = state.prefix.as_str().to_string();
814        let said = state.last_event_said.as_str().to_string();
815        let signed = sign_ksn(&ckp, KeyStateNotice::new(state, "t"))
816            .with_receipts(vec![witness_receipt(&w1kp, &w1, &controller, 1, &said)]);
817        assert_eq!(signed.verify().unwrap().trust, KsnTrust::TrustOnFirstSight);
818    }
819
820    #[test]
821    fn ksn_ignores_duplicate_witness_receipts() {
822        let ckp = real_keypair();
823        let (w1kp, w1) = witness_kp_and_aid();
824        let (_w2kp, w2) = witness_kp_and_aid();
825        let (_w3kp, w3) = witness_kp_and_aid();
826        let state = witnessed_state(&ckp, 1, &[&w1, &w2, &w3], 2, false);
827        let controller = state.prefix.as_str().to_string();
828        let said = state.last_event_said.as_str().to_string();
829        // The same witness twice must not satisfy a threshold of 2.
830        let signed = sign_ksn(&ckp, KeyStateNotice::new(state, "t")).with_receipts(vec![
831            witness_receipt(&w1kp, &w1, &controller, 1, &said),
832            witness_receipt(&w1kp, &w1, &controller, 1, &said),
833        ]);
834        assert_eq!(signed.verify().unwrap().trust, KsnTrust::TrustOnFirstSight);
835    }
836
837    #[test]
838    fn ksn_ignores_receipt_for_wrong_said() {
839        let ckp = real_keypair();
840        let (w1kp, w1) = witness_kp_and_aid();
841        let (w2kp, w2) = witness_kp_and_aid();
842        let state = witnessed_state(&ckp, 1, &[&w1, &w2], 2, false);
843        let controller = state.prefix.as_str().to_string();
844        // Receipts for a different event SAID must not count — set directly to
845        // exercise verify()'s own filtering (not just attach-time vetting).
846        let mut signed = sign_ksn(&ckp, KeyStateNotice::new(state, "t"));
847        let wrong = "EWrongEventSaid0000000000000000000000000000";
848        signed.receipts = vec![
849            witness_receipt(&w1kp, &w1, &controller, 1, wrong),
850            witness_receipt(&w2kp, &w2, &controller, 1, wrong),
851        ];
852        assert_eq!(signed.verify().unwrap().trust, KsnTrust::TrustOnFirstSight);
853    }
854
855    #[test]
856    fn ksn_bt_zero_stays_tofu() {
857        // A backerless (bt=0) KSN has no witnesses to satisfy.
858        let ckp = real_keypair();
859        let signed = sign_ksn(&ckp, KeyStateNotice::new(state_for_key(&ckp, 1), "t"));
860        assert_eq!(signed.verify().unwrap().trust, KsnTrust::TrustOnFirstSight);
861    }
862
863    #[test]
864    fn witnessed_device_ksn_still_refuses_revocation() {
865        let ckp = real_keypair();
866        let (w1kp, w1) = witness_kp_and_aid();
867        let (w2kp, w2) = witness_kp_and_aid();
868        let state = witnessed_state(&ckp, 1, &[&w1, &w2], 2, true); // delegated device
869        let controller = state.prefix.as_str().to_string();
870        let said = state.last_event_said.as_str().to_string();
871        let signed = sign_ksn(&ckp, KeyStateNotice::new(state, "t")).with_receipts(vec![
872            witness_receipt(&w1kp, &w1, &controller, 1, &said),
873            witness_receipt(&w2kp, &w2, &controller, 1, &said),
874        ]);
875        let v = signed.verify().unwrap();
876        assert!(matches!(v.trust, KsnTrust::Witnessed { .. }));
877        // Witnessed, but a device KSN still cannot prove non-revocation or override the KEL.
878        assert!(!v.satisfies_revocation_check());
879        assert!(!v.is_authoritative_over_kel());
880    }
881
882    #[test]
883    fn populating_receipts_preserves_controller_signature() {
884        let ckp = real_keypair();
885        let (w1kp, w1) = witness_kp_and_aid();
886        let (w2kp, w2) = witness_kp_and_aid();
887        let state = witnessed_state(&ckp, 1, &[&w1, &w2], 2, false);
888        let controller = state.prefix.as_str().to_string();
889        let said = state.last_event_said.as_str().to_string();
890        let signed = sign_ksn(&ckp, KeyStateNotice::new(state, "t"));
891        assert!(signed.verify().is_ok()); // controller sig valid before receipts
892
893        let published = signed.with_receipts(vec![
894            witness_receipt(&w1kp, &w1, &controller, 1, &said),
895            witness_receipt(&w2kp, &w2, &controller, 1, &said),
896        ]);
897        // Attaching receipts (outside canonical_bytes) does not break the controller signature.
898        let v = published
899            .verify()
900            .expect("controller signature must still verify after attaching receipts");
901        assert!(matches!(v.trust, KsnTrust::Witnessed { .. }));
902    }
903
904    // ── KERI-conformant key-state record (KeyStateRecord) ────────────────────
905
906    /// A minimal self-addressing inception KEL (single event) parsed from JSON,
907    /// mirroring the keripy `icp`/`KeyStateRecord` reference vector.
908    const ICP_KEL_JSON: &str = r#"[{
909        "v":"KERI10JSON0000fd_","t":"icp",
910        "d":"EOoC9AuwxiwcyUDsa2yNAaZOVWqfiAt4o3R31_8K2Z1J",
911        "i":"EOoC9AuwxiwcyUDsa2yNAaZOVWqfiAt4o3R31_8K2Z1J",
912        "s":"0","kt":"1",
913        "k":["DAABAgMEBQYHCAkKCwwNDg8QERITFBUWFxgZGhscHR4f"],
914        "nt":"0","n":[],"bt":"0","b":[],"c":[],"a":[]
915    }]"#;
916
917    #[test]
918    fn key_state_record_emits_keri_wire_shape() {
919        let events = crate::validate::parse_kel_json(ICP_KEL_JSON).unwrap();
920        let state = crate::validate::TrustedKel::from_trusted_source(&events)
921            .replay()
922            .unwrap();
923        let record =
924            KeyStateRecord::from_kel(&events, &state, "2026-06-12T02:49:41.677319+00:00").unwrap();
925
926        let json = serde_json::to_value(&record).unwrap();
927        let obj = json.as_object().unwrap();
928        // Field order/labels are the KERI ksn record shape, not the auths envelope.
929        let keys: Vec<&str> = obj.keys().map(String::as_str).collect();
930        assert_eq!(
931            keys,
932            vec![
933                "vn", "i", "s", "p", "d", "f", "dt", "et", "kt", "k", "nt", "n", "bt", "b", "c",
934                "ee", "di"
935            ]
936        );
937        assert_eq!(obj["vn"], serde_json::json!([1, 0]));
938        assert_eq!(obj["s"], "0");
939        assert_eq!(obj["p"], "");
940        assert_eq!(obj["et"], "icp");
941        assert_eq!(obj["kt"], "1");
942        assert_eq!(obj["di"], "");
943        let ee = obj["ee"].as_object().unwrap();
944        assert_eq!(
945            ee.keys().map(String::as_str).collect::<Vec<_>>(),
946            vec!["s", "d", "br", "ba"]
947        );
948    }
949
950    #[test]
951    fn key_state_record_round_trips_through_key_state() {
952        let events = crate::validate::parse_kel_json(ICP_KEL_JSON).unwrap();
953        let state = crate::validate::TrustedKel::from_trusted_source(&events)
954            .replay()
955            .unwrap();
956        let record = KeyStateRecord::from_kel(&events, &state, "t").unwrap();
957
958        // Serialize -> deserialize (the peer's wire path) -> project to KeyState.
959        let wire = serde_json::to_string(&record).unwrap();
960        let parsed: KeyStateRecord = serde_json::from_str(&wire).unwrap();
961        assert_eq!(parsed, record);
962
963        let projected = parsed.into_key_state();
964        assert_eq!(projected.prefix, state.prefix);
965        assert_eq!(projected.current_keys, state.current_keys);
966        assert_eq!(projected.sequence, state.sequence);
967        assert_eq!(projected.last_event_said, state.last_event_said);
968        assert_eq!(projected.threshold, state.threshold);
969        assert!(projected.is_non_transferable);
970    }
971
972    #[test]
973    fn key_state_record_check_not_stale() {
974        let events = crate::validate::parse_kel_json(ICP_KEL_JSON).unwrap();
975        let state = crate::validate::TrustedKel::from_trusted_source(&events)
976            .replay()
977            .unwrap();
978        let record = KeyStateRecord::from_kel(&events, &state, "t").unwrap();
979        assert_eq!(record.sequence(), 0);
980        // A verifier that has seen seq 0 accepts a seq-0 notice; one holding a
981        // newer (seq 1) receipt rejects this stale seq-0 view.
982        assert!(record.check_not_stale(0).is_ok());
983        assert!(matches!(
984            record.check_not_stale(1),
985            Err(KsnError::Stale { got: 0, seen: 1 })
986        ));
987    }
988
989    #[test]
990    fn key_state_record_ingests_peer_published_record() {
991        // A keripy-shaped record arriving over the wire (string sequence, hex
992        // thresholds, empty delegator) projects to a usable KeyState.
993        let wire = r#"{
994            "vn":[1,0],
995            "i":"EOoC9AuwxiwcyUDsa2yNAaZOVWqfiAt4o3R31_8K2Z1J",
996            "s":"0","p":"",
997            "d":"EOoC9AuwxiwcyUDsa2yNAaZOVWqfiAt4o3R31_8K2Z1J",
998            "f":"0","dt":"2026-06-12T02:49:41.677319+00:00","et":"icp",
999            "kt":"1","k":["DAABAgMEBQYHCAkKCwwNDg8QERITFBUWFxgZGhscHR4f"],
1000            "nt":"0","n":[],"bt":"0","b":[],"c":[],
1001            "ee":{"s":"0","d":"EOoC9AuwxiwcyUDsa2yNAaZOVWqfiAt4o3R31_8K2Z1J","br":[],"ba":[]},
1002            "di":""
1003        }"#;
1004        let record: KeyStateRecord = serde_json::from_str(wire).unwrap();
1005        let state = record.into_key_state();
1006        assert_eq!(
1007            state.prefix.as_str(),
1008            "EOoC9AuwxiwcyUDsa2yNAaZOVWqfiAt4o3R31_8K2Z1J"
1009        );
1010        assert_eq!(state.sequence, 0);
1011        assert!(state.is_non_transferable);
1012        assert!(state.delegator.is_none());
1013    }
1014}