From b1858c15bde8361adbd79f75baa74689ac930c35 Mon Sep 17 00:00:00 2001 From: Nander Stabel Date: Wed, 26 Jun 2024 11:34:56 +0200 Subject: [PATCH 01/19] WIP --- Cargo.lock | 2 + identity-wallet/Cargo.toml | 2 + .../bindings/user_prompt/CurrentUserPrompt.ts | 2 +- .../bindings/user_prompt/ValidationResult.ts | 2 +- identity-wallet/src/state/did/mod.rs | 3 + .../src/state/did/validate_domain_linkage.rs | 17 ++- .../did/validate_thuiswinkel_waarborg.rs | 135 ++++++++++++++++++ .../reducers/read_authorization_request.rs | 9 +- identity-wallet/src/state/user_prompt.rs | 2 + 9 files changed, 168 insertions(+), 6 deletions(-) create mode 100644 identity-wallet/src/state/did/validate_thuiswinkel_waarborg.rs diff --git a/Cargo.lock b/Cargo.lock index 179f814eb..ac6cfcf64 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4060,6 +4060,7 @@ dependencies = [ "identity_credential", "identity_eddsa_verifier", "identity_iota", + "identity_jose", "iota_stronghold", "itertools 0.10.5", "jsonwebtoken", @@ -4070,6 +4071,7 @@ dependencies = [ "reqwest 0.11.27", "serde", "serde_json", + "serde_with 3.8.1", "serial_test", "sha256", "stronghold_engine", diff --git a/identity-wallet/Cargo.toml b/identity-wallet/Cargo.toml index 888287d2a..0c2bba3e5 100644 --- a/identity-wallet/Cargo.toml +++ b/identity-wallet/Cargo.toml @@ -19,6 +19,7 @@ identity_credential = { version = "1.3.0", default-features = false, features = ] } identity_eddsa_verifier = { version = "1.3.0" } identity_iota = { version = "1.3.0" } +identity_jose = { version = "1.3.0" } iota_stronghold = { version = "=2.1.0" } stronghold_engine = "=2.0.0" anyhow = "1.0" @@ -36,6 +37,7 @@ reqwest = { version = "0.11", default-features = false, features = [ "rustls-tls", ] } serde = { version = "1.0", features = ["derive"] } +serde_with = "3.8" sha256 = "1.4" strum = { version = "0.25", features = ["derive"] } thiserror = "1.0" diff --git a/identity-wallet/bindings/user_prompt/CurrentUserPrompt.ts b/identity-wallet/bindings/user_prompt/CurrentUserPrompt.ts index 15305406a..20f2075ca 100644 --- a/identity-wallet/bindings/user_prompt/CurrentUserPrompt.ts +++ b/identity-wallet/bindings/user_prompt/CurrentUserPrompt.ts @@ -1,4 +1,4 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. 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, } | { "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, thuiswinkel_waarborg_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, }; \ No newline at end of file diff --git a/identity-wallet/bindings/user_prompt/ValidationResult.ts b/identity-wallet/bindings/user_prompt/ValidationResult.ts index fcc211ac3..10ca542da 100644 --- a/identity-wallet/bindings/user_prompt/ValidationResult.ts +++ b/identity-wallet/bindings/user_prompt/ValidationResult.ts @@ -1,4 +1,4 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. import type { ValidationStatus } from "./ValidationStatus"; -export interface ValidationResult { status: ValidationStatus, message: string | null, } \ No newline at end of file +export interface ValidationResult { status: ValidationStatus, name?: string, logo_uri?: String, message?: 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 6d99f468f..3bc2970ad 100644 --- a/identity-wallet/src/state/did/mod.rs +++ b/identity-wallet/src/state/did/mod.rs @@ -1,3 +1,6 @@ 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; diff --git a/identity-wallet/src/state/did/validate_domain_linkage.rs b/identity-wallet/src/state/did/validate_domain_linkage.rs index 533c0ab86..04d24f74a 100644 --- a/identity-wallet/src/state/did/validate_domain_linkage.rs +++ b/identity-wallet/src/state/did/validate_domain_linkage.rs @@ -4,12 +4,17 @@ use identity_eddsa_verifier::EdDSAJwsVerifier; use identity_iota::{core::FromJson, credential::JwtCredentialValidationOptions}; use log::info; use serde::{Deserialize, Serialize}; +use serde_with::skip_serializing_none; use ts_rs::TS; +#[skip_serializing_none] #[derive(Clone, Serialize, Deserialize, Debug, PartialEq, TS, Default)] #[ts(export, export_to = "bindings/user_prompt/ValidationResult.ts")] pub struct ValidationResult { pub(crate) status: ValidationStatus, + pub(crate) name: Option, + #[ts(type = "String", optional)] + pub(crate) logo_uri: Option, pub(crate) message: Option, } @@ -32,6 +37,7 @@ pub async fn validate_domain_linkage(url: url::Url, did: &str) -> ValidationResu return ValidationResult { status: ValidationStatus::Unknown, message: Some(e.to_string()), + ..Default::default() }; } }; @@ -47,6 +53,7 @@ pub async fn validate_domain_linkage(url: url::Url, did: &str) -> ValidationResu return ValidationResult { status: ValidationStatus::Unknown, message: Some(e.to_string()), + ..Default::default() }; } }; @@ -63,12 +70,13 @@ pub async fn validate_domain_linkage(url: url::Url, did: &str) -> ValidationResu if res.is_ok() { ValidationResult { status: ValidationStatus::Success, - message: None, + ..Default::default() } } else { ValidationResult { status: ValidationStatus::Failure, message: res.err().map(|e| e.to_string()), + ..Default::default() } } } @@ -175,6 +183,7 @@ mod tests { ValidationResult { status: ValidationStatus::Unknown, message: Some("failed to decode JSON".to_string()), + ..Default::default() } ); } @@ -234,7 +243,8 @@ mod tests { result, ValidationResult { status: ValidationStatus::Failure, - message: Some("invalid issuer DID".to_string()) + message: Some("invalid issuer DID".to_string()), + ..Default::default() } ); } @@ -267,7 +277,8 @@ mod tests { result, ValidationResult { status: ValidationStatus::Failure, - message: Some("invalid issuer DID".to_string()) + message: Some("invalid issuer DID".to_string()), + ..Default::default() } ); } diff --git a/identity-wallet/src/state/did/validate_thuiswinkel_waarborg.rs b/identity-wallet/src/state/did/validate_thuiswinkel_waarborg.rs new file mode 100644 index 000000000..835558841 --- /dev/null +++ b/identity-wallet/src/state/did/validate_thuiswinkel_waarborg.rs @@ -0,0 +1,135 @@ +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; + +pub async fn validate_thuiswinkel_waarborg(did: &str) -> ValidationResult { + let resolver = Resolver::new().await; + + // 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() + }; + } + }; + + // 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() + } + } + }; + + // 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() + } + } + }; + + // Extract the `name` and `thuiswinkel_waarborg_image` from the Linked Verifiable Presentation to be displayed in + // the frontend. + let (name, thuiswinkel_waarborg_image) = + match get_unverified_jwt_claims(&serde_json::json!(linked_verifiable_presentation)) + .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.get("vc").and_then(|vc| { + vc.get("credentialSubject").and_then(|credential_subject| { + credential_subject + .get("name") + .and_then(serde_json::Value::as_str) + .map(ToString::to_string) + .and_then(|name| { + Some(( + Some(name), + credential_subject + .get("thuiswinkel_waarborg_image") + .and_then(serde_json::Value::as_str) + .map(url::Url::from_str), + )) + }) + }) + }) + }) { + 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)) + } else { + (display_properties.0, None) + } + } + None => { + return ValidationResult { + status: ValidationStatus::Unknown, + ..Default::default() + } + } + }; + + ValidationResult { + status: ValidationStatus::Success, + name, + logo_uri: thuiswinkel_waarborg_image, + ..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 3e19fbfd6..c3d5bd081 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 @@ -6,7 +6,10 @@ use crate::{ connections::reducers::handle_siopv2_authorization_request::get_siopv2_client_name_and_logo_uri, core_utils::{helpers::get_unverified_jwt_claims, ConnectionRequest, CoreUtils}, credentials::reducers::handle_oid4vp_authorization_request::get_oid4vp_client_name_and_logo_uri, - did::validate_domain_linkage::validate_domain_linkage, + did::{ + validate_domain_linkage::validate_domain_linkage, + validate_thuiswinkel_waarborg::validate_thuiswinkel_waarborg, + }, qr_code::actions::qrcode_scanned::QrCodeScanned, user_prompt::CurrentUserPrompt, AppState, @@ -78,6 +81,9 @@ pub async fn read_authorization_request(state: AppState, action: Action) -> Resu let did = siopv2_authorization_request.body.client_id.as_str(); let domain_validation = 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_waarborg_validation = validate_thuiswinkel_waarborg(did).await; drop(state_guard); @@ -92,6 +98,7 @@ pub async fn read_authorization_request(state: AppState, action: Action) -> Resu redirect_uri, previously_connected, domain_validation, + thuiswinkel_waarborg_validation, }), ..state }); diff --git a/identity-wallet/src/state/user_prompt.rs b/identity-wallet/src/state/user_prompt.rs index 2afe33670..ea40b543c 100644 --- a/identity-wallet/src/state/user_prompt.rs +++ b/identity-wallet/src/state/user_prompt.rs @@ -29,6 +29,7 @@ pub enum CurrentUserPrompt { redirect_uri: String, previously_connected: bool, domain_validation: ValidationResult, + thuiswinkel_waarborg_validation: ValidationResult, }, #[serde(rename = "credential-offer")] CredentialOffer { @@ -72,6 +73,7 @@ mod tests { redirect_uri: "https://example.com".to_string(), previously_connected: false, domain_validation: Default::default(), + thuiswinkel_waarborg_validation: Default::default(), }; assert_eq!( serde_json::to_string(&prompt).unwrap(), From a87866c20be5b3b5d2220791a8f03a4ac83731ff Mon Sep 17 00:00:00 2001 From: Thilo Maier Date: Thu, 27 Jun 2024 21:30:16 +0200 Subject: [PATCH 02/19] Add Thuiswinkel verification checkmark --- unime/src-tauri/tauri.conf.json | 2 +- .../src/lib/components/StatusIndicator.svelte | 57 ++++++++ unime/src/lib/components/index.js | 5 + unime/src/routes/(app)/scan/+page@.svelte | 13 +- .../prompt/accept-connection/+page.svelte | 127 +++++++----------- unime/vite.config.ts | 2 +- 6 files changed, 128 insertions(+), 78 deletions(-) create mode 100644 unime/src/lib/components/StatusIndicator.svelte create mode 100644 unime/src/lib/components/index.js diff --git a/unime/src-tauri/tauri.conf.json b/unime/src-tauri/tauri.conf.json index 1e9a07a28..1a2dcabea 100644 --- a/unime/src-tauri/tauri.conf.json +++ b/unime/src-tauri/tauri.conf.json @@ -27,7 +27,7 @@ "build": { "beforeDevCommand": "npm run dev", "beforeBuildCommand": "npm run build", - "devUrl": "http://localhost:5173", + "devUrl": "http://localhost:4173", "frontendDist": "../build" }, "bundle": { diff --git a/unime/src/lib/components/StatusIndicator.svelte b/unime/src/lib/components/StatusIndicator.svelte new file mode 100644 index 000000000..8ae7332b9 --- /dev/null +++ b/unime/src/lib/components/StatusIndicator.svelte @@ -0,0 +1,57 @@ + + + + +
+ {#if logoUrl} +
+ +
+ {/if} +

+ {title} +

+ {#if status === 'Success'} + + {:else if status === 'Failure'} + + {:else} + + {/if} +
+ + +{#if $$slots.popover && $open} +
+
+ +
+{/if} diff --git a/unime/src/lib/components/index.js b/unime/src/lib/components/index.js new file mode 100644 index 000000000..68aabf653 --- /dev/null +++ b/unime/src/lib/components/index.js @@ -0,0 +1,5 @@ +export { default as Button } from './atoms/Button.svelte'; +export { default as Image } from './atoms/Image.svelte'; +export { default as PaddedIcon } from './atoms/PaddedIcon.svelte'; +export { default as TopNavBar } from './molecules/navigation/TopNavBar.svelte'; +export { default as StatusIndicator } from './StatusIndicator.svelte'; diff --git a/unime/src/routes/(app)/scan/+page@.svelte b/unime/src/routes/(app)/scan/+page@.svelte index 42479f669..48e426715 100644 --- a/unime/src/routes/(app)/scan/+page@.svelte +++ b/unime/src/routes/(app)/scan/+page@.svelte @@ -97,10 +97,21 @@ logo_uri: undefined, redirect_uri: 'https://demo.ngdil.com/auth/callback', previously_connected: false, + // domain_validation: { + // status: 'Success', + // }, domain_validation: { - status: 'Unknown', + status: 'Failure', message: 'DomainLinkageConfiguration could not be fetched', }, + thuiswinkel_waarborg_validation: { + status: 'Success', + name: 'Thuiswinkel Waarborg', + logo_uri: 'https://www.thuiswinkel.org/Images/logo-thuiswinkel_waarborg.svg', + }, + // thuiswinkel_waarborg_validation: { + // status: 'Failure', + // }, }, }); }; diff --git a/unime/src/routes/prompt/accept-connection/+page.svelte b/unime/src/routes/prompt/accept-connection/+page.svelte index 7affca2bd..e3e8b3634 100644 --- a/unime/src/routes/prompt/accept-connection/+page.svelte +++ b/unime/src/routes/prompt/accept-connection/+page.svelte @@ -3,41 +3,37 @@ import { goto } from '$app/navigation'; import LL from '$i18n/i18n-svelte'; - import { fade } from 'svelte/transition'; - import type { ValidationResult } from '@bindings/user_prompt/ValidationResult'; - import { createPopover, melt } from '@melt-ui/svelte'; + import type { CurrentUserPrompt } from '@bindings/user_prompt/CurrentUserPrompt'; - import Button from '$lib/components/atoms/Button.svelte'; - import Image from '$lib/components/atoms/Image.svelte'; - import PaddedIcon from '$lib/components/atoms/PaddedIcon.svelte'; - import TopNavBar from '$lib/components/molecules/navigation/TopNavBar.svelte'; + import { Button, Image, PaddedIcon, StatusIndicator, TopNavBar } from '$lib/components'; import { dispatch } from '$lib/dispatcher'; import { error, state } from '$lib/stores'; import { hash } from '$lib/utils'; - import Check from '~icons/ph/check-bold'; import PlugsConnected from '~icons/ph/plugs-connected-fill'; - import QuestionMark from '~icons/ph/question-mark-bold'; import WarningCircle from '~icons/ph/warning-circle-fill'; - import X from '~icons/ph/x-bold'; - - const { - elements: { trigger, content, arrow }, - states: { open }, - } = createPopover(); let loading = false; - let client_name = $state.current_user_prompt.client_name; + // TypeScript does not know that the `current_user_prompt` is of type `accept-connection`. + // Extract the type rather than repeating the type definition. + type IsAcceptConnectionPrompt = T extends { type: 'accept-connection' } ? T : never; + type AcceptConnectionPrompt = IsAcceptConnectionPrompt; - const previously_connected = $state.current_user_prompt.previously_connected; - - const domain_validation: ValidationResult = $state.current_user_prompt.domain_validation; + // Use reactive statement to coerce the type only once. + const { + client_name, + domain_validation, + logo_uri, + previously_connected, + redirect_uri, + thuiswinkel_waarborg_validation, + } = $state.current_user_prompt as AcceptConnectionPrompt; - const hostname = new URL($state.current_user_prompt.redirect_uri).hostname; + $: ({ hostname } = new URL(redirect_uri)); - const imageId = $state.current_user_prompt?.logo_uri ? hash($state.current_user_prompt?.logo_uri) : '_'; + const imageId = logo_uri ? hash(logo_uri) : '_'; // When an error is received, cancel the flow and redirect to the "me" page error.subscribe((err) => { @@ -57,7 +53,7 @@ history.back()} disabled={loading} />
- {#if $state.current_user_prompt.logo_uri} + {#if logo_uri}
@@ -93,63 +89,44 @@
{/if} + -
-

- {$LL.SCAN.CONNECTION_REQUEST.CONNECTED_PREVIOUSLY()} -

- {#if previously_connected} - - {:else} - - {/if} -
+ + -
-
-

{$LL.DOMAIN_LINKAGE.TITLE()}

- {#if $open} -
-
-
- {#if domain_validation.status === 'Success'} - -

{$LL.DOMAIN_LINKAGE.SUCCESS()}

- {:else if domain_validation.status === 'Failure'} -

{$LL.DOMAIN_LINKAGE.FAILURE()}

- -

{$LL.DOMAIN_LINKAGE.CAUTION()}

- {:else} -

{$LL.DOMAIN_LINKAGE.UNKNOWN()}

- -

{$LL.DOMAIN_LINKAGE.CAUTION()}

- {/if} - - {#if $state.dev_mode !== 'Off' && domain_validation.message} - -

{domain_validation.message}

- {/if} -
-
+ +
+ {#if domain_validation.status === 'Success'} + +

{$LL.DOMAIN_LINKAGE.SUCCESS()}

+ {:else if domain_validation.status === 'Failure'} +

{$LL.DOMAIN_LINKAGE.FAILURE()}

+ +

{$LL.DOMAIN_LINKAGE.CAUTION()}

+ {:else} +

{$LL.DOMAIN_LINKAGE.UNKNOWN()}

+ +

{$LL.DOMAIN_LINKAGE.CAUTION()}

+ {/if} + + {#if $state.dev_mode !== 'Off' && domain_validation.message} + +

{domain_validation.message}

{/if}
- {#if domain_validation.status === 'Success'} - - {:else if domain_validation.status === 'Failure'} - - {:else} - - {/if} -
+ + + + {#if thuiswinkel_waarborg_validation.status === 'Success' && thuiswinkel_waarborg_validation.name} + + {/if}
diff --git a/unime/vite.config.ts b/unime/vite.config.ts index 6a3e838d7..3ee263d01 100644 --- a/unime/vite.config.ts +++ b/unime/vite.config.ts @@ -19,7 +19,7 @@ export default defineConfig({ clearScreen: false, server: { host: '0.0.0.0', - port: 5173, + port: 4173, strictPort: true, hmr: { protocol: 'ws', From 768fc78375bd1d7a934274acb4dc62be08fa670c Mon Sep 17 00:00:00 2001 From: Nander Stabel Date: Thu, 27 Jun 2024 15:59:16 +0200 Subject: [PATCH 03/19] fix: fix logo_url binding, renameto thuiswinkel_validation --- .../bindings/user_prompt/CurrentUserPrompt.ts | 2 +- .../bindings/user_prompt/ValidationResult.ts | 2 +- .../src/state/did/validate_domain_linkage.rs | 2 +- .../state/did/validate_thuiswinkel_waarborg.rs | 16 ++++++++++++++++ .../reducers/read_authorization_request.rs | 4 ++-- identity-wallet/src/state/user_prompt.rs | 4 ++-- 6 files changed, 23 insertions(+), 7 deletions(-) diff --git a/identity-wallet/bindings/user_prompt/CurrentUserPrompt.ts b/identity-wallet/bindings/user_prompt/CurrentUserPrompt.ts index 20f2075ca..cef05f19d 100644 --- a/identity-wallet/bindings/user_prompt/CurrentUserPrompt.ts +++ b/identity-wallet/bindings/user_prompt/CurrentUserPrompt.ts @@ -1,4 +1,4 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. 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_waarborg_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, }; \ No newline at end of file +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, }; \ No newline at end of file diff --git a/identity-wallet/bindings/user_prompt/ValidationResult.ts b/identity-wallet/bindings/user_prompt/ValidationResult.ts index 10ca542da..07457efbc 100644 --- a/identity-wallet/bindings/user_prompt/ValidationResult.ts +++ b/identity-wallet/bindings/user_prompt/ValidationResult.ts @@ -1,4 +1,4 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. import type { ValidationStatus } from "./ValidationStatus"; -export interface ValidationResult { status: ValidationStatus, name?: string, logo_uri?: String, message?: string, } \ No newline at end of file +export interface ValidationResult { status: ValidationStatus, name?: string, logo_uri?: string, message?: string, } \ No newline at end of file diff --git a/identity-wallet/src/state/did/validate_domain_linkage.rs b/identity-wallet/src/state/did/validate_domain_linkage.rs index 04d24f74a..878f4c864 100644 --- a/identity-wallet/src/state/did/validate_domain_linkage.rs +++ b/identity-wallet/src/state/did/validate_domain_linkage.rs @@ -13,7 +13,7 @@ use ts_rs::TS; pub struct ValidationResult { pub(crate) status: ValidationStatus, pub(crate) name: Option, - #[ts(type = "String", optional)] + #[ts(type = "string", optional)] pub(crate) logo_uri: Option, pub(crate) message: Option, } diff --git a/identity-wallet/src/state/did/validate_thuiswinkel_waarborg.rs b/identity-wallet/src/state/did/validate_thuiswinkel_waarborg.rs index 835558841..f0506561a 100644 --- a/identity-wallet/src/state/did/validate_thuiswinkel_waarborg.rs +++ b/identity-wallet/src/state/did/validate_thuiswinkel_waarborg.rs @@ -9,10 +9,14 @@ use crate::{ }; use did_manager::Resolver; use identity_iota::core::ToJson; +use log::info; 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, @@ -25,6 +29,8 @@ pub async fn validate_thuiswinkel_waarborg(did: &str) -> ValidationResult { } }; + info!("Document: {:?}", document); + // Extract the URL of the Linked Verifiable Presentation from the Docoment. let linked_verifiable_presentation_url = match document .service() @@ -53,6 +59,11 @@ pub async fn validate_thuiswinkel_waarborg(did: &str) -> ValidationResult { } }; + 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; @@ -68,6 +79,8 @@ pub async fn validate_thuiswinkel_waarborg(did: &str) -> ValidationResult { } }; + 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) = @@ -118,6 +131,9 @@ pub async fn validate_thuiswinkel_waarborg(did: &str) -> ValidationResult { } }; + info!("Thuiswinkel Waarborg Name: {:?}", name); + info!("Thuiswinkel Waarborg Image: {:?}", thuiswinkel_waarborg_image); + ValidationResult { status: ValidationStatus::Success, name, 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 c3d5bd081..ed3834b0e 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 @@ -83,7 +83,7 @@ pub async fn read_authorization_request(state: AppState, action: Action) -> Resu let domain_validation = 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_waarborg_validation = validate_thuiswinkel_waarborg(did).await; + let thuiswinkel_validation = validate_thuiswinkel_waarborg(did).await; drop(state_guard); @@ -98,7 +98,7 @@ pub async fn read_authorization_request(state: AppState, action: Action) -> Resu redirect_uri, previously_connected, domain_validation, - thuiswinkel_waarborg_validation, + thuiswinkel_validation, }), ..state }); diff --git a/identity-wallet/src/state/user_prompt.rs b/identity-wallet/src/state/user_prompt.rs index ea40b543c..d0352ad37 100644 --- a/identity-wallet/src/state/user_prompt.rs +++ b/identity-wallet/src/state/user_prompt.rs @@ -29,7 +29,7 @@ pub enum CurrentUserPrompt { redirect_uri: String, previously_connected: bool, domain_validation: ValidationResult, - thuiswinkel_waarborg_validation: ValidationResult, + thuiswinkel_validation: ValidationResult, }, #[serde(rename = "credential-offer")] CredentialOffer { @@ -73,7 +73,7 @@ mod tests { redirect_uri: "https://example.com".to_string(), previously_connected: false, domain_validation: Default::default(), - thuiswinkel_waarborg_validation: Default::default(), + thuiswinkel_validation: Default::default(), }; assert_eq!( serde_json::to_string(&prompt).unwrap(), From a781199742ee4e2c6b51a9c19cd8dfb14946174c Mon Sep 17 00:00:00 2001 From: Thilo Maier Date: Thu, 27 Jun 2024 21:51:18 +0200 Subject: [PATCH 04/19] Rename `thuiswinkel_waarborg_verification` to `thuiswinkel_verification` in frontend --- unime/src/routes/(app)/scan/+page@.svelte | 4 ++-- .../routes/prompt/accept-connection/+page.svelte | 16 +++++----------- 2 files changed, 7 insertions(+), 13 deletions(-) diff --git a/unime/src/routes/(app)/scan/+page@.svelte b/unime/src/routes/(app)/scan/+page@.svelte index 48e426715..03c93a915 100644 --- a/unime/src/routes/(app)/scan/+page@.svelte +++ b/unime/src/routes/(app)/scan/+page@.svelte @@ -104,12 +104,12 @@ status: 'Failure', message: 'DomainLinkageConfiguration could not be fetched', }, - thuiswinkel_waarborg_validation: { + thuiswinkel_validation: { status: 'Success', name: 'Thuiswinkel Waarborg', logo_uri: 'https://www.thuiswinkel.org/Images/logo-thuiswinkel_waarborg.svg', }, - // thuiswinkel_waarborg_validation: { + // thuiswinkel_validation: { // status: 'Failure', // }, }, diff --git a/unime/src/routes/prompt/accept-connection/+page.svelte b/unime/src/routes/prompt/accept-connection/+page.svelte index e3e8b3634..2c76f0234 100644 --- a/unime/src/routes/prompt/accept-connection/+page.svelte +++ b/unime/src/routes/prompt/accept-connection/+page.svelte @@ -22,14 +22,8 @@ type AcceptConnectionPrompt = IsAcceptConnectionPrompt; // Use reactive statement to coerce the type only once. - const { - client_name, - domain_validation, - logo_uri, - previously_connected, - redirect_uri, - thuiswinkel_waarborg_validation, - } = $state.current_user_prompt as AcceptConnectionPrompt; + const { client_name, domain_validation, logo_uri, previously_connected, redirect_uri, thuiswinkel_validation } = + $state.current_user_prompt as AcceptConnectionPrompt; $: ({ hostname } = new URL(redirect_uri)); @@ -120,11 +114,11 @@
- {#if thuiswinkel_waarborg_validation.status === 'Success' && thuiswinkel_waarborg_validation.name} + {#if thuiswinkel_validation.status === 'Success' && thuiswinkel_validation.name} {/if}
From 987f8611b48722be88884ea51359385cc568a20d Mon Sep 17 00:00:00 2001 From: Nander Stabel Date: Fri, 28 Jun 2024 10:39:17 +0200 Subject: [PATCH 05/19] feat: add `issuance_date` to `ValidationResult` --- .../bindings/user_prompt/ValidationResult.ts | 2 +- .../src/state/did/validate_domain_linkage.rs | 1 + .../did/validate_thuiswinkel_waarborg.rs | 42 +++++++++++-------- 3 files changed, 27 insertions(+), 18 deletions(-) diff --git a/identity-wallet/bindings/user_prompt/ValidationResult.ts b/identity-wallet/bindings/user_prompt/ValidationResult.ts index 07457efbc..9a5206b0d 100644 --- a/identity-wallet/bindings/user_prompt/ValidationResult.ts +++ b/identity-wallet/bindings/user_prompt/ValidationResult.ts @@ -1,4 +1,4 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. import type { ValidationStatus } from "./ValidationStatus"; -export interface ValidationResult { status: ValidationStatus, name?: string, logo_uri?: string, message?: string, } \ No newline at end of file +export interface ValidationResult { status: ValidationStatus, name?: string, logo_uri?: string, issuance_date?: string, message?: string, } \ No newline at end of file diff --git a/identity-wallet/src/state/did/validate_domain_linkage.rs b/identity-wallet/src/state/did/validate_domain_linkage.rs index 878f4c864..5a673975f 100644 --- a/identity-wallet/src/state/did/validate_domain_linkage.rs +++ b/identity-wallet/src/state/did/validate_domain_linkage.rs @@ -15,6 +15,7 @@ pub struct ValidationResult { pub(crate) name: Option, #[ts(type = "string", optional)] pub(crate) logo_uri: Option, + pub(crate) issuance_date: Option, pub(crate) message: Option, } diff --git a/identity-wallet/src/state/did/validate_thuiswinkel_waarborg.rs b/identity-wallet/src/state/did/validate_thuiswinkel_waarborg.rs index f0506561a..1a07b1a30 100644 --- a/identity-wallet/src/state/did/validate_thuiswinkel_waarborg.rs +++ b/identity-wallet/src/state/did/validate_thuiswinkel_waarborg.rs @@ -10,6 +10,7 @@ use crate::{ 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; @@ -83,7 +84,7 @@ pub async fn validate_thuiswinkel_waarborg(did: &str) -> ValidationResult { // Extract the `name` and `thuiswinkel_waarborg_image` from the Linked Verifiable Presentation to be displayed in // the frontend. - let (name, thuiswinkel_waarborg_image) = + let (name, thuiswinkel_waarborg_image, issuance_date) = match get_unverified_jwt_claims(&serde_json::json!(linked_verifiable_presentation)) .get("vp") .and_then(|vp| { @@ -94,20 +95,21 @@ pub async fn validate_thuiswinkel_waarborg(did: &str) -> ValidationResult { .map(|verifiable_credential| get_unverified_jwt_claims(&verifiable_credential)) .and_then(|verifiable_credential| { verifiable_credential.get("vc").and_then(|vc| { - vc.get("credentialSubject").and_then(|credential_subject| { - credential_subject - .get("name") - .and_then(serde_json::Value::as_str) - .map(ToString::to_string) - .and_then(|name| { - Some(( - Some(name), - credential_subject - .get("thuiswinkel_waarborg_image") - .and_then(serde_json::Value::as_str) - .map(url::Url::from_str), - )) - }) + 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), + credential_subject + .get("issuanceDate") + .and_then(Value::as_str) + .map(ToString::to_string), + ) }) }) }) { @@ -118,9 +120,13 @@ pub async fn validate_thuiswinkel_waarborg(did: &str) -> ValidationResult { &hash(thuiswinkel_waarborg_image.as_str()), ) .await; - (display_properties.0, Some(thuiswinkel_waarborg_image)) + ( + display_properties.0, + Some(thuiswinkel_waarborg_image), + display_properties.2, + ) } else { - (display_properties.0, None) + (display_properties.0, None, display_properties.2) } } None => { @@ -133,11 +139,13 @@ pub async fn validate_thuiswinkel_waarborg(did: &str) -> ValidationResult { 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() } } From dca2caaa2ad1e54e8757358e4d68c69386676ff1 Mon Sep 17 00:00:00 2001 From: Thilo Maier Date: Fri, 28 Jun 2024 12:02:21 +0200 Subject: [PATCH 06/19] Change label for domain verification --- unime/src/i18n/de-DE/index.ts | 2 +- unime/src/i18n/en/index.ts | 2 +- unime/src/i18n/nl-NL/index.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/unime/src/i18n/de-DE/index.ts b/unime/src/i18n/de-DE/index.ts index f57fc385a..b164e16ae 100644 --- a/unime/src/i18n/de-DE/index.ts +++ b/unime/src/i18n/de-DE/index.ts @@ -289,7 +289,7 @@ const de = { }, }, DOMAIN_LINKAGE: { - TITLE: 'Verifiziert', + TITLE: 'Verifizierte Website', SUCCESS: 'UniMe konnte die Identität erfolgreich verifizieren, um dir einen sicheren Login zu ermöglichen.', FAILURE: 'UniMe konnte die Verknüpfung der Identität mit der Domain nicht überprüfen.', UNKNOWN: 'UniMe konnte keinen Nachweis über die verbundene Identität der Domain finden.', diff --git a/unime/src/i18n/en/index.ts b/unime/src/i18n/en/index.ts index f940cf465..7ef1ea4a4 100644 --- a/unime/src/i18n/en/index.ts +++ b/unime/src/i18n/en/index.ts @@ -288,7 +288,7 @@ const en = { }, }, DOMAIN_LINKAGE: { - TITLE: 'Verified', + TITLE: 'Verified website', SUCCESS: 'UniMe successfully verified the identity to provide you with a secure login.', FAILURE: 'UniMe could not verify the linkage of the identity to the domain.', UNKNOWN: "UniMe could not find any proof of the domain's associated identity.", diff --git a/unime/src/i18n/nl-NL/index.ts b/unime/src/i18n/nl-NL/index.ts index b49b59053..aa5bb6189 100644 --- a/unime/src/i18n/nl-NL/index.ts +++ b/unime/src/i18n/nl-NL/index.ts @@ -288,7 +288,7 @@ const nl = { }, }, DOMAIN_LINKAGE: { - TITLE: 'Geverifieerd', + TITLE: 'Geverifieerde website', SUCCESS: 'UniMe heeft de identiteit met succes geverifieerd om u een veilige login te geven.', FAILURE: 'UniMe kon de koppeling van de identiteit aan het domein niet verifiëren.', UNKNOWN: 'UniMe kon geen bewijs vinden van de bijbehorende identiteit van het domein.', From 2ff57af6f6b06411135202bb0cff94202b85d6ab Mon Sep 17 00:00:00 2001 From: Nander Stabel Date: Fri, 28 Jun 2024 16:51:41 +0200 Subject: [PATCH 07/19] fix: fix `issuance_date` --- .../src/state/did/validate_thuiswinkel_waarborg.rs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/identity-wallet/src/state/did/validate_thuiswinkel_waarborg.rs b/identity-wallet/src/state/did/validate_thuiswinkel_waarborg.rs index 1a07b1a30..3731d592c 100644 --- a/identity-wallet/src/state/did/validate_thuiswinkel_waarborg.rs +++ b/identity-wallet/src/state/did/validate_thuiswinkel_waarborg.rs @@ -105,10 +105,7 @@ pub async fn validate_thuiswinkel_waarborg(did: &str) -> ValidationResult { .get("thuiswinkel_waarborg_image") .and_then(Value::as_str) .map(url::Url::parse), - credential_subject - .get("issuanceDate") - .and_then(Value::as_str) - .map(ToString::to_string), + vc.get("issuanceDate").and_then(Value::as_str).map(ToString::to_string), ) }) }) From 1c509343cba955e8449be03d1184a902dee25442 Mon Sep 17 00:00:00 2001 From: Thilo Maier Date: Fri, 28 Jun 2024 23:53:39 +0200 Subject: [PATCH 08/19] Add issuance date and change logo position --- .../src/lib/components/StatusIndicator.svelte | 20 ++++++++++++------- unime/src/lib/utils.ts | 7 +++++++ unime/src/routes/(app)/scan/+page@.svelte | 1 + .../prompt/accept-connection/+page.svelte | 9 ++++++++- 4 files changed, 29 insertions(+), 8 deletions(-) diff --git a/unime/src/lib/components/StatusIndicator.svelte b/unime/src/lib/components/StatusIndicator.svelte index 8ae7332b9..e02c9ab08 100644 --- a/unime/src/lib/components/StatusIndicator.svelte +++ b/unime/src/lib/components/StatusIndicator.svelte @@ -13,6 +13,7 @@ export let status: ValidationStatus; export let title: string; + export let description: string | undefined = undefined; export let logoUrl: string | undefined = undefined; const { @@ -25,16 +26,21 @@
+
+

+ {title} +

+ {#if description} +

{description}

+ {/if} +
+ {#if logoUrl} -
- -
+ {/if} -

- {title} -

+ {#if status === 'Success'} {:else if status === 'Failure'} diff --git a/unime/src/lib/utils.ts b/unime/src/lib/utils.ts index fef70bc44..58ef4a304 100644 --- a/unime/src/lib/utils.ts +++ b/unime/src/lib/utils.ts @@ -1,4 +1,5 @@ import { Sha256 } from '@aws-crypto/sha256-js'; +import type { Locale } from '@bindings/profile_settings/Locale'; import { convertFileSrc } from '@tauri-apps/api/core'; import { appDataDir, join } from '@tauri-apps/api/path'; import { exists } from '@tauri-apps/plugin-fs'; @@ -51,3 +52,9 @@ export const hash = (data: string): string => { .map((i) => i.toString(16).padStart(2, '0')) .join(''); }; + +export function formatDate(isoDate: string, locale: Locale) { + return new Intl.DateTimeFormat(locale, { + dateStyle: 'medium', + }).format(new Date(isoDate)); +} diff --git a/unime/src/routes/(app)/scan/+page@.svelte b/unime/src/routes/(app)/scan/+page@.svelte index 03c93a915..39ffc91aa 100644 --- a/unime/src/routes/(app)/scan/+page@.svelte +++ b/unime/src/routes/(app)/scan/+page@.svelte @@ -107,6 +107,7 @@ thuiswinkel_validation: { status: 'Success', name: 'Thuiswinkel Waarborg', + issuance_date: '2024-06-26T08:03:53Z', logo_uri: 'https://www.thuiswinkel.org/Images/logo-thuiswinkel_waarborg.svg', }, // thuiswinkel_validation: { diff --git a/unime/src/routes/prompt/accept-connection/+page.svelte b/unime/src/routes/prompt/accept-connection/+page.svelte index 2c76f0234..948a779ed 100644 --- a/unime/src/routes/prompt/accept-connection/+page.svelte +++ b/unime/src/routes/prompt/accept-connection/+page.svelte @@ -9,7 +9,7 @@ import { Button, Image, PaddedIcon, StatusIndicator, TopNavBar } from '$lib/components'; import { dispatch } from '$lib/dispatcher'; import { error, state } from '$lib/stores'; - import { hash } from '$lib/utils'; + import { formatDate, hash } from '$lib/utils'; import PlugsConnected from '~icons/ph/plugs-connected-fill'; import WarningCircle from '~icons/ph/warning-circle-fill'; @@ -25,6 +25,8 @@ const { client_name, domain_validation, logo_uri, previously_connected, redirect_uri, thuiswinkel_validation } = $state.current_user_prompt as AcceptConnectionPrompt; + const profile_settings = $state.profile_settings; + $: ({ hostname } = new URL(redirect_uri)); const imageId = logo_uri ? hash(logo_uri) : '_'; @@ -115,9 +117,14 @@ {#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} From 72ebdfbbdab728757a3180a7444dbe82391b785b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 2 Jul 2024 15:12:32 +0200 Subject: [PATCH 09/19] chore(deps-dev): bump prettier-plugin-tailwindcss from 0.6.1 to 0.6.5 (#259) Bumps [prettier-plugin-tailwindcss](https://github.com/tailwindlabs/prettier-plugin-tailwindcss) from 0.6.1 to 0.6.5. - [Release notes](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/releases) - [Changelog](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/blob/main/CHANGELOG.md) - [Commits](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/compare/v0.6.1...v0.6.5) --- updated-dependencies: - dependency-name: prettier-plugin-tailwindcss dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package-lock.json | 8 ++++---- package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index f718beda7..14e411e74 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,7 +8,7 @@ "@ianvs/prettier-plugin-sort-imports": "^4.1.0", "prettier": "^3.3.0", "prettier-plugin-svelte": "^3.2.3", - "prettier-plugin-tailwindcss": "^0.6.1" + "prettier-plugin-tailwindcss": "^0.6.5" } }, "node_modules/@ampproject/remapping": { @@ -789,9 +789,9 @@ } }, "node_modules/prettier-plugin-tailwindcss": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/prettier-plugin-tailwindcss/-/prettier-plugin-tailwindcss-0.6.1.tgz", - "integrity": "sha512-AnbeYZu0WGj+QgKciUgdMnRxrqcxltleZPgdwfA5104BHM3siBLONN/HLW1yS2HvzSNkzpQ/JAj+LN0jcJO+0w==", + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/prettier-plugin-tailwindcss/-/prettier-plugin-tailwindcss-0.6.5.tgz", + "integrity": "sha512-axfeOArc/RiGHjOIy9HytehlC0ZLeMaqY09mm8YCkMzznKiDkwFzOpBvtuhuv3xG5qB73+Mj7OCe2j/L1ryfuQ==", "dev": true, "engines": { "node": ">=14.21.3" diff --git a/package.json b/package.json index a6c576fc2..ab3f8694b 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "@ianvs/prettier-plugin-sort-imports": "^4.1.0", "prettier": "^3.3.0", "prettier-plugin-svelte": "^3.2.3", - "prettier-plugin-tailwindcss": "^0.6.1" + "prettier-plugin-tailwindcss": "^0.6.5" }, "type": "module" } From d69775c427922e7267fd9ee15bc5574a6d0b042c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 2 Jul 2024 15:12:49 +0200 Subject: [PATCH 10/19] chore(deps-dev): bump @ianvs/prettier-plugin-sort-imports (#260) Bumps [@ianvs/prettier-plugin-sort-imports](https://github.com/ianvs/prettier-plugin-sort-imports) from 4.2.1 to 4.3.0. - [Release notes](https://github.com/ianvs/prettier-plugin-sort-imports/releases) - [Changelog](https://github.com/IanVS/prettier-plugin-sort-imports/blob/main/CHANGELOG.md) - [Commits](https://github.com/ianvs/prettier-plugin-sort-imports/compare/v4.2.1...v4.3.0) --- updated-dependencies: - dependency-name: "@ianvs/prettier-plugin-sort-imports" dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package-lock.json | 8 ++++---- package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index 14e411e74..bf2b7367d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5,7 +5,7 @@ "packages": { "": { "devDependencies": { - "@ianvs/prettier-plugin-sort-imports": "^4.1.0", + "@ianvs/prettier-plugin-sort-imports": "^4.3.0", "prettier": "^3.3.0", "prettier-plugin-svelte": "^3.2.3", "prettier-plugin-tailwindcss": "^0.6.5" @@ -332,9 +332,9 @@ } }, "node_modules/@ianvs/prettier-plugin-sort-imports": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/@ianvs/prettier-plugin-sort-imports/-/prettier-plugin-sort-imports-4.2.1.tgz", - "integrity": "sha512-NKN1LVFWUDGDGr3vt+6Ey3qPeN/163uR1pOPAlkWpgvAqgxQ6kSdUf1F0it8aHUtKRUzEGcK38Wxd07O61d7+Q==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@ianvs/prettier-plugin-sort-imports/-/prettier-plugin-sort-imports-4.3.0.tgz", + "integrity": "sha512-OOMtUcO4J3LoL63dOKAe7bn+lSRRPeit2DqNHpx+wvBp3Grejo2PMaK4Mp1mwy8pnat64ccSgk/lBZbsAdLErw==", "dev": true, "dependencies": { "@babel/core": "^7.24.0", diff --git a/package.json b/package.json index ab3f8694b..c56213805 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "format:check": "prettier . --check" }, "devDependencies": { - "@ianvs/prettier-plugin-sort-imports": "^4.1.0", + "@ianvs/prettier-plugin-sort-imports": "^4.3.0", "prettier": "^3.3.0", "prettier-plugin-svelte": "^3.2.3", "prettier-plugin-tailwindcss": "^0.6.5" From cebee1effb35cb59a9feb712abed06eca4331f46 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 2 Jul 2024 15:30:56 +0200 Subject: [PATCH 11/19] chore(deps-dev): bump prettier-plugin-svelte from 3.2.3 to 3.2.5 (#262) Bumps [prettier-plugin-svelte](https://github.com/sveltejs/prettier-plugin-svelte) from 3.2.3 to 3.2.5. - [Changelog](https://github.com/sveltejs/prettier-plugin-svelte/blob/master/CHANGELOG.md) - [Commits](https://github.com/sveltejs/prettier-plugin-svelte/compare/v3.2.3...v3.2.5) --- updated-dependencies: - dependency-name: prettier-plugin-svelte dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Nander Stabel --- package-lock.json | 8 ++++---- package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index bf2b7367d..d99060dc1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7,7 +7,7 @@ "devDependencies": { "@ianvs/prettier-plugin-sort-imports": "^4.3.0", "prettier": "^3.3.0", - "prettier-plugin-svelte": "^3.2.3", + "prettier-plugin-svelte": "^3.2.5", "prettier-plugin-tailwindcss": "^0.6.5" } }, @@ -779,9 +779,9 @@ } }, "node_modules/prettier-plugin-svelte": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/prettier-plugin-svelte/-/prettier-plugin-svelte-3.2.3.tgz", - "integrity": "sha512-wJq8RunyFlWco6U0WJV5wNCM7zpBFakS76UBSbmzMGpncpK98NZABaE+s7n8/APDCEVNHXC5Mpq+MLebQtsRlg==", + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/prettier-plugin-svelte/-/prettier-plugin-svelte-3.2.5.tgz", + "integrity": "sha512-vP/M/Goc8z4iVIvrwXwbrYVjJgA0Hf8PO1G4LBh/ocSt6vUP6sLvyu9F3ABEGr+dbKyxZjEKLkeFsWy/yYl0HQ==", "dev": true, "peerDependencies": { "prettier": "^3.0.0", diff --git a/package.json b/package.json index c56213805..bed8ec8aa 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "devDependencies": { "@ianvs/prettier-plugin-sort-imports": "^4.3.0", "prettier": "^3.3.0", - "prettier-plugin-svelte": "^3.2.3", + "prettier-plugin-svelte": "^3.2.5", "prettier-plugin-tailwindcss": "^0.6.5" }, "type": "module" From ad9da848f0cd267208f05d5e0aa08a3e3d440c4a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 2 Jul 2024 20:39:34 +0200 Subject: [PATCH 12/19] chore(deps-dev): bump prettier from 3.3.0 to 3.3.2 (#261) Bumps [prettier](https://github.com/prettier/prettier) from 3.3.0 to 3.3.2. - [Release notes](https://github.com/prettier/prettier/releases) - [Changelog](https://github.com/prettier/prettier/blob/main/CHANGELOG.md) - [Commits](https://github.com/prettier/prettier/compare/3.3.0...3.3.2) --- updated-dependencies: - dependency-name: prettier dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package-lock.json | 8 ++++---- package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index d99060dc1..abe992b38 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6,7 +6,7 @@ "": { "devDependencies": { "@ianvs/prettier-plugin-sort-imports": "^4.3.0", - "prettier": "^3.3.0", + "prettier": "^3.3.2", "prettier-plugin-svelte": "^3.2.5", "prettier-plugin-tailwindcss": "^0.6.5" } @@ -764,9 +764,9 @@ "dev": true }, "node_modules/prettier": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.0.tgz", - "integrity": "sha512-J9odKxERhCQ10OC2yb93583f6UnYutOeiV5i0zEDS7UGTdUt0u+y8erxl3lBKvwo/JHyyoEdXjwp4dke9oyZ/g==", + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.2.tgz", + "integrity": "sha512-rAVeHYMcv8ATV5d508CFdn+8/pHPpXeIid1DdrPwXnaAdH7cqjVbpJaT5eq4yRAFU/lsbwYwSF/n5iNrdJHPQA==", "dev": true, "bin": { "prettier": "bin/prettier.cjs" diff --git a/package.json b/package.json index bed8ec8aa..b2e5a1a3d 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ }, "devDependencies": { "@ianvs/prettier-plugin-sort-imports": "^4.3.0", - "prettier": "^3.3.0", + "prettier": "^3.3.2", "prettier-plugin-svelte": "^3.2.5", "prettier-plugin-tailwindcss": "^0.6.5" }, From 941345424b104a7e6f111654197d2b60e881039d Mon Sep 17 00:00:00 2001 From: Thilo Maier Date: Tue, 2 Jul 2024 21:08:59 +0200 Subject: [PATCH 13/19] Make `imageId` reactive --- unime/src/routes/prompt/accept-connection/+page.svelte | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/unime/src/routes/prompt/accept-connection/+page.svelte b/unime/src/routes/prompt/accept-connection/+page.svelte index 948a779ed..4bc7ccb16 100644 --- a/unime/src/routes/prompt/accept-connection/+page.svelte +++ b/unime/src/routes/prompt/accept-connection/+page.svelte @@ -17,19 +17,17 @@ let loading = false; // TypeScript does not know that the `current_user_prompt` is of type `accept-connection`. - // Extract the type rather than repeating the type definition. + // Extract the type from `CurrentUserPrompt`. type IsAcceptConnectionPrompt = T extends { type: 'accept-connection' } ? T : never; type AcceptConnectionPrompt = IsAcceptConnectionPrompt; - // Use reactive statement to coerce the type only once. const { client_name, domain_validation, logo_uri, previously_connected, redirect_uri, thuiswinkel_validation } = $state.current_user_prompt as AcceptConnectionPrompt; const profile_settings = $state.profile_settings; $: ({ hostname } = new URL(redirect_uri)); - - const imageId = logo_uri ? hash(logo_uri) : '_'; + $: imageId = logo_uri ? hash(logo_uri) : '_'; // When an error is received, cancel the flow and redirect to the "me" page error.subscribe((err) => { From b0d8ff74c7774e43b1a2920c065b85565d2998c5 Mon Sep 17 00:00:00 2001 From: Nander Stabel Date: Tue, 9 Jul 2024 10:42:51 +0200 Subject: [PATCH 14/19] fix: fix lint errors --- .../state/qr_code/reducers/read_authorization_request.rs | 4 ++-- identity-wallet/src/state/user_prompt.rs | 6 +++--- .../src-tauri/tests/fixtures/states/accept_connection.json | 3 +++ 3 files changed, 8 insertions(+), 5 deletions(-) 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 ed3834b0e..283a3692a 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 @@ -80,10 +80,10 @@ pub async fn read_authorization_request(state: AppState, action: Action) -> Resu let did = siopv2_authorization_request.body.client_id.as_str(); - let domain_validation = validate_domain_linkage(url, did).await; + 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 = validate_thuiswinkel_waarborg(did).await; + let thuiswinkel_validation = Box::new(validate_thuiswinkel_waarborg(did).await); drop(state_guard); diff --git a/identity-wallet/src/state/user_prompt.rs b/identity-wallet/src/state/user_prompt.rs index d0352ad37..67013d350 100644 --- a/identity-wallet/src/state/user_prompt.rs +++ b/identity-wallet/src/state/user_prompt.rs @@ -28,8 +28,8 @@ pub enum CurrentUserPrompt { logo_uri: Option, redirect_uri: String, previously_connected: bool, - domain_validation: ValidationResult, - thuiswinkel_validation: ValidationResult, + domain_validation: Box, + thuiswinkel_validation: Box, }, #[serde(rename = "credential-offer")] CredentialOffer { @@ -77,7 +77,7 @@ mod tests { }; 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","message":null}}"# + 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"}}"# ); } } diff --git a/unime/src-tauri/tests/fixtures/states/accept_connection.json b/unime/src-tauri/tests/fixtures/states/accept_connection.json index 7c2c4aac9..df179ff23 100644 --- a/unime/src-tauri/tests/fixtures/states/accept_connection.json +++ b/unime/src-tauri/tests/fixtures/states/accept_connection.json @@ -15,6 +15,9 @@ "domain_validation": { "status": "Unknown", "message": "error decoding response body: expected value at line 1 column 1" + }, + "thuiswinkel_validation": { + "status": "Unknown" } } } From 61fb522f2cf9b936e574ef6691724fa04a4f2b52 Mon Sep 17 00:00:00 2001 From: Nander Stabel Date: Wed, 11 Sep 2024 15:23:09 +0200 Subject: [PATCH 15/19] 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} From e10209841094555a589057925c3a05c8ec104d78 Mon Sep 17 00:00:00 2001 From: Nander Stabel Date: Wed, 11 Sep 2024 15:30:28 +0200 Subject: [PATCH 16/19] refactor: apply clippy suggestions --- Cargo.toml | 2 +- identity-wallet/Cargo.toml | 2 +- ...alidate_linked_verifiable_presentations.rs | 26 +++++++++++-------- unime/src-tauri/Cargo.toml | 2 +- 4 files changed, 18 insertions(+), 14 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 72074e26d..b8338e929 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -46,4 +46,4 @@ keywords = ["identity", "did", "ssi", "wallet", "siopv2"] license = "Apache-2.0" repository = "https://github.com/impierce/identity-wallet" edition = "2021" -rust-version = "1.75.0" +rust-version = "1.76.0" diff --git a/identity-wallet/Cargo.toml b/identity-wallet/Cargo.toml index 85cfc14a2..ff87d90b7 100644 --- a/identity-wallet/Cargo.toml +++ b/identity-wallet/Cargo.toml @@ -2,7 +2,7 @@ name = "identity-wallet" version = "0.6.8" edition = "2021" -rust-version = "1.75.0" +rust-version.workspace = true [dependencies] tauri.workspace = true diff --git a/identity-wallet/src/state/did/validate_linked_verifiable_presentations.rs b/identity-wallet/src/state/did/validate_linked_verifiable_presentations.rs index cc2e0187d..3873c5320 100644 --- a/identity-wallet/src/state/did/validate_linked_verifiable_presentations.rs +++ b/identity-wallet/src/state/did/validate_linked_verifiable_presentations.rs @@ -124,7 +124,7 @@ async fn get_validated_linked_presentation_data( linked_verifiable_presentation_url: Url, ) -> Option> { OptionFuture::from( - validate_linked_verifiable_presentation(&holder_document, linked_verifiable_presentation_url) + validate_linked_verifiable_presentation(holder_document, linked_verifiable_presentation_url) .await .map(|linked_verifiable_presentation| { get_validated_linked_credential_data(resolver, linked_verifiable_presentation) @@ -205,7 +205,7 @@ async fn get_validated_linked_credential_data( 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), + OneOrMany::Many(subjects) => subjects.first(), }; OptionFuture::from(credential_subject.map(|credential_subject| async { @@ -296,7 +296,7 @@ async fn get_issuer_linked_domains(issuer_document: &CoreDocument) -> Vec { linked_domain.get("origins").and_then(|origins| { origins.as_array().and_then(|origins| { origins - .into_iter() + .iter() .map(|origin| { origin.as_str().and_then(|origin| { origin @@ -643,7 +643,7 @@ mod tests { holder.add_well_known_did_json().await; assert_eq!( - validate_linked_verifiable_presentations(&holder.did_document.id().to_string()).await, + validate_linked_verifiable_presentations(holder.did_document.id().to_string().as_ref()).await, vec![ vec![LinkedVerifiableCredentialData { name: Some("Webshop".to_string()), @@ -701,7 +701,7 @@ mod tests { holder.add_well_known_did_json().await; assert_eq!( - validate_linked_verifiable_presentations(&holder.did_document.id().to_string()).await, + validate_linked_verifiable_presentations(holder.did_document.id().to_string().as_ref()).await, // The domain linkage validation of the issuer failed, so the linked verifiable credential is not considered. vec![vec![]] ); @@ -736,7 +736,7 @@ mod tests { get_linked_verifiable_presentation_urls(&holder.did_document.service()[0]) .unwrap() .iter() - .all(|item| vec![ + .all(|item| [ format!("{}{}", holder.domain, linked_verifiable_presentation_endpoint) .parse() .unwrap(), @@ -814,7 +814,11 @@ mod tests { // Succesfully validate the linked domain. assert_eq!( - get_validated_linked_domains(&[issuer1.domain.clone()], &issuer1.did_document.id().to_string()).await, + get_validated_linked_domains( + &[issuer1.domain.clone()], + issuer1.did_document.id().to_string().as_ref() + ) + .await, vec![issuer1.domain.clone()] ); @@ -822,7 +826,7 @@ mod tests { assert_eq!( get_validated_linked_domains( &[issuer1.domain.clone(), "http://invalid-domain.org".parse().unwrap()], - &issuer1.did_document.id().to_string() + issuer1.did_document.id().to_string().as_ref() ) .await, vec![issuer1.domain.clone()] @@ -840,7 +844,7 @@ mod tests { assert_eq!( get_validated_linked_domains( &[issuer1.domain.clone(), issuer2.domain.clone()], - &issuer1.did_document.id().to_string() + issuer1.did_document.id().to_string().as_ref() ) .await, vec![issuer1.domain.clone()] @@ -861,10 +865,10 @@ mod tests { // 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() + issuer1.did_document.id().to_string().as_ref() ) .await .iter() - .all(|item| vec![issuer1.domain.clone(), issuer2.domain.clone()].contains(item))); + .all(|item| [issuer1.domain.clone(), issuer2.domain.clone()].contains(item))); } } diff --git a/unime/src-tauri/Cargo.toml b/unime/src-tauri/Cargo.toml index d37b25e9c..14b1db718 100644 --- a/unime/src-tauri/Cargo.toml +++ b/unime/src-tauri/Cargo.toml @@ -8,7 +8,7 @@ keywords = ["identity", "did", "ssi", "wallet", "siopv2"] license = "Apache-2.0" repository = "https://github.com/impierce/identity-wallet" edition = "2021" -rust-version = "1.75.0" +rust-version.workspace = true [lib] name = "unime" From ca6792fc4eaf2f80d07d302071d5fd7e121642b0 Mon Sep 17 00:00:00 2001 From: Nander Stabel Date: Wed, 18 Sep 2024 21:12:06 +0200 Subject: [PATCH 17/19] feat: use `ServiceEndpoint::from` --- .../did/validate_linked_verifiable_presentations.rs | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/identity-wallet/src/state/did/validate_linked_verifiable_presentations.rs b/identity-wallet/src/state/did/validate_linked_verifiable_presentations.rs index 3873c5320..223d311ac 100644 --- a/identity-wallet/src/state/did/validate_linked_verifiable_presentations.rs +++ b/identity-wallet/src/state/did/validate_linked_verifiable_presentations.rs @@ -352,7 +352,7 @@ mod tests { use did_manager::SecretManager; use identity_credential::domain_linkage::{DomainLinkageConfiguration, DomainLinkageCredentialBuilder}; use identity_iota::{ - core::{Duration, FromJson as _, Object, Timestamp, Url}, + core::{Duration, FromJson as _, Object, OrderedSet, Timestamp, Url}, credential::{Credential, CredentialBuilder, Presentation}, document::{CoreDocument, Service, ServiceEndpoint}, verification::jws::JwsAlgorithm, @@ -507,11 +507,10 @@ mod tests { let service_endpoint = match urls.len() { // Value::String - 1 => serde_json::from_value::(serde_json::json!(urls[0])), + 1 => ServiceEndpoint::from(urls[0].clone()), // Value::Array - _ => serde_json::from_value::(serde_json::json!(urls)), - } - .unwrap(); + _ => ServiceEndpoint::from(OrderedSet::from_iter(urls)), + }; let service = Service::builder(Default::default()) .id(format!("{}#{service_id}", self.did_document.id()).parse().unwrap()) .type_("LinkedVerifiablePresentation") From 6a0776f1b1c00a8b8e58b4c6e27424ee204486f9 Mon Sep 17 00:00:00 2001 From: Nander Stabel Date: Wed, 18 Sep 2024 21:57:22 +0200 Subject: [PATCH 18/19] refactor: remove unused variables --- unime/src/routes/prompt/accept-connection/+page.svelte | 6 ------ 1 file changed, 6 deletions(-) diff --git a/unime/src/routes/prompt/accept-connection/+page.svelte b/unime/src/routes/prompt/accept-connection/+page.svelte index 189feeb30..b403ca041 100644 --- a/unime/src/routes/prompt/accept-connection/+page.svelte +++ b/unime/src/routes/prompt/accept-connection/+page.svelte @@ -5,7 +5,6 @@ import LL from '$i18n/i18n-svelte'; import type { CurrentUserPrompt } from '@bindings/user_prompt/CurrentUserPrompt'; - import { createPopover } from '@melt-ui/svelte'; import { Button, Image, PaddedIcon, StatusIndicator, TopNavBar } from '$lib/components'; import { dispatch } from '$lib/dispatcher'; @@ -13,11 +12,6 @@ import { error, state } from '$lib/stores'; import { formatDate, hash } from '$lib/utils'; - const { - elements: { trigger, content, arrow }, - states: { open }, - } = createPopover(); - const profile_settings = $state.profile_settings; let loading = false; From 74f96bdecf4cf2756d2cda3847e7888a3e0bdd3f Mon Sep 17 00:00:00 2001 From: Nander Stabel Date: Fri, 20 Sep 2024 14:42:11 +0200 Subject: [PATCH 19/19] refactor: replace icon imports --- unime/src/lib/components/StatusIndicator.svelte | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/unime/src/lib/components/StatusIndicator.svelte b/unime/src/lib/components/StatusIndicator.svelte index e02c9ab08..40a418294 100644 --- a/unime/src/lib/components/StatusIndicator.svelte +++ b/unime/src/lib/components/StatusIndicator.svelte @@ -5,12 +5,9 @@ import { createPopover, melt } from '@melt-ui/svelte'; import { Image } from '$lib/components'; + import { CheckBoldIcon, QuestionMarkBoldIcon, XBoldIcon } from '$lib/icons'; import { hash } from '$lib/utils'; - import Check from '~icons/ph/check-bold'; - import QuestionMark from '~icons/ph/question-mark-bold'; - import X from '~icons/ph/x-bold'; - export let status: ValidationStatus; export let title: string; export let description: string | undefined = undefined; @@ -42,11 +39,11 @@ {/if} {#if status === 'Success'} - + {:else if status === 'Failure'} - + {:else} - + {/if}