Skip to main content

auths_verifier/
keri.rs

1//! Stateless KERI KEL verification.
2//!
3//! This module provides verification of KERI event logs without requiring
4//! Git or filesystem access. Events are provided as input, making it
5//! suitable for WASM and FFI consumers.
6
7use std::borrow::Borrow;
8use std::fmt;
9
10use auths_crypto::CryptoProvider;
11use base64::{Engine, engine::general_purpose::URL_SAFE_NO_PAD};
12use serde::ser::SerializeMap;
13use serde::{Deserialize, Serialize, Serializer};
14use subtle::ConstantTimeEq;
15
16// ── KERI Identifier Newtypes ────────────────────────────────────────────────
17
18/// Error when constructing KERI newtypes with invalid values.
19#[derive(Debug, Clone, thiserror::Error, PartialEq, Eq)]
20#[error("Invalid KERI {type_name}: {reason}")]
21pub struct KeriTypeError {
22    /// Which KERI type failed validation.
23    pub type_name: &'static str,
24    /// Why validation failed.
25    pub reason: String,
26}
27
28/// Shared validation for KERI self-addressing identifiers.
29///
30/// Both `Prefix` and `Said` must start with 'E' (Blake3-256 derivation code).
31fn validate_keri_derivation_code(s: &str, type_label: &'static str) -> Result<(), KeriTypeError> {
32    if s.is_empty() {
33        return Err(KeriTypeError {
34            type_name: type_label,
35            reason: "must not be empty".into(),
36        });
37    }
38    if !s.starts_with('E') {
39        return Err(KeriTypeError {
40            type_name: type_label,
41            reason: format!(
42                "must start with 'E' (Blake3 derivation code), got '{}'",
43                &s[..s.len().min(10)]
44            ),
45        });
46    }
47    Ok(())
48}
49
50/// Strongly-typed KERI identifier prefix (e.g., `"ETest123..."`).
51///
52/// A prefix is the self-addressing identifier derived from the inception event's
53/// Blake3 hash. Always starts with 'E' (Blake3-256 derivation code).
54///
55/// Args:
56/// * Inner `String` should start with `'E'` (enforced by `new()`, not by serde).
57///
58/// Usage:
59/// ```ignore
60/// let prefix = Prefix::new("ETest123abc".to_string())?;
61/// assert_eq!(prefix.as_str(), "ETest123abc");
62/// ```
63#[derive(Debug, Clone, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
64#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
65#[repr(transparent)]
66pub struct Prefix(String);
67
68impl Prefix {
69    /// Validates and wraps a KERI prefix string.
70    pub fn new(s: String) -> Result<Self, KeriTypeError> {
71        validate_keri_derivation_code(&s, "Prefix")?;
72        Ok(Self(s))
73    }
74
75    /// Wraps a prefix string without validation (for trusted internal paths).
76    pub fn new_unchecked(s: String) -> Self {
77        Self(s)
78    }
79
80    /// Returns the inner string slice.
81    pub fn as_str(&self) -> &str {
82        &self.0
83    }
84
85    /// Consumes self and returns the inner String.
86    pub fn into_inner(self) -> String {
87        self.0
88    }
89
90    /// Returns true if the inner string is empty (placeholder during event construction).
91    pub fn is_empty(&self) -> bool {
92        self.0.is_empty()
93    }
94}
95
96impl fmt::Display for Prefix {
97    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
98        f.write_str(&self.0)
99    }
100}
101
102impl AsRef<str> for Prefix {
103    fn as_ref(&self) -> &str {
104        &self.0
105    }
106}
107
108impl Borrow<str> for Prefix {
109    fn borrow(&self) -> &str {
110        &self.0
111    }
112}
113
114impl From<Prefix> for String {
115    fn from(p: Prefix) -> String {
116        p.0
117    }
118}
119
120impl PartialEq<str> for Prefix {
121    fn eq(&self, other: &str) -> bool {
122        self.0 == other
123    }
124}
125
126impl PartialEq<&str> for Prefix {
127    fn eq(&self, other: &&str) -> bool {
128        self.0 == *other
129    }
130}
131
132impl PartialEq<Prefix> for str {
133    fn eq(&self, other: &Prefix) -> bool {
134        self == other.0
135    }
136}
137
138impl PartialEq<Prefix> for &str {
139    fn eq(&self, other: &Prefix) -> bool {
140        *self == other.0
141    }
142}
143
144/// KERI Self-Addressing Identifier (SAID).
145///
146/// A Blake3 hash that uniquely identifies a KERI event. Creates the
147/// hash chain: each event's `p` (previous) field is the prior event's SAID.
148///
149/// Structurally identical to `Prefix` (both start with 'E') but semantically
150/// distinct — a prefix identifies an *identity*, a SAID identifies an *event*.
151///
152/// Args:
153/// * Inner `String` should start with `'E'` (enforced by `new()`, not by serde).
154///
155/// Usage:
156/// ```ignore
157/// let said = Said::new("ESAID123".to_string())?;
158/// assert_eq!(said.as_str(), "ESAID123");
159/// ```
160#[derive(Debug, Clone, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
161#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
162#[repr(transparent)]
163pub struct Said(String);
164
165impl Said {
166    /// Validates and wraps a KERI SAID string.
167    pub fn new(s: String) -> Result<Self, KeriTypeError> {
168        validate_keri_derivation_code(&s, "Said")?;
169        Ok(Self(s))
170    }
171
172    /// Wraps a SAID string without validation (for `compute_said()` output and storage loads).
173    pub fn new_unchecked(s: String) -> Self {
174        Self(s)
175    }
176
177    /// Returns the inner string slice.
178    pub fn as_str(&self) -> &str {
179        &self.0
180    }
181
182    /// Consumes self and returns the inner String.
183    pub fn into_inner(self) -> String {
184        self.0
185    }
186
187    /// Returns true if the inner string is empty (placeholder during event construction).
188    pub fn is_empty(&self) -> bool {
189        self.0.is_empty()
190    }
191}
192
193impl fmt::Display for Said {
194    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
195        f.write_str(&self.0)
196    }
197}
198
199impl AsRef<str> for Said {
200    fn as_ref(&self) -> &str {
201        &self.0
202    }
203}
204
205impl Borrow<str> for Said {
206    fn borrow(&self) -> &str {
207        &self.0
208    }
209}
210
211impl From<Said> for String {
212    fn from(s: Said) -> String {
213        s.0
214    }
215}
216
217impl PartialEq<str> for Said {
218    fn eq(&self, other: &str) -> bool {
219        self.0 == other
220    }
221}
222
223impl PartialEq<&str> for Said {
224    fn eq(&self, other: &&str) -> bool {
225        self.0 == *other
226    }
227}
228
229impl PartialEq<Said> for str {
230    fn eq(&self, other: &Said) -> bool {
231        self == other.0
232    }
233}
234
235impl PartialEq<Said> for &str {
236    fn eq(&self, other: &Said) -> bool {
237        *self == other.0
238    }
239}
240
241// ── KERI Verification Errors ────────────────────────────────────────────────
242
243/// Errors specific to KERI KEL verification.
244#[derive(Debug, Clone, thiserror::Error, PartialEq, Eq)]
245pub enum KeriVerifyError {
246    /// The computed SAID does not match the SAID stored in the event.
247    #[error("Invalid SAID: expected {expected}, got {actual}")]
248    InvalidSaid {
249        /// The SAID computed from the event content.
250        expected: Said,
251        /// The SAID found in the event field.
252        actual: Said,
253    },
254    /// The `p` field of an event does not match the SAID of the preceding event.
255    #[error("Broken chain at seq {sequence}: references {referenced}, previous was {actual}")]
256    BrokenChain {
257        /// Sequence number of the event with the broken link.
258        sequence: u64,
259        /// The SAID referenced by the `p` field.
260        referenced: Said,
261        /// The SAID of the actual preceding event.
262        actual: Said,
263    },
264    /// Event sequence number does not follow the expected monotonic order.
265    #[error("Invalid sequence: expected {expected}, got {actual}")]
266    InvalidSequence {
267        /// The expected sequence number.
268        expected: u64,
269        /// The sequence number found in the event.
270        actual: u64,
271    },
272    /// The rotation key does not satisfy the pre-rotation commitment from the prior event.
273    #[error("Pre-rotation commitment mismatch at sequence {sequence}")]
274    CommitmentMismatch {
275        /// Sequence number of the rotation event that failed commitment verification.
276        sequence: u64,
277    },
278    /// Ed25519 signature verification failed.
279    #[error("Signature verification failed at sequence {sequence}")]
280    SignatureFailed {
281        /// Sequence number of the event whose signature failed.
282        sequence: u64,
283    },
284    /// The KEL's first event is not an inception (`icp`) event.
285    #[error("First event must be inception")]
286    NotInception,
287    /// The KEL contains no events.
288    #[error("Empty KEL")]
289    EmptyKel,
290    /// More than one inception event was found in the KEL.
291    #[error("Multiple inception events")]
292    MultipleInceptions,
293    /// JSON serialization or deserialization failed.
294    #[error("Serialization error: {0}")]
295    Serialization(String),
296    /// The key encoding prefix is unsupported or malformed.
297    #[error("Invalid key encoding: {0}")]
298    InvalidKey(String),
299    /// The sequence number string cannot be parsed as a `u64`.
300    #[error("Malformed sequence number: {raw:?}")]
301    MalformedSequence {
302        /// The raw sequence string that could not be parsed.
303        raw: String,
304    },
305}
306
307use auths_crypto::KeriPublicKey;
308
309/// KERI event types for verification.
310#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
311#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
312#[serde(tag = "t")]
313pub enum KeriEvent {
314    /// Inception event (`icp`) — creates the identity and establishes the first key.
315    #[serde(rename = "icp")]
316    Inception(IcpEvent),
317    /// Rotation event (`rot`) — rotates to the pre-committed key.
318    #[serde(rename = "rot")]
319    Rotation(RotEvent),
320    /// Interaction event (`ixn`) — anchors data without rotating keys.
321    #[serde(rename = "ixn")]
322    Interaction(IxnEvent),
323}
324
325impl Serialize for KeriEvent {
326    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
327        match self {
328            KeriEvent::Inception(e) => e.serialize(serializer),
329            KeriEvent::Rotation(e) => e.serialize(serializer),
330            KeriEvent::Interaction(e) => e.serialize(serializer),
331        }
332    }
333}
334
335impl KeriEvent {
336    /// Get the SAID of this event.
337    pub fn said(&self) -> &Said {
338        match self {
339            KeriEvent::Inception(e) => &e.d,
340            KeriEvent::Rotation(e) => &e.d,
341            KeriEvent::Interaction(e) => &e.d,
342        }
343    }
344
345    /// Get the signature of this event.
346    pub fn signature(&self) -> &str {
347        match self {
348            KeriEvent::Inception(e) => &e.x,
349            KeriEvent::Rotation(e) => &e.x,
350            KeriEvent::Interaction(e) => &e.x,
351        }
352    }
353
354    /// Get the sequence number of this event.
355    pub fn sequence(&self) -> Result<u64, KeriVerifyError> {
356        let s = match self {
357            KeriEvent::Inception(e) => &e.s,
358            KeriEvent::Rotation(e) => &e.s,
359            KeriEvent::Interaction(e) => &e.s,
360        };
361        s.parse::<u64>()
362            .map_err(|_| KeriVerifyError::MalformedSequence { raw: s.clone() })
363    }
364}
365
366/// Inception event.
367#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
368#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
369pub struct IcpEvent {
370    /// KERI version string (e.g. `"KERI10JSON"`).
371    pub v: String,
372    /// Self-Addressing Identifier (SAID) of this event.
373    #[serde(default)]
374    pub d: Said,
375    /// KERI prefix — same as `d` for inception.
376    pub i: Prefix,
377    /// Sequence number (always `"0"` for inception).
378    pub s: String,
379    /// Signing key threshold.
380    #[serde(default)]
381    pub kt: String,
382    /// Current signing keys (base64url-encoded with derivation prefix).
383    pub k: Vec<String>,
384    /// Next-key commitment threshold.
385    #[serde(default)]
386    pub nt: String,
387    /// Next-key commitments (Blake3 hashes of the pre-rotation public keys).
388    pub n: Vec<String>,
389    /// Witness threshold.
390    #[serde(default)]
391    pub bt: String,
392    /// Witness list (DIDs or URLs of witnesses).
393    #[serde(default)]
394    pub b: Vec<String>,
395    /// Anchored seals (attached data digests).
396    #[serde(default)]
397    pub a: Vec<Seal>,
398    /// Ed25519 signature over the canonical event body.
399    #[serde(default)]
400    pub x: String,
401}
402
403/// Spec field order: v, t, d, i, s, kt, k, nt, n, bt, b, a, x
404impl Serialize for IcpEvent {
405    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
406        let field_count = 12 + (!self.d.is_empty() as usize) + (!self.x.is_empty() as usize);
407        let mut map = serializer.serialize_map(Some(field_count))?;
408        map.serialize_entry("v", &self.v)?;
409        map.serialize_entry("t", "icp")?;
410        if !self.d.is_empty() {
411            map.serialize_entry("d", &self.d)?;
412        }
413        map.serialize_entry("i", &self.i)?;
414        map.serialize_entry("s", &self.s)?;
415        map.serialize_entry("kt", &self.kt)?;
416        map.serialize_entry("k", &self.k)?;
417        map.serialize_entry("nt", &self.nt)?;
418        map.serialize_entry("n", &self.n)?;
419        map.serialize_entry("bt", &self.bt)?;
420        map.serialize_entry("b", &self.b)?;
421        map.serialize_entry("a", &self.a)?;
422        if !self.x.is_empty() {
423            map.serialize_entry("x", &self.x)?;
424        }
425        map.end()
426    }
427}
428
429/// Rotation event.
430#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
431#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
432pub struct RotEvent {
433    /// KERI version string.
434    pub v: String,
435    /// SAID of this event.
436    #[serde(default)]
437    pub d: Said,
438    /// KERI prefix of the identity being rotated.
439    pub i: Prefix,
440    /// Sequence number.
441    pub s: String,
442    /// SAID of the prior event (chain link).
443    pub p: Said,
444    /// Signing key threshold.
445    #[serde(default)]
446    pub kt: String,
447    /// Current signing keys after rotation.
448    pub k: Vec<String>,
449    /// Next-key commitment threshold.
450    #[serde(default)]
451    pub nt: String,
452    /// Next-key commitments for the subsequent rotation.
453    pub n: Vec<String>,
454    /// Witness threshold.
455    #[serde(default)]
456    pub bt: String,
457    /// Witness list.
458    #[serde(default)]
459    pub b: Vec<String>,
460    /// Anchored seals.
461    #[serde(default)]
462    pub a: Vec<Seal>,
463    /// Ed25519 signature over the canonical event body.
464    #[serde(default)]
465    pub x: String,
466}
467
468/// Spec field order: v, t, d, i, s, p, kt, k, nt, n, bt, b, a, x
469impl Serialize for RotEvent {
470    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
471        let field_count = 13 + (!self.d.is_empty() as usize) + (!self.x.is_empty() as usize);
472        let mut map = serializer.serialize_map(Some(field_count))?;
473        map.serialize_entry("v", &self.v)?;
474        map.serialize_entry("t", "rot")?;
475        if !self.d.is_empty() {
476            map.serialize_entry("d", &self.d)?;
477        }
478        map.serialize_entry("i", &self.i)?;
479        map.serialize_entry("s", &self.s)?;
480        map.serialize_entry("p", &self.p)?;
481        map.serialize_entry("kt", &self.kt)?;
482        map.serialize_entry("k", &self.k)?;
483        map.serialize_entry("nt", &self.nt)?;
484        map.serialize_entry("n", &self.n)?;
485        map.serialize_entry("bt", &self.bt)?;
486        map.serialize_entry("b", &self.b)?;
487        map.serialize_entry("a", &self.a)?;
488        if !self.x.is_empty() {
489            map.serialize_entry("x", &self.x)?;
490        }
491        map.end()
492    }
493}
494
495/// Interaction event.
496#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
497#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
498pub struct IxnEvent {
499    /// KERI version string.
500    pub v: String,
501    /// SAID of this event.
502    #[serde(default)]
503    pub d: Said,
504    /// KERI prefix of the identity.
505    pub i: Prefix,
506    /// Sequence number.
507    pub s: String,
508    /// SAID of the prior event (chain link).
509    pub p: Said,
510    /// Anchored seals (e.g. attestation digests).
511    pub a: Vec<Seal>,
512    /// Ed25519 signature over the canonical event body.
513    #[serde(default)]
514    pub x: String,
515}
516
517/// Spec field order: v, t, d, i, s, p, a, x
518impl Serialize for IxnEvent {
519    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
520        let field_count = 7 + (!self.d.is_empty() as usize) + (!self.x.is_empty() as usize);
521        let mut map = serializer.serialize_map(Some(field_count))?;
522        map.serialize_entry("v", &self.v)?;
523        map.serialize_entry("t", "ixn")?;
524        if !self.d.is_empty() {
525            map.serialize_entry("d", &self.d)?;
526        }
527        map.serialize_entry("i", &self.i)?;
528        map.serialize_entry("s", &self.s)?;
529        map.serialize_entry("p", &self.p)?;
530        map.serialize_entry("a", &self.a)?;
531        if !self.x.is_empty() {
532            map.serialize_entry("x", &self.x)?;
533        }
534        map.end()
535    }
536}
537
538/// A seal anchors external data in a KERI event.
539#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
540#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
541pub struct Seal {
542    /// Digest (SAID) of the anchored data.
543    pub d: Said,
544    /// Semantic type label (e.g. `"device-attestation"`).
545    #[serde(rename = "type")]
546    pub seal_type: String,
547}
548
549/// Result of KEL verification.
550#[derive(Debug, Clone, Serialize)]
551pub struct KeriKeyState {
552    /// The KERI prefix
553    pub prefix: Prefix,
554
555    /// The current public key (raw bytes)
556    #[serde(skip)]
557    pub current_key: Vec<u8>,
558
559    /// The current public key (encoded, e.g. "D..." base64url)
560    pub current_key_encoded: String,
561
562    /// The next-key commitment (if any)
563    pub next_commitment: Option<String>,
564
565    /// The current sequence number
566    pub sequence: u64,
567
568    /// Whether the identity is abandoned (no next commitment)
569    pub is_abandoned: bool,
570
571    /// The SAID of the last processed event
572    pub last_event_said: Said,
573}
574
575/// Verify a KERI event log and return the resulting key state.
576///
577/// This is a stateless function that validates the cryptographic integrity
578/// of a KEL without requiring filesystem access. Verifies SAID integrity,
579/// chain linkage, sequence ordering, pre-rotation commitments, and Ed25519
580/// signatures on every event.
581///
582/// # Arguments
583/// * `events` - Ordered list of events (inception first)
584/// * `provider` - Crypto provider for Ed25519 signature verification
585///
586/// # Returns
587/// * `Ok(KeriKeyState)` - The current key state after replaying events
588/// * `Err(KeriVerifyError)` - If validation fails
589pub async fn verify_kel(
590    events: &[KeriEvent],
591    provider: &dyn CryptoProvider,
592) -> Result<KeriKeyState, KeriVerifyError> {
593    if events.is_empty() {
594        return Err(KeriVerifyError::EmptyKel);
595    }
596
597    let KeriEvent::Inception(icp) = &events[0] else {
598        return Err(KeriVerifyError::NotInception);
599    };
600
601    verify_event_said(&events[0])?;
602
603    let icp_key = icp
604        .k
605        .first()
606        .ok_or(KeriVerifyError::SignatureFailed { sequence: 0 })?;
607    verify_event_signature(&events[0], icp_key, provider).await?;
608
609    let current_key = decode_key(icp_key)?;
610    let current_key_encoded = icp_key.clone();
611
612    let mut state = KeriKeyState {
613        prefix: icp.i.clone(),
614        current_key,
615        current_key_encoded,
616        next_commitment: icp.n.first().cloned(),
617        sequence: 0,
618        is_abandoned: icp.n.is_empty(),
619        last_event_said: icp.d.clone(),
620    };
621
622    for (idx, event) in events.iter().enumerate().skip(1) {
623        let expected_seq = idx as u64;
624
625        verify_event_said(event)?;
626
627        match event {
628            KeriEvent::Rotation(rot) => {
629                let actual_seq = event.sequence()?;
630                if actual_seq != expected_seq {
631                    return Err(KeriVerifyError::InvalidSequence {
632                        expected: expected_seq,
633                        actual: actual_seq,
634                    });
635                }
636
637                if rot.p != state.last_event_said {
638                    return Err(KeriVerifyError::BrokenChain {
639                        sequence: actual_seq,
640                        referenced: rot.p.clone(),
641                        actual: state.last_event_said.clone(),
642                    });
643                }
644
645                if !rot.k.is_empty() {
646                    verify_event_signature(event, &rot.k[0], provider).await?;
647
648                    let new_key_bytes = decode_key(&rot.k[0])?;
649
650                    if let Some(commitment) = &state.next_commitment
651                        && !verify_commitment(&new_key_bytes, commitment)
652                    {
653                        return Err(KeriVerifyError::CommitmentMismatch {
654                            sequence: actual_seq,
655                        });
656                    }
657
658                    state.current_key = new_key_bytes;
659                    state.current_key_encoded = rot.k[0].clone();
660                }
661
662                state.next_commitment = rot.n.first().cloned();
663                state.is_abandoned = rot.n.is_empty();
664                state.sequence = actual_seq;
665                state.last_event_said = rot.d.clone();
666            }
667            KeriEvent::Interaction(ixn) => {
668                let actual_seq = event.sequence()?;
669                if actual_seq != expected_seq {
670                    return Err(KeriVerifyError::InvalidSequence {
671                        expected: expected_seq,
672                        actual: actual_seq,
673                    });
674                }
675
676                if ixn.p != state.last_event_said {
677                    return Err(KeriVerifyError::BrokenChain {
678                        sequence: actual_seq,
679                        referenced: ixn.p.clone(),
680                        actual: state.last_event_said.clone(),
681                    });
682                }
683
684                verify_event_signature(event, &state.current_key_encoded, provider).await?;
685
686                state.sequence = actual_seq;
687                state.last_event_said = ixn.d.clone();
688            }
689            KeriEvent::Inception(_) => {
690                return Err(KeriVerifyError::MultipleInceptions);
691            }
692        }
693    }
694
695    Ok(state)
696}
697
698/// Serialize event for signing/SAID computation (clears d, x, and for ICP also i).
699///
700/// This produces the canonical form over which both SAID and signatures are computed.
701fn serialize_for_signing(event: &KeriEvent) -> Result<Vec<u8>, KeriVerifyError> {
702    match event {
703        KeriEvent::Inception(e) => {
704            let mut copy = e.clone();
705            copy.d = Said::default();
706            copy.i = Prefix::default();
707            copy.x = String::new();
708            serde_json::to_vec(&KeriEvent::Inception(copy))
709        }
710        KeriEvent::Rotation(e) => {
711            let mut copy = e.clone();
712            copy.d = Said::default();
713            copy.x = String::new();
714            serde_json::to_vec(&KeriEvent::Rotation(copy))
715        }
716        KeriEvent::Interaction(e) => {
717            let mut copy = e.clone();
718            copy.d = Said::default();
719            copy.x = String::new();
720            serde_json::to_vec(&KeriEvent::Interaction(copy))
721        }
722    }
723    .map_err(|e| KeriVerifyError::Serialization(e.to_string()))
724}
725
726/// Verify an event's SAID matches its content.
727fn verify_event_said(event: &KeriEvent) -> Result<(), KeriVerifyError> {
728    let json = serialize_for_signing(event)?;
729    let computed = compute_said(&json);
730    let said = event.said();
731
732    if computed != *said {
733        return Err(KeriVerifyError::InvalidSaid {
734            expected: computed,
735            actual: said.clone(),
736        });
737    }
738
739    Ok(())
740}
741
742/// Verify an event's Ed25519 signature using the specified key.
743async fn verify_event_signature(
744    event: &KeriEvent,
745    signing_key: &str,
746    provider: &dyn CryptoProvider,
747) -> Result<(), KeriVerifyError> {
748    let sequence = event.sequence()?;
749
750    let sig_str = event.signature();
751    if sig_str.is_empty() {
752        return Err(KeriVerifyError::SignatureFailed { sequence });
753    }
754    let sig_bytes = URL_SAFE_NO_PAD
755        .decode(sig_str)
756        .map_err(|_| KeriVerifyError::SignatureFailed { sequence })?;
757
758    let key_bytes =
759        decode_key(signing_key).map_err(|_| KeriVerifyError::SignatureFailed { sequence })?;
760
761    let canonical = serialize_for_signing(event)?;
762
763    provider
764        .verify_ed25519(&key_bytes, &canonical, &sig_bytes)
765        .await
766        .map_err(|_| KeriVerifyError::SignatureFailed { sequence })?;
767
768    Ok(())
769}
770
771/// Compute a KERI Self-Addressing Identifier (SAID) using Blake3.
772// SYNC: must match auths-core/src/crypto/said.rs — tested by said_cross_validation
773pub fn compute_said(data: &[u8]) -> Said {
774    let hash = blake3::hash(data);
775    Said::new_unchecked(format!("E{}", URL_SAFE_NO_PAD.encode(hash.as_bytes())))
776}
777
778/// Compute next-key commitment.
779// SYNC: must match auths-core/src/crypto/said.rs — tested by said_cross_validation
780fn compute_commitment(public_key: &[u8]) -> String {
781    let hash = blake3::hash(public_key);
782    format!("E{}", URL_SAFE_NO_PAD.encode(hash.as_bytes()))
783}
784
785// Defense-in-depth: both values are derived from public data, but constant-time
786// comparison prevents timing side-channels on commitment verification.
787fn verify_commitment(public_key: &[u8], commitment: &str) -> bool {
788    let computed = compute_commitment(public_key);
789    computed.as_bytes().ct_eq(commitment.as_bytes()).into()
790}
791
792/// Decode a KERI key (D-prefixed Base64url for Ed25519).
793fn decode_key(key_str: &str) -> Result<Vec<u8>, KeriVerifyError> {
794    KeriPublicKey::parse(key_str)
795        .map(|k| k.into_bytes().to_vec())
796        .map_err(|e| KeriVerifyError::InvalidKey(e.to_string()))
797}
798
799/// Check if a seal with given digest exists in any IXN event.
800///
801/// Returns the sequence number of the IXN event if found.
802pub fn find_seal_in_kel(events: &[KeriEvent], digest: &str) -> Option<u64> {
803    for event in events {
804        if let KeriEvent::Interaction(ixn) = event {
805            for seal in &ixn.a {
806                if seal.d.as_str() == digest {
807                    return ixn.s.parse::<u64>().ok();
808                }
809            }
810        }
811    }
812    None
813}
814
815/// Parse events from JSON.
816pub fn parse_kel_json(json: &str) -> Result<Vec<KeriEvent>, KeriVerifyError> {
817    serde_json::from_str(json).map_err(|e| KeriVerifyError::Serialization(e.to_string()))
818}
819
820#[cfg(all(test, not(target_arch = "wasm32")))]
821#[allow(clippy::unwrap_used, clippy::expect_used)]
822mod tests {
823    use super::*;
824    use auths_crypto::RingCryptoProvider;
825    use ring::rand::SystemRandom;
826    use ring::signature::{Ed25519KeyPair, KeyPair};
827
828    fn provider() -> RingCryptoProvider {
829        RingCryptoProvider
830    }
831
832    fn finalize_icp(mut icp: IcpEvent) -> IcpEvent {
833        icp.d = Said::default();
834        icp.i = Prefix::default();
835        icp.x = String::new();
836        let json = serde_json::to_vec(&KeriEvent::Inception(icp.clone())).unwrap();
837        let said = compute_said(&json);
838        icp.i = Prefix::new_unchecked(said.as_str().to_string());
839        icp.d = said;
840        icp
841    }
842
843    // ── Signed helpers ──
844
845    fn make_signed_icp(keypair: &Ed25519KeyPair, next_commitment: &str) -> IcpEvent {
846        let key_encoded = format!("D{}", URL_SAFE_NO_PAD.encode(keypair.public_key().as_ref()));
847
848        let mut icp = IcpEvent {
849            v: "KERI10JSON".into(),
850            d: Said::default(),
851            i: Prefix::default(),
852            s: "0".into(),
853            kt: "1".into(),
854            k: vec![key_encoded],
855            nt: "1".into(),
856            n: vec![next_commitment.to_string()],
857            bt: "0".into(),
858            b: vec![],
859            a: vec![],
860            x: String::new(),
861        };
862
863        // Finalize SAID
864        icp = finalize_icp(icp);
865
866        // Sign
867        let canonical = serialize_for_signing(&KeriEvent::Inception(icp.clone())).unwrap();
868        let sig = keypair.sign(&canonical);
869        icp.x = URL_SAFE_NO_PAD.encode(sig.as_ref());
870
871        icp
872    }
873
874    fn make_signed_rot(
875        prefix: &str,
876        prev_said: &str,
877        seq: u64,
878        new_keypair: &Ed25519KeyPair,
879        next_commitment: &str,
880    ) -> RotEvent {
881        let key_encoded = format!(
882            "D{}",
883            URL_SAFE_NO_PAD.encode(new_keypair.public_key().as_ref())
884        );
885
886        let mut rot = RotEvent {
887            v: "KERI10JSON".into(),
888            d: Said::default(),
889            i: Prefix::new_unchecked(prefix.to_string()),
890            s: seq.to_string(),
891            p: Said::new_unchecked(prev_said.to_string()),
892            kt: "1".into(),
893            k: vec![key_encoded],
894            nt: "1".into(),
895            n: vec![next_commitment.to_string()],
896            bt: "0".into(),
897            b: vec![],
898            a: vec![],
899            x: String::new(),
900        };
901
902        // Compute SAID
903        let json = serialize_for_signing(&KeriEvent::Rotation(rot.clone())).unwrap();
904        rot.d = compute_said(&json);
905
906        // Sign with the NEW key
907        let canonical = serialize_for_signing(&KeriEvent::Rotation(rot.clone())).unwrap();
908        let sig = new_keypair.sign(&canonical);
909        rot.x = URL_SAFE_NO_PAD.encode(sig.as_ref());
910
911        rot
912    }
913
914    fn make_signed_ixn(
915        prefix: &str,
916        prev_said: &str,
917        seq: u64,
918        keypair: &Ed25519KeyPair,
919        seals: Vec<Seal>,
920    ) -> IxnEvent {
921        let mut ixn = IxnEvent {
922            v: "KERI10JSON".into(),
923            d: Said::default(),
924            i: Prefix::new_unchecked(prefix.to_string()),
925            s: seq.to_string(),
926            p: Said::new_unchecked(prev_said.to_string()),
927            a: seals,
928            x: String::new(),
929        };
930
931        // Compute SAID
932        let json = serialize_for_signing(&KeriEvent::Interaction(ixn.clone())).unwrap();
933        ixn.d = compute_said(&json);
934
935        // Sign
936        let canonical = serialize_for_signing(&KeriEvent::Interaction(ixn.clone())).unwrap();
937        let sig = keypair.sign(&canonical);
938        ixn.x = URL_SAFE_NO_PAD.encode(sig.as_ref());
939
940        ixn
941    }
942
943    fn generate_keypair() -> (Ed25519KeyPair, Vec<u8>) {
944        let rng = SystemRandom::new();
945        let pkcs8 = Ed25519KeyPair::generate_pkcs8(&rng).unwrap();
946        let pkcs8_bytes = pkcs8.as_ref().to_vec();
947        let keypair = Ed25519KeyPair::from_pkcs8(&pkcs8_bytes).unwrap();
948        (keypair, pkcs8_bytes)
949    }
950
951    // ── Structural tests (existing, updated for x field) ──
952
953    #[tokio::test]
954    async fn rejects_empty_kel() {
955        let result = verify_kel(&[], &provider()).await;
956        assert!(matches!(result, Err(KeriVerifyError::EmptyKel)));
957    }
958
959    #[tokio::test]
960    async fn rejects_non_inception_first() {
961        let ixn = KeriEvent::Interaction(IxnEvent {
962            v: "KERI10JSON".into(),
963            d: Said::new_unchecked("EIXN".into()),
964            i: Prefix::new_unchecked("EPrefix".into()),
965            s: "0".into(),
966            p: Said::new_unchecked("EPrev".into()),
967            a: vec![],
968            x: String::new(),
969        });
970
971        let result = verify_kel(&[ixn], &provider()).await;
972        assert!(matches!(result, Err(KeriVerifyError::NotInception)));
973    }
974
975    #[test]
976    fn find_seal_locates_attestation_sync() {
977        let (kp1, _) = generate_keypair();
978        let (kp2, _) = generate_keypair();
979        let next_commitment = compute_commitment(kp2.public_key().as_ref());
980
981        let icp = make_signed_icp(&kp1, &next_commitment);
982
983        // Create signed IXN with seal
984        let ixn = make_signed_ixn(
985            icp.i.as_str(),
986            icp.d.as_str(),
987            1,
988            &kp1,
989            vec![Seal {
990                d: Said::new_unchecked("EAttDigest".into()),
991                seal_type: "device-attestation".into(),
992            }],
993        );
994
995        let events = vec![KeriEvent::Inception(icp), KeriEvent::Interaction(ixn)];
996
997        let found = find_seal_in_kel(&events, "EAttDigest");
998        assert_eq!(found, Some(1));
999
1000        let not_found = find_seal_in_kel(&events, "ENotExist");
1001        assert_eq!(not_found, None);
1002    }
1003
1004    #[test]
1005    fn decode_key_works() {
1006        let key_bytes = [42u8; 32];
1007        let encoded = format!("D{}", URL_SAFE_NO_PAD.encode(key_bytes));
1008
1009        let decoded = decode_key(&encoded).unwrap();
1010        assert_eq!(decoded, key_bytes);
1011    }
1012
1013    #[test]
1014    fn decode_key_rejects_unknown_code() {
1015        let result = decode_key("Xsomething");
1016        assert!(matches!(result, Err(KeriVerifyError::InvalidKey(_))));
1017    }
1018
1019    // ── Signed verification tests ──
1020
1021    #[tokio::test]
1022    async fn verify_signed_inception() {
1023        let (kp1, _) = generate_keypair();
1024        let (kp2, _) = generate_keypair();
1025        let next_commitment = compute_commitment(kp2.public_key().as_ref());
1026
1027        let icp = make_signed_icp(&kp1, &next_commitment);
1028        let events = vec![KeriEvent::Inception(icp.clone())];
1029
1030        let state = verify_kel(&events, &provider()).await.unwrap();
1031        assert_eq!(state.prefix, icp.i);
1032        assert_eq!(state.current_key, kp1.public_key().as_ref());
1033        assert_eq!(state.sequence, 0);
1034        assert!(!state.is_abandoned);
1035    }
1036
1037    #[tokio::test]
1038    async fn verify_icp_rot_ixn_signed() {
1039        let (kp1, _) = generate_keypair();
1040        let (kp2, _) = generate_keypair();
1041        let (kp3, _) = generate_keypair();
1042
1043        let next1_commitment = compute_commitment(kp2.public_key().as_ref());
1044        let next2_commitment = compute_commitment(kp3.public_key().as_ref());
1045
1046        let icp = make_signed_icp(&kp1, &next1_commitment);
1047        let rot = make_signed_rot(icp.i.as_str(), icp.d.as_str(), 1, &kp2, &next2_commitment);
1048        let ixn = make_signed_ixn(
1049            icp.i.as_str(),
1050            rot.d.as_str(),
1051            2,
1052            &kp2,
1053            vec![Seal {
1054                d: Said::new_unchecked("EAttest".into()),
1055                seal_type: "device-attestation".into(),
1056            }],
1057        );
1058
1059        let events = vec![
1060            KeriEvent::Inception(icp.clone()),
1061            KeriEvent::Rotation(rot),
1062            KeriEvent::Interaction(ixn),
1063        ];
1064
1065        let state = verify_kel(&events, &provider()).await.unwrap();
1066        assert_eq!(state.prefix, icp.i);
1067        assert_eq!(state.current_key, kp2.public_key().as_ref());
1068        assert_eq!(state.sequence, 2);
1069    }
1070
1071    #[tokio::test]
1072    async fn rejects_forged_signature() {
1073        let (kp1, _) = generate_keypair();
1074        let (kp2, _) = generate_keypair();
1075        let next_commitment = compute_commitment(kp2.public_key().as_ref());
1076
1077        let mut icp = make_signed_icp(&kp1, &next_commitment);
1078        icp.x = URL_SAFE_NO_PAD.encode([0u8; 64]);
1079
1080        let events = vec![KeriEvent::Inception(icp)];
1081        let result = verify_kel(&events, &provider()).await;
1082        assert!(matches!(
1083            result,
1084            Err(KeriVerifyError::SignatureFailed { sequence: 0 })
1085        ));
1086    }
1087
1088    #[tokio::test]
1089    async fn rejects_missing_signature() {
1090        let (kp1, _) = generate_keypair();
1091        let (kp2, _) = generate_keypair();
1092        let next_commitment = compute_commitment(kp2.public_key().as_ref());
1093
1094        let mut icp = make_signed_icp(&kp1, &next_commitment);
1095        icp.x = String::new();
1096
1097        let events = vec![KeriEvent::Inception(icp)];
1098        let result = verify_kel(&events, &provider()).await;
1099        assert!(matches!(
1100            result,
1101            Err(KeriVerifyError::SignatureFailed { sequence: 0 })
1102        ));
1103    }
1104
1105    #[tokio::test]
1106    async fn rejects_wrong_key_signature() {
1107        let (kp1, _) = generate_keypair();
1108        let (kp_wrong, _) = generate_keypair();
1109        let (kp2, _) = generate_keypair();
1110        let next_commitment = compute_commitment(kp2.public_key().as_ref());
1111
1112        let key_encoded = format!("D{}", URL_SAFE_NO_PAD.encode(kp1.public_key().as_ref()));
1113        let mut icp = IcpEvent {
1114            v: "KERI10JSON".into(),
1115            d: Said::default(),
1116            i: Prefix::default(),
1117            s: "0".into(),
1118            kt: "1".into(),
1119            k: vec![key_encoded],
1120            nt: "1".into(),
1121            n: vec![next_commitment],
1122            bt: "0".into(),
1123            b: vec![],
1124            a: vec![],
1125            x: String::new(),
1126        };
1127        icp = finalize_icp(icp);
1128
1129        let canonical = serialize_for_signing(&KeriEvent::Inception(icp.clone())).unwrap();
1130        let sig = kp_wrong.sign(&canonical);
1131        icp.x = URL_SAFE_NO_PAD.encode(sig.as_ref());
1132
1133        let events = vec![KeriEvent::Inception(icp)];
1134        let result = verify_kel(&events, &provider()).await;
1135        assert!(matches!(
1136            result,
1137            Err(KeriVerifyError::SignatureFailed { sequence: 0 })
1138        ));
1139    }
1140
1141    #[tokio::test]
1142    async fn rejects_rot_signed_with_old_key() {
1143        let (kp1, _) = generate_keypair();
1144        let (kp2, _) = generate_keypair();
1145        let (kp3, _) = generate_keypair();
1146
1147        let next1_commitment = compute_commitment(kp2.public_key().as_ref());
1148        let next2_commitment = compute_commitment(kp3.public_key().as_ref());
1149
1150        let icp = make_signed_icp(&kp1, &next1_commitment);
1151
1152        let key2_encoded = format!("D{}", URL_SAFE_NO_PAD.encode(kp2.public_key().as_ref()));
1153        let mut rot = RotEvent {
1154            v: "KERI10JSON".into(),
1155            d: Said::default(),
1156            i: icp.i.clone(),
1157            s: "1".into(),
1158            p: icp.d.clone(),
1159            kt: "1".into(),
1160            k: vec![key2_encoded],
1161            nt: "1".into(),
1162            n: vec![next2_commitment],
1163            bt: "0".into(),
1164            b: vec![],
1165            a: vec![],
1166            x: String::new(),
1167        };
1168
1169        let json = serialize_for_signing(&KeriEvent::Rotation(rot.clone())).unwrap();
1170        rot.d = compute_said(&json);
1171
1172        let canonical = serialize_for_signing(&KeriEvent::Rotation(rot.clone())).unwrap();
1173        let sig = kp1.sign(&canonical);
1174        rot.x = URL_SAFE_NO_PAD.encode(sig.as_ref());
1175
1176        let events = vec![KeriEvent::Inception(icp), KeriEvent::Rotation(rot)];
1177        let result = verify_kel(&events, &provider()).await;
1178        assert!(matches!(
1179            result,
1180            Err(KeriVerifyError::SignatureFailed { sequence: 1 })
1181        ));
1182    }
1183
1184    #[tokio::test]
1185    async fn rotation_updates_signing_key_for_ixn() {
1186        let (kp1, _) = generate_keypair();
1187        let (kp2, _) = generate_keypair();
1188        let (kp3, _) = generate_keypair();
1189
1190        let next1_commitment = compute_commitment(kp2.public_key().as_ref());
1191        let next2_commitment = compute_commitment(kp3.public_key().as_ref());
1192
1193        let icp = make_signed_icp(&kp1, &next1_commitment);
1194        let rot = make_signed_rot(icp.i.as_str(), icp.d.as_str(), 1, &kp2, &next2_commitment);
1195        let ixn = make_signed_ixn(icp.i.as_str(), rot.d.as_str(), 2, &kp1, vec![]);
1196
1197        let events = vec![
1198            KeriEvent::Inception(icp),
1199            KeriEvent::Rotation(rot),
1200            KeriEvent::Interaction(ixn),
1201        ];
1202        let result = verify_kel(&events, &provider()).await;
1203        assert!(matches!(
1204            result,
1205            Err(KeriVerifyError::SignatureFailed { sequence: 2 })
1206        ));
1207    }
1208
1209    #[tokio::test]
1210    async fn rejects_wrong_commitment() {
1211        let (kp1, _) = generate_keypair();
1212        let (kp2, _) = generate_keypair();
1213        let (kp_wrong, _) = generate_keypair();
1214        let (kp3, _) = generate_keypair();
1215
1216        let next1_commitment = compute_commitment(kp2.public_key().as_ref());
1217        let next2_commitment = compute_commitment(kp3.public_key().as_ref());
1218
1219        let icp = make_signed_icp(&kp1, &next1_commitment);
1220        let rot = make_signed_rot(
1221            icp.i.as_str(),
1222            icp.d.as_str(),
1223            1,
1224            &kp_wrong,
1225            &next2_commitment,
1226        );
1227
1228        let events = vec![KeriEvent::Inception(icp), KeriEvent::Rotation(rot)];
1229        let result = verify_kel(&events, &provider()).await;
1230        assert!(matches!(
1231            result,
1232            Err(KeriVerifyError::CommitmentMismatch { sequence: 1 })
1233        ));
1234    }
1235
1236    #[tokio::test]
1237    async fn rejects_broken_chain() {
1238        let (kp1, _) = generate_keypair();
1239        let (kp2, _) = generate_keypair();
1240        let next_commitment = compute_commitment(kp2.public_key().as_ref());
1241
1242        let icp = make_signed_icp(&kp1, &next_commitment);
1243
1244        let mut ixn = IxnEvent {
1245            v: "KERI10JSON".into(),
1246            d: Said::default(),
1247            i: icp.i.clone(),
1248            s: "1".into(),
1249            p: Said::new_unchecked("EWrongPrevious".into()),
1250            a: vec![],
1251            x: String::new(),
1252        };
1253
1254        let json = serialize_for_signing(&KeriEvent::Interaction(ixn.clone())).unwrap();
1255        ixn.d = compute_said(&json);
1256
1257        let canonical = serialize_for_signing(&KeriEvent::Interaction(ixn.clone())).unwrap();
1258        let sig = kp1.sign(&canonical);
1259        ixn.x = URL_SAFE_NO_PAD.encode(sig.as_ref());
1260
1261        let events = vec![KeriEvent::Inception(icp), KeriEvent::Interaction(ixn)];
1262        let result = verify_kel(&events, &provider()).await;
1263        assert!(matches!(result, Err(KeriVerifyError::BrokenChain { .. })));
1264    }
1265
1266    #[tokio::test]
1267    async fn verify_kel_with_rotation() {
1268        let (kp1, _) = generate_keypair();
1269        let (kp2, _) = generate_keypair();
1270        let (kp3, _) = generate_keypair();
1271
1272        let next1_commitment = compute_commitment(kp2.public_key().as_ref());
1273        let next2_commitment = compute_commitment(kp3.public_key().as_ref());
1274
1275        let icp = make_signed_icp(&kp1, &next1_commitment);
1276        let rot = make_signed_rot(icp.i.as_str(), icp.d.as_str(), 1, &kp2, &next2_commitment);
1277
1278        let events = vec![KeriEvent::Inception(icp), KeriEvent::Rotation(rot)];
1279
1280        let state = verify_kel(&events, &provider()).await.unwrap();
1281        assert_eq!(state.sequence, 1);
1282        assert_eq!(state.current_key, kp2.public_key().as_ref());
1283    }
1284
1285    #[test]
1286    fn rejects_malformed_sequence_number() {
1287        // An event with a non-numeric sequence must be rejected, not coerced to 0
1288        let icp = IcpEvent {
1289            v: "KERI10JSON".into(),
1290            d: Said::default(),
1291            i: Prefix::default(),
1292            s: "not_a_number".to_string(),
1293            kt: "1".to_string(),
1294            k: vec!["DKey".to_string()],
1295            nt: "1".to_string(),
1296            n: vec!["ENext".to_string()],
1297            bt: "0".to_string(),
1298            b: vec![],
1299            a: vec![],
1300            x: String::new(),
1301        };
1302
1303        let event = KeriEvent::Inception(icp);
1304        let result = event.sequence();
1305        assert!(
1306            matches!(result, Err(KeriVerifyError::MalformedSequence { .. })),
1307            "Expected MalformedSequence error, got: {:?}",
1308            result
1309        );
1310    }
1311}