identity_credential/domain_linkage/
domain_linkage_validator.rs

1// Copyright 2020-2023 IOTA Stiftung
2// SPDX-License-Identifier: Apache-2.0
3
4use crate::credential::Credential;
5use crate::credential::Jwt;
6use crate::domain_linkage::DomainLinkageConfiguration;
7use crate::domain_linkage::DomainLinkageValidationError;
8use crate::domain_linkage::DomainLinkageValidationErrorCause;
9use crate::validator::FailFast;
10use crate::validator::JwtCredentialValidationOptions;
11use crate::validator::JwtCredentialValidator;
12use crate::validator::JwtCredentialValidatorUtils;
13use identity_core::common::OneOrMany;
14use identity_core::common::Url;
15use identity_did::CoreDID;
16use identity_document::document::CoreDocument;
17use identity_verification::jws::JwsVerifier;
18
19use crate::validator::DecodedJwtCredential;
20
21use super::DomainLinkageValidationErrorList;
22use super::DomainLinkageValidationResult;
23use crate::utils::url_only_includes_origin;
24/// A validator for a Domain Linkage Configuration and Credentials.
25pub struct JwtDomainLinkageValidator<V: JwsVerifier> {
26  validator: JwtCredentialValidator<V>,
27}
28
29impl<V: JwsVerifier> JwtDomainLinkageValidator<V> {
30  /// Create a new [`JwtDomainLinkageValidator`] that delegates cryptographic signature verification to the given
31  /// `signature_verifier`.
32  pub fn with_signature_verifier(signature_verifier: V) -> Self {
33    Self {
34      validator: JwtCredentialValidator::with_signature_verifier(signature_verifier),
35    }
36  }
37
38  /// Validates the linkage between a domain and a DID.
39  /// [`DomainLinkageConfiguration`] is validated according to [DID Configuration Resource Verification](https://identity.foundation/.well-known/resources/did-configuration/#did-configuration-resource-verification).
40  ///
41  /// * `issuer`: DID Document of the linked DID. Issuer of the Domain Linkage Credential included in the Domain Linkage
42  ///   Configuration.
43  /// * `configuration`: Domain Linkage Configuration fetched from the domain at "/.well-known/did-configuration.json".
44  /// * `domain`: domain from which the Domain Linkage Configuration has been fetched.
45  /// * `validation_options`: Further validation options to be applied on the Domain Linkage Credential.
46  ///
47  /// # Note:
48  /// - Only the [JSON Web Token Proof Format](https://identity.foundation/.well-known/resources/did-configuration/#json-web-token-proof-format)
49  ///   is supported.
50  /// - Only the Credentials issued by `issuer` are verified. All other credentials are ignored.
51  ///
52  /// # Errors
53  ///  - Semantic structure of `configuration` is invalid.
54  ///  - Validation of the matched Domain Linkage Credential fails.
55  pub fn validate_linkage<DOC: AsRef<CoreDocument>>(
56    &self,
57    issuer: &DOC,
58    configuration: &DomainLinkageConfiguration,
59    domain: &Url,
60    validation_options: &JwtCredentialValidationOptions,
61  ) -> DomainLinkageValidationResult {
62    let (ok_results, error_results): (Vec<_>, Vec<_>) = self
63      .validate_linkage_iter(issuer, configuration, domain, validation_options)?
64      .partition(Result::is_ok);
65
66    if !ok_results.is_empty() {
67      Ok(())
68    } else if !error_results.is_empty() {
69      let errors = error_results
70        .into_iter()
71        .map(Result::unwrap_err) // Safety: `errors` is a list of prefiltered `Err(_)`.
72        .collect();
73      Err(DomainLinkageValidationError {
74        cause: DomainLinkageValidationErrorCause::List,
75        source: Some(DomainLinkageValidationErrorList::new(errors).into()),
76      })
77    } else {
78      // this _should_ not be the case, as `validate_linkage_iter` should throw an error if no issuer matches
79      Err(DomainLinkageValidationError {
80        cause: DomainLinkageValidationErrorCause::InvalidStructure,
81        source: None,
82      })
83    }
84  }
85
86  /// Validates the linkage between a domain and a DID.
87  /// [`DomainLinkageConfiguration`] is validated according to [DID Configuration Resource Verification](https://identity.foundation/.well-known/resources/did-configuration/#did-configuration-resource-verification).
88  ///
89  /// * `issuer`: DID Document of the linked DID. Issuer of the Domain Linkage Credential included in the Domain Linkage
90  ///   Configuration.
91  /// * `configuration`: Domain Linkage Configuration fetched from the domain at "/.well-known/did-configuration.json".
92  /// * `domain`: domain from which the Domain Linkage Configuration has been fetched.
93  /// * `validation_options`: Further validation options to be applied on the Domain Linkage Credential.
94  ///
95  /// Returns an iterator, allowing to validate credentials issued by `issuer` one by one. Return values are
96  /// `DomainLinkageValidationResult`, allowing to interpret the single validations as needed (one must be valid, all
97  /// must be valid, etc.).
98  ///
99  /// # Note:
100  /// - Only the [JSON Web Token Proof Format](https://identity.foundation/.well-known/resources/did-configuration/#json-web-token-proof-format)
101  ///   is supported.
102  /// - Only the Credentials issued by `issuer` are verified.
103  ///
104  /// # Errors
105  ///  - Semantic structure of `configuration` is invalid.
106  ///  - Validation of the matched Domain Linkage Credential fails.
107  pub fn validate_linkage_iter<'a, DOC: AsRef<CoreDocument>>(
108    &'a self,
109    issuer: &'a DOC,
110    configuration: &'a DomainLinkageConfiguration,
111    domain: &'a Url,
112    validation_options: &'a JwtCredentialValidationOptions,
113  ) -> Result<impl Iterator<Item = DomainLinkageValidationResult> + use<'a, DOC, V>, DomainLinkageValidationError> {
114    // perform checks about overall structure:
115    // all issuers can be parsed
116    let issuers: Vec<CoreDID> = configuration.issuers().map_err(|err| DomainLinkageValidationError {
117      cause: DomainLinkageValidationErrorCause::InvalidJwt,
118      source: Some(err.into()),
119    })?;
120    // provided issuer can be found in credentials
121    if issuers.iter().filter(|iss| *iss == issuer.as_ref().id()).count() < 1 {
122      return Err(DomainLinkageValidationError {
123        cause: DomainLinkageValidationErrorCause::InvalidStructure,
124        source: None,
125      });
126    };
127
128    // build iterator over filtered list of credentials
129    let validation_iter = configuration
130      .linked_dids()
131      .iter()
132      .map(|v| JwtCredentialValidatorUtils::extract_issuer_from_jwt::<CoreDID>(v).unwrap())
133      .enumerate()
134      .filter_map(|(index, iss)| {
135        if iss == *issuer.as_ref().id() {
136          Some(index)
137        } else {
138          None
139        }
140      })
141      .map(move |index| {
142        configuration
143          .linked_dids()
144          .get(index)
145          .ok_or_else(|| DomainLinkageValidationError {
146            cause: DomainLinkageValidationErrorCause::InvalidIssuer,
147            source: None,
148          })
149          .and_then(|credential| self.validate_credential(&issuer, credential, domain, validation_options))
150      });
151
152    Ok(validation_iter)
153  }
154
155  /// Validates a [Domain Linkage Credential](https://identity.foundation/.well-known/resources/did-configuration/#domain-linkage-credential).
156  ///
157  /// *`issuer`: issuer of the credential.
158  /// *`credential`: domain linkage Credential to be verified.
159  /// *`domain`: the domain hosting the credential.
160  pub fn validate_credential<DOC: AsRef<CoreDocument>>(
161    &self,
162    issuer: &DOC,
163    credential: &Jwt,
164    domain: &Url,
165    validation_options: &JwtCredentialValidationOptions,
166  ) -> DomainLinkageValidationResult {
167    let decoded_credential: DecodedJwtCredential = self
168      .validator
169      .validate(credential, issuer, validation_options, FailFast::AllErrors)
170      .map_err(|err| DomainLinkageValidationError {
171        cause: DomainLinkageValidationErrorCause::CredentialValidationError,
172        source: Some(Box::new(err)),
173      })?;
174
175    let credential: &Credential = &decoded_credential.credential;
176
177    let issuer_did: CoreDID =
178      CoreDID::parse(credential.issuer.url().as_str()).map_err(|err| DomainLinkageValidationError {
179        cause: DomainLinkageValidationErrorCause::InvalidIssuer,
180        source: Some(Box::new(err)),
181      })?;
182
183    if credential.id.is_some() {
184      return Err(DomainLinkageValidationError {
185        cause: DomainLinkageValidationErrorCause::ImpermissibleIdProperty,
186        source: None,
187      });
188    }
189
190    // Validate type.
191    if !credential
192      .types
193      .iter()
194      .any(|type_| type_ == DomainLinkageConfiguration::domain_linkage_type())
195    {
196      return Err(DomainLinkageValidationError {
197        cause: DomainLinkageValidationErrorCause::InvalidTypeProperty,
198        source: None,
199      });
200    }
201
202    // Extract credential subject.
203    let OneOrMany::One(ref credential_subject) = credential.credential_subject else {
204      return Err(DomainLinkageValidationError {
205        cause: DomainLinkageValidationErrorCause::MultipleCredentialSubjects,
206        source: None,
207      });
208    };
209
210    // Validate credential subject.
211    {
212      let subject_id = credential_subject.id.as_deref().ok_or(DomainLinkageValidationError {
213        cause: DomainLinkageValidationErrorCause::MissingSubjectId,
214        source: None,
215      })?;
216      let subject_did = CoreDID::parse(subject_id.as_str()).map_err(|_| DomainLinkageValidationError {
217        cause: DomainLinkageValidationErrorCause::InvalidSubjectId,
218        source: None,
219      })?;
220      if issuer_did != subject_did {
221        return Err(DomainLinkageValidationError {
222          cause: DomainLinkageValidationErrorCause::IssuerSubjectMismatch,
223          source: None,
224        });
225      }
226    }
227
228    // Extract and validate origin.
229    {
230      let origin: &str = credential_subject
231        .properties
232        .get("origin")
233        .and_then(|value| value.as_str())
234        .ok_or(DomainLinkageValidationError {
235          cause: DomainLinkageValidationErrorCause::InvalidSubjectOrigin,
236          source: None,
237        })?;
238      let origin_url: Url = match Url::parse(origin) {
239        Ok(url) => Ok(url),
240        Err(identity_core::Error::InvalidUrl(url::ParseError::RelativeUrlWithoutBase)) => {
241          Url::parse("https://".to_owned() + origin).map_err(|err| DomainLinkageValidationError {
242            cause: DomainLinkageValidationErrorCause::InvalidSubjectOrigin,
243            source: Some(Box::new(err)),
244          })
245        }
246        Err(other_error) => Err(DomainLinkageValidationError {
247          cause: DomainLinkageValidationErrorCause::InvalidSubjectOrigin,
248          source: Some(Box::new(other_error)),
249        }),
250      }?;
251      if !url_only_includes_origin(&origin_url) {
252        return Err(DomainLinkageValidationError {
253          cause: DomainLinkageValidationErrorCause::InvalidSubjectOrigin,
254          source: None,
255        });
256      }
257      if origin_url.origin() != domain.origin() {
258        return Err(DomainLinkageValidationError {
259          cause: DomainLinkageValidationErrorCause::OriginMismatch,
260          source: None,
261        });
262      }
263    }
264    Ok(())
265  }
266}
267
268#[cfg(test)]
269mod tests {
270  use crate::credential::Credential;
271  use crate::credential::Jws;
272  use crate::credential::Jwt;
273  use crate::domain_linkage::DomainLinkageConfiguration;
274  use crate::domain_linkage::DomainLinkageCredentialBuilder;
275  use crate::domain_linkage::DomainLinkageValidationErrorCause;
276  use crate::domain_linkage::DomainLinkageValidationResult;
277  use crate::domain_linkage::JwtDomainLinkageValidator;
278  use crate::validator::test_utils::generate_jwk_document_with_keys;
279  use crate::validator::JwtCredentialValidationOptions;
280
281  use crypto::signatures::ed25519::SecretKey;
282  use identity_core::common::Duration;
283  use identity_core::common::Object;
284  use identity_core::common::OneOrMany;
285  use identity_core::common::OrderedSet;
286  use identity_core::common::Timestamp;
287  use identity_core::common::Url;
288  use identity_did::CoreDID;
289  use identity_document::document::CoreDocument;
290  use identity_eddsa_verifier::EdDSAJwsVerifier;
291  use identity_verification::jws::CharSet;
292  use identity_verification::jws::CompactJwsEncoder;
293  use identity_verification::jws::CompactJwsEncodingOptions;
294  use identity_verification::jws::JwsAlgorithm;
295  use identity_verification::jws::JwsHeader;
296  use identity_verification::MethodData;
297  use identity_verification::VerificationMethod;
298  use once_cell::sync::Lazy;
299
300  static JWT_DOMAIN_LINKAGE_VALIDATOR_ED25519: Lazy<JwtDomainLinkageValidator<EdDSAJwsVerifier>> =
301    Lazy::new(|| JwtDomainLinkageValidator::with_signature_verifier(EdDSAJwsVerifier::default()));
302
303  #[test]
304  pub(crate) fn test_valid_credential() {
305    let (document, secret_key, fragment) = generate_jwk_document_with_keys();
306    let credential: Credential = create_domain_linkage_credential(document.id());
307    let jwt: Jwt = sign_credential_jwt(&credential, &document, &fragment, &secret_key);
308
309    let validation_result: DomainLinkageValidationResult = JWT_DOMAIN_LINKAGE_VALIDATOR_ED25519.validate_credential(
310      &document,
311      &jwt,
312      &url_foo(),
313      &JwtCredentialValidationOptions::default(),
314    );
315
316    assert!(validation_result.is_ok());
317  }
318
319  #[test]
320  pub(crate) fn test_invalid_credential_signature() {
321    let (document, _secret_key, fragment) = generate_jwk_document_with_keys();
322    let credential: Credential = create_domain_linkage_credential(document.id());
323    let other_secret_key: SecretKey = SecretKey::generate().unwrap();
324    // Sign with `other_secret_key` to produce an invalid signature.
325    let jwt: Jwt = sign_credential_jwt(&credential, &document, &fragment, &other_secret_key);
326
327    let validation_result: DomainLinkageValidationResult = JWT_DOMAIN_LINKAGE_VALIDATOR_ED25519.validate_credential(
328      &document,
329      &jwt,
330      &url_foo(),
331      &JwtCredentialValidationOptions::default(),
332    );
333    assert!(matches!(
334      validation_result.unwrap_err().cause,
335      DomainLinkageValidationErrorCause::CredentialValidationError
336    ));
337  }
338
339  #[test]
340  pub(crate) fn test_invalid_id_property() {
341    let (document, secret_key, fragment) = generate_jwk_document_with_keys();
342    let mut credential: Credential = create_domain_linkage_credential(document.id());
343    credential.id = Some(Url::parse("http://random.credential.id").unwrap());
344    let jwt: Jwt = sign_credential_jwt(&credential, &document, &fragment, &secret_key);
345
346    let validation_result: DomainLinkageValidationResult = JWT_DOMAIN_LINKAGE_VALIDATOR_ED25519.validate_credential(
347      &document,
348      &jwt,
349      &url_foo(),
350      &JwtCredentialValidationOptions::default(),
351    );
352
353    assert!(matches!(
354      validation_result.unwrap_err().cause,
355      DomainLinkageValidationErrorCause::ImpermissibleIdProperty
356    ));
357  }
358
359  #[test]
360  pub(crate) fn test_domain_linkage_type_missing() {
361    let (document, secret_key, fragment) = generate_jwk_document_with_keys();
362    let mut credential: Credential = create_domain_linkage_credential(document.id());
363    credential.types = OneOrMany::One(Credential::<Object>::base_type().to_owned());
364    let jwt: Jwt = sign_credential_jwt(&credential, &document, &fragment, &secret_key);
365
366    let validation_result: DomainLinkageValidationResult = JWT_DOMAIN_LINKAGE_VALIDATOR_ED25519.validate_credential(
367      &document,
368      &jwt,
369      &url_foo(),
370      &JwtCredentialValidationOptions::default(),
371    );
372
373    assert!(matches!(
374      validation_result.unwrap_err().cause,
375      DomainLinkageValidationErrorCause::InvalidTypeProperty
376    ));
377  }
378
379  #[test]
380  pub(crate) fn test_extra_type() {
381    let (document, secret_key, fragment) = generate_jwk_document_with_keys();
382    let mut credential: Credential = create_domain_linkage_credential(document.id());
383    credential.types = OneOrMany::Many(vec![
384      Credential::<Object>::base_type().to_owned(),
385      DomainLinkageConfiguration::domain_linkage_type().to_owned(),
386      "not-allowed-type".to_owned(),
387    ]);
388    let jwt: Jwt = sign_credential_jwt(&credential, &document, &fragment, &secret_key);
389
390    let validation_result: DomainLinkageValidationResult = JWT_DOMAIN_LINKAGE_VALIDATOR_ED25519.validate_credential(
391      &document,
392      &jwt,
393      &url_foo(),
394      &JwtCredentialValidationOptions::default(),
395    );
396
397    assert!(validation_result.is_ok());
398  }
399
400  #[test]
401  pub(crate) fn test_origin_mismatch() {
402    let (document, secret_key, fragment) = generate_jwk_document_with_keys();
403    let mut credential: Credential = create_domain_linkage_credential(document.id());
404
405    let mut properties: Object = Object::new();
406    properties.insert("origin".into(), "http://www.example-1.com".into());
407    if let OneOrMany::One(ref mut subject) = credential.credential_subject {
408      subject.properties = properties;
409    }
410    let jwt: Jwt = sign_credential_jwt(&credential, &document, &fragment, &secret_key);
411
412    let validation_result: DomainLinkageValidationResult = JWT_DOMAIN_LINKAGE_VALIDATOR_ED25519.validate_credential(
413      &document,
414      &jwt,
415      &url_foo(),
416      &JwtCredentialValidationOptions::default(),
417    );
418
419    assert!(matches!(
420      validation_result.unwrap_err().cause,
421      DomainLinkageValidationErrorCause::OriginMismatch
422    ));
423  }
424
425  #[test]
426  pub(crate) fn test_empty_origin() {
427    let (document, secret_key, fragment) = generate_jwk_document_with_keys();
428    let mut credential: Credential = create_domain_linkage_credential(document.id());
429
430    let properties: Object = Object::new();
431    if let OneOrMany::One(ref mut subject) = credential.credential_subject {
432      subject.properties = properties;
433    }
434    let jwt: Jwt = sign_credential_jwt(&credential, &document, &fragment, &secret_key);
435
436    let validation_result: DomainLinkageValidationResult = JWT_DOMAIN_LINKAGE_VALIDATOR_ED25519.validate_credential(
437      &document,
438      &jwt,
439      &url_foo(),
440      &JwtCredentialValidationOptions::default(),
441    );
442
443    assert!(matches!(
444      validation_result.unwrap_err().cause,
445      DomainLinkageValidationErrorCause::InvalidSubjectOrigin
446    ));
447  }
448
449  #[test]
450  pub(crate) fn test_origin_without_scheme() {
451    let (document, secret_key, fragment) = generate_jwk_document_with_keys();
452    let mut credential: Credential = create_domain_linkage_credential(document.id());
453
454    let mut properties: Object = Object::new();
455    properties.insert("origin".into(), "foo.example.com".into());
456    if let OneOrMany::One(ref mut subject) = credential.credential_subject {
457      subject.properties = properties;
458    }
459    let jwt: Jwt = sign_credential_jwt(&credential, &document, &fragment, &secret_key);
460
461    let validation_result: DomainLinkageValidationResult = JWT_DOMAIN_LINKAGE_VALIDATOR_ED25519.validate_credential(
462      &document,
463      &jwt,
464      &url_foo(),
465      &JwtCredentialValidationOptions::default(),
466    );
467
468    assert!(validation_result.is_ok());
469  }
470
471  #[test]
472  pub(crate) fn test_no_subject_id() {
473    let (document, secret_key, fragment) = generate_jwk_document_with_keys();
474    let mut credential: Credential = create_domain_linkage_credential(document.id());
475
476    if let OneOrMany::One(ref mut subject) = credential.credential_subject {
477      subject.id = None;
478    }
479    let jwt: Jwt = sign_credential_jwt(&credential, &document, &fragment, &secret_key);
480
481    let validation_result: DomainLinkageValidationResult = JWT_DOMAIN_LINKAGE_VALIDATOR_ED25519.validate_credential(
482      &document,
483      &jwt,
484      &url_foo(),
485      &JwtCredentialValidationOptions::default(),
486    );
487
488    assert!(matches!(
489      validation_result.unwrap_err().cause,
490      DomainLinkageValidationErrorCause::MissingSubjectId
491    ));
492  }
493
494  #[test]
495  pub(crate) fn test_invalid_subject_id() {
496    let (document, secret_key, fragment) = generate_jwk_document_with_keys();
497    let mut credential: Credential = create_domain_linkage_credential(document.id());
498
499    if let OneOrMany::One(ref mut subject) = credential.credential_subject {
500      subject.id = Some(Url::parse("http://invalid.did").unwrap());
501    }
502
503    let jwt: Jwt = sign_credential_jwt(&credential, &document, &fragment, &secret_key);
504
505    let validation_result: DomainLinkageValidationResult = JWT_DOMAIN_LINKAGE_VALIDATOR_ED25519.validate_credential(
506      &document,
507      &jwt,
508      &url_foo(),
509      &JwtCredentialValidationOptions::default(),
510    );
511
512    assert!(matches!(
513      validation_result.unwrap_err().cause,
514      DomainLinkageValidationErrorCause::InvalidSubjectId
515    ));
516  }
517
518  #[test]
519  pub(crate) fn test_issuer_subject_mismatch() {
520    let (document, secret_key, fragment) = generate_jwk_document_with_keys();
521    let mut credential: Credential = create_domain_linkage_credential(document.id());
522
523    if let OneOrMany::One(ref mut subject) = credential.credential_subject {
524      subject.id = Some(Url::parse("did:abc:xyz").unwrap());
525    }
526    let jwt: Jwt = sign_credential_jwt(&credential, &document, &fragment, &secret_key);
527
528    let validation_result: DomainLinkageValidationResult = JWT_DOMAIN_LINKAGE_VALIDATOR_ED25519.validate_credential(
529      &document,
530      &jwt,
531      &url_foo(),
532      &JwtCredentialValidationOptions::default(),
533    );
534
535    assert!(matches!(
536      validation_result.unwrap_err().cause,
537      DomainLinkageValidationErrorCause::IssuerSubjectMismatch
538    ));
539  }
540
541  #[test]
542  pub(crate) fn test_multiple_credential_combinations_with_validate() {
543    let (document, secret_key, fragment) = generate_jwk_document_with_keys();
544
545    let credential_1: Credential = create_domain_linkage_credential(document.id());
546    let jwt_valid: Jwt = sign_credential_jwt(&credential_1, &document, &fragment, &secret_key);
547
548    let mut credential_2: Credential = create_domain_linkage_credential(document.id());
549    if let OneOrMany::One(ref mut subject) = credential_2.credential_subject {
550      subject.id = Some(Url::parse("http://invalid.did").unwrap());
551    }
552    let jwt_invalid: Jwt = sign_credential_jwt(&credential_2, &document, &fragment, &secret_key);
553
554    let configurations: Vec<(DomainLinkageConfiguration, bool)> = vec![
555      (
556        DomainLinkageConfiguration::new(vec![jwt_valid.clone(), jwt_valid.clone()]),
557        true,
558      ),
559      (
560        DomainLinkageConfiguration::new(vec![jwt_invalid.clone(), jwt_valid.clone()]),
561        true,
562      ),
563      (
564        DomainLinkageConfiguration::new(vec![jwt_valid.clone(), jwt_invalid.clone()]),
565        true,
566      ),
567      (
568        DomainLinkageConfiguration::new(vec![jwt_invalid.clone(), jwt_invalid.clone()]),
569        false,
570      ),
571    ];
572
573    let validations: Vec<(bool, bool)> = configurations
574      .into_iter()
575      .map(|(configuration, expected)| {
576        (
577          JWT_DOMAIN_LINKAGE_VALIDATOR_ED25519
578            .validate_linkage(
579              &document,
580              &configuration,
581              &url_foo(),
582              &JwtCredentialValidationOptions::default(),
583            )
584            .is_ok(),
585          expected,
586        )
587      })
588      .collect();
589
590    for (index, (outcome, expected)) in validations.iter().enumerate() {
591      assert_eq!(outcome, expected, "testing result of test config {index}");
592    }
593  }
594
595  #[test]
596  pub(crate) fn test_multiple_credential_combinations_with_validate_iter() {
597    let (document, secret_key, fragment) = generate_jwk_document_with_keys();
598
599    let credential_1: Credential = create_domain_linkage_credential(document.id());
600    let jwt_valid: Jwt = sign_credential_jwt(&credential_1, &document, &fragment, &secret_key);
601
602    let mut credential_2: Credential = create_domain_linkage_credential(document.id());
603    if let OneOrMany::One(ref mut subject) = credential_2.credential_subject {
604      subject.id = Some(Url::parse("http://invalid.did").unwrap());
605    }
606    let jwt_invalid: Jwt = sign_credential_jwt(&credential_2, &document, &fragment, &secret_key);
607
608    let configurations: Vec<(DomainLinkageConfiguration, Vec<bool>)> = vec![
609      (
610        DomainLinkageConfiguration::new(vec![jwt_valid.clone(), jwt_valid.clone()]),
611        vec![true, true],
612      ),
613      (
614        DomainLinkageConfiguration::new(vec![jwt_invalid.clone(), jwt_valid.clone()]),
615        vec![false, true],
616      ),
617      (
618        DomainLinkageConfiguration::new(vec![jwt_valid.clone(), jwt_invalid.clone()]),
619        vec![true, false],
620      ),
621      (
622        DomainLinkageConfiguration::new(vec![jwt_invalid.clone(), jwt_invalid.clone()]),
623        vec![false, false],
624      ),
625    ];
626
627    let validations: Vec<(Vec<bool>, Vec<bool>)> = configurations
628      .into_iter()
629      .map(|(configuration, expected)| {
630        (
631          JWT_DOMAIN_LINKAGE_VALIDATOR_ED25519
632            .validate_linkage_iter(
633              &document,
634              &configuration,
635              &url_foo(),
636              &JwtCredentialValidationOptions::default(),
637            )
638            .expect("validation iterator should be creatable")
639            .map(|r| r.is_ok())
640            .collect(),
641          expected,
642        )
643      })
644      .collect();
645
646    for (index, (outcome, expected)) in validations.iter().enumerate() {
647      assert_eq!(outcome, expected, "testing result of test config {index}");
648    }
649  }
650
651  #[test]
652  pub(crate) fn test_multiple_credential_combinations_with_validate_iter_counts() {
653    let (document, secret_key, fragment) = generate_jwk_document_with_keys();
654
655    let credential_1: Credential = create_domain_linkage_credential(document.id());
656    let jwt_valid: Jwt = sign_credential_jwt(&credential_1, &document, &fragment, &secret_key);
657
658    let mut credential_2: Credential = create_domain_linkage_credential(document.id());
659    if let OneOrMany::One(ref mut subject) = credential_2.credential_subject {
660      subject.id = Some(Url::parse("http://invalid.did").unwrap());
661    }
662    let jwt_invalid: Jwt = sign_credential_jwt(&credential_2, &document, &fragment, &secret_key);
663
664    let configurations = vec![
665      (
666        DomainLinkageConfiguration::new(vec![jwt_valid.clone(), jwt_valid.clone()]),
667        (2, 0),
668      ),
669      (
670        DomainLinkageConfiguration::new(vec![jwt_invalid.clone(), jwt_valid.clone()]),
671        (1, 1),
672      ),
673      (
674        DomainLinkageConfiguration::new(vec![jwt_valid.clone(), jwt_invalid.clone()]),
675        (1, 1),
676      ),
677      (
678        DomainLinkageConfiguration::new(vec![jwt_invalid.clone(), jwt_invalid.clone()]),
679        (0, 2),
680      ),
681    ];
682
683    let validations: Vec<_> = configurations
684      .into_iter()
685      .map(|(configuration, expected)| {
686        let (oks, errors): (Vec<_>, Vec<_>) = JWT_DOMAIN_LINKAGE_VALIDATOR_ED25519
687          .validate_linkage_iter(
688            &document,
689            &configuration,
690            &url_foo(),
691            &JwtCredentialValidationOptions::default(),
692          )
693          .expect("validation iterator should be creatable")
694          .partition(Result::is_ok);
695        ((oks.len(), errors.len()), expected)
696      })
697      .collect();
698
699    for (index, (outcome, expected)) in validations.iter().enumerate() {
700      assert_eq!(outcome, expected, "testing result of test config {index}");
701    }
702  }
703
704  #[test]
705  pub(crate) fn test_valid_configuration() {
706    let (document, secret_key, fragment) = generate_jwk_document_with_keys();
707    let credential: Credential = create_domain_linkage_credential(document.id());
708    let jwt: Jwt = sign_credential_jwt(&credential, &document, &fragment, &secret_key);
709
710    let configuration: DomainLinkageConfiguration = DomainLinkageConfiguration::new(vec![jwt]);
711    let validation_result: DomainLinkageValidationResult = JWT_DOMAIN_LINKAGE_VALIDATOR_ED25519.validate_linkage(
712      &document,
713      &configuration,
714      &url_foo(),
715      &JwtCredentialValidationOptions::default(),
716    );
717
718    assert!(validation_result.is_ok());
719  }
720
721  fn url_foo() -> Url {
722    Url::parse("https://foo.example.com").unwrap()
723  }
724
725  fn create_domain_linkage_credential(did: &CoreDID) -> Credential {
726    let domain: Url = url_foo();
727
728    let mut domains: OrderedSet<Url> = OrderedSet::new();
729    domains.append(domain.clone());
730
731    let credential: Credential = DomainLinkageCredentialBuilder::new()
732      .issuer(did.clone())
733      .origin(domain)
734      .issuance_date(Timestamp::now_utc())
735      .expiration_date(Timestamp::now_utc().checked_add(Duration::days(365)).unwrap())
736      .build()
737      .unwrap();
738    credential
739  }
740
741  fn sign_credential_jwt(
742    credential: &Credential,
743    document: &CoreDocument,
744    fragment: &str,
745    secret_key: &SecretKey,
746  ) -> Jwt {
747    let payload: String = credential.serialize_jwt(None).unwrap();
748    Jwt::new(sign_bytes(document, fragment, payload.as_ref(), secret_key).into())
749  }
750
751  fn sign_bytes(document: &CoreDocument, fragment: &str, payload: &[u8], secret_key: &SecretKey) -> Jws {
752    let method: &VerificationMethod = document.resolve_method(fragment, None).unwrap();
753    let MethodData::PublicKeyJwk(ref jwk) = method.data() else {
754      panic!("not a jwk");
755    };
756    let alg: JwsAlgorithm = jwk.alg().unwrap_or("").parse().unwrap();
757
758    let header: JwsHeader = {
759      let mut header = JwsHeader::new();
760      header.set_alg(alg);
761      header.set_kid(method.id().to_string());
762      header
763    };
764
765    let encoding_options: CompactJwsEncodingOptions = CompactJwsEncodingOptions::NonDetached {
766      charset_requirements: CharSet::Default,
767    };
768
769    let jws_encoder: CompactJwsEncoder<'_> =
770      CompactJwsEncoder::new_with_options(payload, &header, encoding_options).unwrap();
771
772    let signature: [u8; 64] = secret_key.sign(jws_encoder.signing_input()).to_bytes();
773
774    Jws::new(jws_encoder.into_jws(&signature))
775  }
776}