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    /// The raw public-key bytes (32 for Ed25519, 33 compressed SEC1 for P-256) —
194    /// paired with [`Self::curve`] so consumers dispatch on the typed curve and
195    /// never re-match variants.
196    pub fn raw_bytes(&self) -> &[u8] {
197        match self {
198            KeriPublicKey::Ed25519 { key, .. } => key,
199            KeriPublicKey::P256 { key, .. } => key,
200        }
201    }
202
203    /// Whether this key is transferable (rotating).
204    ///
205    /// Each variant carries the transferability recorded from its CESR code:
206    /// Ed25519 from `D`/`B`, P-256 from `1AAJ`/`1AAI`.
207    pub fn is_transferable(&self) -> bool {
208        match self {
209            KeriPublicKey::Ed25519 { transferable, .. }
210            | KeriPublicKey::P256 { transferable, .. } => *transferable,
211        }
212    }
213
214    /// Returns the CESR derivation code prefix for this key type.
215    ///
216    /// `D`/`B` for transferable / non-transferable Ed25519; `1AAJ`/`1AAI` for
217    /// transferable / non-transferable P-256 (per the CESR master code table).
218    pub fn cesr_prefix(&self) -> &'static str {
219        match self {
220            KeriPublicKey::Ed25519 {
221                transferable: true, ..
222            } => "D",
223            KeriPublicKey::Ed25519 {
224                transferable: false,
225                ..
226            } => "B",
227            KeriPublicKey::P256 {
228                transferable: true, ..
229            } => "1AAJ",
230            KeriPublicKey::P256 {
231                transferable: false,
232                ..
233            } => "1AAI",
234        }
235    }
236
237    /// Encode this key as a CESR-qualified qb64 string, byte-identical to keripy.
238    ///
239    /// Ed25519 → `D…`; transferable P-256 → `1AAJ…`; non-transferable P-256 → `1AAI…`.
240    /// This is the CESR-correct encoding (proper lead-byte alignment), not the legacy
241    /// naive `D` + base64url(raw) form.
242    ///
243    /// Usage:
244    /// ```ignore
245    /// let qb64 = key.to_qb64()?;
246    /// ```
247    pub fn to_qb64(&self) -> Result<String, KeriDecodeError> {
248        let code = crate::cesr_encode::verkey_code(self.curve(), self.is_transferable());
249        crate::cesr_encode::encode_verkey(self.as_bytes(), code)
250    }
251
252    /// Construct a transferable Ed25519 verkey from a 32-byte slice.
253    ///
254    /// Ergonomic bridge for raw-byte sources (e.g. a `ring` public key) into the
255    /// typed key. Returns `Err(InvalidLength)` if the slice is not 32 bytes.
256    ///
257    /// Usage:
258    /// ```
259    /// use auths_keri::KeriPublicKey;
260    /// let key = KeriPublicKey::ed25519(&[0u8; 32]).unwrap();
261    /// assert!(matches!(key, KeriPublicKey::Ed25519 { .. }));
262    /// ```
263    pub fn ed25519(bytes: &[u8]) -> Result<Self, KeriDecodeError> {
264        let arr: [u8; 32] = bytes
265            .try_into()
266            .map_err(|_| KeriDecodeError::InvalidLength {
267                expected: 32,
268                actual: bytes.len(),
269            })?;
270        Ok(KeriPublicKey::Ed25519 {
271            key: arr,
272            transferable: true,
273        })
274    }
275
276    /// Construct a transferable verkey from raw bytes plus an explicit curve.
277    ///
278    /// The complement of [`Self::as_bytes`] + [`Self::curve`]: rebuilds the typed key
279    /// when you hold curve-tagged bytes (a `CurveType` carried alongside a `Vec<u8>`),
280    /// instead of re-guessing the curve from byte length. Encodes as transferable
281    /// (`D` / `1AAJ`). Returns `Err(InvalidLength)` if the length doesn't match the curve.
282    ///
283    /// Usage:
284    /// ```
285    /// use auths_keri::KeriPublicKey;
286    /// use auths_crypto::CurveType;
287    /// let key = KeriPublicKey::from_verkey_bytes(&[0u8; 32], CurveType::Ed25519).unwrap();
288    /// assert_eq!(key.curve(), CurveType::Ed25519);
289    /// ```
290    pub fn from_verkey_bytes(
291        bytes: &[u8],
292        curve: auths_crypto::CurveType,
293    ) -> Result<Self, KeriDecodeError> {
294        match curve {
295            auths_crypto::CurveType::Ed25519 => Self::ed25519(bytes),
296            auths_crypto::CurveType::P256 => {
297                let arr: [u8; 33] =
298                    bytes
299                        .try_into()
300                        .map_err(|_| KeriDecodeError::InvalidLength {
301                            expected: 33,
302                            actual: bytes.len(),
303                        })?;
304                Ok(KeriPublicKey::P256 {
305                    key: arr,
306                    transferable: true,
307                })
308            }
309        }
310    }
311
312    /// Verify a signature against this public key.
313    ///
314    /// Dispatches to the correct algorithm based on the key's curve:
315    /// - Ed25519 → `ring::signature::ED25519`
316    /// - P-256 → `p256::ecdsa` (handles compressed SEC1 keys natively)
317    ///
318    /// This method keeps the curve dispatch in one place so validation code
319    /// doesn't need to know about specific algorithms.
320    pub fn verify_signature(&self, message: &[u8], signature: &[u8]) -> Result<(), String> {
321        match self {
322            KeriPublicKey::Ed25519 { key: pk, .. } => {
323                use ring::signature::UnparsedPublicKey;
324                let verifier = UnparsedPublicKey::new(&ring::signature::ED25519, pk);
325                verifier
326                    .verify(message, signature)
327                    .map_err(|_| "Ed25519 signature verification failed".to_string())
328            }
329            KeriPublicKey::P256 { key: pk, .. } => {
330                use p256::ecdsa::{Signature, VerifyingKey, signature::Verifier};
331                // p256 crate handles compressed SEC1 (33 bytes) natively
332                let vk = VerifyingKey::from_sec1_bytes(pk)
333                    .map_err(|e| format!("P-256 key parse failed: {e}"))?;
334                let sig = Signature::from_slice(signature)
335                    .map_err(|e| format!("P-256 signature parse failed: {e}"))?;
336                vk.verify(message, &sig)
337                    .map_err(|e| format!("P-256 signature verification failed: {e}"))
338            }
339        }
340    }
341}
342
343#[cfg(test)]
344mod tests {
345    use super::*;
346    use base64::{Engine, engine::general_purpose::URL_SAFE_NO_PAD};
347
348    #[test]
349    fn parse_ed25519_all_zeros() {
350        let key = KeriPublicKey::parse("DAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA").unwrap();
351        assert_eq!(key.as_bytes(), &[0u8; 32]);
352        assert!(matches!(key, KeriPublicKey::Ed25519 { .. }));
353        assert!(key.is_transferable());
354        assert_eq!(key.curve(), auths_crypto::CurveType::Ed25519);
355        assert_eq!(key.cesr_prefix(), "D");
356    }
357
358    #[test]
359    fn parse_p256_key() {
360        // `1AAJ` is the transferable P-256 verkey code (the rotating default).
361        let zeros_33 = [0u8; 33];
362        let encoded = format!("1AAJ{}", URL_SAFE_NO_PAD.encode(zeros_33));
363        let key = KeriPublicKey::parse(&encoded).unwrap();
364        assert_eq!(key.as_bytes().len(), 33);
365        assert!(matches!(key, KeriPublicKey::P256 { .. }));
366        assert_eq!(key.curve(), auths_crypto::CurveType::P256);
367        assert!(key.is_transferable());
368        assert_eq!(key.cesr_prefix(), "1AAJ");
369    }
370
371    #[test]
372    fn parses_both_1aai_and_1aaj() {
373        // Both P-256 codes decode to the same 33-byte point; transferability
374        // and the round-tripped prefix differ.
375        let zeros_33 = [0u8; 33];
376        let transferable =
377            KeriPublicKey::parse(&format!("1AAJ{}", URL_SAFE_NO_PAD.encode(zeros_33))).unwrap();
378        let non_transferable =
379            KeriPublicKey::parse(&format!("1AAI{}", URL_SAFE_NO_PAD.encode(zeros_33))).unwrap();
380        assert_eq!(transferable.as_bytes(), non_transferable.as_bytes());
381        assert!(transferable.is_transferable());
382        assert!(!non_transferable.is_transferable());
383        assert_eq!(transferable.cesr_prefix(), "1AAJ");
384        assert_eq!(non_transferable.cesr_prefix(), "1AAI");
385    }
386
387    #[test]
388    fn parse_rejects_malformed_qb64_without_panicking() {
389        // A derivation code whose declared primitive size exceeds the actual
390        // input length must fail closed with a typed error, never an
391        // out-of-bounds slice panic inside the CESR decoder.
392        for malformed in ["6AAA1IAA", "1AAJ", "D", "1AAI"] {
393            assert!(
394                KeriPublicKey::parse(malformed).is_err(),
395                "malformed CESR verkey {malformed:?} must be rejected, not panic"
396            );
397        }
398    }
399
400    #[test]
401    fn parse_p256_non_transferable() {
402        let zeros_33 = [0u8; 33];
403        let encoded = format!("1AAI{}", URL_SAFE_NO_PAD.encode(zeros_33));
404        let key = KeriPublicKey::parse(&encoded).unwrap();
405        assert!(matches!(
406            key,
407            KeriPublicKey::P256 {
408                transferable: false,
409                ..
410            }
411        ));
412    }
413
414    #[test]
415    fn rejects_empty_input() {
416        let err = KeriPublicKey::parse("").unwrap_err();
417        assert_eq!(err, KeriDecodeError::EmptyInput);
418    }
419
420    #[test]
421    fn rejects_unknown_prefix() {
422        // Not valid CESR for any verkey code: rejected either as undecodable or
423        // (if it parses to some other matter code) as an unsupported key type.
424        let err = KeriPublicKey::parse("Xsomething").unwrap_err();
425        assert!(
426            matches!(
427                err,
428                KeriDecodeError::DecodeError(_) | KeriDecodeError::UnsupportedKeyType(_)
429            ),
430            "unexpected error: {err:?}"
431        );
432    }
433
434    #[test]
435    fn parses_non_transferable_ed25519_code() {
436        // `B` (Ed25519N) is the standard KERI witness key code. It decodes to the
437        // same 32-byte key as `D`; only transferability and the prefix differ.
438        let b_code =
439            crate::cesr_encode::encode_verkey(&[0u8; 32], cesride::matter::Codex::Ed25519N)
440                .unwrap();
441        let key = KeriPublicKey::parse(&b_code).unwrap();
442        assert!(matches!(
443            key,
444            KeriPublicKey::Ed25519 {
445                transferable: false,
446                ..
447            }
448        ));
449        assert_eq!(key.as_bytes(), &[0u8; 32]);
450        assert_eq!(key.curve(), auths_crypto::CurveType::Ed25519);
451        assert!(!key.is_transferable());
452        assert_eq!(key.cesr_prefix(), "B");
453    }
454
455    #[test]
456    fn parses_both_d_and_b_ed25519() {
457        // Both Ed25519 codes decode to the same 32-byte key; transferability and
458        // the round-tripped prefix differ — mirroring the P-256 1AAJ/1AAI pair.
459        let d =
460            crate::cesr_encode::encode_verkey(&[7u8; 32], cesride::matter::Codex::Ed25519).unwrap();
461        let b = crate::cesr_encode::encode_verkey(&[7u8; 32], cesride::matter::Codex::Ed25519N)
462            .unwrap();
463        let transferable = KeriPublicKey::parse(&d).unwrap();
464        let non_transferable = KeriPublicKey::parse(&b).unwrap();
465        assert_eq!(transferable.as_bytes(), non_transferable.as_bytes());
466        assert!(transferable.is_transferable());
467        assert!(!non_transferable.is_transferable());
468        assert_eq!(transferable.cesr_prefix(), "D");
469        assert_eq!(non_transferable.cesr_prefix(), "B");
470    }
471
472    #[test]
473    fn rejects_invalid_base64() {
474        let err = KeriPublicKey::parse("D!!!invalid!!!").unwrap_err();
475        assert!(matches!(err, KeriDecodeError::DecodeError(_)));
476    }
477
478    #[test]
479    fn rejects_wrong_length_ed25519() {
480        // A naive `D` + base64(31 bytes) has the wrong qb64 length for the
481        // Ed25519 code, so cesride rejects it as malformed CESR.
482        let short = [0u8; 31];
483        let encoded = format!("D{}", URL_SAFE_NO_PAD.encode(short));
484        let err = KeriPublicKey::parse(&encoded).unwrap_err();
485        assert!(matches!(err, KeriDecodeError::DecodeError(_)));
486    }
487
488    #[test]
489    fn rejects_wrong_length_p256() {
490        // A naive `1AAJ` + base64(32 bytes) has the wrong qb64 length for the
491        // ECDSA_256r1 code, so cesride rejects it as malformed CESR.
492        let short = [0u8; 32];
493        let encoded = format!("1AAJ{}", URL_SAFE_NO_PAD.encode(short));
494        let err = KeriPublicKey::parse(&encoded).unwrap_err();
495        assert!(matches!(err, KeriDecodeError::DecodeError(_)));
496    }
497
498    // `as_bytes()` returns `&[u8]`; Ed25519 keys must still convert cleanly to a
499    // 32-byte array for callers that need the fixed-width form.
500    #[test]
501    fn ed25519_as_bytes_is_32() {
502        let key = KeriPublicKey::parse("DAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA").unwrap();
503        let bytes = key.as_bytes();
504        assert_eq!(bytes.len(), 32);
505        // Can still convert to [u8; 32] for backward compat
506        let arr: [u8; 32] = bytes.try_into().unwrap();
507        assert_eq!(arr, [0u8; 32]);
508    }
509
510    /// keripy 1.3.4 reference: `Verfer(bytes(0..32), Ed25519).qb64`. `parse` must
511    /// decode it to the exact 32 raw bytes via CESR alignment (lead-byte aware),
512    /// NOT naive base64-after-`D` (which would recover shifted, wrong bytes).
513    #[test]
514    fn parse_matches_keripy_ed25519_vector() {
515        let raw: Vec<u8> = (0u8..32).collect();
516        let key = KeriPublicKey::parse("DAABAgMEBQYHCAkKCwwNDg8QERITFBUWFxgZGhscHR4f").unwrap();
517        assert_eq!(key.as_bytes(), raw.as_slice());
518        assert_eq!(key.curve(), auths_crypto::CurveType::Ed25519);
519    }
520
521    /// `parse` must invert `to_qb64` (cesride) for a non-zero Ed25519 key.
522    #[test]
523    fn parse_inverts_to_qb64_ed25519() {
524        let raw: Vec<u8> = (0u8..32).collect();
525        let key = KeriPublicKey::ed25519(&raw).unwrap();
526        let qb64 = key.to_qb64().unwrap();
527        let parsed = KeriPublicKey::parse(&qb64).unwrap();
528        assert_eq!(parsed, key, "parse must invert to_qb64 (CESR round-trip)");
529    }
530
531    /// `parse` must invert `to_qb64` for a non-zero transferable P-256 key.
532    #[test]
533    fn parse_inverts_to_qb64_p256() {
534        let mut point = [0u8; 33];
535        point[0] = 0x02;
536        for (i, b) in point.iter_mut().enumerate().skip(1) {
537            *b = i as u8;
538        }
539        let key = KeriPublicKey::from_verkey_bytes(&point, auths_crypto::CurveType::P256).unwrap();
540        let qb64 = key.to_qb64().unwrap();
541        let parsed = KeriPublicKey::parse(&qb64).unwrap();
542        assert_eq!(parsed, key, "P-256 parse must invert to_qb64");
543        assert!(parsed.is_transferable());
544    }
545}