Skip to main content

auths_id/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//!
7//! ## Core Entrypoints (Pure Functions)
8//!
9//! The following functions are **pure** with no side effects:
10//!
11//! - [`validate_kel`] / [`replay_kel`]: Replays a KEL to compute the current `KeyState`
12//!
13//! **What "pure" means for these functions:**
14//! - **Deterministic**: Same inputs always produce same outputs
15//! - **No side effects**: No filesystem, network, or global state access
16//! - **No storage assumptions**: Takes `&[Event]` slice, not registry references
17//! - **Errors are values**: Returns `Result`, never panics on invalid input
18//!
19//! This enables property-based testing and makes the core logic independent of
20//! storage backends.
21
22use auths_core::crypto::said::{compute_said, verify_commitment};
23use auths_crypto::KeriPublicKey;
24use base64::{Engine, engine::general_purpose::URL_SAFE_NO_PAD};
25use ring::signature::UnparsedPublicKey;
26
27use super::types::{Prefix, Said};
28use super::{Event, IcpEvent, IxnEvent, KeyState, RotEvent};
29
30/// Errors specific to KEL validation.
31///
32/// These errors represent **protocol invariant violations**. They indicate
33/// structural corruption or attack, not recoverable conditions.
34///
35/// # Invariants Enforced
36///
37/// - **Append-only KEL**: Sequence numbers must be monotonically increasing
38/// - **Self-addressing**: Each event's SAID must match its content hash
39/// - **Chain integrity**: Each event must reference the previous event's SAID
40#[derive(Debug, Clone, thiserror::Error, PartialEq, Eq)]
41#[non_exhaustive]
42pub enum ValidationError {
43    /// SAID (Self-Addressing Identifier) doesn't match content hash.
44    ///
45    /// This is a **protocol invariant violation**. The event's `d` field
46    /// must equal the Blake3 hash of its canonical serialization.
47    #[error("Invalid SAID: expected {expected}, got {actual}")]
48    InvalidSaid { expected: Said, actual: Said },
49
50    /// Event references wrong previous event.
51    ///
52    /// This is a **chain integrity violation**. Each event's `p` field
53    /// must equal the SAID of the immediately preceding event.
54    #[error("Broken chain: event {sequence} references {referenced}, but previous was {actual}")]
55    BrokenChain {
56        sequence: u64,
57        referenced: Said,
58        actual: Said,
59    },
60
61    /// Sequence number is not monotonically increasing.
62    ///
63    /// This is an **append-only invariant violation**. Sequence numbers
64    /// must be 0, 1, 2, ... with no gaps or duplicates.
65    #[error("Invalid sequence: expected {expected}, got {actual}")]
66    InvalidSequence { expected: u64, actual: u64 },
67
68    #[error("Pre-rotation commitment mismatch at sequence {sequence}")]
69    CommitmentMismatch { sequence: u64 },
70
71    #[error("Signature verification failed at sequence {sequence}")]
72    SignatureFailed { sequence: u64 },
73
74    #[error("First event must be inception")]
75    NotInception,
76
77    #[error("Empty KEL")]
78    EmptyKel,
79
80    #[error("Multiple inception events in KEL")]
81    MultipleInceptions,
82
83    #[error("Serialization error: {0}")]
84    Serialization(String),
85
86    #[error("Malformed sequence number: {raw:?}")]
87    MalformedSequence { raw: String },
88}
89
90impl auths_core::error::AuthsErrorInfo for ValidationError {
91    fn error_code(&self) -> &'static str {
92        match self {
93            Self::InvalidSaid { .. } => "AUTHS-E4501",
94            Self::BrokenChain { .. } => "AUTHS-E4502",
95            Self::InvalidSequence { .. } => "AUTHS-E4503",
96            Self::CommitmentMismatch { .. } => "AUTHS-E4504",
97            Self::SignatureFailed { .. } => "AUTHS-E4505",
98            Self::NotInception => "AUTHS-E4506",
99            Self::EmptyKel => "AUTHS-E4507",
100            Self::MultipleInceptions => "AUTHS-E4508",
101            Self::Serialization(_) => "AUTHS-E4509",
102            Self::MalformedSequence { .. } => "AUTHS-E4510",
103        }
104    }
105
106    fn suggestion(&self) -> Option<&'static str> {
107        match self {
108            Self::InvalidSaid { .. } => {
109                Some("The KEL may have been tampered with; re-sync from a trusted source")
110            }
111            Self::BrokenChain { .. } => {
112                Some("The KEL chain is broken; re-sync from a trusted source")
113            }
114            Self::InvalidSequence { .. } => {
115                Some("The KEL has sequence gaps; re-sync from a trusted source")
116            }
117            Self::CommitmentMismatch { .. } => {
118                Some("The rotation key does not match the pre-rotation commitment")
119            }
120            Self::SignatureFailed { .. } => {
121                Some("The event signature is invalid; the KEL may be corrupted")
122            }
123            Self::NotInception => Some("The first event in a KEL must be an inception event"),
124            Self::EmptyKel => Some("No events found; initialize the identity first"),
125            Self::MultipleInceptions => Some("A KEL must contain exactly one inception event"),
126            Self::Serialization(_) => None,
127            Self::MalformedSequence { .. } => None,
128        }
129    }
130}
131
132/// Validate a KEL and return the resulting KeyState.
133///
134/// This is a **pure function** and serves as the core entrypoint for
135/// KEL replay. It is equivalent to `apply_event_chain` in the domain model.
136///
137/// # Pure Function Guarantees
138///
139/// - **Deterministic**: Same event sequence always produces same `KeyState`
140/// - **No I/O**: No filesystem, network, or global state access
141/// - **No storage assumptions**: Takes `&[Event]` slice directly
142/// - **Errors are values**: Returns `Result`, never panics on invalid input
143///
144/// # Validation Performed
145///
146/// - SAID verification for each event
147/// - Chain linkage (each event's `p` matches previous event's `d`)
148/// - Sequence ordering (strict increment from 0)
149/// - Pre-rotation commitment verification for rotation events
150/// - Signature verification using declared keys
151///
152/// # Example
153///
154/// ```rust,ignore
155/// use auths_id::keri::{validate_kel, Event};
156///
157/// let events: Vec<Event> = load_events_from_storage(...);
158/// let key_state = validate_kel(&events)?;
159/// // key_state now reflects the current identity state
160/// ```
161pub fn validate_kel(events: &[Event]) -> Result<KeyState, ValidationError> {
162    if events.is_empty() {
163        return Err(ValidationError::EmptyKel);
164    }
165
166    let Event::Icp(icp) = &events[0] else {
167        return Err(ValidationError::NotInception);
168    };
169
170    verify_event_said(&events[0])?;
171    let mut state = validate_inception(icp)?;
172
173    for (idx, event) in events.iter().enumerate().skip(1) {
174        let expected_seq = idx as u64;
175        verify_event_said(event)?;
176        verify_sequence(event, expected_seq)?;
177        verify_chain_linkage(event, &state)?;
178
179        match event {
180            Event::Rot(rot) => validate_rotation(rot, event, expected_seq, &mut state)?,
181            Event::Ixn(ixn) => validate_interaction(ixn, event, expected_seq, &mut state)?,
182            Event::Icp(_) => return Err(ValidationError::MultipleInceptions),
183        }
184    }
185
186    Ok(state)
187}
188
189fn parse_threshold(raw: &str) -> Result<u64, ValidationError> {
190    raw.parse::<u64>()
191        .map_err(|_| ValidationError::MalformedSequence {
192            raw: raw.to_string(),
193        })
194}
195
196fn validate_inception(icp: &IcpEvent) -> Result<KeyState, ValidationError> {
197    verify_event_signature(
198        &Event::Icp(icp.clone()),
199        icp.k
200            .first()
201            .ok_or(ValidationError::SignatureFailed { sequence: 0 })?,
202    )?;
203
204    let threshold = parse_threshold(&icp.kt)?;
205    let next_threshold = parse_threshold(&icp.nt)?;
206
207    Ok(KeyState::from_inception(
208        icp.i.clone(),
209        icp.k.clone(),
210        icp.n.clone(),
211        threshold,
212        next_threshold,
213        icp.d.clone(),
214    ))
215}
216
217fn verify_sequence(event: &Event, expected: u64) -> Result<(), ValidationError> {
218    let actual = event.sequence().value();
219    if actual != expected {
220        return Err(ValidationError::InvalidSequence { expected, actual });
221    }
222    Ok(())
223}
224
225fn verify_chain_linkage(event: &Event, state: &KeyState) -> Result<(), ValidationError> {
226    let prev_said = event.previous().ok_or(ValidationError::NotInception)?;
227    if *prev_said != state.last_event_said {
228        return Err(ValidationError::BrokenChain {
229            sequence: event.sequence().value(),
230            referenced: prev_said.clone(),
231            actual: state.last_event_said.clone(),
232        });
233    }
234    Ok(())
235}
236
237fn validate_rotation(
238    rot: &RotEvent,
239    event: &Event,
240    sequence: u64,
241    state: &mut KeyState,
242) -> Result<(), ValidationError> {
243    if !rot.k.is_empty() {
244        verify_event_signature(event, &rot.k[0])?;
245    }
246
247    if !state.next_commitment.is_empty() && !rot.k.is_empty() {
248        let key_bytes = KeriPublicKey::parse(&rot.k[0])
249            .map(|k| k.as_bytes().to_vec())
250            .map_err(|_| ValidationError::CommitmentMismatch { sequence })?;
251
252        if !verify_commitment(&key_bytes, &state.next_commitment[0]) {
253            return Err(ValidationError::CommitmentMismatch { sequence });
254        }
255    }
256
257    let threshold = parse_threshold(&rot.kt)?;
258    let next_threshold = parse_threshold(&rot.nt)?;
259
260    state.apply_rotation(
261        rot.k.clone(),
262        rot.n.clone(),
263        threshold,
264        next_threshold,
265        sequence,
266        rot.d.clone(),
267    );
268
269    Ok(())
270}
271
272fn validate_interaction(
273    ixn: &IxnEvent,
274    event: &Event,
275    sequence: u64,
276    state: &mut KeyState,
277) -> Result<(), ValidationError> {
278    let current_key = state
279        .current_key()
280        .ok_or(ValidationError::SignatureFailed { sequence })?;
281    verify_event_signature(event, current_key)?;
282    state.apply_interaction(sequence, ixn.d.clone());
283    Ok(())
284}
285
286/// Replay a KEL to get the current KeyState.
287///
288/// This is an alias for [`validate_kel`] and shares all its pure function guarantees.
289/// Use whichever name is more semantically appropriate for your context:
290/// - `validate_kel` when emphasis is on validation
291/// - `replay_kel` when emphasis is on state derivation
292pub fn replay_kel(events: &[Event]) -> Result<KeyState, ValidationError> {
293    validate_kel(events)
294}
295
296/// Validate the cryptographic integrity of a single event against the current key state.
297///
298/// This is an O(1) operation — it verifies only the incoming event's signature
299/// and pre-rotation commitment against the cached tip state, avoiding full KEL replay.
300///
301/// For inception events, `current_state` should be `None`. The function verifies
302/// the self-signing property (signature by declared key `k[0]`) and that `i == d`.
303///
304/// # Pure Function Guarantees
305///
306/// - **Deterministic**: Same inputs always produce same result
307/// - **No I/O**: No filesystem, network, or global state access
308/// - **O(1)**: Constant-time relative to KEL length
309pub fn verify_event_crypto(
310    event: &Event,
311    current_state: Option<&KeyState>,
312) -> Result<(), ValidationError> {
313    match event {
314        Event::Icp(icp) => {
315            // Inception: verify self-signed with declared key k[0]
316            let key = icp
317                .k
318                .first()
319                .ok_or(ValidationError::SignatureFailed { sequence: 0 })?;
320            verify_event_signature(event, key)?;
321
322            // Verify self-certifying identifier: i == d
323            if icp.i.as_str() != icp.d.as_str() {
324                return Err(ValidationError::InvalidSaid {
325                    expected: icp.d.clone(),
326                    actual: Said::new_unchecked(icp.i.as_str().to_string()),
327                });
328            }
329
330            Ok(())
331        }
332        Event::Rot(rot) => {
333            let sequence = event.sequence().value();
334            let state = current_state.ok_or(ValidationError::SignatureFailed { sequence })?;
335
336            // Reject rotation on abandoned identity (empty next commitment)
337            if state.is_abandoned || state.next_commitment.is_empty() {
338                return Err(ValidationError::CommitmentMismatch { sequence });
339            }
340
341            // Rotation is signed by the NEW key
342            if rot.k.is_empty() {
343                return Err(ValidationError::SignatureFailed { sequence });
344            }
345            verify_event_signature(event, &rot.k[0])?;
346
347            // Verify pre-rotation commitment: blake3(new_key) == current_state.next_commitment
348            let key_str = &rot.k[0];
349            let key_bytes = KeriPublicKey::parse(key_str)
350                .map(|k| k.as_bytes().to_vec())
351                .map_err(|_| ValidationError::CommitmentMismatch { sequence })?;
352
353            if !verify_commitment(&key_bytes, &state.next_commitment[0]) {
354                return Err(ValidationError::CommitmentMismatch { sequence });
355            }
356
357            Ok(())
358        }
359        Event::Ixn(_) => {
360            let sequence = event.sequence().value();
361            let state = current_state.ok_or(ValidationError::SignatureFailed { sequence })?;
362
363            // Interaction: signed by current key from cached state
364            let current_key = state
365                .current_key()
366                .ok_or(ValidationError::SignatureFailed { sequence })?;
367            verify_event_signature(event, current_key)?;
368
369            Ok(())
370        }
371    }
372}
373
374/// Verify an event's SAID matches its content hash.
375///
376/// The SAID is computed by hashing the event JSON with the `d` field cleared.
377pub fn verify_event_said(event: &Event) -> Result<(), ValidationError> {
378    // Serialize event without the 'd' field for hashing
379    let json = serialize_for_said(event)?;
380    let computed = compute_said(&json);
381    let actual = event.said();
382
383    if computed != actual.as_str() {
384        return Err(ValidationError::InvalidSaid {
385            expected: computed,
386            actual: actual.clone(),
387        });
388    }
389
390    Ok(())
391}
392
393/// Validate a single event for appending to a KEL with known state.
394///
395/// Checks all invariants that `validate_kel` checks per-event:
396/// SAID integrity, sequence continuity, chain linkage, and cryptographic
397/// validity (signature + pre-rotation commitment).
398///
399/// Args:
400/// * `event` - The event to validate for append.
401/// * `state` - The current KeyState (tip of the existing KEL).
402pub fn validate_for_append(event: &Event, state: &KeyState) -> Result<(), ValidationError> {
403    if matches!(event, Event::Icp(_)) {
404        return Err(ValidationError::MultipleInceptions);
405    }
406
407    verify_event_said(event)?;
408    verify_sequence(event, state.sequence + 1)?;
409    verify_chain_linkage(event, state)?;
410    verify_event_crypto(event, Some(state))?;
411
412    Ok(())
413}
414
415/// Compute the SAID for an event.
416///
417/// This serializes the event with an empty `d` field and computes the Blake3 hash.
418pub fn compute_event_said(event: &Event) -> Result<Said, ValidationError> {
419    let json = serialize_for_said(event)?;
420    Ok(compute_said(&json))
421}
422
423/// Serialize an event for SAID computation (with empty `d` and `x` fields).
424/// For inception events, also clears `i` since it's set to the SAID.
425/// The `x` field is cleared because SAID is computed before signature.
426fn serialize_for_said(event: &Event) -> Result<Vec<u8>, ValidationError> {
427    match event {
428        Event::Icp(e) => {
429            let mut e = e.clone();
430            e.d = Said::default();
431            e.i = Prefix::default(); // For inception, `i` equals `d`
432            e.x = String::new(); // SAID computed before signature
433            serde_json::to_vec(&Event::Icp(e))
434        }
435        Event::Rot(e) => {
436            let mut e = e.clone();
437            e.d = Said::default();
438            e.x = String::new(); // SAID computed before signature
439            serde_json::to_vec(&Event::Rot(e))
440        }
441        Event::Ixn(e) => {
442            let mut e = e.clone();
443            e.d = Said::default();
444            e.x = String::new(); // SAID computed before signature
445            serde_json::to_vec(&Event::Ixn(e))
446        }
447    }
448    .map_err(|e| ValidationError::Serialization(e.to_string()))
449}
450
451/// Serialize event for signing (clears d, i for icp, and x fields).
452///
453/// This produces the canonical form over which signatures are computed.
454/// Both SAID and signature are computed over this form to avoid circular dependencies.
455pub fn serialize_for_signing(event: &Event) -> Result<Vec<u8>, ValidationError> {
456    match event {
457        Event::Icp(e) => {
458            let mut e = e.clone();
459            e.d = Said::default();
460            e.i = Prefix::default(); // For inception, `i` equals `d`
461            e.x = String::new();
462            serde_json::to_vec(&Event::Icp(e))
463        }
464        Event::Rot(e) => {
465            let mut e = e.clone();
466            e.d = Said::default();
467            e.x = String::new();
468            serde_json::to_vec(&Event::Rot(e))
469        }
470        Event::Ixn(e) => {
471            let mut e = e.clone();
472            e.d = Said::default();
473            e.x = String::new();
474            serde_json::to_vec(&Event::Ixn(e))
475        }
476    }
477    .map_err(|e| ValidationError::Serialization(e.to_string()))
478}
479
480/// Verify an event's signature using the specified key.
481fn verify_event_signature(event: &Event, signing_key: &str) -> Result<(), ValidationError> {
482    let sequence = event.sequence().value();
483
484    // Decode the signature
485    let sig_str = event.signature();
486    if sig_str.is_empty() {
487        return Err(ValidationError::SignatureFailed { sequence });
488    }
489    let sig_bytes = URL_SAFE_NO_PAD
490        .decode(sig_str)
491        .map_err(|_| ValidationError::SignatureFailed { sequence })?;
492
493    // Decode the signing key
494    let key_bytes = KeriPublicKey::parse(signing_key)
495        .map_err(|_| ValidationError::SignatureFailed { sequence })?;
496
497    // Serialize the event for verification
498    let canonical = serialize_for_signing(event)?;
499
500    // Verify the signature
501    let pk = UnparsedPublicKey::new(&ring::signature::ED25519, key_bytes.as_bytes());
502    pk.verify(&canonical, &sig_bytes)
503        .map_err(|_| ValidationError::SignatureFailed { sequence })?;
504
505    Ok(())
506}
507
508/// Create an inception event with a properly computed SAID.
509pub fn finalize_icp_event(mut icp: IcpEvent) -> Result<IcpEvent, ValidationError> {
510    // Clear SAID for hashing
511    icp.d = Said::default();
512    icp.i = Prefix::default();
513
514    // Compute SAID
515    let json = serde_json::to_vec(&Event::Icp(icp.clone()))
516        .map_err(|e| ValidationError::Serialization(e.to_string()))?;
517    let said = compute_said(&json);
518
519    // Set SAID and prefix (same for inception)
520    icp.d = said.clone();
521    icp.i = Prefix::new_unchecked(said.into_inner());
522
523    Ok(icp)
524}
525
526#[cfg(test)]
527mod tests {
528    use super::*;
529    use crate::keri::{IxnEvent, KERI_VERSION, KeriSequence, Prefix, RotEvent, Said, Seal};
530    use base64::Engine;
531    use base64::engine::general_purpose::URL_SAFE_NO_PAD;
532    use ring::rand::SystemRandom;
533    use ring::signature::{Ed25519KeyPair, KeyPair};
534
535    fn make_raw_icp(key: &str, next: &str) -> IcpEvent {
536        IcpEvent {
537            v: KERI_VERSION.to_string(),
538            d: Said::default(),
539            i: Prefix::default(),
540            s: KeriSequence::new(0),
541            kt: "1".to_string(),
542            k: vec![key.to_string()],
543            nt: "1".to_string(),
544            n: vec![next.to_string()],
545            bt: "0".to_string(),
546            b: vec![],
547            a: vec![],
548            x: String::new(),
549        }
550    }
551
552    /// Create a signed ICP event for testing
553    fn make_signed_icp() -> (IcpEvent, Ed25519KeyPair) {
554        let rng = SystemRandom::new();
555        let pkcs8 = Ed25519KeyPair::generate_pkcs8(&rng).unwrap();
556        let keypair = Ed25519KeyPair::from_pkcs8(pkcs8.as_ref()).unwrap();
557        let key_encoded = format!("D{}", URL_SAFE_NO_PAD.encode(keypair.public_key().as_ref()));
558
559        let icp = IcpEvent {
560            v: KERI_VERSION.to_string(),
561            d: Said::default(),
562            i: Prefix::default(),
563            s: KeriSequence::new(0),
564            kt: "1".to_string(),
565            k: vec![key_encoded],
566            nt: "1".to_string(),
567            n: vec!["ENextCommitment".to_string()],
568            bt: "0".to_string(),
569            b: vec![],
570            a: vec![],
571            x: String::new(),
572        };
573
574        // Finalize SAID
575        let mut finalized = finalize_icp_event(icp).unwrap();
576
577        // Sign
578        let canonical = serialize_for_signing(&Event::Icp(finalized.clone())).unwrap();
579        let sig = keypair.sign(&canonical);
580        finalized.x = URL_SAFE_NO_PAD.encode(sig.as_ref());
581
582        (finalized, keypair)
583    }
584
585    /// Create a signed IXN event for testing
586    fn make_signed_ixn(
587        prefix: &Prefix,
588        prev_said: &Said,
589        seq: u64,
590        keypair: &Ed25519KeyPair,
591    ) -> IxnEvent {
592        let mut ixn = IxnEvent {
593            v: KERI_VERSION.to_string(),
594            d: Said::default(),
595            i: prefix.clone(),
596            s: KeriSequence::new(seq),
597            p: prev_said.clone(),
598            a: vec![Seal::device_attestation("EAttest")],
599            x: String::new(),
600        };
601
602        // Compute SAID
603        let json = serde_json::to_vec(&Event::Ixn(ixn.clone())).unwrap();
604        ixn.d = compute_said(&json);
605
606        // Sign
607        let canonical = serialize_for_signing(&Event::Ixn(ixn.clone())).unwrap();
608        let sig = keypair.sign(&canonical);
609        ixn.x = URL_SAFE_NO_PAD.encode(sig.as_ref());
610
611        ixn
612    }
613
614    #[test]
615    fn finalize_icp_sets_said() {
616        let icp = make_raw_icp("DKey1", "ENext1");
617        let finalized = finalize_icp_event(icp).unwrap();
618
619        // SAID should be set and match prefix
620        assert!(!finalized.d.is_empty());
621        assert_eq!(finalized.d.as_str(), finalized.i.as_str());
622        assert!(finalized.d.as_str().starts_with('E'));
623    }
624
625    #[test]
626    fn validates_single_inception() {
627        let (icp, _keypair) = make_signed_icp();
628        let events = vec![Event::Icp(icp.clone())];
629
630        let state = validate_kel(&events).unwrap();
631        assert_eq!(state.prefix, icp.i);
632        assert_eq!(state.sequence, 0);
633    }
634
635    #[test]
636    fn rejects_empty_kel() {
637        let result = validate_kel(&[]);
638        assert!(matches!(result, Err(ValidationError::EmptyKel)));
639    }
640
641    #[test]
642    fn rejects_non_inception_first() {
643        let ixn = IxnEvent {
644            v: KERI_VERSION.to_string(),
645            d: Said::new_unchecked("ETest".to_string()),
646            i: Prefix::new_unchecked("ETest".to_string()),
647            s: KeriSequence::new(0),
648            p: Said::new_unchecked("EPrev".to_string()),
649            a: vec![],
650            x: String::new(),
651        };
652        let events = vec![Event::Ixn(ixn)];
653        let result = validate_kel(&events);
654        assert!(matches!(result, Err(ValidationError::NotInception)));
655    }
656
657    #[test]
658    fn rejects_broken_sequence() {
659        let (icp, keypair) = make_signed_icp();
660
661        // Create IXN with wrong sequence (but still properly signed)
662        let mut ixn = IxnEvent {
663            v: KERI_VERSION.to_string(),
664            d: Said::default(),
665            i: icp.i.clone(),
666            s: KeriSequence::new(5), // Wrong! Should be 1
667            p: icp.d.clone(),
668            a: vec![],
669            x: String::new(),
670        };
671
672        // Compute SAID for IXN
673        let json = serde_json::to_vec(&Event::Ixn(ixn.clone())).unwrap();
674        ixn.d = compute_said(&json);
675
676        // Sign with current key
677        let canonical = serialize_for_signing(&Event::Ixn(ixn.clone())).unwrap();
678        let sig = keypair.sign(&canonical);
679        ixn.x = URL_SAFE_NO_PAD.encode(sig.as_ref());
680
681        let events = vec![Event::Icp(icp), Event::Ixn(ixn)];
682        let result = validate_kel(&events);
683        assert!(matches!(
684            result,
685            Err(ValidationError::InvalidSequence {
686                expected: 1,
687                actual: 5
688            })
689        ));
690    }
691
692    #[test]
693    fn rejects_broken_chain() {
694        let (icp, keypair) = make_signed_icp();
695
696        // Create IXN with wrong previous SAID (but still properly signed)
697        let mut ixn = IxnEvent {
698            v: KERI_VERSION.to_string(),
699            d: Said::default(),
700            i: icp.i.clone(),
701            s: KeriSequence::new(1),
702            p: Said::new_unchecked("EWrongPrevious".to_string()),
703            a: vec![],
704            x: String::new(),
705        };
706
707        // Compute SAID for IXN
708        let json = serde_json::to_vec(&Event::Ixn(ixn.clone())).unwrap();
709        ixn.d = compute_said(&json);
710
711        // Sign with current key
712        let canonical = serialize_for_signing(&Event::Ixn(ixn.clone())).unwrap();
713        let sig = keypair.sign(&canonical);
714        ixn.x = URL_SAFE_NO_PAD.encode(sig.as_ref());
715
716        let events = vec![Event::Icp(icp), Event::Ixn(ixn)];
717        let result = validate_kel(&events);
718        assert!(matches!(result, Err(ValidationError::BrokenChain { .. })));
719    }
720
721    #[test]
722    fn rejects_invalid_said() {
723        let icp = make_raw_icp("DKey1", "ENext1");
724        let finalized = finalize_icp_event(icp.clone()).unwrap();
725
726        // Tamper with the SAID
727        let mut tampered = finalized.clone();
728        tampered.d = Said::new_unchecked("EWrongSaid".to_string());
729
730        let events = vec![Event::Icp(tampered)];
731        let result = validate_kel(&events);
732        assert!(matches!(result, Err(ValidationError::InvalidSaid { .. })));
733    }
734
735    #[test]
736    fn validates_icp_then_ixn() {
737        let (icp, keypair) = make_signed_icp();
738
739        // Create valid signed IXN
740        let ixn = make_signed_ixn(&icp.i, &icp.d, 1, &keypair);
741
742        let events = vec![Event::Icp(icp), Event::Ixn(ixn.clone())];
743        let state = validate_kel(&events).unwrap();
744        assert_eq!(state.sequence, 1);
745        assert_eq!(state.last_event_said, ixn.d);
746    }
747
748    #[test]
749    fn rejects_multiple_inceptions() {
750        let icp1 = finalize_icp_event(make_raw_icp("DKey1", "ENext1")).unwrap();
751        let icp2 = finalize_icp_event(make_raw_icp("DKey2", "ENext2")).unwrap();
752
753        let events = vec![Event::Icp(icp1), Event::Icp(icp2)];
754        let result = validate_kel(&events);
755        // Will fail on SAID or sequence validation before multiple inceptions check
756        assert!(result.is_err());
757    }
758
759    #[test]
760    fn compute_event_said_works() {
761        let icp = make_raw_icp("DKey1", "ENext1");
762        let event = Event::Icp(icp);
763        let said = compute_event_said(&event).unwrap();
764        assert!(said.as_str().starts_with('E'));
765        assert!(!said.is_empty());
766    }
767
768    #[test]
769    fn rejects_forged_signature() {
770        let (mut icp, _keypair) = make_signed_icp();
771
772        // Replace with forged signature (fake 64 bytes)
773        icp.x = URL_SAFE_NO_PAD.encode([0u8; 64]);
774
775        let events = vec![Event::Icp(icp)];
776        let result = validate_kel(&events);
777        assert!(matches!(
778            result,
779            Err(ValidationError::SignatureFailed { sequence: 0 })
780        ));
781    }
782
783    #[test]
784    fn rejects_missing_signature() {
785        let (mut icp, _keypair) = make_signed_icp();
786
787        // Clear the signature
788        icp.x = String::new();
789
790        let events = vec![Event::Icp(icp)];
791        let result = validate_kel(&events);
792        assert!(matches!(
793            result,
794            Err(ValidationError::SignatureFailed { sequence: 0 })
795        ));
796    }
797
798    #[test]
799    fn rejects_wrong_key_signature() {
800        // Create ICP with one key but sign with a different key
801        let rng = SystemRandom::new();
802        let pkcs8 = Ed25519KeyPair::generate_pkcs8(&rng).unwrap();
803        let keypair = Ed25519KeyPair::from_pkcs8(pkcs8.as_ref()).unwrap();
804        let key_encoded = format!("D{}", URL_SAFE_NO_PAD.encode(keypair.public_key().as_ref()));
805
806        let mut icp = IcpEvent {
807            v: KERI_VERSION.to_string(),
808            d: Said::default(),
809            i: Prefix::default(),
810            s: KeriSequence::new(0),
811            kt: "1".to_string(),
812            k: vec![key_encoded],
813            nt: "1".to_string(),
814            n: vec!["ENextCommitment".to_string()],
815            bt: "0".to_string(),
816            b: vec![],
817            a: vec![],
818            x: String::new(),
819        };
820
821        // Finalize SAID
822        icp = finalize_icp_event(icp).unwrap();
823
824        // Sign with a DIFFERENT key
825        let wrong_pkcs8 = Ed25519KeyPair::generate_pkcs8(&rng).unwrap();
826        let wrong_keypair = Ed25519KeyPair::from_pkcs8(wrong_pkcs8.as_ref()).unwrap();
827        let canonical = serialize_for_signing(&Event::Icp(icp.clone())).unwrap();
828        let sig = wrong_keypair.sign(&canonical);
829        icp.x = URL_SAFE_NO_PAD.encode(sig.as_ref());
830
831        let events = vec![Event::Icp(icp)];
832        let result = validate_kel(&events);
833        assert!(matches!(
834            result,
835            Err(ValidationError::SignatureFailed { sequence: 0 })
836        ));
837    }
838
839    // =========================================================================
840    // verify_event_crypto tests (O(1) delta validation)
841    // =========================================================================
842
843    #[test]
844    fn crypto_accepts_valid_inception() {
845        let (icp, _keypair) = make_signed_icp();
846        let result = verify_event_crypto(&Event::Icp(icp), None);
847        assert!(result.is_ok());
848    }
849
850    #[test]
851    fn crypto_rejects_forged_inception_signature() {
852        let (mut icp, _keypair) = make_signed_icp();
853        icp.x = URL_SAFE_NO_PAD.encode([0u8; 64]);
854        let result = verify_event_crypto(&Event::Icp(icp), None);
855        assert!(matches!(
856            result,
857            Err(ValidationError::SignatureFailed { sequence: 0 })
858        ));
859    }
860
861    #[test]
862    fn crypto_rejects_inception_with_mismatched_prefix() {
863        let (mut icp, _keypair) = make_signed_icp();
864        // Tamper i so it doesn't match d
865        icp.i = Prefix::new_unchecked("EWrongPrefix".to_string());
866        let result = verify_event_crypto(&Event::Icp(icp), None);
867        // Signature will fail because canonical form includes the tampered i
868        assert!(result.is_err());
869    }
870
871    #[test]
872    fn crypto_accepts_valid_interaction() {
873        let (icp, keypair) = make_signed_icp();
874        let ixn = make_signed_ixn(&icp.i, &icp.d, 1, &keypair);
875
876        let threshold = icp.kt.parse().unwrap();
877        let next_threshold = icp.nt.parse().unwrap();
878        let state = KeyState::from_inception(
879            icp.i.clone(),
880            icp.k.clone(),
881            icp.n.clone(),
882            threshold,
883            next_threshold,
884            icp.d.clone(),
885        );
886        let result = verify_event_crypto(&Event::Ixn(ixn), Some(&state));
887        assert!(result.is_ok());
888    }
889
890    #[test]
891    fn crypto_rejects_interaction_with_forged_signature() {
892        let (icp, keypair) = make_signed_icp();
893        let mut ixn = make_signed_ixn(&icp.i, &icp.d, 1, &keypair);
894
895        // Forge the signature
896        ixn.x = URL_SAFE_NO_PAD.encode([0u8; 64]);
897
898        let threshold = icp.kt.parse().unwrap();
899        let next_threshold = icp.nt.parse().unwrap();
900        let state = KeyState::from_inception(
901            icp.i.clone(),
902            icp.k.clone(),
903            icp.n.clone(),
904            threshold,
905            next_threshold,
906            icp.d.clone(),
907        );
908        let result = verify_event_crypto(&Event::Ixn(ixn), Some(&state));
909        assert!(matches!(
910            result,
911            Err(ValidationError::SignatureFailed { sequence: 1 })
912        ));
913    }
914
915    #[test]
916    fn crypto_rejects_interaction_signed_by_wrong_key() {
917        let (icp, _keypair) = make_signed_icp();
918
919        // Sign IXN with a different key
920        let rng = SystemRandom::new();
921        let wrong_pkcs8 = Ed25519KeyPair::generate_pkcs8(&rng).unwrap();
922        let wrong_keypair = Ed25519KeyPair::from_pkcs8(wrong_pkcs8.as_ref()).unwrap();
923        let ixn = make_signed_ixn(&icp.i, &icp.d, 1, &wrong_keypair);
924
925        let threshold = icp.kt.parse().unwrap();
926        let next_threshold = icp.nt.parse().unwrap();
927        let state = KeyState::from_inception(
928            icp.i.clone(),
929            icp.k.clone(),
930            icp.n.clone(),
931            threshold,
932            next_threshold,
933            icp.d.clone(),
934        );
935        let result = verify_event_crypto(&Event::Ixn(ixn), Some(&state));
936        assert!(matches!(
937            result,
938            Err(ValidationError::SignatureFailed { sequence: 1 })
939        ));
940    }
941
942    #[test]
943    fn crypto_rejects_rotation_on_abandoned_identity() {
944        use auths_core::crypto::said::compute_next_commitment;
945
946        let (icp, _keypair) = make_signed_icp();
947
948        // Create state with empty next_commitment (abandoned)
949        let threshold = icp.kt.parse().unwrap();
950        let next_threshold = icp.nt.parse().unwrap();
951        let mut state = KeyState::from_inception(
952            icp.i.clone(),
953            icp.k.clone(),
954            icp.n.clone(),
955            threshold,
956            next_threshold,
957            icp.d.clone(),
958        );
959        state.next_commitment = vec![];
960        state.is_abandoned = true;
961
962        // Generate a new key for rotation
963        let rng = SystemRandom::new();
964        let new_pkcs8 = Ed25519KeyPair::generate_pkcs8(&rng).unwrap();
965        let new_keypair = Ed25519KeyPair::from_pkcs8(new_pkcs8.as_ref()).unwrap();
966        let new_key_encoded = format!(
967            "D{}",
968            URL_SAFE_NO_PAD.encode(new_keypair.public_key().as_ref())
969        );
970        let new_commitment = compute_next_commitment(new_keypair.public_key().as_ref());
971
972        let mut rot = RotEvent {
973            v: KERI_VERSION.to_string(),
974            d: Said::default(),
975            i: icp.i.clone(),
976            s: KeriSequence::new(1),
977            p: icp.d.clone(),
978            kt: "1".to_string(),
979            k: vec![new_key_encoded],
980            nt: "1".to_string(),
981            n: vec![new_commitment],
982            bt: "0".to_string(),
983            b: vec![],
984            a: vec![],
985            x: String::new(),
986        };
987
988        // Compute SAID
989        let json = serde_json::to_vec(&Event::Rot(rot.clone())).unwrap();
990        rot.d = compute_said(&json);
991
992        // Sign
993        let canonical = serialize_for_signing(&Event::Rot(rot.clone())).unwrap();
994        let sig = new_keypair.sign(&canonical);
995        rot.x = URL_SAFE_NO_PAD.encode(sig.as_ref());
996
997        let result = verify_event_crypto(&Event::Rot(rot), Some(&state));
998        assert!(matches!(
999            result,
1000            Err(ValidationError::CommitmentMismatch { .. })
1001        ));
1002    }
1003
1004    #[test]
1005    fn crypto_rejects_rotation_without_precommitted_key() {
1006        use auths_core::crypto::said::compute_next_commitment;
1007
1008        let rng = SystemRandom::new();
1009
1010        // Create an inception with a known next commitment
1011        let pkcs8 = Ed25519KeyPair::generate_pkcs8(&rng).unwrap();
1012        let keypair = Ed25519KeyPair::from_pkcs8(pkcs8.as_ref()).unwrap();
1013        let key_encoded = format!("D{}", URL_SAFE_NO_PAD.encode(keypair.public_key().as_ref()));
1014
1015        // Generate the "real" next key to compute a commitment from
1016        let next_pkcs8 = Ed25519KeyPair::generate_pkcs8(&rng).unwrap();
1017        let next_keypair = Ed25519KeyPair::from_pkcs8(next_pkcs8.as_ref()).unwrap();
1018        let next_commitment = compute_next_commitment(next_keypair.public_key().as_ref());
1019
1020        let icp = IcpEvent {
1021            v: KERI_VERSION.to_string(),
1022            d: Said::default(),
1023            i: Prefix::default(),
1024            s: KeriSequence::new(0),
1025            kt: "1".to_string(),
1026            k: vec![key_encoded.clone()],
1027            nt: "1".to_string(),
1028            n: vec![next_commitment.clone()],
1029            bt: "0".to_string(),
1030            b: vec![],
1031            a: vec![],
1032            x: String::new(),
1033        };
1034        let mut icp = finalize_icp_event(icp).unwrap();
1035        let canonical = serialize_for_signing(&Event::Icp(icp.clone())).unwrap();
1036        let sig = keypair.sign(&canonical);
1037        icp.x = URL_SAFE_NO_PAD.encode(sig.as_ref());
1038
1039        let threshold = icp.kt.parse().unwrap();
1040        let next_threshold = icp.nt.parse().unwrap();
1041        let state = KeyState::from_inception(
1042            icp.i.clone(),
1043            icp.k.clone(),
1044            vec![next_commitment],
1045            threshold,
1046            next_threshold,
1047            icp.d.clone(),
1048        );
1049
1050        // Rotate with a RANDOM key that doesn't match the commitment
1051        let wrong_pkcs8 = Ed25519KeyPair::generate_pkcs8(&rng).unwrap();
1052        let wrong_keypair = Ed25519KeyPair::from_pkcs8(wrong_pkcs8.as_ref()).unwrap();
1053        let wrong_key_encoded = format!(
1054            "D{}",
1055            URL_SAFE_NO_PAD.encode(wrong_keypair.public_key().as_ref())
1056        );
1057        let new_commitment = compute_next_commitment(wrong_keypair.public_key().as_ref());
1058
1059        let mut rot = RotEvent {
1060            v: KERI_VERSION.to_string(),
1061            d: Said::default(),
1062            i: icp.i.clone(),
1063            s: KeriSequence::new(1),
1064            p: icp.d.clone(),
1065            kt: "1".to_string(),
1066            k: vec![wrong_key_encoded],
1067            nt: "1".to_string(),
1068            n: vec![new_commitment],
1069            bt: "0".to_string(),
1070            b: vec![],
1071            a: vec![],
1072            x: String::new(),
1073        };
1074
1075        let json = serde_json::to_vec(&Event::Rot(rot.clone())).unwrap();
1076        rot.d = compute_said(&json);
1077        let canonical = serialize_for_signing(&Event::Rot(rot.clone())).unwrap();
1078        let sig = wrong_keypair.sign(&canonical);
1079        rot.x = URL_SAFE_NO_PAD.encode(sig.as_ref());
1080
1081        let result = verify_event_crypto(&Event::Rot(rot), Some(&state));
1082        assert!(matches!(
1083            result,
1084            Err(ValidationError::CommitmentMismatch { .. })
1085        ));
1086    }
1087
1088    #[test]
1089    fn crypto_accepts_valid_rotation() {
1090        use auths_core::crypto::said::compute_next_commitment;
1091
1092        let rng = SystemRandom::new();
1093
1094        // Create inception with known next commitment
1095        let pkcs8 = Ed25519KeyPair::generate_pkcs8(&rng).unwrap();
1096        let keypair = Ed25519KeyPair::from_pkcs8(pkcs8.as_ref()).unwrap();
1097        let key_encoded = format!("D{}", URL_SAFE_NO_PAD.encode(keypair.public_key().as_ref()));
1098
1099        // Generate the next key and its commitment
1100        let next_pkcs8 = Ed25519KeyPair::generate_pkcs8(&rng).unwrap();
1101        let next_keypair = Ed25519KeyPair::from_pkcs8(next_pkcs8.as_ref()).unwrap();
1102        let next_commitment = compute_next_commitment(next_keypair.public_key().as_ref());
1103
1104        let icp = IcpEvent {
1105            v: KERI_VERSION.to_string(),
1106            d: Said::default(),
1107            i: Prefix::default(),
1108            s: KeriSequence::new(0),
1109            kt: "1".to_string(),
1110            k: vec![key_encoded],
1111            nt: "1".to_string(),
1112            n: vec![next_commitment.clone()],
1113            bt: "0".to_string(),
1114            b: vec![],
1115            a: vec![],
1116            x: String::new(),
1117        };
1118        let mut icp = finalize_icp_event(icp).unwrap();
1119        let canonical = serialize_for_signing(&Event::Icp(icp.clone())).unwrap();
1120        let sig = keypair.sign(&canonical);
1121        icp.x = URL_SAFE_NO_PAD.encode(sig.as_ref());
1122
1123        let threshold = icp.kt.parse().unwrap();
1124        let next_threshold = icp.nt.parse().unwrap();
1125        let state = KeyState::from_inception(
1126            icp.i.clone(),
1127            icp.k.clone(),
1128            vec![next_commitment],
1129            threshold,
1130            next_threshold,
1131            icp.d.clone(),
1132        );
1133
1134        // Rotate with the CORRECT next key
1135        let next_key_encoded = format!(
1136            "D{}",
1137            URL_SAFE_NO_PAD.encode(next_keypair.public_key().as_ref())
1138        );
1139        let third_pkcs8 = Ed25519KeyPair::generate_pkcs8(&rng).unwrap();
1140        let third_keypair = Ed25519KeyPair::from_pkcs8(third_pkcs8.as_ref()).unwrap();
1141        let third_commitment = compute_next_commitment(third_keypair.public_key().as_ref());
1142
1143        let mut rot = RotEvent {
1144            v: KERI_VERSION.to_string(),
1145            d: Said::default(),
1146            i: icp.i.clone(),
1147            s: KeriSequence::new(1),
1148            p: icp.d.clone(),
1149            kt: "1".to_string(),
1150            k: vec![next_key_encoded],
1151            nt: "1".to_string(),
1152            n: vec![third_commitment],
1153            bt: "0".to_string(),
1154            b: vec![],
1155            a: vec![],
1156            x: String::new(),
1157        };
1158
1159        let json = serde_json::to_vec(&Event::Rot(rot.clone())).unwrap();
1160        rot.d = compute_said(&json);
1161        let canonical = serialize_for_signing(&Event::Rot(rot.clone())).unwrap();
1162        let sig = next_keypair.sign(&canonical);
1163        rot.x = URL_SAFE_NO_PAD.encode(sig.as_ref());
1164
1165        let result = verify_event_crypto(&Event::Rot(rot), Some(&state));
1166        assert!(result.is_ok());
1167    }
1168
1169    // =========================================================================
1170    // Extracted helper function tests
1171    // =========================================================================
1172
1173    #[test]
1174    fn parse_threshold_valid() {
1175        assert_eq!(parse_threshold("1").unwrap(), 1);
1176        assert_eq!(parse_threshold("42").unwrap(), 42);
1177        assert_eq!(parse_threshold("0").unwrap(), 0);
1178    }
1179
1180    #[test]
1181    fn parse_threshold_invalid() {
1182        assert!(matches!(
1183            parse_threshold("abc"),
1184            Err(ValidationError::MalformedSequence { .. })
1185        ));
1186        assert!(matches!(
1187            parse_threshold(""),
1188            Err(ValidationError::MalformedSequence { .. })
1189        ));
1190        assert!(matches!(
1191            parse_threshold("-1"),
1192            Err(ValidationError::MalformedSequence { .. })
1193        ));
1194    }
1195
1196    #[test]
1197    fn validate_inception_success() {
1198        let (icp, _keypair) = make_signed_icp();
1199        let state = validate_inception(&icp).unwrap();
1200        assert_eq!(state.prefix, icp.i);
1201        assert_eq!(state.sequence, 0);
1202        assert_eq!(state.last_event_said, icp.d);
1203    }
1204
1205    #[test]
1206    fn validate_inception_bad_signature() {
1207        let (mut icp, _keypair) = make_signed_icp();
1208        icp.x = URL_SAFE_NO_PAD.encode([0u8; 64]);
1209        let result = validate_inception(&icp);
1210        assert!(matches!(
1211            result,
1212            Err(ValidationError::SignatureFailed { sequence: 0 })
1213        ));
1214    }
1215
1216    #[test]
1217    fn verify_sequence_correct() {
1218        let (icp, keypair) = make_signed_icp();
1219        let ixn = make_signed_ixn(&icp.i, &icp.d, 1, &keypair);
1220        assert!(verify_sequence(&Event::Ixn(ixn), 1).is_ok());
1221    }
1222
1223    #[test]
1224    fn verify_sequence_mismatch() {
1225        let (icp, keypair) = make_signed_icp();
1226        let ixn = make_signed_ixn(&icp.i, &icp.d, 5, &keypair);
1227        let result = verify_sequence(&Event::Ixn(ixn), 1);
1228        assert!(matches!(
1229            result,
1230            Err(ValidationError::InvalidSequence {
1231                expected: 1,
1232                actual: 5
1233            })
1234        ));
1235    }
1236
1237    #[test]
1238    fn verify_chain_linkage_correct() {
1239        let (icp, keypair) = make_signed_icp();
1240        let ixn = make_signed_ixn(&icp.i, &icp.d, 1, &keypair);
1241        let state = validate_inception(&icp).unwrap();
1242        assert!(verify_chain_linkage(&Event::Ixn(ixn), &state).is_ok());
1243    }
1244
1245    #[test]
1246    fn verify_chain_linkage_broken() {
1247        let (icp, keypair) = make_signed_icp();
1248        let wrong_said = Said::new_unchecked("EWrongPrevious".to_string());
1249        let ixn = make_signed_ixn(&icp.i, &wrong_said, 1, &keypair);
1250        let state = validate_inception(&icp).unwrap();
1251        let result = verify_chain_linkage(&Event::Ixn(ixn), &state);
1252        assert!(matches!(result, Err(ValidationError::BrokenChain { .. })));
1253    }
1254
1255    #[test]
1256    fn validate_rotation_bad_commitment() {
1257        use auths_core::crypto::said::compute_next_commitment;
1258
1259        let rng = SystemRandom::new();
1260
1261        let pkcs8 = Ed25519KeyPair::generate_pkcs8(&rng).unwrap();
1262        let keypair = Ed25519KeyPair::from_pkcs8(pkcs8.as_ref()).unwrap();
1263        let key_encoded = format!("D{}", URL_SAFE_NO_PAD.encode(keypair.public_key().as_ref()));
1264
1265        let next_pkcs8 = Ed25519KeyPair::generate_pkcs8(&rng).unwrap();
1266        let next_keypair = Ed25519KeyPair::from_pkcs8(next_pkcs8.as_ref()).unwrap();
1267        let next_commitment = compute_next_commitment(next_keypair.public_key().as_ref());
1268
1269        let icp = IcpEvent {
1270            v: KERI_VERSION.to_string(),
1271            d: Said::default(),
1272            i: Prefix::default(),
1273            s: KeriSequence::new(0),
1274            kt: "1".to_string(),
1275            k: vec![key_encoded],
1276            nt: "1".to_string(),
1277            n: vec![next_commitment],
1278            bt: "0".to_string(),
1279            b: vec![],
1280            a: vec![],
1281            x: String::new(),
1282        };
1283        let mut icp = finalize_icp_event(icp).unwrap();
1284        let canonical = serialize_for_signing(&Event::Icp(icp.clone())).unwrap();
1285        let sig = keypair.sign(&canonical);
1286        icp.x = URL_SAFE_NO_PAD.encode(sig.as_ref());
1287
1288        let mut state = validate_inception(&icp).unwrap();
1289
1290        // Rotate with a WRONG key that doesn't match the commitment
1291        let wrong_pkcs8 = Ed25519KeyPair::generate_pkcs8(&rng).unwrap();
1292        let wrong_keypair = Ed25519KeyPair::from_pkcs8(wrong_pkcs8.as_ref()).unwrap();
1293        let wrong_key_encoded = format!(
1294            "D{}",
1295            URL_SAFE_NO_PAD.encode(wrong_keypair.public_key().as_ref())
1296        );
1297        let wrong_commitment = compute_next_commitment(wrong_keypair.public_key().as_ref());
1298
1299        let mut rot = RotEvent {
1300            v: KERI_VERSION.to_string(),
1301            d: Said::default(),
1302            i: icp.i.clone(),
1303            s: KeriSequence::new(1),
1304            p: icp.d.clone(),
1305            kt: "1".to_string(),
1306            k: vec![wrong_key_encoded],
1307            nt: "1".to_string(),
1308            n: vec![wrong_commitment],
1309            bt: "0".to_string(),
1310            b: vec![],
1311            a: vec![],
1312            x: String::new(),
1313        };
1314
1315        let json = serde_json::to_vec(&Event::Rot(rot.clone())).unwrap();
1316        rot.d = compute_said(&json);
1317        let canonical = serialize_for_signing(&Event::Rot(rot.clone())).unwrap();
1318        let sig = wrong_keypair.sign(&canonical);
1319        rot.x = URL_SAFE_NO_PAD.encode(sig.as_ref());
1320
1321        let result = validate_rotation(&rot, &Event::Rot(rot.clone()), 1, &mut state);
1322        assert!(matches!(
1323            result,
1324            Err(ValidationError::CommitmentMismatch { sequence: 1 })
1325        ));
1326    }
1327
1328    #[test]
1329    fn validate_interaction_wrong_key() {
1330        let (icp, _keypair) = make_signed_icp();
1331        let mut state = validate_inception(&icp).unwrap();
1332
1333        let rng = SystemRandom::new();
1334        let wrong_pkcs8 = Ed25519KeyPair::generate_pkcs8(&rng).unwrap();
1335        let wrong_keypair = Ed25519KeyPair::from_pkcs8(wrong_pkcs8.as_ref()).unwrap();
1336        let ixn = make_signed_ixn(&icp.i, &icp.d, 1, &wrong_keypair);
1337
1338        let result = validate_interaction(&ixn, &Event::Ixn(ixn.clone()), 1, &mut state);
1339        assert!(matches!(
1340            result,
1341            Err(ValidationError::SignatureFailed { sequence: 1 })
1342        ));
1343    }
1344}