Skip to main content

auths_keri/
keys.rs

1//! KERI CESR public key parsing for Ed25519 and P-256.
2//!
3//! Decodes KERI-encoded public keys from their CESR-qualified string form.
4//! Ed25519: 'D' prefix (transferable) / 'B' (non-transferable) + base64url(32 bytes).
5//! P-256:   '1AAJ' prefix (transferable) / '1AAI' (non-transferable) + base64url(33 bytes).
6//!
7//! Both curves carry the transferability recorded from their CESR code: rotating
8//! identity keys are transferable (`D` / `1AAJ`), while keys pinned to one
9//! incepting event — most notably KERI witnesses — are non-transferable
10//! (`B` / `1AAI`). The raw bytes and signature algorithm are identical across the
11//! pair; only the code (and thus the rotation semantics) differ.
12//!
13//! Per the CESR master code table (cesride / keripy `MatterCodex`):
14//! `1AAJ` = `ECDSA_256r1` = transferable secp256r1 verification key;
15//! `1AAI` = `ECDSA_256r1N` = the non-transferable variant. This mirrors the
16//! Ed25519 `D`/`B` pair. Auths identities rotate, so they encode verkeys with
17//! the transferable `1AAJ` code.
18
19/// Errors from decoding a KERI-encoded public key.
20#[derive(Debug, Clone, thiserror::Error, PartialEq, Eq)]
21#[non_exhaustive]
22pub enum KeriDecodeError {
23    /// The KERI derivation code prefix was not recognized.
24    #[error("Unsupported KERI key type: prefix '{0}'")]
25    UnsupportedKeyType(String),
26    /// Input string was empty; no derivation code could be read.
27    #[error("Missing KERI prefix: empty string")]
28    EmptyInput,
29    /// Base64url decoding of the key payload failed.
30    #[error("Base64url decode failed: {0}")]
31    DecodeError(String),
32    /// Decoded bytes were not the expected length for the key type.
33    #[error("Invalid key length: expected {expected} bytes, got {actual}")]
34    InvalidLength {
35        /// Expected byte count.
36        expected: usize,
37        /// Actual byte count.
38        actual: usize,
39    },
40}
41
42impl auths_crypto::AuthsErrorInfo for KeriDecodeError {
43    fn error_code(&self) -> &'static str {
44        match self {
45            Self::UnsupportedKeyType(_) => "AUTHS-E1201",
46            Self::EmptyInput => "AUTHS-E1202",
47            Self::DecodeError(_) => "AUTHS-E1203",
48            Self::InvalidLength { .. } => "AUTHS-E1204",
49        }
50    }
51
52    fn suggestion(&self) -> Option<&'static str> {
53        match self {
54            Self::UnsupportedKeyType(_) => Some(
55                "Supported verkey prefixes: 'D'/'B' (Ed25519 transferable/non-transferable), '1AAJ'/'1AAI' (P-256 transferable/non-transferable).",
56            ),
57            Self::EmptyInput => Some("Provide a non-empty KERI-encoded key string"),
58            _ => None,
59        }
60    }
61}
62
63/// A validated KERI public key supporting Ed25519 and P-256.
64///
65/// Parsed from a CESR-qualified string. The derivation code prefix
66/// determines the curve, key size, and transferability.
67///
68/// Usage:
69/// ```
70/// use auths_keri::KeriPublicKey;
71///
72/// // Ed25519 (D prefix, 32 bytes)
73/// let key = KeriPublicKey::parse("DAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA").unwrap();
74/// assert_eq!(key.as_bytes().len(), 32);
75///
76/// // P-256 transferable uses the "1AAJ" prefix (33 bytes compressed SEC1).
77/// ```
78#[derive(Debug, Clone, PartialEq, Eq)]
79pub enum KeriPublicKey {
80    /// Ed25519 public key (32 bytes).
81    ///
82    /// `transferable` records which CESR code qualified it: `D` (true) is the
83    /// rotating verkey code; `B` (false) is the non-transferable one used by
84    /// KERI witnesses. Both decode to the same 32-byte key and verify with the
85    /// same Ed25519 algorithm.
86    Ed25519 {
87        /// Raw Ed25519 public key (32 bytes).
88        key: [u8; 32],
89        /// Whether the key was qualified with the transferable code (`D`).
90        transferable: bool,
91    },
92    /// P-256 compressed public key (33 bytes, SEC1: 0x02/0x03 + x-coordinate).
93    ///
94    /// `transferable` records which CESR code qualified it: `1AAJ` (true) is
95    /// the rotating verkey code; `1AAI` (false) is the non-transferable one.
96    P256 {
97        /// Compressed SEC1 point (33 bytes).
98        key: [u8; 33],
99        /// Whether the key was qualified with the transferable code (`1AAJ`).
100        transferable: bool,
101    },
102}
103
104impl KeriPublicKey {
105    /// Parse a CESR-qualified key string, dispatching on the derivation code prefix.
106    ///
107    /// - `D` prefix → Ed25519 transferable (32 bytes)
108    /// - `B` prefix → Ed25519 non-transferable (32 bytes) — the KERI witness code
109    /// - `1AAJ` prefix → P-256 transferable (33 bytes compressed)
110    /// - `1AAI` prefix → P-256 non-transferable (33 bytes compressed)
111    ///
112    /// Per the CESR master code table, `D`/`B` and `1AAJ`/`1AAI` are the
113    /// transferable / non-transferable verkey codes for each curve. Both members
114    /// of a pair decode to the same raw key; only the recorded transferability
115    /// (and thus the rotation semantics) differ. Any other matter code returns
116    /// `Err(UnsupportedKeyType)`; malformed CESR returns `Err(DecodeError)`.
117    pub fn parse(encoded: &str) -> Result<Self, KeriDecodeError> {
118        if encoded.is_empty() {
119            return Err(KeriDecodeError::EmptyInput);
120        }
121
122        // cesride's qb64 decoder derives a primitive's length from its derivation
123        // code and slices the input by that length, so a non-ASCII, truncated, or
124        // unknown code makes it slice off a char boundary or past the end and panic.
125        // Validate the input is exactly one well-formed CESR primitive (with no
126        // trailing bytes) before the decoder runs; the curve is then selected by the
127        // in-band code below, never by raw byte length.
128        let primitive = crate::cesr_encode::take_matter_qb64(encoded)?;
129        if primitive.len() != encoded.len() {
130            return Err(KeriDecodeError::DecodeError(
131                "trailing bytes after verkey primitive".into(),
132            ));
133        }
134
135        let (bytes, code) = crate::cesr_encode::decode_verkey(encoded)?;
136
137        use cesride::matter::Codex;
138        if code.as_str() == Codex::Ed25519 || code.as_str() == Codex::Ed25519N {
139            let arr: [u8; 32] =
140                bytes
141                    .as_slice()
142                    .try_into()
143                    .map_err(|_| KeriDecodeError::InvalidLength {
144                        expected: 32,
145                        actual: bytes.len(),
146                    })?;
147            Ok(KeriPublicKey::Ed25519 {
148                key: arr,
149                transferable: code.as_str() == Codex::Ed25519,
150            })
151        } else if code.as_str() == Codex::ECDSA_256r1 || code.as_str() == Codex::ECDSA_256r1N {
152            let arr: [u8; 33] =
153                bytes
154                    .as_slice()
155                    .try_into()
156                    .map_err(|_| KeriDecodeError::InvalidLength {
157                        expected: 33,
158                        actual: bytes.len(),
159                    })?;
160            Ok(KeriPublicKey::P256 {
161                key: arr,
162                transferable: code.as_str() == Codex::ECDSA_256r1,
163            })
164        } else {
165            Err(KeriDecodeError::UnsupportedKeyType(code))
166        }
167    }
168
169    /// Returns the raw public key bytes (32 for Ed25519, 33 for P-256).
170    pub fn as_bytes(&self) -> &[u8] {
171        match self {
172            KeriPublicKey::Ed25519 { key, .. } => key,
173            KeriPublicKey::P256 { key, .. } => key,
174        }
175    }
176
177    /// Consume self and return the raw bytes as a Vec.
178    pub fn into_bytes(self) -> Vec<u8> {
179        match self {
180            KeriPublicKey::Ed25519 { key, .. } => key.to_vec(),
181            KeriPublicKey::P256 { key, .. } => key.to_vec(),
182        }
183    }
184
185    /// Returns the curve type.
186    pub fn curve(&self) -> auths_crypto::CurveType {
187        match self {
188            KeriPublicKey::Ed25519 { .. } => auths_crypto::CurveType::Ed25519,
189            KeriPublicKey::P256 { .. } => auths_crypto::CurveType::P256,
190        }
191    }
192
193    /// Whether this key is transferable (rotating).
194    ///
195    /// Each variant carries the transferability recorded from its CESR code:
196    /// Ed25519 from `D`/`B`, P-256 from `1AAJ`/`1AAI`.
197    pub fn is_transferable(&self) -> bool {
198        match self {
199            KeriPublicKey::Ed25519 { transferable, .. }
200            | KeriPublicKey::P256 { transferable, .. } => *transferable,
201        }
202    }
203
204    /// Returns the CESR derivation code prefix for this key type.
205    ///
206    /// `D`/`B` for transferable / non-transferable Ed25519; `1AAJ`/`1AAI` for
207    /// transferable / non-transferable P-256 (per the CESR master code table).
208    pub fn cesr_prefix(&self) -> &'static str {
209        match self {
210            KeriPublicKey::Ed25519 {
211                transferable: true, ..
212            } => "D",
213            KeriPublicKey::Ed25519 {
214                transferable: false,
215                ..
216            } => "B",
217            KeriPublicKey::P256 {
218                transferable: true, ..
219            } => "1AAJ",
220            KeriPublicKey::P256 {
221                transferable: false,
222                ..
223            } => "1AAI",
224        }
225    }
226
227    /// Encode this key as a CESR-qualified qb64 string, byte-identical to keripy.
228    ///
229    /// Ed25519 → `D…`; transferable P-256 → `1AAJ…`; non-transferable P-256 → `1AAI…`.
230    /// This is the CESR-correct encoding (proper lead-byte alignment), not the legacy
231    /// naive `D` + base64url(raw) form.
232    ///
233    /// Usage:
234    /// ```ignore
235    /// let qb64 = key.to_qb64()?;
236    /// ```
237    pub fn to_qb64(&self) -> Result<String, KeriDecodeError> {
238        let code = crate::cesr_encode::verkey_code(self.curve(), self.is_transferable());
239        crate::cesr_encode::encode_verkey(self.as_bytes(), code)
240    }
241
242    /// Construct a transferable Ed25519 verkey from a 32-byte slice.
243    ///
244    /// Ergonomic bridge for raw-byte sources (e.g. a `ring` public key) into the
245    /// typed key. Returns `Err(InvalidLength)` if the slice is not 32 bytes.
246    ///
247    /// Usage:
248    /// ```
249    /// use auths_keri::KeriPublicKey;
250    /// let key = KeriPublicKey::ed25519(&[0u8; 32]).unwrap();
251    /// assert!(matches!(key, KeriPublicKey::Ed25519 { .. }));
252    /// ```
253    pub fn ed25519(bytes: &[u8]) -> Result<Self, KeriDecodeError> {
254        let arr: [u8; 32] = bytes
255            .try_into()
256            .map_err(|_| KeriDecodeError::InvalidLength {
257                expected: 32,
258                actual: bytes.len(),
259            })?;
260        Ok(KeriPublicKey::Ed25519 {
261            key: arr,
262            transferable: true,
263        })
264    }
265
266    /// Construct a transferable verkey from raw bytes plus an explicit curve.
267    ///
268    /// The complement of [`Self::as_bytes`] + [`Self::curve`]: rebuilds the typed key
269    /// when you hold curve-tagged bytes (a `CurveType` carried alongside a `Vec<u8>`),
270    /// instead of re-guessing the curve from byte length. Encodes as transferable
271    /// (`D` / `1AAJ`). Returns `Err(InvalidLength)` if the length doesn't match the curve.
272    ///
273    /// Usage:
274    /// ```
275    /// use auths_keri::KeriPublicKey;
276    /// use auths_crypto::CurveType;
277    /// let key = KeriPublicKey::from_verkey_bytes(&[0u8; 32], CurveType::Ed25519).unwrap();
278    /// assert_eq!(key.curve(), CurveType::Ed25519);
279    /// ```
280    pub fn from_verkey_bytes(
281        bytes: &[u8],
282        curve: auths_crypto::CurveType,
283    ) -> Result<Self, KeriDecodeError> {
284        match curve {
285            auths_crypto::CurveType::Ed25519 => Self::ed25519(bytes),
286            auths_crypto::CurveType::P256 => {
287                let arr: [u8; 33] =
288                    bytes
289                        .try_into()
290                        .map_err(|_| KeriDecodeError::InvalidLength {
291                            expected: 33,
292                            actual: bytes.len(),
293                        })?;
294                Ok(KeriPublicKey::P256 {
295                    key: arr,
296                    transferable: true,
297                })
298            }
299        }
300    }
301
302    /// Verify a signature against this public key.
303    ///
304    /// Dispatches to the correct algorithm based on the key's curve:
305    /// - Ed25519 → `ring::signature::ED25519`
306    /// - P-256 → `p256::ecdsa` (handles compressed SEC1 keys natively)
307    ///
308    /// This method keeps the curve dispatch in one place so validation code
309    /// doesn't need to know about specific algorithms.
310    pub fn verify_signature(&self, message: &[u8], signature: &[u8]) -> Result<(), String> {
311        match self {
312            KeriPublicKey::Ed25519 { key: pk, .. } => {
313                use ring::signature::UnparsedPublicKey;
314                let verifier = UnparsedPublicKey::new(&ring::signature::ED25519, pk);
315                verifier
316                    .verify(message, signature)
317                    .map_err(|_| "Ed25519 signature verification failed".to_string())
318            }
319            KeriPublicKey::P256 { key: pk, .. } => {
320                use p256::ecdsa::{Signature, VerifyingKey, signature::Verifier};
321                // p256 crate handles compressed SEC1 (33 bytes) natively
322                let vk = VerifyingKey::from_sec1_bytes(pk)
323                    .map_err(|e| format!("P-256 key parse failed: {e}"))?;
324                let sig = Signature::from_slice(signature)
325                    .map_err(|e| format!("P-256 signature parse failed: {e}"))?;
326                vk.verify(message, &sig)
327                    .map_err(|e| format!("P-256 signature verification failed: {e}"))
328            }
329        }
330    }
331}
332
333#[cfg(test)]
334mod tests {
335    use super::*;
336    use base64::{Engine, engine::general_purpose::URL_SAFE_NO_PAD};
337
338    #[test]
339    fn parse_ed25519_all_zeros() {
340        let key = KeriPublicKey::parse("DAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA").unwrap();
341        assert_eq!(key.as_bytes(), &[0u8; 32]);
342        assert!(matches!(key, KeriPublicKey::Ed25519 { .. }));
343        assert!(key.is_transferable());
344        assert_eq!(key.curve(), auths_crypto::CurveType::Ed25519);
345        assert_eq!(key.cesr_prefix(), "D");
346    }
347
348    #[test]
349    fn parse_p256_key() {
350        // `1AAJ` is the transferable P-256 verkey code (the rotating default).
351        let zeros_33 = [0u8; 33];
352        let encoded = format!("1AAJ{}", URL_SAFE_NO_PAD.encode(zeros_33));
353        let key = KeriPublicKey::parse(&encoded).unwrap();
354        assert_eq!(key.as_bytes().len(), 33);
355        assert!(matches!(key, KeriPublicKey::P256 { .. }));
356        assert_eq!(key.curve(), auths_crypto::CurveType::P256);
357        assert!(key.is_transferable());
358        assert_eq!(key.cesr_prefix(), "1AAJ");
359    }
360
361    #[test]
362    fn parses_both_1aai_and_1aaj() {
363        // Both P-256 codes decode to the same 33-byte point; transferability
364        // and the round-tripped prefix differ.
365        let zeros_33 = [0u8; 33];
366        let transferable =
367            KeriPublicKey::parse(&format!("1AAJ{}", URL_SAFE_NO_PAD.encode(zeros_33))).unwrap();
368        let non_transferable =
369            KeriPublicKey::parse(&format!("1AAI{}", URL_SAFE_NO_PAD.encode(zeros_33))).unwrap();
370        assert_eq!(transferable.as_bytes(), non_transferable.as_bytes());
371        assert!(transferable.is_transferable());
372        assert!(!non_transferable.is_transferable());
373        assert_eq!(transferable.cesr_prefix(), "1AAJ");
374        assert_eq!(non_transferable.cesr_prefix(), "1AAI");
375    }
376
377    #[test]
378    fn parse_rejects_malformed_qb64_without_panicking() {
379        // A derivation code whose declared primitive size exceeds the actual
380        // input length must fail closed with a typed error, never an
381        // out-of-bounds slice panic inside the CESR decoder.
382        for malformed in ["6AAA1IAA", "1AAJ", "D", "1AAI"] {
383            assert!(
384                KeriPublicKey::parse(malformed).is_err(),
385                "malformed CESR verkey {malformed:?} must be rejected, not panic"
386            );
387        }
388    }
389
390    #[test]
391    fn parse_p256_non_transferable() {
392        let zeros_33 = [0u8; 33];
393        let encoded = format!("1AAI{}", URL_SAFE_NO_PAD.encode(zeros_33));
394        let key = KeriPublicKey::parse(&encoded).unwrap();
395        assert!(matches!(
396            key,
397            KeriPublicKey::P256 {
398                transferable: false,
399                ..
400            }
401        ));
402    }
403
404    #[test]
405    fn rejects_empty_input() {
406        let err = KeriPublicKey::parse("").unwrap_err();
407        assert_eq!(err, KeriDecodeError::EmptyInput);
408    }
409
410    #[test]
411    fn rejects_unknown_prefix() {
412        // Not valid CESR for any verkey code: rejected either as undecodable or
413        // (if it parses to some other matter code) as an unsupported key type.
414        let err = KeriPublicKey::parse("Xsomething").unwrap_err();
415        assert!(
416            matches!(
417                err,
418                KeriDecodeError::DecodeError(_) | KeriDecodeError::UnsupportedKeyType(_)
419            ),
420            "unexpected error: {err:?}"
421        );
422    }
423
424    #[test]
425    fn parses_non_transferable_ed25519_code() {
426        // `B` (Ed25519N) is the standard KERI witness key code. It decodes to the
427        // same 32-byte key as `D`; only transferability and the prefix differ.
428        let b_code =
429            crate::cesr_encode::encode_verkey(&[0u8; 32], cesride::matter::Codex::Ed25519N)
430                .unwrap();
431        let key = KeriPublicKey::parse(&b_code).unwrap();
432        assert!(matches!(
433            key,
434            KeriPublicKey::Ed25519 {
435                transferable: false,
436                ..
437            }
438        ));
439        assert_eq!(key.as_bytes(), &[0u8; 32]);
440        assert_eq!(key.curve(), auths_crypto::CurveType::Ed25519);
441        assert!(!key.is_transferable());
442        assert_eq!(key.cesr_prefix(), "B");
443    }
444
445    #[test]
446    fn parses_both_d_and_b_ed25519() {
447        // Both Ed25519 codes decode to the same 32-byte key; transferability and
448        // the round-tripped prefix differ — mirroring the P-256 1AAJ/1AAI pair.
449        let d =
450            crate::cesr_encode::encode_verkey(&[7u8; 32], cesride::matter::Codex::Ed25519).unwrap();
451        let b = crate::cesr_encode::encode_verkey(&[7u8; 32], cesride::matter::Codex::Ed25519N)
452            .unwrap();
453        let transferable = KeriPublicKey::parse(&d).unwrap();
454        let non_transferable = KeriPublicKey::parse(&b).unwrap();
455        assert_eq!(transferable.as_bytes(), non_transferable.as_bytes());
456        assert!(transferable.is_transferable());
457        assert!(!non_transferable.is_transferable());
458        assert_eq!(transferable.cesr_prefix(), "D");
459        assert_eq!(non_transferable.cesr_prefix(), "B");
460    }
461
462    #[test]
463    fn rejects_invalid_base64() {
464        let err = KeriPublicKey::parse("D!!!invalid!!!").unwrap_err();
465        assert!(matches!(err, KeriDecodeError::DecodeError(_)));
466    }
467
468    #[test]
469    fn rejects_wrong_length_ed25519() {
470        // A naive `D` + base64(31 bytes) has the wrong qb64 length for the
471        // Ed25519 code, so cesride rejects it as malformed CESR.
472        let short = [0u8; 31];
473        let encoded = format!("D{}", URL_SAFE_NO_PAD.encode(short));
474        let err = KeriPublicKey::parse(&encoded).unwrap_err();
475        assert!(matches!(err, KeriDecodeError::DecodeError(_)));
476    }
477
478    #[test]
479    fn rejects_wrong_length_p256() {
480        // A naive `1AAJ` + base64(32 bytes) has the wrong qb64 length for the
481        // ECDSA_256r1 code, so cesride rejects it as malformed CESR.
482        let short = [0u8; 32];
483        let encoded = format!("1AAJ{}", URL_SAFE_NO_PAD.encode(short));
484        let err = KeriPublicKey::parse(&encoded).unwrap_err();
485        assert!(matches!(err, KeriDecodeError::DecodeError(_)));
486    }
487
488    // `as_bytes()` returns `&[u8]`; Ed25519 keys must still convert cleanly to a
489    // 32-byte array for callers that need the fixed-width form.
490    #[test]
491    fn ed25519_as_bytes_is_32() {
492        let key = KeriPublicKey::parse("DAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA").unwrap();
493        let bytes = key.as_bytes();
494        assert_eq!(bytes.len(), 32);
495        // Can still convert to [u8; 32] for backward compat
496        let arr: [u8; 32] = bytes.try_into().unwrap();
497        assert_eq!(arr, [0u8; 32]);
498    }
499
500    /// keripy 1.3.4 reference: `Verfer(bytes(0..32), Ed25519).qb64`. `parse` must
501    /// decode it to the exact 32 raw bytes via CESR alignment (lead-byte aware),
502    /// NOT naive base64-after-`D` (which would recover shifted, wrong bytes).
503    #[test]
504    fn parse_matches_keripy_ed25519_vector() {
505        let raw: Vec<u8> = (0u8..32).collect();
506        let key = KeriPublicKey::parse("DAABAgMEBQYHCAkKCwwNDg8QERITFBUWFxgZGhscHR4f").unwrap();
507        assert_eq!(key.as_bytes(), raw.as_slice());
508        assert_eq!(key.curve(), auths_crypto::CurveType::Ed25519);
509    }
510
511    /// `parse` must invert `to_qb64` (cesride) for a non-zero Ed25519 key.
512    #[test]
513    fn parse_inverts_to_qb64_ed25519() {
514        let raw: Vec<u8> = (0u8..32).collect();
515        let key = KeriPublicKey::ed25519(&raw).unwrap();
516        let qb64 = key.to_qb64().unwrap();
517        let parsed = KeriPublicKey::parse(&qb64).unwrap();
518        assert_eq!(parsed, key, "parse must invert to_qb64 (CESR round-trip)");
519    }
520
521    /// `parse` must invert `to_qb64` for a non-zero transferable P-256 key.
522    #[test]
523    fn parse_inverts_to_qb64_p256() {
524        let mut point = [0u8; 33];
525        point[0] = 0x02;
526        for (i, b) in point.iter_mut().enumerate().skip(1) {
527            *b = i as u8;
528        }
529        let key = KeriPublicKey::from_verkey_bytes(&point, auths_crypto::CurveType::P256).unwrap();
530        let qb64 = key.to_qb64().unwrap();
531        let parsed = KeriPublicKey::parse(&qb64).unwrap();
532        assert_eq!(parsed, key, "P-256 parse must invert to_qb64");
533        assert!(parsed.is_transferable());
534    }
535}