identity_credential/domain_linkage/
domain_linkage_configuration.rsuse crate::credential::Jwt;
use crate::error::Result;
use crate::validator::JwtCredentialValidatorUtils;
use crate::validator::JwtValidationError;
use identity_core::common::Context;
use identity_core::common::Url;
use identity_core::convert::FmtJson;
use identity_did::CoreDID;
use once_cell::sync::Lazy;
use serde::Deserialize;
use serde::Serialize;
use std::fmt::Display;
use std::fmt::Formatter;
use crate::Error::DomainLinkageError;
static WELL_KNOWN_CONTEXT: Lazy<Context> =
Lazy::new(|| Context::Url(Url::parse("https://identity.foundation/.well-known/did-configuration/v1").unwrap()));
#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(try_from = "__DomainLinkageConfiguration")]
pub struct DomainLinkageConfiguration(__DomainLinkageConfiguration);
#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(deny_unknown_fields)]
struct __DomainLinkageConfiguration {
#[serde(rename = "@context")]
context: Context,
linked_dids: Vec<Jwt>,
}
impl __DomainLinkageConfiguration {
fn check_structure(&self) -> Result<()> {
if &self.context != DomainLinkageConfiguration::well_known_context() {
return Err(DomainLinkageError("invalid JSON-LD context".into()));
}
if self.linked_dids.is_empty() {
return Err(DomainLinkageError("empty linked_dids list".into()));
}
Ok(())
}
}
impl TryFrom<__DomainLinkageConfiguration> for DomainLinkageConfiguration {
type Error = &'static str;
fn try_from(config: __DomainLinkageConfiguration) -> Result<Self, Self::Error> {
config.check_structure()?;
Ok(Self(config))
}
}
impl Display for DomainLinkageConfiguration {
fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
self.fmt_json(f)
}
}
impl DomainLinkageConfiguration {
pub fn new(linked_dids: Vec<Jwt>) -> Self {
Self(__DomainLinkageConfiguration {
context: Self::well_known_context().clone(),
linked_dids,
})
}
pub(crate) fn well_known_context() -> &'static Context {
&WELL_KNOWN_CONTEXT
}
pub(crate) const fn domain_linkage_type() -> &'static str {
"DomainLinkageCredential"
}
pub fn linked_dids(&self) -> &Vec<Jwt> {
&self.0.linked_dids
}
pub fn issuers(&self) -> std::result::Result<Vec<CoreDID>, JwtValidationError> {
self
.0
.linked_dids
.iter()
.map(JwtCredentialValidatorUtils::extract_issuer_from_jwt::<CoreDID>)
.collect()
}
pub fn linked_dids_mut(&mut self) -> &mut Vec<Jwt> {
&mut self.0.linked_dids
}
}
#[cfg(feature = "domain-linkage-fetch")]
mod __fetch_configuration {
use crate::domain_linkage::DomainLinkageConfiguration;
use crate::error::Result;
use crate::utils::url_only_includes_origin;
use crate::Error::DomainLinkageError;
use futures::StreamExt;
use identity_core::common::Url;
use identity_core::convert::FromJson;
use reqwest::redirect::Policy;
use reqwest::Client;
impl DomainLinkageConfiguration {
pub async fn fetch_configuration(mut domain: Url) -> Result<DomainLinkageConfiguration> {
if domain.scheme() != "https" {
return Err(DomainLinkageError("domain` does not use `https` protocol".into()));
}
if !url_only_includes_origin(&domain) {
return Err(DomainLinkageError(
"domain must not include any path, query or fragment".into(),
));
}
domain.set_path(".well-known/did-configuration.json");
let client: Client = reqwest::ClientBuilder::new()
.https_only(true)
.redirect(Policy::none())
.build()
.map_err(|err| DomainLinkageError(Box::new(err)))?;
let mut stream = client
.get(domain.to_string())
.send()
.await
.map_err(|err| DomainLinkageError(Box::new(err)))?
.bytes_stream();
let mut json: Vec<u8> = Vec::new();
while let Some(item) = stream.next().await {
match item {
Ok(bytes) => {
json.extend(bytes);
if json.len() > 1_048_576 {
return Err(DomainLinkageError(
"domain linkage configuration can not exceed 1 MiB".into(),
));
}
}
Err(err) => return Err(DomainLinkageError(Box::new(err))),
}
}
let domain_linkage_configuration: DomainLinkageConfiguration =
DomainLinkageConfiguration::from_json_slice(&json).map_err(|err| DomainLinkageError(Box::new(err)))?;
Ok(domain_linkage_configuration)
}
}
}
#[cfg(test)]
mod tests {
use crate::domain_linkage::DomainLinkageConfiguration;
use identity_core::convert::FromJson;
use identity_core::error::Result;
use serde_json::json;
use serde_json::Value;
#[test]
fn test_from_json_valid() {
const JSON1: &str = include_str!("../../tests/fixtures/domain-config-valid.json");
DomainLinkageConfiguration::from_json(JSON1).unwrap();
}
#[test]
fn test_from_json_invalid_context() {
const JSON1: &str = include_str!("../../tests/fixtures/domain-config-invalid-context.json");
let deserialization_result: Result<DomainLinkageConfiguration> = DomainLinkageConfiguration::from_json(JSON1);
assert!(deserialization_result.is_err());
}
#[test]
fn test_from_json_extra_property() {
const JSON1: &str = include_str!("../../tests/fixtures/domain-config-extra-property.json");
let deserialization_result: Result<DomainLinkageConfiguration> = DomainLinkageConfiguration::from_json(JSON1);
assert!(deserialization_result.is_err());
}
#[test]
fn test_from_json_empty_linked_did() {
let json_value: Value = json!({
"@context": "https://identity.foundation/.well-known/did-configuration/v1",
"linked_dids": []
});
let deserialization_result: Result<DomainLinkageConfiguration> =
DomainLinkageConfiguration::from_json_value(json_value);
assert!(deserialization_result.is_err());
}
}