identity_credential/domain_linkage/
domain_linkage_configuration.rs1use 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#[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 #[serde(rename = "@context")]
39 context: Context,
40 linked_dids: Vec<Jwt>,
42}
43
44impl __DomainLinkageConfiguration {
45 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 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 pub fn linked_dids(&self) -> &Vec<Jwt> {
91 &self.0.linked_dids
92 }
93
94 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 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 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 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}