1#[derive(Debug, Clone, thiserror::Error, PartialEq, Eq)]
15#[non_exhaustive]
16pub enum KeriDecodeError {
17 #[error("Unsupported KERI key type: prefix '{0}'")]
19 UnsupportedKeyType(String),
20 #[error("Missing KERI prefix: empty string")]
22 EmptyInput,
23 #[error("Base64url decode failed: {0}")]
25 DecodeError(String),
26 #[error("Invalid key length: expected {expected} bytes, got {actual}")]
28 InvalidLength {
29 expected: usize,
31 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#[derive(Debug, Clone, PartialEq, Eq)]
73pub enum KeriPublicKey {
74 Ed25519([u8; 32]),
76 P256 {
81 key: [u8; 33],
83 transferable: bool,
85 },
86}
87
88impl KeriPublicKey {
89 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 pub fn as_bytes(&self) -> &[u8] {
140 match self {
141 KeriPublicKey::Ed25519(b) => b,
142 KeriPublicKey::P256 { key, .. } => key,
143 }
144 }
145
146 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 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 pub fn is_transferable(&self) -> bool {
167 match self {
168 KeriPublicKey::Ed25519(_) => true,
169 KeriPublicKey::P256 { transferable, .. } => *transferable,
170 }
171 }
172
173 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 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 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 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 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 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 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 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 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 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 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 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 #[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 let arr: [u8; 32] = bytes.try_into().unwrap();
417 assert_eq!(arr, [0u8; 32]);
418 }
419
420 #[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 #[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 #[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}