Skip to main content

xdid_core/
document.rs

1use jose_jwk::Jwk;
2use serde::{
3    Deserialize,
4    Serialize,
5};
6use serde_json::Value;
7use serde_with::{
8    serde_as,
9    skip_serializing_none,
10};
11use smol_str::SmolStr;
12
13use crate::{
14    did::Did,
15    did_url::{
16        relative::RelativeDidUrl,
17        url::DidUrl,
18    },
19};
20
21#[skip_serializing_none]
22#[derive(Serialize, Deserialize, Debug, PartialEq, Eq)]
23#[serde(rename_all = "camelCase")]
24#[serde_as]
25pub struct Document {
26    /// Held as opaque JSON so that re-serializing a resolved document does not
27    /// drop it; entries may be strings or objects and are never interpreted.
28    #[serde(rename = "@context")]
29    #[serde_as(as = "Option<OneOrMany<_>>")]
30    pub context:               Option<Vec<Value>>,
31    pub id:                    Did,
32    pub also_known_as:         Option<Vec<String>>,
33    #[serde_as(as = "Option<OneOrMany<_>>")]
34    pub controller:            Option<Vec<Did>>,
35    pub verification_method:   Option<Vec<VerificationMethodMap>>,
36    pub authentication:        Option<Vec<VerificationMethod>>,
37    pub assertion_method:      Option<Vec<VerificationMethod>>,
38    pub key_agreement:         Option<Vec<VerificationMethod>>,
39    pub capability_invocation: Option<Vec<VerificationMethod>>,
40    pub capability_delegation: Option<Vec<VerificationMethod>>,
41    pub service:               Option<Vec<ServiceEndpoint>>,
42}
43
44impl Document {
45    /// Returns the verification method that the provided [`DidUrl`] is
46    /// referencing, restricted to a given [`VerificationRole`].
47    ///
48    /// Runs in `O(roles + methods)`. Resolving every entry and then comparing
49    /// would be quadratic, which a hostile document can turn into seconds of
50    /// CPU for a few megabytes of input.
51    #[must_use]
52    pub fn resolve_verification_method_url(
53        &self,
54        url: &DidUrl,
55        role: VerificationRole,
56    ) -> Option<&VerificationMethodMap> {
57        let methods = match role {
58            VerificationRole::Assertion => self.assertion_method.as_deref(),
59            VerificationRole::Authentication => self.authentication.as_deref(),
60            VerificationRole::CapabilityDelegation => self.capability_delegation.as_deref(),
61            VerificationRole::CapabilityInvocation => self.capability_invocation.as_deref(),
62            VerificationRole::KeyAgreement => self.key_agreement.as_deref(),
63        }
64        .unwrap_or_default();
65
66        // Every reference that denotes `url` resolves to the same map, so the
67        // scan over `verification_method` happens at most once.
68        let mut referenced = None;
69
70        for method in methods {
71            let denotes_url = match method {
72                VerificationMethod::Map(map) => {
73                    if map.id == *url && *map.id.did() == self.id {
74                        return Some(map);
75                    }
76                    continue;
77                }
78                VerificationMethod::RelativeUrl(relative) => url.matches_relative(relative),
79                VerificationMethod::Url(reference) => reference == url,
80            };
81
82            if denotes_url
83                && let Some(found) = *referenced.get_or_insert_with(|| self.lookup_method(url))
84            {
85                return Some(found);
86            }
87        }
88
89        None
90    }
91
92    fn lookup_method(&self, url: &DidUrl) -> Option<&VerificationMethodMap> {
93        if *url.did() != self.id {
94            return None;
95        }
96
97        self.verification_method
98            .as_deref()?
99            .iter()
100            .find(|method| method.id == *url)
101    }
102
103    /// Resolves a [`VerificationMethod`] to its [`VerificationMethodMap`].
104    /// For embedded maps, returns the map directly. For URL references,
105    /// resolves them against this document's `verification_method` array.
106    ///
107    /// A method whose `id` names a different DID is never returned: otherwise a
108    /// document could embed a method claiming to be another identifier's key.
109    /// Its `controller` may name another DID, which this does not verify.
110    #[must_use]
111    pub fn resolve_verification_method<'a>(
112        &'a self,
113        method: &'a VerificationMethod,
114    ) -> Option<&'a VerificationMethodMap> {
115        let map = match method {
116            VerificationMethod::Map(map) => map.as_ref(),
117            VerificationMethod::RelativeUrl(relative_url) => {
118                self.resolve_relative_url(relative_url)?
119            }
120            VerificationMethod::Url(url) => {
121                if *url.did() != self.id {
122                    // TODO: Support additional DID resolution?
123                    return None;
124                }
125
126                self.resolve_relative_url(&url.to_relative()?)?
127            }
128        };
129
130        (*map.id.did() == self.id).then_some(map)
131    }
132
133    fn resolve_relative_url(&self, url: &RelativeDidUrl) -> Option<&VerificationMethodMap> {
134        self.verification_method
135            .as_deref()?
136            .iter()
137            .find(|method| method.id.matches_relative(url))
138    }
139}
140
141#[derive(Debug, Copy, Clone, PartialEq, Eq)]
142pub enum VerificationRole {
143    Assertion,
144    Authentication,
145    CapabilityDelegation,
146    CapabilityInvocation,
147    KeyAgreement,
148}
149
150#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
151#[serde(untagged)]
152pub enum VerificationMethod {
153    Map(Box<VerificationMethodMap>),
154    RelativeUrl(RelativeDidUrl),
155    Url(DidUrl),
156}
157
158#[skip_serializing_none]
159#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
160pub struct VerificationMethodMap {
161    pub id:                   DidUrl,
162    pub controller:           Did,
163    #[serde(rename = "type")]
164    pub typ:                  SmolStr,
165    pub public_key_jwk:       Option<Jwk>,
166    /// Multibase encoded public key.
167    pub public_key_multibase: Option<String>,
168}
169
170#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
171#[serde(rename_all = "camelCase")]
172#[serde_as]
173pub struct ServiceEndpoint {
174    pub id:               String,
175    #[serde(rename = "type")]
176    #[serde_as(as = "OneOrMany<_>")]
177    pub typ:              Vec<String>,
178    /// Supplied by whoever served the document and not validated here. Fetching
179    /// one on behalf of a caller carries the same server-side request forgery
180    /// exposure as resolving the DID itself, so apply a target policy first.
181    #[serde_as(as = "OneOrMany<_>")]
182    pub service_endpoint: Vec<String>,
183}
184
185#[cfg(test)]
186mod tests {
187    use std::str::FromStr;
188
189    use smol_str::SmolStr;
190
191    use super::*;
192    use crate::did_url::relative::RelativeDidUrlPath;
193
194    fn did(s: &str) -> Did {
195        Did::from_str(s).expect("valid DID")
196    }
197
198    fn url(did: &Did, fragment: &str) -> DidUrl {
199        DidUrl::new(did.clone(), None, None, Some(SmolStr::new(fragment))).expect("valid DID URL")
200    }
201
202    fn map(id: DidUrl, controller: &Did) -> VerificationMethodMap {
203        VerificationMethodMap {
204            id,
205            controller: controller.clone(),
206            typ: "JsonWebKey2020".into(),
207            public_key_jwk: None,
208            public_key_multibase: None,
209        }
210    }
211
212    fn document(id: Did, authentication: Vec<VerificationMethod>) -> Document {
213        Document {
214            context: None,
215            id,
216            also_known_as: None,
217            controller: None,
218            verification_method: None,
219            authentication: Some(authentication),
220            assertion_method: None,
221            key_agreement: None,
222            capability_invocation: None,
223            capability_delegation: None,
224            service: None,
225        }
226    }
227
228    #[test]
229    fn resolves_embedded_map() {
230        let me = did("did:web:example.com");
231        let key = url(&me, "key1");
232        let mut doc = document(
233            me.clone(),
234            vec![VerificationMethod::Map(Box::new(map(key.clone(), &me)))],
235        );
236        doc.verification_method = None;
237
238        assert_eq!(
239            doc.resolve_verification_method_url(&key, VerificationRole::Authentication),
240            Some(&map(key.clone(), &me))
241        );
242    }
243
244    #[test]
245    fn resolves_absolute_and_relative_references() {
246        let me = did("did:web:example.com");
247        let key = url(&me, "key1");
248
249        let relative =
250            RelativeDidUrl::new(RelativeDidUrlPath::Empty, None, Some(SmolStr::new("key1")))
251                .expect("valid relative DID URL");
252
253        for reference in [
254            VerificationMethod::Url(key.clone()),
255            VerificationMethod::RelativeUrl(relative),
256        ] {
257            let mut doc = document(me.clone(), vec![reference]);
258            doc.verification_method = Some(vec![map(key.clone(), &me)]);
259
260            assert_eq!(
261                doc.resolve_verification_method_url(&key, VerificationRole::Authentication),
262                Some(&map(key.clone(), &me))
263            );
264        }
265    }
266
267    #[test]
268    fn returns_none_for_unlisted_url() {
269        let me = did("did:web:example.com");
270        let key = url(&me, "key1");
271        let other = url(&me, "key2");
272
273        let mut doc = document(me.clone(), vec![VerificationMethod::Url(key.clone())]);
274        doc.verification_method = Some(vec![map(key, &me)]);
275
276        assert_eq!(
277            doc.resolve_verification_method_url(&other, VerificationRole::Authentication),
278            None
279        );
280    }
281
282    #[test]
283    fn rejects_embedded_map_claiming_another_did() {
284        let attacker = did("did:web:evil.example");
285        let victim = did("did:web:victim.example");
286        let victim_key = url(&victim, "key1");
287
288        let doc = document(
289            attacker,
290            vec![VerificationMethod::Map(Box::new(map(
291                victim_key.clone(),
292                &victim,
293            )))],
294        );
295
296        assert_eq!(
297            doc.resolve_verification_method_url(&victim_key, VerificationRole::Authentication),
298            None,
299            "a document must not speak for an identifier it does not control"
300        );
301    }
302
303    #[test]
304    fn rejects_verification_method_entry_for_another_did() {
305        let attacker = did("did:web:evil.example");
306        let victim = did("did:web:victim.example");
307        let victim_key = url(&victim, "key1");
308
309        let mut doc = document(attacker, vec![VerificationMethod::Url(victim_key.clone())]);
310        doc.verification_method = Some(vec![map(victim_key.clone(), &victim)]);
311
312        assert_eq!(
313            doc.resolve_verification_method_url(&victim_key, VerificationRole::Authentication),
314            None
315        );
316    }
317
318    #[test]
319    fn role_is_respected() {
320        let me = did("did:web:example.com");
321        let key = url(&me, "key1");
322
323        let mut doc = document(me.clone(), vec![VerificationMethod::Url(key.clone())]);
324        doc.verification_method = Some(vec![map(key.clone(), &me)]);
325
326        assert!(
327            doc.resolve_verification_method_url(&key, VerificationRole::Authentication)
328                .is_some()
329        );
330        assert!(
331            doc.resolve_verification_method_url(&key, VerificationRole::KeyAgreement)
332                .is_none()
333        );
334    }
335}