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