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