use crate::crypto::verify_commitment;
use crate::events::{Event, IcpEvent, IxnEvent, KeriSequence, RotEvent, Seal, SourceSeal};
use crate::keys::KeriPublicKey;
use crate::said::compute_said;
use crate::state::KeyState;
use crate::types::{CesrKey, ConfigTrait, Prefix, Said, Threshold};
use crate::witness::WitnessReceiptLookup;
use crate::witness::agreement::{AgreementStatus, WitnessAgreement};
#[derive(Debug, Clone, thiserror::Error, PartialEq, Eq)]
#[non_exhaustive]
pub enum ValidationError {
#[error("Invalid SAID: expected {expected}, got {actual}")]
InvalidSaid {
expected: Said,
actual: Said,
},
#[error("Broken chain: event {sequence} references {referenced}, but previous was {actual}")]
BrokenChain {
sequence: u128,
referenced: Said,
actual: Said,
},
#[error("Invalid sequence: expected {expected}, got {actual}")]
InvalidSequence {
expected: u128,
actual: u128,
},
#[error("Pre-rotation commitment mismatch at sequence {sequence}")]
CommitmentMismatch {
sequence: u128,
},
#[error("Signature verification failed at sequence {sequence}")]
SignatureFailed {
sequence: u128,
},
#[error("Unsatisfiable threshold at sequence {sequence}: {reason}")]
ThresholdNotSatisfiable {
sequence: u128,
reason: String,
},
#[error("Invalid backer delta at sequence {sequence}: {reason}")]
InvalidBackerDelta {
sequence: u128,
reason: String,
},
#[error("Invalid backer role flip at sequence {sequence}: {reason}")]
BackerRoleFlip {
sequence: u128,
reason: String,
},
#[error(
"Asymmetric key rotation at sequence {sequence}: prior next count {prior_next_count} != new key count {new_key_count} (removing devices requires CESR indexed signatures)"
)]
AsymmetricKeyRotation {
sequence: u128,
prior_next_count: usize,
new_key_count: usize,
},
#[error(
"Delegator seal not found at sequence {sequence}: delegator {delegator_aid} has no ixn-anchored seal for this event"
)]
DelegatorSealNotFound {
sequence: u128,
delegator_aid: String,
},
#[error(
"Delegate source seal missing at sequence {sequence}: delegated event carries no -G back-reference to its anchoring event"
)]
DelegateSourceSealMissing {
sequence: u128,
},
#[error(
"Delegation source seal back-reference mismatch at sequence {sequence}: delegate points at a different anchoring event than the delegator's seal"
)]
SealBackRefMismatch {
sequence: u128,
},
#[error(
"Delegator lookup required for delegated event at sequence {sequence}; call validate_kel_with_lookup"
)]
DelegatorLookupMissing {
sequence: u128,
},
#[error("First event must be inception")]
NotInception,
#[error("Empty KEL")]
EmptyKel,
#[error("Multiple inception events in KEL")]
MultipleInceptions,
#[error("Serialization error: {0}")]
Serialization(String),
#[error("Malformed sequence number: {raw:?}")]
MalformedSequence {
raw: String,
},
#[error("Invalid key encoding: {0}")]
InvalidKey(String),
#[error("Identity abandoned at sequence {sequence}, no more events allowed")]
AbandonedIdentity {
sequence: u128,
},
#[error("Interaction event at sequence {sequence} rejected: KEL is establishment-only (EO)")]
EstablishmentOnly {
sequence: u128,
},
#[error(
"Non-transferable identity: inception had empty next key commitments, no subsequent events allowed"
)]
NonTransferable,
#[error("Duplicate backer AID: {aid}")]
DuplicateBacker {
aid: String,
},
#[error("Invalid backer threshold: bt={bt} but backer_count={backer_count}")]
InvalidBackerThreshold {
bt: u64,
backer_count: usize,
},
#[error("Policy violation: event at seq {sequence} missing `dt`")]
MissingTimestamp {
sequence: u128,
},
#[error(
"Policy violation: timestamps not monotonic at seq {sequence} (prev={prev}, curr={curr})"
)]
NonMonotonicTimestamp {
sequence: u128,
prev: String,
curr: String,
},
#[error(
"Policy violation: rotation cooldown breached at seq {sequence} (interval {interval_secs}s < minimum {min_secs}s)"
)]
RotationCooldown {
sequence: u128,
interval_secs: i64,
min_secs: i64,
},
#[error(
"Policy violation: clock skew at seq {sequence} ({skew_secs}s) exceeds tolerance ({tolerance_secs}s)"
)]
ClockSkew {
sequence: u128,
skew_secs: i64,
tolerance_secs: i64,
},
}
pub fn validate_delegation(
delegated_event: &Event,
delegator_kel: &[Event],
) -> Result<(), ValidationError> {
if !delegated_event.is_delegated() {
return Err(ValidationError::Serialization(
"validate_delegation called on non-delegated event".to_string(),
));
}
let event_said = delegated_event.said();
let event_seq = delegated_event.sequence();
if let Some(Event::Icp(delegator_icp)) = delegator_kel.first()
&& delegator_icp.c.contains(&ConfigTrait::DoNotDelegate)
{
return Err(ValidationError::Serialization(
"Delegator has DoNotDelegate (DND) config trait".to_string(),
));
}
let anchor = delegator_kel.iter().find_map(|event| {
let anchors = event.anchors().iter().any(|seal| {
matches!(
seal,
Seal::KeyEvent { i, s, d }
if i == delegated_event.prefix()
&& s.value() == event_seq.value()
&& d == event_said
)
});
anchors.then(|| SourceSeal {
s: event.sequence(),
d: event.said().clone(),
})
});
let Some(anchor) = anchor else {
return Err(ValidationError::Serialization(format!(
"No delegation seal found in delegator KEL for prefix={}, sn={}, said={}",
delegated_event.prefix(),
event_seq,
event_said
)));
};
enforce_source_seal(delegated_event.source_seal(), &anchor, event_seq.value())
}
fn enforce_source_seal(
source_seal: Option<&SourceSeal>,
anchor: &SourceSeal,
sequence: u128,
) -> Result<(), ValidationError> {
match source_seal {
None => Err(ValidationError::DelegateSourceSealMissing { sequence }),
Some(seal) if seal == anchor => Ok(()),
Some(_) => Err(ValidationError::SealBackRefMismatch { sequence }),
}
}
pub trait DelegatorKelLookup {
fn find_seal(&self, delegator_aid: &Prefix, seal_said: &Said) -> Option<SourceSeal>;
}
pub fn validate_kel(events: &[Event]) -> Result<KeyState, ValidationError> {
validate_kel_with_lookup(events, None::<&dyn DelegatorKelLookup>)
}
pub fn validate_kel_with_lookup(
events: &[Event],
lookup: Option<&dyn DelegatorKelLookup>,
) -> Result<KeyState, ValidationError> {
match replay_kel_gated(events, lookup, None)? {
WitnessedReplay::Accepted(state) => Ok(state),
WitnessedReplay::Pending { state, .. } => Ok(state),
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum WitnessedReplay {
Accepted(KeyState),
Pending {
state: KeyState,
sequence: u128,
said: Said,
required: Threshold,
collected: usize,
},
}
impl WitnessedReplay {
pub fn state(&self) -> &KeyState {
match self {
WitnessedReplay::Accepted(state) | WitnessedReplay::Pending { state, .. } => state,
}
}
}
pub fn validate_kel_with_receipts(
events: &[Event],
delegator_lookup: Option<&dyn DelegatorKelLookup>,
receipt_lookup: &dyn WitnessReceiptLookup,
) -> Result<WitnessedReplay, ValidationError> {
replay_kel_gated(events, delegator_lookup, Some(receipt_lookup))
}
fn replay_kel_gated(
events: &[Event],
lookup: Option<&dyn DelegatorKelLookup>,
receipt_lookup: Option<&dyn WitnessReceiptLookup>,
) -> Result<WitnessedReplay, ValidationError> {
if events.is_empty() {
return Err(ValidationError::EmptyKel);
}
verify_event_said(&events[0])?;
let (mut state, inception_n_is_empty, establishment_only) = match &events[0] {
Event::Icp(icp) => (
validate_inception(icp)?,
icp.n.is_empty(),
icp.c.contains(&ConfigTrait::EstablishmentOnly),
),
Event::Dip(dip) => (
validate_delegated_inception(dip, lookup)?,
dip.n.is_empty(),
dip.c.contains(&ConfigTrait::EstablishmentOnly),
),
_ => return Err(ValidationError::NotInception),
};
let controller = state.prefix.clone();
if let Some(rl) = receipt_lookup
&& let Some(pending) = gate_establishment(&controller, &state, 0, events[0].said(), rl)
{
return Ok(pending);
}
if inception_n_is_empty && events.len() > 1 {
return Err(ValidationError::NonTransferable);
}
for (idx, event) in events.iter().enumerate().skip(1) {
let expected_seq = idx as u128;
if state.is_abandoned {
return Err(ValidationError::AbandonedIdentity {
sequence: expected_seq,
});
}
if establishment_only && matches!(event, Event::Ixn(_)) {
return Err(ValidationError::EstablishmentOnly {
sequence: expected_seq,
});
}
verify_event_said(event)?;
verify_sequence(event, expected_seq)?;
verify_chain_linkage(event, &state)?;
match event {
Event::Rot(rot) => validate_rotation(rot, expected_seq, &mut state)?,
Event::Ixn(ixn) => validate_interaction(ixn, expected_seq, &mut state)?,
Event::Icp(_) | Event::Dip(_) => return Err(ValidationError::MultipleInceptions),
Event::Drt(drt) => {
validate_delegated_rotation(drt, expected_seq, &mut state, lookup)?;
}
}
if let Some(rl) = receipt_lookup
&& matches!(event, Event::Rot(_) | Event::Drt(_))
&& let Some(pending) =
gate_establishment(&controller, &state, expected_seq, event.said(), rl)
{
return Ok(pending);
}
}
Ok(WitnessedReplay::Accepted(state))
}
fn gate_establishment(
controller: &Prefix,
state: &KeyState,
sequence: u128,
event_said: &Said,
receipt_lookup: &dyn WitnessReceiptLookup,
) -> Option<WitnessedReplay> {
let sn = sequence as u64;
let agreement = WitnessAgreement::new(1);
agreement.submit_event(
controller,
sn,
event_said,
&state.backer_threshold,
&state.backers,
);
for receipt in receipt_lookup.receipts_for(controller, KeriSequence::new(sequence), event_said)
{
agreement.add_receipt(controller, sn, event_said, receipt.witness.as_str());
}
match agreement.status(controller, sn, event_said) {
AgreementStatus::Accepted => None,
AgreementStatus::Pending { collected } => Some(WitnessedReplay::Pending {
state: state.clone(),
sequence,
said: event_said.clone(),
required: state.backer_threshold.clone(),
collected,
}),
}
}
fn validate_backer_uniqueness(backers: &[Prefix]) -> Result<(), ValidationError> {
let mut seen = std::collections::HashSet::new();
for b in backers {
if !seen.insert(b.as_str()) {
return Err(ValidationError::DuplicateBacker {
aid: b.as_str().to_string(),
});
}
}
Ok(())
}
fn validate_thresholds(
sequence: u128,
kt: &Threshold,
k_len: usize,
nt: &Threshold,
n_len: usize,
bt: &Threshold,
b_len: usize,
) -> Result<(), ValidationError> {
let check = |t: &Threshold, len: usize, which: &str| {
t.validate_satisfiable(len)
.map_err(|e| ValidationError::ThresholdNotSatisfiable {
sequence,
reason: format!("{which}: {}", e.reason),
})
};
check(kt, k_len, "kt")?;
check(nt, n_len, "nt")?;
check(bt, b_len, "bt")?;
Ok(())
}
fn validate_inception(icp: &IcpEvent) -> Result<KeyState, ValidationError> {
validate_backer_uniqueness(&icp.b)?;
validate_thresholds(
icp.s.value(),
&icp.kt,
icp.k.len(),
&icp.nt,
icp.n.len(),
&icp.bt,
icp.b.len(),
)?;
let bt_val = icp.bt.simple_value().unwrap_or(0);
if icp.b.is_empty() && bt_val != 0 {
return Err(ValidationError::InvalidBackerThreshold {
bt: bt_val,
backer_count: 0,
});
}
Ok(KeyState::from_inception(
icp.i.clone(),
icp.k.clone(),
icp.n.clone(),
icp.kt.clone(),
icp.nt.clone(),
icp.d.clone(),
icp.b.clone(),
icp.bt.clone(),
icp.c.clone(),
))
}
fn verify_sequence(event: &Event, expected: u128) -> Result<(), ValidationError> {
let actual = event.sequence().value();
if actual != expected {
return Err(ValidationError::InvalidSequence { expected, actual });
}
Ok(())
}
fn verify_chain_linkage(event: &Event, state: &KeyState) -> Result<(), ValidationError> {
let prev_said = event.previous().ok_or(ValidationError::NotInception)?;
if *prev_said != state.last_event_said {
return Err(ValidationError::BrokenChain {
sequence: event.sequence().value(),
referenced: prev_said.clone(),
actual: state.last_event_said.clone(),
});
}
Ok(())
}
fn prior_commitments_satisfy_threshold(
next_commitment: &[Said],
next_threshold: &Threshold,
new_keys: &[CesrKey],
) -> bool {
let revealed: Vec<u32> = next_commitment
.iter()
.enumerate()
.filter_map(|(j, commitment)| {
let matched = new_keys.iter().any(|key| {
key.parse()
.map(|pk| verify_commitment(&pk, commitment))
.unwrap_or(false)
});
matched.then_some(j as u32)
})
.collect();
next_threshold.is_satisfied(&revealed, next_commitment.len())
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum BackerRole {
Registrar,
NoRegistrar,
Unspecified,
}
fn backer_role(traits: &[ConfigTrait]) -> BackerRole {
let mut role = BackerRole::Unspecified;
for t in traits {
match t {
ConfigTrait::RegistrarBackers => role = BackerRole::Registrar,
ConfigTrait::NoRegistrarBackers => role = BackerRole::NoRegistrar,
_ => {}
}
}
role
}
fn validate_rotation(
rot: &RotEvent,
sequence: u128,
state: &mut KeyState,
) -> Result<(), ValidationError> {
let post_backer_count =
state.backers.iter().filter(|b| !rot.br.contains(b)).count() + rot.ba.len();
validate_thresholds(
sequence,
&rot.kt,
rot.k.len(),
&rot.nt,
rot.n.len(),
&rot.bt,
post_backer_count,
)?;
if !state.next_commitment.is_empty()
&& !prior_commitments_satisfy_threshold(
&state.next_commitment,
&state.next_threshold,
&rot.k,
)
{
return Err(ValidationError::CommitmentMismatch { sequence });
}
validate_backer_uniqueness(&rot.br)?;
validate_backer_uniqueness(&rot.ba)?;
for aid in &rot.ba {
if rot.br.contains(aid) {
return Err(ValidationError::DuplicateBacker {
aid: aid.as_str().to_string(),
});
}
}
for aid in &rot.br {
if !state.backers.contains(aid) {
return Err(ValidationError::InvalidBackerDelta {
sequence,
reason: format!("br entry {} not in prior backers", aid.as_str()),
});
}
}
let survivors: Vec<_> = state
.backers
.iter()
.filter(|b| !rot.br.contains(b))
.collect();
for aid in &rot.ba {
if survivors.contains(&aid) {
return Err(ValidationError::InvalidBackerDelta {
sequence,
reason: format!("ba entry {} duplicates a surviving backer", aid.as_str()),
});
}
}
if !rot.c.is_empty() {
let old_role = backer_role(&state.config_traits);
let new_role = backer_role(&rot.c);
let is_flip = matches!(
(old_role, new_role),
(BackerRole::Registrar, BackerRole::NoRegistrar)
| (BackerRole::NoRegistrar, BackerRole::Registrar)
);
if is_flip && !survivors.is_empty() {
return Err(ValidationError::BackerRoleFlip {
sequence,
reason: format!(
"{old_role:?}->{new_role:?} but {} prior backer(s) survive; \
a role flip must cut all prior backers",
survivors.len()
),
});
}
}
state.apply_rotation(
rot.k.clone(),
rot.n.clone(),
rot.kt.clone(),
rot.nt.clone(),
sequence,
rot.d.clone(),
&rot.br,
&rot.ba,
rot.bt.clone(),
rot.c.clone(),
);
Ok(())
}
fn validate_interaction(
ixn: &IxnEvent,
sequence: u128,
state: &mut KeyState,
) -> Result<(), ValidationError> {
state
.current_key()
.ok_or(ValidationError::SignatureFailed { sequence })?;
state.apply_interaction(sequence, ixn.d.clone());
Ok(())
}
fn validate_delegated_inception(
dip: &crate::events::DipEvent,
lookup: Option<&dyn DelegatorKelLookup>,
) -> Result<KeyState, ValidationError> {
let sequence = dip.s.value();
let lookup = lookup.ok_or(ValidationError::DelegatorLookupMissing { sequence })?;
let anchor = lookup.find_seal(&dip.di, &dip.d).ok_or_else(|| {
ValidationError::DelegatorSealNotFound {
sequence,
delegator_aid: dip.di.as_str().to_string(),
}
})?;
enforce_source_seal(dip.source_seal.as_ref(), &anchor, sequence)?;
validate_backer_uniqueness(&dip.b)?;
let bt_val = dip.bt.simple_value().unwrap_or(0);
if dip.b.is_empty() && bt_val != 0 {
return Err(ValidationError::InvalidBackerThreshold {
bt: bt_val,
backer_count: 0,
});
}
let is_non_transferable = dip.n.is_empty();
Ok(KeyState {
prefix: dip.i.clone(),
current_keys: dip.k.clone(),
next_commitment: dip.n.clone(),
sequence: dip.s.value(),
last_event_said: dip.d.clone(),
is_abandoned: false,
threshold: dip.kt.clone(),
next_threshold: dip.nt.clone(),
backers: dip.b.clone(),
backer_threshold: dip.bt.clone(),
config_traits: dip.c.clone(),
is_non_transferable,
delegator: Some(dip.di.clone()),
last_establishment_sequence: dip.s.value(),
})
}
fn validate_delegated_rotation(
drt: &crate::events::DrtEvent,
sequence: u128,
state: &mut KeyState,
lookup: Option<&dyn DelegatorKelLookup>,
) -> Result<(), ValidationError> {
let lookup = lookup.ok_or(ValidationError::DelegatorLookupMissing { sequence })?;
let anchor = lookup.find_seal(&drt.di, &drt.d).ok_or_else(|| {
ValidationError::DelegatorSealNotFound {
sequence,
delegator_aid: drt.di.as_str().to_string(),
}
})?;
enforce_source_seal(drt.source_seal.as_ref(), &anchor, sequence)?;
if !state.next_commitment.is_empty()
&& !prior_commitments_satisfy_threshold(
&state.next_commitment,
&state.next_threshold,
&drt.k,
)
{
return Err(ValidationError::CommitmentMismatch { sequence });
}
validate_backer_uniqueness(&drt.br)?;
validate_backer_uniqueness(&drt.ba)?;
for aid in &drt.ba {
if drt.br.contains(aid) {
return Err(ValidationError::DuplicateBacker {
aid: aid.as_str().to_string(),
});
}
}
state.sequence = sequence;
state.last_event_said = drt.d.clone();
state.current_keys = drt.k.clone();
state.next_commitment = drt.n.clone();
state.threshold = drt.kt.clone();
state.next_threshold = drt.nt.clone();
Ok(())
}
pub fn replay_kel(events: &[Event]) -> Result<KeyState, ValidationError> {
validate_kel(events)
}
pub fn verify_event_crypto(
event: &Event,
current_state: Option<&KeyState>,
) -> Result<(), ValidationError> {
match event {
Event::Icp(icp) => {
if icp.k.is_empty() {
return Err(ValidationError::SignatureFailed { sequence: 0 });
}
let is_self_addressing = icp.i.as_str().starts_with('E');
if is_self_addressing {
if icp.i.as_str() != icp.d.as_str() {
return Err(ValidationError::InvalidSaid {
expected: icp.d.clone(),
actual: Said::new_unchecked(icp.i.as_str().to_string()),
});
}
} else {
let i_key = KeriPublicKey::parse(icp.i.as_str())
.map_err(|_| ValidationError::SignatureFailed { sequence: 0 })?;
let k0 = icp.k[0]
.parse()
.map_err(|_| ValidationError::SignatureFailed { sequence: 0 })?;
if i_key.as_bytes() != k0.as_bytes() {
return Err(ValidationError::InvalidSaid {
expected: Said::new_unchecked(icp.k[0].as_str().to_string()),
actual: Said::new_unchecked(icp.i.as_str().to_string()),
});
}
}
Ok(())
}
Event::Rot(rot) => {
let sequence = event.sequence().value();
let state = current_state.ok_or(ValidationError::SignatureFailed { sequence })?;
if state.is_abandoned || state.next_commitment.is_empty() {
return Err(ValidationError::CommitmentMismatch { sequence });
}
if rot.k.is_empty() {
return Err(ValidationError::SignatureFailed { sequence });
}
if !prior_commitments_satisfy_threshold(
&state.next_commitment,
&state.next_threshold,
&rot.k,
) {
return Err(ValidationError::CommitmentMismatch { sequence });
}
Ok(())
}
Event::Ixn(_) => {
let sequence = event.sequence().value();
let state = current_state.ok_or(ValidationError::SignatureFailed { sequence })?;
state
.current_key()
.ok_or(ValidationError::SignatureFailed { sequence })?;
Ok(())
}
Event::Dip(dip) => {
if dip.k.is_empty() {
return Err(ValidationError::SignatureFailed { sequence: 0 });
}
Ok(())
}
Event::Drt(drt) => {
let sequence = event.sequence().value();
let state = current_state.ok_or(ValidationError::SignatureFailed { sequence })?;
if state.is_abandoned || state.next_commitment.is_empty() {
return Err(ValidationError::CommitmentMismatch { sequence });
}
if drt.k.is_empty() {
return Err(ValidationError::SignatureFailed { sequence });
}
Ok(())
}
}
}
pub fn verify_event_said(event: &Event) -> Result<(), ValidationError> {
let value =
serde_json::to_value(event).map_err(|e| ValidationError::Serialization(e.to_string()))?;
let computed =
compute_said(&value).map_err(|e| ValidationError::Serialization(e.to_string()))?;
let actual = event.said();
if computed != *actual {
return Err(ValidationError::InvalidSaid {
expected: computed,
actual: actual.clone(),
});
}
Ok(())
}
pub fn validate_for_append(event: &Event, state: &KeyState) -> Result<(), ValidationError> {
if matches!(event, Event::Icp(_)) {
return Err(ValidationError::MultipleInceptions);
}
verify_event_said(event)?;
verify_sequence(event, state.sequence + 1)?;
verify_chain_linkage(event, state)?;
verify_event_crypto(event, Some(state))?;
Ok(())
}
pub fn compute_event_said(event: &Event) -> Result<Said, ValidationError> {
let value =
serde_json::to_value(event).map_err(|e| ValidationError::Serialization(e.to_string()))?;
compute_said(&value).map_err(|e| ValidationError::Serialization(e.to_string()))
}
pub fn serialize_for_signing(event: &Event) -> Result<Vec<u8>, ValidationError> {
serde_json::to_vec(event).map_err(|e| ValidationError::Serialization(e.to_string()))
}
pub fn validate_signed_event(
signed: &crate::events::SignedEvent,
current_state: Option<&KeyState>,
) -> Result<(), ValidationError> {
let event = &signed.event;
let sequence = event.sequence().value();
if signed.signatures.is_empty() {
return Err(ValidationError::SignatureFailed { sequence });
}
let (keys, threshold) = match event {
Event::Icp(icp) => (&icp.k, &icp.kt),
Event::Dip(dip) => (&dip.k, &dip.kt),
Event::Rot(rot) => (&rot.k, &rot.kt),
Event::Drt(drt) => (&drt.k, &drt.kt),
Event::Ixn(_) => {
let state = current_state.ok_or(ValidationError::SignatureFailed { sequence })?;
(&state.current_keys, &state.threshold)
}
};
if keys.is_empty() {
return Err(ValidationError::SignatureFailed { sequence });
}
let canonical = serialize_for_signing(event)?;
let mut verified_indices = Vec::new();
for sig in &signed.signatures {
let idx = sig.index as usize;
if idx >= keys.len() {
continue; }
let key = &keys[idx];
if let Ok(pk) = key.parse()
&& pk.verify_signature(&canonical, &sig.sig).is_ok()
{
verified_indices.push(sig.index);
}
}
if !threshold.is_satisfied(&verified_indices, keys.len()) {
return Err(ValidationError::SignatureFailed { sequence });
}
if matches!(event, Event::Rot(_) | Event::Drt(_))
&& let Some(state) = current_state
{
let n_len = state.next_commitment.len();
let mut verified_prior: Vec<u32> = Vec::new();
for sig in &signed.signatures {
let Some(key) = keys.get(sig.index as usize) else {
continue;
};
let Ok(pk) = key.parse() else {
continue;
};
if pk.verify_signature(&canonical, &sig.sig).is_err() {
continue;
}
let j = sig.prior_index.unwrap_or(sig.index) as usize;
let Some(commitment) = state.next_commitment.get(j) else {
continue;
};
if crate::crypto::verify_commitment(&pk, commitment) {
verified_prior.push(j as u32);
}
}
if n_len != keys.len() && verified_prior.is_empty() {
return Err(ValidationError::AsymmetricKeyRotation {
sequence,
prior_next_count: n_len,
new_key_count: keys.len(),
});
}
if !state.next_threshold.is_satisfied(&verified_prior, n_len) {
return Err(ValidationError::SignatureFailed { sequence });
}
}
Ok(())
}
pub fn finalize_icp_event(mut icp: IcpEvent) -> Result<IcpEvent, ValidationError> {
let value = serde_json::to_value(Event::Icp(icp.clone()))
.map_err(|e| ValidationError::Serialization(e.to_string()))?;
let said = compute_said(&value).map_err(|e| ValidationError::Serialization(e.to_string()))?;
icp.d = said.clone();
if icp.i.is_empty() || icp.i.as_str().starts_with('E') {
icp.i = Prefix::new_unchecked(said.into_inner());
}
let final_bytes = serde_json::to_vec(&Event::Icp(icp.clone()))
.map_err(|e| ValidationError::Serialization(e.to_string()))?;
icp.v = crate::types::VersionString::json(final_bytes.len() as u32);
Ok(icp)
}
pub fn finalize_dip_event(
mut dip: crate::events::DipEvent,
) -> Result<crate::events::DipEvent, ValidationError> {
let value = serde_json::to_value(Event::Dip(dip.clone()))
.map_err(|e| ValidationError::Serialization(e.to_string()))?;
let said = compute_said(&value).map_err(|e| ValidationError::Serialization(e.to_string()))?;
dip.d = said.clone();
if dip.i.is_empty() || dip.i.as_str().starts_with('E') {
dip.i = Prefix::new_unchecked(said.into_inner());
}
let final_bytes = serde_json::to_vec(&Event::Dip(dip.clone()))
.map_err(|e| ValidationError::Serialization(e.to_string()))?;
dip.v = crate::types::VersionString::json(final_bytes.len() as u32);
Ok(dip)
}
pub fn finalize_rot_event(mut rot: RotEvent) -> Result<RotEvent, ValidationError> {
let value = serde_json::to_value(Event::Rot(rot.clone()))
.map_err(|e| ValidationError::Serialization(e.to_string()))?;
let said = compute_said(&value).map_err(|e| ValidationError::Serialization(e.to_string()))?;
rot.d = said;
let final_bytes = serde_json::to_vec(&Event::Rot(rot.clone()))
.map_err(|e| ValidationError::Serialization(e.to_string()))?;
rot.v = crate::types::VersionString::json(final_bytes.len() as u32);
Ok(rot)
}
pub fn finalize_drt_event(
mut drt: crate::events::DrtEvent,
) -> Result<crate::events::DrtEvent, ValidationError> {
let value = serde_json::to_value(Event::Drt(drt.clone()))
.map_err(|e| ValidationError::Serialization(e.to_string()))?;
let said = compute_said(&value).map_err(|e| ValidationError::Serialization(e.to_string()))?;
drt.d = said;
let final_bytes = serde_json::to_vec(&Event::Drt(drt.clone()))
.map_err(|e| ValidationError::Serialization(e.to_string()))?;
drt.v = crate::types::VersionString::json(final_bytes.len() as u32);
Ok(drt)
}
pub fn finalize_ixn_event(mut ixn: IxnEvent) -> Result<IxnEvent, ValidationError> {
let value = serde_json::to_value(Event::Ixn(ixn.clone()))
.map_err(|e| ValidationError::Serialization(e.to_string()))?;
let said = compute_said(&value).map_err(|e| ValidationError::Serialization(e.to_string()))?;
ixn.d = said;
let final_bytes = serde_json::to_vec(&Event::Ixn(ixn.clone()))
.map_err(|e| ValidationError::Serialization(e.to_string()))?;
ixn.v = crate::types::VersionString::json(final_bytes.len() as u32);
Ok(ixn)
}
pub fn find_seal_in_kel(events: &[Event], digest: &str) -> Option<u128> {
for event in events {
if let Event::Ixn(ixn) = event {
for seal in &ixn.a {
if seal.digest_value().is_some_and(|d| d.as_str() == digest) {
return Some(ixn.s.value());
}
}
}
}
None
}
pub fn parse_kel_json(json: &str) -> Result<Vec<Event>, ValidationError> {
serde_json::from_str(json).map_err(|e| ValidationError::Serialization(e.to_string()))
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used)]
mod tests {
use super::*;
use crate::events::{IndexedSignature, KeriSequence, Seal, SignedEvent};
use crate::types::{CesrKey, Threshold, VersionString};
use ring::rand::SystemRandom;
use ring::signature::{Ed25519KeyPair, KeyPair};
fn gen_keypair() -> Ed25519KeyPair {
let rng = SystemRandom::new();
let pkcs8 = Ed25519KeyPair::generate_pkcs8(&rng).unwrap();
Ed25519KeyPair::from_pkcs8(pkcs8.as_ref()).unwrap()
}
fn encode_pubkey(kp: &Ed25519KeyPair) -> String {
crate::cesr_encode::encode_verkey(kp.public_key().as_ref(), cesride::matter::Codex::Ed25519)
.unwrap()
}
fn make_raw_icp(key: &str, next: &str) -> IcpEvent {
IcpEvent {
v: VersionString::placeholder(),
d: Said::default(),
i: Prefix::default(),
s: KeriSequence::new(0),
kt: Threshold::Simple(1),
k: vec![CesrKey::new_unchecked(key.to_string())],
nt: Threshold::Simple(1),
n: vec![Said::new_unchecked(next.to_string())],
bt: Threshold::Simple(0),
b: vec![],
c: vec![],
a: vec![],
}
}
fn make_signed_icp() -> (IcpEvent, Ed25519KeyPair) {
let rng = SystemRandom::new();
let pkcs8 = Ed25519KeyPair::generate_pkcs8(&rng).unwrap();
let keypair = Ed25519KeyPair::from_pkcs8(pkcs8.as_ref()).unwrap();
let key_encoded = encode_pubkey(&keypair);
let icp = IcpEvent {
v: VersionString::placeholder(),
d: Said::default(),
i: Prefix::default(),
s: KeriSequence::new(0),
kt: Threshold::Simple(1),
k: vec![CesrKey::new_unchecked(key_encoded)],
nt: Threshold::Simple(1),
n: vec![Said::new_unchecked("ENextCommitment".to_string())],
bt: Threshold::Simple(0),
b: vec![],
c: vec![],
a: vec![],
};
let finalized = finalize_icp_event(icp).unwrap();
(finalized, keypair)
}
fn make_signed_ixn(
prefix: &Prefix,
prev_said: &Said,
seq: u128,
_keypair: &Ed25519KeyPair,
) -> IxnEvent {
let mut ixn = IxnEvent {
v: VersionString::placeholder(),
d: Said::default(),
i: prefix.clone(),
s: KeriSequence::new(seq),
p: prev_said.clone(),
a: vec![Seal::digest("EAttest")],
};
let value = serde_json::to_value(Event::Ixn(ixn.clone())).unwrap();
ixn.d = compute_said(&value).unwrap();
ixn
}
#[test]
fn finalize_icp_sets_said() {
let icp = make_raw_icp("DKey1", "ENext1");
let finalized = finalize_icp_event(icp).unwrap();
assert!(!finalized.d.is_empty());
assert_eq!(finalized.d.as_str(), finalized.i.as_str());
assert!(finalized.d.as_str().starts_with('E'));
}
#[test]
fn validates_single_inception() {
let (icp, _keypair) = make_signed_icp();
let events = vec![Event::Icp(icp.clone())];
let state = validate_kel(&events).unwrap();
assert_eq!(state.prefix, icp.i);
assert_eq!(state.sequence, 0);
}
#[test]
fn rejects_empty_kel() {
let result = validate_kel(&[]);
assert!(matches!(result, Err(ValidationError::EmptyKel)));
}
#[test]
fn rejects_non_inception_first() {
let mut ixn = IxnEvent {
v: VersionString::placeholder(),
d: Said::default(),
i: Prefix::new_unchecked("ETest".to_string()),
s: KeriSequence::new(0),
p: Said::new_unchecked("EPrev".to_string()),
a: vec![],
};
let event = Event::Ixn(ixn.clone());
if let Ok(said) = compute_event_said(&event) {
ixn.d = said;
}
let events = vec![Event::Ixn(ixn)];
let result = validate_kel(&events);
assert!(matches!(result, Err(ValidationError::NotInception)));
}
#[test]
fn rejects_broken_sequence() {
let (icp, _keypair) = make_signed_icp();
let mut ixn = IxnEvent {
v: VersionString::placeholder(),
d: Said::default(),
i: icp.i.clone(),
s: KeriSequence::new(5),
p: icp.d.clone(),
a: vec![],
};
let value = serde_json::to_value(Event::Ixn(ixn.clone())).unwrap();
ixn.d = compute_said(&value).unwrap();
let events = vec![Event::Icp(icp), Event::Ixn(ixn)];
let result = validate_kel(&events);
assert!(matches!(
result,
Err(ValidationError::InvalidSequence {
expected: 1,
actual: 5
})
));
}
#[test]
fn rejects_broken_chain() {
let (icp, _keypair) = make_signed_icp();
let mut ixn = IxnEvent {
v: VersionString::placeholder(),
d: Said::default(),
i: icp.i.clone(),
s: KeriSequence::new(1),
p: Said::new_unchecked("EWrongPrevious".to_string()),
a: vec![],
};
let value = serde_json::to_value(Event::Ixn(ixn.clone())).unwrap();
ixn.d = compute_said(&value).unwrap();
let events = vec![Event::Icp(icp), Event::Ixn(ixn)];
let result = validate_kel(&events);
assert!(matches!(result, Err(ValidationError::BrokenChain { .. })));
}
#[test]
fn rejects_invalid_said() {
let icp = make_raw_icp("DKey1", "ENext1");
let finalized = finalize_icp_event(icp).unwrap();
let mut tampered = finalized.clone();
tampered.d = Said::new_unchecked("EWrongSaid".to_string());
let events = vec![Event::Icp(tampered)];
let result = validate_kel(&events);
assert!(matches!(result, Err(ValidationError::InvalidSaid { .. })));
}
#[test]
fn validates_icp_then_ixn() {
let (icp, keypair) = make_signed_icp();
let ixn = make_signed_ixn(&icp.i, &icp.d, 1, &keypair);
let events = vec![Event::Icp(icp), Event::Ixn(ixn.clone())];
let state = validate_kel(&events).unwrap();
assert_eq!(state.sequence, 1);
assert_eq!(state.last_event_said, ixn.d);
}
#[test]
fn compute_event_said_works() {
let icp = make_raw_icp("DKey1", "ENext1");
let event = Event::Icp(icp);
let said = compute_event_said(&event).unwrap();
assert!(said.as_str().starts_with('E'));
assert!(!said.is_empty());
}
#[test]
fn accepts_correct_signature() {
let (icp, keypair) = make_signed_icp();
let event = Event::Icp(icp);
let canonical = serialize_for_signing(&event).unwrap();
let sig = keypair.sign(&canonical).as_ref().to_vec();
let signed = SignedEvent::new(
event,
vec![IndexedSignature {
index: 0,
prior_index: None,
sig,
}],
);
validate_signed_event(&signed, None).expect("correct signature must validate");
}
#[test]
fn rejects_forged_signature() {
let (icp, _keypair) = make_signed_icp();
let event = Event::Icp(icp);
let forged_sig = vec![0u8; 64]; let signed = SignedEvent::new(
event,
vec![IndexedSignature {
index: 0,
prior_index: None,
sig: forged_sig,
}],
);
assert!(matches!(
validate_signed_event(&signed, None),
Err(ValidationError::SignatureFailed { sequence: 0 })
));
}
#[test]
fn rejects_wrong_key_signature() {
let committed = gen_keypair();
let key_encoded = encode_pubkey(&committed);
let icp = IcpEvent {
v: VersionString::placeholder(),
d: Said::default(),
i: Prefix::default(),
s: KeriSequence::new(0),
kt: Threshold::Simple(1),
k: vec![CesrKey::new_unchecked(key_encoded)],
nt: Threshold::Simple(1),
n: vec![Said::new_unchecked("ENextCommitment".to_string())],
bt: Threshold::Simple(0),
b: vec![],
c: vec![],
a: vec![],
};
let icp = finalize_icp_event(icp).unwrap();
let event = Event::Icp(icp);
let wrong = gen_keypair();
let canonical = serialize_for_signing(&event).unwrap();
let wrong_sig = wrong.sign(&canonical).as_ref().to_vec();
let signed = SignedEvent::new(
event,
vec![IndexedSignature {
index: 0,
prior_index: None,
sig: wrong_sig,
}],
);
assert!(matches!(
validate_signed_event(&signed, None),
Err(ValidationError::SignatureFailed { sequence: 0 })
));
}
#[test]
fn crypto_accepts_valid_inception() {
let (icp, _keypair) = make_signed_icp();
let result = verify_event_crypto(&Event::Icp(icp), None);
assert!(result.is_ok());
}
#[test]
fn find_seal_in_kel_finds_digest() {
let (icp, keypair) = make_signed_icp();
let ixn = make_signed_ixn(&icp.i, &icp.d, 1, &keypair);
let events = vec![Event::Icp(icp), Event::Ixn(ixn)];
assert_eq!(find_seal_in_kel(&events, "EAttest"), Some(1));
assert_eq!(find_seal_in_kel(&events, "ENonExistent"), None);
}
#[test]
fn parse_kel_json_rejects_invalid_hex_sequence() {
let json = r#"[{"v":"KERI10JSON","t":"icp","i":"E123","s":"not_hex","kt":"1","k":["DKey"],"nt":"1","n":["ENext"],"bt":"0","b":[]}]"#;
let result = parse_kel_json(json);
assert!(result.is_err(), "expected error for invalid hex sequence");
}
fn make_custom_signed_icp(customize: impl FnOnce(&mut IcpEvent)) -> (IcpEvent, Ed25519KeyPair) {
let rng = SystemRandom::new();
let pkcs8 = Ed25519KeyPair::generate_pkcs8(&rng).unwrap();
let keypair = Ed25519KeyPair::from_pkcs8(pkcs8.as_ref()).unwrap();
let key_encoded = encode_pubkey(&keypair);
let mut icp = IcpEvent {
v: VersionString::placeholder(),
d: Said::default(),
i: Prefix::default(),
s: KeriSequence::new(0),
kt: Threshold::Simple(1),
k: vec![CesrKey::new_unchecked(key_encoded)],
nt: Threshold::Simple(1),
n: vec![Said::new_unchecked("ENextCommitment".to_string())],
bt: Threshold::Simple(0),
b: vec![],
c: vec![],
a: vec![],
};
customize(&mut icp);
let finalized = finalize_icp_event(icp).unwrap();
(finalized, keypair)
}
#[test]
fn rejects_events_after_abandonment() {
let kp2 = gen_keypair();
let commitment2 = crate::crypto::compute_next_commitment(
&crate::keys::KeriPublicKey::ed25519(kp2.public_key().as_ref()).unwrap(),
);
let (icp, _kp1) = make_custom_signed_icp(|icp| {
icp.n = vec![commitment2.clone()];
});
let prefix = icp.i.clone();
let mut rot = RotEvent {
v: VersionString::placeholder(),
d: Said::default(),
i: prefix.clone(),
s: KeriSequence::new(1),
p: icp.d.clone(),
kt: Threshold::Simple(1),
k: vec![CesrKey::new_unchecked(encode_pubkey(&kp2))],
nt: Threshold::Simple(0),
n: vec![],
bt: Threshold::Simple(0),
br: vec![],
ba: vec![],
c: vec![],
a: vec![],
};
let val = serde_json::to_value(Event::Rot(rot.clone())).unwrap();
rot.d = compute_said(&val).unwrap();
let ixn = make_signed_ixn(&prefix, &rot.d, 2, &kp2);
let events = vec![Event::Icp(icp), Event::Rot(rot), Event::Ixn(ixn)];
let result = validate_kel(&events);
assert!(
matches!(result, Err(ValidationError::AbandonedIdentity { .. })),
"expected AbandonedIdentity, got: {result:?}"
);
}
#[test]
fn rejects_ixn_in_establishment_only_kel() {
let (icp, keypair) = make_custom_signed_icp(|icp| {
icp.c = vec![ConfigTrait::EstablishmentOnly];
});
let ixn = make_signed_ixn(&icp.i, &icp.d, 1, &keypair);
let events = vec![Event::Icp(icp), Event::Ixn(ixn)];
let result = validate_kel(&events);
assert!(
matches!(result, Err(ValidationError::EstablishmentOnly { .. })),
"expected EstablishmentOnly, got: {result:?}"
);
}
#[test]
fn rejects_events_after_non_transferable_inception() {
let (icp, keypair) = make_custom_signed_icp(|icp| {
icp.n = vec![];
icp.nt = Threshold::Simple(0);
});
let ixn = make_signed_ixn(&icp.i, &icp.d, 1, &keypair);
let events = vec![Event::Icp(icp), Event::Ixn(ixn)];
let result = validate_kel(&events);
assert!(
matches!(
result,
Err(ValidationError::NonTransferable)
| Err(ValidationError::AbandonedIdentity { .. })
),
"expected NonTransferable or AbandonedIdentity, got: {result:?}"
);
}
#[test]
fn rejects_duplicate_backers() {
let (_, result) = {
let rng = SystemRandom::new();
let pkcs8 = Ed25519KeyPair::generate_pkcs8(&rng).unwrap();
let keypair = Ed25519KeyPair::from_pkcs8(pkcs8.as_ref()).unwrap();
let key_encoded = encode_pubkey(&keypair);
let dup_backer = Prefix::new_unchecked("DWit1".to_string());
let icp = IcpEvent {
v: VersionString::placeholder(),
d: Said::default(),
i: Prefix::default(),
s: KeriSequence::new(0),
kt: Threshold::Simple(1),
k: vec![CesrKey::new_unchecked(key_encoded)],
nt: Threshold::Simple(1),
n: vec![Said::new_unchecked("ENextCommitment".to_string())],
bt: Threshold::Simple(2),
b: vec![dup_backer.clone(), dup_backer],
c: vec![],
a: vec![],
};
let finalized = finalize_icp_event(icp).unwrap();
let events = vec![Event::Icp(finalized)];
(keypair, validate_kel(&events))
};
assert!(
matches!(result, Err(ValidationError::DuplicateBacker { .. })),
"expected DuplicateBacker, got: {result:?}"
);
}
#[test]
fn rejects_invalid_backer_threshold() {
let (_, result) = {
let rng = SystemRandom::new();
let pkcs8 = Ed25519KeyPair::generate_pkcs8(&rng).unwrap();
let keypair = Ed25519KeyPair::from_pkcs8(pkcs8.as_ref()).unwrap();
let key_encoded = encode_pubkey(&keypair);
let icp = IcpEvent {
v: VersionString::placeholder(),
d: Said::default(),
i: Prefix::default(),
s: KeriSequence::new(0),
kt: Threshold::Simple(1),
k: vec![CesrKey::new_unchecked(key_encoded)],
nt: Threshold::Simple(1),
n: vec![Said::new_unchecked("ENextCommitment".to_string())],
bt: Threshold::Simple(2),
b: vec![],
c: vec![],
a: vec![],
};
let finalized = finalize_icp_event(icp).unwrap();
let events = vec![Event::Icp(finalized)];
(keypair, validate_kel(&events))
};
assert!(
matches!(result, Err(ValidationError::ThresholdNotSatisfiable { .. })),
"expected ThresholdNotSatisfiable, got: {result:?}"
);
}
#[test]
fn sign_over_finalized_bytes_roundtrips() {
let (icp, _kp) = make_signed_icp();
let bytes = serialize_for_signing(&Event::Icp(icp.clone())).unwrap();
assert_eq!(
bytes.len() as u32,
icp.v.size,
"signed byte length must equal the version-string size field"
);
let reparsed: Event = serde_json::from_slice(&bytes).unwrap();
assert!(reparsed.is_inception());
}
#[test]
fn threshold_rejects_kt_gt_k() {
let kp = gen_keypair();
let key = encode_pubkey(&kp);
let icp = IcpEvent {
v: VersionString::placeholder(),
d: Said::default(),
i: Prefix::default(),
s: KeriSequence::new(0),
kt: Threshold::Simple(5),
k: vec![CesrKey::new_unchecked(key)],
nt: Threshold::Simple(1),
n: vec![Said::new_unchecked("ENextCommitment".to_string())],
bt: Threshold::Simple(0),
b: vec![],
c: vec![],
a: vec![],
};
let finalized = finalize_icp_event(icp).unwrap();
let result = validate_kel(&[Event::Icp(finalized)]);
assert!(
matches!(result, Err(ValidationError::ThresholdNotSatisfiable { .. })),
"expected ThresholdNotSatisfiable, got: {result:?}"
);
}
#[test]
fn rotation_rejects_br_not_in_prior() {
let state = KeyState::from_inception(
Prefix::new_unchecked("EPrefix".to_string()),
vec![CesrKey::new_unchecked("DKey1".to_string())],
vec![], Threshold::Simple(1),
Threshold::Simple(0),
Said::new_unchecked("ESAID".to_string()),
vec![Prefix::new_unchecked("BWit1".to_string())],
Threshold::Simple(0),
vec![],
);
let make_rot = |br: Vec<Prefix>, ba: Vec<Prefix>| RotEvent {
v: VersionString::placeholder(),
d: Said::default(),
i: Prefix::new_unchecked("EPrefix".to_string()),
s: KeriSequence::new(1),
p: Said::new_unchecked("ESAID".to_string()),
kt: Threshold::Simple(1),
k: vec![CesrKey::new_unchecked("DKey2".to_string())],
nt: Threshold::Simple(0),
n: vec![],
bt: Threshold::Simple(0),
br,
ba,
c: vec![],
a: vec![],
};
let bad_cut = make_rot(vec![Prefix::new_unchecked("BWitX".to_string())], vec![]);
assert!(matches!(
validate_rotation(&bad_cut, 1, &mut state.clone()),
Err(ValidationError::InvalidBackerDelta { .. })
));
let bad_add = make_rot(vec![], vec![Prefix::new_unchecked("BWit1".to_string())]);
assert!(matches!(
validate_rotation(&bad_add, 1, &mut state.clone()),
Err(ValidationError::InvalidBackerDelta { .. })
));
let ok = make_rot(vec![Prefix::new_unchecked("BWit1".to_string())], vec![]);
assert!(validate_rotation(&ok, 1, &mut state.clone()).is_ok());
}
#[test]
fn rotation_rejects_silent_backer_role_flip() {
let nrb_state = || {
KeyState::from_inception(
Prefix::new_unchecked("EPrefix".to_string()),
vec![CesrKey::new_unchecked("DKey1".to_string())],
vec![],
Threshold::Simple(1),
Threshold::Simple(0),
Said::new_unchecked("ESAID".to_string()),
vec![Prefix::new_unchecked("BWit1".to_string())],
Threshold::Simple(0),
vec![ConfigTrait::NoRegistrarBackers],
)
};
let make_rot = |br: Vec<Prefix>, ba: Vec<Prefix>, c: Vec<ConfigTrait>| RotEvent {
v: VersionString::placeholder(),
d: Said::default(),
i: Prefix::new_unchecked("EPrefix".to_string()),
s: KeriSequence::new(1),
p: Said::new_unchecked("ESAID".to_string()),
kt: Threshold::Simple(1),
k: vec![CesrKey::new_unchecked("DKey2".to_string())],
nt: Threshold::Simple(0),
n: vec![],
bt: Threshold::Simple(0),
br,
ba,
c,
a: vec![],
};
let flip_keep = make_rot(vec![], vec![], vec![ConfigTrait::RegistrarBackers]);
assert!(matches!(
validate_rotation(&flip_keep, 1, &mut nrb_state()),
Err(ValidationError::BackerRoleFlip { .. })
));
let flip_rebuild = make_rot(
vec![Prefix::new_unchecked("BWit1".to_string())],
vec![],
vec![ConfigTrait::RegistrarBackers],
);
assert!(validate_rotation(&flip_rebuild, 1, &mut nrb_state()).is_ok());
let same_role = make_rot(vec![], vec![], vec![ConfigTrait::NoRegistrarBackers]);
assert!(validate_rotation(&same_role, 1, &mut nrb_state()).is_ok());
let inherit = make_rot(vec![], vec![], vec![]);
assert!(validate_rotation(&inherit, 1, &mut nrb_state()).is_ok());
}
use crate::witness::WitnessReceipt;
struct MapReceipts {
by_said: std::collections::HashMap<String, Vec<WitnessReceipt>>,
}
impl WitnessReceiptLookup for MapReceipts {
fn receipts_for(
&self,
_controller: &Prefix,
_sn: KeriSequence,
said: &Said,
) -> Vec<WitnessReceipt> {
self.by_said.get(said.as_str()).cloned().unwrap_or_default()
}
}
fn witness_aid(aid: &str) -> Prefix {
Prefix::new_unchecked(aid.to_string())
}
fn receipt_from(aid: &str) -> WitnessReceipt {
WitnessReceipt {
witness: witness_aid(aid),
signature: vec![],
}
}
fn receipts_under(said: &Said, aids: &[&str]) -> MapReceipts {
let mut by_said = std::collections::HashMap::new();
by_said.insert(
said.as_str().to_string(),
aids.iter().map(|a| receipt_from(a)).collect(),
);
MapReceipts { by_said }
}
fn icp_with_backers(aids: &[&str], bt: u64) -> IcpEvent {
let backers: Vec<Prefix> = aids.iter().map(|a| witness_aid(a)).collect();
let (icp, _kp) = make_custom_signed_icp(|icp| {
icp.b = backers.clone();
icp.bt = Threshold::Simple(bt);
});
icp
}
#[test]
fn replay_bt_zero_accepts_without_receipts() {
let (icp, _kp) = make_signed_icp(); let events = vec![Event::Icp(icp)];
let lookup = MapReceipts {
by_said: std::collections::HashMap::new(),
};
let outcome = validate_kel_with_receipts(&events, None, &lookup).unwrap();
assert!(matches!(outcome, WitnessedReplay::Accepted(_)));
}
#[test]
fn replay_at_quorum_accepts() {
let icp = icp_with_backers(&["BWit1", "BWit2"], 2);
let said = icp.d.clone();
let lookup = receipts_under(&said, &["BWit1", "BWit2"]);
let events = vec![Event::Icp(icp)];
let outcome = validate_kel_with_receipts(&events, None, &lookup).unwrap();
assert!(matches!(outcome, WitnessedReplay::Accepted(_)));
}
#[test]
fn replay_under_quorum_is_pending() {
let icp = icp_with_backers(&["BWit1", "BWit2"], 2);
let said = icp.d.clone();
let lookup = receipts_under(&said, &["BWit1"]); let events = vec![Event::Icp(icp)];
match validate_kel_with_receipts(&events, None, &lookup).unwrap() {
WitnessedReplay::Pending {
sequence,
collected,
..
} => {
assert_eq!(sequence, 0);
assert_eq!(collected, 1);
}
WitnessedReplay::Accepted(_) => panic!("expected Pending under quorum"),
}
}
#[test]
fn replay_ignores_duplicate_witness_receipts() {
let icp = icp_with_backers(&["BWit1", "BWit2", "BWit3"], 2);
let said = icp.d.clone();
let lookup = receipts_under(&said, &["BWit1", "BWit1"]); let events = vec![Event::Icp(icp)];
assert!(matches!(
validate_kel_with_receipts(&events, None, &lookup).unwrap(),
WitnessedReplay::Pending { .. }
));
}
#[test]
fn replay_ignores_receipt_for_wrong_said() {
let icp = icp_with_backers(&["BWit1", "BWit2"], 2);
let wrong = Said::new_unchecked("EWrongEventSaid".to_string());
let lookup = receipts_under(&wrong, &["BWit1", "BWit2"]);
let events = vec![Event::Icp(icp)];
match validate_kel_with_receipts(&events, None, &lookup).unwrap() {
WitnessedReplay::Pending { collected, .. } => assert_eq!(collected, 0),
WitnessedReplay::Accepted(_) => panic!("wrong-SAID receipts must not count"),
}
}
#[test]
fn replay_uses_witness_set_in_force_at_seq() {
let kp2 = gen_keypair();
let kp3 = gen_keypair();
let commitment2 = crate::crypto::compute_next_commitment(
&crate::keys::KeriPublicKey::ed25519(kp2.public_key().as_ref()).unwrap(),
);
let commitment3 = crate::crypto::compute_next_commitment(
&crate::keys::KeriPublicKey::ed25519(kp3.public_key().as_ref()).unwrap(),
);
let (icp, _kp1) = make_custom_signed_icp(|icp| {
icp.b = vec![witness_aid("BWit1")];
icp.bt = Threshold::Simple(1);
icp.n = vec![commitment2.clone()];
});
let prefix = icp.i.clone();
let icp_said = icp.d.clone();
let mut rot = RotEvent {
v: VersionString::placeholder(),
d: Said::default(),
i: prefix.clone(),
s: KeriSequence::new(1),
p: icp_said.clone(),
kt: Threshold::Simple(1),
k: vec![CesrKey::new_unchecked(encode_pubkey(&kp2))],
nt: Threshold::Simple(1),
n: vec![commitment3.clone()],
bt: Threshold::Simple(1),
br: vec![witness_aid("BWit1")],
ba: vec![witness_aid("BWit2")],
c: vec![],
a: vec![],
};
let val = serde_json::to_value(Event::Rot(rot.clone())).unwrap();
rot.d = compute_said(&val).unwrap();
let rot_said = rot.d.clone();
let mut by_said = std::collections::HashMap::new();
by_said.insert(icp_said.as_str().to_string(), vec![receipt_from("BWit1")]);
by_said.insert(rot_said.as_str().to_string(), vec![receipt_from("BWit2")]);
let lookup = MapReceipts { by_said };
let events = vec![Event::Icp(icp), Event::Rot(rot)];
assert!(matches!(
validate_kel_with_receipts(&events, None, &lookup).unwrap(),
WitnessedReplay::Accepted(_)
));
}
#[test]
fn validate_kel_advances_without_receipt_gate() {
let icp = icp_with_backers(&["BWit1", "BWit2"], 2);
let events = vec![Event::Icp(icp)];
assert!(validate_kel(&events).is_ok());
}
}
#[derive(Debug, Clone)]
pub struct KelPolicy {
pub min_rotation_interval: chrono::Duration,
pub clock_skew_tolerance: chrono::Duration,
pub emergency_override_did: Option<crate::types::Prefix>,
}
impl Default for KelPolicy {
fn default() -> Self {
Self {
min_rotation_interval: chrono::Duration::hours(24),
clock_skew_tolerance: chrono::Duration::seconds(60),
emergency_override_did: None,
}
}
}
pub fn validate_kel_with_policy(
events: &[Event],
timestamps: &[Option<chrono::DateTime<chrono::Utc>>],
policy: &KelPolicy,
now: chrono::DateTime<chrono::Utc>,
) -> Result<KeyState, ValidationError> {
let state = validate_kel(events)?;
let mut last_rotation_dt: Option<chrono::DateTime<chrono::Utc>> = None;
let mut last_any_dt: Option<chrono::DateTime<chrono::Utc>> = None;
for (idx, evt) in events.iter().enumerate() {
let seq = idx as u128;
let (is_rotation, controller) = match evt {
Event::Icp(e) => (false, &e.i),
Event::Rot(e) => (true, &e.i),
Event::Ixn(e) => (false, &e.i),
Event::Dip(e) => (false, &e.i),
Event::Drt(e) => (true, &e.i),
};
let Some(dt) = timestamps.get(idx).copied().flatten() else {
return Err(ValidationError::MissingTimestamp { sequence: seq });
};
if let Some(prev) = last_any_dt
&& dt < prev
{
return Err(ValidationError::NonMonotonicTimestamp {
sequence: seq,
prev: prev.to_rfc3339(),
curr: dt.to_rfc3339(),
});
}
let skew = (dt - now).num_seconds();
if skew.abs() > policy.clock_skew_tolerance.num_seconds() {
return Err(ValidationError::ClockSkew {
sequence: seq,
skew_secs: skew,
tolerance_secs: policy.clock_skew_tolerance.num_seconds(),
});
}
if is_rotation && let Some(prev) = last_rotation_dt {
let interval = dt - prev;
let is_override = policy
.emergency_override_did
.as_ref()
.is_some_and(|ov| ov == controller);
if !is_override && interval < policy.min_rotation_interval {
return Err(ValidationError::RotationCooldown {
sequence: seq,
interval_secs: interval.num_seconds(),
min_secs: policy.min_rotation_interval.num_seconds(),
});
}
}
last_any_dt = Some(dt);
if is_rotation {
last_rotation_dt = Some(dt);
}
}
Ok(state)
}
#[cfg(test)]
mod policy_tests {
use super::*;
use chrono::{Duration as ChronoDuration, TimeZone, Utc};
fn base_now() -> chrono::DateTime<chrono::Utc> {
Utc.with_ymd_and_hms(2026, 1, 1, 0, 0, 0).unwrap()
}
#[test]
fn policy_rejects_missing_dt_via_empty_kel_path() {
let events: Vec<crate::events::Event> = vec![];
let r = validate_kel_with_policy(&events, &[], &KelPolicy::default(), base_now());
assert!(matches!(r, Err(ValidationError::EmptyKel)));
}
#[test]
fn policy_default_values_match_plan() {
let p = KelPolicy::default();
assert_eq!(p.min_rotation_interval, ChronoDuration::hours(24));
assert_eq!(p.clock_skew_tolerance, ChronoDuration::seconds(60));
assert!(p.emergency_override_did.is_none());
}
}