use base64::engine::general_purpose::URL_SAFE_NO_PAD as B64URL;
use base64::Engine as _;
use serde::{Deserialize, Serialize};
use std::fmt;
use std::str::FromStr;
use thiserror::Error;
#[derive(Debug, Error)]
pub enum DidError {
#[error("invalid DID format: {0}")]
InvalidFormat(String),
#[error("unsupported DID method: {0}")]
UnsupportedMethod(String),
#[error("invalid UBL type: {0}")]
InvalidType(String),
#[error("invalid key encoding: {0}")]
InvalidKeyEncoding(String),
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
#[serde(rename_all = "lowercase")]
pub enum DidMethod {
#[default]
Ubl,
Key,
Web,
}
impl fmt::Display for DidMethod {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
DidMethod::Ubl => write!(f, "ubl"),
DidMethod::Key => write!(f, "key"),
DidMethod::Web => write!(f, "web"),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum DidType {
User,
Org,
Agent,
App,
Wallet,
Service,
}
impl fmt::Display for DidType {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
DidType::User => write!(f, "user"),
DidType::Org => write!(f, "org"),
DidType::Agent => write!(f, "agent"),
DidType::App => write!(f, "app"),
DidType::Wallet => write!(f, "wallet"),
DidType::Service => write!(f, "service"),
}
}
}
impl FromStr for DidType {
type Err = DidError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"user" => Ok(DidType::User),
"org" => Ok(DidType::Org),
"agent" => Ok(DidType::Agent),
"app" => Ok(DidType::App),
"wallet" => Ok(DidType::Wallet),
"service" => Ok(DidType::Service),
_ => Err(DidError::InvalidType(s.to_string())),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct Did {
#[serde(rename = "id")]
raw: String,
#[serde(skip)]
method: DidMethod,
}
impl Did {
pub fn parse(s: &str) -> Result<Self, DidError> {
if !s.starts_with("did:") {
return Err(DidError::InvalidFormat("must start with 'did:'".into()));
}
let parts: Vec<&str> = s.splitn(3, ':').collect();
if parts.len() < 3 {
return Err(DidError::InvalidFormat("missing method or id".into()));
}
let method = match parts[1] {
"ubl" => DidMethod::Ubl,
"key" => DidMethod::Key,
"web" => DidMethod::Web,
m => return Err(DidError::UnsupportedMethod(m.to_string())),
};
Ok(Self {
raw: s.to_string(),
method,
})
}
pub fn ubl(typ: DidType, id: &str) -> Self {
Self {
raw: format!("did:ubl:{}:{}", typ, id),
method: DidMethod::Ubl,
}
}
pub fn key_from_ed25519(pk32: &[u8; 32]) -> Self {
let raw = format!("did:key:z{}", B64URL.encode(pk32));
Self {
raw,
method: DidMethod::Key,
}
}
pub fn key_from_ed25519_w3c(pk32: &[u8; 32]) -> Self {
let mut v = vec![0xed, 0x01]; v.extend_from_slice(pk32);
let raw = format!("did:key:z{}", bs58::encode(v).into_string());
Self {
raw,
method: DidMethod::Key,
}
}
pub fn web(domain: &str) -> Self {
Self {
raw: format!("did:web:{}", domain),
method: DidMethod::Web,
}
}
pub fn as_str(&self) -> &str {
&self.raw
}
pub fn method(&self) -> &DidMethod {
&self.method
}
pub fn ubl_type(&self) -> Option<DidType> {
if self.method != DidMethod::Ubl {
return None;
}
let parts: Vec<&str> = self.raw.splitn(4, ':').collect();
if parts.len() >= 3 {
parts[2].parse().ok()
} else {
None
}
}
pub fn ubl_id(&self) -> Option<&str> {
if self.method != DidMethod::Ubl {
return None;
}
let parts: Vec<&str> = self.raw.splitn(4, ':').collect();
if parts.len() >= 4 {
Some(parts[3])
} else {
None
}
}
pub fn key_bytes(&self) -> Option<[u8; 32]> {
if self.method != DidMethod::Key {
return None;
}
let key_part = self.raw.strip_prefix("did:key:z")?;
let bytes = B64URL.decode(key_part.as_bytes()).ok()?;
bytes.try_into().ok()
}
pub fn can_sign(&self) -> bool {
matches!(self.method, DidMethod::Key | DidMethod::Ubl)
}
}
impl fmt::Display for Did {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.raw)
}
}
impl FromStr for Did {
type Err = DidError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Did::parse(s)
}
}
impl AsRef<str> for Did {
fn as_ref(&self) -> &str {
&self.raw
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_did_ubl() {
let did = Did::ubl(DidType::User, "daniel");
assert_eq!(did.as_str(), "did:ubl:user:daniel");
assert_eq!(did.method(), &DidMethod::Ubl);
assert_eq!(did.ubl_type(), Some(DidType::User));
assert_eq!(did.ubl_id(), Some("daniel"));
}
#[test]
fn test_did_key() {
let pk = [1u8; 32];
let did = Did::key_from_ed25519(&pk);
assert!(did.as_str().starts_with("did:key:z"));
assert_eq!(did.method(), &DidMethod::Key);
assert_eq!(did.key_bytes(), Some(pk));
}
#[test]
fn test_did_parse() {
let did = Did::parse("did:ubl:agent:gpt4").unwrap();
assert_eq!(did.ubl_type(), Some(DidType::Agent));
assert_eq!(did.ubl_id(), Some("gpt4"));
}
}