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 struct KelSealIndex {
391 seals: std::collections::HashMap<Said, SourceSeal>,
393}
394
395impl KelSealIndex {
396 pub fn from_events(events: &[Event]) -> Self {
404 let mut seals = std::collections::HashMap::new();
405 for event in events {
406 for seal in event.anchors() {
407 if let Seal::KeyEvent { d, .. } = seal {
408 seals.entry(d.clone()).or_insert_with(|| SourceSeal {
409 s: event.sequence(),
410 d: event.said().clone(),
411 });
412 }
413 }
414 }
415 Self { seals }
416 }
417}
418
419impl DelegatorKelLookup for KelSealIndex {
420 fn find_seal(&self, _delegator_aid: &Prefix, seal_said: &Said) -> Option<SourceSeal> {
421 self.seals.get(seal_said).cloned()
422 }
423}
424
425#[derive(Clone, Copy)]
442pub struct TrustedKel<'a>(&'a [Event]);
443
444impl<'a> TrustedKel<'a> {
445 pub fn from_trusted_source(events: &'a [Event]) -> Self {
454 Self(events)
455 }
456
457 pub fn events(&self) -> &'a [Event] {
459 self.0
460 }
461
462 pub fn replay(self) -> Result<KeyState, ValidationError> {
464 validate_kel(self.0)
465 }
466
467 pub fn replay_with_lookup(
470 self,
471 lookup: Option<&dyn DelegatorKelLookup>,
472 ) -> Result<KeyState, ValidationError> {
473 validate_kel_with_lookup(self.0, lookup)
474 }
475
476 pub fn replay_with_receipts(
478 self,
479 lookup: Option<&dyn DelegatorKelLookup>,
480 receipt_lookup: &dyn WitnessReceiptLookup,
481 ) -> Result<WitnessedReplay, ValidationError> {
482 validate_kel_with_receipts(self.0, lookup, receipt_lookup)
483 }
484
485 pub fn replay_with_policy(
488 self,
489 timestamps: &[Option<chrono::DateTime<chrono::Utc>>],
490 policy: &KelPolicy,
491 now: chrono::DateTime<chrono::Utc>,
492 ) -> Result<KeyState, ValidationError> {
493 validate_kel_with_policy(self.0, timestamps, policy, now)
494 }
495}
496
497pub(crate) fn validate_kel(events: &[Event]) -> Result<KeyState, ValidationError> {
507 validate_kel_with_lookup(events, None::<&dyn DelegatorKelLookup>)
508}
509
510pub(crate) fn validate_kel_with_lookup(
515 events: &[Event],
516 lookup: Option<&dyn DelegatorKelLookup>,
517) -> Result<KeyState, ValidationError> {
518 match replay_kel_gated(events, lookup, None)? {
519 WitnessedReplay::Accepted(state) => Ok(state),
520 WitnessedReplay::Pending { state, .. } => Ok(state),
524 }
525}
526
527#[derive(Debug, Clone, PartialEq, Eq)]
534pub enum WitnessedReplay {
535 Accepted(KeyState),
538 Pending {
543 state: KeyState,
545 sequence: u128,
547 said: Said,
549 required: Threshold,
551 collected: usize,
553 },
554}
555
556impl WitnessedReplay {
557 pub fn state(&self) -> &KeyState {
559 match self {
560 WitnessedReplay::Accepted(state) | WitnessedReplay::Pending { state, .. } => state,
561 }
562 }
563}
564
565pub(crate) fn validate_kel_with_receipts(
589 events: &[Event],
590 delegator_lookup: Option<&dyn DelegatorKelLookup>,
591 receipt_lookup: &dyn WitnessReceiptLookup,
592) -> Result<WitnessedReplay, ValidationError> {
593 replay_kel_gated(events, delegator_lookup, Some(receipt_lookup))
594}
595
596fn replay_kel_gated(
603 events: &[Event],
604 lookup: Option<&dyn DelegatorKelLookup>,
605 receipt_lookup: Option<&dyn WitnessReceiptLookup>,
606) -> Result<WitnessedReplay, ValidationError> {
607 if events.is_empty() {
608 return Err(ValidationError::EmptyKel);
609 }
610
611 verify_event_said(&events[0])?;
612 let (mut state, inception_n_is_empty, establishment_only) = match &events[0] {
613 Event::Icp(icp) => (
614 validate_inception(icp)?,
615 icp.n.is_empty(),
616 icp.c.contains(&ConfigTrait::EstablishmentOnly),
617 ),
618 Event::Dip(dip) => (
619 validate_delegated_inception(dip, lookup)?,
620 dip.n.is_empty(),
621 dip.c.contains(&ConfigTrait::EstablishmentOnly),
622 ),
623 _ => return Err(ValidationError::NotInception),
624 };
625
626 let controller = state.prefix.clone();
627
628 if let Some(rl) = receipt_lookup
630 && let Some(pending) = gate_establishment(&controller, &state, 0, events[0].said(), rl)
631 {
632 return Ok(pending);
633 }
634
635 if inception_n_is_empty && events.len() > 1 {
637 return Err(ValidationError::NonTransferable);
638 }
639
640 for (idx, event) in events.iter().enumerate().skip(1) {
641 let expected_seq = idx as u128;
642
643 if state.is_abandoned {
645 return Err(ValidationError::AbandonedIdentity {
646 sequence: expected_seq,
647 });
648 }
649
650 if establishment_only && matches!(event, Event::Ixn(_)) {
652 return Err(ValidationError::EstablishmentOnly {
653 sequence: expected_seq,
654 });
655 }
656
657 verify_event_said(event)?;
658 verify_sequence(event, expected_seq)?;
659 verify_chain_linkage(event, &state)?;
660
661 match event {
662 Event::Rot(rot) => validate_rotation(rot, expected_seq, &mut state)?,
663 Event::Ixn(ixn) => validate_interaction(ixn, expected_seq, &mut state)?,
664 Event::Icp(_) | Event::Dip(_) => return Err(ValidationError::MultipleInceptions),
665 Event::Drt(drt) => {
666 validate_delegated_rotation(drt, expected_seq, &mut state, lookup)?;
667 }
668 }
669
670 if let Some(rl) = receipt_lookup
672 && matches!(event, Event::Rot(_) | Event::Drt(_))
673 && let Some(pending) =
674 gate_establishment(&controller, &state, expected_seq, event.said(), rl)
675 {
676 return Ok(pending);
677 }
678 }
679
680 Ok(WitnessedReplay::Accepted(state))
681}
682
683pub fn validate_signed_kel(
707 events: &[crate::events::SignedEvent],
708 lookup: Option<&dyn DelegatorKelLookup>,
709) -> Result<KeyState, ValidationError> {
710 if events.is_empty() {
711 return Err(ValidationError::EmptyKel);
712 }
713
714 let first = &events[0];
717 verify_event_said(&first.event)?;
718 validate_signed_event(first, None)?;
719 let (mut state, inception_n_is_empty, establishment_only) = match &first.event {
720 Event::Icp(icp) => (
721 validate_inception(icp)?,
722 icp.n.is_empty(),
723 icp.c.contains(&ConfigTrait::EstablishmentOnly),
724 ),
725 Event::Dip(dip) => (
726 validate_delegated_inception(dip, lookup)?,
727 dip.n.is_empty(),
728 dip.c.contains(&ConfigTrait::EstablishmentOnly),
729 ),
730 _ => return Err(ValidationError::NotInception),
731 };
732
733 if inception_n_is_empty && events.len() > 1 {
734 return Err(ValidationError::NonTransferable);
735 }
736
737 for (idx, signed) in events.iter().enumerate().skip(1) {
738 let event = &signed.event;
739 let expected_seq = idx as u128;
740
741 if state.is_abandoned {
742 return Err(ValidationError::AbandonedIdentity {
743 sequence: expected_seq,
744 });
745 }
746 if establishment_only && matches!(event, Event::Ixn(_)) {
747 return Err(ValidationError::EstablishmentOnly {
748 sequence: expected_seq,
749 });
750 }
751
752 verify_event_said(event)?;
753 verify_sequence(event, expected_seq)?;
754 verify_chain_linkage(event, &state)?;
755 validate_signed_event(signed, Some(&state))?;
758
759 match event {
760 Event::Rot(rot) => validate_rotation(rot, expected_seq, &mut state)?,
761 Event::Ixn(ixn) => validate_interaction(ixn, expected_seq, &mut state)?,
762 Event::Icp(_) | Event::Dip(_) => return Err(ValidationError::MultipleInceptions),
763 Event::Drt(drt) => {
764 validate_delegated_rotation(drt, expected_seq, &mut state, lookup)?;
765 }
766 }
767 }
768
769 Ok(state)
770}
771
772fn gate_establishment(
779 controller: &Prefix,
780 state: &KeyState,
781 sequence: u128,
782 event_said: &Said,
783 receipt_lookup: &dyn WitnessReceiptLookup,
784) -> Option<WitnessedReplay> {
785 let sn = sequence as u64;
786 let agreement = WitnessAgreement::new(1);
787 agreement.submit_event(
788 controller,
789 sn,
790 event_said,
791 &state.backer_threshold,
792 &state.backers,
793 );
794 for receipt in receipt_lookup.receipts_for(controller, KeriSequence::new(sequence), event_said)
795 {
796 agreement.add_receipt(controller, sn, event_said, receipt.witness.as_str());
797 }
798 match agreement.status(controller, sn, event_said) {
799 AgreementStatus::Accepted => None,
800 AgreementStatus::Pending { collected } => Some(WitnessedReplay::Pending {
801 state: state.clone(),
802 sequence,
803 said: event_said.clone(),
804 required: state.backer_threshold.clone(),
805 collected,
806 }),
807 }
808}
809
810fn validate_backer_uniqueness(backers: &[Prefix]) -> Result<(), ValidationError> {
811 let mut seen = std::collections::HashSet::new();
812 for b in backers {
813 if !seen.insert(b.as_str()) {
814 return Err(ValidationError::DuplicateBacker {
815 aid: b.as_str().to_string(),
816 });
817 }
818 }
819 Ok(())
820}
821
822fn validate_thresholds(
825 sequence: u128,
826 kt: &Threshold,
827 k_len: usize,
828 nt: &Threshold,
829 n_len: usize,
830 bt: &Threshold,
831 b_len: usize,
832) -> Result<(), ValidationError> {
833 let check = |t: &Threshold, len: usize, which: &str| {
834 t.validate_satisfiable(len)
835 .map_err(|e| ValidationError::ThresholdNotSatisfiable {
836 sequence,
837 reason: format!("{which}: {}", e.reason),
838 })
839 };
840 check(kt, k_len, "kt")?;
841 check(nt, n_len, "nt")?;
842 check(bt, b_len, "bt")?;
843 Ok(())
844}
845
846fn verify_inception_self_cert(i: &Prefix, d: &Said, k: &[CesrKey]) -> Result<(), ValidationError> {
859 if k.is_empty() {
861 return Err(ValidationError::SignatureFailed { sequence: 0 });
862 }
863
864 if i.as_str().starts_with('E') {
865 if i.as_str() != d.as_str() {
866 return Err(ValidationError::InvalidSaid {
867 expected: d.clone(),
868 actual: Said::new_unchecked(i.as_str().to_string()),
869 });
870 }
871 } else {
872 let i_key = KeriPublicKey::parse(i.as_str())
875 .map_err(|_| ValidationError::SignatureFailed { sequence: 0 })?;
876 let k0 = k[0]
877 .parse()
878 .map_err(|_| ValidationError::SignatureFailed { sequence: 0 })?;
879 if i_key.as_bytes() != k0.as_bytes() {
880 return Err(ValidationError::InvalidSaid {
881 expected: Said::new_unchecked(k[0].as_str().to_string()),
882 actual: Said::new_unchecked(i.as_str().to_string()),
883 });
884 }
885 }
886
887 Ok(())
888}
889
890fn validate_inception(icp: &IcpEvent) -> Result<KeyState, ValidationError> {
891 verify_inception_self_cert(&icp.i, &icp.d, &icp.k)?;
895
896 validate_backer_uniqueness(&icp.b)?;
898
899 validate_thresholds(
901 icp.s.value(),
902 &icp.kt,
903 icp.k.len(),
904 &icp.nt,
905 icp.n.len(),
906 &icp.bt,
907 icp.b.len(),
908 )?;
909
910 let bt_val = icp.bt.simple_value().unwrap_or(0);
912 if icp.b.is_empty() && bt_val != 0 {
913 return Err(ValidationError::InvalidBackerThreshold {
914 bt: bt_val,
915 backer_count: 0,
916 });
917 }
918
919 Ok(KeyState::from_inception(
920 icp.i.clone(),
921 icp.k.clone(),
922 icp.n.clone(),
923 icp.kt.clone(),
924 icp.nt.clone(),
925 icp.d.clone(),
926 icp.b.clone(),
927 icp.bt.clone(),
928 icp.c.clone(),
929 ))
930}
931
932fn verify_sequence(event: &Event, expected: u128) -> Result<(), ValidationError> {
933 let actual = event.sequence().value();
934 if actual != expected {
935 return Err(ValidationError::InvalidSequence { expected, actual });
936 }
937 Ok(())
938}
939
940fn verify_chain_linkage(event: &Event, state: &KeyState) -> Result<(), ValidationError> {
941 let prev_said = event.previous().ok_or(ValidationError::NotInception)?;
942 if *prev_said != state.last_event_said {
943 return Err(ValidationError::BrokenChain {
944 sequence: event.sequence().value(),
945 referenced: prev_said.clone(),
946 actual: state.last_event_said.clone(),
947 });
948 }
949 Ok(())
950}
951
952fn prior_commitments_satisfy_threshold(
961 next_commitment: &[Said],
962 next_threshold: &Threshold,
963 new_keys: &[CesrKey],
964) -> bool {
965 let revealed: Vec<u32> = next_commitment
966 .iter()
967 .enumerate()
968 .filter_map(|(j, commitment)| {
969 let matched = new_keys.iter().any(|key| {
970 key.parse()
971 .map(|pk| verify_commitment(&pk, commitment))
972 .unwrap_or(false)
973 });
974 matched.then_some(j as u32)
975 })
976 .collect();
977 next_threshold.is_satisfied(&revealed, next_commitment.len())
978}
979
980#[derive(Debug, Clone, Copy, PartialEq, Eq)]
986enum BackerRole {
987 Registrar,
988 NoRegistrar,
989 Unspecified,
990}
991
992fn backer_role(traits: &[ConfigTrait]) -> BackerRole {
994 let mut role = BackerRole::Unspecified;
995 for t in traits {
996 match t {
997 ConfigTrait::RegistrarBackers => role = BackerRole::Registrar,
998 ConfigTrait::NoRegistrarBackers => role = BackerRole::NoRegistrar,
999 _ => {}
1000 }
1001 }
1002 role
1003}
1004
1005fn validate_rotation(
1006 rot: &RotEvent,
1007 sequence: u128,
1008 state: &mut KeyState,
1009) -> Result<(), ValidationError> {
1010 let post_backer_count =
1014 state.backers.iter().filter(|b| !rot.br.contains(b)).count() + rot.ba.len();
1015 validate_thresholds(
1016 sequence,
1017 &rot.kt,
1018 rot.k.len(),
1019 &rot.nt,
1020 rot.n.len(),
1021 &rot.bt,
1022 post_backer_count,
1023 )?;
1024
1025 if !state.next_commitment.is_empty()
1027 && !prior_commitments_satisfy_threshold(
1028 &state.next_commitment,
1029 &state.next_threshold,
1030 &rot.k,
1031 )
1032 {
1033 return Err(ValidationError::CommitmentMismatch { sequence });
1034 }
1035
1036 validate_backer_uniqueness(&rot.br)?;
1038 validate_backer_uniqueness(&rot.ba)?;
1039 for aid in &rot.ba {
1041 if rot.br.contains(aid) {
1042 return Err(ValidationError::DuplicateBacker {
1043 aid: aid.as_str().to_string(),
1044 });
1045 }
1046 }
1047 for aid in &rot.br {
1051 if !state.backers.contains(aid) {
1052 return Err(ValidationError::InvalidBackerDelta {
1053 sequence,
1054 reason: format!("br entry {} not in prior backers", aid.as_str()),
1055 });
1056 }
1057 }
1058 let survivors: Vec<_> = state
1059 .backers
1060 .iter()
1061 .filter(|b| !rot.br.contains(b))
1062 .collect();
1063 for aid in &rot.ba {
1064 if survivors.contains(&aid) {
1065 return Err(ValidationError::InvalidBackerDelta {
1066 sequence,
1067 reason: format!("ba entry {} duplicates a surviving backer", aid.as_str()),
1068 });
1069 }
1070 }
1071
1072 if !rot.c.is_empty() {
1077 let old_role = backer_role(&state.config_traits);
1078 let new_role = backer_role(&rot.c);
1079 let is_flip = matches!(
1080 (old_role, new_role),
1081 (BackerRole::Registrar, BackerRole::NoRegistrar)
1082 | (BackerRole::NoRegistrar, BackerRole::Registrar)
1083 );
1084 if is_flip && !survivors.is_empty() {
1085 return Err(ValidationError::BackerRoleFlip {
1086 sequence,
1087 reason: format!(
1088 "{old_role:?}->{new_role:?} but {} prior backer(s) survive; \
1089 a role flip must cut all prior backers",
1090 survivors.len()
1091 ),
1092 });
1093 }
1094 }
1095
1096 state.apply_rotation(
1097 rot.k.clone(),
1098 rot.n.clone(),
1099 rot.kt.clone(),
1100 rot.nt.clone(),
1101 sequence,
1102 rot.d.clone(),
1103 &rot.br,
1104 &rot.ba,
1105 rot.bt.clone(),
1106 rot.c.clone(),
1107 );
1108
1109 Ok(())
1110}
1111
1112fn validate_interaction(
1113 ixn: &IxnEvent,
1114 sequence: u128,
1115 state: &mut KeyState,
1116) -> Result<(), ValidationError> {
1117 state
1122 .current_key()
1123 .ok_or(ValidationError::SignatureFailed { sequence })?;
1124 state.apply_interaction(sequence, ixn.d.clone());
1125 Ok(())
1126}
1127
1128fn validate_delegated_inception(
1134 dip: &crate::events::DipEvent,
1135 lookup: Option<&dyn DelegatorKelLookup>,
1136) -> Result<KeyState, ValidationError> {
1137 let sequence = dip.s.value();
1138 let lookup = lookup.ok_or(ValidationError::DelegatorLookupMissing { sequence })?;
1139
1140 let anchor = lookup.find_seal(&dip.di, &dip.d).ok_or_else(|| {
1144 ValidationError::DelegatorSealNotFound {
1145 sequence,
1146 delegator_aid: dip.di.as_str().to_string(),
1147 }
1148 })?;
1149 enforce_source_seal(dip.source_seal.as_ref(), &anchor, sequence)?;
1150
1151 verify_inception_self_cert(&dip.i, &dip.d, &dip.k)?;
1154
1155 validate_backer_uniqueness(&dip.b)?;
1157 let bt_val = dip.bt.simple_value().unwrap_or(0);
1158 if dip.b.is_empty() && bt_val != 0 {
1159 return Err(ValidationError::InvalidBackerThreshold {
1160 bt: bt_val,
1161 backer_count: 0,
1162 });
1163 }
1164
1165 let is_non_transferable = dip.n.is_empty();
1167 Ok(KeyState {
1168 prefix: dip.i.clone(),
1169 current_keys: dip.k.clone(),
1170 next_commitment: dip.n.clone(),
1171 sequence: dip.s.value(),
1172 last_event_said: dip.d.clone(),
1173 is_abandoned: false,
1174 threshold: dip.kt.clone(),
1175 next_threshold: dip.nt.clone(),
1176 backers: dip.b.clone(),
1177 backer_threshold: dip.bt.clone(),
1178 config_traits: dip.c.clone(),
1179 is_non_transferable,
1180 delegator: Some(dip.di.clone()),
1181 last_establishment_sequence: dip.s.value(),
1182 })
1183}
1184
1185fn validate_delegated_rotation(
1191 drt: &crate::events::DrtEvent,
1192 sequence: u128,
1193 state: &mut KeyState,
1194 lookup: Option<&dyn DelegatorKelLookup>,
1195) -> Result<(), ValidationError> {
1196 let lookup = lookup.ok_or(ValidationError::DelegatorLookupMissing { sequence })?;
1197
1198 let anchor = lookup.find_seal(&drt.di, &drt.d).ok_or_else(|| {
1201 ValidationError::DelegatorSealNotFound {
1202 sequence,
1203 delegator_aid: drt.di.as_str().to_string(),
1204 }
1205 })?;
1206 enforce_source_seal(drt.source_seal.as_ref(), &anchor, sequence)?;
1207
1208 if !state.next_commitment.is_empty()
1210 && !prior_commitments_satisfy_threshold(
1211 &state.next_commitment,
1212 &state.next_threshold,
1213 &drt.k,
1214 )
1215 {
1216 return Err(ValidationError::CommitmentMismatch { sequence });
1217 }
1218
1219 validate_backer_uniqueness(&drt.br)?;
1220 validate_backer_uniqueness(&drt.ba)?;
1221 for aid in &drt.ba {
1222 if drt.br.contains(aid) {
1223 return Err(ValidationError::DuplicateBacker {
1224 aid: aid.as_str().to_string(),
1225 });
1226 }
1227 }
1228
1229 state.sequence = sequence;
1231 state.last_event_said = drt.d.clone();
1232 state.current_keys = drt.k.clone();
1233 state.next_commitment = drt.n.clone();
1234 state.threshold = drt.kt.clone();
1235 state.next_threshold = drt.nt.clone();
1236 Ok(())
1237}
1238
1239pub fn verify_event_crypto(
1245 event: &Event,
1246 current_state: Option<&KeyState>,
1247) -> Result<(), ValidationError> {
1248 match event {
1249 Event::Icp(icp) => verify_inception_self_cert(&icp.i, &icp.d, &icp.k),
1252 Event::Rot(rot) => {
1253 let sequence = event.sequence().value();
1254 let state = current_state.ok_or(ValidationError::SignatureFailed { sequence })?;
1255
1256 if state.is_abandoned || state.next_commitment.is_empty() {
1257 return Err(ValidationError::CommitmentMismatch { sequence });
1258 }
1259
1260 if rot.k.is_empty() {
1261 return Err(ValidationError::SignatureFailed { sequence });
1262 }
1263
1264 if !prior_commitments_satisfy_threshold(
1266 &state.next_commitment,
1267 &state.next_threshold,
1268 &rot.k,
1269 ) {
1270 return Err(ValidationError::CommitmentMismatch { sequence });
1271 }
1272
1273 Ok(())
1274 }
1275 Event::Ixn(_) => {
1276 let sequence = event.sequence().value();
1277 let state = current_state.ok_or(ValidationError::SignatureFailed { sequence })?;
1278
1279 state
1282 .current_key()
1283 .ok_or(ValidationError::SignatureFailed { sequence })?;
1284
1285 Ok(())
1286 }
1287 Event::Dip(dip) => verify_inception_self_cert(&dip.i, &dip.d, &dip.k),
1290 Event::Drt(drt) => {
1291 let sequence = event.sequence().value();
1292 let state = current_state.ok_or(ValidationError::SignatureFailed { sequence })?;
1293
1294 if state.is_abandoned || state.next_commitment.is_empty() {
1295 return Err(ValidationError::CommitmentMismatch { sequence });
1296 }
1297 if drt.k.is_empty() {
1298 return Err(ValidationError::SignatureFailed { sequence });
1299 }
1300 Ok(())
1301 }
1302 }
1303}
1304
1305pub fn verify_event_said(event: &Event) -> Result<(), ValidationError> {
1310 let value =
1311 serde_json::to_value(event).map_err(|e| ValidationError::Serialization(e.to_string()))?;
1312 let computed =
1313 compute_said(&value).map_err(|e| ValidationError::Serialization(e.to_string()))?;
1314 let actual = event.said();
1315
1316 if computed != *actual {
1317 return Err(ValidationError::InvalidSaid {
1318 expected: computed,
1319 actual: actual.clone(),
1320 });
1321 }
1322
1323 Ok(())
1324}
1325
1326pub fn validate_for_append(event: &Event, state: &KeyState) -> Result<(), ValidationError> {
1332 if matches!(event, Event::Icp(_)) {
1333 return Err(ValidationError::MultipleInceptions);
1334 }
1335
1336 verify_event_said(event)?;
1337 verify_sequence(event, state.sequence + 1)?;
1338 verify_chain_linkage(event, state)?;
1339 verify_event_crypto(event, Some(state))?;
1340
1341 Ok(())
1342}
1343
1344pub fn compute_event_said(event: &Event) -> Result<Said, ValidationError> {
1349 let value =
1350 serde_json::to_value(event).map_err(|e| ValidationError::Serialization(e.to_string()))?;
1351 compute_said(&value).map_err(|e| ValidationError::Serialization(e.to_string()))
1352}
1353
1354pub fn serialize_for_signing(event: &Event) -> Result<Vec<u8>, ValidationError> {
1366 serde_json::to_vec(event).map_err(|e| ValidationError::Serialization(e.to_string()))
1367}
1368
1369pub fn validate_signed_event(
1377 signed: &crate::events::SignedEvent,
1378 current_state: Option<&KeyState>,
1379) -> Result<(), ValidationError> {
1380 let event = &signed.event;
1381 let sequence = event.sequence().value();
1382
1383 if signed.signatures.is_empty() {
1384 return Err(ValidationError::SignatureFailed { sequence });
1385 }
1386
1387 let (keys, threshold) = match event {
1389 Event::Icp(icp) => (&icp.k, &icp.kt),
1390 Event::Dip(dip) => (&dip.k, &dip.kt),
1391 Event::Rot(rot) => (&rot.k, &rot.kt),
1392 Event::Drt(drt) => (&drt.k, &drt.kt),
1393 Event::Ixn(_) => {
1394 let state = current_state.ok_or(ValidationError::SignatureFailed { sequence })?;
1395 (&state.current_keys, &state.threshold)
1396 }
1397 };
1398
1399 if keys.is_empty() {
1400 return Err(ValidationError::SignatureFailed { sequence });
1401 }
1402
1403 let canonical = serialize_for_signing(event)?;
1405 let mut verified_indices = Vec::new();
1406
1407 for sig in &signed.signatures {
1408 let idx = sig.index as usize;
1409 if idx >= keys.len() {
1410 continue; }
1412 let key = &keys[idx];
1413 if let Ok(pk) = key.parse()
1414 && pk.verify_signature(&canonical, &sig.sig).is_ok()
1415 {
1416 verified_indices.push(sig.index);
1417 }
1418 }
1419
1420 if !threshold.is_satisfied(&verified_indices, keys.len()) {
1422 return Err(ValidationError::SignatureFailed { sequence });
1423 }
1424
1425 if matches!(event, Event::Rot(_) | Event::Drt(_))
1429 && let Some(state) = current_state
1430 {
1431 let n_len = state.next_commitment.len();
1432
1433 let mut verified_prior: Vec<u32> = Vec::new();
1438 for sig in &signed.signatures {
1439 let Some(key) = keys.get(sig.index as usize) else {
1440 continue;
1441 };
1442 let Ok(pk) = key.parse() else {
1443 continue;
1444 };
1445 if pk.verify_signature(&canonical, &sig.sig).is_err() {
1446 continue;
1447 }
1448 let j = sig.prior_index.unwrap_or(sig.index) as usize;
1449 let Some(commitment) = state.next_commitment.get(j) else {
1450 continue;
1451 };
1452 if crate::crypto::verify_commitment(&pk, commitment) {
1453 verified_prior.push(j as u32);
1454 }
1455 }
1456
1457 if n_len != keys.len() && verified_prior.is_empty() {
1462 return Err(ValidationError::AsymmetricKeyRotation {
1463 sequence,
1464 prior_next_count: n_len,
1465 new_key_count: keys.len(),
1466 });
1467 }
1468
1469 if !state.next_threshold.is_satisfied(&verified_prior, n_len) {
1470 return Err(ValidationError::SignatureFailed { sequence });
1471 }
1472 }
1473
1474 Ok(())
1475}
1476
1477pub fn finalize_icp_event(mut icp: IcpEvent) -> Result<IcpEvent, ValidationError> {
1482 let value = serde_json::to_value(Event::Icp(icp.clone()))
1483 .map_err(|e| ValidationError::Serialization(e.to_string()))?;
1484 let said = compute_said(&value).map_err(|e| ValidationError::Serialization(e.to_string()))?;
1485
1486 icp.d = said.clone();
1487 if icp.i.is_empty() || icp.i.as_str().starts_with('E') {
1489 icp.i = Prefix::new_unchecked(said.into_inner());
1490 }
1491
1492 let final_bytes = serde_json::to_vec(&Event::Icp(icp.clone()))
1494 .map_err(|e| ValidationError::Serialization(e.to_string()))?;
1495 icp.v = crate::types::VersionString::json(final_bytes.len() as u32);
1496
1497 Ok(icp)
1498}
1499
1500pub fn finalize_dip_event(
1508 mut dip: crate::events::DipEvent,
1509) -> Result<crate::events::DipEvent, ValidationError> {
1510 let value = serde_json::to_value(Event::Dip(dip.clone()))
1511 .map_err(|e| ValidationError::Serialization(e.to_string()))?;
1512 let said = compute_said(&value).map_err(|e| ValidationError::Serialization(e.to_string()))?;
1513
1514 dip.d = said.clone();
1515 if dip.i.is_empty() || dip.i.as_str().starts_with('E') {
1517 dip.i = Prefix::new_unchecked(said.into_inner());
1518 }
1519
1520 let final_bytes = serde_json::to_vec(&Event::Dip(dip.clone()))
1521 .map_err(|e| ValidationError::Serialization(e.to_string()))?;
1522 dip.v = crate::types::VersionString::json(final_bytes.len() as u32);
1523
1524 Ok(dip)
1525}
1526
1527pub fn finalize_rot_event(mut rot: RotEvent) -> Result<RotEvent, ValidationError> {
1532 let value = serde_json::to_value(Event::Rot(rot.clone()))
1533 .map_err(|e| ValidationError::Serialization(e.to_string()))?;
1534 let said = compute_said(&value).map_err(|e| ValidationError::Serialization(e.to_string()))?;
1535 rot.d = said;
1536
1537 let final_bytes = serde_json::to_vec(&Event::Rot(rot.clone()))
1538 .map_err(|e| ValidationError::Serialization(e.to_string()))?;
1539 rot.v = crate::types::VersionString::json(final_bytes.len() as u32);
1540
1541 Ok(rot)
1542}
1543
1544pub fn finalize_drt_event(
1553 mut drt: crate::events::DrtEvent,
1554) -> Result<crate::events::DrtEvent, ValidationError> {
1555 let value = serde_json::to_value(Event::Drt(drt.clone()))
1556 .map_err(|e| ValidationError::Serialization(e.to_string()))?;
1557 let said = compute_said(&value).map_err(|e| ValidationError::Serialization(e.to_string()))?;
1558 drt.d = said;
1559
1560 let final_bytes = serde_json::to_vec(&Event::Drt(drt.clone()))
1561 .map_err(|e| ValidationError::Serialization(e.to_string()))?;
1562 drt.v = crate::types::VersionString::json(final_bytes.len() as u32);
1563
1564 Ok(drt)
1565}
1566
1567pub fn finalize_ixn_event(mut ixn: IxnEvent) -> Result<IxnEvent, ValidationError> {
1572 let value = serde_json::to_value(Event::Ixn(ixn.clone()))
1573 .map_err(|e| ValidationError::Serialization(e.to_string()))?;
1574 let said = compute_said(&value).map_err(|e| ValidationError::Serialization(e.to_string()))?;
1575 ixn.d = said;
1576
1577 let final_bytes = serde_json::to_vec(&Event::Ixn(ixn.clone()))
1578 .map_err(|e| ValidationError::Serialization(e.to_string()))?;
1579 ixn.v = crate::types::VersionString::json(final_bytes.len() as u32);
1580
1581 Ok(ixn)
1582}
1583
1584pub fn find_seal_in_kel(events: &[Event], digest: &str) -> Option<u128> {
1592 for event in events {
1593 if let Event::Ixn(ixn) = event {
1594 for seal in &ixn.a {
1595 if seal.digest_value().is_some_and(|d| d.as_str() == digest) {
1596 return Some(ixn.s.value());
1597 }
1598 }
1599 }
1600 }
1601 None
1602}
1603
1604pub fn parse_kel_json(json: &str) -> Result<Vec<Event>, ValidationError> {
1609 serde_json::from_str(json).map_err(|e| ValidationError::Serialization(e.to_string()))
1610}
1611
1612#[cfg(test)]
1613#[allow(clippy::unwrap_used, clippy::expect_used)]
1614mod tests {
1615 use super::*;
1616 use crate::events::{IndexedSignature, KeriSequence, Seal, SignedEvent};
1617 use crate::types::{CesrKey, Threshold, VersionString};
1618 use ring::rand::SystemRandom;
1619 use ring::signature::{Ed25519KeyPair, KeyPair};
1620
1621 fn gen_keypair() -> Ed25519KeyPair {
1622 let rng = SystemRandom::new();
1623 let pkcs8 = Ed25519KeyPair::generate_pkcs8(&rng).unwrap();
1624 Ed25519KeyPair::from_pkcs8(pkcs8.as_ref()).unwrap()
1625 }
1626
1627 fn encode_pubkey(kp: &Ed25519KeyPair) -> String {
1628 crate::cesr_encode::encode_verkey(kp.public_key().as_ref(), cesride::matter::Codex::Ed25519)
1629 .unwrap()
1630 }
1631
1632 fn make_raw_icp(key: &str, next: &str) -> IcpEvent {
1633 IcpEvent {
1634 v: VersionString::placeholder(),
1635 d: Said::default(),
1636 i: Prefix::default(),
1637 s: KeriSequence::new(0),
1638 kt: Threshold::Simple(1),
1639 k: vec![CesrKey::new_unchecked(key.to_string())],
1640 nt: Threshold::Simple(1),
1641 n: vec![Said::new_unchecked(next.to_string())],
1642 bt: Threshold::Simple(0),
1643 b: vec![],
1644 c: vec![],
1645 a: vec![],
1646 }
1647 }
1648
1649 fn make_signed_icp() -> (IcpEvent, Ed25519KeyPair) {
1650 let rng = SystemRandom::new();
1651 let pkcs8 = Ed25519KeyPair::generate_pkcs8(&rng).unwrap();
1652 let keypair = Ed25519KeyPair::from_pkcs8(pkcs8.as_ref()).unwrap();
1653 let key_encoded = encode_pubkey(&keypair);
1654
1655 let icp = IcpEvent {
1656 v: VersionString::placeholder(),
1657 d: Said::default(),
1658 i: Prefix::default(),
1659 s: KeriSequence::new(0),
1660 kt: Threshold::Simple(1),
1661 k: vec![CesrKey::new_unchecked(key_encoded)],
1662 nt: Threshold::Simple(1),
1663 n: vec![Said::new_unchecked("ENextCommitment".to_string())],
1664 bt: Threshold::Simple(0),
1665 b: vec![],
1666 c: vec![],
1667 a: vec![],
1668 };
1669
1670 let finalized = finalize_icp_event(icp).unwrap();
1671 (finalized, keypair)
1672 }
1673
1674 fn make_signed_ixn(
1675 prefix: &Prefix,
1676 prev_said: &Said,
1677 seq: u128,
1678 _keypair: &Ed25519KeyPair,
1679 ) -> IxnEvent {
1680 let mut ixn = IxnEvent {
1681 v: VersionString::placeholder(),
1682 d: Said::default(),
1683 i: prefix.clone(),
1684 s: KeriSequence::new(seq),
1685 p: prev_said.clone(),
1686 a: vec![Seal::digest("EAttest")],
1687 };
1688
1689 let value = serde_json::to_value(Event::Ixn(ixn.clone())).unwrap();
1690 ixn.d = compute_said(&value).unwrap();
1691
1692 ixn
1693 }
1694
1695 #[test]
1696 fn finalize_icp_sets_said() {
1697 let icp = make_raw_icp("DKey1", "ENext1");
1698 let finalized = finalize_icp_event(icp).unwrap();
1699
1700 assert!(!finalized.d.is_empty());
1701 assert_eq!(finalized.d.as_str(), finalized.i.as_str());
1702 assert!(finalized.d.as_str().starts_with('E'));
1703 }
1704
1705 #[test]
1706 fn validates_single_inception() {
1707 let (icp, _keypair) = make_signed_icp();
1708 let events = vec![Event::Icp(icp.clone())];
1709
1710 let state = validate_kel(&events).unwrap();
1711 assert_eq!(state.prefix, icp.i);
1712 assert_eq!(state.sequence, 0);
1713 }
1714
1715 #[test]
1716 fn rejects_empty_kel() {
1717 let result = validate_kel(&[]);
1718 assert!(matches!(result, Err(ValidationError::EmptyKel)));
1719 }
1720
1721 #[test]
1722 fn rejects_non_inception_first() {
1723 let mut ixn = IxnEvent {
1724 v: VersionString::placeholder(),
1725 d: Said::default(),
1726 i: Prefix::new_unchecked("ETest".to_string()),
1727 s: KeriSequence::new(0),
1728 p: Said::new_unchecked("EPrev".to_string()),
1729 a: vec![],
1730 };
1731 let event = Event::Ixn(ixn.clone());
1734 if let Ok(said) = compute_event_said(&event) {
1735 ixn.d = said;
1736 }
1737 let events = vec![Event::Ixn(ixn)];
1738 let result = validate_kel(&events);
1739 assert!(matches!(result, Err(ValidationError::NotInception)));
1740 }
1741
1742 #[test]
1743 fn rejects_broken_sequence() {
1744 let (icp, _keypair) = make_signed_icp();
1745
1746 let mut ixn = IxnEvent {
1747 v: VersionString::placeholder(),
1748 d: Said::default(),
1749 i: icp.i.clone(),
1750 s: KeriSequence::new(5),
1751 p: icp.d.clone(),
1752 a: vec![],
1753 };
1754
1755 let value = serde_json::to_value(Event::Ixn(ixn.clone())).unwrap();
1756 ixn.d = compute_said(&value).unwrap();
1757
1758 let events = vec![Event::Icp(icp), Event::Ixn(ixn)];
1759 let result = validate_kel(&events);
1760 assert!(matches!(
1761 result,
1762 Err(ValidationError::InvalidSequence {
1763 expected: 1,
1764 actual: 5
1765 })
1766 ));
1767 }
1768
1769 #[test]
1770 fn rejects_broken_chain() {
1771 let (icp, _keypair) = make_signed_icp();
1772
1773 let mut ixn = IxnEvent {
1774 v: VersionString::placeholder(),
1775 d: Said::default(),
1776 i: icp.i.clone(),
1777 s: KeriSequence::new(1),
1778 p: Said::new_unchecked("EWrongPrevious".to_string()),
1779 a: vec![],
1780 };
1781
1782 let value = serde_json::to_value(Event::Ixn(ixn.clone())).unwrap();
1783 ixn.d = compute_said(&value).unwrap();
1784
1785 let events = vec![Event::Icp(icp), Event::Ixn(ixn)];
1786 let result = validate_kel(&events);
1787 assert!(matches!(result, Err(ValidationError::BrokenChain { .. })));
1788 }
1789
1790 #[test]
1791 fn rejects_invalid_said() {
1792 let icp = make_raw_icp("DKey1", "ENext1");
1793 let finalized = finalize_icp_event(icp).unwrap();
1794
1795 let mut tampered = finalized.clone();
1796 tampered.d = Said::new_unchecked("EWrongSaid".to_string());
1797
1798 let events = vec![Event::Icp(tampered)];
1799 let result = validate_kel(&events);
1800 assert!(matches!(result, Err(ValidationError::InvalidSaid { .. })));
1801 }
1802
1803 #[test]
1810 fn rejects_forged_inception_prefix_mismatch() {
1811 let (icp, _kp) = make_signed_icp();
1815 assert_eq!(
1816 icp.i.as_str(),
1817 icp.d.as_str(),
1818 "a finalized inception is self-addressing"
1819 );
1820
1821 let (other, _kp2) = make_signed_icp();
1822 assert_ne!(other.i.as_str(), icp.d.as_str());
1823
1824 let mut forged = icp;
1825 forged.i = other.i;
1826 let result = validate_kel(&[Event::Icp(forged)]);
1827 assert!(
1828 matches!(result, Err(ValidationError::InvalidSaid { .. })),
1829 "forged inception (i != d) must be rejected, got {result:?}"
1830 );
1831 }
1832
1833 #[test]
1834 fn rejects_forged_inception_basic_derivation() {
1835 let prefix_key = encode_pubkey(&gen_keypair());
1839 let committed_key = encode_pubkey(&gen_keypair());
1840 assert_ne!(prefix_key, committed_key);
1841 assert!(!prefix_key.starts_with('E'));
1842
1843 let mut icp = make_raw_icp(&committed_key, "ENext1");
1844 icp.i = Prefix::new_unchecked(prefix_key);
1845 let value = serde_json::to_value(Event::Icp(icp.clone())).unwrap();
1848 icp.d = compute_said(&value).unwrap();
1849
1850 let result = validate_kel(&[Event::Icp(icp)]);
1851 assert!(
1852 matches!(result, Err(ValidationError::InvalidSaid { .. })),
1853 "basic-derivation inception with i != k[0] must be rejected, got {result:?}"
1854 );
1855 }
1856
1857 fn sign_event(event: &Event, kp: &Ed25519KeyPair) -> SignedEvent {
1870 let sig = kp
1871 .sign(&serialize_for_signing(event).unwrap())
1872 .as_ref()
1873 .to_vec();
1874 SignedEvent::new(
1875 event.clone(),
1876 vec![IndexedSignature {
1877 index: 0,
1878 prior_index: None,
1879 sig,
1880 }],
1881 )
1882 }
1883
1884 #[test]
1885 fn validate_signed_kel_accepts_correctly_signed_kel() {
1886 let (icp, kp) = make_signed_icp();
1887 let signed_icp = sign_event(&Event::Icp(icp.clone()), &kp);
1888 let ixn = make_signed_ixn(&icp.i, &icp.d, 1, &kp);
1889 let signed_ixn = sign_event(&Event::Ixn(ixn), &kp);
1890
1891 let state = validate_signed_kel(&[signed_icp, signed_ixn], None)
1892 .expect("a correctly-signed KEL must validate");
1893 assert_eq!(state.sequence, 1);
1894 }
1895
1896 #[test]
1897 fn validate_signed_kel_rejects_unsigned_ixn() {
1898 let (icp, kp) = make_signed_icp();
1901 let signed_icp = sign_event(&Event::Icp(icp.clone()), &kp);
1902 let ixn = make_signed_ixn(&icp.i, &icp.d, 1, &kp);
1903 let unsigned_ixn = SignedEvent::new(Event::Ixn(ixn), vec![]);
1904
1905 let result = validate_signed_kel(&[signed_icp, unsigned_ixn], None);
1906 assert!(
1907 matches!(result, Err(ValidationError::SignatureFailed { .. })),
1908 "unsigned ixn must be rejected, got {result:?}"
1909 );
1910 }
1911
1912 #[test]
1913 fn validate_signed_kel_rejects_wrong_signer_ixn() {
1914 let (icp, kp) = make_signed_icp();
1917 let signed_icp = sign_event(&Event::Icp(icp.clone()), &kp);
1918 let ixn = make_signed_ixn(&icp.i, &icp.d, 1, &kp);
1919 let attacker = gen_keypair();
1920 let forged_ixn = sign_event(&Event::Ixn(ixn), &attacker);
1921
1922 let result = validate_signed_kel(&[signed_icp, forged_ixn], None);
1923 assert!(
1924 matches!(result, Err(ValidationError::SignatureFailed { .. })),
1925 "wrong-signer ixn must be rejected, got {result:?}"
1926 );
1927 }
1928
1929 #[test]
1930 fn validates_icp_then_ixn() {
1931 let (icp, keypair) = make_signed_icp();
1932 let ixn = make_signed_ixn(&icp.i, &icp.d, 1, &keypair);
1933
1934 let events = vec![Event::Icp(icp), Event::Ixn(ixn.clone())];
1935 let state = validate_kel(&events).unwrap();
1936 assert_eq!(state.sequence, 1);
1937 assert_eq!(state.last_event_said, ixn.d);
1938 }
1939
1940 #[test]
1941 fn compute_event_said_works() {
1942 let icp = make_raw_icp("DKey1", "ENext1");
1943 let event = Event::Icp(icp);
1944 let said = compute_event_said(&event).unwrap();
1945 assert!(said.as_str().starts_with('E'));
1946 assert!(!said.is_empty());
1947 }
1948
1949 #[test]
1953 fn accepts_correct_signature() {
1954 let (icp, keypair) = make_signed_icp();
1955 let event = Event::Icp(icp);
1956 let canonical = serialize_for_signing(&event).unwrap();
1957 let sig = keypair.sign(&canonical).as_ref().to_vec();
1958 let signed = SignedEvent::new(
1959 event,
1960 vec![IndexedSignature {
1961 index: 0,
1962 prior_index: None,
1963 sig,
1964 }],
1965 );
1966
1967 validate_signed_event(&signed, None).expect("correct signature must validate");
1968 }
1969
1970 #[test]
1976 fn rejects_forged_signature() {
1977 let (icp, _keypair) = make_signed_icp();
1978 let event = Event::Icp(icp);
1979 let forged_sig = vec![0u8; 64]; let signed = SignedEvent::new(
1981 event,
1982 vec![IndexedSignature {
1983 index: 0,
1984 prior_index: None,
1985 sig: forged_sig,
1986 }],
1987 );
1988
1989 assert!(matches!(
1990 validate_signed_event(&signed, None),
1991 Err(ValidationError::SignatureFailed { sequence: 0 })
1992 ));
1993 }
1994
1995 #[test]
2004 fn rejects_wrong_key_signature() {
2005 let committed = gen_keypair();
2006 let key_encoded = encode_pubkey(&committed);
2007
2008 let icp = IcpEvent {
2009 v: VersionString::placeholder(),
2010 d: Said::default(),
2011 i: Prefix::default(),
2012 s: KeriSequence::new(0),
2013 kt: Threshold::Simple(1),
2014 k: vec![CesrKey::new_unchecked(key_encoded)],
2015 nt: Threshold::Simple(1),
2016 n: vec![Said::new_unchecked("ENextCommitment".to_string())],
2017 bt: Threshold::Simple(0),
2018 b: vec![],
2019 c: vec![],
2020 a: vec![],
2021 };
2022 let icp = finalize_icp_event(icp).unwrap();
2023 let event = Event::Icp(icp);
2024
2025 let wrong = gen_keypair();
2026 let canonical = serialize_for_signing(&event).unwrap();
2027 let wrong_sig = wrong.sign(&canonical).as_ref().to_vec();
2028 let signed = SignedEvent::new(
2029 event,
2030 vec![IndexedSignature {
2031 index: 0,
2032 prior_index: None,
2033 sig: wrong_sig,
2034 }],
2035 );
2036
2037 assert!(matches!(
2038 validate_signed_event(&signed, None),
2039 Err(ValidationError::SignatureFailed { sequence: 0 })
2040 ));
2041 }
2042
2043 #[test]
2044 fn crypto_accepts_valid_inception() {
2045 let (icp, _keypair) = make_signed_icp();
2046 let result = verify_event_crypto(&Event::Icp(icp), None);
2047 assert!(result.is_ok());
2048 }
2049
2050 #[test]
2051 fn find_seal_in_kel_finds_digest() {
2052 let (icp, keypair) = make_signed_icp();
2053 let ixn = make_signed_ixn(&icp.i, &icp.d, 1, &keypair);
2054 let events = vec![Event::Icp(icp), Event::Ixn(ixn)];
2055 assert_eq!(find_seal_in_kel(&events, "EAttest"), Some(1));
2056 assert_eq!(find_seal_in_kel(&events, "ENonExistent"), None);
2057 }
2058
2059 #[test]
2060 fn parse_kel_json_rejects_invalid_hex_sequence() {
2061 let json = r#"[{"v":"KERI10JSON","t":"icp","i":"E123","s":"not_hex","kt":"1","k":["DKey"],"nt":"1","n":["ENext"],"bt":"0","b":[]}]"#;
2062 let result = parse_kel_json(json);
2063 assert!(result.is_err(), "expected error for invalid hex sequence");
2064 }
2065
2066 fn make_custom_signed_icp(customize: impl FnOnce(&mut IcpEvent)) -> (IcpEvent, Ed25519KeyPair) {
2069 let rng = SystemRandom::new();
2070 let pkcs8 = Ed25519KeyPair::generate_pkcs8(&rng).unwrap();
2071 let keypair = Ed25519KeyPair::from_pkcs8(pkcs8.as_ref()).unwrap();
2072 let key_encoded = encode_pubkey(&keypair);
2073
2074 let mut icp = IcpEvent {
2075 v: VersionString::placeholder(),
2076 d: Said::default(),
2077 i: Prefix::default(),
2078 s: KeriSequence::new(0),
2079 kt: Threshold::Simple(1),
2080 k: vec![CesrKey::new_unchecked(key_encoded)],
2081 nt: Threshold::Simple(1),
2082 n: vec![Said::new_unchecked("ENextCommitment".to_string())],
2083 bt: Threshold::Simple(0),
2084 b: vec![],
2085 c: vec![],
2086 a: vec![],
2087 };
2088
2089 customize(&mut icp);
2090
2091 let finalized = finalize_icp_event(icp).unwrap();
2092 (finalized, keypair)
2093 }
2094
2095 #[test]
2096 fn rejects_events_after_abandonment() {
2097 let kp2 = gen_keypair();
2099
2100 let commitment2 = crate::crypto::compute_next_commitment(
2102 &crate::keys::KeriPublicKey::ed25519(kp2.public_key().as_ref()).unwrap(),
2103 );
2104 let (icp, _kp1) = make_custom_signed_icp(|icp| {
2105 icp.n = vec![commitment2.clone()];
2106 });
2107 let prefix = icp.i.clone();
2108
2109 let mut rot = RotEvent {
2111 v: VersionString::placeholder(),
2112 d: Said::default(),
2113 i: prefix.clone(),
2114 s: KeriSequence::new(1),
2115 p: icp.d.clone(),
2116 kt: Threshold::Simple(1),
2117 k: vec![CesrKey::new_unchecked(encode_pubkey(&kp2))],
2118 nt: Threshold::Simple(0),
2119 n: vec![],
2120 bt: Threshold::Simple(0),
2121 br: vec![],
2122 ba: vec![],
2123 c: vec![],
2124 a: vec![],
2125 };
2126 let val = serde_json::to_value(Event::Rot(rot.clone())).unwrap();
2127 rot.d = compute_said(&val).unwrap();
2128
2129 let ixn = make_signed_ixn(&prefix, &rot.d, 2, &kp2);
2130 let events = vec![Event::Icp(icp), Event::Rot(rot), Event::Ixn(ixn)];
2131 let result = validate_kel(&events);
2132 assert!(
2133 matches!(result, Err(ValidationError::AbandonedIdentity { .. })),
2134 "expected AbandonedIdentity, got: {result:?}"
2135 );
2136 }
2137
2138 #[test]
2139 fn rejects_ixn_in_establishment_only_kel() {
2140 let (icp, keypair) = make_custom_signed_icp(|icp| {
2141 icp.c = vec![ConfigTrait::EstablishmentOnly];
2142 });
2143 let ixn = make_signed_ixn(&icp.i, &icp.d, 1, &keypair);
2144 let events = vec![Event::Icp(icp), Event::Ixn(ixn)];
2145 let result = validate_kel(&events);
2146 assert!(
2147 matches!(result, Err(ValidationError::EstablishmentOnly { .. })),
2148 "expected EstablishmentOnly, got: {result:?}"
2149 );
2150 }
2151
2152 #[test]
2153 fn rejects_events_after_non_transferable_inception() {
2154 let (icp, keypair) = make_custom_signed_icp(|icp| {
2155 icp.n = vec![];
2156 icp.nt = Threshold::Simple(0);
2157 });
2158 let ixn = make_signed_ixn(&icp.i, &icp.d, 1, &keypair);
2159 let events = vec![Event::Icp(icp), Event::Ixn(ixn)];
2160 let result = validate_kel(&events);
2161 assert!(
2162 matches!(
2163 result,
2164 Err(ValidationError::NonTransferable)
2165 | Err(ValidationError::AbandonedIdentity { .. })
2166 ),
2167 "expected NonTransferable or AbandonedIdentity, got: {result:?}"
2168 );
2169 }
2170
2171 #[test]
2172 fn rejects_duplicate_backers() {
2173 let (_, result) = {
2174 let rng = SystemRandom::new();
2175 let pkcs8 = Ed25519KeyPair::generate_pkcs8(&rng).unwrap();
2176 let keypair = Ed25519KeyPair::from_pkcs8(pkcs8.as_ref()).unwrap();
2177 let key_encoded = encode_pubkey(&keypair);
2178
2179 let dup_backer = Prefix::new_unchecked("DWit1".to_string());
2180 let icp = IcpEvent {
2181 v: VersionString::placeholder(),
2182 d: Said::default(),
2183 i: Prefix::default(),
2184 s: KeriSequence::new(0),
2185 kt: Threshold::Simple(1),
2186 k: vec![CesrKey::new_unchecked(key_encoded)],
2187 nt: Threshold::Simple(1),
2188 n: vec![Said::new_unchecked("ENextCommitment".to_string())],
2189 bt: Threshold::Simple(2),
2190 b: vec![dup_backer.clone(), dup_backer],
2191 c: vec![],
2192 a: vec![],
2193 };
2194
2195 let finalized = finalize_icp_event(icp).unwrap();
2196 let events = vec![Event::Icp(finalized)];
2197 (keypair, validate_kel(&events))
2198 };
2199 assert!(
2200 matches!(result, Err(ValidationError::DuplicateBacker { .. })),
2201 "expected DuplicateBacker, got: {result:?}"
2202 );
2203 }
2204
2205 #[test]
2206 fn rejects_invalid_backer_threshold() {
2207 let (_, result) = {
2208 let rng = SystemRandom::new();
2209 let pkcs8 = Ed25519KeyPair::generate_pkcs8(&rng).unwrap();
2210 let keypair = Ed25519KeyPair::from_pkcs8(pkcs8.as_ref()).unwrap();
2211 let key_encoded = encode_pubkey(&keypair);
2212
2213 let icp = IcpEvent {
2214 v: VersionString::placeholder(),
2215 d: Said::default(),
2216 i: Prefix::default(),
2217 s: KeriSequence::new(0),
2218 kt: Threshold::Simple(1),
2219 k: vec![CesrKey::new_unchecked(key_encoded)],
2220 nt: Threshold::Simple(1),
2221 n: vec![Said::new_unchecked("ENextCommitment".to_string())],
2222 bt: Threshold::Simple(2),
2223 b: vec![],
2224 c: vec![],
2225 a: vec![],
2226 };
2227
2228 let finalized = finalize_icp_event(icp).unwrap();
2229 let events = vec![Event::Icp(finalized)];
2230 (keypair, validate_kel(&events))
2231 };
2232 assert!(
2236 matches!(result, Err(ValidationError::ThresholdNotSatisfiable { .. })),
2237 "expected ThresholdNotSatisfiable, got: {result:?}"
2238 );
2239 }
2240
2241 #[test]
2242 fn sign_over_finalized_bytes_roundtrips() {
2243 let (icp, _kp) = make_signed_icp();
2247 let bytes = serialize_for_signing(&Event::Icp(icp.clone())).unwrap();
2248 assert_eq!(
2249 bytes.len() as u32,
2250 icp.v.size,
2251 "signed byte length must equal the version-string size field"
2252 );
2253 let reparsed: Event = serde_json::from_slice(&bytes).unwrap();
2254 assert!(reparsed.is_inception());
2255 }
2256
2257 #[test]
2258 fn threshold_rejects_kt_gt_k() {
2259 let kp = gen_keypair();
2262 let key = encode_pubkey(&kp);
2263 let icp = IcpEvent {
2264 v: VersionString::placeholder(),
2265 d: Said::default(),
2266 i: Prefix::default(),
2267 s: KeriSequence::new(0),
2268 kt: Threshold::Simple(5),
2269 k: vec![CesrKey::new_unchecked(key)],
2270 nt: Threshold::Simple(1),
2271 n: vec![Said::new_unchecked("ENextCommitment".to_string())],
2272 bt: Threshold::Simple(0),
2273 b: vec![],
2274 c: vec![],
2275 a: vec![],
2276 };
2277 let finalized = finalize_icp_event(icp).unwrap();
2278 let result = validate_kel(&[Event::Icp(finalized)]);
2279 assert!(
2280 matches!(result, Err(ValidationError::ThresholdNotSatisfiable { .. })),
2281 "expected ThresholdNotSatisfiable, got: {result:?}"
2282 );
2283 }
2284
2285 #[test]
2286 fn rotation_rejects_br_not_in_prior() {
2287 let state = KeyState::from_inception(
2291 Prefix::new_unchecked("EPrefix".to_string()),
2292 vec![CesrKey::new_unchecked("DKey1".to_string())],
2293 vec![], Threshold::Simple(1),
2295 Threshold::Simple(0),
2296 Said::new_unchecked("ESAID".to_string()),
2297 vec![Prefix::new_unchecked("BWit1".to_string())],
2298 Threshold::Simple(0),
2299 vec![],
2300 );
2301
2302 let make_rot = |br: Vec<Prefix>, ba: Vec<Prefix>| RotEvent {
2303 v: VersionString::placeholder(),
2304 d: Said::default(),
2305 i: Prefix::new_unchecked("EPrefix".to_string()),
2306 s: KeriSequence::new(1),
2307 p: Said::new_unchecked("ESAID".to_string()),
2308 kt: Threshold::Simple(1),
2309 k: vec![CesrKey::new_unchecked("DKey2".to_string())],
2310 nt: Threshold::Simple(0),
2311 n: vec![],
2312 bt: Threshold::Simple(0),
2313 br,
2314 ba,
2315 c: vec![],
2316 a: vec![],
2317 };
2318
2319 let bad_cut = make_rot(vec![Prefix::new_unchecked("BWitX".to_string())], vec![]);
2321 assert!(matches!(
2322 validate_rotation(&bad_cut, 1, &mut state.clone()),
2323 Err(ValidationError::InvalidBackerDelta { .. })
2324 ));
2325
2326 let bad_add = make_rot(vec![], vec![Prefix::new_unchecked("BWit1".to_string())]);
2328 assert!(matches!(
2329 validate_rotation(&bad_add, 1, &mut state.clone()),
2330 Err(ValidationError::InvalidBackerDelta { .. })
2331 ));
2332
2333 let ok = make_rot(vec![Prefix::new_unchecked("BWit1".to_string())], vec![]);
2335 assert!(validate_rotation(&ok, 1, &mut state.clone()).is_ok());
2336 }
2337
2338 #[test]
2339 fn rotation_rejects_silent_backer_role_flip() {
2340 let nrb_state = || {
2344 KeyState::from_inception(
2345 Prefix::new_unchecked("EPrefix".to_string()),
2346 vec![CesrKey::new_unchecked("DKey1".to_string())],
2347 vec![],
2348 Threshold::Simple(1),
2349 Threshold::Simple(0),
2350 Said::new_unchecked("ESAID".to_string()),
2351 vec![Prefix::new_unchecked("BWit1".to_string())],
2352 Threshold::Simple(0),
2353 vec![ConfigTrait::NoRegistrarBackers],
2354 )
2355 };
2356
2357 let make_rot = |br: Vec<Prefix>, ba: Vec<Prefix>, c: Vec<ConfigTrait>| RotEvent {
2358 v: VersionString::placeholder(),
2359 d: Said::default(),
2360 i: Prefix::new_unchecked("EPrefix".to_string()),
2361 s: KeriSequence::new(1),
2362 p: Said::new_unchecked("ESAID".to_string()),
2363 kt: Threshold::Simple(1),
2364 k: vec![CesrKey::new_unchecked("DKey2".to_string())],
2365 nt: Threshold::Simple(0),
2366 n: vec![],
2367 bt: Threshold::Simple(0),
2368 br,
2369 ba,
2370 c,
2371 a: vec![],
2372 };
2373
2374 let flip_keep = make_rot(vec![], vec![], vec![ConfigTrait::RegistrarBackers]);
2376 assert!(matches!(
2377 validate_rotation(&flip_keep, 1, &mut nrb_state()),
2378 Err(ValidationError::BackerRoleFlip { .. })
2379 ));
2380
2381 let flip_rebuild = make_rot(
2383 vec![Prefix::new_unchecked("BWit1".to_string())],
2384 vec![],
2385 vec![ConfigTrait::RegistrarBackers],
2386 );
2387 assert!(validate_rotation(&flip_rebuild, 1, &mut nrb_state()).is_ok());
2388
2389 let same_role = make_rot(vec![], vec![], vec![ConfigTrait::NoRegistrarBackers]);
2391 assert!(validate_rotation(&same_role, 1, &mut nrb_state()).is_ok());
2392
2393 let inherit = make_rot(vec![], vec![], vec![]);
2395 assert!(validate_rotation(&inherit, 1, &mut nrb_state()).is_ok());
2396 }
2397
2398 use crate::witness::WitnessReceipt;
2401
2402 struct MapReceipts {
2404 by_said: std::collections::HashMap<String, Vec<WitnessReceipt>>,
2405 }
2406
2407 impl WitnessReceiptLookup for MapReceipts {
2408 fn receipts_for(
2409 &self,
2410 _controller: &Prefix,
2411 _sn: KeriSequence,
2412 said: &Said,
2413 ) -> Vec<WitnessReceipt> {
2414 self.by_said.get(said.as_str()).cloned().unwrap_or_default()
2415 }
2416 }
2417
2418 fn witness_aid(aid: &str) -> Prefix {
2419 Prefix::new_unchecked(aid.to_string())
2420 }
2421
2422 fn receipt_from(aid: &str) -> WitnessReceipt {
2423 WitnessReceipt {
2424 witness: witness_aid(aid),
2425 signature: vec![],
2426 }
2427 }
2428
2429 fn receipts_under(said: &Said, aids: &[&str]) -> MapReceipts {
2430 let mut by_said = std::collections::HashMap::new();
2431 by_said.insert(
2432 said.as_str().to_string(),
2433 aids.iter().map(|a| receipt_from(a)).collect(),
2434 );
2435 MapReceipts { by_said }
2436 }
2437
2438 fn icp_with_backers(aids: &[&str], bt: u64) -> IcpEvent {
2440 let backers: Vec<Prefix> = aids.iter().map(|a| witness_aid(a)).collect();
2441 let (icp, _kp) = make_custom_signed_icp(|icp| {
2442 icp.b = backers.clone();
2443 icp.bt = Threshold::Simple(bt);
2444 });
2445 icp
2446 }
2447
2448 #[test]
2449 fn replay_bt_zero_accepts_without_receipts() {
2450 let (icp, _kp) = make_signed_icp(); let events = vec![Event::Icp(icp)];
2452 let lookup = MapReceipts {
2453 by_said: std::collections::HashMap::new(),
2454 };
2455 let outcome = validate_kel_with_receipts(&events, None, &lookup).unwrap();
2456 assert!(matches!(outcome, WitnessedReplay::Accepted(_)));
2457 }
2458
2459 #[test]
2460 fn replay_at_quorum_accepts() {
2461 let icp = icp_with_backers(&["BWit1", "BWit2"], 2);
2462 let said = icp.d.clone();
2463 let lookup = receipts_under(&said, &["BWit1", "BWit2"]);
2464 let events = vec![Event::Icp(icp)];
2465 let outcome = validate_kel_with_receipts(&events, None, &lookup).unwrap();
2466 assert!(matches!(outcome, WitnessedReplay::Accepted(_)));
2467 }
2468
2469 #[test]
2470 fn replay_under_quorum_is_pending() {
2471 let icp = icp_with_backers(&["BWit1", "BWit2"], 2);
2472 let said = icp.d.clone();
2473 let lookup = receipts_under(&said, &["BWit1"]); let events = vec![Event::Icp(icp)];
2475 match validate_kel_with_receipts(&events, None, &lookup).unwrap() {
2476 WitnessedReplay::Pending {
2477 sequence,
2478 collected,
2479 ..
2480 } => {
2481 assert_eq!(sequence, 0);
2482 assert_eq!(collected, 1);
2483 }
2484 WitnessedReplay::Accepted(_) => panic!("expected Pending under quorum"),
2485 }
2486 }
2487
2488 #[test]
2489 fn replay_ignores_duplicate_witness_receipts() {
2490 let icp = icp_with_backers(&["BWit1", "BWit2", "BWit3"], 2);
2491 let said = icp.d.clone();
2492 let lookup = receipts_under(&said, &["BWit1", "BWit1"]); let events = vec![Event::Icp(icp)];
2494 assert!(matches!(
2495 validate_kel_with_receipts(&events, None, &lookup).unwrap(),
2496 WitnessedReplay::Pending { .. }
2497 ));
2498 }
2499
2500 #[test]
2501 fn replay_ignores_receipt_for_wrong_said() {
2502 let icp = icp_with_backers(&["BWit1", "BWit2"], 2);
2503 let wrong = Said::new_unchecked("EWrongEventSaid".to_string());
2505 let lookup = receipts_under(&wrong, &["BWit1", "BWit2"]);
2506 let events = vec![Event::Icp(icp)];
2507 match validate_kel_with_receipts(&events, None, &lookup).unwrap() {
2508 WitnessedReplay::Pending { collected, .. } => assert_eq!(collected, 0),
2509 WitnessedReplay::Accepted(_) => panic!("wrong-SAID receipts must not count"),
2510 }
2511 }
2512
2513 #[test]
2514 fn replay_uses_witness_set_in_force_at_seq() {
2515 let kp2 = gen_keypair();
2518 let kp3 = gen_keypair();
2519 let commitment2 = crate::crypto::compute_next_commitment(
2520 &crate::keys::KeriPublicKey::ed25519(kp2.public_key().as_ref()).unwrap(),
2521 );
2522 let commitment3 = crate::crypto::compute_next_commitment(
2523 &crate::keys::KeriPublicKey::ed25519(kp3.public_key().as_ref()).unwrap(),
2524 );
2525 let (icp, _kp1) = make_custom_signed_icp(|icp| {
2526 icp.b = vec![witness_aid("BWit1")];
2527 icp.bt = Threshold::Simple(1);
2528 icp.n = vec![commitment2.clone()];
2529 });
2530 let prefix = icp.i.clone();
2531 let icp_said = icp.d.clone();
2532
2533 let mut rot = RotEvent {
2534 v: VersionString::placeholder(),
2535 d: Said::default(),
2536 i: prefix.clone(),
2537 s: KeriSequence::new(1),
2538 p: icp_said.clone(),
2539 kt: Threshold::Simple(1),
2540 k: vec![CesrKey::new_unchecked(encode_pubkey(&kp2))],
2541 nt: Threshold::Simple(1),
2542 n: vec![commitment3.clone()],
2543 bt: Threshold::Simple(1),
2544 br: vec![witness_aid("BWit1")],
2545 ba: vec![witness_aid("BWit2")],
2546 c: vec![],
2547 a: vec![],
2548 };
2549 let val = serde_json::to_value(Event::Rot(rot.clone())).unwrap();
2550 rot.d = compute_said(&val).unwrap();
2551 let rot_said = rot.d.clone();
2552
2553 let mut by_said = std::collections::HashMap::new();
2554 by_said.insert(icp_said.as_str().to_string(), vec![receipt_from("BWit1")]);
2555 by_said.insert(rot_said.as_str().to_string(), vec![receipt_from("BWit2")]);
2556 let lookup = MapReceipts { by_said };
2557
2558 let events = vec![Event::Icp(icp), Event::Rot(rot)];
2559 assert!(matches!(
2562 validate_kel_with_receipts(&events, None, &lookup).unwrap(),
2563 WitnessedReplay::Accepted(_)
2564 ));
2565 }
2566
2567 #[test]
2568 fn validate_kel_advances_without_receipt_gate() {
2569 let icp = icp_with_backers(&["BWit1", "BWit2"], 2);
2571 let events = vec![Event::Icp(icp)];
2572 assert!(validate_kel(&events).is_ok());
2573 }
2574}
2575
2576#[derive(Debug, Clone)]
2587pub struct KelPolicy {
2588 pub min_rotation_interval: chrono::Duration,
2591 pub clock_skew_tolerance: chrono::Duration,
2594 pub emergency_override_did: Option<crate::types::Prefix>,
2599}
2600
2601impl Default for KelPolicy {
2602 fn default() -> Self {
2603 Self {
2604 min_rotation_interval: chrono::Duration::hours(24),
2605 clock_skew_tolerance: chrono::Duration::seconds(60),
2606 emergency_override_did: None,
2607 }
2608 }
2609}
2610
2611pub(crate) fn validate_kel_with_policy(
2635 events: &[Event],
2636 timestamps: &[Option<chrono::DateTime<chrono::Utc>>],
2637 policy: &KelPolicy,
2638 now: chrono::DateTime<chrono::Utc>,
2639) -> Result<KeyState, ValidationError> {
2640 let state = validate_kel(events)?;
2641
2642 let mut last_rotation_dt: Option<chrono::DateTime<chrono::Utc>> = None;
2643 let mut last_any_dt: Option<chrono::DateTime<chrono::Utc>> = None;
2644
2645 for (idx, evt) in events.iter().enumerate() {
2646 let seq = idx as u128;
2647 let (is_rotation, controller) = match evt {
2648 Event::Icp(e) => (false, &e.i),
2649 Event::Rot(e) => (true, &e.i),
2650 Event::Ixn(e) => (false, &e.i),
2651 Event::Dip(e) => (false, &e.i),
2652 Event::Drt(e) => (true, &e.i),
2653 };
2654 let Some(dt) = timestamps.get(idx).copied().flatten() else {
2655 return Err(ValidationError::MissingTimestamp { sequence: seq });
2656 };
2657 if let Some(prev) = last_any_dt
2659 && dt < prev
2660 {
2661 return Err(ValidationError::NonMonotonicTimestamp {
2662 sequence: seq,
2663 prev: prev.to_rfc3339(),
2664 curr: dt.to_rfc3339(),
2665 });
2666 }
2667 let skew = (dt - now).num_seconds();
2669 if skew.abs() > policy.clock_skew_tolerance.num_seconds() {
2670 return Err(ValidationError::ClockSkew {
2671 sequence: seq,
2672 skew_secs: skew,
2673 tolerance_secs: policy.clock_skew_tolerance.num_seconds(),
2674 });
2675 }
2676 if is_rotation && let Some(prev) = last_rotation_dt {
2678 let interval = dt - prev;
2679 let is_override = policy
2680 .emergency_override_did
2681 .as_ref()
2682 .is_some_and(|ov| ov == controller);
2683 if !is_override && interval < policy.min_rotation_interval {
2684 return Err(ValidationError::RotationCooldown {
2685 sequence: seq,
2686 interval_secs: interval.num_seconds(),
2687 min_secs: policy.min_rotation_interval.num_seconds(),
2688 });
2689 }
2690 }
2691 last_any_dt = Some(dt);
2692 if is_rotation {
2693 last_rotation_dt = Some(dt);
2694 }
2695 }
2696
2697 Ok(state)
2698}
2699
2700#[cfg(test)]
2701mod policy_tests {
2702 use super::*;
2703 use chrono::{Duration as ChronoDuration, TimeZone, Utc};
2704
2705 fn base_now() -> chrono::DateTime<chrono::Utc> {
2706 Utc.with_ymd_and_hms(2026, 1, 1, 0, 0, 0).unwrap()
2707 }
2708
2709 #[test]
2710 fn policy_rejects_missing_dt_via_empty_kel_path() {
2711 let events: Vec<crate::events::Event> = vec![];
2715 let r = validate_kel_with_policy(&events, &[], &KelPolicy::default(), base_now());
2716 assert!(matches!(r, Err(ValidationError::EmptyKel)));
2717 }
2718
2719 #[test]
2720 fn policy_default_values_match_plan() {
2721 let p = KelPolicy::default();
2722 assert_eq!(p.min_rotation_interval, ChronoDuration::hours(24));
2723 assert_eq!(p.clock_skew_tolerance, ChronoDuration::seconds(60));
2724 assert!(p.emergency_override_did.is_none());
2725 }
2726}