1use 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#[derive(Debug, Clone, thiserror::Error, PartialEq, Eq)]
21#[non_exhaustive]
22pub enum ValidationError {
23 #[error("Invalid SAID: expected {expected}, got {actual}")]
25 InvalidSaid {
26 expected: Said,
28 actual: Said,
30 },
31
32 #[error("Broken chain: event {sequence} references {referenced}, but previous was {actual}")]
34 BrokenChain {
35 sequence: u128,
37 referenced: Said,
39 actual: Said,
41 },
42
43 #[error("Invalid sequence: expected {expected}, got {actual}")]
45 InvalidSequence {
46 expected: u128,
48 actual: u128,
50 },
51
52 #[error("Pre-rotation commitment mismatch at sequence {sequence}")]
54 CommitmentMismatch {
55 sequence: u128,
57 },
58
59 #[error("Signature verification failed at sequence {sequence}")]
61 SignatureFailed {
62 sequence: u128,
64 },
65
66 #[error("Unsatisfiable threshold at sequence {sequence}: {reason}")]
70 ThresholdNotSatisfiable {
71 sequence: u128,
73 reason: String,
75 },
76
77 #[error("Invalid backer delta at sequence {sequence}: {reason}")]
80 InvalidBackerDelta {
81 sequence: u128,
83 reason: String,
85 },
86
87 #[error("Invalid backer role flip at sequence {sequence}: {reason}")]
93 BackerRoleFlip {
94 sequence: u128,
96 reason: String,
98 },
99
100 #[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 sequence: u128,
110 prior_next_count: usize,
112 new_key_count: usize,
114 },
115
116 #[error(
119 "Delegator seal not found at sequence {sequence}: delegator {delegator_aid} has no ixn-anchored seal for this event"
120 )]
121 DelegatorSealNotFound {
122 sequence: u128,
124 delegator_aid: String,
126 },
127
128 #[error(
133 "Delegate source seal missing at sequence {sequence}: delegated event carries no -G back-reference to its anchoring event"
134 )]
135 DelegateSourceSealMissing {
136 sequence: u128,
138 },
139
140 #[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 sequence: u128,
150 },
151
152 #[error(
156 "Delegator lookup required for delegated event at sequence {sequence}; call validate_kel_with_lookup"
157 )]
158 DelegatorLookupMissing {
159 sequence: u128,
161 },
162
163 #[error("First event must be inception")]
165 NotInception,
166
167 #[error("Empty KEL")]
169 EmptyKel,
170
171 #[error("Multiple inception events in KEL")]
173 MultipleInceptions,
174
175 #[error("Serialization error: {0}")]
177 Serialization(String),
178
179 #[error("Malformed sequence number: {raw:?}")]
181 MalformedSequence {
182 raw: String,
184 },
185
186 #[error("Invalid key encoding: {0}")]
188 InvalidKey(String),
189
190 #[error("Identity abandoned at sequence {sequence}, no more events allowed")]
192 AbandonedIdentity {
193 sequence: u128,
195 },
196
197 #[error("Interaction event at sequence {sequence} rejected: KEL is establishment-only (EO)")]
199 EstablishmentOnly {
200 sequence: u128,
202 },
203
204 #[error(
206 "Non-transferable identity: inception had empty next key commitments, no subsequent events allowed"
207 )]
208 NonTransferable,
209
210 #[error("Duplicate backer AID: {aid}")]
212 DuplicateBacker {
213 aid: String,
215 },
216
217 #[error("Invalid backer threshold: bt={bt} but backer_count={backer_count}")]
219 InvalidBackerThreshold {
220 bt: u64,
222 backer_count: usize,
224 },
225
226 #[error("Policy violation: event at seq {sequence} missing `dt`")]
231 MissingTimestamp {
232 sequence: u128,
234 },
235
236 #[error(
238 "Policy violation: timestamps not monotonic at seq {sequence} (prev={prev}, curr={curr})"
239 )]
240 NonMonotonicTimestamp {
241 sequence: u128,
243 prev: String,
245 curr: String,
247 },
248
249 #[error(
252 "Policy violation: rotation cooldown breached at seq {sequence} (interval {interval_secs}s < minimum {min_secs}s)"
253 )]
254 RotationCooldown {
255 sequence: u128,
257 interval_secs: i64,
259 min_secs: i64,
261 },
262
263 #[error(
265 "Policy violation: clock skew at seq {sequence} ({skew_secs}s) exceeds tolerance ({tolerance_secs}s)"
266 )]
267 ClockSkew {
268 sequence: u128,
270 skew_secs: i64,
272 tolerance_secs: i64,
274 },
275}
276
277pub 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 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 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 enforce_source_seal(delegated_event.source_seal(), &anchor, event_seq.value())
338}
339
340fn 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
355pub trait DelegatorKelLookup {
374 fn find_seal(&self, delegator_aid: &Prefix, seal_said: &Said) -> Option<SourceSeal>;
380}
381
382pub fn validate_kel(events: &[Event]) -> Result<KeyState, ValidationError> {
391 validate_kel_with_lookup(events, None::<&dyn DelegatorKelLookup>)
392}
393
394pub 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 WitnessedReplay::Pending { state, .. } => Ok(state),
408 }
409}
410
411#[derive(Debug, Clone, PartialEq, Eq)]
418pub enum WitnessedReplay {
419 Accepted(KeyState),
422 Pending {
427 state: KeyState,
429 sequence: u128,
431 said: Said,
433 required: Threshold,
435 collected: usize,
437 },
438}
439
440impl WitnessedReplay {
441 pub fn state(&self) -> &KeyState {
443 match self {
444 WitnessedReplay::Accepted(state) | WitnessedReplay::Pending { state, .. } => state,
445 }
446 }
447}
448
449pub 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
480fn 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 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 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 if state.is_abandoned {
529 return Err(ValidationError::AbandonedIdentity {
530 sequence: expected_seq,
531 });
532 }
533
534 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 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
567fn 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
617fn 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(&icp.b)?;
644
645 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 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
698fn 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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
732enum BackerRole {
733 Registrar,
734 NoRegistrar,
735 Unspecified,
736}
737
738fn 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 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 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(&rot.br)?;
784 validate_backer_uniqueness(&rot.ba)?;
785 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 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 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 state
868 .current_key()
869 .ok_or(ValidationError::SignatureFailed { sequence })?;
870 state.apply_interaction(sequence, ixn.d.clone());
871 Ok(())
872}
873
874fn 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 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 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 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
927fn 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 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 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 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
981pub fn replay_kel(events: &[Event]) -> Result<KeyState, ValidationError> {
988 validate_kel(events)
989}
990
991pub fn verify_event_crypto(
997 event: &Event,
998 current_state: Option<&KeyState>,
999) -> Result<(), ValidationError> {
1000 match event {
1001 Event::Icp(icp) => {
1002 if icp.k.is_empty() {
1004 return Err(ValidationError::SignatureFailed { sequence: 0 });
1005 }
1006
1007 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 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 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 state
1065 .current_key()
1066 .ok_or(ValidationError::SignatureFailed { sequence })?;
1067
1068 Ok(())
1069 }
1070 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
1092pub 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
1113pub 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
1131pub 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
1141pub 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
1156pub 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 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 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; }
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 if !threshold.is_satisfied(&verified_indices, keys.len()) {
1209 return Err(ValidationError::SignatureFailed { sequence });
1210 }
1211
1212 if matches!(event, Event::Rot(_) | Event::Drt(_))
1216 && let Some(state) = current_state
1217 {
1218 let n_len = state.next_commitment.len();
1219
1220 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 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
1264pub 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 if icp.i.is_empty() || icp.i.as_str().starts_with('E') {
1276 icp.i = Prefix::new_unchecked(said.into_inner());
1277 }
1278
1279 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
1287pub 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 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
1314pub 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
1331pub 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
1354pub 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
1371pub 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
1391pub 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 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 #[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 #[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]; 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 #[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 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 let kp2 = gen_keypair();
1760
1761 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 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 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 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 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 let state = KeyState::from_inception(
1952 Prefix::new_unchecked("EPrefix".to_string()),
1953 vec![CesrKey::new_unchecked("DKey1".to_string())],
1954 vec![], 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 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 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 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 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 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 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 let same_role = make_rot(vec![], vec![], vec![ConfigTrait::NoRegistrarBackers]);
2052 assert!(validate_rotation(&same_role, 1, &mut nrb_state()).is_ok());
2053
2054 let inherit = make_rot(vec![], vec![], vec![]);
2056 assert!(validate_rotation(&inherit, 1, &mut nrb_state()).is_ok());
2057 }
2058
2059 use crate::witness::WitnessReceipt;
2062
2063 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 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(); 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"]); 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"]); 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 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 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 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 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#[derive(Debug, Clone)]
2248pub struct KelPolicy {
2249 pub min_rotation_interval: chrono::Duration,
2252 pub clock_skew_tolerance: chrono::Duration,
2255 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
2272pub 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 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 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 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 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}