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/// Validate a KEL with no delegator lookup.
383///
384/// Convenience wrapper over [`validate_kel_with_lookup`] for ordinary KELs
385/// that contain only `icp`/`rot`/`ixn` events. Use the lookup variant for KELs
386/// containing delegated events (`dip`/`drt`).
387///
388/// Args:
389/// * `events` - The ordered list of KERI events to replay and validate.
390pub fn validate_kel(events: &[Event]) -> Result<KeyState, ValidationError> {
391    validate_kel_with_lookup(events, None::<&dyn DelegatorKelLookup>)
392}
393
394/// Validate a KEL with a delegator-lookup hook for delegated events.
395///
396/// Required when the KEL contains `dip` or `drt` events; ordinary KELs
397/// (only `icp`/`rot`/`ixn`) can pass `None`.
398pub fn validate_kel_with_lookup(
399    events: &[Event],
400    lookup: Option<&dyn DelegatorKelLookup>,
401) -> Result<KeyState, ValidationError> {
402    match replay_kel_gated(events, lookup, None)? {
403        WitnessedReplay::Accepted(state) => Ok(state),
404        // With no receipt lookup the gate never runs, so `Pending` is
405        // unreachable; returning the structural state preserves the
406        // no-receipt contract (advance regardless of receipts).
407        WitnessedReplay::Pending { state, .. } => Ok(state),
408    }
409}
410
411/// The outcome of replaying a KEL through the witness-receipt gate.
412///
413/// Unlike [`validate_kel`] (structural only), [`validate_kel_with_receipts`]
414/// will not silently advance past an establishment event that lacks M-of-N
415/// witness agreement — it reports [`WitnessedReplay::Pending`] so the caller
416/// (verifier policy, D.7) can warn or refuse.
417#[derive(Debug, Clone, PartialEq, Eq)]
418pub enum WitnessedReplay {
419    /// Every `bt>0` establishment event reached witness quorum; the key-state
420    /// is witness-authoritative.
421    Accepted(KeyState),
422    /// The KEL is structurally valid, but the establishment event at `sequence`
423    /// did not reach quorum. `state` is the structural replay through that event;
424    /// the caller must not treat key-state at or after `sequence` as
425    /// witness-authoritative.
426    Pending {
427        /// Structural replay result through the under-quorum event.
428        state: KeyState,
429        /// Sequence of the first under-quorum establishment event.
430        sequence: u128,
431        /// SAID of that event.
432        said: Said,
433        /// The backer threshold that was required.
434        required: Threshold,
435        /// Distinct, in-force witness receipts collected for it.
436        collected: usize,
437    },
438}
439
440impl WitnessedReplay {
441    /// The replayed key-state, regardless of the witness-quorum outcome.
442    pub fn state(&self) -> &KeyState {
443        match self {
444            WitnessedReplay::Accepted(state) | WitnessedReplay::Pending { state, .. } => state,
445        }
446    }
447}
448
449/// Validate a KEL and gate each establishment event on M-of-N witness receipts.
450///
451/// Extends [`validate_kel_with_lookup`] with receipt-gated replay: a `bt>0`
452/// establishment event advances `KeyState` only when KAWA
453/// ([`WitnessAgreement`](crate::witness::agreement::WitnessAgreement)) reports
454/// agreement over receipts from **distinct** witnesses in the `b[]` set **in
455/// force at that sequence**. `bt=0` events accept without receipts (the
456/// zero-witness path). Receipts are matched by `(controller, sn, said)` via
457/// `receipt_lookup` and deduped by witness AID; a receipt from a non-designated
458/// witness never counts.
459///
460/// Args:
461/// * `events`: The ordered KEL to replay.
462/// * `delegator_lookup`: Cross-KEL seal lookup for delegated events (`dip`/`drt`).
463/// * `receipt_lookup`: Source of witness receipts per event.
464///
465/// Usage:
466/// ```ignore
467/// match validate_kel_with_receipts(&events, None, &receipts)? {
468///     WitnessedReplay::Accepted(state) => trust(state),
469///     WitnessedReplay::Pending { sequence, .. } => warn_or_refuse(sequence),
470/// }
471/// ```
472pub fn validate_kel_with_receipts(
473    events: &[Event],
474    delegator_lookup: Option<&dyn DelegatorKelLookup>,
475    receipt_lookup: &dyn WitnessReceiptLookup,
476) -> Result<WitnessedReplay, ValidationError> {
477    replay_kel_gated(events, delegator_lookup, Some(receipt_lookup))
478}
479
480/// Shared structural replay with an optional witness-receipt gate.
481///
482/// With `receipt_lookup = None` this is pure structural replay (the
483/// [`validate_kel`] contract). With `Some(_)` each establishment event is gated
484/// on witness quorum; the first under-quorum event short-circuits to
485/// [`WitnessedReplay::Pending`].
486fn replay_kel_gated(
487    events: &[Event],
488    lookup: Option<&dyn DelegatorKelLookup>,
489    receipt_lookup: Option<&dyn WitnessReceiptLookup>,
490) -> Result<WitnessedReplay, ValidationError> {
491    if events.is_empty() {
492        return Err(ValidationError::EmptyKel);
493    }
494
495    verify_event_said(&events[0])?;
496    let (mut state, inception_n_is_empty, establishment_only) = match &events[0] {
497        Event::Icp(icp) => (
498            validate_inception(icp)?,
499            icp.n.is_empty(),
500            icp.c.contains(&ConfigTrait::EstablishmentOnly),
501        ),
502        Event::Dip(dip) => (
503            validate_delegated_inception(dip, lookup)?,
504            dip.n.is_empty(),
505            dip.c.contains(&ConfigTrait::EstablishmentOnly),
506        ),
507        _ => return Err(ValidationError::NotInception),
508    };
509
510    let controller = state.prefix.clone();
511
512    // Gate the inception establishment event on witness quorum.
513    if let Some(rl) = receipt_lookup
514        && let Some(pending) = gate_establishment(&controller, &state, 0, events[0].said(), rl)
515    {
516        return Ok(pending);
517    }
518
519    // Non-transferable identities (inception n is empty) cannot have subsequent events
520    if inception_n_is_empty && events.len() > 1 {
521        return Err(ValidationError::NonTransferable);
522    }
523
524    for (idx, event) in events.iter().enumerate().skip(1) {
525        let expected_seq = idx as u128;
526
527        // Reject any event after abandonment
528        if state.is_abandoned {
529            return Err(ValidationError::AbandonedIdentity {
530                sequence: expected_seq,
531            });
532        }
533
534        // Reject IXN in establishment-only KELs
535        if establishment_only && matches!(event, Event::Ixn(_)) {
536            return Err(ValidationError::EstablishmentOnly {
537                sequence: expected_seq,
538            });
539        }
540
541        verify_event_said(event)?;
542        verify_sequence(event, expected_seq)?;
543        verify_chain_linkage(event, &state)?;
544
545        match event {
546            Event::Rot(rot) => validate_rotation(rot, expected_seq, &mut state)?,
547            Event::Ixn(ixn) => validate_interaction(ixn, expected_seq, &mut state)?,
548            Event::Icp(_) | Event::Dip(_) => return Err(ValidationError::MultipleInceptions),
549            Event::Drt(drt) => {
550                validate_delegated_rotation(drt, expected_seq, &mut state, lookup)?;
551            }
552        }
553
554        // Gate establishment events (rot/drt) on witness quorum; ixn never gates.
555        if let Some(rl) = receipt_lookup
556            && matches!(event, Event::Rot(_) | Event::Drt(_))
557            && let Some(pending) =
558                gate_establishment(&controller, &state, expected_seq, event.said(), rl)
559        {
560            return Ok(pending);
561        }
562    }
563
564    Ok(WitnessedReplay::Accepted(state))
565}
566
567/// Gate one establishment event on M-of-N witness agreement.
568///
569/// Returns `Some(WitnessedReplay::Pending)` when the in-force backer threshold
570/// is not met by distinct designated-witness receipts, or `None` when the event
571/// is witness-accepted (including the `bt=0` zero-witness path). KAWA does the
572/// M-of-N math and the AID dedupe / non-designated-witness filtering.
573fn gate_establishment(
574    controller: &Prefix,
575    state: &KeyState,
576    sequence: u128,
577    event_said: &Said,
578    receipt_lookup: &dyn WitnessReceiptLookup,
579) -> Option<WitnessedReplay> {
580    let sn = sequence as u64;
581    let agreement = WitnessAgreement::new(1);
582    agreement.submit_event(
583        controller,
584        sn,
585        event_said,
586        &state.backer_threshold,
587        &state.backers,
588    );
589    for receipt in receipt_lookup.receipts_for(controller, KeriSequence::new(sequence), event_said)
590    {
591        agreement.add_receipt(controller, sn, event_said, receipt.witness.as_str());
592    }
593    match agreement.status(controller, sn, event_said) {
594        AgreementStatus::Accepted => None,
595        AgreementStatus::Pending { collected } => Some(WitnessedReplay::Pending {
596            state: state.clone(),
597            sequence,
598            said: event_said.clone(),
599            required: state.backer_threshold.clone(),
600            collected,
601        }),
602    }
603}
604
605fn validate_backer_uniqueness(backers: &[Prefix]) -> Result<(), ValidationError> {
606    let mut seen = std::collections::HashSet::new();
607    for b in backers {
608        if !seen.insert(b.as_str()) {
609            return Err(ValidationError::DuplicateBacker {
610                aid: b.as_str().to_string(),
611            });
612        }
613    }
614    Ok(())
615}
616
617/// Structural threshold satisfiability for an establishment event's
618/// `kt`/`nt`/`bt` against the key, next-commitment, and backer lists.
619fn validate_thresholds(
620    sequence: u128,
621    kt: &Threshold,
622    k_len: usize,
623    nt: &Threshold,
624    n_len: usize,
625    bt: &Threshold,
626    b_len: usize,
627) -> Result<(), ValidationError> {
628    let check = |t: &Threshold, len: usize, which: &str| {
629        t.validate_satisfiable(len)
630            .map_err(|e| ValidationError::ThresholdNotSatisfiable {
631                sequence,
632                reason: format!("{which}: {}", e.reason),
633            })
634    };
635    check(kt, k_len, "kt")?;
636    check(nt, n_len, "nt")?;
637    check(bt, b_len, "bt")?;
638    Ok(())
639}
640
641fn validate_inception(icp: &IcpEvent) -> Result<KeyState, ValidationError> {
642    // Validate backer uniqueness
643    validate_backer_uniqueness(&icp.b)?;
644
645    // Threshold satisfiability (kt over k, nt over n, bt over b).
646    validate_thresholds(
647        icp.s.value(),
648        &icp.kt,
649        icp.k.len(),
650        &icp.nt,
651        icp.n.len(),
652        &icp.bt,
653        icp.b.len(),
654    )?;
655
656    // Validate bt consistency: empty backers must have bt == 0
657    let bt_val = icp.bt.simple_value().unwrap_or(0);
658    if icp.b.is_empty() && bt_val != 0 {
659        return Err(ValidationError::InvalidBackerThreshold {
660            bt: bt_val,
661            backer_count: 0,
662        });
663    }
664
665    Ok(KeyState::from_inception(
666        icp.i.clone(),
667        icp.k.clone(),
668        icp.n.clone(),
669        icp.kt.clone(),
670        icp.nt.clone(),
671        icp.d.clone(),
672        icp.b.clone(),
673        icp.bt.clone(),
674        icp.c.clone(),
675    ))
676}
677
678fn verify_sequence(event: &Event, expected: u128) -> Result<(), ValidationError> {
679    let actual = event.sequence().value();
680    if actual != expected {
681        return Err(ValidationError::InvalidSequence { expected, actual });
682    }
683    Ok(())
684}
685
686fn verify_chain_linkage(event: &Event, state: &KeyState) -> Result<(), ValidationError> {
687    let prev_said = event.previous().ok_or(ValidationError::NotInception)?;
688    if *prev_said != state.last_event_said {
689        return Err(ValidationError::BrokenChain {
690            sequence: event.sequence().value(),
691            referenced: prev_said.clone(),
692            actual: state.last_event_said.clone(),
693        });
694    }
695    Ok(())
696}
697
698/// Returns whether the new key list reveals enough prior next-key commitments
699/// to satisfy the typed prior `nt` threshold.
700///
701/// Each prior commitment index `j` counts as "revealed" when some new key
702/// hashes to `next_commitment[j]`; the typed [`Threshold::is_satisfied`] then
703/// decides over those indices. This replaces the legacy
704/// `simple_value().unwrap_or(1)` collapse, which silently reduced any weighted
705/// `nt` to a 1-of-N (F-15).
706fn prior_commitments_satisfy_threshold(
707    next_commitment: &[Said],
708    next_threshold: &Threshold,
709    new_keys: &[CesrKey],
710) -> bool {
711    let revealed: Vec<u32> = next_commitment
712        .iter()
713        .enumerate()
714        .filter_map(|(j, commitment)| {
715            let matched = new_keys.iter().any(|key| {
716                key.parse()
717                    .map(|pk| verify_commitment(&pk, commitment))
718                    .unwrap_or(false)
719            });
720            matched.then_some(j as u32)
721        })
722        .collect();
723    next_threshold.is_satisfied(&revealed, next_commitment.len())
724}
725
726/// Registrar-backer role designated by an event's config traits.
727///
728/// `RB` and `NRB` are mutually exclusive backer semantics; the latter wins when
729/// both appear (per [`ConfigTrait`] supersedence). `Unspecified` means the
730/// event's `c[]` named neither, so the role is inherited rather than changed.
731#[derive(Debug, Clone, Copy, PartialEq, Eq)]
732enum BackerRole {
733    Registrar,
734    NoRegistrar,
735    Unspecified,
736}
737
738/// Resolve the registrar-backer role designated by a config-trait list.
739fn backer_role(traits: &[ConfigTrait]) -> BackerRole {
740    let mut role = BackerRole::Unspecified;
741    for t in traits {
742        match t {
743            ConfigTrait::RegistrarBackers => role = BackerRole::Registrar,
744            ConfigTrait::NoRegistrarBackers => role = BackerRole::NoRegistrar,
745            _ => {}
746        }
747    }
748    role
749}
750
751fn validate_rotation(
752    rot: &RotEvent,
753    sequence: u128,
754    state: &mut KeyState,
755) -> Result<(), ValidationError> {
756    // Threshold satisfiability for the new establishment config. `br`/`ba` are
757    // deltas, so the post-rotation backer count is the prior set minus removals
758    // plus additions.
759    let post_backer_count =
760        state.backers.iter().filter(|b| !rot.br.contains(b)).count() + rot.ba.len();
761    validate_thresholds(
762        sequence,
763        &rot.kt,
764        rot.k.len(),
765        &rot.nt,
766        rot.n.len(),
767        &rot.bt,
768        post_backer_count,
769    )?;
770
771    // Verify all pre-rotation commitments against the typed prior `nt`.
772    if !state.next_commitment.is_empty()
773        && !prior_commitments_satisfy_threshold(
774            &state.next_commitment,
775            &state.next_threshold,
776            &rot.k,
777        )
778    {
779        return Err(ValidationError::CommitmentMismatch { sequence });
780    }
781
782    // Validate backer uniqueness within br and ba.
783    validate_backer_uniqueness(&rot.br)?;
784    validate_backer_uniqueness(&rot.ba)?;
785    // br and ba must not overlap.
786    for aid in &rot.ba {
787        if rot.br.contains(aid) {
788            return Err(ValidationError::DuplicateBacker {
789                aid: aid.as_str().to_string(),
790            });
791        }
792    }
793    // Each `br` (cut) must be a current backer; each `ba` (add) must not already
794    // be a surviving backer. Otherwise apply_rotation's retain+extend would
795    // silently corrupt the backer set and `bt` accounting (F-05).
796    for aid in &rot.br {
797        if !state.backers.contains(aid) {
798            return Err(ValidationError::InvalidBackerDelta {
799                sequence,
800                reason: format!("br entry {} not in prior backers", aid.as_str()),
801            });
802        }
803    }
804    let survivors: Vec<_> = state
805        .backers
806        .iter()
807        .filter(|b| !rot.br.contains(b))
808        .collect();
809    for aid in &rot.ba {
810        if survivors.contains(&aid) {
811            return Err(ValidationError::InvalidBackerDelta {
812                sequence,
813                reason: format!("ba entry {} duplicates a surviving backer", aid.as_str()),
814            });
815        }
816    }
817
818    // Reject a silent RB<->NRB role flip that retains prior backers. A
819    // non-empty `c[]` naming the opposite role must rebuild `b[]` — cut every
820    // prior backer — or a survivor ends up governed by semantics it was never
821    // admitted under (F-23). An empty `c[]` inherits the role, so cannot flip.
822    if !rot.c.is_empty() {
823        let old_role = backer_role(&state.config_traits);
824        let new_role = backer_role(&rot.c);
825        let is_flip = matches!(
826            (old_role, new_role),
827            (BackerRole::Registrar, BackerRole::NoRegistrar)
828                | (BackerRole::NoRegistrar, BackerRole::Registrar)
829        );
830        if is_flip && !survivors.is_empty() {
831            return Err(ValidationError::BackerRoleFlip {
832                sequence,
833                reason: format!(
834                    "{old_role:?}->{new_role:?} but {} prior backer(s) survive; \
835                     a role flip must cut all prior backers",
836                    survivors.len()
837                ),
838            });
839        }
840    }
841
842    state.apply_rotation(
843        rot.k.clone(),
844        rot.n.clone(),
845        rot.kt.clone(),
846        rot.nt.clone(),
847        sequence,
848        rot.d.clone(),
849        &rot.br,
850        &rot.ba,
851        rot.bt.clone(),
852        rot.c.clone(),
853    );
854
855    Ok(())
856}
857
858fn validate_interaction(
859    ixn: &IxnEvent,
860    sequence: u128,
861    state: &mut KeyState,
862) -> Result<(), ValidationError> {
863    // Presence check: ixn events are only valid against a transferable,
864    // non-abandoned identity with an available current key. The value itself
865    // is not used here — signature verification against it happens at the
866    // KEL-ingest boundary.
867    state
868        .current_key()
869        .ok_or(ValidationError::SignatureFailed { sequence })?;
870    state.apply_interaction(sequence, ixn.d.clone());
871    Ok(())
872}
873
874/// Validate a delegated inception event (`dip`) per KERI §11.
875///
876/// Beyond the standard inception checks, the validator requires the
877/// delegator's KEL to contain an `ixn` event whose `a[]` seal references
878/// `dip.d`. Without that seal the delegated identifier is not authorized.
879fn validate_delegated_inception(
880    dip: &crate::events::DipEvent,
881    lookup: Option<&dyn DelegatorKelLookup>,
882) -> Result<KeyState, ValidationError> {
883    let sequence = dip.s.value();
884    let lookup = lookup.ok_or(ValidationError::DelegatorLookupMissing { sequence })?;
885
886    // Bilateral delegation binding: the delegator anchored this dip (delegator
887    // side) AND the dip's -G source seal points back at that exact anchoring
888    // event (delegate side).
889    let anchor = lookup.find_seal(&dip.di, &dip.d).ok_or_else(|| {
890        ValidationError::DelegatorSealNotFound {
891            sequence,
892            delegator_aid: dip.di.as_str().to_string(),
893        }
894    })?;
895    enforce_source_seal(dip.source_seal.as_ref(), &anchor, sequence)?;
896
897    // Structural checks mirrored from `validate_inception` — backers, threshold.
898    validate_backer_uniqueness(&dip.b)?;
899    let bt_val = dip.bt.simple_value().unwrap_or(0);
900    if dip.b.is_empty() && bt_val != 0 {
901        return Err(ValidationError::InvalidBackerThreshold {
902            bt: bt_val,
903            backer_count: 0,
904        });
905    }
906
907    // Build state from the dip event.
908    let is_non_transferable = dip.n.is_empty();
909    Ok(KeyState {
910        prefix: dip.i.clone(),
911        current_keys: dip.k.clone(),
912        next_commitment: dip.n.clone(),
913        sequence: dip.s.value(),
914        last_event_said: dip.d.clone(),
915        is_abandoned: false,
916        threshold: dip.kt.clone(),
917        next_threshold: dip.nt.clone(),
918        backers: dip.b.clone(),
919        backer_threshold: dip.bt.clone(),
920        config_traits: dip.c.clone(),
921        is_non_transferable,
922        delegator: Some(dip.di.clone()),
923        last_establishment_sequence: dip.s.value(),
924    })
925}
926
927/// Validate a delegated rotation event (`drt`) per KERI §11.
928///
929/// Requires the delegator's KEL to contain an `ixn` event anchoring this
930/// rotation via its SAID. Standard rotation rules also apply (chain,
931/// sequence, pre-rotation commitment).
932fn validate_delegated_rotation(
933    drt: &crate::events::DrtEvent,
934    sequence: u128,
935    state: &mut KeyState,
936    lookup: Option<&dyn DelegatorKelLookup>,
937) -> Result<(), ValidationError> {
938    let lookup = lookup.ok_or(ValidationError::DelegatorLookupMissing { sequence })?;
939
940    // Bilateral delegation binding (as for dip): delegator-anchored seal AND the
941    // drt's -G source seal pointing back at that anchoring event.
942    let anchor = lookup.find_seal(&drt.di, &drt.d).ok_or_else(|| {
943        ValidationError::DelegatorSealNotFound {
944            sequence,
945            delegator_aid: drt.di.as_str().to_string(),
946        }
947    })?;
948    enforce_source_seal(drt.source_seal.as_ref(), &anchor, sequence)?;
949
950    // Standard rotation commitment/backer checks applied to drt fields.
951    if !state.next_commitment.is_empty()
952        && !prior_commitments_satisfy_threshold(
953            &state.next_commitment,
954            &state.next_threshold,
955            &drt.k,
956        )
957    {
958        return Err(ValidationError::CommitmentMismatch { sequence });
959    }
960
961    validate_backer_uniqueness(&drt.br)?;
962    validate_backer_uniqueness(&drt.ba)?;
963    for aid in &drt.ba {
964        if drt.br.contains(aid) {
965            return Err(ValidationError::DuplicateBacker {
966                aid: aid.as_str().to_string(),
967            });
968        }
969    }
970
971    // Apply: the rotation advances the KEL state the same way a plain rot would.
972    state.sequence = sequence;
973    state.last_event_said = drt.d.clone();
974    state.current_keys = drt.k.clone();
975    state.next_commitment = drt.n.clone();
976    state.threshold = drt.kt.clone();
977    state.next_threshold = drt.nt.clone();
978    Ok(())
979}
980
981/// Replay a KEL to get the current KeyState.
982///
983/// Alias for [`validate_kel`] — use whichever name fits your context better.
984///
985/// Args:
986/// * `events` - The ordered list of KERI events to replay.
987pub fn replay_kel(events: &[Event]) -> Result<KeyState, ValidationError> {
988    validate_kel(events)
989}
990
991/// Validate the cryptographic integrity of a single event against the current key state.
992///
993/// Args:
994/// * `event` - The event to validate.
995/// * `current_state` - The current `KeyState` (None for inception events).
996pub fn verify_event_crypto(
997    event: &Event,
998    current_state: Option<&KeyState>,
999) -> Result<(), ValidationError> {
1000    match event {
1001        Event::Icp(icp) => {
1002            // Presence check only: icp must commit at least one key.
1003            if icp.k.is_empty() {
1004                return Err(ValidationError::SignatureFailed { sequence: 0 });
1005            }
1006
1007            // Self-addressing AIDs (E-prefixed): `i` MUST equal the SAID `d`.
1008            let is_self_addressing = icp.i.as_str().starts_with('E');
1009            if is_self_addressing {
1010                if icp.i.as_str() != icp.d.as_str() {
1011                    return Err(ValidationError::InvalidSaid {
1012                        expected: icp.d.clone(),
1013                        actual: Said::new_unchecked(icp.i.as_str().to_string()),
1014                    });
1015                }
1016            } else {
1017                // Basic-derivation AIDs (D / 1AAI / 1AAJ ...): the prefix IS the
1018                // single inception key, so `i` MUST equal `k[0]`. Without this a
1019                // basic-derivation prefix could point at an arbitrary key list.
1020                let i_key = KeriPublicKey::parse(icp.i.as_str())
1021                    .map_err(|_| ValidationError::SignatureFailed { sequence: 0 })?;
1022                let k0 = icp.k[0]
1023                    .parse()
1024                    .map_err(|_| ValidationError::SignatureFailed { sequence: 0 })?;
1025                if i_key.as_bytes() != k0.as_bytes() {
1026                    return Err(ValidationError::InvalidSaid {
1027                        expected: Said::new_unchecked(icp.k[0].as_str().to_string()),
1028                        actual: Said::new_unchecked(icp.i.as_str().to_string()),
1029                    });
1030                }
1031            }
1032
1033            Ok(())
1034        }
1035        Event::Rot(rot) => {
1036            let sequence = event.sequence().value();
1037            let state = current_state.ok_or(ValidationError::SignatureFailed { sequence })?;
1038
1039            if state.is_abandoned || state.next_commitment.is_empty() {
1040                return Err(ValidationError::CommitmentMismatch { sequence });
1041            }
1042
1043            if rot.k.is_empty() {
1044                return Err(ValidationError::SignatureFailed { sequence });
1045            }
1046
1047            // Verify pre-rotation commitments against the typed prior `nt`.
1048            if !prior_commitments_satisfy_threshold(
1049                &state.next_commitment,
1050                &state.next_threshold,
1051                &rot.k,
1052            ) {
1053                return Err(ValidationError::CommitmentMismatch { sequence });
1054            }
1055
1056            Ok(())
1057        }
1058        Event::Ixn(_) => {
1059            let sequence = event.sequence().value();
1060            let state = current_state.ok_or(ValidationError::SignatureFailed { sequence })?;
1061
1062            // Presence check: ixn requires a transferable, non-abandoned state
1063            // with an available current key.
1064            state
1065                .current_key()
1066                .ok_or(ValidationError::SignatureFailed { sequence })?;
1067
1068            Ok(())
1069        }
1070        // Delegated events use same crypto verification as their non-delegated counterparts
1071        Event::Dip(dip) => {
1072            if dip.k.is_empty() {
1073                return Err(ValidationError::SignatureFailed { sequence: 0 });
1074            }
1075            Ok(())
1076        }
1077        Event::Drt(drt) => {
1078            let sequence = event.sequence().value();
1079            let state = current_state.ok_or(ValidationError::SignatureFailed { sequence })?;
1080
1081            if state.is_abandoned || state.next_commitment.is_empty() {
1082                return Err(ValidationError::CommitmentMismatch { sequence });
1083            }
1084            if drt.k.is_empty() {
1085                return Err(ValidationError::SignatureFailed { sequence });
1086            }
1087            Ok(())
1088        }
1089    }
1090}
1091
1092/// Verify an event's SAID matches its content hash.
1093///
1094/// Args:
1095/// * `event` - The event to verify.
1096pub fn verify_event_said(event: &Event) -> Result<(), ValidationError> {
1097    let value =
1098        serde_json::to_value(event).map_err(|e| ValidationError::Serialization(e.to_string()))?;
1099    let computed =
1100        compute_said(&value).map_err(|e| ValidationError::Serialization(e.to_string()))?;
1101    let actual = event.said();
1102
1103    if computed != *actual {
1104        return Err(ValidationError::InvalidSaid {
1105            expected: computed,
1106            actual: actual.clone(),
1107        });
1108    }
1109
1110    Ok(())
1111}
1112
1113/// Validate a single event for appending to a KEL with known state.
1114///
1115/// Args:
1116/// * `event` - The event to validate for append.
1117/// * `state` - The current `KeyState` (tip of the existing KEL).
1118pub fn validate_for_append(event: &Event, state: &KeyState) -> Result<(), ValidationError> {
1119    if matches!(event, Event::Icp(_)) {
1120        return Err(ValidationError::MultipleInceptions);
1121    }
1122
1123    verify_event_said(event)?;
1124    verify_sequence(event, state.sequence + 1)?;
1125    verify_chain_linkage(event, state)?;
1126    verify_event_crypto(event, Some(state))?;
1127
1128    Ok(())
1129}
1130
1131/// Compute the SAID for an event.
1132///
1133/// Args:
1134/// * `event` - The event to compute the SAID for.
1135pub fn compute_event_said(event: &Event) -> Result<Said, ValidationError> {
1136    let value =
1137        serde_json::to_value(event).map_err(|e| ValidationError::Serialization(e.to_string()))?;
1138    compute_said(&value).map_err(|e| ValidationError::Serialization(e.to_string()))
1139}
1140
1141/// Serialize a finalized event for signing.
1142///
1143/// KERI signs over the fully-formed event bytes — `d` (SAID) and `i` (prefix)
1144/// already populated by `finalize_*_event`, and the version string declaring
1145/// the true body length. A spec verifier (KERIpy/KERIox) parses `v` first and
1146/// frames the body by that length, so the signed bytes MUST equal the wire
1147/// bytes. (The prior implementation cleared `d`/`i` after finalization, making
1148/// the signed body shorter than `v` claimed — a hard interop break.)
1149///
1150/// Args:
1151/// * `event` - The finalized event to serialize for signing.
1152pub fn serialize_for_signing(event: &Event) -> Result<Vec<u8>, ValidationError> {
1153    serde_json::to_vec(event).map_err(|e| ValidationError::Serialization(e.to_string()))
1154}
1155
1156/// Validate a signed event's crypto (signatures + commitments) against key state.
1157///
1158/// This is the preferred entry point for validating events with externalized signatures.
1159///
1160/// Args:
1161/// * `signed` - The signed event with detached signatures.
1162/// * `current_state` - The current `KeyState` (None for inception events).
1163pub fn validate_signed_event(
1164    signed: &crate::events::SignedEvent,
1165    current_state: Option<&KeyState>,
1166) -> Result<(), ValidationError> {
1167    let event = &signed.event;
1168    let sequence = event.sequence().value();
1169
1170    if signed.signatures.is_empty() {
1171        return Err(ValidationError::SignatureFailed { sequence });
1172    }
1173
1174    // Determine the key list and threshold for verification
1175    let (keys, threshold) = match event {
1176        Event::Icp(icp) => (&icp.k, &icp.kt),
1177        Event::Dip(dip) => (&dip.k, &dip.kt),
1178        Event::Rot(rot) => (&rot.k, &rot.kt),
1179        Event::Drt(drt) => (&drt.k, &drt.kt),
1180        Event::Ixn(_) => {
1181            let state = current_state.ok_or(ValidationError::SignatureFailed { sequence })?;
1182            (&state.current_keys, &state.threshold)
1183        }
1184    };
1185
1186    if keys.is_empty() {
1187        return Err(ValidationError::SignatureFailed { sequence });
1188    }
1189
1190    // Verify each signature and collect verified indices
1191    let canonical = serialize_for_signing(event)?;
1192    let mut verified_indices = Vec::new();
1193
1194    for sig in &signed.signatures {
1195        let idx = sig.index as usize;
1196        if idx >= keys.len() {
1197            continue; // out-of-range index, skip
1198        }
1199        let key = &keys[idx];
1200        if let Ok(pk) = key.parse()
1201            && pk.verify_signature(&canonical, &sig.sig).is_ok()
1202        {
1203            verified_indices.push(sig.index);
1204        }
1205    }
1206
1207    // Check threshold satisfaction (current key threshold)
1208    if !threshold.is_satisfied(&verified_indices, keys.len()) {
1209        return Err(ValidationError::SignatureFailed { sequence });
1210    }
1211
1212    // For rotation events: also check prior next-threshold from the previous
1213    // establishment event. The spec requires signatures satisfy BOTH the current
1214    // signing threshold AND the prior next rotation threshold.
1215    if matches!(event, Event::Rot(_) | Event::Drt(_))
1216        && let Some(state) = current_state
1217    {
1218        let n_len = state.next_commitment.len();
1219
1220        // Bind each verifying signature to the prior commitment it reveals: the
1221        // new key `k[index]` must hash to `n[prior_index]` (or `n[index]` for a
1222        // single-index sig, where keripy emits code `A` with ondex == index). The
1223        // prior `nt` must then be met over the DISTINCT prior-commitment indices.
1224        let mut verified_prior: Vec<u32> = Vec::new();
1225        for sig in &signed.signatures {
1226            let Some(key) = keys.get(sig.index as usize) else {
1227                continue;
1228            };
1229            let Ok(pk) = key.parse() else {
1230                continue;
1231            };
1232            if pk.verify_signature(&canonical, &sig.sig).is_err() {
1233                continue;
1234            }
1235            let j = sig.prior_index.unwrap_or(sig.index) as usize;
1236            let Some(commitment) = state.next_commitment.get(j) else {
1237                continue;
1238            };
1239            if crate::crypto::verify_commitment(&pk, commitment) {
1240                verified_prior.push(j as u32);
1241            }
1242        }
1243
1244        // A cardinality-changing rotation in which NO signature revealed a prior
1245        // commitment is unbindable — surface the diagnostic rather than a generic
1246        // signature failure. (A well-formed removal binds at least one; a single
1247        // signer at prior slot 0 binds via the index == ondex fallback.)
1248        if n_len != keys.len() && verified_prior.is_empty() {
1249            return Err(ValidationError::AsymmetricKeyRotation {
1250                sequence,
1251                prior_next_count: n_len,
1252                new_key_count: keys.len(),
1253            });
1254        }
1255
1256        if !state.next_threshold.is_satisfied(&verified_prior, n_len) {
1257            return Err(ValidationError::SignatureFailed { sequence });
1258        }
1259    }
1260
1261    Ok(())
1262}
1263
1264/// Create an inception event with a properly computed SAID.
1265///
1266/// Args:
1267/// * `icp` - The inception event to finalize.
1268pub fn finalize_icp_event(mut icp: IcpEvent) -> Result<IcpEvent, ValidationError> {
1269    let value = serde_json::to_value(Event::Icp(icp.clone()))
1270        .map_err(|e| ValidationError::Serialization(e.to_string()))?;
1271    let said = compute_said(&value).map_err(|e| ValidationError::Serialization(e.to_string()))?;
1272
1273    icp.d = said.clone();
1274    // Only set i = d for self-addressing AIDs (empty or E-prefixed)
1275    if icp.i.is_empty() || icp.i.as_str().starts_with('E') {
1276        icp.i = Prefix::new_unchecked(said.into_inner());
1277    }
1278
1279    // Set version string with actual byte count
1280    let final_bytes = serde_json::to_vec(&Event::Icp(icp.clone()))
1281        .map_err(|e| ValidationError::Serialization(e.to_string()))?;
1282    icp.v = crate::types::VersionString::json(final_bytes.len() as u32);
1283
1284    Ok(icp)
1285}
1286
1287/// Create a delegated inception (`dip`) event with a properly computed SAID.
1288///
1289/// Mirrors [`finalize_icp_event`] for `dip`: a delegated AID's prefix is
1290/// self-addressing (the SAID of its own inception event), so `i` is set to `d`.
1291///
1292/// Args:
1293/// * `dip` - The delegated inception event to finalize (with `di` set to the delegator).
1294pub fn finalize_dip_event(
1295    mut dip: crate::events::DipEvent,
1296) -> Result<crate::events::DipEvent, ValidationError> {
1297    let value = serde_json::to_value(Event::Dip(dip.clone()))
1298        .map_err(|e| ValidationError::Serialization(e.to_string()))?;
1299    let said = compute_said(&value).map_err(|e| ValidationError::Serialization(e.to_string()))?;
1300
1301    dip.d = said.clone();
1302    // A delegated AID is self-addressing: its prefix is the SAID of the dip.
1303    if dip.i.is_empty() || dip.i.as_str().starts_with('E') {
1304        dip.i = Prefix::new_unchecked(said.into_inner());
1305    }
1306
1307    let final_bytes = serde_json::to_vec(&Event::Dip(dip.clone()))
1308        .map_err(|e| ValidationError::Serialization(e.to_string()))?;
1309    dip.v = crate::types::VersionString::json(final_bytes.len() as u32);
1310
1311    Ok(dip)
1312}
1313
1314/// Create a rotation event with a properly computed SAID.
1315///
1316/// Args:
1317/// * `rot` - The rotation event to finalize.
1318pub fn finalize_rot_event(mut rot: RotEvent) -> Result<RotEvent, ValidationError> {
1319    let value = serde_json::to_value(Event::Rot(rot.clone()))
1320        .map_err(|e| ValidationError::Serialization(e.to_string()))?;
1321    let said = compute_said(&value).map_err(|e| ValidationError::Serialization(e.to_string()))?;
1322    rot.d = said;
1323
1324    let final_bytes = serde_json::to_vec(&Event::Rot(rot.clone()))
1325        .map_err(|e| ValidationError::Serialization(e.to_string()))?;
1326    rot.v = crate::types::VersionString::json(final_bytes.len() as u32);
1327
1328    Ok(rot)
1329}
1330
1331/// Create a delegated rotation (`drt`) event with a properly computed SAID.
1332///
1333/// Mirrors [`finalize_rot_event`]. A `drt` is **not** self-addressing — its `i`
1334/// is the existing delegated AID prefix — so only `d` and `v` are set (`i` is
1335/// left unchanged, unlike `dip`).
1336///
1337/// Args:
1338/// * `drt` - The delegated rotation event to finalize.
1339pub fn finalize_drt_event(
1340    mut drt: crate::events::DrtEvent,
1341) -> Result<crate::events::DrtEvent, ValidationError> {
1342    let value = serde_json::to_value(Event::Drt(drt.clone()))
1343        .map_err(|e| ValidationError::Serialization(e.to_string()))?;
1344    let said = compute_said(&value).map_err(|e| ValidationError::Serialization(e.to_string()))?;
1345    drt.d = said;
1346
1347    let final_bytes = serde_json::to_vec(&Event::Drt(drt.clone()))
1348        .map_err(|e| ValidationError::Serialization(e.to_string()))?;
1349    drt.v = crate::types::VersionString::json(final_bytes.len() as u32);
1350
1351    Ok(drt)
1352}
1353
1354/// Create an interaction event with a properly computed SAID.
1355///
1356/// Args:
1357/// * `ixn` - The interaction event to finalize.
1358pub fn finalize_ixn_event(mut ixn: IxnEvent) -> Result<IxnEvent, ValidationError> {
1359    let value = serde_json::to_value(Event::Ixn(ixn.clone()))
1360        .map_err(|e| ValidationError::Serialization(e.to_string()))?;
1361    let said = compute_said(&value).map_err(|e| ValidationError::Serialization(e.to_string()))?;
1362    ixn.d = said;
1363
1364    let final_bytes = serde_json::to_vec(&Event::Ixn(ixn.clone()))
1365        .map_err(|e| ValidationError::Serialization(e.to_string()))?;
1366    ixn.v = crate::types::VersionString::json(final_bytes.len() as u32);
1367
1368    Ok(ixn)
1369}
1370
1371/// Search for a seal with the given digest in any IXN event in the KEL.
1372///
1373/// Returns the sequence number of the IXN event if found.
1374///
1375/// Args:
1376/// * `events` - The event log to search.
1377/// * `digest` - The SAID digest to search for.
1378pub fn find_seal_in_kel(events: &[Event], digest: &str) -> Option<u128> {
1379    for event in events {
1380        if let Event::Ixn(ixn) = event {
1381            for seal in &ixn.a {
1382                if seal.digest_value().is_some_and(|d| d.as_str() == digest) {
1383                    return Some(ixn.s.value());
1384                }
1385            }
1386        }
1387    }
1388    None
1389}
1390
1391/// Parse a KEL from a JSON string.
1392///
1393/// Args:
1394/// * `json` - JSON string containing a list of KERI events.
1395pub fn parse_kel_json(json: &str) -> Result<Vec<Event>, ValidationError> {
1396    serde_json::from_str(json).map_err(|e| ValidationError::Serialization(e.to_string()))
1397}
1398
1399#[cfg(test)]
1400#[allow(clippy::unwrap_used, clippy::expect_used)]
1401mod tests {
1402    use super::*;
1403    use crate::events::{IndexedSignature, KeriSequence, Seal, SignedEvent};
1404    use crate::types::{CesrKey, Threshold, VersionString};
1405    use ring::rand::SystemRandom;
1406    use ring::signature::{Ed25519KeyPair, KeyPair};
1407
1408    fn gen_keypair() -> Ed25519KeyPair {
1409        let rng = SystemRandom::new();
1410        let pkcs8 = Ed25519KeyPair::generate_pkcs8(&rng).unwrap();
1411        Ed25519KeyPair::from_pkcs8(pkcs8.as_ref()).unwrap()
1412    }
1413
1414    fn encode_pubkey(kp: &Ed25519KeyPair) -> String {
1415        crate::cesr_encode::encode_verkey(kp.public_key().as_ref(), cesride::matter::Codex::Ed25519)
1416            .unwrap()
1417    }
1418
1419    fn make_raw_icp(key: &str, next: &str) -> IcpEvent {
1420        IcpEvent {
1421            v: VersionString::placeholder(),
1422            d: Said::default(),
1423            i: Prefix::default(),
1424            s: KeriSequence::new(0),
1425            kt: Threshold::Simple(1),
1426            k: vec![CesrKey::new_unchecked(key.to_string())],
1427            nt: Threshold::Simple(1),
1428            n: vec![Said::new_unchecked(next.to_string())],
1429            bt: Threshold::Simple(0),
1430            b: vec![],
1431            c: vec![],
1432            a: vec![],
1433        }
1434    }
1435
1436    fn make_signed_icp() -> (IcpEvent, Ed25519KeyPair) {
1437        let rng = SystemRandom::new();
1438        let pkcs8 = Ed25519KeyPair::generate_pkcs8(&rng).unwrap();
1439        let keypair = Ed25519KeyPair::from_pkcs8(pkcs8.as_ref()).unwrap();
1440        let key_encoded = encode_pubkey(&keypair);
1441
1442        let icp = IcpEvent {
1443            v: VersionString::placeholder(),
1444            d: Said::default(),
1445            i: Prefix::default(),
1446            s: KeriSequence::new(0),
1447            kt: Threshold::Simple(1),
1448            k: vec![CesrKey::new_unchecked(key_encoded)],
1449            nt: Threshold::Simple(1),
1450            n: vec![Said::new_unchecked("ENextCommitment".to_string())],
1451            bt: Threshold::Simple(0),
1452            b: vec![],
1453            c: vec![],
1454            a: vec![],
1455        };
1456
1457        let finalized = finalize_icp_event(icp).unwrap();
1458        (finalized, keypair)
1459    }
1460
1461    fn make_signed_ixn(
1462        prefix: &Prefix,
1463        prev_said: &Said,
1464        seq: u128,
1465        _keypair: &Ed25519KeyPair,
1466    ) -> IxnEvent {
1467        let mut ixn = IxnEvent {
1468            v: VersionString::placeholder(),
1469            d: Said::default(),
1470            i: prefix.clone(),
1471            s: KeriSequence::new(seq),
1472            p: prev_said.clone(),
1473            a: vec![Seal::digest("EAttest")],
1474        };
1475
1476        let value = serde_json::to_value(Event::Ixn(ixn.clone())).unwrap();
1477        ixn.d = compute_said(&value).unwrap();
1478
1479        ixn
1480    }
1481
1482    #[test]
1483    fn finalize_icp_sets_said() {
1484        let icp = make_raw_icp("DKey1", "ENext1");
1485        let finalized = finalize_icp_event(icp).unwrap();
1486
1487        assert!(!finalized.d.is_empty());
1488        assert_eq!(finalized.d.as_str(), finalized.i.as_str());
1489        assert!(finalized.d.as_str().starts_with('E'));
1490    }
1491
1492    #[test]
1493    fn validates_single_inception() {
1494        let (icp, _keypair) = make_signed_icp();
1495        let events = vec![Event::Icp(icp.clone())];
1496
1497        let state = validate_kel(&events).unwrap();
1498        assert_eq!(state.prefix, icp.i);
1499        assert_eq!(state.sequence, 0);
1500    }
1501
1502    #[test]
1503    fn rejects_empty_kel() {
1504        let result = validate_kel(&[]);
1505        assert!(matches!(result, Err(ValidationError::EmptyKel)));
1506    }
1507
1508    #[test]
1509    fn rejects_non_inception_first() {
1510        let mut ixn = IxnEvent {
1511            v: VersionString::placeholder(),
1512            d: Said::default(),
1513            i: Prefix::new_unchecked("ETest".to_string()),
1514            s: KeriSequence::new(0),
1515            p: Said::new_unchecked("EPrev".to_string()),
1516            a: vec![],
1517        };
1518        // Compute a valid SAID so verify_event_said passes — the test
1519        // should fail on NotInception, not on SaidMismatch.
1520        let event = Event::Ixn(ixn.clone());
1521        if let Ok(said) = compute_event_said(&event) {
1522            ixn.d = said;
1523        }
1524        let events = vec![Event::Ixn(ixn)];
1525        let result = validate_kel(&events);
1526        assert!(matches!(result, Err(ValidationError::NotInception)));
1527    }
1528
1529    #[test]
1530    fn rejects_broken_sequence() {
1531        let (icp, _keypair) = make_signed_icp();
1532
1533        let mut ixn = IxnEvent {
1534            v: VersionString::placeholder(),
1535            d: Said::default(),
1536            i: icp.i.clone(),
1537            s: KeriSequence::new(5),
1538            p: icp.d.clone(),
1539            a: vec![],
1540        };
1541
1542        let value = serde_json::to_value(Event::Ixn(ixn.clone())).unwrap();
1543        ixn.d = compute_said(&value).unwrap();
1544
1545        let events = vec![Event::Icp(icp), Event::Ixn(ixn)];
1546        let result = validate_kel(&events);
1547        assert!(matches!(
1548            result,
1549            Err(ValidationError::InvalidSequence {
1550                expected: 1,
1551                actual: 5
1552            })
1553        ));
1554    }
1555
1556    #[test]
1557    fn rejects_broken_chain() {
1558        let (icp, _keypair) = make_signed_icp();
1559
1560        let mut ixn = IxnEvent {
1561            v: VersionString::placeholder(),
1562            d: Said::default(),
1563            i: icp.i.clone(),
1564            s: KeriSequence::new(1),
1565            p: Said::new_unchecked("EWrongPrevious".to_string()),
1566            a: vec![],
1567        };
1568
1569        let value = serde_json::to_value(Event::Ixn(ixn.clone())).unwrap();
1570        ixn.d = compute_said(&value).unwrap();
1571
1572        let events = vec![Event::Icp(icp), Event::Ixn(ixn)];
1573        let result = validate_kel(&events);
1574        assert!(matches!(result, Err(ValidationError::BrokenChain { .. })));
1575    }
1576
1577    #[test]
1578    fn rejects_invalid_said() {
1579        let icp = make_raw_icp("DKey1", "ENext1");
1580        let finalized = finalize_icp_event(icp).unwrap();
1581
1582        let mut tampered = finalized.clone();
1583        tampered.d = Said::new_unchecked("EWrongSaid".to_string());
1584
1585        let events = vec![Event::Icp(tampered)];
1586        let result = validate_kel(&events);
1587        assert!(matches!(result, Err(ValidationError::InvalidSaid { .. })));
1588    }
1589
1590    #[test]
1591    fn validates_icp_then_ixn() {
1592        let (icp, keypair) = make_signed_icp();
1593        let ixn = make_signed_ixn(&icp.i, &icp.d, 1, &keypair);
1594
1595        let events = vec![Event::Icp(icp), Event::Ixn(ixn.clone())];
1596        let state = validate_kel(&events).unwrap();
1597        assert_eq!(state.sequence, 1);
1598        assert_eq!(state.last_event_said, ixn.d);
1599    }
1600
1601    #[test]
1602    fn compute_event_said_works() {
1603        let icp = make_raw_icp("DKey1", "ENext1");
1604        let event = Event::Icp(icp);
1605        let said = compute_event_said(&event).unwrap();
1606        assert!(said.as_str().starts_with('E'));
1607        assert!(!said.is_empty());
1608    }
1609
1610    // Sanity control: a correctly-signed SignedEvent must be accepted. Without
1611    // this, a regression that makes `validate_signed_event` always return
1612    // `SignatureFailed` would silently "pass" the rejection tests below.
1613    #[test]
1614    fn accepts_correct_signature() {
1615        let (icp, keypair) = make_signed_icp();
1616        let event = Event::Icp(icp);
1617        let canonical = serialize_for_signing(&event).unwrap();
1618        let sig = keypair.sign(&canonical).as_ref().to_vec();
1619        let signed = SignedEvent::new(
1620            event,
1621            vec![IndexedSignature {
1622                index: 0,
1623                prior_index: None,
1624                sig,
1625            }],
1626        );
1627
1628        validate_signed_event(&signed, None).expect("correct signature must validate");
1629    }
1630
1631    // Intent: a SignedEvent whose attached signature bytes do not match the
1632    // canonical event body must be rejected. Uses the externalized-signature
1633    // entry point (`validate_signed_event`); `validate_kel` only checks KEL
1634    // structure and does not consume attached signatures, so it cannot be
1635    // used to test signature-level rejection.
1636    #[test]
1637    fn rejects_forged_signature() {
1638        let (icp, _keypair) = make_signed_icp();
1639        let event = Event::Icp(icp);
1640        let forged_sig = vec![0u8; 64]; // valid length, invalid content
1641        let signed = SignedEvent::new(
1642            event,
1643            vec![IndexedSignature {
1644                index: 0,
1645                prior_index: None,
1646                sig: forged_sig,
1647            }],
1648        );
1649
1650        assert!(matches!(
1651            validate_signed_event(&signed, None),
1652            Err(ValidationError::SignatureFailed { sequence: 0 })
1653        ));
1654    }
1655
1656    // `rejects_missing_signature` was tied to the legacy in-body `x` field.
1657    // Signatures are externalized now; the equivalent check is covered by
1658    // `validate_signed_event` tests in `multi_key_threshold.rs`.
1659
1660    // Intent: a SignedEvent signed by a keypair other than the one committed
1661    // in `icp.k` must be rejected. The wrong-key signature is structurally
1662    // valid (correct length, correct type) but fails Ed25519 verification
1663    // against the committed public key.
1664    #[test]
1665    fn rejects_wrong_key_signature() {
1666        let committed = gen_keypair();
1667        let key_encoded = encode_pubkey(&committed);
1668
1669        let icp = IcpEvent {
1670            v: VersionString::placeholder(),
1671            d: Said::default(),
1672            i: Prefix::default(),
1673            s: KeriSequence::new(0),
1674            kt: Threshold::Simple(1),
1675            k: vec![CesrKey::new_unchecked(key_encoded)],
1676            nt: Threshold::Simple(1),
1677            n: vec![Said::new_unchecked("ENextCommitment".to_string())],
1678            bt: Threshold::Simple(0),
1679            b: vec![],
1680            c: vec![],
1681            a: vec![],
1682        };
1683        let icp = finalize_icp_event(icp).unwrap();
1684        let event = Event::Icp(icp);
1685
1686        let wrong = gen_keypair();
1687        let canonical = serialize_for_signing(&event).unwrap();
1688        let wrong_sig = wrong.sign(&canonical).as_ref().to_vec();
1689        let signed = SignedEvent::new(
1690            event,
1691            vec![IndexedSignature {
1692                index: 0,
1693                prior_index: None,
1694                sig: wrong_sig,
1695            }],
1696        );
1697
1698        assert!(matches!(
1699            validate_signed_event(&signed, None),
1700            Err(ValidationError::SignatureFailed { sequence: 0 })
1701        ));
1702    }
1703
1704    #[test]
1705    fn crypto_accepts_valid_inception() {
1706        let (icp, _keypair) = make_signed_icp();
1707        let result = verify_event_crypto(&Event::Icp(icp), None);
1708        assert!(result.is_ok());
1709    }
1710
1711    #[test]
1712    fn find_seal_in_kel_finds_digest() {
1713        let (icp, keypair) = make_signed_icp();
1714        let ixn = make_signed_ixn(&icp.i, &icp.d, 1, &keypair);
1715        let events = vec![Event::Icp(icp), Event::Ixn(ixn)];
1716        assert_eq!(find_seal_in_kel(&events, "EAttest"), Some(1));
1717        assert_eq!(find_seal_in_kel(&events, "ENonExistent"), None);
1718    }
1719
1720    #[test]
1721    fn parse_kel_json_rejects_invalid_hex_sequence() {
1722        let json = r#"[{"v":"KERI10JSON","t":"icp","i":"E123","s":"not_hex","kt":"1","k":["DKey"],"nt":"1","n":["ENext"],"bt":"0","b":[]}]"#;
1723        let result = parse_kel_json(json);
1724        assert!(result.is_err(), "expected error for invalid hex sequence");
1725    }
1726
1727    /// Build a signed ICP with caller-supplied overrides applied after keypair
1728    /// generation but before finalization and signing.
1729    fn make_custom_signed_icp(customize: impl FnOnce(&mut IcpEvent)) -> (IcpEvent, Ed25519KeyPair) {
1730        let rng = SystemRandom::new();
1731        let pkcs8 = Ed25519KeyPair::generate_pkcs8(&rng).unwrap();
1732        let keypair = Ed25519KeyPair::from_pkcs8(pkcs8.as_ref()).unwrap();
1733        let key_encoded = encode_pubkey(&keypair);
1734
1735        let mut icp = IcpEvent {
1736            v: VersionString::placeholder(),
1737            d: Said::default(),
1738            i: Prefix::default(),
1739            s: KeriSequence::new(0),
1740            kt: Threshold::Simple(1),
1741            k: vec![CesrKey::new_unchecked(key_encoded)],
1742            nt: Threshold::Simple(1),
1743            n: vec![Said::new_unchecked("ENextCommitment".to_string())],
1744            bt: Threshold::Simple(0),
1745            b: vec![],
1746            c: vec![],
1747            a: vec![],
1748        };
1749
1750        customize(&mut icp);
1751
1752        let finalized = finalize_icp_event(icp).unwrap();
1753        (finalized, keypair)
1754    }
1755
1756    #[test]
1757    fn rejects_events_after_abandonment() {
1758        // Abandonment = rotation with empty n (not inception — that's NonTransferable).
1759        let kp2 = gen_keypair();
1760
1761        // Use make_custom_signed_icp with pre-committed key for kp2
1762        let commitment2 = crate::crypto::compute_next_commitment(
1763            &crate::keys::KeriPublicKey::ed25519(kp2.public_key().as_ref()).unwrap(),
1764        );
1765        let (icp, _kp1) = make_custom_signed_icp(|icp| {
1766            icp.n = vec![commitment2.clone()];
1767        });
1768        let prefix = icp.i.clone();
1769
1770        // Rotation that abandons (empty n)
1771        let mut rot = RotEvent {
1772            v: VersionString::placeholder(),
1773            d: Said::default(),
1774            i: prefix.clone(),
1775            s: KeriSequence::new(1),
1776            p: icp.d.clone(),
1777            kt: Threshold::Simple(1),
1778            k: vec![CesrKey::new_unchecked(encode_pubkey(&kp2))],
1779            nt: Threshold::Simple(0),
1780            n: vec![],
1781            bt: Threshold::Simple(0),
1782            br: vec![],
1783            ba: vec![],
1784            c: vec![],
1785            a: vec![],
1786        };
1787        let val = serde_json::to_value(Event::Rot(rot.clone())).unwrap();
1788        rot.d = compute_said(&val).unwrap();
1789
1790        let ixn = make_signed_ixn(&prefix, &rot.d, 2, &kp2);
1791        let events = vec![Event::Icp(icp), Event::Rot(rot), Event::Ixn(ixn)];
1792        let result = validate_kel(&events);
1793        assert!(
1794            matches!(result, Err(ValidationError::AbandonedIdentity { .. })),
1795            "expected AbandonedIdentity, got: {result:?}"
1796        );
1797    }
1798
1799    #[test]
1800    fn rejects_ixn_in_establishment_only_kel() {
1801        let (icp, keypair) = make_custom_signed_icp(|icp| {
1802            icp.c = vec![ConfigTrait::EstablishmentOnly];
1803        });
1804        let ixn = make_signed_ixn(&icp.i, &icp.d, 1, &keypair);
1805        let events = vec![Event::Icp(icp), Event::Ixn(ixn)];
1806        let result = validate_kel(&events);
1807        assert!(
1808            matches!(result, Err(ValidationError::EstablishmentOnly { .. })),
1809            "expected EstablishmentOnly, got: {result:?}"
1810        );
1811    }
1812
1813    #[test]
1814    fn rejects_events_after_non_transferable_inception() {
1815        let (icp, keypair) = make_custom_signed_icp(|icp| {
1816            icp.n = vec![];
1817            icp.nt = Threshold::Simple(0);
1818        });
1819        let ixn = make_signed_ixn(&icp.i, &icp.d, 1, &keypair);
1820        let events = vec![Event::Icp(icp), Event::Ixn(ixn)];
1821        let result = validate_kel(&events);
1822        assert!(
1823            matches!(
1824                result,
1825                Err(ValidationError::NonTransferable)
1826                    | Err(ValidationError::AbandonedIdentity { .. })
1827            ),
1828            "expected NonTransferable or AbandonedIdentity, got: {result:?}"
1829        );
1830    }
1831
1832    #[test]
1833    fn rejects_duplicate_backers() {
1834        let (_, result) = {
1835            let rng = SystemRandom::new();
1836            let pkcs8 = Ed25519KeyPair::generate_pkcs8(&rng).unwrap();
1837            let keypair = Ed25519KeyPair::from_pkcs8(pkcs8.as_ref()).unwrap();
1838            let key_encoded = encode_pubkey(&keypair);
1839
1840            let dup_backer = Prefix::new_unchecked("DWit1".to_string());
1841            let icp = IcpEvent {
1842                v: VersionString::placeholder(),
1843                d: Said::default(),
1844                i: Prefix::default(),
1845                s: KeriSequence::new(0),
1846                kt: Threshold::Simple(1),
1847                k: vec![CesrKey::new_unchecked(key_encoded)],
1848                nt: Threshold::Simple(1),
1849                n: vec![Said::new_unchecked("ENextCommitment".to_string())],
1850                bt: Threshold::Simple(2),
1851                b: vec![dup_backer.clone(), dup_backer],
1852                c: vec![],
1853                a: vec![],
1854            };
1855
1856            let finalized = finalize_icp_event(icp).unwrap();
1857            let events = vec![Event::Icp(finalized)];
1858            (keypair, validate_kel(&events))
1859        };
1860        assert!(
1861            matches!(result, Err(ValidationError::DuplicateBacker { .. })),
1862            "expected DuplicateBacker, got: {result:?}"
1863        );
1864    }
1865
1866    #[test]
1867    fn rejects_invalid_backer_threshold() {
1868        let (_, result) = {
1869            let rng = SystemRandom::new();
1870            let pkcs8 = Ed25519KeyPair::generate_pkcs8(&rng).unwrap();
1871            let keypair = Ed25519KeyPair::from_pkcs8(pkcs8.as_ref()).unwrap();
1872            let key_encoded = encode_pubkey(&keypair);
1873
1874            let icp = IcpEvent {
1875                v: VersionString::placeholder(),
1876                d: Said::default(),
1877                i: Prefix::default(),
1878                s: KeriSequence::new(0),
1879                kt: Threshold::Simple(1),
1880                k: vec![CesrKey::new_unchecked(key_encoded)],
1881                nt: Threshold::Simple(1),
1882                n: vec![Said::new_unchecked("ENextCommitment".to_string())],
1883                bt: Threshold::Simple(2),
1884                b: vec![],
1885                c: vec![],
1886                a: vec![],
1887            };
1888
1889            let finalized = finalize_icp_event(icp).unwrap();
1890            let events = vec![Event::Icp(finalized)];
1891            (keypair, validate_kel(&events))
1892        };
1893        // `bt=2` over zero backers is now caught by the stricter structural
1894        // threshold-satisfiability guard (A.4) before the legacy
1895        // empty-backers/bt!=0 check.
1896        assert!(
1897            matches!(result, Err(ValidationError::ThresholdNotSatisfiable { .. })),
1898            "expected ThresholdNotSatisfiable, got: {result:?}"
1899        );
1900    }
1901
1902    #[test]
1903    fn sign_over_finalized_bytes_roundtrips() {
1904        // A.2: the bytes handed to the signer must equal the wire bytes, whose
1905        // length the version string `v` declares. (Previously d/i were cleared
1906        // after finalize, making the signed body shorter than `v` claimed.)
1907        let (icp, _kp) = make_signed_icp();
1908        let bytes = serialize_for_signing(&Event::Icp(icp.clone())).unwrap();
1909        assert_eq!(
1910            bytes.len() as u32,
1911            icp.v.size,
1912            "signed byte length must equal the version-string size field"
1913        );
1914        let reparsed: Event = serde_json::from_slice(&bytes).unwrap();
1915        assert!(reparsed.is_inception());
1916    }
1917
1918    #[test]
1919    fn threshold_rejects_kt_gt_k() {
1920        // A.4: a signing threshold larger than the key-list length is
1921        // structurally unsatisfiable and must be rejected at validation.
1922        let kp = gen_keypair();
1923        let key = encode_pubkey(&kp);
1924        let icp = IcpEvent {
1925            v: VersionString::placeholder(),
1926            d: Said::default(),
1927            i: Prefix::default(),
1928            s: KeriSequence::new(0),
1929            kt: Threshold::Simple(5),
1930            k: vec![CesrKey::new_unchecked(key)],
1931            nt: Threshold::Simple(1),
1932            n: vec![Said::new_unchecked("ENextCommitment".to_string())],
1933            bt: Threshold::Simple(0),
1934            b: vec![],
1935            c: vec![],
1936            a: vec![],
1937        };
1938        let finalized = finalize_icp_event(icp).unwrap();
1939        let result = validate_kel(&[Event::Icp(finalized)]);
1940        assert!(
1941            matches!(result, Err(ValidationError::ThresholdNotSatisfiable { .. })),
1942            "expected ThresholdNotSatisfiable, got: {result:?}"
1943        );
1944    }
1945
1946    #[test]
1947    fn rotation_rejects_br_not_in_prior() {
1948        // A.10 (F-05): a rotation that cuts a backer not in the prior set, or
1949        // adds a backer that already survives, must be rejected before
1950        // apply_rotation corrupts the backer set.
1951        let state = KeyState::from_inception(
1952            Prefix::new_unchecked("EPrefix".to_string()),
1953            vec![CesrKey::new_unchecked("DKey1".to_string())],
1954            vec![], // empty next_commitment -> commitment check skipped
1955            Threshold::Simple(1),
1956            Threshold::Simple(0),
1957            Said::new_unchecked("ESAID".to_string()),
1958            vec![Prefix::new_unchecked("BWit1".to_string())],
1959            Threshold::Simple(0),
1960            vec![],
1961        );
1962
1963        let make_rot = |br: Vec<Prefix>, ba: Vec<Prefix>| RotEvent {
1964            v: VersionString::placeholder(),
1965            d: Said::default(),
1966            i: Prefix::new_unchecked("EPrefix".to_string()),
1967            s: KeriSequence::new(1),
1968            p: Said::new_unchecked("ESAID".to_string()),
1969            kt: Threshold::Simple(1),
1970            k: vec![CesrKey::new_unchecked("DKey2".to_string())],
1971            nt: Threshold::Simple(0),
1972            n: vec![],
1973            bt: Threshold::Simple(0),
1974            br,
1975            ba,
1976            c: vec![],
1977            a: vec![],
1978        };
1979
1980        // br entry not in prior backers -> rejected.
1981        let bad_cut = make_rot(vec![Prefix::new_unchecked("BWitX".to_string())], vec![]);
1982        assert!(matches!(
1983            validate_rotation(&bad_cut, 1, &mut state.clone()),
1984            Err(ValidationError::InvalidBackerDelta { .. })
1985        ));
1986
1987        // ba entry duplicating a surviving backer -> rejected.
1988        let bad_add = make_rot(vec![], vec![Prefix::new_unchecked("BWit1".to_string())]);
1989        assert!(matches!(
1990            validate_rotation(&bad_add, 1, &mut state.clone()),
1991            Err(ValidationError::InvalidBackerDelta { .. })
1992        ));
1993
1994        // valid delta (cut the existing backer) -> ok.
1995        let ok = make_rot(vec![Prefix::new_unchecked("BWit1".to_string())], vec![]);
1996        assert!(validate_rotation(&ok, 1, &mut state.clone()).is_ok());
1997    }
1998
1999    #[test]
2000    fn rotation_rejects_silent_backer_role_flip() {
2001        // A.13 (F-23): flipping RB<->NRB while a prior backer survives is
2002        // rejected; the same flip is allowed once every prior backer is cut
2003        // (b[] rebuilt). An empty c[] inherits the role and never flips.
2004        let nrb_state = || {
2005            KeyState::from_inception(
2006                Prefix::new_unchecked("EPrefix".to_string()),
2007                vec![CesrKey::new_unchecked("DKey1".to_string())],
2008                vec![],
2009                Threshold::Simple(1),
2010                Threshold::Simple(0),
2011                Said::new_unchecked("ESAID".to_string()),
2012                vec![Prefix::new_unchecked("BWit1".to_string())],
2013                Threshold::Simple(0),
2014                vec![ConfigTrait::NoRegistrarBackers],
2015            )
2016        };
2017
2018        let make_rot = |br: Vec<Prefix>, ba: Vec<Prefix>, c: Vec<ConfigTrait>| RotEvent {
2019            v: VersionString::placeholder(),
2020            d: Said::default(),
2021            i: Prefix::new_unchecked("EPrefix".to_string()),
2022            s: KeriSequence::new(1),
2023            p: Said::new_unchecked("ESAID".to_string()),
2024            kt: Threshold::Simple(1),
2025            k: vec![CesrKey::new_unchecked("DKey2".to_string())],
2026            nt: Threshold::Simple(0),
2027            n: vec![],
2028            bt: Threshold::Simple(0),
2029            br,
2030            ba,
2031            c,
2032            a: vec![],
2033        };
2034
2035        // Flip NRB->RB while BWit1 survives -> rejected.
2036        let flip_keep = make_rot(vec![], vec![], vec![ConfigTrait::RegistrarBackers]);
2037        assert!(matches!(
2038            validate_rotation(&flip_keep, 1, &mut nrb_state()),
2039            Err(ValidationError::BackerRoleFlip { .. })
2040        ));
2041
2042        // Flip NRB->RB after cutting every prior backer -> ok (b[] rebuilt).
2043        let flip_rebuild = make_rot(
2044            vec![Prefix::new_unchecked("BWit1".to_string())],
2045            vec![],
2046            vec![ConfigTrait::RegistrarBackers],
2047        );
2048        assert!(validate_rotation(&flip_rebuild, 1, &mut nrb_state()).is_ok());
2049
2050        // Same role kept (NRB->NRB) with the backer surviving -> ok (no flip).
2051        let same_role = make_rot(vec![], vec![], vec![ConfigTrait::NoRegistrarBackers]);
2052        assert!(validate_rotation(&same_role, 1, &mut nrb_state()).is_ok());
2053
2054        // Empty c[] inherits the role -> ok even though the backer survives.
2055        let inherit = make_rot(vec![], vec![], vec![]);
2056        assert!(validate_rotation(&inherit, 1, &mut nrb_state()).is_ok());
2057    }
2058
2059    // ── D.6: receipt-gated replay ────────────────────────────────────────────
2060
2061    use crate::witness::WitnessReceipt;
2062
2063    /// Said-keyed witness-receipt source for replay-gate tests.
2064    struct MapReceipts {
2065        by_said: std::collections::HashMap<String, Vec<WitnessReceipt>>,
2066    }
2067
2068    impl WitnessReceiptLookup for MapReceipts {
2069        fn receipts_for(
2070            &self,
2071            _controller: &Prefix,
2072            _sn: KeriSequence,
2073            said: &Said,
2074        ) -> Vec<WitnessReceipt> {
2075            self.by_said.get(said.as_str()).cloned().unwrap_or_default()
2076        }
2077    }
2078
2079    fn witness_aid(aid: &str) -> Prefix {
2080        Prefix::new_unchecked(aid.to_string())
2081    }
2082
2083    fn receipt_from(aid: &str) -> WitnessReceipt {
2084        WitnessReceipt {
2085            witness: witness_aid(aid),
2086            signature: vec![],
2087        }
2088    }
2089
2090    fn receipts_under(said: &Said, aids: &[&str]) -> MapReceipts {
2091        let mut by_said = std::collections::HashMap::new();
2092        by_said.insert(
2093            said.as_str().to_string(),
2094            aids.iter().map(|a| receipt_from(a)).collect(),
2095        );
2096        MapReceipts { by_said }
2097    }
2098
2099    /// A finalized inception designating `aids` as backers with threshold `bt`.
2100    fn icp_with_backers(aids: &[&str], bt: u64) -> IcpEvent {
2101        let backers: Vec<Prefix> = aids.iter().map(|a| witness_aid(a)).collect();
2102        let (icp, _kp) = make_custom_signed_icp(|icp| {
2103            icp.b = backers.clone();
2104            icp.bt = Threshold::Simple(bt);
2105        });
2106        icp
2107    }
2108
2109    #[test]
2110    fn replay_bt_zero_accepts_without_receipts() {
2111        let (icp, _kp) = make_signed_icp(); // bt=0, b=[]
2112        let events = vec![Event::Icp(icp)];
2113        let lookup = MapReceipts {
2114            by_said: std::collections::HashMap::new(),
2115        };
2116        let outcome = validate_kel_with_receipts(&events, None, &lookup).unwrap();
2117        assert!(matches!(outcome, WitnessedReplay::Accepted(_)));
2118    }
2119
2120    #[test]
2121    fn replay_at_quorum_accepts() {
2122        let icp = icp_with_backers(&["BWit1", "BWit2"], 2);
2123        let said = icp.d.clone();
2124        let lookup = receipts_under(&said, &["BWit1", "BWit2"]);
2125        let events = vec![Event::Icp(icp)];
2126        let outcome = validate_kel_with_receipts(&events, None, &lookup).unwrap();
2127        assert!(matches!(outcome, WitnessedReplay::Accepted(_)));
2128    }
2129
2130    #[test]
2131    fn replay_under_quorum_is_pending() {
2132        let icp = icp_with_backers(&["BWit1", "BWit2"], 2);
2133        let said = icp.d.clone();
2134        let lookup = receipts_under(&said, &["BWit1"]); // only 1 of 2 required
2135        let events = vec![Event::Icp(icp)];
2136        match validate_kel_with_receipts(&events, None, &lookup).unwrap() {
2137            WitnessedReplay::Pending {
2138                sequence,
2139                collected,
2140                ..
2141            } => {
2142                assert_eq!(sequence, 0);
2143                assert_eq!(collected, 1);
2144            }
2145            WitnessedReplay::Accepted(_) => panic!("expected Pending under quorum"),
2146        }
2147    }
2148
2149    #[test]
2150    fn replay_ignores_duplicate_witness_receipts() {
2151        let icp = icp_with_backers(&["BWit1", "BWit2", "BWit3"], 2);
2152        let said = icp.d.clone();
2153        let lookup = receipts_under(&said, &["BWit1", "BWit1"]); // same witness twice
2154        let events = vec![Event::Icp(icp)];
2155        assert!(matches!(
2156            validate_kel_with_receipts(&events, None, &lookup).unwrap(),
2157            WitnessedReplay::Pending { .. }
2158        ));
2159    }
2160
2161    #[test]
2162    fn replay_ignores_receipt_for_wrong_said() {
2163        let icp = icp_with_backers(&["BWit1", "BWit2"], 2);
2164        // Receipts stored under a different event SAID must never satisfy this event.
2165        let wrong = Said::new_unchecked("EWrongEventSaid".to_string());
2166        let lookup = receipts_under(&wrong, &["BWit1", "BWit2"]);
2167        let events = vec![Event::Icp(icp)];
2168        match validate_kel_with_receipts(&events, None, &lookup).unwrap() {
2169            WitnessedReplay::Pending { collected, .. } => assert_eq!(collected, 0),
2170            WitnessedReplay::Accepted(_) => panic!("wrong-SAID receipts must not count"),
2171        }
2172    }
2173
2174    #[test]
2175    fn replay_uses_witness_set_in_force_at_seq() {
2176        // icp designates {BWit1} bt=1; rot at seq 1 cuts BWit1, adds BWit2, bt=1.
2177        // The seq-1 gate must use the post-rotation set {BWit2}.
2178        let kp2 = gen_keypair();
2179        let kp3 = gen_keypair();
2180        let commitment2 = crate::crypto::compute_next_commitment(
2181            &crate::keys::KeriPublicKey::ed25519(kp2.public_key().as_ref()).unwrap(),
2182        );
2183        let commitment3 = crate::crypto::compute_next_commitment(
2184            &crate::keys::KeriPublicKey::ed25519(kp3.public_key().as_ref()).unwrap(),
2185        );
2186        let (icp, _kp1) = make_custom_signed_icp(|icp| {
2187            icp.b = vec![witness_aid("BWit1")];
2188            icp.bt = Threshold::Simple(1);
2189            icp.n = vec![commitment2.clone()];
2190        });
2191        let prefix = icp.i.clone();
2192        let icp_said = icp.d.clone();
2193
2194        let mut rot = RotEvent {
2195            v: VersionString::placeholder(),
2196            d: Said::default(),
2197            i: prefix.clone(),
2198            s: KeriSequence::new(1),
2199            p: icp_said.clone(),
2200            kt: Threshold::Simple(1),
2201            k: vec![CesrKey::new_unchecked(encode_pubkey(&kp2))],
2202            nt: Threshold::Simple(1),
2203            n: vec![commitment3.clone()],
2204            bt: Threshold::Simple(1),
2205            br: vec![witness_aid("BWit1")],
2206            ba: vec![witness_aid("BWit2")],
2207            c: vec![],
2208            a: vec![],
2209        };
2210        let val = serde_json::to_value(Event::Rot(rot.clone())).unwrap();
2211        rot.d = compute_said(&val).unwrap();
2212        let rot_said = rot.d.clone();
2213
2214        let mut by_said = std::collections::HashMap::new();
2215        by_said.insert(icp_said.as_str().to_string(), vec![receipt_from("BWit1")]);
2216        by_said.insert(rot_said.as_str().to_string(), vec![receipt_from("BWit2")]);
2217        let lookup = MapReceipts { by_said };
2218
2219        let events = vec![Event::Icp(icp), Event::Rot(rot)];
2220        // BWit2 is only in the post-rotation set; acceptance proves the in-force
2221        // set (not the stale {BWit1}) gated the rotation.
2222        assert!(matches!(
2223            validate_kel_with_receipts(&events, None, &lookup).unwrap(),
2224            WitnessedReplay::Accepted(_)
2225        ));
2226    }
2227
2228    #[test]
2229    fn validate_kel_advances_without_receipt_gate() {
2230        // Back-compat: plain validate_kel ignores receipts and advances a bt>0 KEL.
2231        let icp = icp_with_backers(&["BWit1", "BWit2"], 2);
2232        let events = vec![Event::Icp(icp)];
2233        assert!(validate_kel(&events).is_ok());
2234    }
2235}
2236
2237// =============================================================================
2238// Time-aware policy validation — rotation cooldown, clock-skew, emergency
2239// override. `validate_kel` stays pure / clock-free (structural invariants
2240// only); callers who want time-aware checks reach for
2241// `validate_kel_with_policy`.
2242// =============================================================================
2243
2244/// Configurable policy for time-aware KEL validation. Defaults match
2245/// the plan text: 24h minimum rotation interval, 60s clock-skew
2246/// tolerance, no emergency-override identifier.
2247#[derive(Debug, Clone)]
2248pub struct KelPolicy {
2249    /// Minimum wall-clock interval between two consecutive rotation
2250    /// events. Default: 24 hours.
2251    pub min_rotation_interval: chrono::Duration,
2252    /// Maximum allowed skew between an event's `dt` and the wall
2253    /// clock used for validation. Default: 60 seconds.
2254    pub clock_skew_tolerance: chrono::Duration,
2255    /// AID that is permitted to skip the rotation-cooldown check
2256    /// (e.g. the controller's emergency-rotation key). `None` means
2257    /// no override is configured and every rotation must respect
2258    /// the cooldown.
2259    pub emergency_override_did: Option<crate::types::Prefix>,
2260}
2261
2262impl Default for KelPolicy {
2263    fn default() -> Self {
2264        Self {
2265            min_rotation_interval: chrono::Duration::hours(24),
2266            clock_skew_tolerance: chrono::Duration::seconds(60),
2267            emergency_override_did: None,
2268        }
2269    }
2270}
2271
2272/// Validate a KEL against a time-aware [`KelPolicy`].
2273///
2274/// Runs the structural [`validate_kel`] first; on success, layers on
2275/// three additional checks that depend on the `dt` field added to
2276/// establishment and interaction events:
2277///
2278/// 1. Every event MUST carry a `dt`. Pre-`dt`-migration events
2279///    (where `dt` is `None`) fail with
2280///    [`ValidationError::MissingTimestamp`].
2281/// 2. `dt` MUST be monotonically non-decreasing across consecutive
2282///    events. Backward-moving timestamps are evidence of tampering.
2283/// 3. Consecutive rotation events MUST be at least
2284///    [`KelPolicy::min_rotation_interval`] apart (unless the event's
2285///    controller matches [`KelPolicy::emergency_override_did`]).
2286/// 4. Every `dt` must be within
2287///    [`KelPolicy::clock_skew_tolerance`] of `now`.
2288///
2289/// Args:
2290/// * `events`: The ordered KEL.
2291/// * `policy`: [`KelPolicy`] governing the time checks.
2292/// * `now`: The daemon's wall clock at validation time. Inject via
2293///   [`chrono::Utc::now`] at the presentation boundary; domain layers
2294///   pass a clock.
2295pub fn validate_kel_with_policy(
2296    events: &[Event],
2297    timestamps: &[Option<chrono::DateTime<chrono::Utc>>],
2298    policy: &KelPolicy,
2299    now: chrono::DateTime<chrono::Utc>,
2300) -> Result<KeyState, ValidationError> {
2301    let state = validate_kel(events)?;
2302
2303    let mut last_rotation_dt: Option<chrono::DateTime<chrono::Utc>> = None;
2304    let mut last_any_dt: Option<chrono::DateTime<chrono::Utc>> = None;
2305
2306    for (idx, evt) in events.iter().enumerate() {
2307        let seq = idx as u128;
2308        let (is_rotation, controller) = match evt {
2309            Event::Icp(e) => (false, &e.i),
2310            Event::Rot(e) => (true, &e.i),
2311            Event::Ixn(e) => (false, &e.i),
2312            Event::Dip(e) => (false, &e.i),
2313            Event::Drt(e) => (true, &e.i),
2314        };
2315        let Some(dt) = timestamps.get(idx).copied().flatten() else {
2316            return Err(ValidationError::MissingTimestamp { sequence: seq });
2317        };
2318        // Monotonicity.
2319        if let Some(prev) = last_any_dt
2320            && dt < prev
2321        {
2322            return Err(ValidationError::NonMonotonicTimestamp {
2323                sequence: seq,
2324                prev: prev.to_rfc3339(),
2325                curr: dt.to_rfc3339(),
2326            });
2327        }
2328        // Clock skew.
2329        let skew = (dt - now).num_seconds();
2330        if skew.abs() > policy.clock_skew_tolerance.num_seconds() {
2331            return Err(ValidationError::ClockSkew {
2332                sequence: seq,
2333                skew_secs: skew,
2334                tolerance_secs: policy.clock_skew_tolerance.num_seconds(),
2335            });
2336        }
2337        // Cooldown on rotations.
2338        if is_rotation && let Some(prev) = last_rotation_dt {
2339            let interval = dt - prev;
2340            let is_override = policy
2341                .emergency_override_did
2342                .as_ref()
2343                .is_some_and(|ov| ov == controller);
2344            if !is_override && interval < policy.min_rotation_interval {
2345                return Err(ValidationError::RotationCooldown {
2346                    sequence: seq,
2347                    interval_secs: interval.num_seconds(),
2348                    min_secs: policy.min_rotation_interval.num_seconds(),
2349                });
2350            }
2351        }
2352        last_any_dt = Some(dt);
2353        if is_rotation {
2354            last_rotation_dt = Some(dt);
2355        }
2356    }
2357
2358    Ok(state)
2359}
2360
2361#[cfg(test)]
2362mod policy_tests {
2363    use super::*;
2364    use chrono::{Duration as ChronoDuration, TimeZone, Utc};
2365
2366    fn base_now() -> chrono::DateTime<chrono::Utc> {
2367        Utc.with_ymd_and_hms(2026, 1, 1, 0, 0, 0).unwrap()
2368    }
2369
2370    #[test]
2371    fn policy_rejects_missing_dt_via_empty_kel_path() {
2372        // Structural validation fires first; empty KEL is rejected
2373        // before any policy check runs. Locks in that the policy
2374        // validator doesn't accidentally accept an empty KEL.
2375        let events: Vec<crate::events::Event> = vec![];
2376        let r = validate_kel_with_policy(&events, &[], &KelPolicy::default(), base_now());
2377        assert!(matches!(r, Err(ValidationError::EmptyKel)));
2378    }
2379
2380    #[test]
2381    fn policy_default_values_match_plan() {
2382        let p = KelPolicy::default();
2383        assert_eq!(p.min_rotation_interval, ChronoDuration::hours(24));
2384        assert_eq!(p.clock_skew_tolerance, ChronoDuration::seconds(60));
2385        assert!(p.emergency_override_did.is_none());
2386    }
2387}