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