1use std::collections::BTreeSet;
2use std::net::IpAddr;
3use std::str::FromStr;
4
5use anp::PublicKeyMaterial;
6use serde::{Deserialize, Serialize};
7use serde_json::json;
8
9use crate::{DidError, DidResult};
10
11const MAX_DOMAIN_LEN: usize = 253;
12const MAX_PATH_SEGMENT_LEN: usize = 128;
13const MAX_KID_FRAGMENT_LEN: usize = 128;
14const MAX_SERVICE_VALUE_LEN: usize = 2048;
15
16#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
17#[serde(rename_all = "snake_case")]
18pub enum DidProfile {
19 E1,
20}
21
22#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
23#[serde(rename_all = "snake_case")]
24pub enum KeyRole {
25 RootControl,
26 DeviceSigning,
27 RequestSigning,
28 E2eeSigning,
29 E2eeAgreement,
30}
31
32#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
33#[serde(deny_unknown_fields)]
34pub struct Capabilities {
35 #[serde(default)]
36 pub did_wba: bool,
37}
38
39#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
40#[serde(deny_unknown_fields)]
41pub struct ManagedKeySpec {
42 pub fragment: String,
43 pub role: KeyRole,
44}
45
46#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
47#[serde(deny_unknown_fields)]
48pub struct PublicOkpJwk {
49 pub kty: String,
50 pub crv: String,
51 pub x: String,
52}
53
54#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
55#[serde(tag = "format", rename_all = "snake_case")]
56pub enum ExternalPublicKeyMaterial {
57 Multibase { value: String },
58 Jwk { public_key_jwk: PublicOkpJwk },
59}
60
61#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
62#[serde(deny_unknown_fields)]
63pub struct ExternalPublicKeySpec {
64 pub kid: String,
65 pub role: KeyRole,
66 pub material: ExternalPublicKeyMaterial,
67}
68
69#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
70#[serde(deny_unknown_fields)]
71pub struct ServiceSpec {
72 pub id: String,
73 pub service_type: String,
74 pub service_endpoint: String,
75 pub service_did: Option<String>,
76 #[serde(default)]
77 pub profiles: Vec<String>,
78 #[serde(default)]
79 pub security_profiles: Vec<String>,
80}
81
82#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
83#[serde(deny_unknown_fields)]
84pub struct DeviceManifestEntrySpec {
85 pub device_id: String,
86 pub signing_key_id: String,
87 pub e2ee_key_id: String,
88 #[serde(default)]
89 pub profiles: Vec<String>,
90}
91
92#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
93#[serde(deny_unknown_fields)]
94pub struct DeviceManifestSpec {
95 pub devices: Vec<DeviceManifestEntrySpec>,
96}
97
98#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
99#[serde(tag = "type", content = "value", rename_all = "snake_case")]
100pub enum DidExtensionSpec {
101 DeviceManifest(DeviceManifestSpec),
102}
103
104#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
105#[serde(deny_unknown_fields)]
106pub struct DidCreateSpec {
107 pub profile: DidProfile,
108 pub domain: String,
109 pub port: Option<u16>,
110 pub path_segments: Vec<String>,
111 #[serde(default)]
112 pub capabilities: Capabilities,
113 pub managed_keys: Vec<ManagedKeySpec>,
114 #[serde(default)]
115 pub external_keys: Vec<ExternalPublicKeySpec>,
116 #[serde(default)]
117 pub services: Vec<ServiceSpec>,
118 pub agent_description_url: Option<String>,
119 #[serde(default)]
120 pub extensions: Vec<DidExtensionSpec>,
121}
122
123impl DidCreateSpec {
124 pub fn validate(&self) -> DidResult<()> {
125 validate_domain(&self.domain)?;
126 if self.path_segments.is_empty() {
127 return Err(DidError::EmptyPath);
128 }
129 for segment in &self.path_segments {
130 validate_path_segment(segment)?;
131 }
132 let root_count = self
133 .managed_keys
134 .iter()
135 .filter(|key| key.role == KeyRole::RootControl)
136 .count();
137 if root_count != 1 {
138 return Err(DidError::InvalidManagedRootCount);
139 }
140 if self
141 .external_keys
142 .iter()
143 .any(|key| key.role == KeyRole::RootControl)
144 {
145 return Err(DidError::ExternalRootControl);
146 }
147 if self.capabilities.did_wba
148 && !self
149 .managed_keys
150 .iter()
151 .any(|key| matches!(key.role, KeyRole::DeviceSigning | KeyRole::RequestSigning))
152 {
153 return Err(DidError::MissingManagedRequestSigning);
154 }
155 let mut managed_fragments = BTreeSet::new();
156 for key in &self.managed_keys {
157 validate_fragment(&key.fragment)?;
158 if !managed_fragments.insert(key.fragment.as_str()) {
159 return Err(DidError::DuplicateKid);
160 }
161 }
162 for key in &self.external_keys {
163 key.parse_public_key()?;
164 }
165 for service in &self.services {
166 service.validate()?;
167 }
168 if self
169 .agent_description_url
170 .as_ref()
171 .is_some_and(|value| value.trim().is_empty() || value.len() > MAX_SERVICE_VALUE_LEN)
172 {
173 return Err(DidError::InvalidService);
174 }
175 for extension in &self.extensions {
176 extension.validate()?;
177 }
178 Ok(())
179 }
180
181 pub fn validate_for_did(&self, did: &str) -> DidResult<()> {
182 self.validate()?;
183 let managed = self.managed_keys.iter().map(|key| key.fragment.as_str());
184 let external = self.external_keys.iter().map(|key| key.kid.as_str());
185 validate_unique_kids(did, managed.chain(external))
186 }
187}
188
189impl ExternalPublicKeySpec {
190 pub(crate) fn parse_public_key(&self) -> DidResult<PublicKeyMaterial> {
191 if self.role == KeyRole::RootControl {
192 return Err(DidError::ExternalRootControl);
193 }
194 let method_type = match self.role {
195 KeyRole::DeviceSigning | KeyRole::RequestSigning | KeyRole::E2eeSigning => "Multikey",
196 KeyRole::E2eeAgreement => "X25519KeyAgreementKey2019",
197 KeyRole::RootControl => return Err(DidError::ExternalRootControl),
198 };
199 let method = match &self.material {
200 ExternalPublicKeyMaterial::Multibase { value } => json!({
201 "id": "#external",
202 "type": method_type,
203 "publicKeyMultibase": value,
204 }),
205 ExternalPublicKeyMaterial::Jwk { public_key_jwk } => json!({
206 "id": "#external",
207 "type": "JsonWebKey2020",
208 "publicKeyJwk": public_key_jwk,
209 }),
210 };
211 let key = anp::authentication::extract_public_key(&method)
212 .map_err(|_| DidError::InvalidPublicKey)?;
213 match (&self.role, &key) {
214 (
215 KeyRole::DeviceSigning | KeyRole::RequestSigning | KeyRole::E2eeSigning,
216 PublicKeyMaterial::Ed25519(_),
217 )
218 | (KeyRole::E2eeAgreement, PublicKeyMaterial::X25519(_)) => Ok(key),
219 _ => Err(DidError::InvalidPublicKey),
220 }
221 }
222}
223
224impl ServiceSpec {
225 pub(crate) fn validate(&self) -> DidResult<()> {
226 let fragment = self.id.strip_prefix('#').unwrap_or(&self.id);
227 if fragment.starts_with('#') {
228 return Err(DidError::InvalidService);
229 }
230 validate_fragment(fragment).map_err(|_| DidError::InvalidService)?;
231 if self.service_type.trim().is_empty()
232 || self.service_endpoint.trim().is_empty()
233 || self.service_type.len() > MAX_SERVICE_VALUE_LEN
234 || self.service_endpoint.len() > MAX_SERVICE_VALUE_LEN
235 {
236 return Err(DidError::InvalidService);
237 }
238 if self.service_did.as_ref().is_some_and(|value| {
239 value.trim().is_empty() || value.trim() != value || value.len() > MAX_SERVICE_VALUE_LEN
240 }) {
241 return Err(DidError::InvalidService);
242 }
243 Ok(())
244 }
245}
246
247impl DidExtensionSpec {
248 fn validate(&self) -> DidResult<()> {
249 match self {
250 Self::DeviceManifest(manifest) if manifest.devices.is_empty() => {
251 Err(DidError::InvalidExtension)
252 }
253 Self::DeviceManifest(manifest) => {
254 for device in &manifest.devices {
255 if device.device_id.trim().is_empty()
256 || device.device_id.len() > MAX_KID_FRAGMENT_LEN
257 || device.signing_key_id.trim().is_empty()
258 || device.signing_key_id.len() > MAX_SERVICE_VALUE_LEN
259 || device.e2ee_key_id.trim().is_empty()
260 || device.e2ee_key_id.len() > MAX_SERVICE_VALUE_LEN
261 || device.profiles.is_empty()
262 || device
263 .profiles
264 .iter()
265 .any(|profile| profile.trim().is_empty())
266 {
267 return Err(DidError::InvalidExtension);
268 }
269 }
270 Ok(())
271 }
272 }
273 }
274}
275
276fn validate_domain(domain: &str) -> DidResult<()> {
277 if domain.is_empty()
278 || domain.len() > MAX_DOMAIN_LEN
279 || domain.trim() != domain
280 || IpAddr::from_str(domain).is_ok()
281 || !domain.split('.').all(|label| {
282 !label.is_empty()
283 && label.len() <= 63
284 && label
285 .bytes()
286 .all(|byte| byte.is_ascii_alphanumeric() || byte == b'-')
287 && label
288 .as_bytes()
289 .first()
290 .is_some_and(u8::is_ascii_alphanumeric)
291 && label
292 .as_bytes()
293 .last()
294 .is_some_and(u8::is_ascii_alphanumeric)
295 })
296 {
297 return Err(DidError::InvalidDomain);
298 }
299 Ok(())
300}
301
302fn validate_path_segment(segment: &str) -> DidResult<()> {
303 if segment.is_empty()
304 || segment.len() > MAX_PATH_SEGMENT_LEN
305 || !segment.bytes().all(is_identifier_byte)
306 {
307 return Err(DidError::InvalidPathSegment);
308 }
309 Ok(())
310}
311
312pub(crate) fn validate_fragment(fragment: &str) -> DidResult<()> {
313 if fragment.is_empty()
314 || fragment.len() > MAX_KID_FRAGMENT_LEN
315 || !fragment.bytes().all(is_identifier_byte)
316 {
317 return Err(DidError::InvalidKidFragment);
318 }
319 Ok(())
320}
321
322fn is_identifier_byte(byte: u8) -> bool {
323 byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_' | b'.' | b'~')
324}
325
326pub(crate) fn canonicalize_kid(did: &str, kid: &str) -> DidResult<String> {
327 let prefix = format!("{did}#");
328 let fragment = if let Some(fragment) = kid.strip_prefix('#') {
329 fragment
330 } else if let Some(fragment) = kid.strip_prefix(&prefix) {
331 fragment
332 } else if !kid.contains(':') && !kid.contains('#') {
333 kid
334 } else {
335 return Err(DidError::ForeignKid);
336 };
337 validate_fragment(fragment)?;
338 Ok(format!("{prefix}{fragment}"))
339}
340
341fn validate_unique_kids<'a>(did: &str, kids: impl IntoIterator<Item = &'a str>) -> DidResult<()> {
342 let mut canonical = BTreeSet::new();
343 for kid in kids {
344 if !canonical.insert(canonicalize_kid(did, kid)?) {
345 return Err(DidError::DuplicateKid);
346 }
347 }
348 Ok(())
349}
350
351#[cfg(test)]
352mod tests {
353 use super::*;
354
355 fn valid_spec() -> DidCreateSpec {
356 DidCreateSpec {
357 profile: DidProfile::E1,
358 domain: "example.com".to_string(),
359 port: None,
360 path_segments: vec!["agents".to_string(), "alice".to_string()],
361 capabilities: Capabilities { did_wba: true },
362 managed_keys: vec![
363 ManagedKeySpec {
364 fragment: "root".to_string(),
365 role: KeyRole::RootControl,
366 },
367 ManagedKeySpec {
368 fragment: "request".to_string(),
369 role: KeyRole::RequestSigning,
370 },
371 ],
372 external_keys: Vec::new(),
373 services: Vec::new(),
374 agent_description_url: None,
375 extensions: Vec::new(),
376 }
377 }
378
379 #[test]
380 fn valid_e1_shape_passes() {
381 assert_eq!(valid_spec().validate(), Ok(()));
382 }
383
384 #[test]
385 fn canonical_kid_validation_rejects_equivalent_and_foreign_ids() {
386 let mut spec = valid_spec();
387 let did = "did:wba:example.com:agents:alice:e1_test";
388 spec.external_keys.push(ExternalPublicKeySpec {
389 kid: format!("{did}#request"),
390 role: KeyRole::RequestSigning,
391 material: ExternalPublicKeyMaterial::Multibase {
392 value: "z6MkiTBz1yZ9fCkmQCQqfYbANPAG7dJbqvBKTqYBq4pXx7nG".to_string(),
393 },
394 });
395 assert_eq!(spec.validate_for_did(did), Err(DidError::DuplicateKid));
396 spec.external_keys[0].kid = "did:wba:evil.example#request".to_string();
397 assert_eq!(spec.validate_for_did(did), Err(DidError::ForeignKid));
398 }
399
400 #[test]
401 fn device_signing_satisfies_did_wba_managed_auth_requirement() {
402 let mut spec = valid_spec();
403 spec.managed_keys
404 .retain(|key| key.role != KeyRole::RequestSigning);
405 spec.managed_keys.push(ManagedKeySpec {
406 fragment: "device".to_string(),
407 role: KeyRole::DeviceSigning,
408 });
409
410 spec.validate().unwrap();
411 }
412}