Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

feat: Add third-party issuer validation during SIOP auth request #256

Open
wants to merge 24 commits into
base: dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
b1858c1
WIP
nanderstabel Jun 26, 2024
a87866c
Add Thuiswinkel verification checkmark
maiertech Jun 27, 2024
768fc78
fix: fix logo_url binding, renameto thuiswinkel_validation
nanderstabel Jun 27, 2024
a781199
Rename `thuiswinkel_waarborg_verification` to `thuiswinkel_verificati…
maiertech Jun 27, 2024
987f861
feat: add `issuance_date` to `ValidationResult`
nanderstabel Jun 28, 2024
dca2caa
Change label for domain verification
maiertech Jun 28, 2024
2ff57af
fix: fix `issuance_date`
nanderstabel Jun 28, 2024
1c50934
Add issuance date and change logo position
maiertech Jun 28, 2024
72ebdfb
chore(deps-dev): bump prettier-plugin-tailwindcss from 0.6.1 to 0.6.5…
dependabot[bot] Jul 2, 2024
d69775c
chore(deps-dev): bump @ianvs/prettier-plugin-sort-imports (#260)
dependabot[bot] Jul 2, 2024
cebee1e
chore(deps-dev): bump prettier-plugin-svelte from 3.2.3 to 3.2.5 (#262)
dependabot[bot] Jul 2, 2024
ad9da84
chore(deps-dev): bump prettier from 3.3.0 to 3.3.2 (#261)
dependabot[bot] Jul 2, 2024
9413454
Make `imageId` reactive
maiertech Jul 2, 2024
b792b33
Merge branch 'dev' into feat/linked-vp
maiertech Jul 3, 2024
b0d8ff7
fix: fix lint errors
nanderstabel Jul 9, 2024
7b50623
Merge branch 'dev' into feat/linked-vp
nanderstabel Jul 9, 2024
bad8844
Merge branch 'dev' into feat/linked-vp
nanderstabel Sep 5, 2024
61fb522
feat: add `validate_linked_verifiable_presentations`
nanderstabel Sep 11, 2024
e102098
refactor: apply clippy suggestions
nanderstabel Sep 11, 2024
ca6792f
feat: use `ServiceEndpoint::from`
nanderstabel Sep 18, 2024
2835141
Merge branch 'dev' into feat/linked-vp
nanderstabel Sep 18, 2024
6a0776f
refactor: remove unused variables
nanderstabel Sep 18, 2024
ab4d19b
Merge branch 'dev' into feat/linked-vp
nanderstabel Sep 19, 2024
74f96bd
refactor: replace icon imports
nanderstabel Sep 20, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions identity-wallet/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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"
Expand Down
2 changes: 1 addition & 1 deletion identity-wallet/bindings/user_prompt/CurrentUserPrompt.ts
Original file line number Diff line number Diff line change
@@ -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<string, any>, } | { "type": "share-credentials", client_name: string, logo_uri?: string, options: Array<string>, };
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<string, any>, } | { "type": "share-credentials", client_name: string, logo_uri?: string, options: Array<string>, };
2 changes: 1 addition & 1 deletion identity-wallet/bindings/user_prompt/ValidationResult.ts
Original file line number Diff line number Diff line change
@@ -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, }
export interface ValidationResult { status: ValidationStatus, name?: string, logo_uri?: string, issuance_date?: string, message?: string, }
3 changes: 3 additions & 0 deletions identity-wallet/src/state/did/mod.rs
Original file line number Diff line number Diff line change
@@ -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;
18 changes: 15 additions & 3 deletions identity-wallet/src/state/did/validate_domain_linkage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,18 @@ 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<String>,
#[ts(type = "string", optional)]
pub(crate) logo_uri: Option<url::Url>,
pub(crate) issuance_date: Option<String>,
pub(crate) message: Option<String>,
}

Expand All @@ -32,6 +38,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()
};
}
};
Expand All @@ -47,6 +54,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()
};
}
};
Expand All @@ -63,12 +71,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()
}
}
}
Expand Down Expand Up @@ -175,6 +184,7 @@ mod tests {
ValidationResult {
status: ValidationStatus::Unknown,
message: Some("failed to decode JSON".to_string()),
..Default::default()
}
);
}
Expand Down Expand Up @@ -234,7 +244,8 @@ mod tests {
result,
ValidationResult {
status: ValidationStatus::Failure,
message: Some("invalid issuer DID".to_string())
message: Some("invalid issuer DID".to_string()),
..Default::default()
}
);
}
Expand Down Expand Up @@ -267,7 +278,8 @@ mod tests {
result,
ValidationResult {
status: ValidationStatus::Failure,
message: Some("invalid issuer DID".to_string())
message: Some("invalid issuer DID".to_string()),
..Default::default()
}
);
}
Expand Down
156 changes: 156 additions & 0 deletions identity-wallet/src/state/did/validate_thuiswinkel_waarborg.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
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))
.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").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<String, String> {
// 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())
}
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -77,7 +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 = Box::new(validate_thuiswinkel_waarborg(did).await);

drop(state_guard);

Expand All @@ -92,6 +98,7 @@ pub async fn read_authorization_request(state: AppState, action: Action) -> Resu
redirect_uri,
previously_connected,
domain_validation,
thuiswinkel_validation,
}),
..state
});
Expand Down
6 changes: 4 additions & 2 deletions identity-wallet/src/state/user_prompt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@ pub enum CurrentUserPrompt {
logo_uri: Option<String>,
redirect_uri: String,
previously_connected: bool,
domain_validation: ValidationResult,
domain_validation: Box<ValidationResult>,
thuiswinkel_validation: Box<ValidationResult>,
},
#[serde(rename = "credential-offer")]
CredentialOffer {
Expand Down Expand Up @@ -72,10 +73,11 @@ mod tests {
redirect_uri: "https://example.com".to_string(),
previously_connected: false,
domain_validation: Default::default(),
thuiswinkel_validation: 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","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"}}"#
);
}
}
2 changes: 1 addition & 1 deletion unime/src-tauri/tauri.conf.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
"build": {
"beforeDevCommand": "npm run dev",
"beforeBuildCommand": "npm run build",
"devUrl": "http://localhost:5173",
"devUrl": "http://localhost:4173",
daniel-mader marked this conversation as resolved.
Show resolved Hide resolved
"frontendDist": "../build"
},
"bundle": {
Expand Down
3 changes: 3 additions & 0 deletions unime/src-tauri/tests/fixtures/states/accept_connection.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@
"domain_validation": {
"status": "Unknown",
"message": "error decoding response body: expected value at line 1 column 1"
},
"thuiswinkel_validation": {
"status": "Unknown"
}
}
}
2 changes: 1 addition & 1 deletion unime/src/i18n/de-DE/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.',
Expand Down
2 changes: 1 addition & 1 deletion unime/src/i18n/en/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -288,7 +288,7 @@ const en = {
},
},
DOMAIN_LINKAGE: {
TITLE: 'Verified',
TITLE: 'Verified website',
daniel-mader marked this conversation as resolved.
Show resolved Hide resolved
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.",
Expand Down
2 changes: 1 addition & 1 deletion unime/src/i18n/nl-NL/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.',
Expand Down
Loading
Loading