1use crate::AID_PUBKEY_IDENTIFIER_LEN;
13use base64ct::{Base64UrlUnpadded, Encoding};
14use serde::{Deserialize, Serialize};
15use std::fmt;
16
17const AID_PUBKEY_PREFIX: &str = "aid:pubkey:";
19
20const AID_PUBKEY_ED25519_PREFIX: &str = "aid:pubkey:ed25519:";
22
23const AID_PUBKEY_P256_PREFIX: &str = "aid:pubkey:p256:";
25
26const AID_P256_IDENTIFIER_LEN: usize = 44;
29
30#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
36#[non_exhaustive]
37pub enum AidAlgorithm {
38 Ed25519,
40 P256,
43}
44
45impl AidAlgorithm {
46 pub fn as_str(&self) -> &'static str {
48 match self {
49 AidAlgorithm::Ed25519 => "ed25519",
50 AidAlgorithm::P256 => "p256",
51 }
52 }
53}
54
55#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
62#[serde(try_from = "String", into = "String")]
63pub struct Aid(String);
64
65impl Aid {
66 pub fn parse(s: &str) -> Result<Self, AidParseError> {
74 if let Some(identifier) = s.strip_prefix(AID_PUBKEY_ED25519_PREFIX) {
75 validate_ed25519_identifier(identifier)?;
76 return Ok(Self(s.to_string()));
77 }
78 if let Some(identifier) = s.strip_prefix(AID_PUBKEY_P256_PREFIX) {
79 validate_p256_identifier(identifier)?;
80 return Ok(Self(s.to_string()));
81 }
82 if let Some(identifier) = s.strip_prefix(AID_PUBKEY_PREFIX) {
83 if identifier.contains(':') {
87 let (method, _) = identifier.split_once(':').unwrap();
88 return Err(AidParseError::UnsupportedMethod(format!("pubkey:{method}")));
89 }
90 validate_ed25519_identifier(identifier)?;
91 return Ok(Self(s.to_string()));
92 }
93 if let Some(rest) = s.strip_prefix("aid:") {
95 let method = rest.split(':').next().unwrap_or("");
96 return Err(AidParseError::UnsupportedMethod(method.to_string()));
97 }
98 Err(AidParseError::MissingScheme)
99 }
100
101 pub fn from_ed25519(pubkey: &[u8; 32]) -> Self {
106 let identifier = Base64UrlUnpadded::encode_string(pubkey);
107 debug_assert_eq!(identifier.len(), AID_PUBKEY_IDENTIFIER_LEN);
108 Self(format!("{AID_PUBKEY_PREFIX}{identifier}"))
109 }
110
111 pub fn from_ed25519_tagged(pubkey: &[u8; 32]) -> Self {
113 let identifier = Base64UrlUnpadded::encode_string(pubkey);
114 debug_assert_eq!(identifier.len(), AID_PUBKEY_IDENTIFIER_LEN);
115 Self(format!("{AID_PUBKEY_ED25519_PREFIX}{identifier}"))
116 }
117
118 pub fn from_p256(compressed_point: &[u8; 33]) -> Self {
120 let identifier = Base64UrlUnpadded::encode_string(compressed_point);
121 debug_assert_eq!(identifier.len(), AID_P256_IDENTIFIER_LEN);
122 Self(format!("{AID_PUBKEY_P256_PREFIX}{identifier}"))
123 }
124
125 pub fn algorithm(&self) -> AidAlgorithm {
127 if self.0.starts_with(AID_PUBKEY_P256_PREFIX) {
128 AidAlgorithm::P256
129 } else {
130 AidAlgorithm::Ed25519
131 }
132 }
133
134 pub fn identifier(&self) -> &str {
139 if let Some(id) = self.0.strip_prefix(AID_PUBKEY_ED25519_PREFIX) {
140 id
141 } else if let Some(id) = self.0.strip_prefix(AID_PUBKEY_P256_PREFIX) {
142 id
143 } else {
144 &self.0[AID_PUBKEY_PREFIX.len()..]
145 }
146 }
147
148 pub fn try_to_ed25519_bytes(&self) -> Option<[u8; 32]> {
153 if !matches!(self.algorithm(), AidAlgorithm::Ed25519) {
154 return None;
155 }
156 let mut out = [0u8; 32];
157 Base64UrlUnpadded::decode(self.identifier(), &mut out)
158 .expect("Aid is validated on construction; identifier MUST decode to 32 bytes");
159 Some(out)
160 }
161
162 pub fn to_ed25519_bytes(&self) -> [u8; 32] {
167 self.try_to_ed25519_bytes()
168 .expect("Aid::to_ed25519_bytes called on non-Ed25519 AID")
169 }
170
171 pub fn try_to_p256_bytes(&self) -> Option<[u8; 33]> {
174 if !matches!(self.algorithm(), AidAlgorithm::P256) {
175 return None;
176 }
177 let mut out = [0u8; 33];
178 Base64UrlUnpadded::decode(self.identifier(), &mut out)
179 .expect("Aid is validated on construction; identifier MUST decode to 33 bytes");
180 Some(out)
181 }
182
183 pub fn to_p256_bytes(&self) -> [u8; 33] {
187 self.try_to_p256_bytes()
188 .expect("Aid::to_p256_bytes called on non-P-256 AID")
189 }
190
191 pub fn pubkey_compressed_bytes(&self) -> Vec<u8> {
200 match self.algorithm() {
201 AidAlgorithm::Ed25519 => self.to_ed25519_bytes().to_vec(),
202 AidAlgorithm::P256 => self.to_p256_bytes().to_vec(),
203 }
204 }
205
206 pub fn as_str(&self) -> &str {
208 &self.0
209 }
210}
211
212fn validate_ed25519_identifier(identifier: &str) -> Result<(), AidParseError> {
213 if identifier.len() != AID_PUBKEY_IDENTIFIER_LEN {
214 return Err(AidParseError::WrongLength(identifier.len()));
215 }
216 if !identifier.bytes().all(is_base64url_byte) {
217 return Err(AidParseError::InvalidChars);
218 }
219 let mut buf = [0u8; 32];
220 Base64UrlUnpadded::decode(identifier, &mut buf).map_err(|_| AidParseError::InvalidChars)?;
221 Ok(())
222}
223
224fn validate_p256_identifier(identifier: &str) -> Result<(), AidParseError> {
225 if identifier.len() != AID_P256_IDENTIFIER_LEN {
226 return Err(AidParseError::WrongLength(identifier.len()));
227 }
228 if !identifier.bytes().all(is_base64url_byte) {
229 return Err(AidParseError::InvalidChars);
230 }
231 let mut buf = [0u8; 33];
232 Base64UrlUnpadded::decode(identifier, &mut buf).map_err(|_| AidParseError::InvalidChars)?;
233 if buf[0] != 0x02 && buf[0] != 0x03 {
236 return Err(AidParseError::InvalidChars);
237 }
238 Ok(())
239}
240
241impl fmt::Display for Aid {
242 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
243 f.write_str(&self.0)
244 }
245}
246
247impl TryFrom<String> for Aid {
248 type Error = AidParseError;
249 fn try_from(s: String) -> Result<Self, Self::Error> {
250 Aid::parse(&s)
251 }
252}
253
254impl From<Aid> for String {
255 fn from(a: Aid) -> String {
256 a.0
257 }
258}
259
260#[derive(Debug, thiserror::Error, PartialEq, Eq)]
265#[non_exhaustive]
266pub enum AidParseError {
267 #[error("AID does not start with 'aid:'")]
269 MissingScheme,
270
271 #[error("AID method '{0}' is not supported in v0.1; expected 'pubkey'")]
273 UnsupportedMethod(String),
274
275 #[error(
277 "AID identifier must be exactly {} characters; got {0}",
278 AID_PUBKEY_IDENTIFIER_LEN
279 )]
280 WrongLength(usize),
281
282 #[error("AID identifier contains non-base64url characters")]
284 InvalidChars,
285}
286
287fn is_base64url_byte(b: u8) -> bool {
288 matches!(b, b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'_')
289}
290
291#[cfg(test)]
292mod tests {
293 use super::*;
294
295 fn sample_pubkey() -> [u8; 32] {
296 let mut k = [0u8; 32];
297 for (i, b) in k.iter_mut().enumerate() {
298 *b = i as u8;
299 }
300 k
301 }
302
303 #[test]
304 fn rejects_missing_scheme() {
305 assert!(matches!(
306 Aid::parse("pubkey:abc"),
307 Err(AidParseError::MissingScheme)
308 ));
309 }
310
311 #[test]
312 fn rejects_unsupported_method() {
313 let s = format!("aid:did:{}", "A".repeat(AID_PUBKEY_IDENTIFIER_LEN));
314 assert!(matches!(
315 Aid::parse(&s),
316 Err(AidParseError::UnsupportedMethod(m)) if m == "did"
317 ));
318 }
319
320 #[test]
321 fn rejects_wrong_length() {
322 assert!(matches!(
323 Aid::parse(&format!("aid:pubkey:{}", "A".repeat(42))),
324 Err(AidParseError::WrongLength(42))
325 ));
326 assert!(matches!(
327 Aid::parse(&format!("aid:pubkey:{}", "A".repeat(44))),
328 Err(AidParseError::WrongLength(44))
329 ));
330 }
331
332 #[test]
333 fn rejects_padding() {
334 let mut s = "A".repeat(42);
336 s.push('=');
337 assert!(matches!(
338 Aid::parse(&format!("aid:pubkey:{}", s)),
339 Err(AidParseError::InvalidChars)
340 ));
341 }
342
343 #[test]
344 fn rejects_invalid_chars() {
345 let mut id = "A".repeat(42);
346 id.push('!');
347 assert!(matches!(
348 Aid::parse(&format!("aid:pubkey:{}", id)),
349 Err(AidParseError::InvalidChars)
350 ));
351 }
352
353 #[test]
354 fn round_trips_pubkey_bytes() {
355 let pk = sample_pubkey();
356 let aid = Aid::from_ed25519(&pk);
357 assert!(aid.as_str().starts_with("aid:pubkey:"));
358 assert_eq!(aid.identifier().len(), AID_PUBKEY_IDENTIFIER_LEN);
359 assert_eq!(aid.to_ed25519_bytes(), pk);
360 }
361
362 #[test]
363 fn parse_accepts_valid_aid() {
364 let aid = Aid::from_ed25519(&sample_pubkey());
365 let parsed = Aid::parse(aid.as_str()).unwrap();
366 assert_eq!(parsed, aid);
367 }
368
369 #[test]
370 fn serde_round_trip() {
371 let aid = Aid::from_ed25519(&sample_pubkey());
372 let json = serde_json::to_string(&aid).unwrap();
373 let back: Aid = serde_json::from_str(&json).unwrap();
374 assert_eq!(back, aid);
375 }
376
377 #[test]
378 fn parse_accepts_tagged_ed25519() {
379 let pk = sample_pubkey();
380 let tagged = Aid::from_ed25519_tagged(&pk);
381 assert!(tagged.as_str().starts_with("aid:pubkey:ed25519:"));
382 assert_eq!(tagged.algorithm(), AidAlgorithm::Ed25519);
383 let parsed = Aid::parse(tagged.as_str()).unwrap();
384 assert_eq!(parsed, tagged);
385 let legacy = Aid::from_ed25519(&pk);
389 assert_ne!(tagged.as_str(), legacy.as_str());
390 }
391
392 #[test]
393 fn parse_accepts_p256_kat() {
394 let aid_str = "aid:pubkey:p256:AweBDql0zqV3PmO4l_N-O-mgnnpf6blxpE0QZawqOpMR";
398 let aid = Aid::parse(aid_str).unwrap();
399 assert_eq!(aid.algorithm(), AidAlgorithm::P256);
400 let pubkey = aid.to_p256_bytes();
401 assert_eq!(pubkey[0], 0x03);
403 }
404
405 #[test]
406 fn p256_round_trip() {
407 let mut pubkey = [0u8; 33];
408 pubkey[0] = 0x02;
409 pubkey[1] = 0xAB;
410 let aid = Aid::from_p256(&pubkey);
411 let parsed = Aid::parse(aid.as_str()).unwrap();
412 assert_eq!(parsed.algorithm(), AidAlgorithm::P256);
413 assert_eq!(parsed.to_p256_bytes(), pubkey);
414 }
415
416 #[test]
417 fn p256_rejects_wrong_sec1_tag() {
418 let mut pubkey = [0u8; 33];
421 pubkey[0] = 0x04;
422 let identifier = Base64UrlUnpadded::encode_string(&pubkey);
423 let aid_str = format!("aid:pubkey:p256:{identifier}");
424 assert!(Aid::parse(&aid_str).is_err());
425 }
426
427 #[test]
428 fn rejects_unknown_algorithm_tag() {
429 let identifier = Base64UrlUnpadded::encode_string(&[0xFFu8; 33]);
431 let aid_str = format!("aid:pubkey:rsa4096:{identifier}");
432 assert!(Aid::parse(&aid_str).is_err());
433 }
434}