use core::fmt;
use std::collections::BTreeMap;
use std::error::Error;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Deserializer, Serialize};
use serde_json::Value;
use url::Url;
use crate::cause::cause;
use crate::did::{Did, DidDocument, DidUrl, ResolutionError, Service, ServiceEndpoint, Uri};
use crate::jose::{DefaultVerifier, JwsHeader, JwsVerifier, Signer};
use crate::methods::web::{HttpClient, HttpError};
use crate::resolver::Resolver;
use crate::vc::validation::{
CredentialValidationOptions, DecodedCredential, JwtCredentialValidator, KeyReferenceError,
KeyResolver, ValidationError, decode_credential,
};
use crate::vc::{Credential, CredentialError, Jwt, JwtClaims, Subject};
pub const CONTEXT: &str = "https://identity.foundation/.well-known/did-configuration/v1";
pub const CREDENTIAL_TYPE: &str = "DomainLinkageCredential";
pub const WELL_KNOWN_PATH: &str = "/.well-known/did-configuration.json";
pub const SERVICE_TYPE: &str = "LinkedDomains";
const ORIGINS: &str = "origins";
pub fn configuration_url(origin: &Url) -> Result<Url, DomainLinkageError> {
let origin = normalize_origin(origin.as_str())?;
Url::parse(&format!("{origin}{WELL_KNOWN_PATH}")).map_err(|e| {
invalid_origin(
&origin,
"its well-known URL does not parse",
Some(Box::new(e)),
)
})
}
pub fn normalize_origin(origin: &str) -> Result<String, DomainLinkageError> {
let with_scheme = if has_scheme(origin) {
origin.to_owned()
} else {
format!("https://{origin}")
};
let url = Url::parse(&with_scheme)
.map_err(|e| invalid_origin(origin, "not a URL", Some(Box::new(e))))?;
if url.host_str().is_none() {
return Err(invalid_origin(origin, "no host", None));
}
let normalized = url.origin();
if !normalized.is_tuple() {
return Err(invalid_origin(origin, "not a domain origin", None));
}
let normalized = normalized.ascii_serialization();
Uri::parse(&normalized)
.map_err(|e| invalid_origin(origin, "not an RFC 3986 URI", Some(Box::new(e))))?;
Ok(normalized)
}
fn invalid_origin(
origin: &str,
reason: &str,
source: Option<Box<dyn Error + Send + Sync>>,
) -> DomainLinkageError {
DomainLinkageError::InvalidOrigin {
origin: origin.to_owned(),
reason: reason.to_owned(),
source,
}
}
fn has_scheme(s: &str) -> bool {
let Some((scheme, rest)) = s.split_once(':') else {
return false;
};
let mut chars = scheme.chars();
chars.next().is_some_and(|c| c.is_ascii_alphabetic())
&& chars.all(|c| c.is_ascii_alphanumeric() || matches!(c, '+' | '-' | '.'))
&& !rest.starts_with(|c: char| c.is_ascii_digit())
}
pub fn service(
id: DidUrl,
origins: impl IntoIterator<Item = Url>,
) -> Result<Service, DomainLinkageError> {
let invalid = |reason: String, source: Option<Box<dyn Error + Send + Sync>>| {
DomainLinkageError::InvalidService {
id: id.clone(),
reason,
source,
}
};
let origins = origins
.into_iter()
.map(|origin| {
normalize_origin(origin.as_str())
.map_err(|e| invalid(format!("origin `{origin}` is invalid"), Some(Box::new(e))))
})
.collect::<Result<Vec<_>, _>>()?;
let endpoint = match origins.as_slice() {
[] => return Err(invalid("no origin".to_owned(), None)),
[origin] => ServiceEndpoint::Uri(
Uri::parse(origin)
.map_err(|e| invalid(format!("`{origin}` is not a URI"), Some(Box::new(e))))?,
),
_ => ServiceEndpoint::Map([(ORIGINS.to_owned(), Value::from(origins))].into()),
};
Ok(Service::new(id, SERVICE_TYPE, endpoint))
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct LinkedDomainsService<'a> {
pub service: &'a Service,
pub origins: Vec<Url>,
}
impl LinkedDomainsService<'_> {
pub fn id(&self) -> &DidUrl {
&self.service.id
}
}
pub fn services(
document: &DidDocument,
) -> impl Iterator<Item = Result<LinkedDomainsService<'_>, DomainLinkageError>> {
document
.service
.iter()
.filter(|service| service.has_type(SERVICE_TYPE))
.map(linked_domains_service)
}
fn linked_domains_service(
service: &Service,
) -> Result<LinkedDomainsService<'_>, DomainLinkageError> {
let invalid = |reason: String, source: Option<Box<dyn Error + Send + Sync>>| {
DomainLinkageError::InvalidService {
id: service.id.clone(),
reason,
source,
}
};
let origins: Vec<&str> = match &service.service_endpoint {
ServiceEndpoint::Map(map) => {
let Some(Value::Array(origins)) = map.get(ORIGINS) else {
return Err(invalid(
format!("`serviceEndpoint` is a map without an `{ORIGINS}` array"),
None,
));
};
origins
.iter()
.map(|origin| {
origin
.as_str()
.ok_or_else(|| invalid(format!("origin {origin} is not a string"), None))
})
.collect::<Result<_, _>>()?
}
endpoint => endpoint.uris().map(Uri::as_str).collect(),
};
if origins.is_empty() {
return Err(invalid("no origin".to_owned(), None));
}
let origins = origins
.into_iter()
.map(|origin| {
let normalized = normalize_origin(origin)
.map_err(|e| invalid(format!("origin {origin:?} is invalid"), Some(Box::new(e))))?;
Url::parse(&normalized)
.map_err(|e| invalid(format!("origin {origin:?} is not a URL"), Some(Box::new(e))))
})
.collect::<Result<_, _>>()?;
Ok(LinkedDomainsService { service, origins })
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(untagged)]
#[non_exhaustive]
pub enum LinkedDid {
Jwt(Jwt),
LinkedDataProof(serde_json::Map<String, Value>),
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
#[non_exhaustive]
pub struct DomainLinkageConfiguration {
#[serde(rename = "@context")]
pub context: String,
pub linked_dids: Vec<LinkedDid>,
}
impl Default for DomainLinkageConfiguration {
fn default() -> Self {
Self::new()
}
}
impl DomainLinkageConfiguration {
pub fn new() -> Self {
Self {
context: CONTEXT.to_owned(),
linked_dids: Vec::new(),
}
}
#[must_use]
pub fn with_credential(mut self, credential: Jwt) -> Self {
self.linked_dids.push(LinkedDid::Jwt(credential));
self
}
pub fn jwts(&self) -> impl Iterator<Item = (usize, &Jwt)> {
self.linked_dids
.iter()
.enumerate()
.filter_map(|(i, entry)| match entry {
LinkedDid::Jwt(jwt) => Some((i, jwt)),
_ => None,
})
}
pub fn entries(
&self,
) -> impl Iterator<Item = (usize, Result<UnverifiedLinkage, DomainLinkageError>)> + '_ {
self.jwts()
.map(|(index, jwt)| (index, UnverifiedLinkage::from_jwt(jwt)))
}
pub fn earliest_expiry(&self) -> Option<DateTime<Utc>> {
self.entries()
.filter_map(|(_, entry)| entry.ok()?.expires)
.min()
}
pub fn remove_origin(&mut self, origin: &Url) -> Result<usize, DomainLinkageError> {
let origin = normalize_origin(origin.as_str())?;
let before = self.linked_dids.len();
self.linked_dids.retain(|entry| match entry {
LinkedDid::Jwt(jwt) => {
UnverifiedLinkage::from_jwt(jwt).map_or(true, |linkage| linkage.origin != origin)
}
LinkedDid::LinkedDataProof(_) => true,
});
Ok(before - self.linked_dids.len())
}
pub fn check(&self) -> Result<(), DomainLinkageError> {
if self.context != CONTEXT {
return Err(DomainLinkageError::InvalidContext(self.context.clone()));
}
Ok(())
}
}
impl<'de> Deserialize<'de> for DomainLinkageConfiguration {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct Raw {
#[serde(rename = "@context")]
context: String,
linked_dids: Vec<LinkedDid>,
}
let raw = Raw::deserialize(deserializer)?;
let configuration = Self {
context: raw.context,
linked_dids: raw.linked_dids,
};
configuration.check().map_err(serde::de::Error::custom)?;
Ok(configuration)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct UnverifiedLinkage {
pub did: Did,
pub origin: String,
pub issuer: Uri,
pub not_before: DateTime<Utc>,
pub expires: Option<DateTime<Utc>>,
}
impl UnverifiedLinkage {
pub fn from_jwt(jwt: &Jwt) -> Result<Self, DomainLinkageError> {
let credential = decode_credential(jwt)?.credential;
let subject = credential
.subject()
.ok_or(DomainLinkageError::SubjectNotDid(None))?;
Ok(Self {
did: subject_did(subject)?,
origin: subject_origin(subject)?,
issuer: credential.issuer.id().clone(),
not_before: credential.issuance_date,
expires: credential.expiration_date,
})
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct DomainLinkageCredentialBuilder {
pub did: Did,
pub origin: String,
pub issuance_date: DateTime<Utc>,
pub expiration_date: DateTime<Utc>,
}
impl DomainLinkageCredentialBuilder {
pub fn new(
did: Did,
origin: &Url,
issuance_date: DateTime<Utc>,
expiration_date: DateTime<Utc>,
) -> Result<Self, DomainLinkageError> {
Ok(Self {
did,
origin: normalize_origin(origin.as_str())?,
issuance_date,
expiration_date,
})
}
pub fn credential(&self) -> Credential {
let subject =
Subject::new(Uri::from(self.did.clone())).with_property("origin", self.origin.clone());
Credential::new(self.did.clone(), self.issuance_date, subject)
.with_context(CONTEXT)
.with_type(CREDENTIAL_TYPE)
.with_expiration_date(self.expiration_date)
}
pub fn jwt_claims(&self) -> JwtClaims {
let mut claims = JwtClaims::new()
.with_nbf(self.issuance_date)
.with_exp(self.expiration_date);
claims.iss = Some(self.did.as_str().to_owned());
claims.sub = Some(self.did.as_str().to_owned());
claims.additional.insert(
"vc".to_owned(),
serde_json::to_value(self.credential()).expect("a credential serializes"),
);
claims
}
pub async fn sign(&self, kid: &DidUrl, signer: &dyn Signer) -> Result<Jwt, DomainLinkageError> {
let header = JwsHeader::new(signer.alg()).with_kid(kid.as_str());
let jwt = Jwt::sign(&header, &self.jwt_claims(), signer)
.await
.map_err(ValidationError::from)?;
Ok(jwt)
}
}
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum DomainLinkageError {
#[error("invalid origin {origin:?}: {reason}{}", cause(.source.as_ref()))]
InvalidOrigin {
origin: String,
reason: String,
source: Option<Box<dyn Error + Send + Sync>>,
},
#[error("`@context` is {0:?}, not `{CONTEXT}`")]
InvalidContext(String),
#[error("header `typ` {0:?} must not be present")]
TypPresent(String),
#[error("header parameter `{0}` must not be present")]
UnexpectedHeaderParameter(String),
#[error("claim `{0}` must not be present")]
UnexpectedClaim(String),
#[error("claim `{0}` is required")]
MissingClaim(&'static str),
#[error("the credential must not have an `id`")]
IdPresent,
#[error("the credential's `@context` does not include `{CONTEXT}`")]
MissingContext,
#[error("the credential's `type` does not include `{CREDENTIAL_TYPE}`")]
MissingType,
#[error("the credential must have an `expirationDate`")]
MissingExpirationDate,
#[error("`credentialSubject.id` {0:?} is not a DID")]
SubjectNotDid(Option<String>),
#[error("{role} `{found}` is not the subject `{subject}`")]
SubjectMismatch {
role: &'static str,
found: String,
subject: Did,
},
#[error("`credentialSubject.origin` {0:?} is not one origin")]
InvalidOriginProperty(Option<Value>),
#[error("origin {found:?} is not {expected:?}")]
OriginMismatch {
expected: String,
found: String,
},
#[error(transparent)]
Validation(#[from] ValidationError),
#[error("the configuration holds no JWT credential for `{0}`")]
NoCredentialFor(Did),
#[error("no domain linkage credential validated: {}", format_errors(.0))]
NoValidCredential(BTreeMap<usize, DomainLinkageError>),
#[error("service `{id}`: {reason}{}", cause(.source.as_ref()))]
InvalidService {
id: DidUrl,
reason: String,
source: Option<Box<dyn Error + Send + Sync>>,
},
#[error("`{0}` is not an HTTPS origin")]
InsecureOrigin(String),
#[error("fetching `{url}` failed: {source}")]
Http {
url: Url,
#[source]
source: HttpError,
},
#[error("`{url}` publishes no DID configuration (HTTP {status})")]
NotPublished {
url: Url,
status: u16,
},
#[error("fetching `{url}` returned HTTP {status}")]
HttpStatus {
url: Url,
status: u16,
},
#[error("`{url}` is not a DID configuration resource: {source}")]
InvalidConfiguration {
url: Url,
#[source]
source: serde_json::Error,
},
#[error("resolving the DID failed: {0}")]
Resolution(#[from] ResolutionError),
#[error("the DID document lists more than {0} origins")]
TooManyOrigins(usize),
#[error("the configuration holds more than {0} credentials to validate")]
TooManyCredentials(usize),
}
impl DomainLinkageError {
pub fn is_not_published(&self) -> bool {
matches!(self, Self::NotPublished { .. })
}
}
impl From<CredentialError> for DomainLinkageError {
fn from(error: CredentialError) -> Self {
Self::Validation(error.into())
}
}
fn format_errors(errors: &BTreeMap<usize, DomainLinkageError>) -> String {
errors
.iter()
.map(|(index, error)| format!("{index}: {error}"))
.collect::<Vec<_>>()
.join("; ")
}
#[derive(Debug)]
#[non_exhaustive]
pub struct DomainLinkageReport {
pub linked: Vec<(usize, Did, DecodedCredential)>,
pub failed: BTreeMap<usize, DomainLinkageError>,
}
impl DomainLinkageReport {
pub fn dids(&self) -> impl Iterator<Item = &Did> {
self.linked.iter().map(|(_, did, _)| did)
}
}
#[derive(Debug)]
pub struct DomainLinkageValidator<R, V = DefaultVerifier> {
credentials: JwtCredentialValidator<R, V>,
max_credentials: usize,
}
impl<R, V> DomainLinkageValidator<R, V> {
pub const DEFAULT_MAX_CREDENTIALS: usize = 32;
}
impl<R: KeyResolver, V: JwsVerifier> DomainLinkageValidator<R, V> {
pub fn new(resolver: R, verifier: V) -> Self {
Self::from_credential_validator(JwtCredentialValidator::new(resolver, verifier))
}
pub fn from_credential_validator(credentials: JwtCredentialValidator<R, V>) -> Self {
Self {
credentials,
max_credentials: Self::DEFAULT_MAX_CREDENTIALS,
}
}
#[must_use]
pub fn with_max_credentials(mut self, max_credentials: usize) -> Self {
self.max_credentials = max_credentials;
self
}
pub fn max_credentials(&self) -> usize {
self.max_credentials
}
pub fn credential_validator(&self) -> &JwtCredentialValidator<R, V> {
&self.credentials
}
pub async fn validate_credential(
&self,
jwt: &Jwt,
origin: &Url,
options: &CredentialValidationOptions,
) -> Result<DecodedCredential, DomainLinkageError> {
let expected = normalize_origin(origin.as_str())?;
let decoded = decode_credential(jwt)?;
check_format(&decoded, &expected)?;
self.credentials.validate(jwt, options).await?;
Ok(decoded)
}
pub async fn validate_linkage(
&self,
configuration: &DomainLinkageConfiguration,
did: &Did,
origin: &Url,
options: &CredentialValidationOptions,
) -> Result<DecodedCredential, DomainLinkageError> {
configuration.check()?;
let mut errors = BTreeMap::new();
let mut validated = 0;
for (index, jwt) in configuration.jwts() {
if issuer_of(jwt).as_ref() != Some(did) {
continue;
}
if validated == self.max_credentials {
errors.insert(
index,
DomainLinkageError::TooManyCredentials(self.max_credentials),
);
continue;
}
validated += 1;
match self.validate_credential(jwt, origin, options).await {
Ok(decoded) => return Ok(decoded),
Err(error) => {
errors.insert(index, error);
}
}
}
if errors.is_empty() {
return Err(DomainLinkageError::NoCredentialFor(did.clone()));
}
Err(DomainLinkageError::NoValidCredential(errors))
}
pub async fn validate_configuration(
&self,
configuration: &DomainLinkageConfiguration,
origin: &Url,
options: &CredentialValidationOptions,
) -> Result<DomainLinkageReport, DomainLinkageError> {
configuration.check()?;
let mut linked = Vec::new();
let mut failed = BTreeMap::new();
for (position, (index, jwt)) in configuration.jwts().enumerate() {
if position >= self.max_credentials {
failed.insert(
index,
DomainLinkageError::TooManyCredentials(self.max_credentials),
);
continue;
}
match self.validate_credential(jwt, origin, options).await {
Ok(decoded) => {
let did = Did::parse(decoded.credential.issuer.id().as_str())
.expect("the format check accepted the issuer as a DID");
linked.push((index, did, decoded));
}
Err(error) => {
failed.insert(index, error);
}
}
}
if linked.is_empty() {
return Err(DomainLinkageError::NoValidCredential(failed));
}
Ok(DomainLinkageReport { linked, failed })
}
}
pub async fn fetch_configuration<C: HttpClient + ?Sized>(
http: &C,
origin: &Url,
) -> Result<DomainLinkageConfiguration, DomainLinkageError> {
let url = configuration_url(origin)?;
if url.scheme() != "https" {
return Err(DomainLinkageError::InsecureOrigin(
url.origin().ascii_serialization(),
));
}
let response = match http.get(&url).await {
Ok(response) => response,
Err(source) => return Err(DomainLinkageError::Http { url, source }),
};
match response.status {
200..=299 => {}
status @ (404 | 410) => return Err(DomainLinkageError::NotPublished { url, status }),
status => return Err(DomainLinkageError::HttpStatus { url, status }),
}
serde_json::from_slice(&response.body)
.map_err(|source| DomainLinkageError::InvalidConfiguration { url, source })
}
#[derive(Debug)]
#[non_exhaustive]
pub struct LinkedDomainReport {
pub service: DidUrl,
pub origin: Option<Url>,
pub result: Result<DecodedCredential, DomainLinkageError>,
}
#[derive(Debug)]
pub struct DomainLinkageClient<C, R, V = DefaultVerifier> {
http: C,
validator: DomainLinkageValidator<R, V>,
max_origins: usize,
}
impl<C, R, V> DomainLinkageClient<C, R, V> {
pub const DEFAULT_MAX_ORIGINS: usize = 32;
}
impl<C: HttpClient, R: KeyResolver, V: JwsVerifier> DomainLinkageClient<C, R, V> {
pub fn new(http: C, validator: DomainLinkageValidator<R, V>) -> Self {
Self {
http,
validator,
max_origins: Self::DEFAULT_MAX_ORIGINS,
}
}
#[must_use]
pub fn with_max_origins(mut self, max_origins: usize) -> Self {
self.max_origins = max_origins;
self
}
pub fn max_origins(&self) -> usize {
self.max_origins
}
pub fn http(&self) -> &C {
&self.http
}
pub fn validator(&self) -> &DomainLinkageValidator<R, V> {
&self.validator
}
pub async fn fetch_configuration(
&self,
origin: &Url,
) -> Result<DomainLinkageConfiguration, DomainLinkageError> {
fetch_configuration(&self.http, origin).await
}
pub async fn validate_origin(
&self,
origin: &Url,
options: &CredentialValidationOptions,
) -> Result<DomainLinkageReport, DomainLinkageError> {
let configuration = self.fetch_configuration(origin).await?;
self.validator
.validate_configuration(&configuration, origin, options)
.await
}
pub async fn validate_linkage(
&self,
did: &Did,
origin: &Url,
options: &CredentialValidationOptions,
) -> Result<DecodedCredential, DomainLinkageError> {
let configuration = self.fetch_configuration(origin).await?;
self.validator
.validate_linkage(&configuration, did, origin, options)
.await
}
pub async fn validate_document(
&self,
document: &DidDocument,
options: &CredentialValidationOptions,
) -> Vec<LinkedDomainReport> {
let mut reports = Vec::new();
let mut fetched = 0;
for service in document
.service
.iter()
.filter(|service| service.has_type(SERVICE_TYPE))
{
let linked = match linked_domains_service(service) {
Ok(linked) => linked,
Err(error) => {
reports.push(LinkedDomainReport {
service: service.id.clone(),
origin: None,
result: Err(error),
});
continue;
}
};
for origin in linked.origins {
let result = if fetched < self.max_origins {
fetched += 1;
self.validate_linkage(&document.id, &origin, options).await
} else {
Err(DomainLinkageError::TooManyOrigins(self.max_origins))
};
reports.push(LinkedDomainReport {
service: service.id.clone(),
origin: Some(origin),
result,
});
}
}
reports
}
}
impl<C: HttpClient, V: JwsVerifier> DomainLinkageClient<C, Resolver, V> {
pub async fn validate_did(
&self,
did: &Did,
options: &CredentialValidationOptions,
) -> Result<Vec<LinkedDomainReport>, DomainLinkageError> {
let document = self
.validator
.credential_validator()
.resolver()
.resolve_document(did)
.await?;
Ok(self.validate_document(&document, options).await)
}
}
fn issuer_of(jwt: &Jwt) -> Option<Did> {
crate::vc::validation::extract_issuer(jwt).ok()
}
fn check_format(
decoded: &DecodedCredential,
expected_origin: &str,
) -> Result<(), DomainLinkageError> {
let header = &decoded.header;
if let Some(typ) = &header.typ {
return Err(DomainLinkageError::TypPresent(typ.clone()));
}
if header.kid.is_none() {
return Err(ValidationError::KeyReference(KeyReferenceError::Missing).into());
}
for (name, present) in [
("cty", header.cty.is_some()),
("jwk", header.jwk.is_some()),
("crit", header.crit.is_some()),
] {
if present {
return Err(DomainLinkageError::UnexpectedHeaderParameter(
name.to_owned(),
));
}
}
if let Some(name) = header.additional.keys().next() {
return Err(DomainLinkageError::UnexpectedHeaderParameter(name.clone()));
}
let claims = &decoded.claims;
for (name, present) in [
("aud", claims.aud.is_some()),
("iat", claims.iat.is_some()),
("jti", claims.jti.is_some()),
] {
if present {
return Err(DomainLinkageError::UnexpectedClaim(name.to_owned()));
}
}
if let Some(name) = claims.additional.keys().find(|name| *name != "vc") {
return Err(DomainLinkageError::UnexpectedClaim(name.clone()));
}
let iss = claims
.iss
.as_deref()
.ok_or(DomainLinkageError::MissingClaim("iss"))?;
let sub = claims
.sub
.as_deref()
.ok_or(DomainLinkageError::MissingClaim("sub"))?;
if claims.nbf.is_none() {
return Err(DomainLinkageError::MissingClaim("nbf"));
}
if claims.exp.is_none() {
return Err(DomainLinkageError::MissingClaim("exp"));
}
let credential = &decoded.credential;
if credential.id.is_some() {
return Err(DomainLinkageError::IdPresent);
}
if !credential.context.contains(CONTEXT) {
return Err(DomainLinkageError::MissingContext);
}
if !credential.has_type(CREDENTIAL_TYPE) {
return Err(DomainLinkageError::MissingType);
}
if credential.expiration_date.is_none() {
return Err(DomainLinkageError::MissingExpirationDate);
}
let subject = credential
.subject()
.ok_or(DomainLinkageError::SubjectNotDid(None))?;
let subject_did = subject_did(subject)?;
for (role, found) in [
("issuer", credential.issuer.id().as_str()),
("iss", iss),
("sub", sub),
] {
if found != subject_did.as_str() {
return Err(DomainLinkageError::SubjectMismatch {
role,
found: found.to_owned(),
subject: subject_did,
});
}
}
let found = subject_origin(subject)?;
if found != expected_origin {
return Err(DomainLinkageError::OriginMismatch {
expected: expected_origin.to_owned(),
found,
});
}
Ok(())
}
fn subject_did(subject: &Subject) -> Result<Did, DomainLinkageError> {
let id = subject.id.as_ref().map(Uri::as_str);
id.and_then(|id| Did::parse(id).ok())
.ok_or_else(|| DomainLinkageError::SubjectNotDid(id.map(str::to_owned)))
}
fn subject_origin(subject: &Subject) -> Result<String, DomainLinkageError> {
let origin = subject.properties.get("origin");
let Some(Value::String(origin)) = origin else {
return Err(DomainLinkageError::InvalidOriginProperty(origin.cloned()));
};
normalize_origin(origin)
}
impl fmt::Display for DomainLinkageConfiguration {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let json = serde_json::to_string(self).map_err(|_| fmt::Error)?;
f.write_str(&json)
}
}