Skip to main content

auths_crypto/
keri.rs

1//! KERI CESR Ed25519 key parsing.
2//!
3//! Decodes KERI-encoded public keys: 'D' derivation code prefix + base64url-no-pad
4//! encoded 32-byte Ed25519 key, as defined by the KERI CESR specification.
5
6use base64::{Engine, engine::general_purpose::URL_SAFE_NO_PAD};
7
8/// Errors from decoding a KERI-encoded Ed25519 public key.
9#[derive(Debug, Clone, thiserror::Error, PartialEq, Eq)]
10#[non_exhaustive]
11pub enum KeriDecodeError {
12    #[error("Invalid KERI prefix: expected 'D' for Ed25519, got '{0}'")]
13    InvalidPrefix(char),
14    #[error("Missing KERI prefix: empty string")]
15    EmptyInput,
16    #[error("Base64url decode failed: {0}")]
17    DecodeError(String),
18    #[error("Invalid Ed25519 key length: expected 32 bytes, got {0}")]
19    InvalidLength(usize),
20}
21
22impl crate::AuthsErrorInfo for KeriDecodeError {
23    fn error_code(&self) -> &'static str {
24        match self {
25            Self::InvalidPrefix(_) => "AUTHS-E1201",
26            Self::EmptyInput => "AUTHS-E1202",
27            Self::DecodeError(_) => "AUTHS-E1203",
28            Self::InvalidLength(_) => "AUTHS-E1204",
29        }
30    }
31
32    fn suggestion(&self) -> Option<&'static str> {
33        match self {
34            Self::InvalidPrefix(_) => Some("KERI Ed25519 keys must start with 'D' prefix"),
35            Self::EmptyInput => Some("Provide a non-empty KERI-encoded key string"),
36            _ => None,
37        }
38    }
39}
40
41/// A validated KERI Ed25519 public key (32 bytes).
42///
43/// Args:
44/// * The inner `[u8; 32]` is the raw Ed25519 public key bytes, decoded from
45///   a KERI CESR-encoded string with 'D' derivation code prefix.
46///
47/// Usage:
48/// ```
49/// use auths_crypto::KeriPublicKey;
50///
51/// let key = KeriPublicKey::parse("DAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA").unwrap();
52/// assert_eq!(key.as_bytes(), &[0u8; 32]);
53/// ```
54#[derive(Debug)]
55pub struct KeriPublicKey([u8; 32]);
56
57impl KeriPublicKey {
58    /// Parses a KERI-encoded Ed25519 public key string into a validated type.
59    ///
60    /// The input must be a 'D'-prefixed base64url-no-pad encoded 32-byte Ed25519 key,
61    /// as defined by the KERI CESR specification.
62    ///
63    /// Args:
64    /// * `encoded`: The KERI-encoded string (e.g., `"D<base64url>"`).
65    ///
66    /// Usage:
67    /// ```
68    /// use auths_crypto::KeriPublicKey;
69    ///
70    /// let key = KeriPublicKey::parse("DAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA").unwrap();
71    /// let raw = key.as_bytes();
72    /// ```
73    pub fn parse(encoded: &str) -> Result<Self, KeriDecodeError> {
74        let payload = validate_and_strip_prefix(encoded)?;
75        let bytes = decode_base64url(payload)?;
76        let array = enforce_key_length(bytes)?;
77        Ok(Self(array))
78    }
79
80    /// Returns the raw 32-byte Ed25519 public key.
81    pub fn as_bytes(&self) -> &[u8; 32] {
82        &self.0
83    }
84
85    /// Consumes self and returns the inner 32-byte array.
86    pub fn into_bytes(self) -> [u8; 32] {
87        self.0
88    }
89}
90
91fn validate_and_strip_prefix(encoded: &str) -> Result<&str, KeriDecodeError> {
92    match encoded.strip_prefix('D') {
93        Some(payload) => Ok(payload),
94        None => match encoded.chars().next() {
95            Some(c) => Err(KeriDecodeError::InvalidPrefix(c)),
96            None => Err(KeriDecodeError::EmptyInput),
97        },
98    }
99}
100
101fn decode_base64url(payload: &str) -> Result<Vec<u8>, KeriDecodeError> {
102    URL_SAFE_NO_PAD
103        .decode(payload)
104        .map_err(|e| KeriDecodeError::DecodeError(e.to_string()))
105}
106
107fn enforce_key_length(bytes: Vec<u8>) -> Result<[u8; 32], KeriDecodeError> {
108    let len = bytes.len();
109    bytes
110        .try_into()
111        .map_err(|_| KeriDecodeError::InvalidLength(len))
112}
113
114#[cfg(test)]
115mod tests {
116    use super::*;
117
118    #[test]
119    fn parse_all_zeros() {
120        let key = KeriPublicKey::parse("DAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA").unwrap();
121        assert_eq!(key.as_bytes(), &[0u8; 32]);
122    }
123
124    #[test]
125    fn roundtrip_into_bytes() {
126        let key = KeriPublicKey::parse("DAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA").unwrap();
127        let bytes = key.into_bytes();
128        assert_eq!(bytes, [0u8; 32]);
129    }
130
131    #[test]
132    fn rejects_empty_input() {
133        let err = KeriPublicKey::parse("").unwrap_err();
134        assert_eq!(err, KeriDecodeError::EmptyInput);
135    }
136
137    #[test]
138    fn rejects_wrong_prefix() {
139        let err = KeriPublicKey::parse("Xsomething").unwrap_err();
140        assert!(matches!(err, KeriDecodeError::InvalidPrefix('X')));
141    }
142
143    #[test]
144    fn rejects_invalid_base64() {
145        let err = KeriPublicKey::parse("D!!!invalid!!!").unwrap_err();
146        assert!(matches!(err, KeriDecodeError::DecodeError(_)));
147    }
148}