identity_credential/domain_linkage/
domain_linkage_credential_builder.rs1use 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#[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 pub fn new() -> Self {
37 Self::default()
38 }
39
40 #[must_use]
44 pub fn issuer(mut self, did: CoreDID) -> Self {
45 self.issuer = Some(did.into_url().into());
46 self
47 }
48
49 #[must_use]
51 pub fn issuance_date(mut self, value: Timestamp) -> Self {
52 self.issuance_date = Some(value);
53 self
54 }
55
56 #[must_use]
58 pub fn expiration_date(mut self, value: Timestamp) -> Self {
59 self.expiration_date = Some(value);
60 self
61 }
62
63 #[must_use]
67 pub fn origin(mut self, value: Url) -> Self {
68 self.origin = Some(value);
69 self
70 }
71
72 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 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}