identity_credential/domain_linkage/
domain_linkage_configuration.rs

1// Copyright 2020-2023 IOTA Stiftung
2// SPDX-License-Identifier: Apache-2.0
3
4use crate::credential::Jwt;
5use crate::error::Result;
6use crate::validator::JwtCredentialValidatorUtils;
7use crate::validator::JwtValidationError;
8use identity_core::common::Context;
9use identity_core::common::Url;
10use identity_core::convert::FmtJson;
11use identity_did::CoreDID;
12use once_cell::sync::Lazy;
13use serde::Deserialize;
14use serde::Serialize;
15use std::fmt::Display;
16use std::fmt::Formatter;
17
18use crate::Error::DomainLinkageError;
19
20static WELL_KNOWN_CONTEXT: Lazy<Context> =
21  Lazy::new(|| Context::Url(Url::parse("https://identity.foundation/.well-known/did-configuration/v1").unwrap()));
22
23/// DID Configuration Resource which contains Domain Linkage Credentials.
24///
25/// It can be placed in an origin's `.well-known` directory to prove linkage between the origin and a DID.
26/// See: <https://identity.foundation/.well-known/resources/did-configuration/#did-configuration-resource>
27///
28/// Note:
29/// - Only the [JSON Web Token Proof Format](https://identity.foundation/.well-known/resources/did-configuration/#json-web-token-proof-format)
30#[derive(Clone, Debug, Deserialize, Serialize)]
31#[serde(try_from = "__DomainLinkageConfiguration")]
32pub struct DomainLinkageConfiguration(__DomainLinkageConfiguration);
33
34#[derive(Clone, Debug, Deserialize, Serialize)]
35#[serde(deny_unknown_fields)]
36struct __DomainLinkageConfiguration {
37  /// Fixed context.
38  #[serde(rename = "@context")]
39  context: Context,
40  /// Linked JWT credentials.
41  linked_dids: Vec<Jwt>,
42}
43
44impl __DomainLinkageConfiguration {
45  /// Validates the semantic structure.
46  fn check_structure(&self) -> Result<()> {
47    if &self.context != DomainLinkageConfiguration::well_known_context() {
48      return Err(DomainLinkageError("invalid JSON-LD context".into()));
49    }
50    if self.linked_dids.is_empty() {
51      return Err(DomainLinkageError("empty linked_dids list".into()));
52    }
53    Ok(())
54  }
55}
56
57impl TryFrom<__DomainLinkageConfiguration> for DomainLinkageConfiguration {
58  type Error = &'static str;
59
60  fn try_from(config: __DomainLinkageConfiguration) -> Result<Self, Self::Error> {
61    config.check_structure()?;
62    Ok(Self(config))
63  }
64}
65
66impl Display for DomainLinkageConfiguration {
67  fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
68    self.fmt_json(f)
69  }
70}
71
72impl DomainLinkageConfiguration {
73  /// Creates a new DID Configuration Resource.
74  pub fn new(linked_dids: Vec<Jwt>) -> Self {
75    Self(__DomainLinkageConfiguration {
76      context: Self::well_known_context().clone(),
77      linked_dids,
78    })
79  }
80
81  pub(crate) fn well_known_context() -> &'static Context {
82    &WELL_KNOWN_CONTEXT
83  }
84
85  pub(crate) const fn domain_linkage_type() -> &'static str {
86    "DomainLinkageCredential"
87  }
88
89  /// List of Domain Linkage Credentials.
90  pub fn linked_dids(&self) -> &Vec<Jwt> {
91    &self.0.linked_dids
92  }
93
94  /// List of the issuers of the Domain Linkage Credentials.
95  pub fn issuers(&self) -> std::result::Result<Vec<CoreDID>, JwtValidationError> {
96    self
97      .0
98      .linked_dids
99      .iter()
100      .map(JwtCredentialValidatorUtils::extract_issuer_from_jwt::<CoreDID>)
101      .collect()
102  }
103
104  /// List of domain Linkage Credentials.
105  pub fn linked_dids_mut(&mut self) -> &mut Vec<Jwt> {
106    &mut self.0.linked_dids
107  }
108}
109
110#[cfg(feature = "domain-linkage-fetch")]
111mod __fetch_configuration {
112  use crate::domain_linkage::DomainLinkageConfiguration;
113  use crate::error::Result;
114  use crate::utils::url_only_includes_origin;
115  use crate::Error::DomainLinkageError;
116  use futures::StreamExt;
117  use identity_core::common::Url;
118  use identity_core::convert::FromJson;
119  use reqwest::redirect::Policy;
120  use reqwest::Client;
121
122  impl DomainLinkageConfiguration {
123    /// Fetches the the DID Configuration resource via a GET request at the
124    /// well-known location: "`domain`/.well-known/did-configuration.json".
125    ///
126    /// The maximum size of the domain linkage configuration that can be retrieved with this method is 1 MiB.
127    /// To download larger ones, use your own HTTP client.
128    pub async fn fetch_configuration(mut domain: Url) -> Result<DomainLinkageConfiguration> {
129      if domain.scheme() != "https" {
130        return Err(DomainLinkageError("domain` does not use `https` protocol".into()));
131      }
132      if !url_only_includes_origin(&domain) {
133        return Err(DomainLinkageError(
134          "domain must not include any path, query or fragment".into(),
135        ));
136      }
137      domain.set_path(".well-known/did-configuration.json");
138
139      let client: Client = reqwest::ClientBuilder::new()
140        .https_only(true)
141        .redirect(Policy::none())
142        .build()
143        .map_err(|err| DomainLinkageError(Box::new(err)))?;
144
145      // We use a stream so we can limit the size of the response to 1 MiB.
146      let mut stream = client
147        .get(domain.to_string())
148        .send()
149        .await
150        .map_err(|err| DomainLinkageError(Box::new(err)))?
151        .bytes_stream();
152
153      let mut json: Vec<u8> = Vec::new();
154      while let Some(item) = stream.next().await {
155        match item {
156          Ok(bytes) => {
157            json.extend(bytes);
158            if json.len() > 1_048_576 {
159              return Err(DomainLinkageError(
160                "domain linkage configuration can not exceed 1 MiB".into(),
161              ));
162            }
163          }
164          Err(err) => return Err(DomainLinkageError(Box::new(err))),
165        }
166      }
167      let domain_linkage_configuration: DomainLinkageConfiguration =
168        DomainLinkageConfiguration::from_json_slice(&json).map_err(|err| DomainLinkageError(Box::new(err)))?;
169      Ok(domain_linkage_configuration)
170    }
171  }
172}
173
174#[cfg(test)]
175mod tests {
176  use crate::domain_linkage::DomainLinkageConfiguration;
177  use identity_core::convert::FromJson;
178  use identity_core::error::Result;
179  use serde_json::json;
180  use serde_json::Value;
181
182  #[test]
183  fn test_from_json_valid() {
184    const JSON1: &str = include_str!("../../tests/fixtures/domain-config-valid.json");
185    DomainLinkageConfiguration::from_json(JSON1).unwrap();
186  }
187
188  #[test]
189  fn test_from_json_invalid_context() {
190    const JSON1: &str = include_str!("../../tests/fixtures/domain-config-invalid-context.json");
191    let deserialization_result: Result<DomainLinkageConfiguration> = DomainLinkageConfiguration::from_json(JSON1);
192    assert!(deserialization_result.is_err());
193  }
194
195  #[test]
196  fn test_from_json_extra_property() {
197    const JSON1: &str = include_str!("../../tests/fixtures/domain-config-extra-property.json");
198    let deserialization_result: Result<DomainLinkageConfiguration> = DomainLinkageConfiguration::from_json(JSON1);
199    assert!(deserialization_result.is_err());
200  }
201
202  #[test]
203  fn test_from_json_empty_linked_did() {
204    let json_value: Value = json!({
205      "@context": "https://identity.foundation/.well-known/did-configuration/v1",
206      "linked_dids": []
207    });
208    let deserialization_result: Result<DomainLinkageConfiguration> =
209      DomainLinkageConfiguration::from_json_value(json_value);
210    assert!(deserialization_result.is_err());
211  }
212}