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