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