From 61fb522f2cf9b936e574ef6691724fa04a4f2b52 Mon Sep 17 00:00:00 2001 From: Nander Stabel Date: Wed, 11 Sep 2024 15:23:09 +0200 Subject: [PATCH] feat: add `validate_linked_verifiable_presentations` --- Cargo.lock | 42 +- Cargo.toml | 3 +- identity-wallet/Cargo.toml | 2 + .../bindings/user_prompt/CurrentUserPrompt.ts | 3 +- .../LinkedVerifiableCredentialData.ts | 3 + identity-wallet/src/state/did/mod.rs | 4 +- .../src/state/did/validate_domain_linkage.rs | 4 +- ...alidate_linked_verifiable_presentations.rs | 870 ++++++++++++++++++ .../did/validate_thuiswinkel_waarborg.rs | 157 ---- .../reducers/read_authorization_request.rs | 15 +- identity-wallet/src/state/user_prompt.rs | 8 +- identity-wallet/src/subject.rs | 37 +- unime/src-tauri/tests/common/mod.rs | 21 +- .../fixtures/states/accept_connection.json | 4 +- .../prompt/accept-connection/+page.svelte | 38 +- 15 files changed, 982 insertions(+), 229 deletions(-) create mode 100644 identity-wallet/bindings/user_prompt/LinkedVerifiableCredentialData.ts create mode 100644 identity-wallet/src/state/did/validate_linked_verifiable_presentations.rs delete mode 100644 identity-wallet/src/state/did/validate_thuiswinkel_waarborg.rs diff --git a/Cargo.lock b/Cargo.lock index d1133a100..fb38b3930 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1316,7 +1316,7 @@ checksum = "f7144d30dcf0fafbce74250a3963025d8d52177934239851c917d29f1df280c2" [[package]] name = "consumer" version = "0.1.0" -source = "git+https://github.com/impierce/did-manager.git?rev=2b88f55#2b88f55b8f5e42aa794a701c5fe11bc962250139" +source = "git+https://git@github.com/impierce/did-manager.git?tag=v1.0.0-beta.3#3ad5e3dba7bc76df8d6cb4a4fd2df2238d88710b" dependencies = [ "did_iota", "did_jwk", @@ -1929,7 +1929,7 @@ dependencies = [ [[package]] name = "did_iota" version = "0.1.0" -source = "git+https://github.com/impierce/did-manager.git?rev=2b88f55#2b88f55b8f5e42aa794a701c5fe11bc962250139" +source = "git+https://git@github.com/impierce/did-manager.git?tag=v1.0.0-beta.3#3ad5e3dba7bc76df8d6cb4a4fd2df2238d88710b" dependencies = [ "bls12_381_plus 0.8.15", "identity_iota", @@ -1943,7 +1943,7 @@ dependencies = [ [[package]] name = "did_jwk" version = "0.1.0" -source = "git+https://github.com/impierce/did-manager.git?rev=2b88f55#2b88f55b8f5e42aa794a701c5fe11bc962250139" +source = "git+https://git@github.com/impierce/did-manager.git?tag=v1.0.0-beta.3#3ad5e3dba7bc76df8d6cb4a4fd2df2238d88710b" dependencies = [ "did-jwk", "identity_iota", @@ -1960,7 +1960,7 @@ dependencies = [ [[package]] name = "did_key" version = "0.1.0" -source = "git+https://github.com/impierce/did-manager.git?rev=2b88f55#2b88f55b8f5e42aa794a701c5fe11bc962250139" +source = "git+https://git@github.com/impierce/did-manager.git?tag=v1.0.0-beta.3#3ad5e3dba7bc76df8d6cb4a4fd2df2238d88710b" dependencies = [ "did-method-key", "identity_iota", @@ -1978,7 +1978,7 @@ dependencies = [ [[package]] name = "did_manager" version = "0.1.0" -source = "git+https://github.com/impierce/did-manager.git?rev=2b88f55#2b88f55b8f5e42aa794a701c5fe11bc962250139" +source = "git+https://git@github.com/impierce/did-manager.git?tag=v1.0.0-beta.3#3ad5e3dba7bc76df8d6cb4a4fd2df2238d88710b" dependencies = [ "consumer", "producer", @@ -2006,7 +2006,7 @@ dependencies = [ [[package]] name = "did_web" version = "0.1.0" -source = "git+https://github.com/impierce/did-manager.git?rev=2b88f55#2b88f55b8f5e42aa794a701c5fe11bc962250139" +source = "git+https://git@github.com/impierce/did-manager.git?tag=v1.0.0-beta.3#3ad5e3dba7bc76df8d6cb4a4fd2df2238d88710b" dependencies = [ "did-web", "identity_iota", @@ -4051,9 +4051,11 @@ dependencies = [ "dyn-clone", "futures", "icu", + "identity_core", "identity_credential", "identity_eddsa_verifier", "identity_iota", + "identity_jose", "iota_stronghold", "itertools 0.10.5", "jsonwebtoken", @@ -4069,7 +4071,7 @@ dependencies = [ "serial_test", "sha256", "stronghold_engine", - "stronghold_ext", + "stronghold_ext 0.1.0 (git+https://github.com/tensor-programming/stronghold_ext)", "strum", "tauri", "tempfile", @@ -4282,7 +4284,7 @@ dependencies = [ [[package]] name = "identity_stronghold_ext" version = "0.1.0" -source = "git+https://github.com/impierce/did-manager.git?rev=2b88f55#2b88f55b8f5e42aa794a701c5fe11bc962250139" +source = "git+https://git@github.com/impierce/did-manager.git?tag=v1.0.0-beta.3#3ad5e3dba7bc76df8d6cb4a4fd2df2238d88710b" dependencies = [ "async-trait", "elliptic-curve 0.13.8", @@ -4294,7 +4296,7 @@ dependencies = [ "log", "p256 0.13.2", "serde_json", - "stronghold_ext", + "stronghold_ext 0.1.0 (git+https://github.com/impierce/stronghold_ext.git)", "tokio", ] @@ -6801,7 +6803,7 @@ dependencies = [ [[package]] name = "producer" version = "0.1.0" -source = "git+https://github.com/impierce/did-manager.git?rev=2b88f55#2b88f55b8f5e42aa794a701c5fe11bc962250139" +source = "git+https://git@github.com/impierce/did-manager.git?tag=v1.0.0-beta.3#3ad5e3dba7bc76df8d6cb4a4fd2df2238d88710b" dependencies = [ "did_iota", "did_jwk", @@ -8098,7 +8100,7 @@ dependencies = [ [[package]] name = "shared" version = "0.1.0" -source = "git+https://github.com/impierce/did-manager.git?rev=2b88f55#2b88f55b8f5e42aa794a701c5fe11bc962250139" +source = "git+https://git@github.com/impierce/did-manager.git?tag=v1.0.0-beta.3#3ad5e3dba7bc76df8d6cb4a4fd2df2238d88710b" dependencies = [ "identity_iota", "identity_storage", @@ -8688,6 +8690,24 @@ dependencies = [ "zeroize", ] +[[package]] +name = "stronghold_ext" +version = "0.1.0" +source = "git+https://github.com/impierce/stronghold_ext.git#cad0e5ac4d9011a38c88303b668e790e2f2f3a5e" +dependencies = [ + "ecdsa 0.16.9", + "iota_stronghold", + "k256", + "p256 0.13.2", + "rand 0.8.5", + "serde", + "sha2 0.10.8", + "stronghold-utils", + "stronghold_engine", + "thiserror", + "zeroize", +] + [[package]] name = "stronghold_ext" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index 2c45aa796..72074e26d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,7 +25,8 @@ tauri-runtime-wry = { version = "=2.0.0-beta.20" } tauri-utils = { version = "=2.0.0-beta.19", features = [ "resources" ] } tauri-winres = "=0.1" -did_manager = { git = "https://github.com/impierce/did-manager.git", rev = "2b88f55" } +agent_shared = { git = "https://git@github.com/impierce/ssi-agent.git", rev = "1823810" } +did_manager = { git = "https://git@github.com/impierce/did-manager.git", tag = "v1.0.0-beta.3" } jsonwebtoken = "9.3" log = "^0.4" oid4vc = { git = "https://git@github.com/impierce/openid4vc.git", rev = "d095db0" } diff --git a/identity-wallet/Cargo.toml b/identity-wallet/Cargo.toml index ea1aca5ba..85cfc14a2 100644 --- a/identity-wallet/Cargo.toml +++ b/identity-wallet/Cargo.toml @@ -23,8 +23,10 @@ identity_credential = { version = "1.3", default-features = false, features = [ "presentation", "validator", ] } +identity_core = { version = "1.3" } identity_eddsa_verifier = { version = "1.3" } identity_iota = { version = "1.3" } +identity_jose = { version = "1.3" } iota_stronghold = { version = "2.1" } itertools = "0.10.5" jsonwebtoken.workspace = true diff --git a/identity-wallet/bindings/user_prompt/CurrentUserPrompt.ts b/identity-wallet/bindings/user_prompt/CurrentUserPrompt.ts index 38c5e51bf..f7e7b9976 100644 --- a/identity-wallet/bindings/user_prompt/CurrentUserPrompt.ts +++ b/identity-wallet/bindings/user_prompt/CurrentUserPrompt.ts @@ -1,4 +1,5 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { LinkedVerifiableCredentialData } from "./LinkedVerifiableCredentialData"; import type { ValidationResult } from "./ValidationResult"; -export type CurrentUserPrompt = { "type": "redirect", target: string, } | { "type": "password-required" } | { "type": "accept-connection", client_name: string, logo_uri?: string, redirect_uri: string, previously_connected: boolean, domain_validation: ValidationResult, thuiswinkel_validation: ValidationResult, } | { "type": "credential-offer", issuer_name: string, logo_uri?: string, credential_configurations: Record, } | { "type": "share-credentials", client_name: string, logo_uri?: string, options: Array, }; +export type CurrentUserPrompt = { "type": "redirect", target: string, } | { "type": "password-required" } | { "type": "accept-connection", client_name: string, logo_uri?: string, redirect_uri: string, previously_connected: boolean, domain_validation: ValidationResult, linked_verifiable_presentations: Array, } | { "type": "credential-offer", issuer_name: string, logo_uri?: string, credential_configurations: Record, } | { "type": "share-credentials", client_name: string, logo_uri?: string, options: Array, }; \ No newline at end of file diff --git a/identity-wallet/bindings/user_prompt/LinkedVerifiableCredentialData.ts b/identity-wallet/bindings/user_prompt/LinkedVerifiableCredentialData.ts new file mode 100644 index 000000000..9203f2df2 --- /dev/null +++ b/identity-wallet/bindings/user_prompt/LinkedVerifiableCredentialData.ts @@ -0,0 +1,3 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export interface LinkedVerifiableCredentialData { name: string | null, logo_uri: string | null, issuance_date: string, } \ No newline at end of file diff --git a/identity-wallet/src/state/did/mod.rs b/identity-wallet/src/state/did/mod.rs index 3bc2970ad..60bd8a5ee 100644 --- a/identity-wallet/src/state/did/mod.rs +++ b/identity-wallet/src/state/did/mod.rs @@ -1,6 +1,4 @@ pub mod actions; pub mod reducers; pub mod validate_domain_linkage; -// TODO(proj-e-commerce): This needs to be properly implemented. For now it just demonstrates how the Thuiswinkel -// Waarborg would work in UniMe. -pub mod validate_thuiswinkel_waarborg; +pub mod validate_linked_verifiable_presentations; diff --git a/identity-wallet/src/state/did/validate_domain_linkage.rs b/identity-wallet/src/state/did/validate_domain_linkage.rs index 652842cc8..6f406a5f0 100644 --- a/identity-wallet/src/state/did/validate_domain_linkage.rs +++ b/identity-wallet/src/state/did/validate_domain_linkage.rs @@ -39,7 +39,7 @@ pub enum ValidationStatus { } /// This `Verifier` uses `jsonwebtoken` under the hood to verify verification input. -struct Verifier; +pub struct Verifier; impl JwsVerifier for Verifier { fn verify(&self, input: VerificationInput, public_key: &IotaIdentityJwk) -> Result<(), SignatureVerificationError> { use SignatureVerificationErrorKind::*; @@ -104,6 +104,8 @@ pub async fn validate_domain_linkage(url: url::Url, did: &str) -> ValidationResu } }; + info!("Resolved document: {:?}", document); + let url = identity_iota::core::Url::from(url); let res = validator.validate_linkage( diff --git a/identity-wallet/src/state/did/validate_linked_verifiable_presentations.rs b/identity-wallet/src/state/did/validate_linked_verifiable_presentations.rs new file mode 100644 index 000000000..cc2e0187d --- /dev/null +++ b/identity-wallet/src/state/did/validate_linked_verifiable_presentations.rs @@ -0,0 +1,870 @@ +use crate::{ + persistence::{download_asset, hash}, + state::did::validate_domain_linkage::{validate_domain_linkage, ValidationStatus, Verifier}, +}; +use did_manager::Resolver; +use futures::{ + future::OptionFuture, + stream::{iter, FuturesUnordered}, + StreamExt, +}; +use identity_iota::{ + core::{OneOrMany, ToJson}, + credential::{DecodedJwtPresentation, FailFast, Jwt, JwtCredentialValidator, JwtPresentationValidator, Subject}, + document::{CoreDocument, Service}, + verification::jws::Decoder, +}; +use identity_jose::jwt::JwtClaims; +use log::{info, warn}; +use serde::{Deserialize, Serialize}; +use serde_json::Value; +use ts_rs::TS; +use url::Url; + +#[cfg_attr(not(test), derive(PartialEq))] +#[derive(Clone, Serialize, Deserialize, Debug, TS, Default)] +#[ts(export, export_to = "bindings/user_prompt/LinkedVerifiableCredentialData.ts")] +pub struct LinkedVerifiableCredentialData { + pub name: Option, + pub logo_uri: Option, + pub issuance_date: String, + #[ts(skip)] + pub issuer_linked_domains: Vec, +} + +// Skip the partial equality check for `issuance_date` during testing. +#[cfg(test)] +impl PartialEq for LinkedVerifiableCredentialData { + fn eq(&self, other: &Self) -> bool { + self.name == other.name + && self.logo_uri == other.logo_uri + && self.issuer_linked_domains == other.issuer_linked_domains + } +} + +/// Validate the linked verifiable presentations for the given holder DID. Returns a list of linked verifiable +/// credential data. It starts by resolving the holder DID and then iterates over the linked verifiable presentation +/// URLs. For each linked verifiable presentation, it validates the presentation and then validates the linked +/// verifiable credentials. It only considers linked verifiable credentials with successful domain linkage validation. +pub async fn validate_linked_verifiable_presentations(holder_did: &str) -> Vec> { + info!("Validating linked verifiable presentations for holder DID: {holder_did}"); + + let resolver = Resolver::new().await; + + let holder_document = match resolver.resolve(holder_did).await { + Ok(holder_document) => holder_document, + _ => { + warn!("Failed to resolve holder DID: {holder_did}"); + return vec![]; + } + }; + + info!("Holder document: {holder_document:#?}"); + + iter( + // Get all linked verifiable presentation URLs from the holder document + holder_document + .service() + .iter() + .filter_map(get_linked_verifiable_presentation_urls) + .flatten(), + ) + .filter_map(|linked_verifiable_presentation_url| { + // Validate the linked verifiable presentation and get the linked verifiable credential data + get_validated_linked_presentation_data(&resolver, &holder_document, linked_verifiable_presentation_url) + }) + .collect::>() + .await +} + +/// Get the linked verifiable presentation URLs from the service. It returns a list of URLs if the service type is a +/// `LinkedVerifiablePresentation`. +fn get_linked_verifiable_presentation_urls(service: &Service) -> Option> { + service + .type_() + .contains("LinkedVerifiablePresentation") + .then(|| { + info!("Found LinkedVerifiablePresentation service: {service:#?}"); + service.service_endpoint() + }) + .and_then(|service_endpoint| service_endpoint.to_json_value().ok()) + .and_then( + // Parse the linked verifiable presentation URLs from the service endpoint. The service endpoint must be + // either a string or an array of strings: https://identity.foundation/linked-vp/#linked-verifiable-presentation + |linked_verifiable_presentation_urls| match linked_verifiable_presentation_urls { + Value::String(url) => url + .parse() + .inspect_err(|err| warn!("Failed to parse linked verifiable presentation URL: {}", err)) + .ok() + .map(|url| vec![url]), + Value::Array(array) => Some( + array + .into_iter() + .filter_map(|url| { + url.as_str().and_then(|url| { + url.parse() + .inspect_err(|err| { + warn!("Failed to parse linked verifiable presentation URL: {}", err) + }) + .ok() + }) + }) + .collect(), + ), + _ => None, + }, + ) +} + +/// Validate the linked verifiable presentations for the given holder document and linked verifiable presentation URL. +/// It returns a list of linked verifiable credential data. +async fn get_validated_linked_presentation_data( + resolver: &Resolver, + holder_document: &CoreDocument, + linked_verifiable_presentation_url: Url, +) -> Option> { + OptionFuture::from( + validate_linked_verifiable_presentation(&holder_document, linked_verifiable_presentation_url) + .await + .map(|linked_verifiable_presentation| { + get_validated_linked_credential_data(resolver, linked_verifiable_presentation) + }), + ) + .await +} + +/// Retrieves the linked verifiable presentation from the given URL and validates it against the holder document. +/// Returns the decoded linked verifiable presentation if successful. +async fn validate_linked_verifiable_presentation( + holder_document: &CoreDocument, + linked_verifiable_presentation_url: Url, +) -> Option> { + let response = reqwest::get(linked_verifiable_presentation_url) + .await + .inspect_err(|err| { + warn!("Failed to retrieve linked verifiable presentation: {}", err); + }) + .ok()?; + let status = response.status(); + + response + .text() + .await + .inspect_err(|err| { + warn!("Failed to read linked verifiable presentation response: {}", err); + }) + .ok() + .and_then(|presentation_jwt| { + status.is_success().then(|| { + let validator = JwtPresentationValidator::with_signature_verifier(Verifier); + validator + .validate(&presentation_jwt.into(), &holder_document, &Default::default()) + .inspect_err(|err| { + warn!("Failed to validate linked verifiable presentation: {:#?}", err); + }) + .ok() + })? + }) +} + +/// Validate the linked verifiable credentials in the linked verifiable presentation. Skips invalid credentials or credentials with invalid domain linkage. +/// Since anyone can host a linked verifiable presentation, it is important to validate the linked verifiable +/// credentials. The `issuer` field in the linked verifiable credential is used to resolve the issuer document and which +/// is then used to retrieve the linked domains. The linked domains then are used to validate the domain linkage. +async fn get_validated_linked_credential_data( + resolver: &Resolver, + linked_verifiable_presentation: DecodedJwtPresentation, +) -> Vec { + iter(linked_verifiable_presentation.presentation.verifiable_credential) + .filter_map(|linked_verifiable_credential| async move { + // Resolve the issuer document and issuer DID + let issuer_document = get_issuer_document(resolver, &linked_verifiable_credential).await?; + let issuer_did = issuer_document.id().to_string(); + + info!("Issuer document: {issuer_document:#?}"); + + // Resolve the issuer linked domains from the issuer document + let issuer_linked_domains = get_issuer_linked_domains(&issuer_document).await; + + info!("Issuer linked domains: {issuer_linked_domains:#?}"); + + // Only linked verifiable credentials with at least one successful domain linkage validation are considered + let validated_linked_domains = get_validated_linked_domains(&issuer_linked_domains, &issuer_did).await; + if !validated_linked_domains.is_empty() { + let validator = JwtCredentialValidator::with_signature_verifier(Verifier); + + // Decode the linked verifiable credential and validate it + if let Ok(linked_verifiable_credential) = validator.validate::<_, Value>( + &linked_verifiable_credential, + &issuer_document, + &Default::default(), + FailFast::FirstError, + ) { + info!("Validated linked verifiable credential: {linked_verifiable_credential:#?}"); + + let credential_subject = match &linked_verifiable_credential.credential.credential_subject { + OneOrMany::One(subject) => Some(subject), + // TODO: how to handle multiple credential subjects? + OneOrMany::Many(subjects) => subjects.get(0), + }; + + OptionFuture::from(credential_subject.map(|credential_subject| async { + let name = get_name(credential_subject); + let logo_uri = get_logo_uri(credential_subject).await; + let issuance_date = linked_verifiable_credential.credential.issuance_date.to_rfc3339(); + + LinkedVerifiableCredentialData { + name, + logo_uri, + issuance_date, + issuer_linked_domains: validated_linked_domains, + } + })) + .await + } else { + warn!("Failed to validate linked verifiable credential: {linked_verifiable_credential:#?}"); + // TODO: Should we add more fine-grained error handling? `None` here means that the linked verifiable credential is invalid. + None + } + } else { + warn!("No validated linked domains for issuer DID: {issuer_did}"); + // TODO: Should we add more fine-grained error handling? `None` here means that the domain linkage + // validation failed or is unknown. + None + } + }) + .collect::>() + .await +} + +/// Returns a Vec of successfully validated issuer linked domains. +async fn get_validated_linked_domains(issuer_linked_domains: &[Url], issuer_did: &str) -> Vec { + FuturesUnordered::from_iter(issuer_linked_domains.iter().map(|issuer_linked_domain| async move { + let validation_status = validate_domain_linkage(issuer_linked_domain.clone(), issuer_did) + .await + .status; + + if validation_status == ValidationStatus::Success { + info!("Successfully validated domain linkage for issuer linked domain: {issuer_linked_domain}"); + Some(issuer_linked_domain.clone()) + } else { + warn!("Failed to validate domain linkage for issuer linked domain: {issuer_linked_domain}"); + None + } + })) + .filter_map(|result| async move { result }) + .collect() + .await +} + +/// This function uses the linked verifiable credential to resolve the issuer document. +async fn get_issuer_document(resolver: &Resolver, linked_verifiable_credential: &Jwt) -> Option { + let decoder = Decoder::new(); + + // Decode the linked verifiable credential. + let decoded_linked_verifiable_credential = decoder + .decode_compact_serialization(linked_verifiable_credential.as_str().as_bytes(), None) + .inspect_err(|err| warn!("Failed to decode linked verifiable credential: {:#?}", err)) + .ok()?; + + let claims: JwtClaims = serde_json::from_slice(decoded_linked_verifiable_credential.claims()) + .inspect_err(|err| warn!("Failed to parse linked verifiable credential claims: {:#?}", err)) + .ok()?; + + info!("Linked verifiable credential claims: {:#?}", claims); + + // Resolve the DID + resolver + .resolve(claims.iss()?) + .await + .inspect_err(|err| warn!("Failed to resolve issuer DID.: {:#?}", err)) + .ok() +} + +/// Get the linked domains from the issuer document. It returns a list of URLs if the service type is `LinkedDomains`. +async fn get_issuer_linked_domains(issuer_document: &CoreDocument) -> Vec { + issuer_document + .service() + .iter() + .filter_map(|service| { + service + .type_() + .contains("LinkedDomains") + .then(|| service.service_endpoint()) + .and_then(|service_endpoint| service_endpoint.to_json_value().ok()) + .and_then(|linked_domain| { + linked_domain.get("origins").and_then(|origins| { + origins.as_array().and_then(|origins| { + origins + .into_iter() + .map(|origin| { + origin.as_str().and_then(|origin| { + origin + .parse() + .inspect_err(|err| warn!("Failed to parse linked domain: {:#?}", err)) + .ok() + }) + }) + .collect::>>() + }) + }) + }) + }) + .flatten() + .collect() +} + +fn get_name(credential_subject: &Subject) -> Option { + credential_subject + .properties + .get("name") + .and_then(Value::as_str) + .map(ToString::to_string) +} + +async fn get_logo_uri(credential_subject: &Subject) -> Option { + OptionFuture::from( + credential_subject + .properties + .get("image") + .and_then(Value::as_str) + .map(|image| async { + let _ = download_asset( + image + .parse() + .inspect_err(|err| warn!("Failed to parse logo URI: {:#?}", err)) + .ok()?, + &hash(image), + ) + .await; + Some(image.to_string()) + }), + ) + .await + .flatten() +} + +#[cfg(test)] +mod tests { + use super::*; + use base64::engine::general_purpose::URL_SAFE_NO_PAD; + use base64::Engine; + use did_manager::SecretManager; + use identity_credential::domain_linkage::{DomainLinkageConfiguration, DomainLinkageCredentialBuilder}; + use identity_iota::{ + core::{Duration, FromJson as _, Object, Timestamp, Url}, + credential::{Credential, CredentialBuilder, Presentation}, + document::{CoreDocument, Service, ServiceEndpoint}, + verification::jws::JwsAlgorithm, + }; + use jsonwebtoken::{Algorithm, Header}; + use serde_json::json; + use std::sync::Arc; + use tempfile::TempDir; + use tokio::sync::Mutex; + use wiremock::{ + matchers::{method, path}, + Mock, MockServer, ResponseTemplate, + }; + + // 'Entity' struct that represents a digital identity including a DID Document, a domain, and a secret manager. + struct TestEntity { + pub mock_server: MockServer, + pub domain: url::Url, + pub did_document: CoreDocument, + pub secret_manager: Arc>, + } + + impl TestEntity { + // Create a new 'Entity' with a DID Document, mock server, a domain, and a secret manager. + async fn new() -> Self { + engine::snapshot::try_set_encrypt_work_factor(0).unwrap(); + + let mock_server = MockServer::start().await; + + let uri = mock_server.uri(); + let port = uri.split(':').last().unwrap(); + let domain: url::Url = format!("http://localhost:{port}").parse().unwrap(); + + let temp_dir = TempDir::new().unwrap(); + let path = temp_dir.path().join("stronghold.stronghold"); + let snapshot_path = path.as_os_str().to_str().unwrap(); + + let mut secret_manager = SecretManager::builder() + .snapshot_path(snapshot_path) + .password("sup3rSecr3t") + .build() + .await + .unwrap(); + + let did_document = secret_manager + .produce_document( + did_manager::DidMethod::Web, + Some(did_manager::MethodSpecificParameters::Web { + origin: domain.origin(), + }), + identity_iota::verification::jws::JwsAlgorithm::ES256, + ) + .await + .unwrap(); + + TestEntity { + mock_server, + domain, + did_document, + secret_manager: Arc::new(Mutex::new(secret_manager)), + } + } + + // Add the `.well-known/did.json` endpoint to the mock server. + async fn add_well_known_did_json(&self) { + Mock::given(method("GET")) + .and(path(".well-known/did.json")) + .respond_with(ResponseTemplate::new(200).set_body_json(json!(self.did_document))) + .mount(&self.mock_server) + .await; + } + + // Add the `.well-known/did-configuration.json` endpoint to the mock server. + async fn add_well_known_did_configuration_json(&mut self, service_id: &str, origins: &[Url]) { + let service = Service::builder(Default::default()) + .id(format!("{}#{service_id}", self.did_document.id()).parse().unwrap()) + .type_("LinkedDomains") + .service_endpoint( + serde_json::from_value::(serde_json::json!( + { + "origins": origins + } + )) + .unwrap(), + ) + .build() + .expect("Failed to create DID Configuration Resource"); + self.did_document + .insert_service(service) + .expect("Service already exists in DID Document"); + + let domain_linkage_configuration = { + let origin = Url::parse(self.domain.origin().ascii_serialization()).unwrap(); + let payload = DomainLinkageCredentialBuilder::new() + .issuer(self.did_document.id().clone()) + .origin(origin) + .issuance_date(Timestamp::now_utc()) + .expiration_date(Timestamp::now_utc().checked_add(Duration::seconds(60)).unwrap()) + .build() + .and_then(|credential| credential.serialize_jwt(Default::default())) + .unwrap(); + + DomainLinkageConfiguration::new(vec![self.generate_jwt(payload).await]) + }; + + Mock::given(method("GET")) + .and(path(".well-known/did-configuration.json")) + .respond_with(ResponseTemplate::new(200).set_body_json(json!(domain_linkage_configuration))) + .mount(&self.mock_server) + .await; + } + + // Add a linked verifiable presentation to the DID Document and the mock server. + async fn add_linked_verifiable_presentation( + &mut self, + service_id: &str, + linked_verifiable_presentation_data: Vec<(String, Vec)>, + ) { + let mut urls: Vec = vec![]; + + for (linked_verifiable_presentation_endpoint, linked_verifiable_credential_jwts) in + linked_verifiable_presentation_data + { + let url = format!( + "{}/{linked_verifiable_presentation_endpoint}", + self.domain.origin().ascii_serialization() + ) + .parse() + .unwrap(); + urls.push(url); + + let linked_verifiable_presentation = { + let presentation = { + let mut builder = + Presentation::builder(self.did_document.id().to_string().parse().unwrap(), Object::new()); + for linked_verifiable_credential_jwt in linked_verifiable_credential_jwts { + builder = builder.credential(linked_verifiable_credential_jwt); + } + builder.build().unwrap() + }; + + self.generate_jwt(presentation.serialize_jwt(&Default::default()).unwrap()) + .await + }; + + Mock::given(method("GET")) + .and(path(format!("/{linked_verifiable_presentation_endpoint}"))) + .respond_with(ResponseTemplate::new(200).set_body_string(linked_verifiable_presentation.as_str())) + .mount(&self.mock_server) + .await; + } + + let service_endpoint = match urls.len() { + // Value::String + 1 => serde_json::from_value::(serde_json::json!(urls[0])), + // Value::Array + _ => serde_json::from_value::(serde_json::json!(urls)), + } + .unwrap(); + let service = Service::builder(Default::default()) + .id(format!("{}#{service_id}", self.did_document.id()).parse().unwrap()) + .type_("LinkedVerifiablePresentation") + .service_endpoint(service_endpoint) + .build() + .unwrap(); + + self.did_document + .insert_service(service) + .expect("Service already exists in DID Document"); + } + + // 'Issues' a Credential Jwt to a subject. + async fn issue_credential(&mut self, subject_id: &str, subject_name: &str, subject_image: Url) -> Jwt { + let subject = identity_credential::credential::Subject::from_json_value(json!({ + "id": subject_id, + "name": subject_name, + "image": subject_image + })) + .unwrap(); + + let issuer = identity_iota::credential::Issuer::Url(self.did_document.id().to_string().parse().unwrap()); + + let credential: Credential = CredentialBuilder::default() + .issuer(issuer) + .subject(subject) + .build() + .unwrap(); + + self.generate_jwt(credential.serialize_jwt(Default::default()).unwrap()) + .await + } + + // Generates a JWT with the given payload. + async fn generate_jwt(&mut self, payload: String) -> Jwt { + let subject_did = self.did_document.id().to_string(); + + // Compose JWT + let header = Header { + alg: Algorithm::ES256, + typ: Some("JWT".to_string()), + kid: Some(format!("{subject_did}#key-0")), + ..Default::default() + }; + + let message = [ + URL_SAFE_NO_PAD.encode(serde_json::to_vec(&header).unwrap().as_slice()), + URL_SAFE_NO_PAD.encode(payload.as_bytes()), + ] + .join("."); + + let secret_manager = self.secret_manager.lock().await; + + let proof_value = secret_manager + .sign(message.as_bytes(), JwsAlgorithm::ES256) + .await + .unwrap(); + let signature = URL_SAFE_NO_PAD.encode(proof_value.as_slice()); + let message = [message, signature].join("."); + + Jwt::from(message) + } + } + + #[tokio::test] + async fn validate_linked_verifiable_presentations_successfully_validates_multiple_presentations() { + let mut holder = TestEntity::new().await; + + let mut issuer_a = TestEntity::new().await; + + // Add the `/did_configuration.json` and `/did.json` endpoints to the issuer A mock server. + issuer_a + .add_well_known_did_configuration_json("linked-domain", &[issuer_a.domain.clone().into()]) + .await; + issuer_a.add_well_known_did_json().await; + + let mut issuer_b = TestEntity::new().await; + + // Add the `/did_configuration.json` and `/did.json` endpoints to the issuer B mock server. + issuer_b + .add_well_known_did_configuration_json("linked-domain", &[issuer_b.domain.clone().into()]) + .await; + issuer_b.add_well_known_did_json().await; + + let verifiable_credential_jwt = issuer_a + .issue_credential( + holder.did_document.id().to_string().as_str(), + "Webshop", + "https://webshop.com/logo.jpg".parse().unwrap(), + ) + .await; + + let service_id = "linked-verifiable-presentation"; + let linked_verifiable_presentation_endpoint = "linked-verifiable-presentation.jwt"; + + // Add the first linked verifiable presentation endpoint and the service to the holder DID Document. + holder + .add_linked_verifiable_presentation( + service_id, + vec![( + linked_verifiable_presentation_endpoint.to_string(), + vec![verifiable_credential_jwt], + )], + ) + .await; + + let verifiable_credential_jwt_2 = issuer_b + .issue_credential( + holder.did_document.id().to_string().as_str(), + "Webshop", + "https://webshop.com/logo.jpg".parse().unwrap(), + ) + .await; + + let service_id2 = "linked-verifiable-presentation-2"; + + // Add the second linked verifiable presentation endpoint and the service to the holder DID Document. + let linked_verifiable_presentation_endpoint2 = "linked-verifiable-presentation2.jwt"; + holder + .add_linked_verifiable_presentation( + service_id2, + vec![( + linked_verifiable_presentation_endpoint2.to_string(), + vec![verifiable_credential_jwt_2], + )], + ) + .await; + + holder.add_well_known_did_json().await; + + assert_eq!( + validate_linked_verifiable_presentations(&holder.did_document.id().to_string()).await, + vec![ + vec![LinkedVerifiableCredentialData { + name: Some("Webshop".to_string()), + logo_uri: Some("https://webshop.com/logo.jpg".to_string()), + issuer_linked_domains: vec![issuer_a.domain.clone()], + ..Default::default() + }], + vec![LinkedVerifiableCredentialData { + name: Some("Webshop".to_string()), + logo_uri: Some("https://webshop.com/logo.jpg".to_string()), + issuer_linked_domains: vec![issuer_b.domain.clone()], + ..Default::default() + }] + ] + ); + } + + #[tokio::test] + async fn validate_linked_verifiable_presentations_successfully_considers_missing_issuer_domain_linkage() { + let mut holder = TestEntity::new().await; + + let mut issuer = TestEntity::new().await; + + // Add the `/did_configuration.json` and `/did.json` endpoints to the issuer mock server. + issuer + .add_well_known_did_configuration_json("linked-domain", &[issuer.domain.clone().into()]) + .await; + + // This time we do not add the `/did.json` endpoint to the issuer mock server, which makes it impossible to + // validate the domain linkage of the issuer. + // issuer.add_well_known_did_json().await; + + let verifiable_credential_jwt = issuer + .issue_credential( + holder.did_document.id().to_string().as_str(), + "Webshop", + "https://webshop.com/logo.jpg".parse().unwrap(), + ) + .await; + + let service_id = "linked-verifiable-presentation"; + let linked_verifiable_presentation_endpoint = "linked-verifiable-presentation.jwt"; + + // Add the linked verifiable presentation endpoint and the service to the holder DID Document. + holder + .add_linked_verifiable_presentation( + service_id, + vec![( + linked_verifiable_presentation_endpoint.to_string(), + vec![verifiable_credential_jwt], + )], + ) + .await; + + holder.add_well_known_did_json().await; + + assert_eq!( + validate_linked_verifiable_presentations(&holder.did_document.id().to_string()).await, + // The domain linkage validation of the issuer failed, so the linked verifiable credential is not considered. + vec![vec![]] + ); + } + + #[tokio::test] + async fn get_linked_verifiable_presentation_urls_successfully_retrieves_urls() { + let mut holder = TestEntity::new().await; + + let service_id = "linked-verifiable-presentation"; + let linked_verifiable_presentation_endpoint = "linked-verifiable-presentation.jwt"; + let linked_verifiable_presentation_endpoint2 = "linked-verifiable-presentation2.jwt"; + holder + .add_linked_verifiable_presentation( + service_id, + vec![ + ( + linked_verifiable_presentation_endpoint.to_string(), + // Linked verifiable presentation can include multiple linked verifiable credentials. + vec![Jwt::from("test1".to_string()), Jwt::from("test2".to_string())], + ), + ( + linked_verifiable_presentation_endpoint2.to_string(), + vec![Jwt::from("test3".to_string())], + ), + ], + ) + .await; + + // Assert that the URLs of both linked verifiable presentations are retrieved. + assert!( + get_linked_verifiable_presentation_urls(&holder.did_document.service()[0]) + .unwrap() + .iter() + .all(|item| vec![ + format!("{}{}", holder.domain, linked_verifiable_presentation_endpoint) + .parse() + .unwrap(), + format!("{}{}", holder.domain, linked_verifiable_presentation_endpoint2) + .parse() + .unwrap() + ] + .contains(item)) + ); + } + + #[tokio::test] + async fn get_validated_linked_credential_data_succesfully_returns_linked_verifiable_credential_data() { + let mut issuer = TestEntity::new().await; + + // Add the `/did_configuration.json` and `/did.json` endpoints to the issuer mock server. + issuer + .add_well_known_did_configuration_json("linked-domain", &[issuer.domain.clone().into()]) + .await; + issuer.add_well_known_did_json().await; + + let mut holder = TestEntity::new().await; + + let verifiable_credential_jwt = issuer + .issue_credential( + holder.did_document.id().to_string().as_str(), + "Webshop", + "https://webshop.com/logo.jpg".parse().unwrap(), + ) + .await; + + let service_id = "linked-verifiable-presentation"; + let linked_verifiable_presentation_endpoint = "linked-verifiable-presentation.jwt"; + holder + .add_linked_verifiable_presentation( + service_id, + vec![( + linked_verifiable_presentation_endpoint.to_string(), + vec![verifiable_credential_jwt], + )], + ) + .await; + + let resolver = Resolver::new().await; + + let linked_verifiable_presentation_url: url::Url = + format!("{}{linked_verifiable_presentation_endpoint}", holder.domain) + .parse() + .unwrap(); + + let validated_linked_presentation_data = + get_validated_linked_presentation_data(&resolver, &holder.did_document, linked_verifiable_presentation_url) + .await; + + assert_eq!( + validated_linked_presentation_data, + Some(vec![LinkedVerifiableCredentialData { + name: Some("Webshop".to_string()), + logo_uri: Some("https://webshop.com/logo.jpg".to_string()), + issuer_linked_domains: vec![issuer.domain.clone()], + ..Default::default() + }]) + ); + } + + #[tokio::test] + async fn get_validated_linked_domains_returns_only_succesfully_validated_linked_domains() { + let mut issuer1 = TestEntity::new().await; + + // Add the `/did_configuration.json` and `/did.json` endpoints to the issuer mock server. + issuer1 + .add_well_known_did_configuration_json("linked-domain", &[issuer1.domain.clone().into()]) + .await; + issuer1.add_well_known_did_json().await; + + // Succesfully validate the linked domain. + assert_eq!( + get_validated_linked_domains(&[issuer1.domain.clone()], &issuer1.did_document.id().to_string()).await, + vec![issuer1.domain.clone()] + ); + + // Assert that only one domain was validated. + assert_eq!( + get_validated_linked_domains( + &[issuer1.domain.clone(), "http://invalid-domain.org".parse().unwrap()], + &issuer1.did_document.id().to_string() + ) + .await, + vec![issuer1.domain.clone()] + ); + + let mut issuer2 = TestEntity::new().await; + + // Add the `/did_configuration.json` and `/did.json` endpoints to the issuer mock server. + issuer2 + .add_well_known_did_configuration_json("linked-domain-2", &[issuer2.domain.clone().into()]) + .await; + issuer2.add_well_known_did_json().await; + + // Assert that only one domain was validated. The second domain cannot be validated because the issuer DID is different. + assert_eq!( + get_validated_linked_domains( + &[issuer1.domain.clone(), issuer2.domain.clone()], + &issuer1.did_document.id().to_string() + ) + .await, + vec![issuer1.domain.clone()] + ); + + // Add the `/did_configuration.json` and `/did.json` endpoints to the issuer mock server. Use the same issuer DID as + // issuer1, but a different domain. + let mut issuer2 = TestEntity::new().await; + issuer2.did_document = issuer1.did_document.clone(); + issuer2.secret_manager = issuer1.secret_manager.clone(); + + // Add the `/did_configuration.json` and `/did.json` endpoints to the issuer mock server. + issuer2 + .add_well_known_did_configuration_json("linked-domain-2", &[issuer2.domain.clone().into()]) + .await; + issuer2.add_well_known_did_json().await; + + // Assert that both domains were validated (regardless of the order). + assert!(get_validated_linked_domains( + &[issuer1.domain.clone(), issuer2.domain.clone()], + &issuer1.did_document.id().to_string() + ) + .await + .iter() + .all(|item| vec![issuer1.domain.clone(), issuer2.domain.clone()].contains(item))); + } +} diff --git a/identity-wallet/src/state/did/validate_thuiswinkel_waarborg.rs b/identity-wallet/src/state/did/validate_thuiswinkel_waarborg.rs deleted file mode 100644 index 39e7fe7bb..000000000 --- a/identity-wallet/src/state/did/validate_thuiswinkel_waarborg.rs +++ /dev/null @@ -1,157 +0,0 @@ -use std::str::FromStr; - -use crate::{ - persistence::{download_asset, hash}, - state::{ - core_utils::helpers::get_unverified_jwt_claims, - did::validate_domain_linkage::{ValidationResult, ValidationStatus}, - }, -}; -use did_manager::Resolver; -use identity_iota::core::ToJson; -use log::info; -use serde_json::Value; - -pub async fn validate_thuiswinkel_waarborg(did: &str) -> ValidationResult { - let resolver = Resolver::new().await; - - info!("Validating Thuiswinkel Waarborg"); - info!("DID: {}", did); - - // Resolve the Document from the DID. - let document = match resolver.resolve(did).await { - Ok(document) => document, - Err(e) => { - return ValidationResult { - status: ValidationStatus::Unknown, - message: Some(e.to_string()), - ..Default::default() - }; - } - }; - - info!("Document: {:?}", document); - - // Extract the URL of the Linked Verifiable Presentation from the Docoment. - let linked_verifiable_presentation_url = match document - .service() - .iter() - .find_map(|service| { - service - .type_() - .contains("LinkedVerifiablePresentation") - .then(|| service.service_endpoint()) - }) - .and_then(|service_endpoint| service_endpoint.to_json_value().ok()) - .and_then(|service_endpoint| service_endpoint.get("origins").cloned()) - .and_then(|origins| { - origins.as_array().and_then(|origins| { - origins - .first() - .and_then(|origin| origin.as_str().map(url::Url::from_str)) - }) - }) { - Some(Ok(linked_verifiable_presentation_url)) => linked_verifiable_presentation_url, - _ => { - return ValidationResult { - status: ValidationStatus::Unknown, - ..Default::default() - } - } - }; - - info!( - "Linked Verifiable Presentation URL: {}", - linked_verifiable_presentation_url - ); - - // Fetch the actual Linked Verifiable Presentation from the service endpoint. - let linked_verifiable_presentation_result = - fetch_linked_verifiable_presentation(linked_verifiable_presentation_url).await; - - let linked_verifiable_presentation = match linked_verifiable_presentation_result { - Ok(linked_verifiable_presentation) => linked_verifiable_presentation, - Err(e) => { - return ValidationResult { - status: ValidationStatus::Unknown, - message: Some(e), - ..Default::default() - } - } - }; - - info!("Linked Verifiable Presentation: {}", linked_verifiable_presentation); - - // Extract the `name` and `thuiswinkel_waarborg_image` from the Linked Verifiable Presentation to be displayed in - // the frontend. - let (name, thuiswinkel_waarborg_image, issuance_date) = - match get_unverified_jwt_claims(&serde_json::json!(linked_verifiable_presentation)) - .unwrap() - .get("vp") - .and_then(|vp| { - vp.get("verifiableCredential") - .and_then(|verifiable_credentials| verifiable_credentials.as_array()) - }) - .and_then(|verifiable_credential| verifiable_credential.first().cloned()) - .map(|verifiable_credential| get_unverified_jwt_claims(&verifiable_credential)) - .and_then(|verifiable_credential| { - verifiable_credential.unwrap().get("vc").and_then(|vc| { - vc.get("credentialSubject").map(|credential_subject| { - ( - credential_subject - .get("name") - .and_then(Value::as_str) - .map(ToString::to_string), - credential_subject - .get("thuiswinkel_waarborg_image") - .and_then(Value::as_str) - .map(url::Url::parse), - vc.get("issuanceDate").and_then(Value::as_str).map(ToString::to_string), - ) - }) - }) - }) { - Some(display_properties) => { - if let Some(Ok(thuiswinkel_waarborg_image)) = display_properties.1 { - let _ = download_asset( - thuiswinkel_waarborg_image.clone(), - &hash(thuiswinkel_waarborg_image.as_str()), - ) - .await; - ( - display_properties.0, - Some(thuiswinkel_waarborg_image), - display_properties.2, - ) - } else { - (display_properties.0, None, display_properties.2) - } - } - None => { - return ValidationResult { - status: ValidationStatus::Unknown, - ..Default::default() - } - } - }; - - info!("Thuiswinkel Waarborg Name: {:?}", name); - info!("Thuiswinkel Waarborg Image: {:?}", thuiswinkel_waarborg_image); - info!("Thuiswinkel Waarborg Issuance Date: {:?}", issuance_date); - - ValidationResult { - status: ValidationStatus::Success, - name, - logo_uri: thuiswinkel_waarborg_image, - issuance_date, - ..Default::default() - } -} - -async fn fetch_linked_verifiable_presentation(url: url::Url) -> Result { - // 1. Fetch the resource - let response = reqwest::get(url).await.map_err(|e| e.to_string())?; - - // 2. Return the Linked Verifiable Presentation (as Jwt) - response.text().await.map_err(|e| e.to_string()) -} diff --git a/identity-wallet/src/state/qr_code/reducers/read_authorization_request.rs b/identity-wallet/src/state/qr_code/reducers/read_authorization_request.rs index 090e26c4c..3b3451413 100644 --- a/identity-wallet/src/state/qr_code/reducers/read_authorization_request.rs +++ b/identity-wallet/src/state/qr_code/reducers/read_authorization_request.rs @@ -4,13 +4,13 @@ use crate::{ state::{ actions::{listen, Action}, connections::reducers::handle_siopv2_authorization_request::get_siopv2_client_name_and_logo_uri, - core_utils::{helpers::get_unverified_jwt_claims, ConnectionRequest, CoreUtils}, + core_utils::{ConnectionRequest, CoreUtils}, credentials::reducers::handle_oid4vp_authorization_request::{ get_oid4vp_client_name_and_logo_uri, OID4VPClientMetadata, }, did::{ validate_domain_linkage::validate_domain_linkage, - validate_thuiswinkel_waarborg::validate_thuiswinkel_waarborg, + validate_linked_verifiable_presentations::validate_linked_verifiable_presentations, }, qr_code::actions::qrcode_scanned::QrCodeScanned, user_prompt::CurrentUserPrompt, @@ -86,9 +86,12 @@ pub async fn read_authorization_request(state: AppState, action: Action) -> Resu let did = siopv2_authorization_request.body.client_id.as_str(); let domain_validation = Box::new(validate_domain_linkage(url, did).await); - // TODO(proj-e-commerce): This needs to be properly implemented. For now it just demonstrates how the Thuiswinkel - // Waarborg would work in UniMe. - let thuiswinkel_validation = Box::new(validate_thuiswinkel_waarborg(did).await); + + let linked_verifiable_presentations = validate_linked_verifiable_presentations(did) + .await + .into_iter() + .flatten() + .collect(); drop(state_guard); @@ -103,7 +106,7 @@ pub async fn read_authorization_request(state: AppState, action: Action) -> Resu redirect_uri, previously_connected, domain_validation, - thuiswinkel_validation, + linked_verifiable_presentations, }), ..state }); diff --git a/identity-wallet/src/state/user_prompt.rs b/identity-wallet/src/state/user_prompt.rs index 67013d350..ba5a59613 100644 --- a/identity-wallet/src/state/user_prompt.rs +++ b/identity-wallet/src/state/user_prompt.rs @@ -6,6 +6,8 @@ use ts_rs::TS; use crate::state::did::validate_domain_linkage::ValidationResult; +use super::did::validate_linked_verifiable_presentations::LinkedVerifiableCredentialData; + /// "User prompts" are a way for the backend to communicate a desired/required user interaction to the frontend. /// This application design leaves it up to the frontend how it wants to handle such "user prompts". /// Having too much frontend logic in the backend would pollute the loose coupling and make it a lot harder to maintain. @@ -29,7 +31,7 @@ pub enum CurrentUserPrompt { redirect_uri: String, previously_connected: bool, domain_validation: Box, - thuiswinkel_validation: Box, + linked_verifiable_presentations: Vec, }, #[serde(rename = "credential-offer")] CredentialOffer { @@ -73,11 +75,11 @@ mod tests { redirect_uri: "https://example.com".to_string(), previously_connected: false, domain_validation: Default::default(), - thuiswinkel_validation: Default::default(), + linked_verifiable_presentations: Default::default(), }; assert_eq!( serde_json::to_string(&prompt).unwrap(), - r#"{"type":"accept-connection","client_name":"Test Client","logo_uri":null,"redirect_uri":"https://example.com","previously_connected":false,"domain_validation":{"status":"Unknown"},"thuiswinkel_validation":{"status":"Unknown"}}"# + r#"{"type":"accept-connection","client_name":"Test Client","logo_uri":null,"redirect_uri":"https://example.com","previously_connected":false,"domain_validation":{"status":"Unknown"},"linked_verifiable_presentations":[]}"# ); } } diff --git a/identity-wallet/src/subject.rs b/identity-wallet/src/subject.rs index 04becca2d..fcb59c59c 100644 --- a/identity-wallet/src/subject.rs +++ b/identity-wallet/src/subject.rs @@ -11,13 +11,14 @@ use identity_iota::{ use jsonwebtoken::Algorithm; use oid4vc::oid4vc_core::{authentication::sign::ExternalSign, Sign, Verify}; use std::sync::Arc; +use tokio::sync::Mutex; /// A `Subject` implements functions required for signatures and verification. /// In UniMe, it serves as the "binding link" between the protocol libraries (OID4VC) and the secret management (DID Manager). #[derive(Debug)] pub struct Subject { pub stronghold_manager: Arc, - pub secret_manager: SecretManager, + pub secret_manager: Arc>, } #[async_trait] @@ -25,7 +26,9 @@ impl Sign for Subject { async fn key_id(&self, subject_syntax_type: &str, algorithm: Algorithm) -> Option { let method: DidMethod = serde_json::from_str(&format!("{subject_syntax_type:?}")).ok()?; - self.secret_manager + let mut secret_manager = self.secret_manager.lock().await; + + secret_manager .produce_document(method, None, algorithm.into_jws_algorithm()) .await .ok() @@ -34,8 +37,9 @@ impl Sign for Subject { } async fn sign(&self, message: &str, _subject_syntax_type: &str, algorithm: Algorithm) -> anyhow::Result> { - Ok(self - .secret_manager + let secret_manager = self.secret_manager.lock().await; + + Ok(secret_manager .sign(message.as_bytes(), algorithm.into_jws_algorithm()) .await?) } @@ -49,9 +53,9 @@ impl Sign for Subject { impl oid4vc::oid4vc_core::Subject for Subject { async fn identifier(&self, subject_syntax_type: &str, algorithm: Algorithm) -> anyhow::Result { let method: DidMethod = serde_json::from_str(&format!("{subject_syntax_type:?}"))?; + let mut secret_manager = self.secret_manager.lock().await; - Ok(self - .secret_manager + Ok(secret_manager .produce_document(method, None, algorithm.into_jws_algorithm()) .await .map(|document| document.id().to_string())?) @@ -114,17 +118,16 @@ pub async fn subject(stronghold_manager: Arc, password: Strin Arc::new(Subject { stronghold_manager: stronghold_manager.clone(), - secret_manager: SecretManager::load( - client_path, - password, - Some("ed25519-0".to_owned()), - Some("es256-0".to_owned()), - Some("es256k-0".to_owned()), - None, - None, - ) - .await - .unwrap(), + secret_manager: Arc::new(Mutex::new( + SecretManager::builder() + .snapshot_path(&client_path) + .with_ed25519_key("ed25519-0") + .with_es256_key("es256-0") + .password(&password) + .build() + .await + .unwrap(), + )), }) } diff --git a/unime/src-tauri/tests/common/mod.rs b/unime/src-tauri/tests/common/mod.rs index c1852cb1f..b4054eeee 100644 --- a/unime/src-tauri/tests/common/mod.rs +++ b/unime/src-tauri/tests/common/mod.rs @@ -12,6 +12,7 @@ use identity_wallet::{ state::core_utils::{IdentityManager, Managers}, stronghold::StrongholdManager, }; +use tokio::sync::Mutex; use self::assert_state_update::setup_stronghold; use serde::de::DeserializeOwned; @@ -54,17 +55,15 @@ pub async fn test_managers( let subject: Arc = Arc::new(Subject { stronghold_manager: stronghold_manager.clone(), - secret_manager: SecretManager::load( - stronghold_snapshot_path, - TEST_PASSWORD.to_string(), - Some(KEY_ID.to_string()), - None, - None, - None, - None, - ) - .await - .unwrap(), + secret_manager: Arc::new(Mutex::new( + SecretManager::builder() + .snapshot_path(&stronghold_snapshot_path) + .with_ed25519_key(KEY_ID) + .password(TEST_PASSWORD) + .build() + .await + .unwrap(), + )), }); let provider_manager = ProviderManager::new( diff --git a/unime/src-tauri/tests/fixtures/states/accept_connection.json b/unime/src-tauri/tests/fixtures/states/accept_connection.json index df179ff23..ad93e2cca 100644 --- a/unime/src-tauri/tests/fixtures/states/accept_connection.json +++ b/unime/src-tauri/tests/fixtures/states/accept_connection.json @@ -16,8 +16,6 @@ "status": "Unknown", "message": "error decoding response body: expected value at line 1 column 1" }, - "thuiswinkel_validation": { - "status": "Unknown" - } + "linked_verifiable_presentations": [] } } diff --git a/unime/src/routes/prompt/accept-connection/+page.svelte b/unime/src/routes/prompt/accept-connection/+page.svelte index 491d7556d..189feeb30 100644 --- a/unime/src/routes/prompt/accept-connection/+page.svelte +++ b/unime/src/routes/prompt/accept-connection/+page.svelte @@ -27,8 +27,14 @@ type IsAcceptConnectionPrompt = T extends { type: 'accept-connection' } ? T : never; type AcceptConnectionPrompt = IsAcceptConnectionPrompt; - const { client_name, domain_validation, logo_uri, previously_connected, redirect_uri, thuiswinkel_validation } = - $state.current_user_prompt as AcceptConnectionPrompt; + const { + client_name, + domain_validation, + logo_uri, + previously_connected, + redirect_uri, + linked_verifiable_presentations, + } = $state.current_user_prompt as AcceptConnectionPrompt; $: ({ hostname } = new URL(redirect_uri)); $: imageId = logo_uri ? hash(logo_uri) : '_'; @@ -117,19 +123,21 @@ - - {#if thuiswinkel_validation.status === 'Success' && thuiswinkel_validation.name} - {@const issuanceDate = - thuiswinkel_validation.issuance_date && profile_settings.locale - ? formatDate(thuiswinkel_validation.issuance_date, profile_settings.locale) - : undefined} - - {/if} + + {#each linked_verifiable_presentations as presentation} + {#if presentation.name} + {@const issuanceDate = + presentation.issuance_date && profile_settings.locale + ? formatDate(presentation.issuance_date, profile_settings.locale) + : undefined} + + {/if} + {/each}