use crate::AID_PUBKEY_IDENTIFIER_LEN;
use base64ct::{Base64UrlUnpadded, Encoding};
use serde::{Deserialize, Serialize};
use std::fmt;
const AID_PUBKEY_PREFIX: &str = "aid:pubkey:";
const AID_PUBKEY_ED25519_PREFIX: &str = "aid:pubkey:ed25519:";
const AID_PUBKEY_P256_PREFIX: &str = "aid:pubkey:p256:";
const AID_P256_IDENTIFIER_LEN: usize = 44;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum AidAlgorithm {
Ed25519,
P256,
}
impl AidAlgorithm {
pub fn as_str(&self) -> &'static str {
match self {
AidAlgorithm::Ed25519 => "ed25519",
AidAlgorithm::P256 => "p256",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(try_from = "String", into = "String")]
pub struct Aid(String);
impl Aid {
pub fn parse(s: &str) -> Result<Self, AidParseError> {
if let Some(identifier) = s.strip_prefix(AID_PUBKEY_ED25519_PREFIX) {
validate_ed25519_identifier(identifier)?;
return Ok(Self(s.to_string()));
}
if let Some(identifier) = s.strip_prefix(AID_PUBKEY_P256_PREFIX) {
validate_p256_identifier(identifier)?;
return Ok(Self(s.to_string()));
}
if let Some(identifier) = s.strip_prefix(AID_PUBKEY_PREFIX) {
if identifier.contains(':') {
let (method, _) = identifier.split_once(':').unwrap();
return Err(AidParseError::UnsupportedMethod(format!("pubkey:{method}")));
}
validate_ed25519_identifier(identifier)?;
return Ok(Self(s.to_string()));
}
if let Some(rest) = s.strip_prefix("aid:") {
let method = rest.split(':').next().unwrap_or("");
return Err(AidParseError::UnsupportedMethod(method.to_string()));
}
Err(AidParseError::MissingScheme)
}
pub fn from_ed25519(pubkey: &[u8; 32]) -> Self {
let identifier = Base64UrlUnpadded::encode_string(pubkey);
debug_assert_eq!(identifier.len(), AID_PUBKEY_IDENTIFIER_LEN);
Self(format!("{AID_PUBKEY_PREFIX}{identifier}"))
}
pub fn from_ed25519_tagged(pubkey: &[u8; 32]) -> Self {
let identifier = Base64UrlUnpadded::encode_string(pubkey);
debug_assert_eq!(identifier.len(), AID_PUBKEY_IDENTIFIER_LEN);
Self(format!("{AID_PUBKEY_ED25519_PREFIX}{identifier}"))
}
pub fn from_p256(compressed_point: &[u8; 33]) -> Self {
let identifier = Base64UrlUnpadded::encode_string(compressed_point);
debug_assert_eq!(identifier.len(), AID_P256_IDENTIFIER_LEN);
Self(format!("{AID_PUBKEY_P256_PREFIX}{identifier}"))
}
pub fn algorithm(&self) -> AidAlgorithm {
if self.0.starts_with(AID_PUBKEY_P256_PREFIX) {
AidAlgorithm::P256
} else {
AidAlgorithm::Ed25519
}
}
pub fn identifier(&self) -> &str {
if let Some(id) = self.0.strip_prefix(AID_PUBKEY_ED25519_PREFIX) {
id
} else if let Some(id) = self.0.strip_prefix(AID_PUBKEY_P256_PREFIX) {
id
} else {
&self.0[AID_PUBKEY_PREFIX.len()..]
}
}
pub fn try_to_ed25519_bytes(&self) -> Option<[u8; 32]> {
if !matches!(self.algorithm(), AidAlgorithm::Ed25519) {
return None;
}
let mut out = [0u8; 32];
Base64UrlUnpadded::decode(self.identifier(), &mut out)
.expect("Aid is validated on construction; identifier MUST decode to 32 bytes");
Some(out)
}
pub fn try_to_p256_bytes(&self) -> Option<[u8; 33]> {
if !matches!(self.algorithm(), AidAlgorithm::P256) {
return None;
}
let mut out = [0u8; 33];
Base64UrlUnpadded::decode(self.identifier(), &mut out)
.expect("Aid is validated on construction; identifier MUST decode to 33 bytes");
Some(out)
}
pub fn pubkey_compressed_bytes(&self) -> Vec<u8> {
match self.algorithm() {
AidAlgorithm::Ed25519 => self
.try_to_ed25519_bytes()
.expect("Ed25519 arm guarded by algorithm()")
.to_vec(),
AidAlgorithm::P256 => self
.try_to_p256_bytes()
.expect("P-256 arm guarded by algorithm()")
.to_vec(),
}
}
pub fn as_str(&self) -> &str {
&self.0
}
}
fn validate_ed25519_identifier(identifier: &str) -> Result<(), AidParseError> {
if identifier.len() != AID_PUBKEY_IDENTIFIER_LEN {
return Err(AidParseError::WrongLength(identifier.len()));
}
if !identifier.bytes().all(is_base64url_byte) {
return Err(AidParseError::InvalidChars);
}
let mut buf = [0u8; 32];
Base64UrlUnpadded::decode(identifier, &mut buf).map_err(|_| AidParseError::InvalidChars)?;
Ok(())
}
fn validate_p256_identifier(identifier: &str) -> Result<(), AidParseError> {
if identifier.len() != AID_P256_IDENTIFIER_LEN {
return Err(AidParseError::WrongLength(identifier.len()));
}
if !identifier.bytes().all(is_base64url_byte) {
return Err(AidParseError::InvalidChars);
}
let mut buf = [0u8; 33];
Base64UrlUnpadded::decode(identifier, &mut buf).map_err(|_| AidParseError::InvalidChars)?;
if buf[0] != 0x02 && buf[0] != 0x03 {
return Err(AidParseError::InvalidChars);
}
Ok(())
}
impl fmt::Display for Aid {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.0)
}
}
impl TryFrom<String> for Aid {
type Error = AidParseError;
fn try_from(s: String) -> Result<Self, Self::Error> {
Aid::parse(&s)
}
}
impl From<Aid> for String {
fn from(a: Aid) -> String {
a.0
}
}
#[derive(Debug, thiserror::Error, PartialEq, Eq)]
#[non_exhaustive]
pub enum AidParseError {
#[error("AID does not start with 'aid:'")]
MissingScheme,
#[error("AID method '{0}' is not supported in v0.1; expected 'pubkey'")]
UnsupportedMethod(String),
#[error(
"AID identifier must be exactly {} characters; got {0}",
AID_PUBKEY_IDENTIFIER_LEN
)]
WrongLength(usize),
#[error("AID identifier contains non-base64url characters")]
InvalidChars,
}
fn is_base64url_byte(b: u8) -> bool {
matches!(b, b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'_')
}
#[cfg(test)]
mod tests {
use super::*;
fn sample_pubkey() -> [u8; 32] {
let mut k = [0u8; 32];
for (i, b) in k.iter_mut().enumerate() {
*b = i as u8;
}
k
}
#[test]
fn rejects_missing_scheme() {
assert!(matches!(
Aid::parse("pubkey:abc"),
Err(AidParseError::MissingScheme)
));
}
#[test]
fn rejects_unsupported_method() {
let s = format!("aid:did:{}", "A".repeat(AID_PUBKEY_IDENTIFIER_LEN));
assert!(matches!(
Aid::parse(&s),
Err(AidParseError::UnsupportedMethod(m)) if m == "did"
));
}
#[test]
fn rejects_wrong_length() {
assert!(matches!(
Aid::parse(&format!("aid:pubkey:{}", "A".repeat(42))),
Err(AidParseError::WrongLength(42))
));
assert!(matches!(
Aid::parse(&format!("aid:pubkey:{}", "A".repeat(44))),
Err(AidParseError::WrongLength(44))
));
}
#[test]
fn rejects_padding() {
let mut s = "A".repeat(42);
s.push('=');
assert!(matches!(
Aid::parse(&format!("aid:pubkey:{}", s)),
Err(AidParseError::InvalidChars)
));
}
#[test]
fn rejects_invalid_chars() {
let mut id = "A".repeat(42);
id.push('!');
assert!(matches!(
Aid::parse(&format!("aid:pubkey:{}", id)),
Err(AidParseError::InvalidChars)
));
}
#[test]
fn round_trips_pubkey_bytes() {
let pk = sample_pubkey();
let aid = Aid::from_ed25519(&pk);
assert!(aid.as_str().starts_with("aid:pubkey:"));
assert_eq!(aid.identifier().len(), AID_PUBKEY_IDENTIFIER_LEN);
assert_eq!(aid.try_to_ed25519_bytes().unwrap(), pk);
}
#[test]
fn parse_accepts_valid_aid() {
let aid = Aid::from_ed25519(&sample_pubkey());
let parsed = Aid::parse(aid.as_str()).unwrap();
assert_eq!(parsed, aid);
}
#[test]
fn serde_round_trip() {
let aid = Aid::from_ed25519(&sample_pubkey());
let json = serde_json::to_string(&aid).unwrap();
let back: Aid = serde_json::from_str(&json).unwrap();
assert_eq!(back, aid);
}
#[test]
fn parse_accepts_tagged_ed25519() {
let pk = sample_pubkey();
let tagged = Aid::from_ed25519_tagged(&pk);
assert!(tagged.as_str().starts_with("aid:pubkey:ed25519:"));
assert_eq!(tagged.algorithm(), AidAlgorithm::Ed25519);
let parsed = Aid::parse(tagged.as_str()).unwrap();
assert_eq!(parsed, tagged);
let legacy = Aid::from_ed25519(&pk);
assert_ne!(tagged.as_str(), legacy.as_str());
}
#[test]
fn parse_accepts_p256_kat() {
let aid_str = "aid:pubkey:p256:AweBDql0zqV3PmO4l_N-O-mgnnpf6blxpE0QZawqOpMR";
let aid = Aid::parse(aid_str).unwrap();
assert_eq!(aid.algorithm(), AidAlgorithm::P256);
let pubkey = aid.try_to_p256_bytes().unwrap();
assert_eq!(pubkey[0], 0x03);
}
#[test]
fn p256_round_trip() {
let mut pubkey = [0u8; 33];
pubkey[0] = 0x02;
pubkey[1] = 0xAB;
let aid = Aid::from_p256(&pubkey);
let parsed = Aid::parse(aid.as_str()).unwrap();
assert_eq!(parsed.algorithm(), AidAlgorithm::P256);
assert_eq!(parsed.try_to_p256_bytes().unwrap(), pubkey);
}
#[test]
fn p256_rejects_wrong_sec1_tag() {
let mut pubkey = [0u8; 33];
pubkey[0] = 0x04;
let identifier = Base64UrlUnpadded::encode_string(&pubkey);
let aid_str = format!("aid:pubkey:p256:{identifier}");
assert!(Aid::parse(&aid_str).is_err());
}
#[test]
fn rejects_unknown_algorithm_tag() {
let identifier = Base64UrlUnpadded::encode_string(&[0xFFu8; 33]);
let aid_str = format!("aid:pubkey:rsa4096:{identifier}");
assert!(Aid::parse(&aid_str).is_err());
}
}