diff --git a/bindings/grpc/proto/domain_linkage.proto b/bindings/grpc/proto/domain_linkage.proto index f2fe3426df..5edee5e18a 100644 --- a/bindings/grpc/proto/domain_linkage.proto +++ b/bindings/grpc/proto/domain_linkage.proto @@ -4,6 +4,52 @@ syntax = "proto3"; package domain_linkage; +message ValidateDidResponse { + string did = 1; + message Domains { + message ValidDomain { + string url = 1; + string credential = 2; + string service_id = 3; + } + + repeated ValidDomain valid = 1; + + message InvalidDomain { + string url = 1; + optional string credential = 2; + string service_id = 3; + string error = 4; + } + + repeated InvalidDomain invalid = 2; + } + Domains domains = 2; +} + +message ValidateDomainResponse { + string domain = 1; + message LinkedDids { + message ValidDid { + string did = 1; + string credential = 2; + string service_id = 3; + } + + repeated ValidDid valid = 1; + + message InvalidDid { + optional string did = 1; + optional string credential = 2; + optional string service_id = 3; + string error = 4; + } + + repeated InvalidDid invalid = 2; + } + LinkedDids linked_dids = 2; +} + message ValidateDomainRequest { // domain to validate string domain = 1; @@ -16,27 +62,6 @@ message ValidateDomainAgainstDidConfigurationRequest { string did_configuration = 2; } -message LinkedDidValidationStatus { - // validation succeeded or not, `error` property is added for `false` cases - 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 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; @@ -49,15 +74,13 @@ message ValidateDidAgainstDidConfigurationsRequest { repeated ValidateDomainAgainstDidConfigurationRequest did_configurations = 2; } -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_domain_against_did_configuration(ValidateDomainAgainstDidConfigurationRequest) 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); + rpc validate_did_against_did_configurations( + ValidateDidAgainstDidConfigurationsRequest) returns (ValidateDidResponse); } \ No newline at end of file diff --git a/bindings/grpc/rustfmt.toml b/bindings/grpc/rustfmt.toml new file mode 100644 index 0000000000..c0842c2114 --- /dev/null +++ b/bindings/grpc/rustfmt.toml @@ -0,0 +1,8 @@ +comment_width = 120 +format_code_in_doc_comments = true +max_width = 120 +normalize_comments = false +normalize_doc_attributes = false +tab_spaces = 2 +wrap_comments = true +imports_granularity = "Item" diff --git a/bindings/grpc/src/services/domain_linkage.rs b/bindings/grpc/src/services/domain_linkage.rs index 3c3935a413..bb8b214982 100644 --- a/bindings/grpc/src/services/domain_linkage.rs +++ b/bindings/grpc/src/services/domain_linkage.rs @@ -4,18 +4,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 _domain_linkage::domain_linkage_server::DomainLinkage; +use _domain_linkage::domain_linkage_server::DomainLinkageServer; +use _domain_linkage::validate_did_response::domains::InvalidDomain; +use _domain_linkage::validate_did_response::domains::ValidDomain; +use _domain_linkage::validate_did_response::Domains; +use _domain_linkage::validate_domain_response::linked_dids::InvalidDid; +use _domain_linkage::validate_domain_response::linked_dids::ValidDid; +use _domain_linkage::validate_domain_response::LinkedDids; +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 identity_eddsa_verifier::EdDSAJwsVerifier; use identity_iota::core::FromJson; use identity_iota::core::Url; @@ -36,8 +38,7 @@ use tonic::Response; use tonic::Status; use url::Origin; -#[allow(clippy::module_inception)] -mod domain_linkage { +mod _domain_linkage { tonic::include_proto!("domain_linkage"); } @@ -90,11 +91,15 @@ impl DomainValidationConfig { } /// Builds a validation status for a failed validation from an `Error`. -fn get_validation_failed_status(message: &str, err: &impl Error) -> LinkedDidValidationStatus { - LinkedDidValidationStatus { - valid: false, - document: None, - error: Some(format!("{}; {}", message, &err.to_string())), +fn get_linked_did_validation_failed_status(message: &str, err: &impl Error) -> LinkedDids { + LinkedDids { + valid: vec![], + invalid: vec![InvalidDid { + service_id: None, + credential: None, + did: None, + error: format!("{}: {}", message, err), + }], } } @@ -121,8 +126,7 @@ impl DomainLinkageService { &self, did: &IotaDID, did_configurations: Option>, - ) -> Result, DomainLinkageError> { - // fetch DID document for given DID + ) -> Result { let did_document = self .resolver .resolve(did) @@ -144,36 +148,66 @@ impl DomainLinkageService { None => HashMap::new(), }; - // check validation for all services and endpoints in them - let mut service_futures = FuturesOrdered::new(); + let mut futures = vec![]; + for service in services { - let service_id: CoreDID = did.clone().into(); + let service_id = service.id().to_string(); 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 { - 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, - )); + for domain in domains { + let config = config_map.get(&domain.origin()).cloned(); + let service_id_clone = service_id.clone(); + futures.push({ + let domain = domain.clone(); + async move { + let result = self + .validate_domains_with_optional_configuration(&domain, Some(did.clone().into()), config) + .await; + + (service_id_clone, domain, result) + } + }); + } + } + + let results = futures::future::join_all(futures).await; + + let mut valid_domains = vec![]; + let mut invalid_domains = vec![]; + + for (service_id, domain, result) in results { + match result { + Ok(status) => { + status.valid.iter().for_each(|valid| { + valid_domains.push(ValidDomain { + service_id: service_id.to_string(), + url: domain.to_string(), + credential: valid.credential.clone(), + }); + }); + status.invalid.iter().for_each(|invalid| { + invalid_domains.push(InvalidDomain { + service_id: service_id.to_string(), + credential: invalid.credential.clone(), + url: domain.to_string(), + error: invalid.error.clone(), + }); + }); } - domain_futures - .try_collect::>>() - .await - .map(|value| LinkedDidEndpointValidationStatus { - id: service_id.to_string(), - service_endpoint: value.into_iter().flatten().collect(), - }) - }); + Err(err) => { + invalid_domains.push(InvalidDomain { + service_id: service_id.to_string(), + credential: None, + url: domain.to_string(), + error: err.to_string(), + }); + } + } } - let endpoint_validation_status = service_futures - .try_collect::>() - .await?; - Ok(endpoint_validation_status) + Ok(Domains { + valid: valid_domains, + invalid: invalid_domains, + }) } /// Validates domain linkage for given origin. @@ -186,10 +220,10 @@ impl DomainLinkageService { /// origin async fn validate_domains_with_optional_configuration( &self, - domain: Url, + domain: &Url, did: Option, config: Option, - ) -> Result, DomainLinkageError> { + ) -> Result { // get domain linkage config let domain_linkage_configuration: DomainLinkageConfiguration = if let Some(config_value) = config { config_value @@ -197,10 +231,10 @@ impl DomainLinkageService { match DomainLinkageConfiguration::fetch_configuration(domain.clone()).await { Ok(value) => value, Err(err) => { - return Ok(vec![get_validation_failed_status( + return Ok(get_linked_did_validation_failed_status( "could not get domain linkage config", &err, - )]); + )); } } }; @@ -212,10 +246,10 @@ impl DomainLinkageService { match domain_linkage_configuration.issuers() { Ok(value) => value, Err(err) => { - return Ok(vec![get_validation_failed_status( + return Ok(get_linked_did_validation_failed_status( "could not get issuers from domain linkage config credential", &err, - )]); + )); } } }; @@ -224,40 +258,51 @@ impl DomainLinkageService { let resolved = match self.resolver.resolve_multiple(&linked_dids).await { Ok(value) => value, Err(err) => { - return Ok(vec![get_validation_failed_status( + return Ok(get_linked_did_validation_failed_status( "could not resolve linked DIDs from domain linkage config", &err, - )]); + )); } }; + let mut valid_dids = vec![]; + let mut invalid_dids = vec![]; + // check linked DIDs separately - let errors: Vec> = resolved - .values() - .map(|issuer_did_doc| { - JwtDomainLinkageValidator::with_signature_verifier(EdDSAJwsVerifier::default()) + domain_linkage_configuration + .linked_dids() + .iter() + .zip(resolved.values()) + .for_each(|(credential, issuer_did_doc)| { + let id = issuer_did_doc.id().to_string(); + + if let Err(err) = JwtDomainLinkageValidator::with_signature_verifier(EdDSAJwsVerifier::default()) .validate_linkage( &issuer_did_doc, &domain_linkage_configuration, - &domain.clone(), + &domain, &JwtCredentialValidationOptions::default(), ) - .err() - .map(|err| err.to_string()) - }) - .collect(); + { + invalid_dids.push(InvalidDid { + service_id: Some(id), + credential: Some(credential.as_str().to_string()), + did: Some(issuer_did_doc.to_string()), + error: err.to_string(), + }); + } else { + valid_dids.push(ValidDid { + service_id: id, + did: issuer_did_doc.to_string(), + credential: credential.as_str().to_string(), + }); + } + }); - // collect resolved documents and their validation status into array following the order of `linked_dids` - let status_infos = domain_linkage_configuration - .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(); + let status_infos = LinkedDids { + valid: valid_dids, + invalid: invalid_dids, + }; Ok(status_infos) } @@ -282,12 +327,13 @@ impl DomainLinkage for DomainLinkageService { Url::parse(&request_data.domain).map_err(|err| DomainLinkageError::DomainParsing(err.to_string()))?; // get validation status for all issuer dids - let status_infos = self - .validate_domains_with_optional_configuration(domain, None, None) + let linked_dids = self + .validate_domains_with_optional_configuration(&domain, None, None) .await?; Ok(Response::new(ValidateDomainResponse { - linked_dids: status_infos, + domain: domain.to_string(), + linked_dids: Some(linked_dids), })) } @@ -306,18 +352,19 @@ impl DomainLinkage for DomainLinkageService { // parse given domain 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::DidConfigurationParsing(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)) + let linked_dids = self + .validate_domains_with_optional_configuration(&domain, None, Some(config)) .await?; Ok(Response::new(ValidateDomainResponse { - linked_dids: status_infos, + domain: request_data.domain.clone(), + linked_dids: Some(linked_dids), })) } @@ -332,10 +379,11 @@ impl DomainLinkage for DomainLinkageService { // 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 domains = self.validate_did_with_optional_configurations(&did, None).await?; let response = ValidateDidResponse { - service: endpoint_validation_status, + did: did.to_string(), + domains: Some(domains), }; Ok(Response::new(response)) @@ -360,12 +408,13 @@ impl DomainLinkage for DomainLinkageService { .map(DomainValidationConfig::try_parse) .collect::, DomainLinkageError>>()?; - let endpoint_validation_status = self + let domains = self .validate_did_with_optional_configurations(&did, Some(did_configurations)) .await?; let response = ValidateDidResponse { - service: endpoint_validation_status, + did: did.to_string(), + domains: Some(domains), }; Ok(Response::new(response)) diff --git a/bindings/grpc/tests/api/domain_linkage.rs b/bindings/grpc/tests/api/domain_linkage.rs index a79b732d58..4870c74a8d 100644 --- a/bindings/grpc/tests/api/domain_linkage.rs +++ b/bindings/grpc/tests/api/domain_linkage.rs @@ -1,7 +1,14 @@ // Copyright 2020-2024 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 +use _credentials::validate_did_response::Domains; +use _credentials::validate_domain_response::LinkedDids; + +use crate::domain_linkage::_credentials::validate_did_response::domains::InvalidDomain; +use crate::domain_linkage::_credentials::validate_did_response::domains::ValidDomain; +use crate::domain_linkage::_credentials::validate_domain_response::linked_dids::ValidDid; use identity_iota::core::Duration; +use identity_iota::core::FromJson; use identity_iota::core::Object; use identity_iota::core::OrderedSet; use identity_iota::core::Timestamp; @@ -13,13 +20,13 @@ use identity_iota::credential::Jwt; use identity_iota::credential::LinkedDomainService; use identity_iota::did::DIDUrl; use identity_iota::did::DID; +use identity_iota::iota::IotaDocument; 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; @@ -33,7 +40,7 @@ mod _credentials { } /// 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)> { +async fn prepare_test() -> anyhow::Result<(TestServer, Url, Url, String, Jwt)> { let stronghold = StrongholdStorage::new(make_stronghold()); let server = TestServer::new_with_stronghold(stronghold.clone()).await; let api_client = server.client(); @@ -42,9 +49,9 @@ async fn prepare_test() -> anyhow::Result<(TestServer, Url, String, Jwt)> { 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(); + .ok_or_else(|| anyhow::anyhow!("no DID document for issuer"))?; + + let did = did.id().clone(); // ===================================================== // Create Linked Domain service // ===================================================== @@ -70,6 +77,8 @@ async fn prepare_test() -> anyhow::Result<(TestServer, Url, String, Jwt)> { .document() .ok_or_else(|| anyhow::anyhow!("no DID document for issuer"))?; + let did_string = updated_did_document.to_string(); + println!("DID document with linked domain service: {updated_did_document:#}"); // ===================================================== @@ -101,12 +110,12 @@ async fn prepare_test() -> anyhow::Result<(TestServer, Url, String, Jwt)> { ) .await?; - Ok((server, domain_1, did_string, jwt)) + Ok((server, domain_1, domain_2, did_string, jwt)) } #[tokio::test] async fn can_validate_domain() -> anyhow::Result<()> { - let (server, linked_domain, _, jwt) = prepare_test().await?; + let (server, linked_domain, _, did, jwt) = prepare_test().await?; let configuration_resource: DomainLinkageConfiguration = DomainLinkageConfiguration::new(vec![jwt.clone()]); let mut grpc_client = DomainLinkageClient::connect(server.endpoint()).await?; @@ -116,15 +125,20 @@ async fn can_validate_domain() -> anyhow::Result<()> { did_configuration: configuration_resource.to_string(), }) .await?; + let did_id = IotaDocument::from_json(&did)?.id().to_string(); assert_eq!( response.into_inner(), ValidateDomainResponse { - linked_dids: vec![LinkedDidValidationStatus { - valid: true, - document: Some(jwt.as_str().to_string()), - error: None, - }], + linked_dids: Some(LinkedDids { + invalid: vec![], + valid: vec![ValidDid { + service_id: did_id, + did: did.to_string().clone(), + credential: jwt.as_str().to_string(), + }] + }), + domain: linked_domain.to_string(), } ); @@ -133,13 +147,14 @@ async fn can_validate_domain() -> anyhow::Result<()> { #[tokio::test] async fn can_validate_did() -> anyhow::Result<()> { - let (server, linked_domain, issuer_did, jwt) = prepare_test().await?; + let (server, linked_domain, domain2, 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 did_id = IotaDocument::from_json(&issuer_did)?.id().to_string(); let response = grpc_client .validate_did_against_did_configurations(ValidateDidAgainstDidConfigurationsRequest { - did: issuer_did.clone(), + did: did_id.clone(), did_configurations: vec![ValidateDomainAgainstDidConfigurationRequest { domain: linked_domain.to_string(), did_configuration: configuration_resource.to_string(), @@ -147,26 +162,31 @@ async fn can_validate_did() -> anyhow::Result<()> { }) .await?; + let service_id = format!("{}#domain-linkage", did_id); + + let valid_domain = ValidDomain { + service_id: service_id.clone(), + url: linked_domain.to_string(), + credential: jwt.as_str().to_string(), + }; + + let error = format!("could not get domain linkage config: domain linkage error: error sending request for url ({}.well-known/did-configuration.json): error trying to connect: dns error: failed to lookup address information: nodename nor servname provided, or not known", domain2.to_string()); + + let invalid_domain = InvalidDomain { + service_id: service_id.clone(), + credential: None, + url: domain2.to_string(), + error, + }; + 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()), - } - ], - } - ] + did: did_id, + domains: Some(Domains { + invalid: vec![invalid_domain], + valid: vec![valid_domain], + }), } ); diff --git a/bindings/grpc/tests/api/utils.rs b/bindings/grpc/tests/api/utils.rs index 9c863bf3de..9b320bd154 100644 --- a/bindings/grpc/tests/api/utils.rs +++ b/bindings/grpc/tests/api/utils.rs @@ -6,6 +6,7 @@ use _utils::DataSigningRequest; use identity_iota::verification::jws::JwsAlgorithm; use identity_storage::JwkStorage; use identity_storage::KeyType; +use identity_stronghold::StrongholdKeyType; use identity_stronghold::StrongholdStorage; use crate::helpers::make_stronghold; @@ -28,7 +29,9 @@ async fn raw_data_signing_works() -> anyhow::Result<()> { .key_id; let expected_signature = { - let public_key_jwk = stronghold.get_public_key(&key_id).await?; + let public_key_jwk = stronghold + .get_public_key_with_type(&key_id, StrongholdKeyType::Ed25519) + .await?; stronghold.sign(&key_id, SAMPLE_SIGNING_DATA, &public_key_jwk).await? }; 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 index 802f453e3e..579b9ee575 100644 --- 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 @@ -3,4 +3,4 @@ "linked_dids": [ "add your domain linkage credential here" ] -} \ No newline at end of file +} diff --git a/examples/1_advanced/6_domain_linkage.rs b/examples/1_advanced/6_domain_linkage.rs index 6e7a629110..03d0472a37 100644 --- a/examples/1_advanced/6_domain_linkage.rs +++ b/examples/1_advanced/6_domain_linkage.rs @@ -49,12 +49,14 @@ async fn main() -> anyhow::Result<()> { .with_primary_node(API_ENDPOINT, None)? .finish() .await?; + let stronghold_path = random_stronghold_path(); + println!("Using stronghold path: {stronghold_path:?}"); // Create a new secret manager backed by a Stronghold. let mut secret_manager: SecretManager = SecretManager::Stronghold( StrongholdSecretManager::builder() .password(Password::from("secure_password".to_owned())) - .build(random_stronghold_path())?, + .build(stronghold_path)?, ); // Create a DID for the entity that will issue the Domain Linkage Credential.