From 3f97f388c89960bd55805be9f3747289db5c0b9f Mon Sep 17 00:00:00 2001 From: Sebastian Wolfram Date: Fri, 15 Mar 2024 16:01:46 +0100 Subject: [PATCH 1/8] add domain linkage proto and implementation for domain verification --- bindings/grpc/Cargo.toml | 2 +- bindings/grpc/proto/domain_linkage.proto | 31 ++++ bindings/grpc/src/services/domain_linkage.rs | 148 +++++++++++++++++++ bindings/grpc/src/services/mod.rs | 2 + bindings/grpc/tests/api/helpers.rs | 4 +- bindings/grpc/tests/api/sd_jwt_validation.rs | 6 +- 6 files changed, 187 insertions(+), 6 deletions(-) create mode 100644 bindings/grpc/proto/domain_linkage.proto create mode 100644 bindings/grpc/src/services/domain_linkage.rs diff --git a/bindings/grpc/Cargo.toml b/bindings/grpc/Cargo.toml index 0aba12461f..5b57832f8e 100644 --- a/bindings/grpc/Cargo.toml +++ b/bindings/grpc/Cargo.toml @@ -22,7 +22,7 @@ prost = "0.12" tokio = { version = "1.0", features = ["macros", "rt-multi-thread"] } anyhow = "1.0.75" tokio-stream = { version = "0.1.14", features = ["net"] } -identity_iota = { path = "../../identity_iota", features = ["resolver", "sd-jwt"] } +identity_iota = { path = "../../identity_iota", features = ["resolver", "sd-jwt", "domain-linkage", "domain-linkage-fetch"] } identity_eddsa_verifier = { path = "../../identity_eddsa_verifier" } identity_stronghold = { path = "../../identity_stronghold", features = ["send-sync-storage"] } iota-sdk = { version = "1.1.2", features = ["stronghold"] } diff --git a/bindings/grpc/proto/domain_linkage.proto b/bindings/grpc/proto/domain_linkage.proto new file mode 100644 index 0000000000..c9b9ee602b --- /dev/null +++ b/bindings/grpc/proto/domain_linkage.proto @@ -0,0 +1,31 @@ +syntax = "proto3"; +package domain_linkage; +import "google/protobuf/empty.proto"; + +message ValidateDomainRequest { + // domain to validate + string domain = 1; +} + +message ValidateDomainLinkedDidValidationResult { + // credential from `linked_dids` as compact JWT domain linkage credential + string document = 1; + // validation succeeded or not, `error` property is added for `false` cases + bool valid = 2; + // an error message, that occurred when validated, omitted if valid + optional string error = 3; +} + +message ValidateDomainResponse { + // list of JWT domain linkage credential, uses the same order as the `did-configuration.json` file for domain + repeated ValidateDomainLinkedDidValidationResult linked_dids = 1; +} + +message ValidateDidRequest { + string did = 1; +} + +service DomainLinkage { + rpc validate_domain(ValidateDomainRequest) returns (ValidateDomainResponse); + rpc validate_did(ValidateDidRequest) returns (google.protobuf.Empty); +} \ No newline at end of file diff --git a/bindings/grpc/src/services/domain_linkage.rs b/bindings/grpc/src/services/domain_linkage.rs new file mode 100644 index 0000000000..e2cb4b05c5 --- /dev/null +++ b/bindings/grpc/src/services/domain_linkage.rs @@ -0,0 +1,148 @@ +use std::error::Error; + +use domain_linkage::domain_linkage_server::DomainLinkage; +use domain_linkage::domain_linkage_server::DomainLinkageServer; +use domain_linkage::ValidateDidRequest; +use domain_linkage::ValidateDomainLinkedDidValidationResult; +use domain_linkage::ValidateDomainRequest; +use domain_linkage::ValidateDomainResponse; +use identity_eddsa_verifier::EdDSAJwsVerifier; +use identity_iota::core::Url; +use identity_iota::credential::DomainLinkageConfiguration; +use identity_iota::credential::JwtCredentialValidationOptions; +use identity_iota::credential::JwtDomainLinkageValidator; +use identity_iota::did::CoreDID; +// use identity_iota::iota::IotaDID; +use identity_iota::iota::IotaDocument; +use identity_iota::resolver::Resolver; +use iota_sdk::client::Client; +use serde::Deserialize; +use serde::Serialize; +use thiserror::Error; +use tonic::Request; +use tonic::Response; +use tonic::Status; + +#[allow(clippy::module_inception)] +mod domain_linkage { + tonic::include_proto!("domain_linkage"); +} + +#[derive(Debug, Error, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +#[serde(tag = "error", content = "reason")] +enum DomainLinkageError { + #[error("Failed to fetch domain linkage configuration: {0}")] + DomainLinkageConfiguration(String), + #[error("domain argument invalid: {0}")] + DomainParsingFailed(String), +} + +impl From for tonic::Status { + fn from(value: DomainLinkageError) -> Self { + let code = match &value { + DomainLinkageError::DomainLinkageConfiguration(_) => tonic::Code::Internal, + DomainLinkageError::DomainParsingFailed(_) => tonic::Code::InvalidArgument, + }; + let message = value.to_string(); + let error_json = serde_json::to_vec(&value).expect("plenty of memory!"); // ? + + dbg!(&message); + tonic::Status::with_details(code, message, error_json.into()) + } +} + +#[derive(Debug)] +pub struct DomainLinkageService { + resolver: Resolver, +} + +impl DomainLinkageService { + pub fn new(client: &Client) -> Self { + let mut resolver = Resolver::new(); + resolver.attach_iota_handler(client.clone()); + Self { resolver } + } +} + +#[tonic::async_trait] +impl DomainLinkage for DomainLinkageService { + #[tracing::instrument( + name = "domain_linkage_validate_domain", + skip_all, + fields(request = ?req.get_ref()) + ret, + err, + )] + async fn validate_domain( + &self, + req: Request, + ) -> Result, Status> { + // fetch DID configuration resource + let domain: Url = Url::parse(&req.into_inner().domain.to_string()) + .map_err(|err| DomainLinkageError::DomainParsingFailed(err.to_string()))?; + let configuration_resource: DomainLinkageConfiguration = + DomainLinkageConfiguration::fetch_configuration(domain.clone()) + .await + .map_err(|err| DomainLinkageError::DomainLinkageConfiguration(err.source().unwrap_or(&err).to_string()))?; + + // get issuers of `linked_dids` credentials + let linked_dids: Vec = configuration_resource + .issuers() + .map_err(|e| Status::internal(e.to_string()))?; + + // resolve all issuers + let resolved = self + .resolver + .resolve_multiple(&linked_dids) + .await + .map_err(|e| Status::internal(e.to_string()))?; + + // check linked DIDs separately + let errors: Vec> = resolved + .values() + .map(|issuer_did_doc| { + JwtDomainLinkageValidator::with_signature_verifier(EdDSAJwsVerifier::default()) + .validate_linkage( + &issuer_did_doc, + &configuration_resource, + &domain.clone(), + &JwtCredentialValidationOptions::default(), + ) + .err() + .map(|err| err.to_string()) + }) + .collect(); + + // collect resolved documents and their validation status into array following the order of `linked_dids` + let response = ValidateDomainResponse { + linked_dids: configuration_resource + .linked_dids() + .iter() + .zip(errors.iter()) + .map(|(credential, error)| ValidateDomainLinkedDidValidationResult { + document: credential.as_str().to_string(), + valid: error.is_none(), + error: error.clone(), + }) + .collect(), + }; + + Ok(Response::new(response)) + } + + #[tracing::instrument( + name = "domain_linkage_validate_did", + skip_all, + fields(request = ?_req.get_ref()) + ret, + err, + )] + async fn validate_did(&self, _req: Request) -> Result, Status> { + todo!("implement validate_did") + } +} + +pub fn service(client: &Client) -> DomainLinkageServer { + DomainLinkageServer::new(DomainLinkageService::new(client)) +} diff --git a/bindings/grpc/src/services/mod.rs b/bindings/grpc/src/services/mod.rs index 03ca0431ee..cb34f6f231 100644 --- a/bindings/grpc/src/services/mod.rs +++ b/bindings/grpc/src/services/mod.rs @@ -1,4 +1,5 @@ pub mod credential; +pub mod domain_linkage; pub mod health_check; pub mod sd_jwt; @@ -12,6 +13,7 @@ pub fn routes(client: &Client, stronghold: &StrongholdStorage) -> Routes { routes.add_service(health_check::service()); credential::init_services(&mut routes, client, stronghold); routes.add_service(sd_jwt::service(client)); + routes.add_service(domain_linkage::service(client)); routes.routes() } diff --git a/bindings/grpc/tests/api/helpers.rs b/bindings/grpc/tests/api/helpers.rs index 6d6896cdd6..e6508d5dfb 100644 --- a/bindings/grpc/tests/api/helpers.rs +++ b/bindings/grpc/tests/api/helpers.rs @@ -35,8 +35,8 @@ use tonic::transport::Uri; pub type MemStorage = Storage; -pub static API_ENDPOINT: &str = "http://localhost:14265"; -pub static FAUCET_ENDPOINT: &str = "http://localhost:8091/api/enqueue"; +pub static API_ENDPOINT: &str = "http://localhost"; +pub static FAUCET_ENDPOINT: &str = "http://localhost/faucet/api/enqueue"; #[derive(Debug)] pub struct TestServer { diff --git a/bindings/grpc/tests/api/sd_jwt_validation.rs b/bindings/grpc/tests/api/sd_jwt_validation.rs index 9901bcddfd..f5e4a645b5 100644 --- a/bindings/grpc/tests/api/sd_jwt_validation.rs +++ b/bindings/grpc/tests/api/sd_jwt_validation.rs @@ -65,9 +65,9 @@ async fn sd_jwt_validation_works() -> anyhow::Result<()> { // Make "locality", "postal_code" and "street_address" selectively disclosable while keeping // other properties in plain text. let disclosures = vec![ - encoder.conceal(&["vc", "credentialSubject", "address", "locality"], None)?, - encoder.conceal(&["vc", "credentialSubject", "address", "postal_code"], None)?, - encoder.conceal(&["vc", "credentialSubject", "address", "street_address"], None)?, + encoder.conceal(&"vc/credentialSubject/address/locality", None)?, + encoder.conceal(&"vc/credentialSubject/address/postal_code", None)?, + encoder.conceal(&"vc/credentialSubject/address/street_address", None)?, ]; // Add the `_sd_alg` property. From 23815de7b705420d584c286d869ee51bc307a94c Mon Sep 17 00:00:00 2001 From: Sebastian Wolfram Date: Fri, 15 Mar 2024 16:01:46 +0100 Subject: [PATCH 2/8] add domain linkage verification for dids, refactor error/status handling --- bindings/grpc/Cargo.toml | 1 + bindings/grpc/proto/domain_linkage.proto | 26 ++- bindings/grpc/src/services/domain_linkage.rs | 174 ++++++++++++++----- 3 files changed, 153 insertions(+), 48 deletions(-) diff --git a/bindings/grpc/Cargo.toml b/bindings/grpc/Cargo.toml index 5b57832f8e..91c4ac13e3 100644 --- a/bindings/grpc/Cargo.toml +++ b/bindings/grpc/Cargo.toml @@ -21,6 +21,7 @@ tonic = "0.10" prost = "0.12" tokio = { version = "1.0", features = ["macros", "rt-multi-thread"] } anyhow = "1.0.75" +futures = { version = "0.3" } tokio-stream = { version = "0.1.14", features = ["net"] } identity_iota = { path = "../../identity_iota", features = ["resolver", "sd-jwt", "domain-linkage", "domain-linkage-fetch"] } identity_eddsa_verifier = { path = "../../identity_eddsa_verifier" } diff --git a/bindings/grpc/proto/domain_linkage.proto b/bindings/grpc/proto/domain_linkage.proto index c9b9ee602b..3f94b0d5ac 100644 --- a/bindings/grpc/proto/domain_linkage.proto +++ b/bindings/grpc/proto/domain_linkage.proto @@ -1,31 +1,43 @@ syntax = "proto3"; package domain_linkage; -import "google/protobuf/empty.proto"; message ValidateDomainRequest { // domain to validate string domain = 1; } -message ValidateDomainLinkedDidValidationResult { - // credential from `linked_dids` as compact JWT domain linkage credential - string document = 1; +message LinkedDidValidationStatus { // validation succeeded or not, `error` property is added for `false` cases - bool valid = 2; + bool valid = 1; + // credential from `linked_dids` as compact JWT domain linkage credential if it could be retrieved + optional string document = 2; // an error message, that occurred when validated, omitted if valid optional string error = 3; } message ValidateDomainResponse { // list of JWT domain linkage credential, uses the same order as the `did-configuration.json` file for domain - repeated ValidateDomainLinkedDidValidationResult linked_dids = 1; + repeated LinkedDidValidationStatus linked_dids = 1; +} + +message LinkedDidEndpointValidationStatus { + // id of service endpoint entry + string id = 1; + // list of JWT domain linkage credential, uses the same order as the `did-configuration.json` file for domain + repeated LinkedDidValidationStatus service_endpoint = 2; } message ValidateDidRequest { + // DID to validate string did = 1; } +message ValidateDidResponse { + // mapping of service entries from DID with validation status for endpoint URLs + repeated LinkedDidEndpointValidationStatus service = 1; +} + service DomainLinkage { rpc validate_domain(ValidateDomainRequest) returns (ValidateDomainResponse); - rpc validate_did(ValidateDidRequest) returns (google.protobuf.Empty); + rpc validate_did(ValidateDidRequest) returns (ValidateDidResponse); } \ No newline at end of file diff --git a/bindings/grpc/src/services/domain_linkage.rs b/bindings/grpc/src/services/domain_linkage.rs index e2cb4b05c5..6d8be02692 100644 --- a/bindings/grpc/src/services/domain_linkage.rs +++ b/bindings/grpc/src/services/domain_linkage.rs @@ -2,17 +2,22 @@ use std::error::Error; use domain_linkage::domain_linkage_server::DomainLinkage; use domain_linkage::domain_linkage_server::DomainLinkageServer; +use domain_linkage::LinkedDidEndpointValidationStatus; +use domain_linkage::LinkedDidValidationStatus; use domain_linkage::ValidateDidRequest; -use domain_linkage::ValidateDomainLinkedDidValidationResult; +use domain_linkage::ValidateDidResponse; use domain_linkage::ValidateDomainRequest; use domain_linkage::ValidateDomainResponse; +use futures::stream::FuturesOrdered; +use futures::TryStreamExt; use identity_eddsa_verifier::EdDSAJwsVerifier; use identity_iota::core::Url; use identity_iota::credential::DomainLinkageConfiguration; use identity_iota::credential::JwtCredentialValidationOptions; use identity_iota::credential::JwtDomainLinkageValidator; +use identity_iota::credential::LinkedDomainService; use identity_iota::did::CoreDID; -// use identity_iota::iota::IotaDID; +use identity_iota::iota::IotaDID; use identity_iota::iota::IotaDocument; use identity_iota::resolver::Resolver; use iota_sdk::client::Client; @@ -32,8 +37,6 @@ mod domain_linkage { #[serde(rename_all = "snake_case")] #[serde(tag = "error", content = "reason")] enum DomainLinkageError { - #[error("Failed to fetch domain linkage configuration: {0}")] - DomainLinkageConfiguration(String), #[error("domain argument invalid: {0}")] DomainParsingFailed(String), } @@ -41,17 +44,29 @@ enum DomainLinkageError { impl From for tonic::Status { fn from(value: DomainLinkageError) -> Self { let code = match &value { - DomainLinkageError::DomainLinkageConfiguration(_) => tonic::Code::Internal, DomainLinkageError::DomainParsingFailed(_) => tonic::Code::InvalidArgument, }; let message = value.to_string(); let error_json = serde_json::to_vec(&value).expect("plenty of memory!"); // ? - dbg!(&message); tonic::Status::with_details(code, message, error_json.into()) } } +fn get_validation_failed_status(message: &str, err: &impl Error) -> LinkedDidValidationStatus { + let source_suffix = err + .source() + .map(|err| format!("; {}", &err.to_string())) + .unwrap_or_default(); + let inner_error_message = format!("{}{}", &err.to_string(), source_suffix); + + LinkedDidValidationStatus { + valid: false, + document: None, + error: Some(format!("{}; {}", message, inner_error_message)), + } +} + #[derive(Debug)] pub struct DomainLinkageService { resolver: Resolver, @@ -81,23 +96,113 @@ impl DomainLinkage for DomainLinkageService { // fetch DID configuration resource let domain: Url = Url::parse(&req.into_inner().domain.to_string()) .map_err(|err| DomainLinkageError::DomainParsingFailed(err.to_string()))?; - let configuration_resource: DomainLinkageConfiguration = - DomainLinkageConfiguration::fetch_configuration(domain.clone()) - .await - .map_err(|err| DomainLinkageError::DomainLinkageConfiguration(err.source().unwrap_or(&err).to_string()))?; - // get issuers of `linked_dids` credentials - let linked_dids: Vec = configuration_resource - .issuers() - .map_err(|e| Status::internal(e.to_string()))?; + // get validation status for all issuer dids + let status_infos = self.validate_domains_linked_dids(domain, None).await?; - // resolve all issuers - let resolved = self + Ok(Response::new(ValidateDomainResponse { + linked_dids: status_infos, + })) + } + + #[tracing::instrument( + name = "domain_linkage_validate_did", + skip_all, + fields(request = ?req.get_ref()) + ret, + err, + )] + async fn validate_did(&self, req: Request) -> Result, Status> { + // fetch DID document for given DID + let did: IotaDID = IotaDID::parse(&req.into_inner().did).map_err(|e| Status::internal(e.to_string()))?; + let did_document = self .resolver - .resolve_multiple(&linked_dids) + .resolve(&did) .await .map_err(|e| Status::internal(e.to_string()))?; + let services: Vec = did_document + .service() + .iter() + .cloned() + .filter_map(|service| LinkedDomainService::try_from(service).ok()) + .collect(); + + // check validation for all services and endpoints in them + let mut service_futures = FuturesOrdered::new(); + for service in services { + let service_id = service.id().did().clone(); + let domains: Vec = service.domains().into(); + service_futures.push_back(async move { + let mut domain_futures = FuturesOrdered::new(); + for domain in domains { + domain_futures.push_back(self.validate_domains_linked_dids(domain.clone(), Some(service_id.clone()))); + } + domain_futures + .try_collect::>>() + .await + .map(|value| LinkedDidEndpointValidationStatus { + id: service_id.to_string(), + service_endpoint: value.into_iter().flatten().collect(), + }) + }); + } + let endpoint_validation_status = service_futures + .try_collect::>() + .await?; + + let response = ValidateDidResponse { + service: endpoint_validation_status, + }; + + Ok(Response::new(response)) + } +} + +impl DomainLinkageService { + async fn validate_domains_linked_dids( + &self, + domain: Url, + did: Option, + ) -> Result, DomainLinkageError> { + // get domain linkage config + let configuration_resource: DomainLinkageConfiguration = + match DomainLinkageConfiguration::fetch_configuration(domain.clone()).await { + Ok(value) => value, + Err(err) => { + return Ok(vec![get_validation_failed_status( + "could not get domain linkage config", + &err, + )]); + } + }; + + // get issuers of `linked_dids` credentials + let linked_dids: Vec = if let Some(issuer_did) = did { + vec![issuer_did] + } else { + match configuration_resource.issuers() { + Ok(value) => value, + Err(err) => { + return Ok(vec![get_validation_failed_status( + "could not get issuers from domain linkage config credential", + &err, + )]); + } + } + }; + + // resolve all issuers + let resolved = match self.resolver.resolve_multiple(&linked_dids).await { + Ok(value) => value, + Err(err) => { + return Ok(vec![get_validation_failed_status( + "could not resolve linked DIDs from domain linkage config", + &err, + )]); + } + }; + // check linked DIDs separately let errors: Vec> = resolved .values() @@ -115,31 +220,18 @@ impl DomainLinkage for DomainLinkageService { .collect(); // collect resolved documents and their validation status into array following the order of `linked_dids` - let response = ValidateDomainResponse { - linked_dids: configuration_resource - .linked_dids() - .iter() - .zip(errors.iter()) - .map(|(credential, error)| ValidateDomainLinkedDidValidationResult { - document: credential.as_str().to_string(), - valid: error.is_none(), - error: error.clone(), - }) - .collect(), - }; - - Ok(Response::new(response)) - } + let status_infos = configuration_resource + .linked_dids() + .iter() + .zip(errors.iter()) + .map(|(credential, error)| LinkedDidValidationStatus { + valid: error.is_none(), + document: Some(credential.as_str().to_string()), + error: error.clone(), + }) + .collect(); - #[tracing::instrument( - name = "domain_linkage_validate_did", - skip_all, - fields(request = ?_req.get_ref()) - ret, - err, - )] - async fn validate_did(&self, _req: Request) -> Result, Status> { - todo!("implement validate_did") + Ok(status_infos) } } From 7ea8abc58a7b665c6c3591a919e318c30351bec7 Mon Sep 17 00:00:00 2001 From: Sebastian Wolfram Date: Fri, 15 Mar 2024 16:11:56 +0100 Subject: [PATCH 3/8] add endpoints with pre-fetched configurations, add tests --- bindings/grpc/Cargo.toml | 1 + bindings/grpc/build.rs | 1 - bindings/grpc/proto/domain_linkage.proto | 17 ++ bindings/grpc/src/services/domain_linkage.rs | 242 +++++++++++++++---- bindings/grpc/tests/api/domain_linkage.rs | 171 +++++++++++++ bindings/grpc/tests/api/main.rs | 1 + 6 files changed, 381 insertions(+), 52 deletions(-) create mode 100644 bindings/grpc/tests/api/domain_linkage.rs diff --git a/bindings/grpc/Cargo.toml b/bindings/grpc/Cargo.toml index 91c4ac13e3..b44cf71113 100644 --- a/bindings/grpc/Cargo.toml +++ b/bindings/grpc/Cargo.toml @@ -33,6 +33,7 @@ rand = "0.8.5" serde = { version = "1.0.193", features = ["derive", "alloc"] } tracing = { version = "0.1.40", features = ["async-await"] } tracing-subscriber = "0.3.18" +url = { version = "2.5", default-features = false } [dev-dependencies] identity_storage = { path = "../../identity_storage", features = ["memstore"] } diff --git a/bindings/grpc/build.rs b/bindings/grpc/build.rs index b72c4193f6..562a01141e 100644 --- a/bindings/grpc/build.rs +++ b/bindings/grpc/build.rs @@ -1,5 +1,4 @@ fn main() -> Result<(), Box> { - //tonic_build::compile_protos("proto/helloworld.proto")?; let proto_files = std::fs::read_dir("./proto")? .filter_map(|entry| entry.ok().map(|e| e.path())) .filter(|path| path.extension().and_then(|ext| ext.to_str()) == Some("proto")); diff --git a/bindings/grpc/proto/domain_linkage.proto b/bindings/grpc/proto/domain_linkage.proto index 3f94b0d5ac..b137e86e6c 100644 --- a/bindings/grpc/proto/domain_linkage.proto +++ b/bindings/grpc/proto/domain_linkage.proto @@ -6,6 +6,13 @@ message ValidateDomainRequest { string domain = 1; } +message ValidateDomainAgainstDidConfigurationRequest { + // domain to validate + string domain = 1; + // already resolved domain linkage config + string did_configuration = 2; +} + message LinkedDidValidationStatus { // validation succeeded or not, `error` property is added for `false` cases bool valid = 1; @@ -32,6 +39,13 @@ message ValidateDidRequest { string did = 1; } +message ValidateDidAgainstDidConfigurationsRequest { + // DID to validate + string did = 1; + // already resolved domain linkage configs + repeated ValidateDomainAgainstDidConfigurationRequest did_configurations = 2; +} + message ValidateDidResponse { // mapping of service entries from DID with validation status for endpoint URLs repeated LinkedDidEndpointValidationStatus service = 1; @@ -39,5 +53,8 @@ message ValidateDidResponse { service DomainLinkage { rpc validate_domain(ValidateDomainRequest) returns (ValidateDomainResponse); + rpc validate_domain_against_did_configuration(ValidateDomainAgainstDidConfigurationRequest) returns (ValidateDomainResponse); + rpc validate_did(ValidateDidRequest) returns (ValidateDidResponse); + rpc validate_did_against_did_configurations(ValidateDidAgainstDidConfigurationsRequest) returns (ValidateDidResponse); } \ No newline at end of file diff --git a/bindings/grpc/src/services/domain_linkage.rs b/bindings/grpc/src/services/domain_linkage.rs index 6d8be02692..4144185793 100644 --- a/bindings/grpc/src/services/domain_linkage.rs +++ b/bindings/grpc/src/services/domain_linkage.rs @@ -1,16 +1,20 @@ +use std::collections::HashMap; use std::error::Error; use domain_linkage::domain_linkage_server::DomainLinkage; use domain_linkage::domain_linkage_server::DomainLinkageServer; use domain_linkage::LinkedDidEndpointValidationStatus; use domain_linkage::LinkedDidValidationStatus; +use domain_linkage::ValidateDidAgainstDidConfigurationsRequest; use domain_linkage::ValidateDidRequest; use domain_linkage::ValidateDidResponse; +use domain_linkage::ValidateDomainAgainstDidConfigurationRequest; use domain_linkage::ValidateDomainRequest; use domain_linkage::ValidateDomainResponse; use futures::stream::FuturesOrdered; use futures::TryStreamExt; use identity_eddsa_verifier::EdDSAJwsVerifier; +use identity_iota::core::FromJson; use identity_iota::core::Url; use identity_iota::credential::DomainLinkageConfiguration; use identity_iota::credential::JwtCredentialValidationOptions; @@ -27,6 +31,7 @@ use thiserror::Error; use tonic::Request; use tonic::Response; use tonic::Status; +use url::Origin; #[allow(clippy::module_inception)] mod domain_linkage { @@ -39,12 +44,18 @@ mod domain_linkage { enum DomainLinkageError { #[error("domain argument invalid: {0}")] DomainParsingFailed(String), + #[error("did configuration argument invalid: {0}")] + DidConfigurationParsingFailed(String), + #[error("did resolving failed: {0}")] + DidResolvingFailed(String), } impl From for tonic::Status { fn from(value: DomainLinkageError) -> Self { let code = match &value { DomainLinkageError::DomainParsingFailed(_) => tonic::Code::InvalidArgument, + DomainLinkageError::DidConfigurationParsingFailed(_) => tonic::Code::InvalidArgument, + DomainLinkageError::DidResolvingFailed(_) => tonic::Code::Internal, }; let message = value.to_string(); let error_json = serde_json::to_vec(&value).expect("plenty of memory!"); // ? @@ -53,6 +64,29 @@ impl From for tonic::Status { } } +/// Helper struct that allows to convert `ValidateDomainAgainstDidConfigurationRequest` input struct +/// with `String` config to a struct with `DomainLinkageService` config. +struct DomainValidationConfig { + domain: Url, + config: DomainLinkageConfiguration, +} + +impl DomainValidationConfig { + /// Parses did-configuration inputs from: + /// + /// - `validate_domain_against_did_configuration` + /// - `validate_did_against_did_configurations` + pub fn try_parse(request_config: &ValidateDomainAgainstDidConfigurationRequest) -> Result { + Ok(Self { + domain: Url::parse(&request_config.domain).map_err(|e| DomainLinkageError::DomainParsingFailed(e.to_string()))?, + config: DomainLinkageConfiguration::from_json(&request_config.did_configuration).map_err(|err| { + DomainLinkageError::DidConfigurationParsingFailed(format!("could not parse given DID configuration; {}", &err)) + })?, + }) + } +} + +/// Builds a validation status for a failed validation from an `Error`. fn get_validation_failed_status(message: &str, err: &impl Error) -> LinkedDidValidationStatus { let source_suffix = err .source() @@ -78,48 +112,25 @@ impl DomainLinkageService { resolver.attach_iota_handler(client.clone()); Self { resolver } } -} -#[tonic::async_trait] -impl DomainLinkage for DomainLinkageService { - #[tracing::instrument( - name = "domain_linkage_validate_domain", - skip_all, - fields(request = ?req.get_ref()) - ret, - err, - )] - async fn validate_domain( + /// Validates a DID' `LinkedDomains` service endpoints. Pre-fetched did-configurations can be passed to skip fetching + /// them on server. + /// + /// Arguments: + /// + /// * `did`: DID to validate + /// * `did_configurations`: A list of domains and their did-configuration, if omitted config will be fetched + async fn validate_did_with_optional_configurations( &self, - req: Request, - ) -> Result, Status> { - // fetch DID configuration resource - let domain: Url = Url::parse(&req.into_inner().domain.to_string()) - .map_err(|err| DomainLinkageError::DomainParsingFailed(err.to_string()))?; - - // get validation status for all issuer dids - let status_infos = self.validate_domains_linked_dids(domain, None).await?; - - Ok(Response::new(ValidateDomainResponse { - linked_dids: status_infos, - })) - } - - #[tracing::instrument( - name = "domain_linkage_validate_did", - skip_all, - fields(request = ?req.get_ref()) - ret, - err, - )] - async fn validate_did(&self, req: Request) -> Result, Status> { + did: &IotaDID, + did_configurations: Option>, + ) -> Result, DomainLinkageError> { // fetch DID document for given DID - let did: IotaDID = IotaDID::parse(&req.into_inner().did).map_err(|e| Status::internal(e.to_string()))?; let did_document = self .resolver - .resolve(&did) + .resolve(did) .await - .map_err(|e| Status::internal(e.to_string()))?; + .map_err(|e| DomainLinkageError::DidResolvingFailed(e.to_string()))?; let services: Vec = did_document .service() @@ -128,15 +139,29 @@ impl DomainLinkage for DomainLinkageService { .filter_map(|service| LinkedDomainService::try_from(service).ok()) .collect(); + let config_map: HashMap = match did_configurations { + Some(configurations) => configurations + .into_iter() + .map(|value| (value.domain.origin(), value.config)) + .collect::>(), + None => HashMap::new(), + }; + // check validation for all services and endpoints in them let mut service_futures = FuturesOrdered::new(); for service in services { - let service_id = service.id().did().clone(); + let service_id: CoreDID = did.clone().into(); let domains: Vec = service.domains().into(); + let local_config_map = config_map.clone(); service_futures.push_back(async move { let mut domain_futures = FuturesOrdered::new(); for domain in domains { - domain_futures.push_back(self.validate_domains_linked_dids(domain.clone(), Some(service_id.clone()))); + let config = local_config_map.get(&domain.origin()).map(|value| value.to_owned()); + domain_futures.push_back(self.validate_domains_with_optional_configuration( + domain.clone(), + Some(did.clone().into()), + config, + )); } domain_futures .try_collect::>>() @@ -151,22 +176,27 @@ impl DomainLinkage for DomainLinkageService { .try_collect::>() .await?; - let response = ValidateDidResponse { - service: endpoint_validation_status, - }; - - Ok(Response::new(response)) + Ok(endpoint_validation_status) } -} -impl DomainLinkageService { - async fn validate_domains_linked_dids( + /// Validates domain linkage for given origin. + /// + /// Arguments: + /// + /// * `domain`: An origin to validate domain linkage for + /// * `did`: A DID to restrict validation to, if omitted all DIDs from config will be validated + /// * `config`: A domain linkage configuration can be passed if already loaded, if omitted config will be fetched from + /// origin + async fn validate_domains_with_optional_configuration( &self, domain: Url, did: Option, + config: Option, ) -> Result, DomainLinkageError> { // get domain linkage config - let configuration_resource: DomainLinkageConfiguration = + let domain_linkage_configuration: DomainLinkageConfiguration = if let Some(config_value) = config { + config_value + } else { match DomainLinkageConfiguration::fetch_configuration(domain.clone()).await { Ok(value) => value, Err(err) => { @@ -175,13 +205,14 @@ impl DomainLinkageService { &err, )]); } - }; + } + }; // get issuers of `linked_dids` credentials let linked_dids: Vec = if let Some(issuer_did) = did { vec![issuer_did] } else { - match configuration_resource.issuers() { + match domain_linkage_configuration.issuers() { Ok(value) => value, Err(err) => { return Ok(vec![get_validation_failed_status( @@ -210,7 +241,7 @@ impl DomainLinkageService { JwtDomainLinkageValidator::with_signature_verifier(EdDSAJwsVerifier::default()) .validate_linkage( &issuer_did_doc, - &configuration_resource, + &domain_linkage_configuration, &domain.clone(), &JwtCredentialValidationOptions::default(), ) @@ -220,7 +251,7 @@ impl DomainLinkageService { .collect(); // collect resolved documents and their validation status into array following the order of `linked_dids` - let status_infos = configuration_resource + let status_infos = domain_linkage_configuration .linked_dids() .iter() .zip(errors.iter()) @@ -235,6 +266,115 @@ impl DomainLinkageService { } } +#[tonic::async_trait] +impl DomainLinkage for DomainLinkageService { + #[tracing::instrument( + name = "validate_domain", + skip_all, + fields(request = ?req.get_ref()) + ret, + err, + )] + async fn validate_domain( + &self, + req: Request, + ) -> Result, Status> { + let request_data = &req.into_inner(); + // parse given domain + let domain: Url = Url::parse(&request_data.domain.to_string()) + .map_err(|err| DomainLinkageError::DomainParsingFailed(err.to_string()))?; + + // get validation status for all issuer dids + let status_infos = self + .validate_domains_with_optional_configuration(domain, None, None) + .await?; + + Ok(Response::new(ValidateDomainResponse { + linked_dids: status_infos, + })) + } + + #[tracing::instrument( + name = "validate_domain_against_did_configuration", + skip_all, + fields(request = ?req.get_ref()) + ret, + err, + )] + async fn validate_domain_against_did_configuration( + &self, + req: Request, + ) -> Result, Status> { + let request_data = &req.into_inner(); + // parse given domain + let domain: Url = Url::parse(&request_data.domain.to_string()) + .map_err(|err| DomainLinkageError::DomainParsingFailed(err.to_string()))?; + // parse config + let config = DomainLinkageConfiguration::from_json(&request_data.did_configuration.to_string()).map_err(|err| { + DomainLinkageError::DidConfigurationParsingFailed(format!("could not parse given DID configuration; {}", &err)) + })?; + + // get validation status for all issuer dids + let status_infos = self + .validate_domains_with_optional_configuration(domain, None, Some(config)) + .await?; + + Ok(Response::new(ValidateDomainResponse { + linked_dids: status_infos, + })) + } + + #[tracing::instrument( + name = "validate_did", + skip_all, + fields(request = ?req.get_ref()) + ret, + err, + )] + async fn validate_did(&self, req: Request) -> Result, Status> { + // fetch DID document for given DID + let did: IotaDID = IotaDID::parse(&req.into_inner().did).map_err(|e| Status::internal(e.to_string()))?; + + let endpoint_validation_status = self.validate_did_with_optional_configurations(&did, None).await?; + + let response = ValidateDidResponse { + service: endpoint_validation_status, + }; + + Ok(Response::new(response)) + } + + #[tracing::instrument( + name = "validate_did_against_did_configurations", + skip_all, + fields(request = ?req.get_ref()) + ret, + err, + )] + async fn validate_did_against_did_configurations( + &self, + req: Request, + ) -> Result, Status> { + let request_data = &req.into_inner(); + let did: IotaDID = IotaDID::parse(&request_data.did).map_err(|e| Status::internal(e.to_string()))?; + let did_configurations = request_data + .did_configurations + .iter() + .map(|configuration| DomainValidationConfig::try_parse(&configuration)) + .collect::, DomainLinkageError>>()?; + + let endpoint_validation_status = self + .validate_did_with_optional_configurations(&did, Some(did_configurations)) + .await?; + + let response = ValidateDidResponse { + service: endpoint_validation_status, + }; + + Ok(Response::new(response)) + } +} + pub fn service(client: &Client) -> DomainLinkageServer { DomainLinkageServer::new(DomainLinkageService::new(client)) } diff --git a/bindings/grpc/tests/api/domain_linkage.rs b/bindings/grpc/tests/api/domain_linkage.rs new file mode 100644 index 0000000000..82ca873561 --- /dev/null +++ b/bindings/grpc/tests/api/domain_linkage.rs @@ -0,0 +1,171 @@ +use identity_iota::core::Duration; +use identity_iota::core::Object; +use identity_iota::core::OrderedSet; +use identity_iota::core::Timestamp; +use identity_iota::core::Url; +use identity_iota::credential::Credential; +use identity_iota::credential::DomainLinkageConfiguration; +use identity_iota::credential::DomainLinkageCredentialBuilder; +use identity_iota::credential::Jwt; +use identity_iota::credential::LinkedDomainService; +use identity_iota::did::DIDUrl; +use identity_iota::did::DID; +use identity_storage::JwkDocumentExt; +use identity_storage::JwsSignatureOptions; +use identity_stronghold::StrongholdStorage; + +use crate::domain_linkage::_credentials::domain_linkage_client::DomainLinkageClient; +use crate::domain_linkage::_credentials::LinkedDidEndpointValidationStatus; +use crate::domain_linkage::_credentials::LinkedDidValidationStatus; +use crate::domain_linkage::_credentials::ValidateDidAgainstDidConfigurationsRequest; +use crate::domain_linkage::_credentials::ValidateDidResponse; +use crate::domain_linkage::_credentials::ValidateDomainAgainstDidConfigurationRequest; +use crate::domain_linkage::_credentials::ValidateDomainResponse; +use crate::helpers::make_stronghold; +use crate::helpers::Entity; +use crate::helpers::TestServer; + +mod _credentials { + tonic::include_proto!("domain_linkage"); +} + +/// Prepares basically the same test setup as in test `examples/1_advanced/6_domain_linkage.rs`. +async fn prepare_test() -> anyhow::Result<(TestServer, Url, String, Jwt)> { + let stronghold = StrongholdStorage::new(make_stronghold()); + let server = TestServer::new_with_stronghold(stronghold.clone()).await; + let api_client = server.client(); + + let mut issuer = Entity::new_with_stronghold(stronghold); + issuer.create_did(api_client).await?; + let did = issuer + .document() + .ok_or_else(|| anyhow::anyhow!("no DID document for issuer"))? + .id(); + let did_string = did.to_string(); + // ===================================================== + // Create Linked Domain service + // ===================================================== + + // The DID should be linked to the following domains. + let domain_1: Url = Url::parse("https://foo.example.com")?; + let domain_2: Url = Url::parse("https://bar.example.com")?; + + let mut domains: OrderedSet = OrderedSet::new(); + domains.append(domain_1.clone()); + domains.append(domain_2.clone()); + + // Create a Linked Domain Service to enable the discovery of the linked domains through the DID Document. + // This is optional since it is not a hard requirement by the specs. + let service_url: DIDUrl = did.clone().join("#domain-linkage")?; + let linked_domain_service: LinkedDomainService = LinkedDomainService::new(service_url, domains, Object::new())?; + issuer + .update_document(&api_client, |mut doc| { + doc.insert_service(linked_domain_service.into()).ok().map(|_| doc) + }) + .await?; + let updated_did_document = issuer + .document() + .ok_or_else(|| anyhow::anyhow!("no DID document for issuer"))?; + + println!("DID document with linked domain service: {updated_did_document:#}"); + + // ===================================================== + // Create DID Configuration resource + // ===================================================== + + // Create the Domain Linkage Credential. + let domain_linkage_credential: Credential = DomainLinkageCredentialBuilder::new() + .issuer(updated_did_document.id().clone().into()) + .origin(domain_1.clone()) + .issuance_date(Timestamp::now_utc()) + // Expires after a year. + .expiration_date( + Timestamp::now_utc() + .checked_add(Duration::days(365)) + .ok_or_else(|| anyhow::anyhow!("calculation should not overflow"))?, + ) + .build()?; + + let jwt: Jwt = updated_did_document + .create_credential_jwt( + &domain_linkage_credential, + &issuer.storage(), + &issuer + .fragment() + .ok_or_else(|| anyhow::anyhow!("no fragment for issuer"))?, + &JwsSignatureOptions::default(), + None, + ) + .await?; + + Ok((server, domain_1, did_string, jwt)) +} + +#[tokio::test] +async fn can_validate_domain() -> anyhow::Result<()> { + let (server, linked_domain, _, jwt) = prepare_test().await?; + let configuration_resource: DomainLinkageConfiguration = DomainLinkageConfiguration::new(vec![jwt.clone()]); + let mut grpc_client = DomainLinkageClient::connect(server.endpoint()).await?; + + let response = grpc_client + .validate_domain_against_did_configuration(ValidateDomainAgainstDidConfigurationRequest { + domain: linked_domain.to_string(), + did_configuration: configuration_resource.to_string(), + }) + .await?; + + assert_eq!( + response.into_inner(), + ValidateDomainResponse { + linked_dids: vec![LinkedDidValidationStatus { + valid: true, + document: Some(jwt.as_str().to_string()), + error: None, + }], + } + ); + + Ok(()) +} + +#[tokio::test] +async fn can_validate_did() -> anyhow::Result<()> { + let (server, linked_domain, issuer_did, jwt) = prepare_test().await?; + let configuration_resource: DomainLinkageConfiguration = DomainLinkageConfiguration::new(vec![jwt.clone()]); + let mut grpc_client = DomainLinkageClient::connect(server.endpoint()).await?; + + let response = grpc_client + .validate_did_against_did_configurations(ValidateDidAgainstDidConfigurationsRequest { + did: issuer_did.clone(), + did_configurations: vec![ValidateDomainAgainstDidConfigurationRequest { + domain: linked_domain.to_string(), + did_configuration: configuration_resource.to_string(), + }], + }) + .await?; + + assert_eq!( + response.into_inner(), + ValidateDidResponse { + service: vec![ + LinkedDidEndpointValidationStatus { + id: issuer_did, + service_endpoint: vec![ + LinkedDidValidationStatus { + valid: true, + document: Some(jwt.as_str().to_string()), + error: None, + }, + LinkedDidValidationStatus { + valid: false, + document: None, + error: Some("could not get domain linkage config; domain linkage error; error sending request for url (https://bar.example.com/.well-known/did-configuration.json): error trying to connect: dns error: failed to lookup address information: nodename nor servname provided, or not known".to_string()), + } + ], + } + ] + } + ); + + Ok(()) +} diff --git a/bindings/grpc/tests/api/main.rs b/bindings/grpc/tests/api/main.rs index d195fec34a..71aacde296 100644 --- a/bindings/grpc/tests/api/main.rs +++ b/bindings/grpc/tests/api/main.rs @@ -1,4 +1,5 @@ mod credential_revocation_check; +mod domain_linkage; mod health_check; mod helpers; mod jwt; From 5306bfff29b8e9108eab952931476f55637ab802 Mon Sep 17 00:00:00 2001 From: Sebastian Wolfram Date: Fri, 15 Mar 2024 16:24:30 +0100 Subject: [PATCH 4/8] add error details to DomainLinkageError message --- bindings/grpc/src/services/domain_linkage.rs | 8 +------- bindings/grpc/tests/api/domain_linkage.rs | 2 +- identity_credential/src/error.rs | 2 +- 3 files changed, 3 insertions(+), 9 deletions(-) diff --git a/bindings/grpc/src/services/domain_linkage.rs b/bindings/grpc/src/services/domain_linkage.rs index 4144185793..1274f871b8 100644 --- a/bindings/grpc/src/services/domain_linkage.rs +++ b/bindings/grpc/src/services/domain_linkage.rs @@ -88,16 +88,10 @@ impl DomainValidationConfig { /// Builds a validation status for a failed validation from an `Error`. fn get_validation_failed_status(message: &str, err: &impl Error) -> LinkedDidValidationStatus { - let source_suffix = err - .source() - .map(|err| format!("; {}", &err.to_string())) - .unwrap_or_default(); - let inner_error_message = format!("{}{}", &err.to_string(), source_suffix); - LinkedDidValidationStatus { valid: false, document: None, - error: Some(format!("{}; {}", message, inner_error_message)), + error: Some(format!("{}; {}", message, &err.to_string())), } } diff --git a/bindings/grpc/tests/api/domain_linkage.rs b/bindings/grpc/tests/api/domain_linkage.rs index 82ca873561..6e272ccab2 100644 --- a/bindings/grpc/tests/api/domain_linkage.rs +++ b/bindings/grpc/tests/api/domain_linkage.rs @@ -159,7 +159,7 @@ async fn can_validate_did() -> anyhow::Result<()> { LinkedDidValidationStatus { valid: false, document: None, - error: Some("could not get domain linkage config; domain linkage error; error sending request for url (https://bar.example.com/.well-known/did-configuration.json): error trying to connect: dns error: failed to lookup address information: nodename nor servname provided, or not known".to_string()), + error: Some("could not get domain linkage config; domain linkage error: error sending request for url (https://bar.example.com/.well-known/did-configuration.json): error trying to connect: dns error: failed to lookup address information: nodename nor servname provided, or not known".to_string()), } ], } diff --git a/identity_credential/src/error.rs b/identity_credential/src/error.rs index 356d89d3d2..1b528f6944 100644 --- a/identity_credential/src/error.rs +++ b/identity_credential/src/error.rs @@ -35,7 +35,7 @@ pub enum Error { #[error("invalid credential status: {0}")] InvalidStatus(String), /// Caused when constructing an invalid `LinkedDomainService` or `DomainLinkageConfiguration`. - #[error("domain linkage error")] + #[error("domain linkage error; {0}")] DomainLinkageError(#[source] Box), /// Caused when attempting to encode a `Credential` containing multiple subjects as a JWT. #[error("could not create JWT claim set from verifiable credential: more than one subject")] From 7b51dcf4b14f61b6619120408bfeab2e290b2d87 Mon Sep 17 00:00:00 2001 From: Sebastian Wolfram Date: Fri, 15 Mar 2024 16:25:58 +0100 Subject: [PATCH 5/8] add getter for `id` in `LinkedDomainService` --- identity_credential/src/credential/linked_domain_service.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/identity_credential/src/credential/linked_domain_service.rs b/identity_credential/src/credential/linked_domain_service.rs index c6efbae255..3a76b10eb5 100644 --- a/identity_credential/src/credential/linked_domain_service.rs +++ b/identity_credential/src/credential/linked_domain_service.rs @@ -144,6 +144,11 @@ impl LinkedDomainService { .as_slice(), } } + + /// Returns a reference to the `Service` id. + pub fn id(&self) -> &DIDUrl { + self.service.id() + } } #[cfg(test)] From 7526ab26b7cbde62889678a4ba8c16fb6d2db5e8 Mon Sep 17 00:00:00 2001 From: Sebastian Wolfram Date: Fri, 15 Mar 2024 16:26:30 +0100 Subject: [PATCH 6/8] align error detail format --- identity_credential/src/error.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/identity_credential/src/error.rs b/identity_credential/src/error.rs index 1b528f6944..62964415e4 100644 --- a/identity_credential/src/error.rs +++ b/identity_credential/src/error.rs @@ -35,7 +35,7 @@ pub enum Error { #[error("invalid credential status: {0}")] InvalidStatus(String), /// Caused when constructing an invalid `LinkedDomainService` or `DomainLinkageConfiguration`. - #[error("domain linkage error; {0}")] + #[error("domain linkage error: {0}")] DomainLinkageError(#[source] Box), /// Caused when attempting to encode a `Credential` containing multiple subjects as a JWT. #[error("could not create JWT claim set from verifiable credential: more than one subject")] From 0beada866a71b557d2e6d6c23a810fdd80c84243 Mon Sep 17 00:00:00 2001 From: Sebastian Wolfram Date: Fri, 15 Mar 2024 17:26:52 +0100 Subject: [PATCH 7/8] update readme with test setup description and add tooling scripts/files --- bindings/grpc/README.md | 116 ++++++++++++++++-- .../.well-known/did-configuration.json | 6 + bindings/grpc/tooling/start-http-server.sh | 4 + bindings/grpc/tooling/start-rpc-server.sh | 7 ++ 4 files changed, 126 insertions(+), 7 deletions(-) create mode 100644 bindings/grpc/tooling/domain-linkage-test-server/.well-known/did-configuration.json create mode 100644 bindings/grpc/tooling/start-http-server.sh create mode 100755 bindings/grpc/tooling/start-rpc-server.sh diff --git a/bindings/grpc/README.md b/bindings/grpc/README.md index 955a7bce91..39c69632f0 100644 --- a/bindings/grpc/README.md +++ b/bindings/grpc/README.md @@ -15,11 +15,113 @@ The provided docker image requires the following variables to be set in order to Make sure to provide a valid stronghold snapshot at the provided `SNAPSHOT_PATH` prefilled with all the needed key material. ### Available services -| Service description | Service Id | Proto File | -|--------------------------------|------------------------------------------|------------| -| Credential Revocation Checking | `credentials/CredentialRevocation.check` | [credentials.proto](https://github.com/iotaledger/identity.rs/blob/grpc-bindings/bindings/grpc/proto/credentials.proto) | -| SD-JWT Validation | `sd_jwt/Verification.verify` | [sd_jwt.proto](https://github.com/iotaledger/identity.rs/blob/grpc-bindings/bindings/grpc/proto/sd_jwt.proto) | -| Credential JWT creation | `credentials/Jwt.create` | [credentials.proto](https://github.com/iotaledger/identity.rs/blob/grpc-bindings/bindings/grpc/proto/credentials.proto) | -| Credential JWT validation | `credentials/VcValidation.validate` | [credentials.proto](https://github.com/iotaledger/identity.rs/blob/grpc-bindings/bindings/grpc/proto/credentials.proto) | -| DID Document Creation | `document/DocumentService.create` | [document.proto](https://github.com/iotaledger/identity.rs/blob/grpc-bindings/bindings/grpc/proto/document.proto) | +| Service description | Service Id | Proto File | +| ------------------------------------------------------------------------------ | ------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------- | +| Credential Revocation Checking | `credentials/CredentialRevocation.check` | [credentials.proto](https://github.com/iotaledger/identity.rs/blob/grpc-bindings/bindings/grpc/proto/credentials.proto) | +| SD-JWT Validation | `sd_jwt/Verification.verify` | [sd_jwt.proto](https://github.com/iotaledger/identity.rs/blob/grpc-bindings/bindings/grpc/proto/sd_jwt.proto) | +| Credential JWT creation | `credentials/Jwt.create` | [credentials.proto](https://github.com/iotaledger/identity.rs/blob/grpc-bindings/bindings/grpc/proto/credentials.proto) | +| Credential JWT validation | `credentials/VcValidation.validate` | [credentials.proto](https://github.com/iotaledger/identity.rs/blob/grpc-bindings/bindings/grpc/proto/credentials.proto) | +| DID Document Creation | `document/DocumentService.create` | [document.proto](https://github.com/iotaledger/identity.rs/blob/grpc-bindings/bindings/grpc/proto/document.proto) | +| Domain Linkage - validate domain, let server fetch did-configuration | `domain_linkage/DomainLinkage.validate_domain` | [domain_linkage.proto](https://github.com/iotaledger/identity.rs/blob/grpc-bindings/bindings/grpc/proto/domain_linkage.proto) | +| Domain Linkage - validate domain, pass did-configuration to service | `domain_linkage/DomainLinkage.validate_domain_against_did_configuration` | [domain_linkage.proto](https://github.com/iotaledger/identity.rs/blob/grpc-bindings/bindings/grpc/proto/domain_linkage.proto) | +| Domain Linkage - validate endpoints in DID, let server fetch did-configuration | `domain_linkage/DomainLinkage.validate_did` | [domain_linkage.proto](https://github.com/iotaledger/identity.rs/blob/grpc-bindings/bindings/grpc/proto/domain_linkage.proto) | +| Domain Linkage - validate endpoints in DID, pass did-configuration to service | `domain_linkage/DomainLinkage.validate_did_against_did_configurations` | [domain_linkage.proto](https://github.com/iotaledger/identity.rs/blob/grpc-bindings/bindings/grpc/proto/domain_linkage.proto) | +## Testing +### Domain Linkage +Following is a description about how to manually test the domain linkage service. The steps for the other services might vary a bit. + +#### Http server +If you want to test domain linkage, you need a server, that's reachable via HTTPS. If you already have one, ignore the server setup steps here and just make sure your server provides the `did-configuration.json` file as described here. + +- create test server folder with did configuration in it, e.g. (you can also use the template in `./tooling/domain-linkage-test-server`) + ```raw + test-server/ + └── .well-known + └── did-configuration.json + ``` + + `did-configuration` looks like this for now: + + ```json + { + "@context": "https://identity.foundation/.well-known/did-configuration/v1", + "linked_dids": [ + "add your domain linkage credential here" + ] + } + ``` +- start a server, that will serve this folder, e.g. with a "http-server" from NodeJs : `http-server ./test-server/`, in this example the server should now be running on local port 8080 +- now tunnel your server's port (here 8080) to a public domain with https, e.g. with ngrok: + `ngrok http http://127.0.0.1:8080` + the output should now have a line like + `Forwarding https://0d40-2003-d3-2710-e200-485f-e8bb-7431-79a7.ngrok-free.app -> http://127.0.0.1:8080` + check that the https url is reachable, this will be used in the next step. you can also start ngrok with a static domain, that you do not have to update credentials after each http server restart +- for convenience, you can find a script to start the HTTP server, that you can adjust in `tooling/start-http-server.sh`, don't forget to insert your static domain or to remove the `--domain` parameter + +#### Domain linkage credential +- copy this public url and insert it into the advanced test 6 (the one for domain linkage) as domain 1, e.g. `let domain_1: Url = Url::parse("https://0d40-2003-d3-2710-e200-485f-e8bb-7431-79a7.ngrok-free.app")?;` +- run the example with `cargo run --release --example 6_domain_linkage` + +#### GRPC server +- grab the configuration resource from the log and replace the contents of your `did-configuration.json` with it +- you now have a publicly reachable (sub)domain, that serves a `did-configuration` file containing a credential pointing to your DID +- to verify this, run the server via Docker or with the following command, remember to replace the placeholders ;) `API_ENDPOINT=replace_me STRONGHOLD_PWD=replace_me SNAPSHOT_PATH=replace_me cargo run --release`, arguments can be taken from examples, e.g. after running a `6_domain_linkage.rs`, that also logs snapshot path passed to secret manager (`let snapshot_path = random_stronghold_path(); dbg!(&snapshot_path.to_str());`), for example + - API_ENDPOINT: `"http://localhost"` + - STRONGHOLD_PWD: `"secure_password"` + - SNAPSHOT_PATH: `"/var/folders/41/s1sm86jx0xl4x435t81j81440000gn/T/test_strongholds/8o2Nyiv5ENBi7Ik3dEDq9gNzSrqeUdqi.stronghold"` +- for convenience, you can find a script to start the GRPC server, that you can adjust in `tooling/start-rpc-server.sh`, don't forget to insert the env variables as described above + +#### Calling the endpoints +- call the `validate_domain` endpoint with your domain, e.g with: + + ```json + { + "domain": "https://0d40-2003-d3-2710-e200-485f-e8bb-7431-79a7.ngrok-free.app" + } + ``` + + you should now receive a response like this: + + ```json + { + "linked_dids": [ + { + "document": "... (compact JWT domain linkage credential)", + "status": "ok" + } + ] + } + ``` + +- to call the `validate_did` endpoint, you need a DID to check, you can find a testable in you domain linkage credential. for this just decode it (e.g. on jwt.io) and get the `iss` value, then you can submit as "did" like following + + ```json + { + "did": "did:iota:snd:0x967bf8f0c7487f61378611b6a1c6a59cb99e65b839681ee70be691b09a024ab9" + } + ``` + + you should not receive a response like this: + + ```json + { + "service": [ + { + "service_endpoint": [ + { + "valid": true, + "document": "eyJraWQiOiJkaWQ6aW90YTpzbmQ6MHg5NjdiZjhmMGM3NDg3ZjYxMzc4NjExYjZhMWM2YTU5Y2I5OWU2NWI4Mzk2ODFlZTcwYmU2OTFiMDlhMDI0YWI5IzA3QjVWRkxBa0FabkRhaC1OTnYwYUN3TzJ5ZnRzX09ZZ0YzNFNudUloMlUiLCJ0eXAiOiJKV1QiLCJhbGciOiJFZERTQSJ9.eyJleHAiOjE3NDE2NzgyNzUsImlzcyI6ImRpZDppb3RhOnNuZDoweDk2N2JmOGYwYzc0ODdmNjEzNzg2MTFiNmExYzZhNTljYjk5ZTY1YjgzOTY4MWVlNzBiZTY5MWIwOWEwMjRhYjkiLCJuYmYiOjE3MTAxNDIyNzUsInN1YiI6ImRpZDppb3RhOnNuZDoweDk2N2JmOGYwYzc0ODdmNjEzNzg2MTFiNmExYzZhNTljYjk5ZTY1YjgzOTY4MWVlNzBiZTY5MWIwOWEwMjRhYjkiLCJ2YyI6eyJAY29udGV4dCI6WyJodHRwczovL3d3dy53My5vcmcvMjAxOC9jcmVkZW50aWFscy92MSIsImh0dHBzOi8vaWRlbnRpdHkuZm91bmRhdGlvbi8ud2VsbC1rbm93bi9kaWQtY29uZmlndXJhdGlvbi92MSJdLCJ0eXBlIjpbIlZlcmlmaWFibGVDcmVkZW50aWFsIiwiRG9tYWluTGlua2FnZUNyZWRlbnRpYWwiXSwiY3JlZGVudGlhbFN1YmplY3QiOnsib3JpZ2luIjoiaHR0cHM6Ly9ob3QtYnVsbGRvZy1wcm9mb3VuZC5uZ3Jvay1mcmVlLmFwcC8ifX19.69e7T0DbRw9Kz7eEQ96P9E5HWbEo5F1fLuMjyQN6_Oa1lwBdbfj0wLlhS1j_d8AuNmvu60lMdLVixjMZJLQ5AA" + }, + { + "valid": false, + "error": "domain linkage error: error sending request for url (https://bar.example.com/.well-known/did-configuration.json): error trying to connect: dns error: failed to lookup address information: nodename nor servname provided, or not known" + } + ], + "id": "did:iota:snd:0x967bf8f0c7487f61378611b6a1c6a59cb99e65b839681ee70be691b09a024ab9" + } + ] + } + ``` + + Which tells us that it found a DID document with one matching service with a serviceEndpoint, that contains two domains. Out of these domains one links back to the given DID, the other domain could not be resolved. diff --git a/bindings/grpc/tooling/domain-linkage-test-server/.well-known/did-configuration.json b/bindings/grpc/tooling/domain-linkage-test-server/.well-known/did-configuration.json new file mode 100644 index 0000000000..802f453e3e --- /dev/null +++ b/bindings/grpc/tooling/domain-linkage-test-server/.well-known/did-configuration.json @@ -0,0 +1,6 @@ +{ + "@context": "https://identity.foundation/.well-known/did-configuration/v1", + "linked_dids": [ + "add your domain linkage credential here" + ] +} \ No newline at end of file diff --git a/bindings/grpc/tooling/start-http-server.sh b/bindings/grpc/tooling/start-http-server.sh new file mode 100644 index 0000000000..4cebbf82d2 --- /dev/null +++ b/bindings/grpc/tooling/start-http-server.sh @@ -0,0 +1,4 @@ +#!/bin/sh +http-server ./domain-linkage-test-server & +# replace or omint the --domain parameter if you don't have a static domain or don't want to use it +ngrok http --domain=example-static-domain.ngrok-free.app 8080 \ No newline at end of file diff --git a/bindings/grpc/tooling/start-rpc-server.sh b/bindings/grpc/tooling/start-rpc-server.sh new file mode 100755 index 0000000000..69c207f6cf --- /dev/null +++ b/bindings/grpc/tooling/start-rpc-server.sh @@ -0,0 +1,7 @@ +#!/bin/sh +cd .. + +API_ENDPOINT=replace_me \ +STRONGHOLD_PWD=replace_me \ +SNAPSHOT_PATH=replace_me \ +cargo +nightly run --release From 755817ade82c1cb150c7afae2212a24035546e2d Mon Sep 17 00:00:00 2001 From: Sebastian Wolfram Date: Mon, 18 Mar 2024 09:40:33 +0100 Subject: [PATCH 8/8] fix clippy suggestions --- bindings/grpc/src/services/domain_linkage.rs | 32 ++++++++++---------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/bindings/grpc/src/services/domain_linkage.rs b/bindings/grpc/src/services/domain_linkage.rs index 1274f871b8..fc62e11425 100644 --- a/bindings/grpc/src/services/domain_linkage.rs +++ b/bindings/grpc/src/services/domain_linkage.rs @@ -43,19 +43,19 @@ mod domain_linkage { #[serde(tag = "error", content = "reason")] enum DomainLinkageError { #[error("domain argument invalid: {0}")] - DomainParsingFailed(String), + DomainParsing(String), #[error("did configuration argument invalid: {0}")] - DidConfigurationParsingFailed(String), + DidConfigurationParsing(String), #[error("did resolving failed: {0}")] - DidResolvingFailed(String), + DidResolving(String), } impl From for tonic::Status { fn from(value: DomainLinkageError) -> Self { let code = match &value { - DomainLinkageError::DomainParsingFailed(_) => tonic::Code::InvalidArgument, - DomainLinkageError::DidConfigurationParsingFailed(_) => tonic::Code::InvalidArgument, - DomainLinkageError::DidResolvingFailed(_) => tonic::Code::Internal, + DomainLinkageError::DomainParsing(_) => tonic::Code::InvalidArgument, + DomainLinkageError::DidConfigurationParsing(_) => tonic::Code::InvalidArgument, + DomainLinkageError::DidResolving(_) => tonic::Code::Internal, }; let message = value.to_string(); let error_json = serde_json::to_vec(&value).expect("plenty of memory!"); // ? @@ -78,9 +78,9 @@ impl DomainValidationConfig { /// - `validate_did_against_did_configurations` pub fn try_parse(request_config: &ValidateDomainAgainstDidConfigurationRequest) -> Result { Ok(Self { - domain: Url::parse(&request_config.domain).map_err(|e| DomainLinkageError::DomainParsingFailed(e.to_string()))?, + domain: Url::parse(&request_config.domain).map_err(|e| DomainLinkageError::DomainParsing(e.to_string()))?, config: DomainLinkageConfiguration::from_json(&request_config.did_configuration).map_err(|err| { - DomainLinkageError::DidConfigurationParsingFailed(format!("could not parse given DID configuration; {}", &err)) + DomainLinkageError::DidConfigurationParsing(format!("could not parse given DID configuration; {}", &err)) })?, }) } @@ -124,7 +124,7 @@ impl DomainLinkageService { .resolver .resolve(did) .await - .map_err(|e| DomainLinkageError::DidResolvingFailed(e.to_string()))?; + .map_err(|e| DomainLinkageError::DidResolving(e.to_string()))?; let services: Vec = did_document .service() @@ -275,8 +275,8 @@ impl DomainLinkage for DomainLinkageService { ) -> Result, Status> { let request_data = &req.into_inner(); // parse given domain - let domain: Url = Url::parse(&request_data.domain.to_string()) - .map_err(|err| DomainLinkageError::DomainParsingFailed(err.to_string()))?; + let domain: Url = Url::parse(&request_data.domain) + .map_err(|err| DomainLinkageError::DomainParsing(err.to_string()))?; // get validation status for all issuer dids let status_infos = self @@ -301,11 +301,11 @@ impl DomainLinkage for DomainLinkageService { ) -> Result, Status> { let request_data = &req.into_inner(); // parse given domain - let domain: Url = Url::parse(&request_data.domain.to_string()) - .map_err(|err| DomainLinkageError::DomainParsingFailed(err.to_string()))?; + let domain: Url = Url::parse(&request_data.domain) + .map_err(|err| DomainLinkageError::DomainParsing(err.to_string()))?; // parse config let config = DomainLinkageConfiguration::from_json(&request_data.did_configuration.to_string()).map_err(|err| { - DomainLinkageError::DidConfigurationParsingFailed(format!("could not parse given DID configuration; {}", &err)) + DomainLinkageError::DidConfigurationParsing(format!("could not parse given DID configuration; {}", &err)) })?; // get validation status for all issuer dids @@ -327,7 +327,7 @@ impl DomainLinkage for DomainLinkageService { )] async fn validate_did(&self, req: Request) -> Result, Status> { // fetch DID document for given DID - let did: IotaDID = IotaDID::parse(&req.into_inner().did).map_err(|e| Status::internal(e.to_string()))?; + let did: IotaDID = IotaDID::parse(req.into_inner().did).map_err(|e| Status::internal(e.to_string()))?; let endpoint_validation_status = self.validate_did_with_optional_configurations(&did, None).await?; @@ -354,7 +354,7 @@ impl DomainLinkage for DomainLinkageService { let did_configurations = request_data .did_configurations .iter() - .map(|configuration| DomainValidationConfig::try_parse(&configuration)) + .map(DomainValidationConfig::try_parse) .collect::, DomainLinkageError>>()?; let endpoint_validation_status = self