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("Event at sequence {sequence} applied without prior key state")]
166 MissingPriorState {
167 sequence: u128,
169 },
170
171 #[error("First event must be inception")]
173 NotInception,
174
175 #[error("Empty KEL")]
177 EmptyKel,
178
179 #[error("Multiple inception events in KEL")]
181 MultipleInceptions,
182
183 #[error("Serialization error: {0}")]
185 Serialization(String),
186
187 #[error("Malformed sequence number: {raw:?}")]
189 MalformedSequence {
190 raw: String,
192 },
193
194 #[error("Invalid key encoding: {0}")]
196 InvalidKey(String),
197
198 #[error("Identity abandoned at sequence {sequence}, no more events allowed")]
200 AbandonedIdentity {
201 sequence: u128,
203 },
204
205 #[error("Interaction event at sequence {sequence} rejected: KEL is establishment-only (EO)")]
207 EstablishmentOnly {
208 sequence: u128,
210 },
211
212 #[error(
214 "Non-transferable identity: inception had empty next key commitments, no subsequent events allowed"
215 )]
216 NonTransferable,
217
218 #[error("Duplicate backer AID: {aid}")]
220 DuplicateBacker {
221 aid: String,
223 },
224
225 #[error("Invalid backer threshold: bt={bt} but backer_count={backer_count}")]
227 InvalidBackerThreshold {
228 bt: u64,
230 backer_count: usize,
232 },
233
234 #[error("Policy violation: event at seq {sequence} missing `dt`")]
239 MissingTimestamp {
240 sequence: u128,
242 },
243
244 #[error(
246 "Policy violation: timestamps not monotonic at seq {sequence} (prev={prev}, curr={curr})"
247 )]
248 NonMonotonicTimestamp {
249 sequence: u128,
251 prev: String,
253 curr: String,
255 },
256
257 #[error(
260 "Policy violation: rotation cooldown breached at seq {sequence} (interval {interval_secs}s < minimum {min_secs}s)"
261 )]
262 RotationCooldown {
263 sequence: u128,
265 interval_secs: i64,
267 min_secs: i64,
269 },
270
271 #[error(
273 "Policy violation: clock skew at seq {sequence} ({skew_secs}s) exceeds tolerance ({tolerance_secs}s)"
274 )]
275 ClockSkew {
276 sequence: u128,
278 skew_secs: i64,
280 tolerance_secs: i64,
282 },
283}
284
285pub fn validate_delegation(
295 delegated_event: &Event,
296 delegator_kel: &[Event],
297) -> Result<(), ValidationError> {
298 if !delegated_event.is_delegated() {
299 return Err(ValidationError::Serialization(
300 "validate_delegation called on non-delegated event".to_string(),
301 ));
302 }
303
304 let event_said = delegated_event.said();
305 let event_seq = delegated_event.sequence();
306
307 if let Some(Event::Icp(delegator_icp)) = delegator_kel.first()
309 && delegator_icp.c.contains(&ConfigTrait::DoNotDelegate)
310 {
311 return Err(ValidationError::Serialization(
312 "Delegator has DoNotDelegate (DND) config trait".to_string(),
313 ));
314 }
315
316 let anchor = delegator_kel.iter().find_map(|event| {
319 let anchors = event.anchors().iter().any(|seal| {
320 matches!(
321 seal,
322 Seal::KeyEvent { i, s, d }
323 if i == delegated_event.prefix()
324 && s.value() == event_seq.value()
325 && d == event_said
326 )
327 });
328 anchors.then(|| SourceSeal {
329 s: event.sequence(),
330 d: event.said().clone(),
331 })
332 });
333
334 let Some(anchor) = anchor else {
335 return Err(ValidationError::Serialization(format!(
336 "No delegation seal found in delegator KEL for prefix={}, sn={}, said={}",
337 delegated_event.prefix(),
338 event_seq,
339 event_said
340 )));
341 };
342
343 enforce_source_seal(delegated_event.source_seal(), &anchor, event_seq.value())
346}
347
348fn enforce_source_seal(
352 source_seal: Option<&SourceSeal>,
353 anchor: &SourceSeal,
354 sequence: u128,
355) -> Result<(), ValidationError> {
356 match source_seal {
357 None => Err(ValidationError::DelegateSourceSealMissing { sequence }),
358 Some(seal) if seal == anchor => Ok(()),
359 Some(_) => Err(ValidationError::SealBackRefMismatch { sequence }),
360 }
361}
362
363pub trait DelegatorKelLookup {
382 fn find_seal(&self, delegator_aid: &Prefix, seal_said: &Said) -> Option<SourceSeal>;
388}
389
390pub struct KelSealIndex {
399 seals: std::collections::HashMap<Said, SourceSeal>,
401}
402
403impl KelSealIndex {
404 pub fn from_events(events: &[Event]) -> Self {
412 let mut seals = std::collections::HashMap::new();
413 for event in events {
414 for seal in event.anchors() {
415 if let Seal::KeyEvent { d, .. } = seal {
416 seals.entry(d.clone()).or_insert_with(|| SourceSeal {
417 s: event.sequence(),
418 d: event.said().clone(),
419 });
420 }
421 }
422 }
423 Self { seals }
424 }
425}
426
427impl DelegatorKelLookup for KelSealIndex {
428 fn find_seal(&self, _delegator_aid: &Prefix, seal_said: &Said) -> Option<SourceSeal> {
429 self.seals.get(seal_said).cloned()
430 }
431}
432
433#[derive(Clone, Copy)]
450pub struct TrustedKel<'a>(&'a [Event]);
451
452impl<'a> TrustedKel<'a> {
453 pub fn from_trusted_source(events: &'a [Event]) -> Self {
462 Self(events)
463 }
464
465 pub fn events(&self) -> &'a [Event] {
467 self.0
468 }
469
470 pub fn replay(self) -> Result<KeyState, ValidationError> {
472 validate_kel(self.0)
473 }
474
475 pub fn replay_with_lookup(
478 self,
479 lookup: Option<&dyn DelegatorKelLookup>,
480 ) -> Result<KeyState, ValidationError> {
481 validate_kel_with_lookup(self.0, lookup)
482 }
483
484 pub fn replay_with_receipts(
486 self,
487 lookup: Option<&dyn DelegatorKelLookup>,
488 receipt_lookup: &dyn WitnessReceiptLookup,
489 ) -> Result<WitnessedReplay, ValidationError> {
490 validate_kel_with_receipts(self.0, lookup, receipt_lookup)
491 }
492
493 pub fn replay_with_policy(
496 self,
497 timestamps: &[Option<chrono::DateTime<chrono::Utc>>],
498 policy: &KelPolicy,
499 now: chrono::DateTime<chrono::Utc>,
500 ) -> Result<KeyState, ValidationError> {
501 validate_kel_with_policy(self.0, timestamps, policy, now)
502 }
503}
504
505pub(crate) fn validate_kel(events: &[Event]) -> Result<KeyState, ValidationError> {
515 validate_kel_with_lookup(events, None::<&dyn DelegatorKelLookup>)
516}
517
518pub(crate) fn validate_kel_with_lookup(
523 events: &[Event],
524 lookup: Option<&dyn DelegatorKelLookup>,
525) -> Result<KeyState, ValidationError> {
526 match replay_kel_gated(events, lookup, None)? {
527 WitnessedReplay::Accepted(state) => Ok(state),
528 WitnessedReplay::Pending { state, .. } => Ok(state),
532 }
533}
534
535#[derive(Debug, Clone, PartialEq, Eq)]
542pub enum WitnessedReplay {
543 Accepted(KeyState),
546 Pending {
551 state: KeyState,
553 sequence: u128,
555 said: Said,
557 required: Threshold,
559 collected: usize,
561 },
562}
563
564impl WitnessedReplay {
565 pub fn state(&self) -> &KeyState {
567 match self {
568 WitnessedReplay::Accepted(state) | WitnessedReplay::Pending { state, .. } => state,
569 }
570 }
571}
572
573pub(crate) fn validate_kel_with_receipts(
597 events: &[Event],
598 delegator_lookup: Option<&dyn DelegatorKelLookup>,
599 receipt_lookup: &dyn WitnessReceiptLookup,
600) -> Result<WitnessedReplay, ValidationError> {
601 replay_kel_gated(events, delegator_lookup, Some(receipt_lookup))
602}
603
604fn replay_kel_gated(
611 events: &[Event],
612 lookup: Option<&dyn DelegatorKelLookup>,
613 receipt_lookup: Option<&dyn WitnessReceiptLookup>,
614) -> Result<WitnessedReplay, ValidationError> {
615 if events.is_empty() {
616 return Err(ValidationError::EmptyKel);
617 }
618
619 verify_event_said(&events[0])?;
620 let (mut state, inception_n_is_empty, establishment_only) = match &events[0] {
621 Event::Icp(icp) => (
622 validate_inception(icp)?,
623 icp.n.is_empty(),
624 icp.c.contains(&ConfigTrait::EstablishmentOnly),
625 ),
626 Event::Dip(dip) => (
627 validate_delegated_inception(dip, lookup)?,
628 dip.n.is_empty(),
629 dip.c.contains(&ConfigTrait::EstablishmentOnly),
630 ),
631 _ => return Err(ValidationError::NotInception),
632 };
633
634 let controller = state.prefix.clone();
635
636 if let Some(rl) = receipt_lookup
638 && let Some(pending) = gate_establishment(&controller, &state, 0, events[0].said(), rl)
639 {
640 return Ok(pending);
641 }
642
643 if inception_n_is_empty && events.len() > 1 {
645 return Err(ValidationError::NonTransferable);
646 }
647
648 for (idx, event) in events.iter().enumerate().skip(1) {
649 let expected_seq = idx as u128;
650
651 if state.is_abandoned {
653 return Err(ValidationError::AbandonedIdentity {
654 sequence: expected_seq,
655 });
656 }
657
658 if establishment_only && matches!(event, Event::Ixn(_)) {
660 return Err(ValidationError::EstablishmentOnly {
661 sequence: expected_seq,
662 });
663 }
664
665 verify_event_said(event)?;
666 verify_sequence(event, expected_seq)?;
667 verify_chain_linkage(event, &state)?;
668
669 match event {
670 Event::Rot(rot) => validate_rotation(rot, expected_seq, &mut state)?,
671 Event::Ixn(ixn) => validate_interaction(ixn, expected_seq, &mut state)?,
672 Event::Icp(_) | Event::Dip(_) => return Err(ValidationError::MultipleInceptions),
673 Event::Drt(drt) => {
674 validate_delegated_rotation(drt, expected_seq, &mut state, lookup)?;
675 }
676 }
677
678 if let Some(rl) = receipt_lookup
680 && matches!(event, Event::Rot(_) | Event::Drt(_))
681 && let Some(pending) =
682 gate_establishment(&controller, &state, expected_seq, event.said(), rl)
683 {
684 return Ok(pending);
685 }
686 }
687
688 Ok(WitnessedReplay::Accepted(state))
689}
690
691pub fn validate_signed_kel(
715 events: &[crate::events::SignedEvent],
716 lookup: Option<&dyn DelegatorKelLookup>,
717) -> Result<KeyState, ValidationError> {
718 if events.is_empty() {
719 return Err(ValidationError::EmptyKel);
720 }
721
722 let first = &events[0];
725 verify_event_said(&first.event)?;
726 validate_signed_event(first, None)?;
727 let (mut state, inception_n_is_empty, establishment_only) = match &first.event {
728 Event::Icp(icp) => (
729 validate_inception(icp)?,
730 icp.n.is_empty(),
731 icp.c.contains(&ConfigTrait::EstablishmentOnly),
732 ),
733 Event::Dip(dip) => (
734 validate_delegated_inception(dip, lookup)?,
735 dip.n.is_empty(),
736 dip.c.contains(&ConfigTrait::EstablishmentOnly),
737 ),
738 _ => return Err(ValidationError::NotInception),
739 };
740
741 if inception_n_is_empty && events.len() > 1 {
742 return Err(ValidationError::NonTransferable);
743 }
744
745 for (idx, signed) in events.iter().enumerate().skip(1) {
746 let event = &signed.event;
747 let expected_seq = idx as u128;
748
749 if state.is_abandoned {
750 return Err(ValidationError::AbandonedIdentity {
751 sequence: expected_seq,
752 });
753 }
754 if establishment_only && matches!(event, Event::Ixn(_)) {
755 return Err(ValidationError::EstablishmentOnly {
756 sequence: expected_seq,
757 });
758 }
759
760 verify_event_said(event)?;
761 verify_sequence(event, expected_seq)?;
762 verify_chain_linkage(event, &state)?;
763 validate_signed_event(signed, Some(&state))?;
766
767 match event {
768 Event::Rot(rot) => validate_rotation(rot, expected_seq, &mut state)?,
769 Event::Ixn(ixn) => validate_interaction(ixn, expected_seq, &mut state)?,
770 Event::Icp(_) | Event::Dip(_) => return Err(ValidationError::MultipleInceptions),
771 Event::Drt(drt) => {
772 validate_delegated_rotation(drt, expected_seq, &mut state, lookup)?;
773 }
774 }
775 }
776
777 Ok(state)
778}
779
780fn gate_establishment(
787 controller: &Prefix,
788 state: &KeyState,
789 sequence: u128,
790 event_said: &Said,
791 receipt_lookup: &dyn WitnessReceiptLookup,
792) -> Option<WitnessedReplay> {
793 let sn = sequence as u64;
794 let agreement = WitnessAgreement::new(1);
795 agreement.submit_event(
796 controller,
797 sn,
798 event_said,
799 &state.backer_threshold,
800 &state.backers,
801 );
802 for receipt in receipt_lookup.receipts_for(controller, KeriSequence::new(sequence), event_said)
803 {
804 agreement.add_receipt(controller, sn, event_said, receipt.witness.as_str());
805 }
806 match agreement.status(controller, sn, event_said) {
807 AgreementStatus::Accepted => None,
808 AgreementStatus::Pending { collected } => Some(WitnessedReplay::Pending {
809 state: state.clone(),
810 sequence,
811 said: event_said.clone(),
812 required: state.backer_threshold.clone(),
813 collected,
814 }),
815 }
816}
817
818fn validate_backer_uniqueness(backers: &[Prefix]) -> Result<(), ValidationError> {
819 let mut seen = std::collections::HashSet::new();
820 for b in backers {
821 if !seen.insert(b.as_str()) {
822 return Err(ValidationError::DuplicateBacker {
823 aid: b.as_str().to_string(),
824 });
825 }
826 }
827 Ok(())
828}
829
830fn validate_thresholds(
833 sequence: u128,
834 kt: &Threshold,
835 k_len: usize,
836 nt: &Threshold,
837 n_len: usize,
838 bt: &Threshold,
839 b_len: usize,
840) -> Result<(), ValidationError> {
841 let check = |t: &Threshold, len: usize, which: &str| {
842 t.validate_satisfiable(len)
843 .map_err(|e| ValidationError::ThresholdNotSatisfiable {
844 sequence,
845 reason: format!("{which}: {}", e.reason),
846 })
847 };
848 check(kt, k_len, "kt")?;
849 check(nt, n_len, "nt")?;
850 check(bt, b_len, "bt")?;
851 Ok(())
852}
853
854fn verify_inception_self_cert(i: &Prefix, d: &Said, k: &[CesrKey]) -> Result<(), ValidationError> {
867 if k.is_empty() {
869 return Err(ValidationError::SignatureFailed { sequence: 0 });
870 }
871
872 if i.as_str().starts_with('E') {
873 if i.as_str() != d.as_str() {
874 return Err(ValidationError::InvalidSaid {
875 expected: d.clone(),
876 actual: Said::new_unchecked(i.as_str().to_string()),
877 });
878 }
879 } else {
880 let i_key = KeriPublicKey::parse(i.as_str())
883 .map_err(|_| ValidationError::SignatureFailed { sequence: 0 })?;
884 let k0 = k[0]
885 .parse()
886 .map_err(|_| ValidationError::SignatureFailed { sequence: 0 })?;
887 if i_key.as_bytes() != k0.as_bytes() {
888 return Err(ValidationError::InvalidSaid {
889 expected: Said::new_unchecked(k[0].as_str().to_string()),
890 actual: Said::new_unchecked(i.as_str().to_string()),
891 });
892 }
893 }
894
895 Ok(())
896}
897
898fn validate_inception(icp: &IcpEvent) -> Result<KeyState, ValidationError> {
899 verify_inception_self_cert(&icp.i, &icp.d, &icp.k)?;
903
904 validate_backer_uniqueness(&icp.b)?;
906
907 validate_thresholds(
909 icp.s.value(),
910 &icp.kt,
911 icp.k.len(),
912 &icp.nt,
913 icp.n.len(),
914 &icp.bt,
915 icp.b.len(),
916 )?;
917
918 let bt_val = icp.bt.simple_value().unwrap_or(0);
920 if icp.b.is_empty() && bt_val != 0 {
921 return Err(ValidationError::InvalidBackerThreshold {
922 bt: bt_val,
923 backer_count: 0,
924 });
925 }
926
927 Ok(KeyState::from_inception(
928 icp.i.clone(),
929 icp.k.clone(),
930 icp.n.clone(),
931 icp.kt.clone(),
932 icp.nt.clone(),
933 icp.d.clone(),
934 icp.b.clone(),
935 icp.bt.clone(),
936 icp.c.clone(),
937 ))
938}
939
940fn verify_sequence(event: &Event, expected: u128) -> Result<(), ValidationError> {
941 let actual = event.sequence().value();
942 if actual != expected {
943 return Err(ValidationError::InvalidSequence { expected, actual });
944 }
945 Ok(())
946}
947
948fn verify_chain_linkage(event: &Event, state: &KeyState) -> Result<(), ValidationError> {
949 let prev_said = event.previous().ok_or(ValidationError::NotInception)?;
950 if *prev_said != state.last_event_said {
951 return Err(ValidationError::BrokenChain {
952 sequence: event.sequence().value(),
953 referenced: prev_said.clone(),
954 actual: state.last_event_said.clone(),
955 });
956 }
957 Ok(())
958}
959
960fn prior_commitments_satisfy_threshold(
969 next_commitment: &[Said],
970 next_threshold: &Threshold,
971 new_keys: &[CesrKey],
972) -> bool {
973 let revealed: Vec<u32> = next_commitment
974 .iter()
975 .enumerate()
976 .filter_map(|(j, commitment)| {
977 let matched = new_keys.iter().any(|key| {
978 key.parse()
979 .map(|pk| verify_commitment(&pk, commitment))
980 .unwrap_or(false)
981 });
982 matched.then_some(j as u32)
983 })
984 .collect();
985 next_threshold.is_satisfied(&revealed, next_commitment.len())
986}
987
988#[derive(Debug, Clone, Copy, PartialEq, Eq)]
994enum BackerRole {
995 Registrar,
996 NoRegistrar,
997 Unspecified,
998}
999
1000fn backer_role(traits: &[ConfigTrait]) -> BackerRole {
1002 let mut role = BackerRole::Unspecified;
1003 for t in traits {
1004 match t {
1005 ConfigTrait::RegistrarBackers => role = BackerRole::Registrar,
1006 ConfigTrait::NoRegistrarBackers => role = BackerRole::NoRegistrar,
1007 _ => {}
1008 }
1009 }
1010 role
1011}
1012
1013fn validate_rotation(
1014 rot: &RotEvent,
1015 sequence: u128,
1016 state: &mut KeyState,
1017) -> Result<(), ValidationError> {
1018 let post_backer_count =
1022 state.backers.iter().filter(|b| !rot.br.contains(b)).count() + rot.ba.len();
1023 validate_thresholds(
1024 sequence,
1025 &rot.kt,
1026 rot.k.len(),
1027 &rot.nt,
1028 rot.n.len(),
1029 &rot.bt,
1030 post_backer_count,
1031 )?;
1032
1033 if !state.next_commitment.is_empty()
1035 && !prior_commitments_satisfy_threshold(
1036 &state.next_commitment,
1037 &state.next_threshold,
1038 &rot.k,
1039 )
1040 {
1041 return Err(ValidationError::CommitmentMismatch { sequence });
1042 }
1043
1044 validate_backer_uniqueness(&rot.br)?;
1046 validate_backer_uniqueness(&rot.ba)?;
1047 for aid in &rot.ba {
1049 if rot.br.contains(aid) {
1050 return Err(ValidationError::DuplicateBacker {
1051 aid: aid.as_str().to_string(),
1052 });
1053 }
1054 }
1055 for aid in &rot.br {
1059 if !state.backers.contains(aid) {
1060 return Err(ValidationError::InvalidBackerDelta {
1061 sequence,
1062 reason: format!("br entry {} not in prior backers", aid.as_str()),
1063 });
1064 }
1065 }
1066 let survivors: Vec<_> = state
1067 .backers
1068 .iter()
1069 .filter(|b| !rot.br.contains(b))
1070 .collect();
1071 for aid in &rot.ba {
1072 if survivors.contains(&aid) {
1073 return Err(ValidationError::InvalidBackerDelta {
1074 sequence,
1075 reason: format!("ba entry {} duplicates a surviving backer", aid.as_str()),
1076 });
1077 }
1078 }
1079
1080 if !rot.c.is_empty() {
1085 let old_role = backer_role(&state.config_traits);
1086 let new_role = backer_role(&rot.c);
1087 let is_flip = matches!(
1088 (old_role, new_role),
1089 (BackerRole::Registrar, BackerRole::NoRegistrar)
1090 | (BackerRole::NoRegistrar, BackerRole::Registrar)
1091 );
1092 if is_flip && !survivors.is_empty() {
1093 return Err(ValidationError::BackerRoleFlip {
1094 sequence,
1095 reason: format!(
1096 "{old_role:?}->{new_role:?} but {} prior backer(s) survive; \
1097 a role flip must cut all prior backers",
1098 survivors.len()
1099 ),
1100 });
1101 }
1102 }
1103
1104 state.apply_rotation(
1105 rot.k.clone(),
1106 rot.n.clone(),
1107 rot.kt.clone(),
1108 rot.nt.clone(),
1109 sequence,
1110 rot.d.clone(),
1111 &rot.br,
1112 &rot.ba,
1113 rot.bt.clone(),
1114 rot.c.clone(),
1115 );
1116
1117 Ok(())
1118}
1119
1120fn validate_interaction(
1121 ixn: &IxnEvent,
1122 sequence: u128,
1123 state: &mut KeyState,
1124) -> Result<(), ValidationError> {
1125 state
1130 .current_key()
1131 .ok_or(ValidationError::SignatureFailed { sequence })?;
1132 state.apply_interaction(sequence, ixn.d.clone());
1133 Ok(())
1134}
1135
1136fn validate_delegated_inception(
1142 dip: &crate::events::DipEvent,
1143 lookup: Option<&dyn DelegatorKelLookup>,
1144) -> Result<KeyState, ValidationError> {
1145 let sequence = dip.s.value();
1146 let lookup = lookup.ok_or(ValidationError::DelegatorLookupMissing { sequence })?;
1147
1148 let anchor = lookup.find_seal(&dip.di, &dip.d).ok_or_else(|| {
1152 ValidationError::DelegatorSealNotFound {
1153 sequence,
1154 delegator_aid: dip.di.as_str().to_string(),
1155 }
1156 })?;
1157 enforce_source_seal(dip.source_seal.as_ref(), &anchor, sequence)?;
1158
1159 verify_inception_self_cert(&dip.i, &dip.d, &dip.k)?;
1162
1163 validate_backer_uniqueness(&dip.b)?;
1165 let bt_val = dip.bt.simple_value().unwrap_or(0);
1166 if dip.b.is_empty() && bt_val != 0 {
1167 return Err(ValidationError::InvalidBackerThreshold {
1168 bt: bt_val,
1169 backer_count: 0,
1170 });
1171 }
1172
1173 let is_non_transferable = dip.n.is_empty();
1175 Ok(KeyState {
1176 prefix: dip.i.clone(),
1177 current_keys: dip.k.clone(),
1178 next_commitment: dip.n.clone(),
1179 sequence: dip.s.value(),
1180 last_event_said: dip.d.clone(),
1181 is_abandoned: false,
1182 threshold: dip.kt.clone(),
1183 next_threshold: dip.nt.clone(),
1184 backers: dip.b.clone(),
1185 backer_threshold: dip.bt.clone(),
1186 config_traits: dip.c.clone(),
1187 is_non_transferable,
1188 delegator: Some(dip.di.clone()),
1189 last_establishment_sequence: dip.s.value(),
1190 })
1191}
1192
1193fn validate_delegated_rotation(
1199 drt: &crate::events::DrtEvent,
1200 sequence: u128,
1201 state: &mut KeyState,
1202 lookup: Option<&dyn DelegatorKelLookup>,
1203) -> Result<(), ValidationError> {
1204 let lookup = lookup.ok_or(ValidationError::DelegatorLookupMissing { sequence })?;
1205
1206 let anchor = lookup.find_seal(&drt.di, &drt.d).ok_or_else(|| {
1209 ValidationError::DelegatorSealNotFound {
1210 sequence,
1211 delegator_aid: drt.di.as_str().to_string(),
1212 }
1213 })?;
1214 enforce_source_seal(drt.source_seal.as_ref(), &anchor, sequence)?;
1215
1216 if !state.next_commitment.is_empty()
1218 && !prior_commitments_satisfy_threshold(
1219 &state.next_commitment,
1220 &state.next_threshold,
1221 &drt.k,
1222 )
1223 {
1224 return Err(ValidationError::CommitmentMismatch { sequence });
1225 }
1226
1227 validate_backer_uniqueness(&drt.br)?;
1228 validate_backer_uniqueness(&drt.ba)?;
1229 for aid in &drt.ba {
1230 if drt.br.contains(aid) {
1231 return Err(ValidationError::DuplicateBacker {
1232 aid: aid.as_str().to_string(),
1233 });
1234 }
1235 }
1236
1237 state.sequence = sequence;
1239 state.last_event_said = drt.d.clone();
1240 state.current_keys = drt.k.clone();
1241 state.next_commitment = drt.n.clone();
1242 state.threshold = drt.kt.clone();
1243 state.next_threshold = drt.nt.clone();
1244 Ok(())
1245}
1246
1247pub fn verify_event_crypto(
1253 event: &Event,
1254 current_state: Option<&KeyState>,
1255) -> Result<(), ValidationError> {
1256 match event {
1257 Event::Icp(icp) => verify_inception_self_cert(&icp.i, &icp.d, &icp.k),
1260 Event::Rot(rot) => {
1261 let sequence = event.sequence().value();
1262 let state = current_state.ok_or(ValidationError::SignatureFailed { sequence })?;
1263
1264 if state.is_abandoned || state.next_commitment.is_empty() {
1265 return Err(ValidationError::CommitmentMismatch { sequence });
1266 }
1267
1268 if rot.k.is_empty() {
1269 return Err(ValidationError::SignatureFailed { sequence });
1270 }
1271
1272 if !prior_commitments_satisfy_threshold(
1274 &state.next_commitment,
1275 &state.next_threshold,
1276 &rot.k,
1277 ) {
1278 return Err(ValidationError::CommitmentMismatch { sequence });
1279 }
1280
1281 Ok(())
1282 }
1283 Event::Ixn(_) => {
1284 let sequence = event.sequence().value();
1285 let state = current_state.ok_or(ValidationError::SignatureFailed { sequence })?;
1286
1287 state
1290 .current_key()
1291 .ok_or(ValidationError::SignatureFailed { sequence })?;
1292
1293 Ok(())
1294 }
1295 Event::Dip(dip) => verify_inception_self_cert(&dip.i, &dip.d, &dip.k),
1298 Event::Drt(drt) => {
1299 let sequence = event.sequence().value();
1300 let state = current_state.ok_or(ValidationError::SignatureFailed { sequence })?;
1301
1302 if state.is_abandoned || state.next_commitment.is_empty() {
1303 return Err(ValidationError::CommitmentMismatch { sequence });
1304 }
1305 if drt.k.is_empty() {
1306 return Err(ValidationError::SignatureFailed { sequence });
1307 }
1308 Ok(())
1309 }
1310 }
1311}
1312
1313pub fn state_after_event(
1331 current_state: Option<&KeyState>,
1332 event: &Event,
1333) -> Result<KeyState, ValidationError> {
1334 let sequence = event.sequence().value();
1335 match event {
1336 Event::Icp(icp) => Ok(KeyState::from_inception(
1337 icp.i.clone(),
1338 icp.k.clone(),
1339 icp.n.clone(),
1340 icp.kt.clone(),
1341 icp.nt.clone(),
1342 icp.d.clone(),
1343 icp.b.clone(),
1344 icp.bt.clone(),
1345 icp.c.clone(),
1346 )),
1347 Event::Rot(rot) => {
1348 let mut state = current_state
1349 .cloned()
1350 .ok_or(ValidationError::MissingPriorState { sequence })?;
1351 state.apply_rotation(
1352 rot.k.clone(),
1353 rot.n.clone(),
1354 rot.kt.clone(),
1355 rot.nt.clone(),
1356 sequence,
1357 rot.d.clone(),
1358 &rot.br,
1359 &rot.ba,
1360 rot.bt.clone(),
1361 rot.c.clone(),
1362 );
1363 Ok(state)
1364 }
1365 Event::Ixn(ixn) => {
1366 let mut state = current_state
1367 .cloned()
1368 .ok_or(ValidationError::MissingPriorState { sequence })?;
1369 state.apply_interaction(sequence, ixn.d.clone());
1370 Ok(state)
1371 }
1372 Event::Dip(dip) => {
1373 let mut state = KeyState::from_inception(
1374 dip.i.clone(),
1375 dip.k.clone(),
1376 dip.n.clone(),
1377 dip.kt.clone(),
1378 dip.nt.clone(),
1379 dip.d.clone(),
1380 dip.b.clone(),
1381 dip.bt.clone(),
1382 dip.c.clone(),
1383 );
1384 state.delegator = Some(dip.di.clone());
1388 Ok(state)
1389 }
1390 Event::Drt(drt) => {
1391 let mut state = current_state
1392 .cloned()
1393 .ok_or(ValidationError::MissingPriorState { sequence })?;
1394 state.apply_rotation(
1395 drt.k.clone(),
1396 drt.n.clone(),
1397 drt.kt.clone(),
1398 drt.nt.clone(),
1399 sequence,
1400 drt.d.clone(),
1401 &drt.br,
1402 &drt.ba,
1403 drt.bt.clone(),
1404 drt.c.clone(),
1405 );
1406 Ok(state)
1407 }
1408 }
1409}
1410
1411pub fn verify_event_said(event: &Event) -> Result<(), ValidationError> {
1416 let value =
1417 serde_json::to_value(event).map_err(|e| ValidationError::Serialization(e.to_string()))?;
1418 let computed =
1419 compute_said(&value).map_err(|e| ValidationError::Serialization(e.to_string()))?;
1420 let actual = event.said();
1421
1422 if computed != *actual {
1423 return Err(ValidationError::InvalidSaid {
1424 expected: computed,
1425 actual: actual.clone(),
1426 });
1427 }
1428
1429 Ok(())
1430}
1431
1432pub fn validate_for_append(event: &Event, state: &KeyState) -> Result<(), ValidationError> {
1438 if matches!(event, Event::Icp(_)) {
1439 return Err(ValidationError::MultipleInceptions);
1440 }
1441
1442 verify_event_said(event)?;
1443 verify_sequence(event, state.sequence + 1)?;
1444 verify_chain_linkage(event, state)?;
1445 verify_event_crypto(event, Some(state))?;
1446
1447 Ok(())
1448}
1449
1450pub fn compute_event_said(event: &Event) -> Result<Said, ValidationError> {
1455 let value =
1456 serde_json::to_value(event).map_err(|e| ValidationError::Serialization(e.to_string()))?;
1457 compute_said(&value).map_err(|e| ValidationError::Serialization(e.to_string()))
1458}
1459
1460pub fn serialize_for_signing(event: &Event) -> Result<Vec<u8>, ValidationError> {
1472 serde_json::to_vec(event).map_err(|e| ValidationError::Serialization(e.to_string()))
1473}
1474
1475pub fn validate_signed_event(
1483 signed: &crate::events::SignedEvent,
1484 current_state: Option<&KeyState>,
1485) -> Result<(), ValidationError> {
1486 let event = &signed.event;
1487 let sequence = event.sequence().value();
1488
1489 if signed.signatures.is_empty() {
1490 return Err(ValidationError::SignatureFailed { sequence });
1491 }
1492
1493 let (keys, threshold) = match event {
1495 Event::Icp(icp) => (&icp.k, &icp.kt),
1496 Event::Dip(dip) => (&dip.k, &dip.kt),
1497 Event::Rot(rot) => (&rot.k, &rot.kt),
1498 Event::Drt(drt) => (&drt.k, &drt.kt),
1499 Event::Ixn(_) => {
1500 let state = current_state.ok_or(ValidationError::SignatureFailed { sequence })?;
1501 (&state.current_keys, &state.threshold)
1502 }
1503 };
1504
1505 if keys.is_empty() {
1506 return Err(ValidationError::SignatureFailed { sequence });
1507 }
1508
1509 let canonical = serialize_for_signing(event)?;
1511 let mut verified_indices = Vec::new();
1512
1513 for sig in &signed.signatures {
1514 let idx = sig.index as usize;
1515 if idx >= keys.len() {
1516 continue; }
1518 let key = &keys[idx];
1519 if let Ok(pk) = key.parse()
1520 && pk.verify_signature(&canonical, &sig.sig).is_ok()
1521 {
1522 verified_indices.push(sig.index);
1523 }
1524 }
1525
1526 if !threshold.is_satisfied(&verified_indices, keys.len()) {
1528 return Err(ValidationError::SignatureFailed { sequence });
1529 }
1530
1531 if matches!(event, Event::Rot(_) | Event::Drt(_))
1535 && let Some(state) = current_state
1536 {
1537 let n_len = state.next_commitment.len();
1538
1539 let mut verified_prior: Vec<u32> = Vec::new();
1544 for sig in &signed.signatures {
1545 let Some(key) = keys.get(sig.index as usize) else {
1546 continue;
1547 };
1548 let Ok(pk) = key.parse() else {
1549 continue;
1550 };
1551 if pk.verify_signature(&canonical, &sig.sig).is_err() {
1552 continue;
1553 }
1554 let j = sig.prior_index.unwrap_or(sig.index) as usize;
1555 let Some(commitment) = state.next_commitment.get(j) else {
1556 continue;
1557 };
1558 if crate::crypto::verify_commitment(&pk, commitment) {
1559 verified_prior.push(j as u32);
1560 }
1561 }
1562
1563 if n_len != keys.len() && verified_prior.is_empty() {
1568 return Err(ValidationError::AsymmetricKeyRotation {
1569 sequence,
1570 prior_next_count: n_len,
1571 new_key_count: keys.len(),
1572 });
1573 }
1574
1575 if !state.next_threshold.is_satisfied(&verified_prior, n_len) {
1576 return Err(ValidationError::SignatureFailed { sequence });
1577 }
1578 }
1579
1580 Ok(())
1581}
1582
1583pub fn finalize_icp_event(mut icp: IcpEvent) -> Result<IcpEvent, ValidationError> {
1588 let value = serde_json::to_value(Event::Icp(icp.clone()))
1589 .map_err(|e| ValidationError::Serialization(e.to_string()))?;
1590 let said = compute_said(&value).map_err(|e| ValidationError::Serialization(e.to_string()))?;
1591
1592 icp.d = said.clone();
1593 if icp.i.is_empty() || icp.i.as_str().starts_with('E') {
1595 icp.i = Prefix::new_unchecked(said.into_inner());
1596 }
1597
1598 let final_bytes = serde_json::to_vec(&Event::Icp(icp.clone()))
1600 .map_err(|e| ValidationError::Serialization(e.to_string()))?;
1601 icp.v = crate::types::VersionString::json(final_bytes.len() as u32);
1602
1603 Ok(icp)
1604}
1605
1606pub fn finalize_dip_event(
1614 mut dip: crate::events::DipEvent,
1615) -> Result<crate::events::DipEvent, ValidationError> {
1616 let value = serde_json::to_value(Event::Dip(dip.clone()))
1617 .map_err(|e| ValidationError::Serialization(e.to_string()))?;
1618 let said = compute_said(&value).map_err(|e| ValidationError::Serialization(e.to_string()))?;
1619
1620 dip.d = said.clone();
1621 if dip.i.is_empty() || dip.i.as_str().starts_with('E') {
1623 dip.i = Prefix::new_unchecked(said.into_inner());
1624 }
1625
1626 let final_bytes = serde_json::to_vec(&Event::Dip(dip.clone()))
1627 .map_err(|e| ValidationError::Serialization(e.to_string()))?;
1628 dip.v = crate::types::VersionString::json(final_bytes.len() as u32);
1629
1630 Ok(dip)
1631}
1632
1633pub fn finalize_rot_event(mut rot: RotEvent) -> Result<RotEvent, ValidationError> {
1638 let value = serde_json::to_value(Event::Rot(rot.clone()))
1639 .map_err(|e| ValidationError::Serialization(e.to_string()))?;
1640 let said = compute_said(&value).map_err(|e| ValidationError::Serialization(e.to_string()))?;
1641 rot.d = said;
1642
1643 let final_bytes = serde_json::to_vec(&Event::Rot(rot.clone()))
1644 .map_err(|e| ValidationError::Serialization(e.to_string()))?;
1645 rot.v = crate::types::VersionString::json(final_bytes.len() as u32);
1646
1647 Ok(rot)
1648}
1649
1650pub fn finalize_drt_event(
1659 mut drt: crate::events::DrtEvent,
1660) -> Result<crate::events::DrtEvent, ValidationError> {
1661 let value = serde_json::to_value(Event::Drt(drt.clone()))
1662 .map_err(|e| ValidationError::Serialization(e.to_string()))?;
1663 let said = compute_said(&value).map_err(|e| ValidationError::Serialization(e.to_string()))?;
1664 drt.d = said;
1665
1666 let final_bytes = serde_json::to_vec(&Event::Drt(drt.clone()))
1667 .map_err(|e| ValidationError::Serialization(e.to_string()))?;
1668 drt.v = crate::types::VersionString::json(final_bytes.len() as u32);
1669
1670 Ok(drt)
1671}
1672
1673pub fn finalize_ixn_event(mut ixn: IxnEvent) -> Result<IxnEvent, ValidationError> {
1678 let value = serde_json::to_value(Event::Ixn(ixn.clone()))
1679 .map_err(|e| ValidationError::Serialization(e.to_string()))?;
1680 let said = compute_said(&value).map_err(|e| ValidationError::Serialization(e.to_string()))?;
1681 ixn.d = said;
1682
1683 let final_bytes = serde_json::to_vec(&Event::Ixn(ixn.clone()))
1684 .map_err(|e| ValidationError::Serialization(e.to_string()))?;
1685 ixn.v = crate::types::VersionString::json(final_bytes.len() as u32);
1686
1687 Ok(ixn)
1688}
1689
1690pub fn find_seal_in_kel(events: &[Event], digest: &str) -> Option<u128> {
1698 for event in events {
1699 if let Event::Ixn(ixn) = event {
1700 for seal in &ixn.a {
1701 if seal.digest_value().is_some_and(|d| d.as_str() == digest) {
1702 return Some(ixn.s.value());
1703 }
1704 }
1705 }
1706 }
1707 None
1708}
1709
1710pub fn parse_kel_json(json: &str) -> Result<Vec<Event>, ValidationError> {
1715 serde_json::from_str(json).map_err(|e| ValidationError::Serialization(e.to_string()))
1716}
1717
1718#[cfg(test)]
1719#[allow(clippy::unwrap_used, clippy::expect_used)]
1720mod tests {
1721 use super::*;
1722 use crate::events::{IndexedSignature, KeriSequence, Seal, SignedEvent};
1723 use crate::types::{CesrKey, Threshold, VersionString};
1724 use ring::rand::SystemRandom;
1725 use ring::signature::{Ed25519KeyPair, KeyPair};
1726
1727 fn gen_keypair() -> Ed25519KeyPair {
1728 let rng = SystemRandom::new();
1729 let pkcs8 = Ed25519KeyPair::generate_pkcs8(&rng).unwrap();
1730 Ed25519KeyPair::from_pkcs8(pkcs8.as_ref()).unwrap()
1731 }
1732
1733 fn encode_pubkey(kp: &Ed25519KeyPair) -> String {
1734 crate::cesr_encode::encode_verkey(kp.public_key().as_ref(), cesride::matter::Codex::Ed25519)
1735 .unwrap()
1736 }
1737
1738 fn make_raw_icp(key: &str, next: &str) -> IcpEvent {
1739 IcpEvent {
1740 v: VersionString::placeholder(),
1741 d: Said::default(),
1742 i: Prefix::default(),
1743 s: KeriSequence::new(0),
1744 kt: Threshold::Simple(1),
1745 k: vec![CesrKey::new_unchecked(key.to_string())],
1746 nt: Threshold::Simple(1),
1747 n: vec![Said::new_unchecked(next.to_string())],
1748 bt: Threshold::Simple(0),
1749 b: vec![],
1750 c: vec![],
1751 a: vec![],
1752 }
1753 }
1754
1755 fn make_signed_icp() -> (IcpEvent, Ed25519KeyPair) {
1756 let rng = SystemRandom::new();
1757 let pkcs8 = Ed25519KeyPair::generate_pkcs8(&rng).unwrap();
1758 let keypair = Ed25519KeyPair::from_pkcs8(pkcs8.as_ref()).unwrap();
1759 let key_encoded = encode_pubkey(&keypair);
1760
1761 let icp = IcpEvent {
1762 v: VersionString::placeholder(),
1763 d: Said::default(),
1764 i: Prefix::default(),
1765 s: KeriSequence::new(0),
1766 kt: Threshold::Simple(1),
1767 k: vec![CesrKey::new_unchecked(key_encoded)],
1768 nt: Threshold::Simple(1),
1769 n: vec![Said::new_unchecked("ENextCommitment".to_string())],
1770 bt: Threshold::Simple(0),
1771 b: vec![],
1772 c: vec![],
1773 a: vec![],
1774 };
1775
1776 let finalized = finalize_icp_event(icp).unwrap();
1777 (finalized, keypair)
1778 }
1779
1780 fn make_signed_ixn(
1781 prefix: &Prefix,
1782 prev_said: &Said,
1783 seq: u128,
1784 _keypair: &Ed25519KeyPair,
1785 ) -> IxnEvent {
1786 let mut ixn = IxnEvent {
1787 v: VersionString::placeholder(),
1788 d: Said::default(),
1789 i: prefix.clone(),
1790 s: KeriSequence::new(seq),
1791 p: prev_said.clone(),
1792 a: vec![Seal::digest("EAttest")],
1793 };
1794
1795 let value = serde_json::to_value(Event::Ixn(ixn.clone())).unwrap();
1796 ixn.d = compute_said(&value).unwrap();
1797
1798 ixn
1799 }
1800
1801 #[test]
1802 fn finalize_icp_sets_said() {
1803 let icp = make_raw_icp("DKey1", "ENext1");
1804 let finalized = finalize_icp_event(icp).unwrap();
1805
1806 assert!(!finalized.d.is_empty());
1807 assert_eq!(finalized.d.as_str(), finalized.i.as_str());
1808 assert!(finalized.d.as_str().starts_with('E'));
1809 }
1810
1811 #[test]
1812 fn validates_single_inception() {
1813 let (icp, _keypair) = make_signed_icp();
1814 let events = vec![Event::Icp(icp.clone())];
1815
1816 let state = validate_kel(&events).unwrap();
1817 assert_eq!(state.prefix, icp.i);
1818 assert_eq!(state.sequence, 0);
1819 }
1820
1821 #[test]
1822 fn rejects_empty_kel() {
1823 let result = validate_kel(&[]);
1824 assert!(matches!(result, Err(ValidationError::EmptyKel)));
1825 }
1826
1827 #[test]
1828 fn rejects_non_inception_first() {
1829 let mut ixn = IxnEvent {
1830 v: VersionString::placeholder(),
1831 d: Said::default(),
1832 i: Prefix::new_unchecked("ETest".to_string()),
1833 s: KeriSequence::new(0),
1834 p: Said::new_unchecked("EPrev".to_string()),
1835 a: vec![],
1836 };
1837 let event = Event::Ixn(ixn.clone());
1840 if let Ok(said) = compute_event_said(&event) {
1841 ixn.d = said;
1842 }
1843 let events = vec![Event::Ixn(ixn)];
1844 let result = validate_kel(&events);
1845 assert!(matches!(result, Err(ValidationError::NotInception)));
1846 }
1847
1848 #[test]
1849 fn rejects_broken_sequence() {
1850 let (icp, _keypair) = make_signed_icp();
1851
1852 let mut ixn = IxnEvent {
1853 v: VersionString::placeholder(),
1854 d: Said::default(),
1855 i: icp.i.clone(),
1856 s: KeriSequence::new(5),
1857 p: icp.d.clone(),
1858 a: vec![],
1859 };
1860
1861 let value = serde_json::to_value(Event::Ixn(ixn.clone())).unwrap();
1862 ixn.d = compute_said(&value).unwrap();
1863
1864 let events = vec![Event::Icp(icp), Event::Ixn(ixn)];
1865 let result = validate_kel(&events);
1866 assert!(matches!(
1867 result,
1868 Err(ValidationError::InvalidSequence {
1869 expected: 1,
1870 actual: 5
1871 })
1872 ));
1873 }
1874
1875 #[test]
1876 fn rejects_broken_chain() {
1877 let (icp, _keypair) = make_signed_icp();
1878
1879 let mut ixn = IxnEvent {
1880 v: VersionString::placeholder(),
1881 d: Said::default(),
1882 i: icp.i.clone(),
1883 s: KeriSequence::new(1),
1884 p: Said::new_unchecked("EWrongPrevious".to_string()),
1885 a: vec![],
1886 };
1887
1888 let value = serde_json::to_value(Event::Ixn(ixn.clone())).unwrap();
1889 ixn.d = compute_said(&value).unwrap();
1890
1891 let events = vec![Event::Icp(icp), Event::Ixn(ixn)];
1892 let result = validate_kel(&events);
1893 assert!(matches!(result, Err(ValidationError::BrokenChain { .. })));
1894 }
1895
1896 #[test]
1897 fn rejects_invalid_said() {
1898 let icp = make_raw_icp("DKey1", "ENext1");
1899 let finalized = finalize_icp_event(icp).unwrap();
1900
1901 let mut tampered = finalized.clone();
1902 tampered.d = Said::new_unchecked("EWrongSaid".to_string());
1903
1904 let events = vec![Event::Icp(tampered)];
1905 let result = validate_kel(&events);
1906 assert!(matches!(result, Err(ValidationError::InvalidSaid { .. })));
1907 }
1908
1909 #[test]
1916 fn rejects_forged_inception_prefix_mismatch() {
1917 let (icp, _kp) = make_signed_icp();
1921 assert_eq!(
1922 icp.i.as_str(),
1923 icp.d.as_str(),
1924 "a finalized inception is self-addressing"
1925 );
1926
1927 let (other, _kp2) = make_signed_icp();
1928 assert_ne!(other.i.as_str(), icp.d.as_str());
1929
1930 let mut forged = icp;
1931 forged.i = other.i;
1932 let result = validate_kel(&[Event::Icp(forged)]);
1933 assert!(
1934 matches!(result, Err(ValidationError::InvalidSaid { .. })),
1935 "forged inception (i != d) must be rejected, got {result:?}"
1936 );
1937 }
1938
1939 #[test]
1940 fn rejects_forged_inception_basic_derivation() {
1941 let prefix_key = encode_pubkey(&gen_keypair());
1945 let committed_key = encode_pubkey(&gen_keypair());
1946 assert_ne!(prefix_key, committed_key);
1947 assert!(!prefix_key.starts_with('E'));
1948
1949 let mut icp = make_raw_icp(&committed_key, "ENext1");
1950 icp.i = Prefix::new_unchecked(prefix_key);
1951 let value = serde_json::to_value(Event::Icp(icp.clone())).unwrap();
1954 icp.d = compute_said(&value).unwrap();
1955
1956 let result = validate_kel(&[Event::Icp(icp)]);
1957 assert!(
1958 matches!(result, Err(ValidationError::InvalidSaid { .. })),
1959 "basic-derivation inception with i != k[0] must be rejected, got {result:?}"
1960 );
1961 }
1962
1963 fn sign_event(event: &Event, kp: &Ed25519KeyPair) -> SignedEvent {
1976 let sig = kp
1977 .sign(&serialize_for_signing(event).unwrap())
1978 .as_ref()
1979 .to_vec();
1980 SignedEvent::new(
1981 event.clone(),
1982 vec![IndexedSignature {
1983 index: 0,
1984 prior_index: None,
1985 sig,
1986 }],
1987 )
1988 }
1989
1990 #[test]
1991 fn validate_signed_kel_accepts_correctly_signed_kel() {
1992 let (icp, kp) = make_signed_icp();
1993 let signed_icp = sign_event(&Event::Icp(icp.clone()), &kp);
1994 let ixn = make_signed_ixn(&icp.i, &icp.d, 1, &kp);
1995 let signed_ixn = sign_event(&Event::Ixn(ixn), &kp);
1996
1997 let state = validate_signed_kel(&[signed_icp, signed_ixn], None)
1998 .expect("a correctly-signed KEL must validate");
1999 assert_eq!(state.sequence, 1);
2000 }
2001
2002 #[test]
2003 fn validate_signed_kel_rejects_unsigned_ixn() {
2004 let (icp, kp) = make_signed_icp();
2007 let signed_icp = sign_event(&Event::Icp(icp.clone()), &kp);
2008 let ixn = make_signed_ixn(&icp.i, &icp.d, 1, &kp);
2009 let unsigned_ixn = SignedEvent::new(Event::Ixn(ixn), vec![]);
2010
2011 let result = validate_signed_kel(&[signed_icp, unsigned_ixn], None);
2012 assert!(
2013 matches!(result, Err(ValidationError::SignatureFailed { .. })),
2014 "unsigned ixn must be rejected, got {result:?}"
2015 );
2016 }
2017
2018 #[test]
2019 fn validate_signed_kel_rejects_wrong_signer_ixn() {
2020 let (icp, kp) = make_signed_icp();
2023 let signed_icp = sign_event(&Event::Icp(icp.clone()), &kp);
2024 let ixn = make_signed_ixn(&icp.i, &icp.d, 1, &kp);
2025 let attacker = gen_keypair();
2026 let forged_ixn = sign_event(&Event::Ixn(ixn), &attacker);
2027
2028 let result = validate_signed_kel(&[signed_icp, forged_ixn], None);
2029 assert!(
2030 matches!(result, Err(ValidationError::SignatureFailed { .. })),
2031 "wrong-signer ixn must be rejected, got {result:?}"
2032 );
2033 }
2034
2035 #[test]
2036 fn validates_icp_then_ixn() {
2037 let (icp, keypair) = make_signed_icp();
2038 let ixn = make_signed_ixn(&icp.i, &icp.d, 1, &keypair);
2039
2040 let events = vec![Event::Icp(icp), Event::Ixn(ixn.clone())];
2041 let state = validate_kel(&events).unwrap();
2042 assert_eq!(state.sequence, 1);
2043 assert_eq!(state.last_event_said, ixn.d);
2044 }
2045
2046 #[test]
2047 fn compute_event_said_works() {
2048 let icp = make_raw_icp("DKey1", "ENext1");
2049 let event = Event::Icp(icp);
2050 let said = compute_event_said(&event).unwrap();
2051 assert!(said.as_str().starts_with('E'));
2052 assert!(!said.is_empty());
2053 }
2054
2055 #[test]
2059 fn accepts_correct_signature() {
2060 let (icp, keypair) = make_signed_icp();
2061 let event = Event::Icp(icp);
2062 let canonical = serialize_for_signing(&event).unwrap();
2063 let sig = keypair.sign(&canonical).as_ref().to_vec();
2064 let signed = SignedEvent::new(
2065 event,
2066 vec![IndexedSignature {
2067 index: 0,
2068 prior_index: None,
2069 sig,
2070 }],
2071 );
2072
2073 validate_signed_event(&signed, None).expect("correct signature must validate");
2074 }
2075
2076 #[test]
2082 fn rejects_forged_signature() {
2083 let (icp, _keypair) = make_signed_icp();
2084 let event = Event::Icp(icp);
2085 let forged_sig = vec![0u8; 64]; let signed = SignedEvent::new(
2087 event,
2088 vec![IndexedSignature {
2089 index: 0,
2090 prior_index: None,
2091 sig: forged_sig,
2092 }],
2093 );
2094
2095 assert!(matches!(
2096 validate_signed_event(&signed, None),
2097 Err(ValidationError::SignatureFailed { sequence: 0 })
2098 ));
2099 }
2100
2101 #[test]
2110 fn rejects_wrong_key_signature() {
2111 let committed = gen_keypair();
2112 let key_encoded = encode_pubkey(&committed);
2113
2114 let icp = IcpEvent {
2115 v: VersionString::placeholder(),
2116 d: Said::default(),
2117 i: Prefix::default(),
2118 s: KeriSequence::new(0),
2119 kt: Threshold::Simple(1),
2120 k: vec![CesrKey::new_unchecked(key_encoded)],
2121 nt: Threshold::Simple(1),
2122 n: vec![Said::new_unchecked("ENextCommitment".to_string())],
2123 bt: Threshold::Simple(0),
2124 b: vec![],
2125 c: vec![],
2126 a: vec![],
2127 };
2128 let icp = finalize_icp_event(icp).unwrap();
2129 let event = Event::Icp(icp);
2130
2131 let wrong = gen_keypair();
2132 let canonical = serialize_for_signing(&event).unwrap();
2133 let wrong_sig = wrong.sign(&canonical).as_ref().to_vec();
2134 let signed = SignedEvent::new(
2135 event,
2136 vec![IndexedSignature {
2137 index: 0,
2138 prior_index: None,
2139 sig: wrong_sig,
2140 }],
2141 );
2142
2143 assert!(matches!(
2144 validate_signed_event(&signed, None),
2145 Err(ValidationError::SignatureFailed { sequence: 0 })
2146 ));
2147 }
2148
2149 #[test]
2150 fn crypto_accepts_valid_inception() {
2151 let (icp, _keypair) = make_signed_icp();
2152 let result = verify_event_crypto(&Event::Icp(icp), None);
2153 assert!(result.is_ok());
2154 }
2155
2156 #[test]
2157 fn find_seal_in_kel_finds_digest() {
2158 let (icp, keypair) = make_signed_icp();
2159 let ixn = make_signed_ixn(&icp.i, &icp.d, 1, &keypair);
2160 let events = vec![Event::Icp(icp), Event::Ixn(ixn)];
2161 assert_eq!(find_seal_in_kel(&events, "EAttest"), Some(1));
2162 assert_eq!(find_seal_in_kel(&events, "ENonExistent"), None);
2163 }
2164
2165 #[test]
2166 fn parse_kel_json_rejects_invalid_hex_sequence() {
2167 let json = r#"[{"v":"KERI10JSON","t":"icp","i":"E123","s":"not_hex","kt":"1","k":["DKey"],"nt":"1","n":["ENext"],"bt":"0","b":[]}]"#;
2168 let result = parse_kel_json(json);
2169 assert!(result.is_err(), "expected error for invalid hex sequence");
2170 }
2171
2172 fn make_custom_signed_icp(customize: impl FnOnce(&mut IcpEvent)) -> (IcpEvent, Ed25519KeyPair) {
2175 let rng = SystemRandom::new();
2176 let pkcs8 = Ed25519KeyPair::generate_pkcs8(&rng).unwrap();
2177 let keypair = Ed25519KeyPair::from_pkcs8(pkcs8.as_ref()).unwrap();
2178 let key_encoded = encode_pubkey(&keypair);
2179
2180 let mut icp = IcpEvent {
2181 v: VersionString::placeholder(),
2182 d: Said::default(),
2183 i: Prefix::default(),
2184 s: KeriSequence::new(0),
2185 kt: Threshold::Simple(1),
2186 k: vec![CesrKey::new_unchecked(key_encoded)],
2187 nt: Threshold::Simple(1),
2188 n: vec![Said::new_unchecked("ENextCommitment".to_string())],
2189 bt: Threshold::Simple(0),
2190 b: vec![],
2191 c: vec![],
2192 a: vec![],
2193 };
2194
2195 customize(&mut icp);
2196
2197 let finalized = finalize_icp_event(icp).unwrap();
2198 (finalized, keypair)
2199 }
2200
2201 #[test]
2202 fn rejects_events_after_abandonment() {
2203 let kp2 = gen_keypair();
2205
2206 let commitment2 = crate::crypto::compute_next_commitment(
2208 &crate::keys::KeriPublicKey::ed25519(kp2.public_key().as_ref()).unwrap(),
2209 );
2210 let (icp, _kp1) = make_custom_signed_icp(|icp| {
2211 icp.n = vec![commitment2.clone()];
2212 });
2213 let prefix = icp.i.clone();
2214
2215 let mut rot = RotEvent {
2217 v: VersionString::placeholder(),
2218 d: Said::default(),
2219 i: prefix.clone(),
2220 s: KeriSequence::new(1),
2221 p: icp.d.clone(),
2222 kt: Threshold::Simple(1),
2223 k: vec![CesrKey::new_unchecked(encode_pubkey(&kp2))],
2224 nt: Threshold::Simple(0),
2225 n: vec![],
2226 bt: Threshold::Simple(0),
2227 br: vec![],
2228 ba: vec![],
2229 c: vec![],
2230 a: vec![],
2231 };
2232 let val = serde_json::to_value(Event::Rot(rot.clone())).unwrap();
2233 rot.d = compute_said(&val).unwrap();
2234
2235 let ixn = make_signed_ixn(&prefix, &rot.d, 2, &kp2);
2236 let events = vec![Event::Icp(icp), Event::Rot(rot), Event::Ixn(ixn)];
2237 let result = validate_kel(&events);
2238 assert!(
2239 matches!(result, Err(ValidationError::AbandonedIdentity { .. })),
2240 "expected AbandonedIdentity, got: {result:?}"
2241 );
2242 }
2243
2244 #[test]
2245 fn rejects_ixn_in_establishment_only_kel() {
2246 let (icp, keypair) = make_custom_signed_icp(|icp| {
2247 icp.c = vec![ConfigTrait::EstablishmentOnly];
2248 });
2249 let ixn = make_signed_ixn(&icp.i, &icp.d, 1, &keypair);
2250 let events = vec![Event::Icp(icp), Event::Ixn(ixn)];
2251 let result = validate_kel(&events);
2252 assert!(
2253 matches!(result, Err(ValidationError::EstablishmentOnly { .. })),
2254 "expected EstablishmentOnly, got: {result:?}"
2255 );
2256 }
2257
2258 #[test]
2259 fn rejects_events_after_non_transferable_inception() {
2260 let (icp, keypair) = make_custom_signed_icp(|icp| {
2261 icp.n = vec![];
2262 icp.nt = Threshold::Simple(0);
2263 });
2264 let ixn = make_signed_ixn(&icp.i, &icp.d, 1, &keypair);
2265 let events = vec![Event::Icp(icp), Event::Ixn(ixn)];
2266 let result = validate_kel(&events);
2267 assert!(
2268 matches!(
2269 result,
2270 Err(ValidationError::NonTransferable)
2271 | Err(ValidationError::AbandonedIdentity { .. })
2272 ),
2273 "expected NonTransferable or AbandonedIdentity, got: {result:?}"
2274 );
2275 }
2276
2277 #[test]
2278 fn rejects_duplicate_backers() {
2279 let (_, result) = {
2280 let rng = SystemRandom::new();
2281 let pkcs8 = Ed25519KeyPair::generate_pkcs8(&rng).unwrap();
2282 let keypair = Ed25519KeyPair::from_pkcs8(pkcs8.as_ref()).unwrap();
2283 let key_encoded = encode_pubkey(&keypair);
2284
2285 let dup_backer = Prefix::new_unchecked("DWit1".to_string());
2286 let icp = IcpEvent {
2287 v: VersionString::placeholder(),
2288 d: Said::default(),
2289 i: Prefix::default(),
2290 s: KeriSequence::new(0),
2291 kt: Threshold::Simple(1),
2292 k: vec![CesrKey::new_unchecked(key_encoded)],
2293 nt: Threshold::Simple(1),
2294 n: vec![Said::new_unchecked("ENextCommitment".to_string())],
2295 bt: Threshold::Simple(2),
2296 b: vec![dup_backer.clone(), dup_backer],
2297 c: vec![],
2298 a: vec![],
2299 };
2300
2301 let finalized = finalize_icp_event(icp).unwrap();
2302 let events = vec![Event::Icp(finalized)];
2303 (keypair, validate_kel(&events))
2304 };
2305 assert!(
2306 matches!(result, Err(ValidationError::DuplicateBacker { .. })),
2307 "expected DuplicateBacker, got: {result:?}"
2308 );
2309 }
2310
2311 #[test]
2312 fn rejects_invalid_backer_threshold() {
2313 let (_, result) = {
2314 let rng = SystemRandom::new();
2315 let pkcs8 = Ed25519KeyPair::generate_pkcs8(&rng).unwrap();
2316 let keypair = Ed25519KeyPair::from_pkcs8(pkcs8.as_ref()).unwrap();
2317 let key_encoded = encode_pubkey(&keypair);
2318
2319 let icp = IcpEvent {
2320 v: VersionString::placeholder(),
2321 d: Said::default(),
2322 i: Prefix::default(),
2323 s: KeriSequence::new(0),
2324 kt: Threshold::Simple(1),
2325 k: vec![CesrKey::new_unchecked(key_encoded)],
2326 nt: Threshold::Simple(1),
2327 n: vec![Said::new_unchecked("ENextCommitment".to_string())],
2328 bt: Threshold::Simple(2),
2329 b: vec![],
2330 c: vec![],
2331 a: vec![],
2332 };
2333
2334 let finalized = finalize_icp_event(icp).unwrap();
2335 let events = vec![Event::Icp(finalized)];
2336 (keypair, validate_kel(&events))
2337 };
2338 assert!(
2342 matches!(result, Err(ValidationError::ThresholdNotSatisfiable { .. })),
2343 "expected ThresholdNotSatisfiable, got: {result:?}"
2344 );
2345 }
2346
2347 #[test]
2348 fn sign_over_finalized_bytes_roundtrips() {
2349 let (icp, _kp) = make_signed_icp();
2353 let bytes = serialize_for_signing(&Event::Icp(icp.clone())).unwrap();
2354 assert_eq!(
2355 bytes.len() as u32,
2356 icp.v.size,
2357 "signed byte length must equal the version-string size field"
2358 );
2359 let reparsed: Event = serde_json::from_slice(&bytes).unwrap();
2360 assert!(reparsed.is_inception());
2361 }
2362
2363 #[test]
2364 fn threshold_rejects_kt_gt_k() {
2365 let kp = gen_keypair();
2368 let key = encode_pubkey(&kp);
2369 let icp = IcpEvent {
2370 v: VersionString::placeholder(),
2371 d: Said::default(),
2372 i: Prefix::default(),
2373 s: KeriSequence::new(0),
2374 kt: Threshold::Simple(5),
2375 k: vec![CesrKey::new_unchecked(key)],
2376 nt: Threshold::Simple(1),
2377 n: vec![Said::new_unchecked("ENextCommitment".to_string())],
2378 bt: Threshold::Simple(0),
2379 b: vec![],
2380 c: vec![],
2381 a: vec![],
2382 };
2383 let finalized = finalize_icp_event(icp).unwrap();
2384 let result = validate_kel(&[Event::Icp(finalized)]);
2385 assert!(
2386 matches!(result, Err(ValidationError::ThresholdNotSatisfiable { .. })),
2387 "expected ThresholdNotSatisfiable, got: {result:?}"
2388 );
2389 }
2390
2391 #[test]
2392 fn rotation_rejects_br_not_in_prior() {
2393 let state = KeyState::from_inception(
2397 Prefix::new_unchecked("EPrefix".to_string()),
2398 vec![CesrKey::new_unchecked("DKey1".to_string())],
2399 vec![], Threshold::Simple(1),
2401 Threshold::Simple(0),
2402 Said::new_unchecked("ESAID".to_string()),
2403 vec![Prefix::new_unchecked("BWit1".to_string())],
2404 Threshold::Simple(0),
2405 vec![],
2406 );
2407
2408 let make_rot = |br: Vec<Prefix>, ba: Vec<Prefix>| RotEvent {
2409 v: VersionString::placeholder(),
2410 d: Said::default(),
2411 i: Prefix::new_unchecked("EPrefix".to_string()),
2412 s: KeriSequence::new(1),
2413 p: Said::new_unchecked("ESAID".to_string()),
2414 kt: Threshold::Simple(1),
2415 k: vec![CesrKey::new_unchecked("DKey2".to_string())],
2416 nt: Threshold::Simple(0),
2417 n: vec![],
2418 bt: Threshold::Simple(0),
2419 br,
2420 ba,
2421 c: vec![],
2422 a: vec![],
2423 };
2424
2425 let bad_cut = make_rot(vec![Prefix::new_unchecked("BWitX".to_string())], vec![]);
2427 assert!(matches!(
2428 validate_rotation(&bad_cut, 1, &mut state.clone()),
2429 Err(ValidationError::InvalidBackerDelta { .. })
2430 ));
2431
2432 let bad_add = make_rot(vec![], vec![Prefix::new_unchecked("BWit1".to_string())]);
2434 assert!(matches!(
2435 validate_rotation(&bad_add, 1, &mut state.clone()),
2436 Err(ValidationError::InvalidBackerDelta { .. })
2437 ));
2438
2439 let ok = make_rot(vec![Prefix::new_unchecked("BWit1".to_string())], vec![]);
2441 assert!(validate_rotation(&ok, 1, &mut state.clone()).is_ok());
2442 }
2443
2444 #[test]
2445 fn rotation_rejects_silent_backer_role_flip() {
2446 let nrb_state = || {
2450 KeyState::from_inception(
2451 Prefix::new_unchecked("EPrefix".to_string()),
2452 vec![CesrKey::new_unchecked("DKey1".to_string())],
2453 vec![],
2454 Threshold::Simple(1),
2455 Threshold::Simple(0),
2456 Said::new_unchecked("ESAID".to_string()),
2457 vec![Prefix::new_unchecked("BWit1".to_string())],
2458 Threshold::Simple(0),
2459 vec![ConfigTrait::NoRegistrarBackers],
2460 )
2461 };
2462
2463 let make_rot = |br: Vec<Prefix>, ba: Vec<Prefix>, c: Vec<ConfigTrait>| RotEvent {
2464 v: VersionString::placeholder(),
2465 d: Said::default(),
2466 i: Prefix::new_unchecked("EPrefix".to_string()),
2467 s: KeriSequence::new(1),
2468 p: Said::new_unchecked("ESAID".to_string()),
2469 kt: Threshold::Simple(1),
2470 k: vec![CesrKey::new_unchecked("DKey2".to_string())],
2471 nt: Threshold::Simple(0),
2472 n: vec![],
2473 bt: Threshold::Simple(0),
2474 br,
2475 ba,
2476 c,
2477 a: vec![],
2478 };
2479
2480 let flip_keep = make_rot(vec![], vec![], vec![ConfigTrait::RegistrarBackers]);
2482 assert!(matches!(
2483 validate_rotation(&flip_keep, 1, &mut nrb_state()),
2484 Err(ValidationError::BackerRoleFlip { .. })
2485 ));
2486
2487 let flip_rebuild = make_rot(
2489 vec![Prefix::new_unchecked("BWit1".to_string())],
2490 vec![],
2491 vec![ConfigTrait::RegistrarBackers],
2492 );
2493 assert!(validate_rotation(&flip_rebuild, 1, &mut nrb_state()).is_ok());
2494
2495 let same_role = make_rot(vec![], vec![], vec![ConfigTrait::NoRegistrarBackers]);
2497 assert!(validate_rotation(&same_role, 1, &mut nrb_state()).is_ok());
2498
2499 let inherit = make_rot(vec![], vec![], vec![]);
2501 assert!(validate_rotation(&inherit, 1, &mut nrb_state()).is_ok());
2502 }
2503
2504 use crate::witness::WitnessReceipt;
2507
2508 struct MapReceipts {
2510 by_said: std::collections::HashMap<String, Vec<WitnessReceipt>>,
2511 }
2512
2513 impl WitnessReceiptLookup for MapReceipts {
2514 fn receipts_for(
2515 &self,
2516 _controller: &Prefix,
2517 _sn: KeriSequence,
2518 said: &Said,
2519 ) -> Vec<WitnessReceipt> {
2520 self.by_said.get(said.as_str()).cloned().unwrap_or_default()
2521 }
2522 }
2523
2524 fn witness_aid(aid: &str) -> Prefix {
2525 Prefix::new_unchecked(aid.to_string())
2526 }
2527
2528 fn receipt_from(aid: &str) -> WitnessReceipt {
2529 WitnessReceipt {
2530 witness: witness_aid(aid),
2531 signature: vec![],
2532 }
2533 }
2534
2535 fn receipts_under(said: &Said, aids: &[&str]) -> MapReceipts {
2536 let mut by_said = std::collections::HashMap::new();
2537 by_said.insert(
2538 said.as_str().to_string(),
2539 aids.iter().map(|a| receipt_from(a)).collect(),
2540 );
2541 MapReceipts { by_said }
2542 }
2543
2544 fn icp_with_backers(aids: &[&str], bt: u64) -> IcpEvent {
2546 let backers: Vec<Prefix> = aids.iter().map(|a| witness_aid(a)).collect();
2547 let (icp, _kp) = make_custom_signed_icp(|icp| {
2548 icp.b = backers.clone();
2549 icp.bt = Threshold::Simple(bt);
2550 });
2551 icp
2552 }
2553
2554 #[test]
2555 fn replay_bt_zero_accepts_without_receipts() {
2556 let (icp, _kp) = make_signed_icp(); let events = vec![Event::Icp(icp)];
2558 let lookup = MapReceipts {
2559 by_said: std::collections::HashMap::new(),
2560 };
2561 let outcome = validate_kel_with_receipts(&events, None, &lookup).unwrap();
2562 assert!(matches!(outcome, WitnessedReplay::Accepted(_)));
2563 }
2564
2565 #[test]
2566 fn replay_at_quorum_accepts() {
2567 let icp = icp_with_backers(&["BWit1", "BWit2"], 2);
2568 let said = icp.d.clone();
2569 let lookup = receipts_under(&said, &["BWit1", "BWit2"]);
2570 let events = vec![Event::Icp(icp)];
2571 let outcome = validate_kel_with_receipts(&events, None, &lookup).unwrap();
2572 assert!(matches!(outcome, WitnessedReplay::Accepted(_)));
2573 }
2574
2575 #[test]
2576 fn replay_under_quorum_is_pending() {
2577 let icp = icp_with_backers(&["BWit1", "BWit2"], 2);
2578 let said = icp.d.clone();
2579 let lookup = receipts_under(&said, &["BWit1"]); let events = vec![Event::Icp(icp)];
2581 match validate_kel_with_receipts(&events, None, &lookup).unwrap() {
2582 WitnessedReplay::Pending {
2583 sequence,
2584 collected,
2585 ..
2586 } => {
2587 assert_eq!(sequence, 0);
2588 assert_eq!(collected, 1);
2589 }
2590 WitnessedReplay::Accepted(_) => panic!("expected Pending under quorum"),
2591 }
2592 }
2593
2594 #[test]
2595 fn replay_ignores_duplicate_witness_receipts() {
2596 let icp = icp_with_backers(&["BWit1", "BWit2", "BWit3"], 2);
2597 let said = icp.d.clone();
2598 let lookup = receipts_under(&said, &["BWit1", "BWit1"]); let events = vec![Event::Icp(icp)];
2600 assert!(matches!(
2601 validate_kel_with_receipts(&events, None, &lookup).unwrap(),
2602 WitnessedReplay::Pending { .. }
2603 ));
2604 }
2605
2606 #[test]
2607 fn replay_ignores_receipt_for_wrong_said() {
2608 let icp = icp_with_backers(&["BWit1", "BWit2"], 2);
2609 let wrong = Said::new_unchecked("EWrongEventSaid".to_string());
2611 let lookup = receipts_under(&wrong, &["BWit1", "BWit2"]);
2612 let events = vec![Event::Icp(icp)];
2613 match validate_kel_with_receipts(&events, None, &lookup).unwrap() {
2614 WitnessedReplay::Pending { collected, .. } => assert_eq!(collected, 0),
2615 WitnessedReplay::Accepted(_) => panic!("wrong-SAID receipts must not count"),
2616 }
2617 }
2618
2619 #[test]
2620 fn replay_uses_witness_set_in_force_at_seq() {
2621 let kp2 = gen_keypair();
2624 let kp3 = gen_keypair();
2625 let commitment2 = crate::crypto::compute_next_commitment(
2626 &crate::keys::KeriPublicKey::ed25519(kp2.public_key().as_ref()).unwrap(),
2627 );
2628 let commitment3 = crate::crypto::compute_next_commitment(
2629 &crate::keys::KeriPublicKey::ed25519(kp3.public_key().as_ref()).unwrap(),
2630 );
2631 let (icp, _kp1) = make_custom_signed_icp(|icp| {
2632 icp.b = vec![witness_aid("BWit1")];
2633 icp.bt = Threshold::Simple(1);
2634 icp.n = vec![commitment2.clone()];
2635 });
2636 let prefix = icp.i.clone();
2637 let icp_said = icp.d.clone();
2638
2639 let mut rot = RotEvent {
2640 v: VersionString::placeholder(),
2641 d: Said::default(),
2642 i: prefix.clone(),
2643 s: KeriSequence::new(1),
2644 p: icp_said.clone(),
2645 kt: Threshold::Simple(1),
2646 k: vec![CesrKey::new_unchecked(encode_pubkey(&kp2))],
2647 nt: Threshold::Simple(1),
2648 n: vec![commitment3.clone()],
2649 bt: Threshold::Simple(1),
2650 br: vec![witness_aid("BWit1")],
2651 ba: vec![witness_aid("BWit2")],
2652 c: vec![],
2653 a: vec![],
2654 };
2655 let val = serde_json::to_value(Event::Rot(rot.clone())).unwrap();
2656 rot.d = compute_said(&val).unwrap();
2657 let rot_said = rot.d.clone();
2658
2659 let mut by_said = std::collections::HashMap::new();
2660 by_said.insert(icp_said.as_str().to_string(), vec![receipt_from("BWit1")]);
2661 by_said.insert(rot_said.as_str().to_string(), vec![receipt_from("BWit2")]);
2662 let lookup = MapReceipts { by_said };
2663
2664 let events = vec![Event::Icp(icp), Event::Rot(rot)];
2665 assert!(matches!(
2668 validate_kel_with_receipts(&events, None, &lookup).unwrap(),
2669 WitnessedReplay::Accepted(_)
2670 ));
2671 }
2672
2673 #[test]
2674 fn validate_kel_advances_without_receipt_gate() {
2675 let icp = icp_with_backers(&["BWit1", "BWit2"], 2);
2677 let events = vec![Event::Icp(icp)];
2678 assert!(validate_kel(&events).is_ok());
2679 }
2680}
2681
2682#[derive(Debug, Clone)]
2693pub struct KelPolicy {
2694 pub min_rotation_interval: chrono::Duration,
2697 pub clock_skew_tolerance: chrono::Duration,
2700 pub emergency_override_did: Option<crate::types::Prefix>,
2705}
2706
2707impl Default for KelPolicy {
2708 fn default() -> Self {
2709 Self {
2710 min_rotation_interval: chrono::Duration::hours(24),
2711 clock_skew_tolerance: chrono::Duration::seconds(60),
2712 emergency_override_did: None,
2713 }
2714 }
2715}
2716
2717pub(crate) fn validate_kel_with_policy(
2741 events: &[Event],
2742 timestamps: &[Option<chrono::DateTime<chrono::Utc>>],
2743 policy: &KelPolicy,
2744 now: chrono::DateTime<chrono::Utc>,
2745) -> Result<KeyState, ValidationError> {
2746 let state = validate_kel(events)?;
2747
2748 let mut last_rotation_dt: Option<chrono::DateTime<chrono::Utc>> = None;
2749 let mut last_any_dt: Option<chrono::DateTime<chrono::Utc>> = None;
2750
2751 for (idx, evt) in events.iter().enumerate() {
2752 let seq = idx as u128;
2753 let (is_rotation, controller) = match evt {
2754 Event::Icp(e) => (false, &e.i),
2755 Event::Rot(e) => (true, &e.i),
2756 Event::Ixn(e) => (false, &e.i),
2757 Event::Dip(e) => (false, &e.i),
2758 Event::Drt(e) => (true, &e.i),
2759 };
2760 let Some(dt) = timestamps.get(idx).copied().flatten() else {
2761 return Err(ValidationError::MissingTimestamp { sequence: seq });
2762 };
2763 if let Some(prev) = last_any_dt
2765 && dt < prev
2766 {
2767 return Err(ValidationError::NonMonotonicTimestamp {
2768 sequence: seq,
2769 prev: prev.to_rfc3339(),
2770 curr: dt.to_rfc3339(),
2771 });
2772 }
2773 let skew = (dt - now).num_seconds();
2775 if skew.abs() > policy.clock_skew_tolerance.num_seconds() {
2776 return Err(ValidationError::ClockSkew {
2777 sequence: seq,
2778 skew_secs: skew,
2779 tolerance_secs: policy.clock_skew_tolerance.num_seconds(),
2780 });
2781 }
2782 if is_rotation && let Some(prev) = last_rotation_dt {
2784 let interval = dt - prev;
2785 let is_override = policy
2786 .emergency_override_did
2787 .as_ref()
2788 .is_some_and(|ov| ov == controller);
2789 if !is_override && interval < policy.min_rotation_interval {
2790 return Err(ValidationError::RotationCooldown {
2791 sequence: seq,
2792 interval_secs: interval.num_seconds(),
2793 min_secs: policy.min_rotation_interval.num_seconds(),
2794 });
2795 }
2796 }
2797 last_any_dt = Some(dt);
2798 if is_rotation {
2799 last_rotation_dt = Some(dt);
2800 }
2801 }
2802
2803 Ok(state)
2804}
2805
2806#[cfg(test)]
2807mod policy_tests {
2808 use super::*;
2809 use chrono::{Duration as ChronoDuration, TimeZone, Utc};
2810
2811 fn base_now() -> chrono::DateTime<chrono::Utc> {
2812 Utc.with_ymd_and_hms(2026, 1, 1, 0, 0, 0).unwrap()
2813 }
2814
2815 #[test]
2816 fn policy_rejects_missing_dt_via_empty_kel_path() {
2817 let events: Vec<crate::events::Event> = vec![];
2821 let r = validate_kel_with_policy(&events, &[], &KelPolicy::default(), base_now());
2822 assert!(matches!(r, Err(ValidationError::EmptyKel)));
2823 }
2824
2825 #[test]
2826 fn policy_default_values_match_plan() {
2827 let p = KelPolicy::default();
2828 assert_eq!(p.min_rotation_interval, ChronoDuration::hours(24));
2829 assert_eq!(p.clock_skew_tolerance, ChronoDuration::seconds(60));
2830 assert!(p.emergency_override_did.is_none());
2831 }
2832}