Skip to main content

anp_identity/
input.rs

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}