Skip to main content

auths_keri/
validate.rs

1//! KEL validation: SAID verification, chain linkage, signature verification,
2//! and pre-rotation commitment checks.
3//!
4//! This module provides validation functions for ensuring a Key Event Log
5//! is cryptographically valid and properly chained.
6
7use crate::crypto::verify_commitment;
8use crate::events::{Event, IcpEvent, IxnEvent, KeriSequence, RotEvent, Seal, SourceSeal};
9use crate::keys::KeriPublicKey;
10use crate::said::compute_said;
11use crate::state::KeyState;
12use crate::types::{CesrKey, ConfigTrait, Prefix, Said, Threshold};
13use crate::witness::WitnessReceiptLookup;
14use crate::witness::agreement::{AgreementStatus, WitnessAgreement};
15
16/// Errors specific to KEL validation.
17///
18/// These errors represent **protocol invariant violations**. They indicate
19/// structural corruption or attack, not recoverable conditions.
20#[derive(Debug, Clone, thiserror::Error, PartialEq, Eq)]
21#[non_exhaustive]
22pub enum ValidationError {
23    /// SAID (Self-Addressing Identifier) doesn't match content hash.
24    #[error("Invalid SAID: expected {expected}, got {actual}")]
25    InvalidSaid {
26        /// The SAID that was expected from the content hash.
27        expected: Said,
28        /// The SAID that was actually found in the event.
29        actual: Said,
30    },
31
32    /// Event references wrong previous event.
33    #[error("Broken chain: event {sequence} references {referenced}, but previous was {actual}")]
34    BrokenChain {
35        /// Zero-based position of the event in the KEL.
36        sequence: u128,
37        /// The previous SAID referenced by this event.
38        referenced: Said,
39        /// The actual SAID of the previous event.
40        actual: Said,
41    },
42
43    /// Sequence number is not monotonically increasing.
44    #[error("Invalid sequence: expected {expected}, got {actual}")]
45    InvalidSequence {
46        /// The sequence number that was expected.
47        expected: u128,
48        /// The sequence number that was found.
49        actual: u128,
50    },
51
52    /// Pre-rotation commitment doesn't match the new current key.
53    #[error("Pre-rotation commitment mismatch at sequence {sequence}")]
54    CommitmentMismatch {
55        /// Zero-based position of the rotation event that failed.
56        sequence: u128,
57    },
58
59    /// Cryptographic signature verification failed for an event.
60    #[error("Signature verification failed at sequence {sequence}")]
61    SignatureFailed {
62        /// Zero-based position of the event whose signature failed.
63        sequence: u128,
64    },
65
66    /// A threshold (`kt`, `nt`, or `bt`) is structurally unsatisfiable against
67    /// the list it governs — e.g. `kt=5` over a single key, or a weighted
68    /// clause whose length differs from the key-list length.
69    #[error("Unsatisfiable threshold at sequence {sequence}: {reason}")]
70    ThresholdNotSatisfiable {
71        /// Zero-based position of the offending event.
72        sequence: u128,
73        /// Which threshold and why it cannot be met.
74        reason: String,
75    },
76
77    /// A rotation's backer delta is invalid: a `br` (cut) entry isn't in the
78    /// prior backer set, or a `ba` (add) entry duplicates a surviving backer.
79    #[error("Invalid backer delta at sequence {sequence}: {reason}")]
80    InvalidBackerDelta {
81        /// Zero-based position of the offending rotation.
82        sequence: u128,
83        /// What was wrong with the delta.
84        reason: String,
85    },
86
87    /// A rotation flips the registrar-backer role (`RB` <-> `NRB`) while
88    /// retaining prior backers via a partial `br`/`ba` delta. `RB` and `NRB`
89    /// carry different backer-list semantics, so a surviving backer would be
90    /// governed by semantics it was never admitted under. A role flip must
91    /// rebuild `b[]` — every prior backer cut (F-23).
92    #[error("Invalid backer role flip at sequence {sequence}: {reason}")]
93    BackerRoleFlip {
94        /// Zero-based position of the offending rotation.
95        sequence: u128,
96        /// Which roles flipped and how many backers survived.
97        reason: String,
98    },
99
100    /// Rotation event's key-list size differs from the prior next-commitment
101    /// list. Properly expressing this case requires CESR indexed-signature
102    /// type codes so verified indices can be mapped distinctly against prior
103    /// and current key lists. Until that lands, such rotations are rejected.
104    #[error(
105        "Asymmetric key rotation at sequence {sequence}: prior next count {prior_next_count} != new key count {new_key_count} (removing devices requires CESR indexed signatures)"
106    )]
107    AsymmetricKeyRotation {
108        /// Zero-based position of the rotation event.
109        sequence: u128,
110        /// Number of entries in the prior event's next-commitment list.
111        prior_next_count: usize,
112        /// Number of entries in this rotation's key list.
113        new_key_count: usize,
114    },
115
116    /// A delegated event (`dip` / `drt`) references a delegator but no
117    /// matching seal could be found in the delegator's KEL.
118    #[error(
119        "Delegator seal not found at sequence {sequence}: delegator {delegator_aid} has no ixn-anchored seal for this event"
120    )]
121    DelegatorSealNotFound {
122        /// Zero-based position of the delegated event.
123        sequence: u128,
124        /// Delegator AID the event referenced (dip.di / drt.di).
125        delegator_aid: String,
126    },
127
128    /// A delegated event (`dip` / `drt`) has no delegate-side source seal
129    /// (`-G` couple). The delegator anchored it, but the event itself doesn't
130    /// point back at that anchoring event — a one-directional (and therefore
131    /// non-keripy-interoperable, weakly-bound) delegation. Bilateral required.
132    #[error(
133        "Delegate source seal missing at sequence {sequence}: delegated event carries no -G back-reference to its anchoring event"
134    )]
135    DelegateSourceSealMissing {
136        /// Zero-based position of the delegated event.
137        sequence: u128,
138    },
139
140    /// A delegated event's source seal (`-G` couple) points at a different
141    /// delegator event than the one that actually anchored it. The bilateral
142    /// binding is broken: the delegate claims anchoring location L while the
143    /// delegator's `Seal::KeyEvent` lives at L′ ≠ L.
144    #[error(
145        "Delegation source seal back-reference mismatch at sequence {sequence}: delegate points at a different anchoring event than the delegator's seal"
146    )]
147    SealBackRefMismatch {
148        /// Zero-based position of the delegated event.
149        sequence: u128,
150    },
151
152    /// A delegated event was submitted but no `DelegatorKelLookup` was
153    /// provided. Use `validate_kel_with_lookup` when processing KELs that
154    /// contain `dip` or `drt` events.
155    #[error(
156        "Delegator lookup required for delegated event at sequence {sequence}; call validate_kel_with_lookup"
157    )]
158    DelegatorLookupMissing {
159        /// Zero-based position of the delegated event.
160        sequence: u128,
161    },
162
163    /// A non-inception event was applied without the prior key state it
164    /// chains from (missing inception or out-of-order application).
165    #[error("Event at sequence {sequence} applied without prior key state")]
166    MissingPriorState {
167        /// Zero-based position of the event that lacked prior state.
168        sequence: u128,
169    },
170
171    /// The first event in a KEL must be an Inception event.
172    #[error("First event must be inception")]
173    NotInception,
174
175    /// The KEL contains no events.
176    #[error("Empty KEL")]
177    EmptyKel,
178
179    /// More than one Inception event was found in the KEL.
180    #[error("Multiple inception events in KEL")]
181    MultipleInceptions,
182
183    /// JSON serialization or deserialization failed.
184    #[error("Serialization error: {0}")]
185    Serialization(String),
186
187    /// A sequence field could not be parsed as a valid hex number.
188    #[error("Malformed sequence number: {raw:?}")]
189    MalformedSequence {
190        /// The raw string that could not be parsed.
191        raw: String,
192    },
193
194    /// The key encoding prefix is unsupported or malformed.
195    #[error("Invalid key encoding: {0}")]
196    InvalidKey(String),
197
198    /// The identity has been abandoned (empty next commitment) and no more events are allowed.
199    #[error("Identity abandoned at sequence {sequence}, no more events allowed")]
200    AbandonedIdentity {
201        /// The sequence number of the rejected event.
202        sequence: u128,
203    },
204
205    /// An interaction event was found in an establishment-only KEL.
206    #[error("Interaction event at sequence {sequence} rejected: KEL is establishment-only (EO)")]
207    EstablishmentOnly {
208        /// The sequence number of the rejected event.
209        sequence: u128,
210    },
211
212    /// The identity is non-transferable (inception had empty next commitments).
213    #[error(
214        "Non-transferable identity: inception had empty next key commitments, no subsequent events allowed"
215    )]
216    NonTransferable,
217
218    /// A backer AID appears more than once in the backer list.
219    #[error("Duplicate backer AID: {aid}")]
220    DuplicateBacker {
221        /// The duplicated AID.
222        aid: String,
223    },
224
225    /// The backer threshold is inconsistent with the backer list size.
226    #[error("Invalid backer threshold: bt={bt} but backer_count={backer_count}")]
227    InvalidBackerThreshold {
228        /// The backer threshold value.
229        bt: u64,
230        /// The number of backers.
231        backer_count: usize,
232    },
233
234    /// A policy-only variant: an establishment event is missing the `dt`
235    /// field, so the cooldown cannot be enforced. Structural validation
236    /// (`validate_kel`) permits missing `dt`; the policy validator
237    /// (`validate_kel_with_policy`) does not.
238    #[error("Policy violation: event at seq {sequence} missing `dt`")]
239    MissingTimestamp {
240        /// Zero-based position of the event in the KEL.
241        sequence: u128,
242    },
243
244    /// Two consecutive events have non-monotonic `dt`.
245    #[error(
246        "Policy violation: timestamps not monotonic at seq {sequence} (prev={prev}, curr={curr})"
247    )]
248    NonMonotonicTimestamp {
249        /// Zero-based position of the offending event.
250        sequence: u128,
251        /// Previous event's `dt`.
252        prev: String,
253        /// Current event's `dt`.
254        curr: String,
255    },
256
257    /// Two rotations happened closer together than the configured
258    /// cooldown allows (and the event is not an emergency override).
259    #[error(
260        "Policy violation: rotation cooldown breached at seq {sequence} (interval {interval_secs}s < minimum {min_secs}s)"
261    )]
262    RotationCooldown {
263        /// Zero-based position of the offending rotation.
264        sequence: u128,
265        /// Observed inter-rotation interval (seconds).
266        interval_secs: i64,
267        /// Configured minimum interval (seconds).
268        min_secs: i64,
269    },
270
271    /// An event's `dt` is beyond the configured clock-skew tolerance.
272    #[error(
273        "Policy violation: clock skew at seq {sequence} ({skew_secs}s) exceeds tolerance ({tolerance_secs}s)"
274    )]
275    ClockSkew {
276        /// Zero-based position of the event.
277        sequence: u128,
278        /// Observed skew vs server clock (seconds, signed).
279        skew_secs: i64,
280        /// Configured tolerance (seconds).
281        tolerance_secs: i64,
282    },
283}
284
285/// Validate a delegated event against the delegator's KEL.
286///
287/// Searches the delegator's KEL for an anchoring key event seal that matches
288/// the delegated event's prefix, sequence number, and SAID. Also enforces
289/// the `DND` (Do Not Delegate) configuration trait.
290///
291/// Args:
292/// * `delegated_event` - The delegated event (dip or drt) to validate.
293/// * `delegator_kel` - The delegator's full KEL.
294pub fn validate_delegation(
295    delegated_event: &Event,
296    delegator_kel: &[Event],
297) -> Result<(), ValidationError> {
298    if !delegated_event.is_delegated() {
299        return Err(ValidationError::Serialization(
300            "validate_delegation called on non-delegated event".to_string(),
301        ));
302    }
303
304    let event_said = delegated_event.said();
305    let event_seq = delegated_event.sequence();
306
307    // Check DND enforcement on delegator
308    if let Some(Event::Icp(delegator_icp)) = delegator_kel.first()
309        && delegator_icp.c.contains(&ConfigTrait::DoNotDelegate)
310    {
311        return Err(ValidationError::Serialization(
312            "Delegator has DoNotDelegate (DND) config trait".to_string(),
313        ));
314    }
315
316    // Delegator side: find the anchoring event whose a[] carries a KeyEvent seal
317    // for this delegated event, and capture that event's own (sequence, SAID).
318    let anchor = delegator_kel.iter().find_map(|event| {
319        let anchors = event.anchors().iter().any(|seal| {
320            matches!(
321                seal,
322                Seal::KeyEvent { i, s, d }
323                if i == delegated_event.prefix()
324                    && s.value() == event_seq.value()
325                    && d == event_said
326            )
327        });
328        anchors.then(|| SourceSeal {
329            s: event.sequence(),
330            d: event.said().clone(),
331        })
332    });
333
334    let Some(anchor) = anchor else {
335        return Err(ValidationError::Serialization(format!(
336            "No delegation seal found in delegator KEL for prefix={}, sn={}, said={}",
337            delegated_event.prefix(),
338            event_seq,
339            event_said
340        )));
341    };
342
343    // Delegate side: the event's -G source seal must point back at that exact
344    // anchoring event. A missing or mismatched back-reference is rejected.
345    enforce_source_seal(delegated_event.source_seal(), &anchor, event_seq.value())
346}
347
348/// Enforce the delegate side of the bilateral delegation binding: the delegated
349/// event's `-G` source seal must be present and equal the delegator's anchoring
350/// event `(sequence, SAID)`.
351fn enforce_source_seal(
352    source_seal: Option<&SourceSeal>,
353    anchor: &SourceSeal,
354    sequence: u128,
355) -> Result<(), ValidationError> {
356    match source_seal {
357        None => Err(ValidationError::DelegateSourceSealMissing { sequence }),
358        Some(seal) if seal == anchor => Ok(()),
359        Some(_) => Err(ValidationError::SealBackRefMismatch { sequence }),
360    }
361}
362
363/// Validate a KEL and return the resulting KeyState.
364///
365/// This is a **pure function** serving as the core entrypoint for KEL replay.
366///
367/// Args:
368/// * `events` - The ordered list of KERI events to validate.
369///
370/// Usage:
371/// ```ignore
372/// let key_state = validate_kel(&events)?;
373/// ```
374/// Pluggable cross-KEL seal lookup for validating delegated events.
375///
376/// A delegated identifier's rotation or inception must be anchored by the
377/// delegator's KEL via an `ixn` event whose `a[]` seal references the
378/// delegated event's SAID. This trait lets the validator ask "does my
379/// delegator have a seal for this event?" without depending on any
380/// particular KEL storage backend.
381pub trait DelegatorKelLookup {
382    /// Return the delegator's anchoring event — its sequence **and** SAID — whose
383    /// `a[]` carries a `Seal::KeyEvent` for `seal_said`, or `None` if the
384    /// delegator's KEL contains none. The returned [`SourceSeal`] is exactly what
385    /// the delegated event's `-G` back-reference must equal for the bilateral
386    /// binding to hold.
387    fn find_seal(&self, delegator_aid: &Prefix, seal_said: &Said) -> Option<SourceSeal>;
388}
389
390/// A precomputed index of a delegator KEL's anchoring seals.
391///
392/// Build it once from a KEL slice with [`KelSealIndex::from_events`]; `find_seal`
393/// is then an O(1) map lookup. This is the shared [`DelegatorKelLookup`] every
394/// verify path uses to resolve the [`SourceSeal`] that authorizes a delegated
395/// (`dip`/`drt`) event — replacing the per-call-site linear scans the commit,
396/// presentation, and offline-org verifiers each used to carry (so the lookup is
397/// defined once, with one performance profile, instead of three times).
398pub struct KelSealIndex {
399    /// `sealed-event SAID → SourceSeal of the anchoring event`.
400    seals: std::collections::HashMap<Said, SourceSeal>,
401}
402
403impl KelSealIndex {
404    /// Index every `Seal::KeyEvent` anchored in `events`, mapping the sealed event
405    /// SAID to the [`SourceSeal`] (sequence + SAID) of the event that anchored it.
406    /// On a duplicate sealed SAID the first (lowest-sequence) anchor wins —
407    /// identical to a forward linear scan over an ordered KEL.
408    ///
409    /// Args:
410    /// * `events`: The delegator's KEL.
411    pub fn from_events(events: &[Event]) -> Self {
412        let mut seals = std::collections::HashMap::new();
413        for event in events {
414            for seal in event.anchors() {
415                if let Seal::KeyEvent { d, .. } = seal {
416                    seals.entry(d.clone()).or_insert_with(|| SourceSeal {
417                        s: event.sequence(),
418                        d: event.said().clone(),
419                    });
420                }
421            }
422        }
423        Self { seals }
424    }
425}
426
427impl DelegatorKelLookup for KelSealIndex {
428    fn find_seal(&self, _delegator_aid: &Prefix, seal_said: &Said) -> Option<SourceSeal> {
429        self.seals.get(seal_said).cloned()
430    }
431}
432
433/// A KEL the caller asserts comes from a **trusted source** — the local identity
434/// registry / a self-owned store, or a chain already authenticated via
435/// [`validate_signed_kel`].
436///
437/// Structural replay (SAID + sequence + chain-linkage + pre-rotation commitment,
438/// *without* re-verifying each event's signature) is exposed to other crates
439/// **only** through this type. Bare-`&[Event]` structural replay
440/// ([`validate_kel`] and friends) is `pub(crate)`, so untrusted input — a CI
441/// `--identity-bundle`, a `--remote`/`--oobi` fetch, a WASM/FFI buffer — cannot be
442/// structurally replayed from outside auths-keri without either an explicit,
443/// greppable trust assertion ([`TrustedKel::from_trusted_source`]) or prior
444/// authentication via [`validate_signed_kel`] (RT-002 / #263). The assertion is a
445/// reviewable, lint-gated decision rather than an invisible `validate_kel(bytes)`
446/// call.
447///
448/// Borrowing and `Copy` — zero-cost over a `&[Event]`.
449#[derive(Clone, Copy)]
450pub struct TrustedKel<'a>(&'a [Event]);
451
452impl<'a> TrustedKel<'a> {
453    /// Assert that `events` come from a trusted source. Every call site is a
454    /// reviewable trust assertion — **never** call this on attacker-influenced
455    /// bytes (bundle / `--remote` / `--oobi` / WASM / FFI); authenticate those
456    /// through [`validate_signed_kel`] instead.
457    ///
458    /// Args:
459    /// * `events`: A KEL whose provenance the caller vouches for (local registry
460    ///   read, or an already-authenticated chain).
461    pub fn from_trusted_source(events: &'a [Event]) -> Self {
462        Self(events)
463    }
464
465    /// The underlying events.
466    pub fn events(&self) -> &'a [Event] {
467        self.0
468    }
469
470    /// Structural replay to the current [`KeyState`].
471    pub fn replay(self) -> Result<KeyState, ValidationError> {
472        validate_kel(self.0)
473    }
474
475    /// Structural replay with a delegator-seal lookup for delegated (`dip`/`drt`)
476    /// events.
477    pub fn replay_with_lookup(
478        self,
479        lookup: Option<&dyn DelegatorKelLookup>,
480    ) -> Result<KeyState, ValidationError> {
481        validate_kel_with_lookup(self.0, lookup)
482    }
483
484    /// Structural replay with the M-of-N witness-receipt gate.
485    pub fn replay_with_receipts(
486        self,
487        lookup: Option<&dyn DelegatorKelLookup>,
488        receipt_lookup: &dyn WitnessReceiptLookup,
489    ) -> Result<WitnessedReplay, ValidationError> {
490        validate_kel_with_receipts(self.0, lookup, receipt_lookup)
491    }
492
493    /// Structural replay with the time / rotation-cadence policy checks
494    /// ([`KelPolicy`]). `timestamps[i]` is the optional signing time of `events[i]`.
495    pub fn replay_with_policy(
496        self,
497        timestamps: &[Option<chrono::DateTime<chrono::Utc>>],
498        policy: &KelPolicy,
499        now: chrono::DateTime<chrono::Utc>,
500    ) -> Result<KeyState, ValidationError> {
501        validate_kel_with_policy(self.0, timestamps, policy, now)
502    }
503}
504
505/// Validate a KEL with no delegator lookup.
506///
507/// Crate-private (RT-002 / #263): other crates reach structural replay only via
508/// [`TrustedKel`], so untrusted input cannot be replayed without an explicit trust
509/// assertion. Convenience wrapper over [`validate_kel_with_lookup`] for ordinary
510/// KELs that contain only `icp`/`rot`/`ixn` events.
511///
512/// Args:
513/// * `events` - The ordered list of KERI events to replay and validate.
514pub(crate) fn validate_kel(events: &[Event]) -> Result<KeyState, ValidationError> {
515    validate_kel_with_lookup(events, None::<&dyn DelegatorKelLookup>)
516}
517
518/// Validate a KEL with a delegator-lookup hook for delegated events.
519///
520/// Required when the KEL contains `dip` or `drt` events; ordinary KELs
521/// (only `icp`/`rot`/`ixn`) can pass `None`.
522pub(crate) fn validate_kel_with_lookup(
523    events: &[Event],
524    lookup: Option<&dyn DelegatorKelLookup>,
525) -> Result<KeyState, ValidationError> {
526    match replay_kel_gated(events, lookup, None)? {
527        WitnessedReplay::Accepted(state) => Ok(state),
528        // With no receipt lookup the gate never runs, so `Pending` is
529        // unreachable; returning the structural state preserves the
530        // no-receipt contract (advance regardless of receipts).
531        WitnessedReplay::Pending { state, .. } => Ok(state),
532    }
533}
534
535/// The outcome of replaying a KEL through the witness-receipt gate.
536///
537/// Unlike [`validate_kel`] (structural only), [`validate_kel_with_receipts`]
538/// will not silently advance past an establishment event that lacks M-of-N
539/// witness agreement — it reports [`WitnessedReplay::Pending`] so the caller
540/// (verifier policy, D.7) can warn or refuse.
541#[derive(Debug, Clone, PartialEq, Eq)]
542pub enum WitnessedReplay {
543    /// Every `bt>0` establishment event reached witness quorum; the key-state
544    /// is witness-authoritative.
545    Accepted(KeyState),
546    /// The KEL is structurally valid, but the establishment event at `sequence`
547    /// did not reach quorum. `state` is the structural replay through that event;
548    /// the caller must not treat key-state at or after `sequence` as
549    /// witness-authoritative.
550    Pending {
551        /// Structural replay result through the under-quorum event.
552        state: KeyState,
553        /// Sequence of the first under-quorum establishment event.
554        sequence: u128,
555        /// SAID of that event.
556        said: Said,
557        /// The backer threshold that was required.
558        required: Threshold,
559        /// Distinct, in-force witness receipts collected for it.
560        collected: usize,
561    },
562}
563
564impl WitnessedReplay {
565    /// The replayed key-state, regardless of the witness-quorum outcome.
566    pub fn state(&self) -> &KeyState {
567        match self {
568            WitnessedReplay::Accepted(state) | WitnessedReplay::Pending { state, .. } => state,
569        }
570    }
571}
572
573/// Validate a KEL and gate each establishment event on M-of-N witness receipts.
574///
575/// Extends [`validate_kel_with_lookup`] with receipt-gated replay: a `bt>0`
576/// establishment event advances `KeyState` only when KAWA
577/// ([`WitnessAgreement`](crate::witness::agreement::WitnessAgreement)) reports
578/// agreement over receipts from **distinct** witnesses in the `b[]` set **in
579/// force at that sequence**. `bt=0` events accept without receipts (the
580/// zero-witness path). Receipts are matched by `(controller, sn, said)` via
581/// `receipt_lookup` and deduped by witness AID; a receipt from a non-designated
582/// witness never counts.
583///
584/// Args:
585/// * `events`: The ordered KEL to replay.
586/// * `delegator_lookup`: Cross-KEL seal lookup for delegated events (`dip`/`drt`).
587/// * `receipt_lookup`: Source of witness receipts per event.
588///
589/// Usage:
590/// ```ignore
591/// match validate_kel_with_receipts(&events, None, &receipts)? {
592///     WitnessedReplay::Accepted(state) => trust(state),
593///     WitnessedReplay::Pending { sequence, .. } => warn_or_refuse(sequence),
594/// }
595/// ```
596pub(crate) fn validate_kel_with_receipts(
597    events: &[Event],
598    delegator_lookup: Option<&dyn DelegatorKelLookup>,
599    receipt_lookup: &dyn WitnessReceiptLookup,
600) -> Result<WitnessedReplay, ValidationError> {
601    replay_kel_gated(events, delegator_lookup, Some(receipt_lookup))
602}
603
604/// Shared structural replay with an optional witness-receipt gate.
605///
606/// With `receipt_lookup = None` this is pure structural replay (the
607/// [`validate_kel`] contract). With `Some(_)` each establishment event is gated
608/// on witness quorum; the first under-quorum event short-circuits to
609/// [`WitnessedReplay::Pending`].
610fn replay_kel_gated(
611    events: &[Event],
612    lookup: Option<&dyn DelegatorKelLookup>,
613    receipt_lookup: Option<&dyn WitnessReceiptLookup>,
614) -> Result<WitnessedReplay, ValidationError> {
615    if events.is_empty() {
616        return Err(ValidationError::EmptyKel);
617    }
618
619    verify_event_said(&events[0])?;
620    let (mut state, inception_n_is_empty, establishment_only) = match &events[0] {
621        Event::Icp(icp) => (
622            validate_inception(icp)?,
623            icp.n.is_empty(),
624            icp.c.contains(&ConfigTrait::EstablishmentOnly),
625        ),
626        Event::Dip(dip) => (
627            validate_delegated_inception(dip, lookup)?,
628            dip.n.is_empty(),
629            dip.c.contains(&ConfigTrait::EstablishmentOnly),
630        ),
631        _ => return Err(ValidationError::NotInception),
632    };
633
634    let controller = state.prefix.clone();
635
636    // Gate the inception establishment event on witness quorum.
637    if let Some(rl) = receipt_lookup
638        && let Some(pending) = gate_establishment(&controller, &state, 0, events[0].said(), rl)
639    {
640        return Ok(pending);
641    }
642
643    // Non-transferable identities (inception n is empty) cannot have subsequent events
644    if inception_n_is_empty && events.len() > 1 {
645        return Err(ValidationError::NonTransferable);
646    }
647
648    for (idx, event) in events.iter().enumerate().skip(1) {
649        let expected_seq = idx as u128;
650
651        // Reject any event after abandonment
652        if state.is_abandoned {
653            return Err(ValidationError::AbandonedIdentity {
654                sequence: expected_seq,
655            });
656        }
657
658        // Reject IXN in establishment-only KELs
659        if establishment_only && matches!(event, Event::Ixn(_)) {
660            return Err(ValidationError::EstablishmentOnly {
661                sequence: expected_seq,
662            });
663        }
664
665        verify_event_said(event)?;
666        verify_sequence(event, expected_seq)?;
667        verify_chain_linkage(event, &state)?;
668
669        match event {
670            Event::Rot(rot) => validate_rotation(rot, expected_seq, &mut state)?,
671            Event::Ixn(ixn) => validate_interaction(ixn, expected_seq, &mut state)?,
672            Event::Icp(_) | Event::Dip(_) => return Err(ValidationError::MultipleInceptions),
673            Event::Drt(drt) => {
674                validate_delegated_rotation(drt, expected_seq, &mut state, lookup)?;
675            }
676        }
677
678        // Gate establishment events (rot/drt) on witness quorum; ixn never gates.
679        if let Some(rl) = receipt_lookup
680            && matches!(event, Event::Rot(_) | Event::Drt(_))
681            && let Some(pending) =
682                gate_establishment(&controller, &state, expected_seq, event.said(), rl)
683        {
684            return Ok(pending);
685        }
686    }
687
688    Ok(WitnessedReplay::Accepted(state))
689}
690
691/// Replay a KEL of **signed** events, verifying each event's signature against the
692/// key-state that authorizes it — the authenticated counterpart to the
693/// structural-only [`validate_kel`] (RT-002).
694///
695/// Where [`validate_kel`] authorizes by log *structure* alone (SAID + sequence +
696/// chain-linkage + pre-rotation commitment), this folds [`validate_signed_event`]
697/// into the replay so every event must also carry a valid signature from the
698/// in-force key-state: inception/`dip` against their own committed keys under
699/// `kt`; `rot`/`drt` against the new keys plus the prior pre-rotation commitment;
700/// `ixn` against the current key-state. An event with no — or an invalid —
701/// signature fails closed with [`ValidationError::SignatureFailed`].
702///
703/// This is the function the stateless verify entrypoints call to AUTHENTICATE an
704/// ingested KEL: the identity bundle carries a CESR signature attachment per `kel`
705/// event (`IdentityBundle::kel_attachments`, paired via `pair_kel_attachments`),
706/// and the WASM KEL boundary replays through this function and deliberately does
707/// not expose the structural `validate_kel`. Structural checks are applied here
708/// too, so a forged SAID or broken chain is still rejected — but the signature
709/// check is the point: an unsigned or wrong-signer event fails closed.
710///
711/// Args:
712/// * `events`: The ordered KEL of signed events to replay.
713/// * `lookup`: Cross-KEL seal lookup for delegated events (`dip`/`drt`).
714pub fn validate_signed_kel(
715    events: &[crate::events::SignedEvent],
716    lookup: Option<&dyn DelegatorKelLookup>,
717) -> Result<KeyState, ValidationError> {
718    if events.is_empty() {
719        return Err(ValidationError::EmptyKel);
720    }
721
722    // Inception: structural (SAID + self-certification) AND a signature from the
723    // event's own committed keys.
724    let first = &events[0];
725    verify_event_said(&first.event)?;
726    validate_signed_event(first, None)?;
727    let (mut state, inception_n_is_empty, establishment_only) = match &first.event {
728        Event::Icp(icp) => (
729            validate_inception(icp)?,
730            icp.n.is_empty(),
731            icp.c.contains(&ConfigTrait::EstablishmentOnly),
732        ),
733        Event::Dip(dip) => (
734            validate_delegated_inception(dip, lookup)?,
735            dip.n.is_empty(),
736            dip.c.contains(&ConfigTrait::EstablishmentOnly),
737        ),
738        _ => return Err(ValidationError::NotInception),
739    };
740
741    if inception_n_is_empty && events.len() > 1 {
742        return Err(ValidationError::NonTransferable);
743    }
744
745    for (idx, signed) in events.iter().enumerate().skip(1) {
746        let event = &signed.event;
747        let expected_seq = idx as u128;
748
749        if state.is_abandoned {
750            return Err(ValidationError::AbandonedIdentity {
751                sequence: expected_seq,
752            });
753        }
754        if establishment_only && matches!(event, Event::Ixn(_)) {
755            return Err(ValidationError::EstablishmentOnly {
756                sequence: expected_seq,
757            });
758        }
759
760        verify_event_said(event)?;
761        verify_sequence(event, expected_seq)?;
762        verify_chain_linkage(event, &state)?;
763        // Authenticate against the in-force key-state BEFORE applying the event
764        // (rot/drt verify the prior next-threshold against the pre-rotation state).
765        validate_signed_event(signed, Some(&state))?;
766
767        match event {
768            Event::Rot(rot) => validate_rotation(rot, expected_seq, &mut state)?,
769            Event::Ixn(ixn) => validate_interaction(ixn, expected_seq, &mut state)?,
770            Event::Icp(_) | Event::Dip(_) => return Err(ValidationError::MultipleInceptions),
771            Event::Drt(drt) => {
772                validate_delegated_rotation(drt, expected_seq, &mut state, lookup)?;
773            }
774        }
775    }
776
777    Ok(state)
778}
779
780/// Gate one establishment event on M-of-N witness agreement.
781///
782/// Returns `Some(WitnessedReplay::Pending)` when the in-force backer threshold
783/// is not met by distinct designated-witness receipts, or `None` when the event
784/// is witness-accepted (including the `bt=0` zero-witness path). KAWA does the
785/// M-of-N math and the AID dedupe / non-designated-witness filtering.
786fn gate_establishment(
787    controller: &Prefix,
788    state: &KeyState,
789    sequence: u128,
790    event_said: &Said,
791    receipt_lookup: &dyn WitnessReceiptLookup,
792) -> Option<WitnessedReplay> {
793    let sn = sequence as u64;
794    let agreement = WitnessAgreement::new(1);
795    agreement.submit_event(
796        controller,
797        sn,
798        event_said,
799        &state.backer_threshold,
800        &state.backers,
801    );
802    for receipt in receipt_lookup.receipts_for(controller, KeriSequence::new(sequence), event_said)
803    {
804        agreement.add_receipt(controller, sn, event_said, receipt.witness.as_str());
805    }
806    match agreement.status(controller, sn, event_said) {
807        AgreementStatus::Accepted => None,
808        AgreementStatus::Pending { collected } => Some(WitnessedReplay::Pending {
809            state: state.clone(),
810            sequence,
811            said: event_said.clone(),
812            required: state.backer_threshold.clone(),
813            collected,
814        }),
815    }
816}
817
818fn validate_backer_uniqueness(backers: &[Prefix]) -> Result<(), ValidationError> {
819    let mut seen = std::collections::HashSet::new();
820    for b in backers {
821        if !seen.insert(b.as_str()) {
822            return Err(ValidationError::DuplicateBacker {
823                aid: b.as_str().to_string(),
824            });
825        }
826    }
827    Ok(())
828}
829
830/// Structural threshold satisfiability for an establishment event's
831/// `kt`/`nt`/`bt` against the key, next-commitment, and backer lists.
832fn validate_thresholds(
833    sequence: u128,
834    kt: &Threshold,
835    k_len: usize,
836    nt: &Threshold,
837    n_len: usize,
838    bt: &Threshold,
839    b_len: usize,
840) -> Result<(), ValidationError> {
841    let check = |t: &Threshold, len: usize, which: &str| {
842        t.validate_satisfiable(len)
843            .map_err(|e| ValidationError::ThresholdNotSatisfiable {
844                sequence,
845                reason: format!("{which}: {}", e.reason),
846            })
847    };
848    check(kt, k_len, "kt")?;
849    check(nt, n_len, "nt")?;
850    check(bt, b_len, "bt")?;
851    Ok(())
852}
853
854/// Enforce inception self-certification — bind the controller prefix `i` to the
855/// event so a forged inception cannot claim an arbitrary prefix with
856/// attacker-controlled keys (RT-001).
857///
858/// `compute_said` blanks `i` before hashing (an inception's prefix derives FROM
859/// its SAID, not the reverse), so verifying `d == compute_said(body)` does NOT
860/// bind `i`. This supplies that binding:
861/// - self-addressing (`E`-prefixed) AIDs: `i` MUST equal the SAID `d`;
862/// - basic-derivation AIDs (`D`/`1AAI`/…): `i` MUST equal the lone key `k[0]`.
863///
864/// This is the same rule [`verify_event_crypto`] enforces on the append path;
865/// both now route through here so the two paths cannot drift.
866fn verify_inception_self_cert(i: &Prefix, d: &Said, k: &[CesrKey]) -> Result<(), ValidationError> {
867    // Presence: an inception must commit at least one key.
868    if k.is_empty() {
869        return Err(ValidationError::SignatureFailed { sequence: 0 });
870    }
871
872    if i.as_str().starts_with('E') {
873        if i.as_str() != d.as_str() {
874            return Err(ValidationError::InvalidSaid {
875                expected: d.clone(),
876                actual: Said::new_unchecked(i.as_str().to_string()),
877            });
878        }
879    } else {
880        // Basic-derivation: the prefix IS the single inception key. Without this
881        // a `D…`/`1AAI…` prefix could point at an arbitrary key list.
882        let i_key = KeriPublicKey::parse(i.as_str())
883            .map_err(|_| ValidationError::SignatureFailed { sequence: 0 })?;
884        let k0 = k[0]
885            .parse()
886            .map_err(|_| ValidationError::SignatureFailed { sequence: 0 })?;
887        if i_key.as_bytes() != k0.as_bytes() {
888            return Err(ValidationError::InvalidSaid {
889                expected: Said::new_unchecked(k[0].as_str().to_string()),
890                actual: Said::new_unchecked(i.as_str().to_string()),
891            });
892        }
893    }
894
895    Ok(())
896}
897
898fn validate_inception(icp: &IcpEvent) -> Result<KeyState, ValidationError> {
899    // Self-certification: bind `i` to the event before adopting it as the
900    // controller prefix (RT-001). Runs after `verify_event_said` has confirmed
901    // `d` is the true SAID, so `i == d` means `i` is the true SAID too.
902    verify_inception_self_cert(&icp.i, &icp.d, &icp.k)?;
903
904    // Validate backer uniqueness
905    validate_backer_uniqueness(&icp.b)?;
906
907    // Threshold satisfiability (kt over k, nt over n, bt over b).
908    validate_thresholds(
909        icp.s.value(),
910        &icp.kt,
911        icp.k.len(),
912        &icp.nt,
913        icp.n.len(),
914        &icp.bt,
915        icp.b.len(),
916    )?;
917
918    // Validate bt consistency: empty backers must have bt == 0
919    let bt_val = icp.bt.simple_value().unwrap_or(0);
920    if icp.b.is_empty() && bt_val != 0 {
921        return Err(ValidationError::InvalidBackerThreshold {
922            bt: bt_val,
923            backer_count: 0,
924        });
925    }
926
927    Ok(KeyState::from_inception(
928        icp.i.clone(),
929        icp.k.clone(),
930        icp.n.clone(),
931        icp.kt.clone(),
932        icp.nt.clone(),
933        icp.d.clone(),
934        icp.b.clone(),
935        icp.bt.clone(),
936        icp.c.clone(),
937    ))
938}
939
940fn verify_sequence(event: &Event, expected: u128) -> Result<(), ValidationError> {
941    let actual = event.sequence().value();
942    if actual != expected {
943        return Err(ValidationError::InvalidSequence { expected, actual });
944    }
945    Ok(())
946}
947
948fn verify_chain_linkage(event: &Event, state: &KeyState) -> Result<(), ValidationError> {
949    let prev_said = event.previous().ok_or(ValidationError::NotInception)?;
950    if *prev_said != state.last_event_said {
951        return Err(ValidationError::BrokenChain {
952            sequence: event.sequence().value(),
953            referenced: prev_said.clone(),
954            actual: state.last_event_said.clone(),
955        });
956    }
957    Ok(())
958}
959
960/// Returns whether the new key list reveals enough prior next-key commitments
961/// to satisfy the typed prior `nt` threshold.
962///
963/// Each prior commitment index `j` counts as "revealed" when some new key
964/// hashes to `next_commitment[j]`; the typed [`Threshold::is_satisfied`] then
965/// decides over those indices. This replaces the legacy
966/// `simple_value().unwrap_or(1)` collapse, which silently reduced any weighted
967/// `nt` to a 1-of-N (F-15).
968fn prior_commitments_satisfy_threshold(
969    next_commitment: &[Said],
970    next_threshold: &Threshold,
971    new_keys: &[CesrKey],
972) -> bool {
973    let revealed: Vec<u32> = next_commitment
974        .iter()
975        .enumerate()
976        .filter_map(|(j, commitment)| {
977            let matched = new_keys.iter().any(|key| {
978                key.parse()
979                    .map(|pk| verify_commitment(&pk, commitment))
980                    .unwrap_or(false)
981            });
982            matched.then_some(j as u32)
983        })
984        .collect();
985    next_threshold.is_satisfied(&revealed, next_commitment.len())
986}
987
988/// Registrar-backer role designated by an event's config traits.
989///
990/// `RB` and `NRB` are mutually exclusive backer semantics; the latter wins when
991/// both appear (per [`ConfigTrait`] supersedence). `Unspecified` means the
992/// event's `c[]` named neither, so the role is inherited rather than changed.
993#[derive(Debug, Clone, Copy, PartialEq, Eq)]
994enum BackerRole {
995    Registrar,
996    NoRegistrar,
997    Unspecified,
998}
999
1000/// Resolve the registrar-backer role designated by a config-trait list.
1001fn backer_role(traits: &[ConfigTrait]) -> BackerRole {
1002    let mut role = BackerRole::Unspecified;
1003    for t in traits {
1004        match t {
1005            ConfigTrait::RegistrarBackers => role = BackerRole::Registrar,
1006            ConfigTrait::NoRegistrarBackers => role = BackerRole::NoRegistrar,
1007            _ => {}
1008        }
1009    }
1010    role
1011}
1012
1013fn validate_rotation(
1014    rot: &RotEvent,
1015    sequence: u128,
1016    state: &mut KeyState,
1017) -> Result<(), ValidationError> {
1018    // Threshold satisfiability for the new establishment config. `br`/`ba` are
1019    // deltas, so the post-rotation backer count is the prior set minus removals
1020    // plus additions.
1021    let post_backer_count =
1022        state.backers.iter().filter(|b| !rot.br.contains(b)).count() + rot.ba.len();
1023    validate_thresholds(
1024        sequence,
1025        &rot.kt,
1026        rot.k.len(),
1027        &rot.nt,
1028        rot.n.len(),
1029        &rot.bt,
1030        post_backer_count,
1031    )?;
1032
1033    // Verify all pre-rotation commitments against the typed prior `nt`.
1034    if !state.next_commitment.is_empty()
1035        && !prior_commitments_satisfy_threshold(
1036            &state.next_commitment,
1037            &state.next_threshold,
1038            &rot.k,
1039        )
1040    {
1041        return Err(ValidationError::CommitmentMismatch { sequence });
1042    }
1043
1044    // Validate backer uniqueness within br and ba.
1045    validate_backer_uniqueness(&rot.br)?;
1046    validate_backer_uniqueness(&rot.ba)?;
1047    // br and ba must not overlap.
1048    for aid in &rot.ba {
1049        if rot.br.contains(aid) {
1050            return Err(ValidationError::DuplicateBacker {
1051                aid: aid.as_str().to_string(),
1052            });
1053        }
1054    }
1055    // Each `br` (cut) must be a current backer; each `ba` (add) must not already
1056    // be a surviving backer. Otherwise apply_rotation's retain+extend would
1057    // silently corrupt the backer set and `bt` accounting (F-05).
1058    for aid in &rot.br {
1059        if !state.backers.contains(aid) {
1060            return Err(ValidationError::InvalidBackerDelta {
1061                sequence,
1062                reason: format!("br entry {} not in prior backers", aid.as_str()),
1063            });
1064        }
1065    }
1066    let survivors: Vec<_> = state
1067        .backers
1068        .iter()
1069        .filter(|b| !rot.br.contains(b))
1070        .collect();
1071    for aid in &rot.ba {
1072        if survivors.contains(&aid) {
1073            return Err(ValidationError::InvalidBackerDelta {
1074                sequence,
1075                reason: format!("ba entry {} duplicates a surviving backer", aid.as_str()),
1076            });
1077        }
1078    }
1079
1080    // Reject a silent RB<->NRB role flip that retains prior backers. A
1081    // non-empty `c[]` naming the opposite role must rebuild `b[]` — cut every
1082    // prior backer — or a survivor ends up governed by semantics it was never
1083    // admitted under (F-23). An empty `c[]` inherits the role, so cannot flip.
1084    if !rot.c.is_empty() {
1085        let old_role = backer_role(&state.config_traits);
1086        let new_role = backer_role(&rot.c);
1087        let is_flip = matches!(
1088            (old_role, new_role),
1089            (BackerRole::Registrar, BackerRole::NoRegistrar)
1090                | (BackerRole::NoRegistrar, BackerRole::Registrar)
1091        );
1092        if is_flip && !survivors.is_empty() {
1093            return Err(ValidationError::BackerRoleFlip {
1094                sequence,
1095                reason: format!(
1096                    "{old_role:?}->{new_role:?} but {} prior backer(s) survive; \
1097                     a role flip must cut all prior backers",
1098                    survivors.len()
1099                ),
1100            });
1101        }
1102    }
1103
1104    state.apply_rotation(
1105        rot.k.clone(),
1106        rot.n.clone(),
1107        rot.kt.clone(),
1108        rot.nt.clone(),
1109        sequence,
1110        rot.d.clone(),
1111        &rot.br,
1112        &rot.ba,
1113        rot.bt.clone(),
1114        rot.c.clone(),
1115    );
1116
1117    Ok(())
1118}
1119
1120fn validate_interaction(
1121    ixn: &IxnEvent,
1122    sequence: u128,
1123    state: &mut KeyState,
1124) -> Result<(), ValidationError> {
1125    // Presence check: ixn events are only valid against a transferable,
1126    // non-abandoned identity with an available current key. The value itself
1127    // is not used here — signature verification against it happens at the
1128    // KEL-ingest boundary.
1129    state
1130        .current_key()
1131        .ok_or(ValidationError::SignatureFailed { sequence })?;
1132    state.apply_interaction(sequence, ixn.d.clone());
1133    Ok(())
1134}
1135
1136/// Validate a delegated inception event (`dip`) per KERI §11.
1137///
1138/// Beyond the standard inception checks, the validator requires the
1139/// delegator's KEL to contain an `ixn` event whose `a[]` seal references
1140/// `dip.d`. Without that seal the delegated identifier is not authorized.
1141fn validate_delegated_inception(
1142    dip: &crate::events::DipEvent,
1143    lookup: Option<&dyn DelegatorKelLookup>,
1144) -> Result<KeyState, ValidationError> {
1145    let sequence = dip.s.value();
1146    let lookup = lookup.ok_or(ValidationError::DelegatorLookupMissing { sequence })?;
1147
1148    // Bilateral delegation binding: the delegator anchored this dip (delegator
1149    // side) AND the dip's -G source seal points back at that exact anchoring
1150    // event (delegate side).
1151    let anchor = lookup.find_seal(&dip.di, &dip.d).ok_or_else(|| {
1152        ValidationError::DelegatorSealNotFound {
1153            sequence,
1154            delegator_aid: dip.di.as_str().to_string(),
1155        }
1156    })?;
1157    enforce_source_seal(dip.source_seal.as_ref(), &anchor, sequence)?;
1158
1159    // Self-certification (RT-001): a delegated AID's prefix is the SAID of its
1160    // own inception, so `i == d` must hold here as well.
1161    verify_inception_self_cert(&dip.i, &dip.d, &dip.k)?;
1162
1163    // Structural checks mirrored from `validate_inception` — backers, threshold.
1164    validate_backer_uniqueness(&dip.b)?;
1165    let bt_val = dip.bt.simple_value().unwrap_or(0);
1166    if dip.b.is_empty() && bt_val != 0 {
1167        return Err(ValidationError::InvalidBackerThreshold {
1168            bt: bt_val,
1169            backer_count: 0,
1170        });
1171    }
1172
1173    // Build state from the dip event.
1174    let is_non_transferable = dip.n.is_empty();
1175    Ok(KeyState {
1176        prefix: dip.i.clone(),
1177        current_keys: dip.k.clone(),
1178        next_commitment: dip.n.clone(),
1179        sequence: dip.s.value(),
1180        last_event_said: dip.d.clone(),
1181        is_abandoned: false,
1182        threshold: dip.kt.clone(),
1183        next_threshold: dip.nt.clone(),
1184        backers: dip.b.clone(),
1185        backer_threshold: dip.bt.clone(),
1186        config_traits: dip.c.clone(),
1187        is_non_transferable,
1188        delegator: Some(dip.di.clone()),
1189        last_establishment_sequence: dip.s.value(),
1190    })
1191}
1192
1193/// Validate a delegated rotation event (`drt`) per KERI §11.
1194///
1195/// Requires the delegator's KEL to contain an `ixn` event anchoring this
1196/// rotation via its SAID. Standard rotation rules also apply (chain,
1197/// sequence, pre-rotation commitment).
1198fn validate_delegated_rotation(
1199    drt: &crate::events::DrtEvent,
1200    sequence: u128,
1201    state: &mut KeyState,
1202    lookup: Option<&dyn DelegatorKelLookup>,
1203) -> Result<(), ValidationError> {
1204    let lookup = lookup.ok_or(ValidationError::DelegatorLookupMissing { sequence })?;
1205
1206    // Bilateral delegation binding (as for dip): delegator-anchored seal AND the
1207    // drt's -G source seal pointing back at that anchoring event.
1208    let anchor = lookup.find_seal(&drt.di, &drt.d).ok_or_else(|| {
1209        ValidationError::DelegatorSealNotFound {
1210            sequence,
1211            delegator_aid: drt.di.as_str().to_string(),
1212        }
1213    })?;
1214    enforce_source_seal(drt.source_seal.as_ref(), &anchor, sequence)?;
1215
1216    // Standard rotation commitment/backer checks applied to drt fields.
1217    if !state.next_commitment.is_empty()
1218        && !prior_commitments_satisfy_threshold(
1219            &state.next_commitment,
1220            &state.next_threshold,
1221            &drt.k,
1222        )
1223    {
1224        return Err(ValidationError::CommitmentMismatch { sequence });
1225    }
1226
1227    validate_backer_uniqueness(&drt.br)?;
1228    validate_backer_uniqueness(&drt.ba)?;
1229    for aid in &drt.ba {
1230        if drt.br.contains(aid) {
1231            return Err(ValidationError::DuplicateBacker {
1232                aid: aid.as_str().to_string(),
1233            });
1234        }
1235    }
1236
1237    // Apply: the rotation advances the KEL state the same way a plain rot would.
1238    state.sequence = sequence;
1239    state.last_event_said = drt.d.clone();
1240    state.current_keys = drt.k.clone();
1241    state.next_commitment = drt.n.clone();
1242    state.threshold = drt.kt.clone();
1243    state.next_threshold = drt.nt.clone();
1244    Ok(())
1245}
1246
1247/// Validate the cryptographic integrity of a single event against the current key state.
1248///
1249/// Args:
1250/// * `event` - The event to validate.
1251/// * `current_state` - The current `KeyState` (None for inception events).
1252pub fn verify_event_crypto(
1253    event: &Event,
1254    current_state: Option<&KeyState>,
1255) -> Result<(), ValidationError> {
1256    match event {
1257        // Self-certification (`i==d` / `i==k[0]`) is enforced by the shared
1258        // helper so the append and replay paths cannot drift (RT-001).
1259        Event::Icp(icp) => verify_inception_self_cert(&icp.i, &icp.d, &icp.k),
1260        Event::Rot(rot) => {
1261            let sequence = event.sequence().value();
1262            let state = current_state.ok_or(ValidationError::SignatureFailed { sequence })?;
1263
1264            if state.is_abandoned || state.next_commitment.is_empty() {
1265                return Err(ValidationError::CommitmentMismatch { sequence });
1266            }
1267
1268            if rot.k.is_empty() {
1269                return Err(ValidationError::SignatureFailed { sequence });
1270            }
1271
1272            // Verify pre-rotation commitments against the typed prior `nt`.
1273            if !prior_commitments_satisfy_threshold(
1274                &state.next_commitment,
1275                &state.next_threshold,
1276                &rot.k,
1277            ) {
1278                return Err(ValidationError::CommitmentMismatch { sequence });
1279            }
1280
1281            Ok(())
1282        }
1283        Event::Ixn(_) => {
1284            let sequence = event.sequence().value();
1285            let state = current_state.ok_or(ValidationError::SignatureFailed { sequence })?;
1286
1287            // Presence check: ixn requires a transferable, non-abandoned state
1288            // with an available current key.
1289            state
1290                .current_key()
1291                .ok_or(ValidationError::SignatureFailed { sequence })?;
1292
1293            Ok(())
1294        }
1295        // Delegated inception is self-addressing too: enforce `i==d` via the
1296        // shared helper rather than only a presence check (RT-001).
1297        Event::Dip(dip) => verify_inception_self_cert(&dip.i, &dip.d, &dip.k),
1298        Event::Drt(drt) => {
1299            let sequence = event.sequence().value();
1300            let state = current_state.ok_or(ValidationError::SignatureFailed { sequence })?;
1301
1302            if state.is_abandoned || state.next_commitment.is_empty() {
1303                return Err(ValidationError::CommitmentMismatch { sequence });
1304            }
1305            if drt.k.is_empty() {
1306                return Err(ValidationError::SignatureFailed { sequence });
1307            }
1308            Ok(())
1309        }
1310    }
1311}
1312
1313/// Compute the key state after applying `event` on top of `current_state`.
1314///
1315/// The single authoritative event→state transition, shared by every KEL
1316/// store (packed registry, per-prefix witness store) and the replay paths so
1317/// they cannot drift. Inception events (`icp`/`dip`) require `current_state`
1318/// to be `None`-compatible (they ignore it); every other event type requires
1319/// the prior state. A `dip` carries its delegator into
1320/// [`KeyState::delegator`].
1321///
1322/// Args:
1323/// * `current_state` - The state before this event (`None` before inception).
1324/// * `event` - The event to apply.
1325///
1326/// Usage:
1327/// ```ignore
1328/// let next = state_after_event(state.as_ref(), &event)?;
1329/// ```
1330pub fn state_after_event(
1331    current_state: Option<&KeyState>,
1332    event: &Event,
1333) -> Result<KeyState, ValidationError> {
1334    let sequence = event.sequence().value();
1335    match event {
1336        Event::Icp(icp) => Ok(KeyState::from_inception(
1337            icp.i.clone(),
1338            icp.k.clone(),
1339            icp.n.clone(),
1340            icp.kt.clone(),
1341            icp.nt.clone(),
1342            icp.d.clone(),
1343            icp.b.clone(),
1344            icp.bt.clone(),
1345            icp.c.clone(),
1346        )),
1347        Event::Rot(rot) => {
1348            let mut state = current_state
1349                .cloned()
1350                .ok_or(ValidationError::MissingPriorState { sequence })?;
1351            state.apply_rotation(
1352                rot.k.clone(),
1353                rot.n.clone(),
1354                rot.kt.clone(),
1355                rot.nt.clone(),
1356                sequence,
1357                rot.d.clone(),
1358                &rot.br,
1359                &rot.ba,
1360                rot.bt.clone(),
1361                rot.c.clone(),
1362            );
1363            Ok(state)
1364        }
1365        Event::Ixn(ixn) => {
1366            let mut state = current_state
1367                .cloned()
1368                .ok_or(ValidationError::MissingPriorState { sequence })?;
1369            state.apply_interaction(sequence, ixn.d.clone());
1370            Ok(state)
1371        }
1372        Event::Dip(dip) => {
1373            let mut state = KeyState::from_inception(
1374                dip.i.clone(),
1375                dip.k.clone(),
1376                dip.n.clone(),
1377                dip.kt.clone(),
1378                dip.nt.clone(),
1379                dip.d.clone(),
1380                dip.b.clone(),
1381                dip.bt.clone(),
1382                dip.c.clone(),
1383            );
1384            // A delegated inception CARRIES its delegator; dropping `di` here
1385            // leaves every downstream key state reporting `delegator: null`,
1386            // breaking any consumer that proves the chain-to-root off it.
1387            state.delegator = Some(dip.di.clone());
1388            Ok(state)
1389        }
1390        Event::Drt(drt) => {
1391            let mut state = current_state
1392                .cloned()
1393                .ok_or(ValidationError::MissingPriorState { sequence })?;
1394            state.apply_rotation(
1395                drt.k.clone(),
1396                drt.n.clone(),
1397                drt.kt.clone(),
1398                drt.nt.clone(),
1399                sequence,
1400                drt.d.clone(),
1401                &drt.br,
1402                &drt.ba,
1403                drt.bt.clone(),
1404                drt.c.clone(),
1405            );
1406            Ok(state)
1407        }
1408    }
1409}
1410
1411/// Verify an event's SAID matches its content hash.
1412///
1413/// Args:
1414/// * `event` - The event to verify.
1415pub fn verify_event_said(event: &Event) -> Result<(), ValidationError> {
1416    let value =
1417        serde_json::to_value(event).map_err(|e| ValidationError::Serialization(e.to_string()))?;
1418    let computed =
1419        compute_said(&value).map_err(|e| ValidationError::Serialization(e.to_string()))?;
1420    let actual = event.said();
1421
1422    if computed != *actual {
1423        return Err(ValidationError::InvalidSaid {
1424            expected: computed,
1425            actual: actual.clone(),
1426        });
1427    }
1428
1429    Ok(())
1430}
1431
1432/// Validate a single event for appending to a KEL with known state.
1433///
1434/// Args:
1435/// * `event` - The event to validate for append.
1436/// * `state` - The current `KeyState` (tip of the existing KEL).
1437pub fn validate_for_append(event: &Event, state: &KeyState) -> Result<(), ValidationError> {
1438    if matches!(event, Event::Icp(_)) {
1439        return Err(ValidationError::MultipleInceptions);
1440    }
1441
1442    verify_event_said(event)?;
1443    verify_sequence(event, state.sequence + 1)?;
1444    verify_chain_linkage(event, state)?;
1445    verify_event_crypto(event, Some(state))?;
1446
1447    Ok(())
1448}
1449
1450/// Compute the SAID for an event.
1451///
1452/// Args:
1453/// * `event` - The event to compute the SAID for.
1454pub fn compute_event_said(event: &Event) -> Result<Said, ValidationError> {
1455    let value =
1456        serde_json::to_value(event).map_err(|e| ValidationError::Serialization(e.to_string()))?;
1457    compute_said(&value).map_err(|e| ValidationError::Serialization(e.to_string()))
1458}
1459
1460/// Serialize a finalized event for signing.
1461///
1462/// KERI signs over the fully-formed event bytes — `d` (SAID) and `i` (prefix)
1463/// already populated by `finalize_*_event`, and the version string declaring
1464/// the true body length. A spec verifier (KERIpy/KERIox) parses `v` first and
1465/// frames the body by that length, so the signed bytes MUST equal the wire
1466/// bytes. (The prior implementation cleared `d`/`i` after finalization, making
1467/// the signed body shorter than `v` claimed — a hard interop break.)
1468///
1469/// Args:
1470/// * `event` - The finalized event to serialize for signing.
1471pub fn serialize_for_signing(event: &Event) -> Result<Vec<u8>, ValidationError> {
1472    serde_json::to_vec(event).map_err(|e| ValidationError::Serialization(e.to_string()))
1473}
1474
1475/// Validate a signed event's crypto (signatures + commitments) against key state.
1476///
1477/// This is the preferred entry point for validating events with externalized signatures.
1478///
1479/// Args:
1480/// * `signed` - The signed event with detached signatures.
1481/// * `current_state` - The current `KeyState` (None for inception events).
1482pub fn validate_signed_event(
1483    signed: &crate::events::SignedEvent,
1484    current_state: Option<&KeyState>,
1485) -> Result<(), ValidationError> {
1486    let event = &signed.event;
1487    let sequence = event.sequence().value();
1488
1489    if signed.signatures.is_empty() {
1490        return Err(ValidationError::SignatureFailed { sequence });
1491    }
1492
1493    // Determine the key list and threshold for verification
1494    let (keys, threshold) = match event {
1495        Event::Icp(icp) => (&icp.k, &icp.kt),
1496        Event::Dip(dip) => (&dip.k, &dip.kt),
1497        Event::Rot(rot) => (&rot.k, &rot.kt),
1498        Event::Drt(drt) => (&drt.k, &drt.kt),
1499        Event::Ixn(_) => {
1500            let state = current_state.ok_or(ValidationError::SignatureFailed { sequence })?;
1501            (&state.current_keys, &state.threshold)
1502        }
1503    };
1504
1505    if keys.is_empty() {
1506        return Err(ValidationError::SignatureFailed { sequence });
1507    }
1508
1509    // Verify each signature and collect verified indices
1510    let canonical = serialize_for_signing(event)?;
1511    let mut verified_indices = Vec::new();
1512
1513    for sig in &signed.signatures {
1514        let idx = sig.index as usize;
1515        if idx >= keys.len() {
1516            continue; // out-of-range index, skip
1517        }
1518        let key = &keys[idx];
1519        if let Ok(pk) = key.parse()
1520            && pk.verify_signature(&canonical, &sig.sig).is_ok()
1521        {
1522            verified_indices.push(sig.index);
1523        }
1524    }
1525
1526    // Check threshold satisfaction (current key threshold)
1527    if !threshold.is_satisfied(&verified_indices, keys.len()) {
1528        return Err(ValidationError::SignatureFailed { sequence });
1529    }
1530
1531    // For rotation events: also check prior next-threshold from the previous
1532    // establishment event. The spec requires signatures satisfy BOTH the current
1533    // signing threshold AND the prior next rotation threshold.
1534    if matches!(event, Event::Rot(_) | Event::Drt(_))
1535        && let Some(state) = current_state
1536    {
1537        let n_len = state.next_commitment.len();
1538
1539        // Bind each verifying signature to the prior commitment it reveals: the
1540        // new key `k[index]` must hash to `n[prior_index]` (or `n[index]` for a
1541        // single-index sig, where keripy emits code `A` with ondex == index). The
1542        // prior `nt` must then be met over the DISTINCT prior-commitment indices.
1543        let mut verified_prior: Vec<u32> = Vec::new();
1544        for sig in &signed.signatures {
1545            let Some(key) = keys.get(sig.index as usize) else {
1546                continue;
1547            };
1548            let Ok(pk) = key.parse() else {
1549                continue;
1550            };
1551            if pk.verify_signature(&canonical, &sig.sig).is_err() {
1552                continue;
1553            }
1554            let j = sig.prior_index.unwrap_or(sig.index) as usize;
1555            let Some(commitment) = state.next_commitment.get(j) else {
1556                continue;
1557            };
1558            if crate::crypto::verify_commitment(&pk, commitment) {
1559                verified_prior.push(j as u32);
1560            }
1561        }
1562
1563        // A cardinality-changing rotation in which NO signature revealed a prior
1564        // commitment is unbindable — surface the diagnostic rather than a generic
1565        // signature failure. (A well-formed removal binds at least one; a single
1566        // signer at prior slot 0 binds via the index == ondex fallback.)
1567        if n_len != keys.len() && verified_prior.is_empty() {
1568            return Err(ValidationError::AsymmetricKeyRotation {
1569                sequence,
1570                prior_next_count: n_len,
1571                new_key_count: keys.len(),
1572            });
1573        }
1574
1575        if !state.next_threshold.is_satisfied(&verified_prior, n_len) {
1576            return Err(ValidationError::SignatureFailed { sequence });
1577        }
1578    }
1579
1580    Ok(())
1581}
1582
1583/// Create an inception event with a properly computed SAID.
1584///
1585/// Args:
1586/// * `icp` - The inception event to finalize.
1587pub fn finalize_icp_event(mut icp: IcpEvent) -> Result<IcpEvent, ValidationError> {
1588    let value = serde_json::to_value(Event::Icp(icp.clone()))
1589        .map_err(|e| ValidationError::Serialization(e.to_string()))?;
1590    let said = compute_said(&value).map_err(|e| ValidationError::Serialization(e.to_string()))?;
1591
1592    icp.d = said.clone();
1593    // Only set i = d for self-addressing AIDs (empty or E-prefixed)
1594    if icp.i.is_empty() || icp.i.as_str().starts_with('E') {
1595        icp.i = Prefix::new_unchecked(said.into_inner());
1596    }
1597
1598    // Set version string with actual byte count
1599    let final_bytes = serde_json::to_vec(&Event::Icp(icp.clone()))
1600        .map_err(|e| ValidationError::Serialization(e.to_string()))?;
1601    icp.v = crate::types::VersionString::json(final_bytes.len() as u32);
1602
1603    Ok(icp)
1604}
1605
1606/// Create a delegated inception (`dip`) event with a properly computed SAID.
1607///
1608/// Mirrors [`finalize_icp_event`] for `dip`: a delegated AID's prefix is
1609/// self-addressing (the SAID of its own inception event), so `i` is set to `d`.
1610///
1611/// Args:
1612/// * `dip` - The delegated inception event to finalize (with `di` set to the delegator).
1613pub fn finalize_dip_event(
1614    mut dip: crate::events::DipEvent,
1615) -> Result<crate::events::DipEvent, ValidationError> {
1616    let value = serde_json::to_value(Event::Dip(dip.clone()))
1617        .map_err(|e| ValidationError::Serialization(e.to_string()))?;
1618    let said = compute_said(&value).map_err(|e| ValidationError::Serialization(e.to_string()))?;
1619
1620    dip.d = said.clone();
1621    // A delegated AID is self-addressing: its prefix is the SAID of the dip.
1622    if dip.i.is_empty() || dip.i.as_str().starts_with('E') {
1623        dip.i = Prefix::new_unchecked(said.into_inner());
1624    }
1625
1626    let final_bytes = serde_json::to_vec(&Event::Dip(dip.clone()))
1627        .map_err(|e| ValidationError::Serialization(e.to_string()))?;
1628    dip.v = crate::types::VersionString::json(final_bytes.len() as u32);
1629
1630    Ok(dip)
1631}
1632
1633/// Create a rotation event with a properly computed SAID.
1634///
1635/// Args:
1636/// * `rot` - The rotation event to finalize.
1637pub fn finalize_rot_event(mut rot: RotEvent) -> Result<RotEvent, ValidationError> {
1638    let value = serde_json::to_value(Event::Rot(rot.clone()))
1639        .map_err(|e| ValidationError::Serialization(e.to_string()))?;
1640    let said = compute_said(&value).map_err(|e| ValidationError::Serialization(e.to_string()))?;
1641    rot.d = said;
1642
1643    let final_bytes = serde_json::to_vec(&Event::Rot(rot.clone()))
1644        .map_err(|e| ValidationError::Serialization(e.to_string()))?;
1645    rot.v = crate::types::VersionString::json(final_bytes.len() as u32);
1646
1647    Ok(rot)
1648}
1649
1650/// Create a delegated rotation (`drt`) event with a properly computed SAID.
1651///
1652/// Mirrors [`finalize_rot_event`]. A `drt` is **not** self-addressing — its `i`
1653/// is the existing delegated AID prefix — so only `d` and `v` are set (`i` is
1654/// left unchanged, unlike `dip`).
1655///
1656/// Args:
1657/// * `drt` - The delegated rotation event to finalize.
1658pub fn finalize_drt_event(
1659    mut drt: crate::events::DrtEvent,
1660) -> Result<crate::events::DrtEvent, ValidationError> {
1661    let value = serde_json::to_value(Event::Drt(drt.clone()))
1662        .map_err(|e| ValidationError::Serialization(e.to_string()))?;
1663    let said = compute_said(&value).map_err(|e| ValidationError::Serialization(e.to_string()))?;
1664    drt.d = said;
1665
1666    let final_bytes = serde_json::to_vec(&Event::Drt(drt.clone()))
1667        .map_err(|e| ValidationError::Serialization(e.to_string()))?;
1668    drt.v = crate::types::VersionString::json(final_bytes.len() as u32);
1669
1670    Ok(drt)
1671}
1672
1673/// Create an interaction event with a properly computed SAID.
1674///
1675/// Args:
1676/// * `ixn` - The interaction event to finalize.
1677pub fn finalize_ixn_event(mut ixn: IxnEvent) -> Result<IxnEvent, ValidationError> {
1678    let value = serde_json::to_value(Event::Ixn(ixn.clone()))
1679        .map_err(|e| ValidationError::Serialization(e.to_string()))?;
1680    let said = compute_said(&value).map_err(|e| ValidationError::Serialization(e.to_string()))?;
1681    ixn.d = said;
1682
1683    let final_bytes = serde_json::to_vec(&Event::Ixn(ixn.clone()))
1684        .map_err(|e| ValidationError::Serialization(e.to_string()))?;
1685    ixn.v = crate::types::VersionString::json(final_bytes.len() as u32);
1686
1687    Ok(ixn)
1688}
1689
1690/// Search for a seal with the given digest in any IXN event in the KEL.
1691///
1692/// Returns the sequence number of the IXN event if found.
1693///
1694/// Args:
1695/// * `events` - The event log to search.
1696/// * `digest` - The SAID digest to search for.
1697pub fn find_seal_in_kel(events: &[Event], digest: &str) -> Option<u128> {
1698    for event in events {
1699        if let Event::Ixn(ixn) = event {
1700            for seal in &ixn.a {
1701                if seal.digest_value().is_some_and(|d| d.as_str() == digest) {
1702                    return Some(ixn.s.value());
1703                }
1704            }
1705        }
1706    }
1707    None
1708}
1709
1710/// Parse a KEL from a JSON string.
1711///
1712/// Args:
1713/// * `json` - JSON string containing a list of KERI events.
1714pub fn parse_kel_json(json: &str) -> Result<Vec<Event>, ValidationError> {
1715    serde_json::from_str(json).map_err(|e| ValidationError::Serialization(e.to_string()))
1716}
1717
1718#[cfg(test)]
1719#[allow(clippy::unwrap_used, clippy::expect_used)]
1720mod tests {
1721    use super::*;
1722    use crate::events::{IndexedSignature, KeriSequence, Seal, SignedEvent};
1723    use crate::types::{CesrKey, Threshold, VersionString};
1724    use ring::rand::SystemRandom;
1725    use ring::signature::{Ed25519KeyPair, KeyPair};
1726
1727    fn gen_keypair() -> Ed25519KeyPair {
1728        let rng = SystemRandom::new();
1729        let pkcs8 = Ed25519KeyPair::generate_pkcs8(&rng).unwrap();
1730        Ed25519KeyPair::from_pkcs8(pkcs8.as_ref()).unwrap()
1731    }
1732
1733    fn encode_pubkey(kp: &Ed25519KeyPair) -> String {
1734        crate::cesr_encode::encode_verkey(kp.public_key().as_ref(), cesride::matter::Codex::Ed25519)
1735            .unwrap()
1736    }
1737
1738    fn make_raw_icp(key: &str, next: &str) -> IcpEvent {
1739        IcpEvent {
1740            v: VersionString::placeholder(),
1741            d: Said::default(),
1742            i: Prefix::default(),
1743            s: KeriSequence::new(0),
1744            kt: Threshold::Simple(1),
1745            k: vec![CesrKey::new_unchecked(key.to_string())],
1746            nt: Threshold::Simple(1),
1747            n: vec![Said::new_unchecked(next.to_string())],
1748            bt: Threshold::Simple(0),
1749            b: vec![],
1750            c: vec![],
1751            a: vec![],
1752        }
1753    }
1754
1755    fn make_signed_icp() -> (IcpEvent, Ed25519KeyPair) {
1756        let rng = SystemRandom::new();
1757        let pkcs8 = Ed25519KeyPair::generate_pkcs8(&rng).unwrap();
1758        let keypair = Ed25519KeyPair::from_pkcs8(pkcs8.as_ref()).unwrap();
1759        let key_encoded = encode_pubkey(&keypair);
1760
1761        let icp = IcpEvent {
1762            v: VersionString::placeholder(),
1763            d: Said::default(),
1764            i: Prefix::default(),
1765            s: KeriSequence::new(0),
1766            kt: Threshold::Simple(1),
1767            k: vec![CesrKey::new_unchecked(key_encoded)],
1768            nt: Threshold::Simple(1),
1769            n: vec![Said::new_unchecked("ENextCommitment".to_string())],
1770            bt: Threshold::Simple(0),
1771            b: vec![],
1772            c: vec![],
1773            a: vec![],
1774        };
1775
1776        let finalized = finalize_icp_event(icp).unwrap();
1777        (finalized, keypair)
1778    }
1779
1780    fn make_signed_ixn(
1781        prefix: &Prefix,
1782        prev_said: &Said,
1783        seq: u128,
1784        _keypair: &Ed25519KeyPair,
1785    ) -> IxnEvent {
1786        let mut ixn = IxnEvent {
1787            v: VersionString::placeholder(),
1788            d: Said::default(),
1789            i: prefix.clone(),
1790            s: KeriSequence::new(seq),
1791            p: prev_said.clone(),
1792            a: vec![Seal::digest("EAttest")],
1793        };
1794
1795        let value = serde_json::to_value(Event::Ixn(ixn.clone())).unwrap();
1796        ixn.d = compute_said(&value).unwrap();
1797
1798        ixn
1799    }
1800
1801    #[test]
1802    fn finalize_icp_sets_said() {
1803        let icp = make_raw_icp("DKey1", "ENext1");
1804        let finalized = finalize_icp_event(icp).unwrap();
1805
1806        assert!(!finalized.d.is_empty());
1807        assert_eq!(finalized.d.as_str(), finalized.i.as_str());
1808        assert!(finalized.d.as_str().starts_with('E'));
1809    }
1810
1811    #[test]
1812    fn validates_single_inception() {
1813        let (icp, _keypair) = make_signed_icp();
1814        let events = vec![Event::Icp(icp.clone())];
1815
1816        let state = validate_kel(&events).unwrap();
1817        assert_eq!(state.prefix, icp.i);
1818        assert_eq!(state.sequence, 0);
1819    }
1820
1821    #[test]
1822    fn rejects_empty_kel() {
1823        let result = validate_kel(&[]);
1824        assert!(matches!(result, Err(ValidationError::EmptyKel)));
1825    }
1826
1827    #[test]
1828    fn rejects_non_inception_first() {
1829        let mut ixn = IxnEvent {
1830            v: VersionString::placeholder(),
1831            d: Said::default(),
1832            i: Prefix::new_unchecked("ETest".to_string()),
1833            s: KeriSequence::new(0),
1834            p: Said::new_unchecked("EPrev".to_string()),
1835            a: vec![],
1836        };
1837        // Compute a valid SAID so verify_event_said passes — the test
1838        // should fail on NotInception, not on SaidMismatch.
1839        let event = Event::Ixn(ixn.clone());
1840        if let Ok(said) = compute_event_said(&event) {
1841            ixn.d = said;
1842        }
1843        let events = vec![Event::Ixn(ixn)];
1844        let result = validate_kel(&events);
1845        assert!(matches!(result, Err(ValidationError::NotInception)));
1846    }
1847
1848    #[test]
1849    fn rejects_broken_sequence() {
1850        let (icp, _keypair) = make_signed_icp();
1851
1852        let mut ixn = IxnEvent {
1853            v: VersionString::placeholder(),
1854            d: Said::default(),
1855            i: icp.i.clone(),
1856            s: KeriSequence::new(5),
1857            p: icp.d.clone(),
1858            a: vec![],
1859        };
1860
1861        let value = serde_json::to_value(Event::Ixn(ixn.clone())).unwrap();
1862        ixn.d = compute_said(&value).unwrap();
1863
1864        let events = vec![Event::Icp(icp), Event::Ixn(ixn)];
1865        let result = validate_kel(&events);
1866        assert!(matches!(
1867            result,
1868            Err(ValidationError::InvalidSequence {
1869                expected: 1,
1870                actual: 5
1871            })
1872        ));
1873    }
1874
1875    #[test]
1876    fn rejects_broken_chain() {
1877        let (icp, _keypair) = make_signed_icp();
1878
1879        let mut ixn = IxnEvent {
1880            v: VersionString::placeholder(),
1881            d: Said::default(),
1882            i: icp.i.clone(),
1883            s: KeriSequence::new(1),
1884            p: Said::new_unchecked("EWrongPrevious".to_string()),
1885            a: vec![],
1886        };
1887
1888        let value = serde_json::to_value(Event::Ixn(ixn.clone())).unwrap();
1889        ixn.d = compute_said(&value).unwrap();
1890
1891        let events = vec![Event::Icp(icp), Event::Ixn(ixn)];
1892        let result = validate_kel(&events);
1893        assert!(matches!(result, Err(ValidationError::BrokenChain { .. })));
1894    }
1895
1896    #[test]
1897    fn rejects_invalid_said() {
1898        let icp = make_raw_icp("DKey1", "ENext1");
1899        let finalized = finalize_icp_event(icp).unwrap();
1900
1901        let mut tampered = finalized.clone();
1902        tampered.d = Said::new_unchecked("EWrongSaid".to_string());
1903
1904        let events = vec![Event::Icp(tampered)];
1905        let result = validate_kel(&events);
1906        assert!(matches!(result, Err(ValidationError::InvalidSaid { .. })));
1907    }
1908
1909    // RT-001 (A.2): forged-inception self-certification on the replay path.
1910    // `compute_said` blanks `i` before hashing, so a valid SAID `d` does NOT
1911    // bind the controller prefix `i`. Without the `i==d` / `i==k[0]` check a KEL
1912    // handed to a stateless verifier could claim an arbitrary prefix with
1913    // attacker keys. These two tests are red before A.2 and green after.
1914
1915    #[test]
1916    fn rejects_forged_inception_prefix_mismatch() {
1917        // Self-addressing arm: replace a finalized inception's prefix `i` with a
1918        // DIFFERENT well-formed `E…` prefix. The SAID `d` still verifies
1919        // (compute_said blanks `i`); only the `i == d` self-cert check catches it.
1920        let (icp, _kp) = make_signed_icp();
1921        assert_eq!(
1922            icp.i.as_str(),
1923            icp.d.as_str(),
1924            "a finalized inception is self-addressing"
1925        );
1926
1927        let (other, _kp2) = make_signed_icp();
1928        assert_ne!(other.i.as_str(), icp.d.as_str());
1929
1930        let mut forged = icp;
1931        forged.i = other.i;
1932        let result = validate_kel(&[Event::Icp(forged)]);
1933        assert!(
1934            matches!(result, Err(ValidationError::InvalidSaid { .. })),
1935            "forged inception (i != d) must be rejected, got {result:?}"
1936        );
1937    }
1938
1939    #[test]
1940    fn rejects_forged_inception_basic_derivation() {
1941        // Basic-derivation arm: a non-`E` prefix IS the inception key, so `i`
1942        // must equal `k[0]`. Forge an inception whose prefix names a DIFFERENT
1943        // key than the one it commits.
1944        let prefix_key = encode_pubkey(&gen_keypair());
1945        let committed_key = encode_pubkey(&gen_keypair());
1946        assert_ne!(prefix_key, committed_key);
1947        assert!(!prefix_key.starts_with('E'));
1948
1949        let mut icp = make_raw_icp(&committed_key, "ENext1");
1950        icp.i = Prefix::new_unchecked(prefix_key);
1951        // Valid SAID (compute_said blanks `i` for icp), so verify_event_said
1952        // passes and only the `i == k[0]` self-cert check should reject.
1953        let value = serde_json::to_value(Event::Icp(icp.clone())).unwrap();
1954        icp.d = compute_said(&value).unwrap();
1955
1956        let result = validate_kel(&[Event::Icp(icp)]);
1957        assert!(
1958            matches!(result, Err(ValidationError::InvalidSaid { .. })),
1959            "basic-derivation inception with i != k[0] must be rejected, got {result:?}"
1960        );
1961    }
1962
1963    // `validate_signed_kel` is the AUTHENTICATED replay — it verifies each event's
1964    // signature against the controlling key-state, so a forged unsigned /
1965    // wrong-signer `ixn`/`rot`/`icp` is rejected (tests below).
1966    // The structural `validate_kel`/`replay_kel_gated` remain for the trusted-local
1967    // path (replaying a KEL already authenticated on write to the registry), where
1968    // they authorize by log structure only. The stateless verify entrypoints that
1969    // ingest an untrusted KEL DO authenticate: the identity bundle carries a CESR
1970    // signature attachment per event and the bundle/WASM paths call
1971    // `validate_signed_kel` (see `auths-verifier` `commit_bundle.rs` and `wasm.rs`,
1972    // and the forged/stripped-signature rejection tests there). Do not mistake the
1973    // structural path for authentication — it is the trusted-local replay only.
1974
1975    fn sign_event(event: &Event, kp: &Ed25519KeyPair) -> SignedEvent {
1976        let sig = kp
1977            .sign(&serialize_for_signing(event).unwrap())
1978            .as_ref()
1979            .to_vec();
1980        SignedEvent::new(
1981            event.clone(),
1982            vec![IndexedSignature {
1983                index: 0,
1984                prior_index: None,
1985                sig,
1986            }],
1987        )
1988    }
1989
1990    #[test]
1991    fn validate_signed_kel_accepts_correctly_signed_kel() {
1992        let (icp, kp) = make_signed_icp();
1993        let signed_icp = sign_event(&Event::Icp(icp.clone()), &kp);
1994        let ixn = make_signed_ixn(&icp.i, &icp.d, 1, &kp);
1995        let signed_ixn = sign_event(&Event::Ixn(ixn), &kp);
1996
1997        let state = validate_signed_kel(&[signed_icp, signed_ixn], None)
1998            .expect("a correctly-signed KEL must validate");
1999        assert_eq!(state.sequence, 1);
2000    }
2001
2002    #[test]
2003    fn validate_signed_kel_rejects_unsigned_ixn() {
2004        // RT-002: a structurally-valid but UNSIGNED ixn (e.g. anchoring a forged
2005        // delegation/scope seal) must be rejected by the authenticated replay.
2006        let (icp, kp) = make_signed_icp();
2007        let signed_icp = sign_event(&Event::Icp(icp.clone()), &kp);
2008        let ixn = make_signed_ixn(&icp.i, &icp.d, 1, &kp);
2009        let unsigned_ixn = SignedEvent::new(Event::Ixn(ixn), vec![]);
2010
2011        let result = validate_signed_kel(&[signed_icp, unsigned_ixn], None);
2012        assert!(
2013            matches!(result, Err(ValidationError::SignatureFailed { .. })),
2014            "unsigned ixn must be rejected, got {result:?}"
2015        );
2016    }
2017
2018    #[test]
2019    fn validate_signed_kel_rejects_wrong_signer_ixn() {
2020        // RT-002: an ixn signed by a key OTHER than the controlling key-state
2021        // must be rejected — a forged interaction cannot be smuggled in.
2022        let (icp, kp) = make_signed_icp();
2023        let signed_icp = sign_event(&Event::Icp(icp.clone()), &kp);
2024        let ixn = make_signed_ixn(&icp.i, &icp.d, 1, &kp);
2025        let attacker = gen_keypair();
2026        let forged_ixn = sign_event(&Event::Ixn(ixn), &attacker);
2027
2028        let result = validate_signed_kel(&[signed_icp, forged_ixn], None);
2029        assert!(
2030            matches!(result, Err(ValidationError::SignatureFailed { .. })),
2031            "wrong-signer ixn must be rejected, got {result:?}"
2032        );
2033    }
2034
2035    #[test]
2036    fn validates_icp_then_ixn() {
2037        let (icp, keypair) = make_signed_icp();
2038        let ixn = make_signed_ixn(&icp.i, &icp.d, 1, &keypair);
2039
2040        let events = vec![Event::Icp(icp), Event::Ixn(ixn.clone())];
2041        let state = validate_kel(&events).unwrap();
2042        assert_eq!(state.sequence, 1);
2043        assert_eq!(state.last_event_said, ixn.d);
2044    }
2045
2046    #[test]
2047    fn compute_event_said_works() {
2048        let icp = make_raw_icp("DKey1", "ENext1");
2049        let event = Event::Icp(icp);
2050        let said = compute_event_said(&event).unwrap();
2051        assert!(said.as_str().starts_with('E'));
2052        assert!(!said.is_empty());
2053    }
2054
2055    // Sanity control: a correctly-signed SignedEvent must be accepted. Without
2056    // this, a regression that makes `validate_signed_event` always return
2057    // `SignatureFailed` would silently "pass" the rejection tests below.
2058    #[test]
2059    fn accepts_correct_signature() {
2060        let (icp, keypair) = make_signed_icp();
2061        let event = Event::Icp(icp);
2062        let canonical = serialize_for_signing(&event).unwrap();
2063        let sig = keypair.sign(&canonical).as_ref().to_vec();
2064        let signed = SignedEvent::new(
2065            event,
2066            vec![IndexedSignature {
2067                index: 0,
2068                prior_index: None,
2069                sig,
2070            }],
2071        );
2072
2073        validate_signed_event(&signed, None).expect("correct signature must validate");
2074    }
2075
2076    // Intent: a SignedEvent whose attached signature bytes do not match the
2077    // canonical event body must be rejected. Uses the externalized-signature
2078    // entry point (`validate_signed_event`); `validate_kel` only checks KEL
2079    // structure and does not consume attached signatures, so it cannot be
2080    // used to test signature-level rejection.
2081    #[test]
2082    fn rejects_forged_signature() {
2083        let (icp, _keypair) = make_signed_icp();
2084        let event = Event::Icp(icp);
2085        let forged_sig = vec![0u8; 64]; // valid length, invalid content
2086        let signed = SignedEvent::new(
2087            event,
2088            vec![IndexedSignature {
2089                index: 0,
2090                prior_index: None,
2091                sig: forged_sig,
2092            }],
2093        );
2094
2095        assert!(matches!(
2096            validate_signed_event(&signed, None),
2097            Err(ValidationError::SignatureFailed { sequence: 0 })
2098        ));
2099    }
2100
2101    // `rejects_missing_signature` was tied to the legacy in-body `x` field.
2102    // Signatures are externalized now; the equivalent check is covered by
2103    // `validate_signed_event` tests in `multi_key_threshold.rs`.
2104
2105    // Intent: a SignedEvent signed by a keypair other than the one committed
2106    // in `icp.k` must be rejected. The wrong-key signature is structurally
2107    // valid (correct length, correct type) but fails Ed25519 verification
2108    // against the committed public key.
2109    #[test]
2110    fn rejects_wrong_key_signature() {
2111        let committed = gen_keypair();
2112        let key_encoded = encode_pubkey(&committed);
2113
2114        let icp = IcpEvent {
2115            v: VersionString::placeholder(),
2116            d: Said::default(),
2117            i: Prefix::default(),
2118            s: KeriSequence::new(0),
2119            kt: Threshold::Simple(1),
2120            k: vec![CesrKey::new_unchecked(key_encoded)],
2121            nt: Threshold::Simple(1),
2122            n: vec![Said::new_unchecked("ENextCommitment".to_string())],
2123            bt: Threshold::Simple(0),
2124            b: vec![],
2125            c: vec![],
2126            a: vec![],
2127        };
2128        let icp = finalize_icp_event(icp).unwrap();
2129        let event = Event::Icp(icp);
2130
2131        let wrong = gen_keypair();
2132        let canonical = serialize_for_signing(&event).unwrap();
2133        let wrong_sig = wrong.sign(&canonical).as_ref().to_vec();
2134        let signed = SignedEvent::new(
2135            event,
2136            vec![IndexedSignature {
2137                index: 0,
2138                prior_index: None,
2139                sig: wrong_sig,
2140            }],
2141        );
2142
2143        assert!(matches!(
2144            validate_signed_event(&signed, None),
2145            Err(ValidationError::SignatureFailed { sequence: 0 })
2146        ));
2147    }
2148
2149    #[test]
2150    fn crypto_accepts_valid_inception() {
2151        let (icp, _keypair) = make_signed_icp();
2152        let result = verify_event_crypto(&Event::Icp(icp), None);
2153        assert!(result.is_ok());
2154    }
2155
2156    #[test]
2157    fn find_seal_in_kel_finds_digest() {
2158        let (icp, keypair) = make_signed_icp();
2159        let ixn = make_signed_ixn(&icp.i, &icp.d, 1, &keypair);
2160        let events = vec![Event::Icp(icp), Event::Ixn(ixn)];
2161        assert_eq!(find_seal_in_kel(&events, "EAttest"), Some(1));
2162        assert_eq!(find_seal_in_kel(&events, "ENonExistent"), None);
2163    }
2164
2165    #[test]
2166    fn parse_kel_json_rejects_invalid_hex_sequence() {
2167        let json = r#"[{"v":"KERI10JSON","t":"icp","i":"E123","s":"not_hex","kt":"1","k":["DKey"],"nt":"1","n":["ENext"],"bt":"0","b":[]}]"#;
2168        let result = parse_kel_json(json);
2169        assert!(result.is_err(), "expected error for invalid hex sequence");
2170    }
2171
2172    /// Build a signed ICP with caller-supplied overrides applied after keypair
2173    /// generation but before finalization and signing.
2174    fn make_custom_signed_icp(customize: impl FnOnce(&mut IcpEvent)) -> (IcpEvent, Ed25519KeyPair) {
2175        let rng = SystemRandom::new();
2176        let pkcs8 = Ed25519KeyPair::generate_pkcs8(&rng).unwrap();
2177        let keypair = Ed25519KeyPair::from_pkcs8(pkcs8.as_ref()).unwrap();
2178        let key_encoded = encode_pubkey(&keypair);
2179
2180        let mut icp = IcpEvent {
2181            v: VersionString::placeholder(),
2182            d: Said::default(),
2183            i: Prefix::default(),
2184            s: KeriSequence::new(0),
2185            kt: Threshold::Simple(1),
2186            k: vec![CesrKey::new_unchecked(key_encoded)],
2187            nt: Threshold::Simple(1),
2188            n: vec![Said::new_unchecked("ENextCommitment".to_string())],
2189            bt: Threshold::Simple(0),
2190            b: vec![],
2191            c: vec![],
2192            a: vec![],
2193        };
2194
2195        customize(&mut icp);
2196
2197        let finalized = finalize_icp_event(icp).unwrap();
2198        (finalized, keypair)
2199    }
2200
2201    #[test]
2202    fn rejects_events_after_abandonment() {
2203        // Abandonment = rotation with empty n (not inception — that's NonTransferable).
2204        let kp2 = gen_keypair();
2205
2206        // Use make_custom_signed_icp with pre-committed key for kp2
2207        let commitment2 = crate::crypto::compute_next_commitment(
2208            &crate::keys::KeriPublicKey::ed25519(kp2.public_key().as_ref()).unwrap(),
2209        );
2210        let (icp, _kp1) = make_custom_signed_icp(|icp| {
2211            icp.n = vec![commitment2.clone()];
2212        });
2213        let prefix = icp.i.clone();
2214
2215        // Rotation that abandons (empty n)
2216        let mut rot = RotEvent {
2217            v: VersionString::placeholder(),
2218            d: Said::default(),
2219            i: prefix.clone(),
2220            s: KeriSequence::new(1),
2221            p: icp.d.clone(),
2222            kt: Threshold::Simple(1),
2223            k: vec![CesrKey::new_unchecked(encode_pubkey(&kp2))],
2224            nt: Threshold::Simple(0),
2225            n: vec![],
2226            bt: Threshold::Simple(0),
2227            br: vec![],
2228            ba: vec![],
2229            c: vec![],
2230            a: vec![],
2231        };
2232        let val = serde_json::to_value(Event::Rot(rot.clone())).unwrap();
2233        rot.d = compute_said(&val).unwrap();
2234
2235        let ixn = make_signed_ixn(&prefix, &rot.d, 2, &kp2);
2236        let events = vec![Event::Icp(icp), Event::Rot(rot), Event::Ixn(ixn)];
2237        let result = validate_kel(&events);
2238        assert!(
2239            matches!(result, Err(ValidationError::AbandonedIdentity { .. })),
2240            "expected AbandonedIdentity, got: {result:?}"
2241        );
2242    }
2243
2244    #[test]
2245    fn rejects_ixn_in_establishment_only_kel() {
2246        let (icp, keypair) = make_custom_signed_icp(|icp| {
2247            icp.c = vec![ConfigTrait::EstablishmentOnly];
2248        });
2249        let ixn = make_signed_ixn(&icp.i, &icp.d, 1, &keypair);
2250        let events = vec![Event::Icp(icp), Event::Ixn(ixn)];
2251        let result = validate_kel(&events);
2252        assert!(
2253            matches!(result, Err(ValidationError::EstablishmentOnly { .. })),
2254            "expected EstablishmentOnly, got: {result:?}"
2255        );
2256    }
2257
2258    #[test]
2259    fn rejects_events_after_non_transferable_inception() {
2260        let (icp, keypair) = make_custom_signed_icp(|icp| {
2261            icp.n = vec![];
2262            icp.nt = Threshold::Simple(0);
2263        });
2264        let ixn = make_signed_ixn(&icp.i, &icp.d, 1, &keypair);
2265        let events = vec![Event::Icp(icp), Event::Ixn(ixn)];
2266        let result = validate_kel(&events);
2267        assert!(
2268            matches!(
2269                result,
2270                Err(ValidationError::NonTransferable)
2271                    | Err(ValidationError::AbandonedIdentity { .. })
2272            ),
2273            "expected NonTransferable or AbandonedIdentity, got: {result:?}"
2274        );
2275    }
2276
2277    #[test]
2278    fn rejects_duplicate_backers() {
2279        let (_, result) = {
2280            let rng = SystemRandom::new();
2281            let pkcs8 = Ed25519KeyPair::generate_pkcs8(&rng).unwrap();
2282            let keypair = Ed25519KeyPair::from_pkcs8(pkcs8.as_ref()).unwrap();
2283            let key_encoded = encode_pubkey(&keypair);
2284
2285            let dup_backer = Prefix::new_unchecked("DWit1".to_string());
2286            let icp = IcpEvent {
2287                v: VersionString::placeholder(),
2288                d: Said::default(),
2289                i: Prefix::default(),
2290                s: KeriSequence::new(0),
2291                kt: Threshold::Simple(1),
2292                k: vec![CesrKey::new_unchecked(key_encoded)],
2293                nt: Threshold::Simple(1),
2294                n: vec![Said::new_unchecked("ENextCommitment".to_string())],
2295                bt: Threshold::Simple(2),
2296                b: vec![dup_backer.clone(), dup_backer],
2297                c: vec![],
2298                a: vec![],
2299            };
2300
2301            let finalized = finalize_icp_event(icp).unwrap();
2302            let events = vec![Event::Icp(finalized)];
2303            (keypair, validate_kel(&events))
2304        };
2305        assert!(
2306            matches!(result, Err(ValidationError::DuplicateBacker { .. })),
2307            "expected DuplicateBacker, got: {result:?}"
2308        );
2309    }
2310
2311    #[test]
2312    fn rejects_invalid_backer_threshold() {
2313        let (_, result) = {
2314            let rng = SystemRandom::new();
2315            let pkcs8 = Ed25519KeyPair::generate_pkcs8(&rng).unwrap();
2316            let keypair = Ed25519KeyPair::from_pkcs8(pkcs8.as_ref()).unwrap();
2317            let key_encoded = encode_pubkey(&keypair);
2318
2319            let icp = IcpEvent {
2320                v: VersionString::placeholder(),
2321                d: Said::default(),
2322                i: Prefix::default(),
2323                s: KeriSequence::new(0),
2324                kt: Threshold::Simple(1),
2325                k: vec![CesrKey::new_unchecked(key_encoded)],
2326                nt: Threshold::Simple(1),
2327                n: vec![Said::new_unchecked("ENextCommitment".to_string())],
2328                bt: Threshold::Simple(2),
2329                b: vec![],
2330                c: vec![],
2331                a: vec![],
2332            };
2333
2334            let finalized = finalize_icp_event(icp).unwrap();
2335            let events = vec![Event::Icp(finalized)];
2336            (keypair, validate_kel(&events))
2337        };
2338        // `bt=2` over zero backers is now caught by the stricter structural
2339        // threshold-satisfiability guard (A.4) before the legacy
2340        // empty-backers/bt!=0 check.
2341        assert!(
2342            matches!(result, Err(ValidationError::ThresholdNotSatisfiable { .. })),
2343            "expected ThresholdNotSatisfiable, got: {result:?}"
2344        );
2345    }
2346
2347    #[test]
2348    fn sign_over_finalized_bytes_roundtrips() {
2349        // A.2: the bytes handed to the signer must equal the wire bytes, whose
2350        // length the version string `v` declares. (Previously d/i were cleared
2351        // after finalize, making the signed body shorter than `v` claimed.)
2352        let (icp, _kp) = make_signed_icp();
2353        let bytes = serialize_for_signing(&Event::Icp(icp.clone())).unwrap();
2354        assert_eq!(
2355            bytes.len() as u32,
2356            icp.v.size,
2357            "signed byte length must equal the version-string size field"
2358        );
2359        let reparsed: Event = serde_json::from_slice(&bytes).unwrap();
2360        assert!(reparsed.is_inception());
2361    }
2362
2363    #[test]
2364    fn threshold_rejects_kt_gt_k() {
2365        // A.4: a signing threshold larger than the key-list length is
2366        // structurally unsatisfiable and must be rejected at validation.
2367        let kp = gen_keypair();
2368        let key = encode_pubkey(&kp);
2369        let icp = IcpEvent {
2370            v: VersionString::placeholder(),
2371            d: Said::default(),
2372            i: Prefix::default(),
2373            s: KeriSequence::new(0),
2374            kt: Threshold::Simple(5),
2375            k: vec![CesrKey::new_unchecked(key)],
2376            nt: Threshold::Simple(1),
2377            n: vec![Said::new_unchecked("ENextCommitment".to_string())],
2378            bt: Threshold::Simple(0),
2379            b: vec![],
2380            c: vec![],
2381            a: vec![],
2382        };
2383        let finalized = finalize_icp_event(icp).unwrap();
2384        let result = validate_kel(&[Event::Icp(finalized)]);
2385        assert!(
2386            matches!(result, Err(ValidationError::ThresholdNotSatisfiable { .. })),
2387            "expected ThresholdNotSatisfiable, got: {result:?}"
2388        );
2389    }
2390
2391    #[test]
2392    fn rotation_rejects_br_not_in_prior() {
2393        // A.10 (F-05): a rotation that cuts a backer not in the prior set, or
2394        // adds a backer that already survives, must be rejected before
2395        // apply_rotation corrupts the backer set.
2396        let state = KeyState::from_inception(
2397            Prefix::new_unchecked("EPrefix".to_string()),
2398            vec![CesrKey::new_unchecked("DKey1".to_string())],
2399            vec![], // empty next_commitment -> commitment check skipped
2400            Threshold::Simple(1),
2401            Threshold::Simple(0),
2402            Said::new_unchecked("ESAID".to_string()),
2403            vec![Prefix::new_unchecked("BWit1".to_string())],
2404            Threshold::Simple(0),
2405            vec![],
2406        );
2407
2408        let make_rot = |br: Vec<Prefix>, ba: Vec<Prefix>| RotEvent {
2409            v: VersionString::placeholder(),
2410            d: Said::default(),
2411            i: Prefix::new_unchecked("EPrefix".to_string()),
2412            s: KeriSequence::new(1),
2413            p: Said::new_unchecked("ESAID".to_string()),
2414            kt: Threshold::Simple(1),
2415            k: vec![CesrKey::new_unchecked("DKey2".to_string())],
2416            nt: Threshold::Simple(0),
2417            n: vec![],
2418            bt: Threshold::Simple(0),
2419            br,
2420            ba,
2421            c: vec![],
2422            a: vec![],
2423        };
2424
2425        // br entry not in prior backers -> rejected.
2426        let bad_cut = make_rot(vec![Prefix::new_unchecked("BWitX".to_string())], vec![]);
2427        assert!(matches!(
2428            validate_rotation(&bad_cut, 1, &mut state.clone()),
2429            Err(ValidationError::InvalidBackerDelta { .. })
2430        ));
2431
2432        // ba entry duplicating a surviving backer -> rejected.
2433        let bad_add = make_rot(vec![], vec![Prefix::new_unchecked("BWit1".to_string())]);
2434        assert!(matches!(
2435            validate_rotation(&bad_add, 1, &mut state.clone()),
2436            Err(ValidationError::InvalidBackerDelta { .. })
2437        ));
2438
2439        // valid delta (cut the existing backer) -> ok.
2440        let ok = make_rot(vec![Prefix::new_unchecked("BWit1".to_string())], vec![]);
2441        assert!(validate_rotation(&ok, 1, &mut state.clone()).is_ok());
2442    }
2443
2444    #[test]
2445    fn rotation_rejects_silent_backer_role_flip() {
2446        // A.13 (F-23): flipping RB<->NRB while a prior backer survives is
2447        // rejected; the same flip is allowed once every prior backer is cut
2448        // (b[] rebuilt). An empty c[] inherits the role and never flips.
2449        let nrb_state = || {
2450            KeyState::from_inception(
2451                Prefix::new_unchecked("EPrefix".to_string()),
2452                vec![CesrKey::new_unchecked("DKey1".to_string())],
2453                vec![],
2454                Threshold::Simple(1),
2455                Threshold::Simple(0),
2456                Said::new_unchecked("ESAID".to_string()),
2457                vec![Prefix::new_unchecked("BWit1".to_string())],
2458                Threshold::Simple(0),
2459                vec![ConfigTrait::NoRegistrarBackers],
2460            )
2461        };
2462
2463        let make_rot = |br: Vec<Prefix>, ba: Vec<Prefix>, c: Vec<ConfigTrait>| RotEvent {
2464            v: VersionString::placeholder(),
2465            d: Said::default(),
2466            i: Prefix::new_unchecked("EPrefix".to_string()),
2467            s: KeriSequence::new(1),
2468            p: Said::new_unchecked("ESAID".to_string()),
2469            kt: Threshold::Simple(1),
2470            k: vec![CesrKey::new_unchecked("DKey2".to_string())],
2471            nt: Threshold::Simple(0),
2472            n: vec![],
2473            bt: Threshold::Simple(0),
2474            br,
2475            ba,
2476            c,
2477            a: vec![],
2478        };
2479
2480        // Flip NRB->RB while BWit1 survives -> rejected.
2481        let flip_keep = make_rot(vec![], vec![], vec![ConfigTrait::RegistrarBackers]);
2482        assert!(matches!(
2483            validate_rotation(&flip_keep, 1, &mut nrb_state()),
2484            Err(ValidationError::BackerRoleFlip { .. })
2485        ));
2486
2487        // Flip NRB->RB after cutting every prior backer -> ok (b[] rebuilt).
2488        let flip_rebuild = make_rot(
2489            vec![Prefix::new_unchecked("BWit1".to_string())],
2490            vec![],
2491            vec![ConfigTrait::RegistrarBackers],
2492        );
2493        assert!(validate_rotation(&flip_rebuild, 1, &mut nrb_state()).is_ok());
2494
2495        // Same role kept (NRB->NRB) with the backer surviving -> ok (no flip).
2496        let same_role = make_rot(vec![], vec![], vec![ConfigTrait::NoRegistrarBackers]);
2497        assert!(validate_rotation(&same_role, 1, &mut nrb_state()).is_ok());
2498
2499        // Empty c[] inherits the role -> ok even though the backer survives.
2500        let inherit = make_rot(vec![], vec![], vec![]);
2501        assert!(validate_rotation(&inherit, 1, &mut nrb_state()).is_ok());
2502    }
2503
2504    // ── D.6: receipt-gated replay ────────────────────────────────────────────
2505
2506    use crate::witness::WitnessReceipt;
2507
2508    /// Said-keyed witness-receipt source for replay-gate tests.
2509    struct MapReceipts {
2510        by_said: std::collections::HashMap<String, Vec<WitnessReceipt>>,
2511    }
2512
2513    impl WitnessReceiptLookup for MapReceipts {
2514        fn receipts_for(
2515            &self,
2516            _controller: &Prefix,
2517            _sn: KeriSequence,
2518            said: &Said,
2519        ) -> Vec<WitnessReceipt> {
2520            self.by_said.get(said.as_str()).cloned().unwrap_or_default()
2521        }
2522    }
2523
2524    fn witness_aid(aid: &str) -> Prefix {
2525        Prefix::new_unchecked(aid.to_string())
2526    }
2527
2528    fn receipt_from(aid: &str) -> WitnessReceipt {
2529        WitnessReceipt {
2530            witness: witness_aid(aid),
2531            signature: vec![],
2532        }
2533    }
2534
2535    fn receipts_under(said: &Said, aids: &[&str]) -> MapReceipts {
2536        let mut by_said = std::collections::HashMap::new();
2537        by_said.insert(
2538            said.as_str().to_string(),
2539            aids.iter().map(|a| receipt_from(a)).collect(),
2540        );
2541        MapReceipts { by_said }
2542    }
2543
2544    /// A finalized inception designating `aids` as backers with threshold `bt`.
2545    fn icp_with_backers(aids: &[&str], bt: u64) -> IcpEvent {
2546        let backers: Vec<Prefix> = aids.iter().map(|a| witness_aid(a)).collect();
2547        let (icp, _kp) = make_custom_signed_icp(|icp| {
2548            icp.b = backers.clone();
2549            icp.bt = Threshold::Simple(bt);
2550        });
2551        icp
2552    }
2553
2554    #[test]
2555    fn replay_bt_zero_accepts_without_receipts() {
2556        let (icp, _kp) = make_signed_icp(); // bt=0, b=[]
2557        let events = vec![Event::Icp(icp)];
2558        let lookup = MapReceipts {
2559            by_said: std::collections::HashMap::new(),
2560        };
2561        let outcome = validate_kel_with_receipts(&events, None, &lookup).unwrap();
2562        assert!(matches!(outcome, WitnessedReplay::Accepted(_)));
2563    }
2564
2565    #[test]
2566    fn replay_at_quorum_accepts() {
2567        let icp = icp_with_backers(&["BWit1", "BWit2"], 2);
2568        let said = icp.d.clone();
2569        let lookup = receipts_under(&said, &["BWit1", "BWit2"]);
2570        let events = vec![Event::Icp(icp)];
2571        let outcome = validate_kel_with_receipts(&events, None, &lookup).unwrap();
2572        assert!(matches!(outcome, WitnessedReplay::Accepted(_)));
2573    }
2574
2575    #[test]
2576    fn replay_under_quorum_is_pending() {
2577        let icp = icp_with_backers(&["BWit1", "BWit2"], 2);
2578        let said = icp.d.clone();
2579        let lookup = receipts_under(&said, &["BWit1"]); // only 1 of 2 required
2580        let events = vec![Event::Icp(icp)];
2581        match validate_kel_with_receipts(&events, None, &lookup).unwrap() {
2582            WitnessedReplay::Pending {
2583                sequence,
2584                collected,
2585                ..
2586            } => {
2587                assert_eq!(sequence, 0);
2588                assert_eq!(collected, 1);
2589            }
2590            WitnessedReplay::Accepted(_) => panic!("expected Pending under quorum"),
2591        }
2592    }
2593
2594    #[test]
2595    fn replay_ignores_duplicate_witness_receipts() {
2596        let icp = icp_with_backers(&["BWit1", "BWit2", "BWit3"], 2);
2597        let said = icp.d.clone();
2598        let lookup = receipts_under(&said, &["BWit1", "BWit1"]); // same witness twice
2599        let events = vec![Event::Icp(icp)];
2600        assert!(matches!(
2601            validate_kel_with_receipts(&events, None, &lookup).unwrap(),
2602            WitnessedReplay::Pending { .. }
2603        ));
2604    }
2605
2606    #[test]
2607    fn replay_ignores_receipt_for_wrong_said() {
2608        let icp = icp_with_backers(&["BWit1", "BWit2"], 2);
2609        // Receipts stored under a different event SAID must never satisfy this event.
2610        let wrong = Said::new_unchecked("EWrongEventSaid".to_string());
2611        let lookup = receipts_under(&wrong, &["BWit1", "BWit2"]);
2612        let events = vec![Event::Icp(icp)];
2613        match validate_kel_with_receipts(&events, None, &lookup).unwrap() {
2614            WitnessedReplay::Pending { collected, .. } => assert_eq!(collected, 0),
2615            WitnessedReplay::Accepted(_) => panic!("wrong-SAID receipts must not count"),
2616        }
2617    }
2618
2619    #[test]
2620    fn replay_uses_witness_set_in_force_at_seq() {
2621        // icp designates {BWit1} bt=1; rot at seq 1 cuts BWit1, adds BWit2, bt=1.
2622        // The seq-1 gate must use the post-rotation set {BWit2}.
2623        let kp2 = gen_keypair();
2624        let kp3 = gen_keypair();
2625        let commitment2 = crate::crypto::compute_next_commitment(
2626            &crate::keys::KeriPublicKey::ed25519(kp2.public_key().as_ref()).unwrap(),
2627        );
2628        let commitment3 = crate::crypto::compute_next_commitment(
2629            &crate::keys::KeriPublicKey::ed25519(kp3.public_key().as_ref()).unwrap(),
2630        );
2631        let (icp, _kp1) = make_custom_signed_icp(|icp| {
2632            icp.b = vec![witness_aid("BWit1")];
2633            icp.bt = Threshold::Simple(1);
2634            icp.n = vec![commitment2.clone()];
2635        });
2636        let prefix = icp.i.clone();
2637        let icp_said = icp.d.clone();
2638
2639        let mut rot = RotEvent {
2640            v: VersionString::placeholder(),
2641            d: Said::default(),
2642            i: prefix.clone(),
2643            s: KeriSequence::new(1),
2644            p: icp_said.clone(),
2645            kt: Threshold::Simple(1),
2646            k: vec![CesrKey::new_unchecked(encode_pubkey(&kp2))],
2647            nt: Threshold::Simple(1),
2648            n: vec![commitment3.clone()],
2649            bt: Threshold::Simple(1),
2650            br: vec![witness_aid("BWit1")],
2651            ba: vec![witness_aid("BWit2")],
2652            c: vec![],
2653            a: vec![],
2654        };
2655        let val = serde_json::to_value(Event::Rot(rot.clone())).unwrap();
2656        rot.d = compute_said(&val).unwrap();
2657        let rot_said = rot.d.clone();
2658
2659        let mut by_said = std::collections::HashMap::new();
2660        by_said.insert(icp_said.as_str().to_string(), vec![receipt_from("BWit1")]);
2661        by_said.insert(rot_said.as_str().to_string(), vec![receipt_from("BWit2")]);
2662        let lookup = MapReceipts { by_said };
2663
2664        let events = vec![Event::Icp(icp), Event::Rot(rot)];
2665        // BWit2 is only in the post-rotation set; acceptance proves the in-force
2666        // set (not the stale {BWit1}) gated the rotation.
2667        assert!(matches!(
2668            validate_kel_with_receipts(&events, None, &lookup).unwrap(),
2669            WitnessedReplay::Accepted(_)
2670        ));
2671    }
2672
2673    #[test]
2674    fn validate_kel_advances_without_receipt_gate() {
2675        // Back-compat: plain validate_kel ignores receipts and advances a bt>0 KEL.
2676        let icp = icp_with_backers(&["BWit1", "BWit2"], 2);
2677        let events = vec![Event::Icp(icp)];
2678        assert!(validate_kel(&events).is_ok());
2679    }
2680}
2681
2682// =============================================================================
2683// Time-aware policy validation — rotation cooldown, clock-skew, emergency
2684// override. `validate_kel` stays pure / clock-free (structural invariants
2685// only); callers who want time-aware checks reach for
2686// `validate_kel_with_policy`.
2687// =============================================================================
2688
2689/// Configurable policy for time-aware KEL validation. Defaults match
2690/// the plan text: 24h minimum rotation interval, 60s clock-skew
2691/// tolerance, no emergency-override identifier.
2692#[derive(Debug, Clone)]
2693pub struct KelPolicy {
2694    /// Minimum wall-clock interval between two consecutive rotation
2695    /// events. Default: 24 hours.
2696    pub min_rotation_interval: chrono::Duration,
2697    /// Maximum allowed skew between an event's `dt` and the wall
2698    /// clock used for validation. Default: 60 seconds.
2699    pub clock_skew_tolerance: chrono::Duration,
2700    /// AID that is permitted to skip the rotation-cooldown check
2701    /// (e.g. the controller's emergency-rotation key). `None` means
2702    /// no override is configured and every rotation must respect
2703    /// the cooldown.
2704    pub emergency_override_did: Option<crate::types::Prefix>,
2705}
2706
2707impl Default for KelPolicy {
2708    fn default() -> Self {
2709        Self {
2710            min_rotation_interval: chrono::Duration::hours(24),
2711            clock_skew_tolerance: chrono::Duration::seconds(60),
2712            emergency_override_did: None,
2713        }
2714    }
2715}
2716
2717/// Validate a KEL against a time-aware [`KelPolicy`].
2718///
2719/// Runs the structural [`validate_kel`] first; on success, layers on
2720/// three additional checks that depend on the `dt` field added to
2721/// establishment and interaction events:
2722///
2723/// 1. Every event MUST carry a `dt`. Pre-`dt`-migration events
2724///    (where `dt` is `None`) fail with
2725///    [`ValidationError::MissingTimestamp`].
2726/// 2. `dt` MUST be monotonically non-decreasing across consecutive
2727///    events. Backward-moving timestamps are evidence of tampering.
2728/// 3. Consecutive rotation events MUST be at least
2729///    [`KelPolicy::min_rotation_interval`] apart (unless the event's
2730///    controller matches [`KelPolicy::emergency_override_did`]).
2731/// 4. Every `dt` must be within
2732///    [`KelPolicy::clock_skew_tolerance`] of `now`.
2733///
2734/// Args:
2735/// * `events`: The ordered KEL.
2736/// * `policy`: [`KelPolicy`] governing the time checks.
2737/// * `now`: The daemon's wall clock at validation time. Inject via
2738///   [`chrono::Utc::now`] at the presentation boundary; domain layers
2739///   pass a clock.
2740pub(crate) fn validate_kel_with_policy(
2741    events: &[Event],
2742    timestamps: &[Option<chrono::DateTime<chrono::Utc>>],
2743    policy: &KelPolicy,
2744    now: chrono::DateTime<chrono::Utc>,
2745) -> Result<KeyState, ValidationError> {
2746    let state = validate_kel(events)?;
2747
2748    let mut last_rotation_dt: Option<chrono::DateTime<chrono::Utc>> = None;
2749    let mut last_any_dt: Option<chrono::DateTime<chrono::Utc>> = None;
2750
2751    for (idx, evt) in events.iter().enumerate() {
2752        let seq = idx as u128;
2753        let (is_rotation, controller) = match evt {
2754            Event::Icp(e) => (false, &e.i),
2755            Event::Rot(e) => (true, &e.i),
2756            Event::Ixn(e) => (false, &e.i),
2757            Event::Dip(e) => (false, &e.i),
2758            Event::Drt(e) => (true, &e.i),
2759        };
2760        let Some(dt) = timestamps.get(idx).copied().flatten() else {
2761            return Err(ValidationError::MissingTimestamp { sequence: seq });
2762        };
2763        // Monotonicity.
2764        if let Some(prev) = last_any_dt
2765            && dt < prev
2766        {
2767            return Err(ValidationError::NonMonotonicTimestamp {
2768                sequence: seq,
2769                prev: prev.to_rfc3339(),
2770                curr: dt.to_rfc3339(),
2771            });
2772        }
2773        // Clock skew.
2774        let skew = (dt - now).num_seconds();
2775        if skew.abs() > policy.clock_skew_tolerance.num_seconds() {
2776            return Err(ValidationError::ClockSkew {
2777                sequence: seq,
2778                skew_secs: skew,
2779                tolerance_secs: policy.clock_skew_tolerance.num_seconds(),
2780            });
2781        }
2782        // Cooldown on rotations.
2783        if is_rotation && let Some(prev) = last_rotation_dt {
2784            let interval = dt - prev;
2785            let is_override = policy
2786                .emergency_override_did
2787                .as_ref()
2788                .is_some_and(|ov| ov == controller);
2789            if !is_override && interval < policy.min_rotation_interval {
2790                return Err(ValidationError::RotationCooldown {
2791                    sequence: seq,
2792                    interval_secs: interval.num_seconds(),
2793                    min_secs: policy.min_rotation_interval.num_seconds(),
2794                });
2795            }
2796        }
2797        last_any_dt = Some(dt);
2798        if is_rotation {
2799            last_rotation_dt = Some(dt);
2800        }
2801    }
2802
2803    Ok(state)
2804}
2805
2806#[cfg(test)]
2807mod policy_tests {
2808    use super::*;
2809    use chrono::{Duration as ChronoDuration, TimeZone, Utc};
2810
2811    fn base_now() -> chrono::DateTime<chrono::Utc> {
2812        Utc.with_ymd_and_hms(2026, 1, 1, 0, 0, 0).unwrap()
2813    }
2814
2815    #[test]
2816    fn policy_rejects_missing_dt_via_empty_kel_path() {
2817        // Structural validation fires first; empty KEL is rejected
2818        // before any policy check runs. Locks in that the policy
2819        // validator doesn't accidentally accept an empty KEL.
2820        let events: Vec<crate::events::Event> = vec![];
2821        let r = validate_kel_with_policy(&events, &[], &KelPolicy::default(), base_now());
2822        assert!(matches!(r, Err(ValidationError::EmptyKel)));
2823    }
2824
2825    #[test]
2826    fn policy_default_values_match_plan() {
2827        let p = KelPolicy::default();
2828        assert_eq!(p.min_rotation_interval, ChronoDuration::hours(24));
2829        assert_eq!(p.clock_skew_tolerance, ChronoDuration::seconds(60));
2830        assert!(p.emergency_override_did.is_none());
2831    }
2832}