identity_credential/domain_linkage/
domain_linkage_credential_builder.rs

1// Copyright 2020-2023 IOTA Stiftung
2// SPDX-License-Identifier: Apache-2.0
3
4use crate::credential::Credential;
5use crate::credential::CredentialV2;
6use crate::credential::Issuer;
7use crate::credential::Subject;
8use crate::domain_linkage::DomainLinkageConfiguration;
9use crate::error::Result;
10use crate::Error;
11use identity_core::common::Object;
12use identity_core::common::OneOrMany;
13use identity_core::common::Timestamp;
14use identity_core::common::Url;
15use identity_did::CoreDID;
16use identity_did::DID;
17
18use crate::utils::url_only_includes_origin;
19
20/// Convenient builder to create a spec compliant Domain Linkage Credential.
21///
22/// See: <https://identity.foundation/.well-known/resources/did-configuration/#linked-data-proof-format>
23///
24/// The builder expects `issuer`, `expirationDate` and `origin` to be set.
25/// Setting `issuanceDate` is optional. If unset the current time will be used.
26#[derive(Debug, Default)]
27pub struct DomainLinkageCredentialBuilder {
28  pub(crate) issuer: Option<Url>,
29  pub(crate) issuance_date: Option<Timestamp>,
30  pub(crate) expiration_date: Option<Timestamp>,
31  pub(crate) origin: Option<Url>,
32}
33
34impl DomainLinkageCredentialBuilder {
35  /// Creates a new `DomainLinkageCredentialBuilder`.
36  pub fn new() -> Self {
37    Self::default()
38  }
39
40  /// Sets the value of the `issuer`.
41  ///
42  /// The issuer will also be set as `credentialSubject.id`.
43  #[must_use]
44  pub fn issuer(mut self, did: CoreDID) -> Self {
45    self.issuer = Some(did.into_url().into());
46    self
47  }
48
49  /// Sets the value of the `Credential` `issuanceDate`.
50  #[must_use]
51  pub fn issuance_date(mut self, value: Timestamp) -> Self {
52    self.issuance_date = Some(value);
53    self
54  }
55
56  /// Sets the value of the `Credential` `expirationDate`.
57  #[must_use]
58  pub fn expiration_date(mut self, value: Timestamp) -> Self {
59    self.expiration_date = Some(value);
60    self
61  }
62
63  /// Sets the origin in `credentialSubject`.
64  ///
65  /// Must be a domain origin.
66  #[must_use]
67  pub fn origin(mut self, value: Url) -> Self {
68    self.origin = Some(value);
69    self
70  }
71
72  /// Returns a new `Credential` based on the `DomainLinkageCredentialBuilder` configuration.
73  pub fn build(self) -> Result<Credential<Object>> {
74    let origin: Url = self.origin.ok_or(Error::MissingOrigin)?;
75    if origin.domain().is_none() {
76      return Err(Error::DomainLinkageError(
77        "origin must be a domain with http(s) scheme".into(),
78      ));
79    }
80    if !url_only_includes_origin(&origin) {
81      return Err(Error::DomainLinkageError(
82        "origin must not contain any path, query or fragment".into(),
83      ));
84    }
85
86    let mut properties: Object = Object::new();
87    properties.insert("origin".into(), origin.into_string().into());
88    let issuer: Url = self.issuer.ok_or(Error::MissingIssuer)?;
89
90    Ok(Credential {
91      context: OneOrMany::Many(vec![
92        Credential::<Object>::base_context().clone(),
93        DomainLinkageConfiguration::well_known_context().clone(),
94      ]),
95      id: None,
96      types: OneOrMany::Many(vec![
97        Credential::<Object>::base_type().to_owned(),
98        DomainLinkageConfiguration::domain_linkage_type().to_owned(),
99      ]),
100      credential_subject: OneOrMany::One(Subject::with_id_and_properties(issuer.clone(), properties)),
101      issuer: Issuer::Url(issuer),
102      issuance_date: self.issuance_date.unwrap_or_else(Timestamp::now_utc),
103      expiration_date: Some(self.expiration_date.ok_or(Error::MissingExpirationDate)?),
104      credential_status: None,
105      credential_schema: Vec::new().into(),
106      refresh_service: Vec::new().into(),
107      terms_of_use: Vec::new().into(),
108      evidence: Vec::new().into(),
109      non_transferable: None,
110      properties: Object::new(),
111      proof: None,
112    })
113  }
114
115  /// Returns a new VC Data Model 2.0 `Credential` based on the `DomainLinkageCredentialBuilder` configuration.
116  pub fn build_v2(self) -> Result<CredentialV2<Object>> {
117    let origin: Url = self.origin.ok_or(Error::MissingOrigin)?;
118    if origin.domain().is_none() {
119      return Err(Error::DomainLinkageError(
120        "origin must be a domain with http(s) scheme".into(),
121      ));
122    }
123    if !url_only_includes_origin(&origin) {
124      return Err(Error::DomainLinkageError(
125        "origin must not contain any path, query or fragment".into(),
126      ));
127    }
128
129    let mut properties: Object = Object::new();
130    properties.insert("origin".into(), origin.into_string().into());
131    let issuer: Url = self.issuer.ok_or(Error::MissingIssuer)?;
132
133    Ok(CredentialV2 {
134      context: OneOrMany::Many(vec![
135        Credential::<Object>::base_context().clone(),
136        DomainLinkageConfiguration::well_known_context().clone(),
137      ]),
138      id: None,
139      types: OneOrMany::Many(vec![
140        Credential::<Object>::base_type().to_owned(),
141        DomainLinkageConfiguration::domain_linkage_type().to_owned(),
142      ]),
143      credential_subject: OneOrMany::One(Subject::with_id_and_properties(issuer.clone(), properties)),
144      issuer: Issuer::Url(issuer),
145      valid_from: self.issuance_date.unwrap_or_else(Timestamp::now_utc),
146      valid_until: Some(self.expiration_date.ok_or(Error::MissingExpirationDate)?),
147      credential_status: None,
148      credential_schema: Vec::new().into(),
149      refresh_service: Vec::new().into(),
150      terms_of_use: Vec::new().into(),
151      evidence: Vec::new().into(),
152      non_transferable: None,
153      properties: Object::new(),
154      proof: None,
155    })
156  }
157}
158
159#[cfg(test)]
160mod tests {
161  use crate::credential::Credential;
162  use crate::domain_linkage::DomainLinkageCredentialBuilder;
163  use crate::error::Result;
164  use crate::Error;
165  use identity_core::common::Timestamp;
166  use identity_core::common::Url;
167  use identity_did::CoreDID;
168
169  #[test]
170  fn test_builder_with_all_fields_set_succeeds() {
171    let issuer: CoreDID = "did:example:issuer".parse().unwrap();
172    assert!(DomainLinkageCredentialBuilder::new()
173      .issuance_date(Timestamp::now_utc())
174      .expiration_date(Timestamp::now_utc())
175      .issuer(issuer)
176      .origin(Url::parse("http://www.example.com").unwrap())
177      .build()
178      .is_ok());
179  }
180
181  #[test]
182  fn test_builder_origin_is_not_a_domain() {
183    let issuer: CoreDID = "did:example:issuer".parse().unwrap();
184    let err: Error = DomainLinkageCredentialBuilder::new()
185      .issuance_date(Timestamp::now_utc())
186      .expiration_date(Timestamp::now_utc())
187      .issuer(issuer)
188      .origin(Url::parse("did:example:origin").unwrap())
189      .build()
190      .unwrap_err();
191    assert!(matches!(err, Error::DomainLinkageError(_)));
192  }
193  #[test]
194  fn test_builder_origin_is_a_url() {
195    let urls = [
196      "https://example.com/foo?bar=420#baz",
197      "https://example.com/?bar=420",
198      "https://example.com/#baz",
199      "https://example.com/?bar=420#baz",
200    ];
201    let issuer: CoreDID = "did:example:issuer".parse().unwrap();
202    for url in urls {
203      let err: Error = DomainLinkageCredentialBuilder::new()
204        .issuance_date(Timestamp::now_utc())
205        .expiration_date(Timestamp::now_utc())
206        .issuer(issuer.clone())
207        .origin(Url::parse(url).unwrap())
208        .build()
209        .unwrap_err();
210      assert!(matches!(err, Error::DomainLinkageError(_)));
211    }
212  }
213
214  #[test]
215  fn test_builder_no_issuer() {
216    let credential: Result<Credential> = DomainLinkageCredentialBuilder::new()
217      .issuance_date(Timestamp::now_utc())
218      .expiration_date(Timestamp::now_utc())
219      .origin(Url::parse("http://www.example.com").unwrap())
220      .build();
221
222    assert!(matches!(credential, Err(Error::MissingIssuer)));
223  }
224
225  #[test]
226  fn test_builder_no_origin() {
227    let issuer: CoreDID = "did:example:issuer".parse().unwrap();
228    let credential: Result<Credential> = DomainLinkageCredentialBuilder::new()
229      .issuance_date(Timestamp::now_utc())
230      .expiration_date(Timestamp::now_utc())
231      .issuer(issuer)
232      .build();
233
234    assert!(matches!(credential, Err(Error::MissingOrigin)));
235  }
236
237  #[test]
238  fn test_builder_no_expiration_date() {
239    let issuer: CoreDID = "did:example:issuer".parse().unwrap();
240    let credential: Result<Credential> = DomainLinkageCredentialBuilder::new()
241      .issuance_date(Timestamp::now_utc())
242      .issuer(issuer)
243      .origin(Url::parse("http://www.example.com").unwrap())
244      .build();
245
246    assert!(matches!(credential, Err(Error::MissingExpirationDate)));
247  }
248}