Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add domain linkage grpc #1337

Merged
merged 9 commits into from
Mar 22, 2024
4 changes: 3 additions & 1 deletion bindings/grpc/Cargo.toml
Original file line number Diff line number Diff line change
@@ -18,8 +18,9 @@ path = "src/main.rs"

[dependencies]
anyhow = "1.0.75"
futures = { version = "0.3" }
identity_eddsa_verifier = { path = "../../identity_eddsa_verifier" }
identity_iota = { path = "../../identity_iota", features = ["resolver", "sd-jwt"] }
identity_iota = { path = "../../identity_iota", features = ["resolver", "sd-jwt", "domain-linkage", "domain-linkage-fetch"] }
identity_stronghold = { path = "../../identity_stronghold", features = ["send-sync-storage"] }
iota-sdk = { version = "1.1.2", features = ["stronghold"] }
prost = "0.12"
@@ -32,6 +33,7 @@ tokio-stream = { version = "0.1.14", features = ["net"] }
tonic = "0.10"
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"] }
116 changes: 109 additions & 7 deletions bindings/grpc/README.md
Original file line number Diff line number Diff line change
@@ -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.
1 change: 0 additions & 1 deletion bindings/grpc/build.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
fn main() -> Result<(), Box<dyn std::error::Error>> {
//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"));
60 changes: 60 additions & 0 deletions bindings/grpc/proto/domain_linkage.proto
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
syntax = "proto3";
package domain_linkage;

message ValidateDomainRequest {
// domain to validate
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;
// 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;
}

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;
}

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);
}
374 changes: 374 additions & 0 deletions bindings/grpc/src/services/domain_linkage.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,374 @@
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;
use identity_iota::credential::JwtDomainLinkageValidator;
use identity_iota::credential::LinkedDomainService;
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;
use url::Origin;

#[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("domain argument invalid: {0}")]
DomainParsing(String),
#[error("did configuration argument invalid: {0}")]
DidConfigurationParsing(String),
#[error("did resolving failed: {0}")]
DidResolving(String),
}

impl From<DomainLinkageError> for tonic::Status {
fn from(value: DomainLinkageError) -> Self {
let code = match &value {
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!"); // ?

tonic::Status::with_details(code, message, error_json.into())
}
}

/// 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<Self, DomainLinkageError> {
Ok(Self {
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::DidConfigurationParsing(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 {
LinkedDidValidationStatus {
valid: false,
document: None,
error: Some(format!("{}; {}", message, &err.to_string())),
}
}

#[derive(Debug)]
pub struct DomainLinkageService {
resolver: Resolver<IotaDocument>,
}

impl DomainLinkageService {
pub fn new(client: &Client) -> Self {
let mut resolver = Resolver::new();
resolver.attach_iota_handler(client.clone());
Self { resolver }
}

/// 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,
did: &IotaDID,
did_configurations: Option<Vec<DomainValidationConfig>>,
) -> Result<Vec<LinkedDidEndpointValidationStatus>, DomainLinkageError> {
// fetch DID document for given DID
let did_document = self
.resolver
.resolve(did)
.await
.map_err(|e| DomainLinkageError::DidResolving(e.to_string()))?;

let services: Vec<LinkedDomainService> = did_document
.service()
.iter()
.cloned()
.filter_map(|service| LinkedDomainService::try_from(service).ok())
.collect();

let config_map: HashMap<Origin, DomainLinkageConfiguration> = match did_configurations {
Some(configurations) => configurations
.into_iter()
.map(|value| (value.domain.origin(), value.config))
.collect::<HashMap<Origin, DomainLinkageConfiguration>>(),
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: CoreDID = did.clone().into();
let domains: Vec<Url> = 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,
));
}
domain_futures
.try_collect::<Vec<Vec<LinkedDidValidationStatus>>>()
.await
.map(|value| LinkedDidEndpointValidationStatus {
id: service_id.to_string(),
service_endpoint: value.into_iter().flatten().collect(),
})
});
}
let endpoint_validation_status = service_futures
.try_collect::<Vec<LinkedDidEndpointValidationStatus>>()
.await?;

Ok(endpoint_validation_status)
}

/// 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<CoreDID>,
config: Option<DomainLinkageConfiguration>,
) -> Result<Vec<LinkedDidValidationStatus>, DomainLinkageError> {
// get domain linkage config
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) => {
return Ok(vec![get_validation_failed_status(
"could not get domain linkage config",
&err,
)]);
}
}
};

// get issuers of `linked_dids` credentials
let linked_dids: Vec<CoreDID> = if let Some(issuer_did) = did {
vec![issuer_did]
} else {
match domain_linkage_configuration.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<Option<String>> = resolved
.values()
.map(|issuer_did_doc| {
JwtDomainLinkageValidator::with_signature_verifier(EdDSAJwsVerifier::default())
.validate_linkage(
&issuer_did_doc,
&domain_linkage_configuration,
&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 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();

Ok(status_infos)
}
}

#[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<ValidateDomainRequest>,
) -> Result<Response<ValidateDomainResponse>, Status> {
let request_data = &req.into_inner();
// parse given domain
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
.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<ValidateDomainAgainstDidConfigurationRequest>,
) -> Result<Response<ValidateDomainResponse>, Status> {
let request_data = &req.into_inner();
// 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))
.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<ValidateDidRequest>) -> Result<Response<ValidateDidResponse>, 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<ValidateDidAgainstDidConfigurationsRequest>,
) -> Result<Response<ValidateDidResponse>, 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(DomainValidationConfig::try_parse)
.collect::<Result<Vec<DomainValidationConfig>, 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<DomainLinkageService> {
DomainLinkageServer::new(DomainLinkageService::new(client))
}
2 changes: 2 additions & 0 deletions bindings/grpc/src/services/mod.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
pub mod credential;
pub mod document;
pub mod domain_linkage;
pub mod health_check;
pub mod sd_jwt;

@@ -13,6 +14,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.add_service(document::service(client, stronghold));

routes.routes()
171 changes: 171 additions & 0 deletions bindings/grpc/tests/api/domain_linkage.rs
Original file line number Diff line number Diff line change
@@ -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<Url> = 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(())
}
1 change: 1 addition & 0 deletions bindings/grpc/tests/api/main.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
mod credential_revocation_check;
mod credential_validation;
mod did_document_creation;
mod domain_linkage;
mod health_check;
mod helpers;
mod jwt;
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"@context": "https://identity.foundation/.well-known/did-configuration/v1",
"linked_dids": [
"add your domain linkage credential here"
]
}
4 changes: 4 additions & 0 deletions bindings/grpc/tooling/start-http-server.sh
Original file line number Diff line number Diff line change
@@ -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
7 changes: 7 additions & 0 deletions bindings/grpc/tooling/start-rpc-server.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
#!/bin/sh
cd ..

API_ENDPOINT=replace_me \
STRONGHOLD_PWD=replace_me \
SNAPSHOT_PATH=replace_me \
cargo +nightly run --release
5 changes: 5 additions & 0 deletions identity_credential/src/credential/linked_domain_service.rs
Original file line number Diff line number Diff line change
@@ -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)]
2 changes: 1 addition & 1 deletion identity_credential/src/error.rs
Original file line number Diff line number Diff line change
@@ -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<dyn std::error::Error + Send + Sync + 'static>),
/// 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")]