use serde::{Deserialize, Serialize};
use crate::witness::StoredReceipt;
use crate::witness::agreement::{AgreementStatus, WitnessAgreement};
use crate::{CesrKey, KeyState};
pub const KSN_VERSION: u32 = 1;
pub const KSN_TYPE: &str = "ksn";
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum KsnError {
#[error("KSN serialization failed: {0}")]
Serialize(String),
#[error("KSN signing failed: {0}")]
Signer(String),
#[error("KSN names no current key")]
NoCurrentKey,
#[error("not a KSN (t = {0:?})")]
WrongType(String),
#[error("KSN signature is invalid")]
BadSignature,
#[error("KSN current key is undecodable: {0}")]
UndecodableKey(String),
#[error("KSN is stale: seq {got} < last-seen {seen}")]
Stale {
got: u128,
seen: u128,
},
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct KeyStateNotice {
pub version: u32,
pub t: String,
pub state: KeyState,
pub dt: String,
}
impl KeyStateNotice {
pub fn new(state: KeyState, dt: impl Into<String>) -> Self {
Self {
version: KSN_VERSION,
t: KSN_TYPE.to_string(),
state,
dt: dt.into(),
}
}
pub fn canonical_bytes(&self) -> Result<Vec<u8>, KsnError> {
serde_json::to_vec(self).map_err(|e| KsnError::Serialize(e.to_string()))
}
pub fn signing_key(&self) -> Option<&CesrKey> {
self.state.current_keys.first()
}
pub fn sequence(&self) -> u128 {
self.state.sequence
}
pub fn names_delegated_device(&self) -> bool {
self.state.delegator.is_some()
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SignedKsn {
pub notice: KeyStateNotice,
#[serde(with = "hex::serde")]
pub signature: Vec<u8>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub receipts: Vec<StoredReceipt>,
}
impl SignedKsn {
pub fn sign_with(
notice: KeyStateNotice,
signer: impl FnOnce(&[u8]) -> Result<Vec<u8>, String>,
) -> Result<Self, KsnError> {
if notice.signing_key().is_none() {
return Err(KsnError::NoCurrentKey);
}
let bytes = notice.canonical_bytes()?;
let signature = signer(&bytes).map_err(KsnError::Signer)?;
Ok(Self {
notice,
signature,
receipts: Vec::new(),
})
}
pub fn with_receipts(mut self, candidates: Vec<StoredReceipt>) -> Self {
let state = &self.notice.state;
let said = state.last_event_said.clone();
let valid: Vec<StoredReceipt> = candidates
.into_iter()
.filter(|r| {
r.signed.receipt.d == said
&& state.backers.iter().any(|b| b == &r.witness)
&& receipt_signature_valid(r)
})
.collect();
self.receipts = valid;
self
}
pub fn verify(&self) -> Result<VerifiedKsn, KsnError> {
if self.notice.t != KSN_TYPE {
return Err(KsnError::WrongType(self.notice.t.clone()));
}
let key_cesr = self.notice.signing_key().ok_or(KsnError::NoCurrentKey)?;
let key = crate::KeriPublicKey::parse(key_cesr.as_str())
.map_err(|e| KsnError::UndecodableKey(e.to_string()))?;
let bytes = self.notice.canonical_bytes()?;
key.verify_signature(&bytes, &self.signature)
.map_err(|_| KsnError::BadSignature)?;
Ok(VerifiedKsn {
state: self.notice.state.clone(),
trust: self.witness_trust(),
})
}
fn witness_trust(&self) -> KsnTrust {
let state = &self.notice.state;
let required = state.backer_threshold.simple_value().unwrap_or(0) as usize;
if state.backers.is_empty() || required == 0 {
return KsnTrust::TrustOnFirstSight;
}
let said = &state.last_event_said;
let sn = state.sequence as u64;
let agreement = WitnessAgreement::new(1);
agreement.submit_event(
&state.prefix,
sn,
said,
&state.backer_threshold,
&state.backers,
);
let mut distinct = std::collections::HashSet::new();
for r in &self.receipts {
if &r.signed.receipt.d == said && state.backers.iter().any(|b| b == &r.witness) {
agreement.add_receipt(&state.prefix, sn, said, r.witness.as_str());
distinct.insert(r.witness.as_str());
}
}
match agreement.status(&state.prefix, sn, said) {
AgreementStatus::Accepted => KsnTrust::Witnessed {
receipts: distinct.len(),
threshold: required,
},
AgreementStatus::Pending { .. } => KsnTrust::TrustOnFirstSight,
}
}
pub fn check_not_stale(&self, last_seen_seq: u128) -> Result<(), KsnError> {
let got = self.notice.sequence();
if got < last_seen_seq {
return Err(KsnError::Stale {
got,
seen: last_seen_seq,
});
}
Ok(())
}
}
fn receipt_signature_valid(stored: &StoredReceipt) -> bool {
let Ok(key) = crate::KeriPublicKey::parse(stored.witness.as_str()) else {
return false;
};
let Ok(payload) = serde_json::to_vec(&stored.signed.receipt) else {
return false;
};
key.verify_signature(&payload, &stored.signed.signature)
.is_ok()
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum KsnTrust {
TrustOnFirstSight,
Witnessed {
receipts: usize,
threshold: usize,
},
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct VerifiedKsn {
pub state: KeyState,
pub trust: KsnTrust,
}
impl VerifiedKsn {
pub fn is_authoritative_over_kel(&self) -> bool {
false
}
pub fn satisfies_revocation_check(&self) -> bool {
false
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used)]
mod tests {
use super::*;
use crate::{KeriPublicKey, Prefix, Said, Threshold};
use ring::rand::SystemRandom;
use ring::signature::{Ed25519KeyPair, KeyPair};
fn real_keypair() -> Ed25519KeyPair {
let rng = SystemRandom::new();
let pkcs8 = Ed25519KeyPair::generate_pkcs8(&rng).unwrap();
Ed25519KeyPair::from_pkcs8(pkcs8.as_ref()).unwrap()
}
fn state_for_key(kp: &Ed25519KeyPair, seq: u128) -> KeyState {
let cesr = KeriPublicKey::ed25519(kp.public_key().as_ref())
.unwrap()
.to_qb64()
.unwrap();
let mut state = key_state(seq, false);
state.current_keys = vec![CesrKey::new_unchecked(cesr)];
state
}
fn sign_ksn(kp: &Ed25519KeyPair, notice: KeyStateNotice) -> SignedKsn {
SignedKsn::sign_with(notice, |bytes| Ok(kp.sign(bytes).as_ref().to_vec())).unwrap()
}
fn key_state(seq: u128, delegated: bool) -> KeyState {
let key = KeriPublicKey::ed25519(&[3u8; 32]).unwrap();
let mut state = KeyState::from_inception(
Prefix::new_unchecked("EksnTestPrefix000000000000000000000000000000".to_string()),
vec![CesrKey::new_unchecked(key.to_qb64().unwrap())],
vec![Said::new_unchecked(
"ENextCommitment0000000000000000000000000000".to_string(),
)],
Threshold::Simple(1),
Threshold::Simple(1),
Said::new_unchecked("ELastEvent00000000000000000000000000000000000".to_string()),
vec![],
Threshold::Simple(0),
vec![],
);
state.sequence = seq;
if delegated {
state.delegator = Some(Prefix::new_unchecked(
"ERootDelegator00000000000000000000000000000".to_string(),
));
}
state
}
#[test]
fn canonical_bytes_is_deterministic() {
let notice = KeyStateNotice::new(key_state(2, false), "2026-06-03T00:00:00Z");
assert_eq!(
notice.canonical_bytes().unwrap(),
notice.canonical_bytes().unwrap()
);
}
#[test]
fn sign_with_and_round_trips() {
let notice = KeyStateNotice::new(key_state(0, false), "2026-06-03T00:00:00Z");
let signed = SignedKsn::sign_with(notice, |_| Ok(vec![7u8; 64])).unwrap();
assert_eq!(signed.signature, vec![7u8; 64]);
assert!(signed.receipts.is_empty());
let json = serde_json::to_string(&signed).unwrap();
assert!(!json.contains("receipts"));
let parsed: SignedKsn = serde_json::from_str(&json).unwrap();
assert_eq!(parsed, signed);
}
#[test]
fn sign_with_rejects_no_current_key() {
let mut state = key_state(0, false);
state.current_keys.clear();
let notice = KeyStateNotice::new(state, "2026-06-03T00:00:00Z");
let err = SignedKsn::sign_with(notice, |_| Ok(vec![0u8; 64])).unwrap_err();
assert!(matches!(err, KsnError::NoCurrentKey));
}
#[test]
fn signer_error_propagates() {
let notice = KeyStateNotice::new(key_state(0, false), "2026-06-03T00:00:00Z");
let err = SignedKsn::sign_with(notice, |_| Err("keychain locked".to_string())).unwrap_err();
assert!(matches!(err, KsnError::Signer(_)));
}
#[test]
fn delegated_device_is_flagged() {
assert!(KeyStateNotice::new(key_state(1, true), "t").names_delegated_device());
assert!(!KeyStateNotice::new(key_state(1, false), "t").names_delegated_device());
}
#[test]
fn verify_accepts_valid_ksn() {
let kp = real_keypair();
let notice = KeyStateNotice::new(state_for_key(&kp, 1), "2026-06-03T00:00:00Z");
let signed = sign_ksn(&kp, notice);
let verified = signed.verify().unwrap();
assert_eq!(verified.trust, KsnTrust::TrustOnFirstSight);
assert!(!verified.is_authoritative_over_kel());
assert!(!verified.satisfies_revocation_check());
}
#[test]
fn verify_rejects_tampered_notice() {
let kp = real_keypair();
let notice = KeyStateNotice::new(state_for_key(&kp, 1), "2026-06-03T00:00:00Z");
let mut signed = sign_ksn(&kp, notice);
signed.notice.dt = "2099-01-01T00:00:00Z".to_string(); assert!(matches!(signed.verify(), Err(KsnError::BadSignature)));
}
#[test]
fn verify_rejects_signature_by_non_current_key() {
let signer = real_keypair();
let other = real_keypair();
let notice = KeyStateNotice::new(state_for_key(&other, 1), "2026-06-03T00:00:00Z");
let signed = sign_ksn(&signer, notice);
assert!(matches!(signed.verify(), Err(KsnError::BadSignature)));
}
#[test]
fn verify_rejects_unsigned_or_garbage_signature() {
let kp = real_keypair();
let notice = KeyStateNotice::new(state_for_key(&kp, 1), "2026-06-03T00:00:00Z");
let mut signed = sign_ksn(&kp, notice);
signed.signature = vec![0u8; 64]; assert!(matches!(signed.verify(), Err(KsnError::BadSignature)));
}
#[test]
fn verify_rejects_wrong_type() {
let kp = real_keypair();
let mut notice = KeyStateNotice::new(state_for_key(&kp, 1), "t");
notice.t = "rpy".to_string();
let signed = sign_ksn(&kp, notice);
assert!(matches!(signed.verify(), Err(KsnError::WrongType(_))));
}
#[test]
fn check_not_stale_rejects_rollback() {
let kp = real_keypair();
let signed = sign_ksn(&kp, KeyStateNotice::new(state_for_key(&kp, 2), "t"));
assert!(matches!(
signed.check_not_stale(3),
Err(KsnError::Stale { .. })
));
assert!(signed.check_not_stale(2).is_ok());
assert!(signed.check_not_stale(1).is_ok());
}
use crate::witness::{Receipt, ReceiptTag, SignedReceipt};
use crate::{KeriSequence, VersionString};
fn witness_kp_and_aid() -> (Ed25519KeyPair, String) {
let kp = real_keypair();
let aid = KeriPublicKey::ed25519(kp.public_key().as_ref())
.unwrap()
.to_qb64()
.unwrap();
(kp, aid)
}
fn witness_receipt(
witness_kp: &Ed25519KeyPair,
witness_aid: &str,
controller: &str,
seq: u128,
event_said: &str,
) -> StoredReceipt {
let receipt = Receipt {
v: VersionString::placeholder(),
t: ReceiptTag,
d: Said::new_unchecked(event_said.to_string()),
i: Prefix::new_unchecked(controller.to_string()),
s: KeriSequence::new(seq),
};
let payload = serde_json::to_vec(&receipt).unwrap();
let signature = witness_kp.sign(&payload).as_ref().to_vec();
StoredReceipt {
signed: SignedReceipt { receipt, signature },
witness: Prefix::new_unchecked(witness_aid.to_string()),
}
}
fn witnessed_state(
controller_kp: &Ed25519KeyPair,
seq: u128,
backers: &[&str],
bt: u64,
delegated: bool,
) -> KeyState {
let mut state = state_for_key(controller_kp, seq);
state.backers = backers
.iter()
.map(|a| Prefix::new_unchecked(a.to_string()))
.collect();
state.backer_threshold = Threshold::Simple(bt);
if delegated {
state.delegator = Some(Prefix::new_unchecked(
"ERootDelegator00000000000000000000000000000".to_string(),
));
}
state
}
#[test]
fn ksn_witnessed_when_quorum_met() {
let ckp = real_keypair();
let (w1kp, w1) = witness_kp_and_aid();
let (w2kp, w2) = witness_kp_and_aid();
let state = witnessed_state(&ckp, 1, &[&w1, &w2], 2, false);
let controller = state.prefix.as_str().to_string();
let said = state.last_event_said.as_str().to_string();
let signed = sign_ksn(&ckp, KeyStateNotice::new(state, "t")).with_receipts(vec![
witness_receipt(&w1kp, &w1, &controller, 1, &said),
witness_receipt(&w2kp, &w2, &controller, 1, &said),
]);
assert_eq!(
signed.verify().unwrap().trust,
KsnTrust::Witnessed {
receipts: 2,
threshold: 2
}
);
}
#[test]
fn ksn_stays_tofu_under_quorum() {
let ckp = real_keypair();
let (w1kp, w1) = witness_kp_and_aid();
let (_w2kp, w2) = witness_kp_and_aid();
let state = witnessed_state(&ckp, 1, &[&w1, &w2], 2, false);
let controller = state.prefix.as_str().to_string();
let said = state.last_event_said.as_str().to_string();
let signed = sign_ksn(&ckp, KeyStateNotice::new(state, "t"))
.with_receipts(vec![witness_receipt(&w1kp, &w1, &controller, 1, &said)]);
assert_eq!(signed.verify().unwrap().trust, KsnTrust::TrustOnFirstSight);
}
#[test]
fn ksn_ignores_duplicate_witness_receipts() {
let ckp = real_keypair();
let (w1kp, w1) = witness_kp_and_aid();
let (_w2kp, w2) = witness_kp_and_aid();
let (_w3kp, w3) = witness_kp_and_aid();
let state = witnessed_state(&ckp, 1, &[&w1, &w2, &w3], 2, false);
let controller = state.prefix.as_str().to_string();
let said = state.last_event_said.as_str().to_string();
let signed = sign_ksn(&ckp, KeyStateNotice::new(state, "t")).with_receipts(vec![
witness_receipt(&w1kp, &w1, &controller, 1, &said),
witness_receipt(&w1kp, &w1, &controller, 1, &said),
]);
assert_eq!(signed.verify().unwrap().trust, KsnTrust::TrustOnFirstSight);
}
#[test]
fn ksn_ignores_receipt_for_wrong_said() {
let ckp = real_keypair();
let (w1kp, w1) = witness_kp_and_aid();
let (w2kp, w2) = witness_kp_and_aid();
let state = witnessed_state(&ckp, 1, &[&w1, &w2], 2, false);
let controller = state.prefix.as_str().to_string();
let mut signed = sign_ksn(&ckp, KeyStateNotice::new(state, "t"));
let wrong = "EWrongEventSaid0000000000000000000000000000";
signed.receipts = vec![
witness_receipt(&w1kp, &w1, &controller, 1, wrong),
witness_receipt(&w2kp, &w2, &controller, 1, wrong),
];
assert_eq!(signed.verify().unwrap().trust, KsnTrust::TrustOnFirstSight);
}
#[test]
fn ksn_bt_zero_stays_tofu() {
let ckp = real_keypair();
let signed = sign_ksn(&ckp, KeyStateNotice::new(state_for_key(&ckp, 1), "t"));
assert_eq!(signed.verify().unwrap().trust, KsnTrust::TrustOnFirstSight);
}
#[test]
fn witnessed_device_ksn_still_refuses_revocation() {
let ckp = real_keypair();
let (w1kp, w1) = witness_kp_and_aid();
let (w2kp, w2) = witness_kp_and_aid();
let state = witnessed_state(&ckp, 1, &[&w1, &w2], 2, true); let controller = state.prefix.as_str().to_string();
let said = state.last_event_said.as_str().to_string();
let signed = sign_ksn(&ckp, KeyStateNotice::new(state, "t")).with_receipts(vec![
witness_receipt(&w1kp, &w1, &controller, 1, &said),
witness_receipt(&w2kp, &w2, &controller, 1, &said),
]);
let v = signed.verify().unwrap();
assert!(matches!(v.trust, KsnTrust::Witnessed { .. }));
assert!(!v.satisfies_revocation_check());
assert!(!v.is_authoritative_over_kel());
}
#[test]
fn populating_receipts_preserves_controller_signature() {
let ckp = real_keypair();
let (w1kp, w1) = witness_kp_and_aid();
let (w2kp, w2) = witness_kp_and_aid();
let state = witnessed_state(&ckp, 1, &[&w1, &w2], 2, false);
let controller = state.prefix.as_str().to_string();
let said = state.last_event_said.as_str().to_string();
let signed = sign_ksn(&ckp, KeyStateNotice::new(state, "t"));
assert!(signed.verify().is_ok());
let published = signed.with_receipts(vec![
witness_receipt(&w1kp, &w1, &controller, 1, &said),
witness_receipt(&w2kp, &w2, &controller, 1, &said),
]);
let v = published
.verify()
.expect("controller signature must still verify after attaching receipts");
assert!(matches!(v.trust, KsnTrust::Witnessed { .. }));
}
}