ubl_id/
did.rs

1//! DID (Decentralized Identifier) types and utilities
2//!
3//! Supports multiple DID methods:
4//! - `did:ubl:*` — UBL native identifiers
5//! - `did:key:*` — Self-certifying Ed25519 keys
6//! - `did:web:*` — Domain-linked identifiers
7//!
8//! # UBL DID Types
9//!
10//! ```text
11//! did:ubl:user:daniel       — Human user
12//! did:ubl:org:logline       — Organization
13//! did:ubl:agent:gpt4        — LLM/AI agent
14//! did:ubl:app:minicontratos — Application
15//! did:ubl:wallet:sess123    — Ephemeral wallet/session
16//! did:ubl:service:registry  — System service
17//! ```
18
19use base64::engine::general_purpose::URL_SAFE_NO_PAD as B64URL;
20use base64::Engine as _;
21use serde::{Deserialize, Serialize};
22use std::fmt;
23use std::str::FromStr;
24use thiserror::Error;
25
26/// DID parsing/validation errors
27#[derive(Debug, Error)]
28pub enum DidError {
29    #[error("invalid DID format: {0}")]
30    InvalidFormat(String),
31    #[error("unsupported DID method: {0}")]
32    UnsupportedMethod(String),
33    #[error("invalid UBL type: {0}")]
34    InvalidType(String),
35    #[error("invalid key encoding: {0}")]
36    InvalidKeyEncoding(String),
37}
38
39/// DID method (the second component after `did:`)
40#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
41#[serde(rename_all = "lowercase")]
42pub enum DidMethod {
43    /// UBL native: `did:ubl:type:id`
44    #[default]
45    Ubl,
46    /// Self-certifying key: `did:key:z...`
47    Key,
48    /// Domain-linked: `did:web:example.com`
49    Web,
50}
51
52impl fmt::Display for DidMethod {
53    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
54        match self {
55            DidMethod::Ubl => write!(f, "ubl"),
56            DidMethod::Key => write!(f, "key"),
57            DidMethod::Web => write!(f, "web"),
58        }
59    }
60}
61
62/// UBL entity type (for `did:ubl:type:id`)
63#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
64#[serde(rename_all = "lowercase")]
65pub enum DidType {
66    /// Human user
67    User,
68    /// Organization/company
69    Org,
70    /// LLM/AI agent
71    Agent,
72    /// Application
73    App,
74    /// Ephemeral wallet/session
75    Wallet,
76    /// System service
77    Service,
78}
79
80impl fmt::Display for DidType {
81    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
82        match self {
83            DidType::User => write!(f, "user"),
84            DidType::Org => write!(f, "org"),
85            DidType::Agent => write!(f, "agent"),
86            DidType::App => write!(f, "app"),
87            DidType::Wallet => write!(f, "wallet"),
88            DidType::Service => write!(f, "service"),
89        }
90    }
91}
92
93impl FromStr for DidType {
94    type Err = DidError;
95    fn from_str(s: &str) -> Result<Self, Self::Err> {
96        match s {
97            "user" => Ok(DidType::User),
98            "org" => Ok(DidType::Org),
99            "agent" => Ok(DidType::Agent),
100            "app" => Ok(DidType::App),
101            "wallet" => Ok(DidType::Wallet),
102            "service" => Ok(DidType::Service),
103            _ => Err(DidError::InvalidType(s.to_string())),
104        }
105    }
106}
107
108/// A Decentralized Identifier
109#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
110pub struct Did {
111    /// Full DID string
112    #[serde(rename = "id")]
113    raw: String,
114    /// Parsed method
115    #[serde(skip)]
116    method: DidMethod,
117}
118
119impl Did {
120    /// Parse a DID string
121    pub fn parse(s: &str) -> Result<Self, DidError> {
122        if !s.starts_with("did:") {
123            return Err(DidError::InvalidFormat("must start with 'did:'".into()));
124        }
125        let parts: Vec<&str> = s.splitn(3, ':').collect();
126        if parts.len() < 3 {
127            return Err(DidError::InvalidFormat("missing method or id".into()));
128        }
129        let method = match parts[1] {
130            "ubl" => DidMethod::Ubl,
131            "key" => DidMethod::Key,
132            "web" => DidMethod::Web,
133            m => return Err(DidError::UnsupportedMethod(m.to_string())),
134        };
135        Ok(Self {
136            raw: s.to_string(),
137            method,
138        })
139    }
140
141    /// Create a `did:ubl:type:id`
142    pub fn ubl(typ: DidType, id: &str) -> Self {
143        Self {
144            raw: format!("did:ubl:{}:{}", typ, id),
145            method: DidMethod::Ubl,
146        }
147    }
148
149    /// Create a `did:key:z...` from Ed25519 public key (UBL-compatible format)
150    pub fn key_from_ed25519(pk32: &[u8; 32]) -> Self {
151        let raw = format!("did:key:z{}", B64URL.encode(pk32));
152        Self {
153            raw,
154            method: DidMethod::Key,
155        }
156    }
157
158    /// Create a `did:key:z...` from Ed25519 public key (W3C multicodec format)
159    pub fn key_from_ed25519_w3c(pk32: &[u8; 32]) -> Self {
160        let mut v = vec![0xed, 0x01]; // Ed25519 multicodec prefix
161        v.extend_from_slice(pk32);
162        let raw = format!("did:key:z{}", bs58::encode(v).into_string());
163        Self {
164            raw,
165            method: DidMethod::Key,
166        }
167    }
168
169    /// Create a `did:web:domain`
170    pub fn web(domain: &str) -> Self {
171        Self {
172            raw: format!("did:web:{}", domain),
173            method: DidMethod::Web,
174        }
175    }
176
177    /// Get the full DID string
178    pub fn as_str(&self) -> &str {
179        &self.raw
180    }
181
182    /// Get the DID method
183    pub fn method(&self) -> &DidMethod {
184        &self.method
185    }
186
187    /// For `did:ubl:type:id`, get the type
188    pub fn ubl_type(&self) -> Option<DidType> {
189        if self.method != DidMethod::Ubl {
190            return None;
191        }
192        let parts: Vec<&str> = self.raw.splitn(4, ':').collect();
193        if parts.len() >= 3 {
194            parts[2].parse().ok()
195        } else {
196            None
197        }
198    }
199
200    /// For `did:ubl:type:id`, get the id
201    pub fn ubl_id(&self) -> Option<&str> {
202        if self.method != DidMethod::Ubl {
203            return None;
204        }
205        let parts: Vec<&str> = self.raw.splitn(4, ':').collect();
206        if parts.len() >= 4 {
207            Some(parts[3])
208        } else {
209            None
210        }
211    }
212
213    /// For `did:key:z...`, extract the Ed25519 public key bytes (UBL format)
214    pub fn key_bytes(&self) -> Option<[u8; 32]> {
215        if self.method != DidMethod::Key {
216            return None;
217        }
218        let key_part = self.raw.strip_prefix("did:key:z")?;
219        let bytes = B64URL.decode(key_part.as_bytes()).ok()?;
220        bytes.try_into().ok()
221    }
222
223    /// Can this DID sign things? (has associated private key capability)
224    pub fn can_sign(&self) -> bool {
225        matches!(self.method, DidMethod::Key | DidMethod::Ubl)
226    }
227}
228
229impl fmt::Display for Did {
230    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
231        write!(f, "{}", self.raw)
232    }
233}
234
235impl FromStr for Did {
236    type Err = DidError;
237    fn from_str(s: &str) -> Result<Self, Self::Err> {
238        Did::parse(s)
239    }
240}
241
242impl AsRef<str> for Did {
243    fn as_ref(&self) -> &str {
244        &self.raw
245    }
246}
247
248#[cfg(test)]
249mod tests {
250    use super::*;
251
252    #[test]
253    fn test_did_ubl() {
254        let did = Did::ubl(DidType::User, "daniel");
255        assert_eq!(did.as_str(), "did:ubl:user:daniel");
256        assert_eq!(did.method(), &DidMethod::Ubl);
257        assert_eq!(did.ubl_type(), Some(DidType::User));
258        assert_eq!(did.ubl_id(), Some("daniel"));
259    }
260
261    #[test]
262    fn test_did_key() {
263        let pk = [1u8; 32];
264        let did = Did::key_from_ed25519(&pk);
265        assert!(did.as_str().starts_with("did:key:z"));
266        assert_eq!(did.method(), &DidMethod::Key);
267        assert_eq!(did.key_bytes(), Some(pk));
268    }
269
270    #[test]
271    fn test_did_parse() {
272        let did = Did::parse("did:ubl:agent:gpt4").unwrap();
273        assert_eq!(did.ubl_type(), Some(DidType::Agent));
274        assert_eq!(did.ubl_id(), Some("gpt4"));
275    }
276}