From e581d456fab0058938c2f0b424715e8ac8b36da9 Mon Sep 17 00:00:00 2001 From: Nander Stabel Date: Mon, 13 May 2024 22:05:23 +0200 Subject: [PATCH] feat: add support for OpenID4VP (#53) * feat: implement `Subject` responsible for singing and verifying data * feat: add `oid4vp` support * docs: update `/v1/authorization_request` in `openapi` file * chore: update postman collection * fix: set `presentation_definition` to contain `VerifiableCredential` * fix: use slice directly * style: rename endpoint deserialization helpers * docs: add comment for `GenericAuthorizationResponse` * style: change `AuthorizationRequestsRequest` to `AuthorizationRequestsEndpointRequest` --- Cargo.lock | 58 ++++-- Cargo.toml | 9 +- agent_api_rest/Cargo.toml | 1 + agent_api_rest/openapi.yaml | 6 + .../postman/ssi-agent.postman_collection.json | 5 +- .../issuance/credential_issuer/credential.rs | 8 +- agent_api_rest/src/issuance/credentials.rs | 4 +- agent_api_rest/src/issuance/offers.rs | 4 +- .../verification/authorization_requests.rs | 46 ++++- .../verification/relying_party/redirect.rs | 28 ++- agent_application/docker/docker-compose.yml | 4 + agent_application/src/main.rs | 13 +- agent_verification/Cargo.toml | 10 + .../presentation_definition.json | 23 +++ .../src/authorization_request/aggregate.rs | 186 +++++++++++++----- .../src/authorization_request/command.rs | 7 +- .../src/authorization_request/error.rs | 9 +- .../src/authorization_request/event.rs | 4 +- .../src/authorization_request/queries.rs | 22 +-- .../src/connection/aggregate.rs | 175 ++++++++++++---- agent_verification/src/connection/command.rs | 11 +- agent_verification/src/connection/error.rs | 4 + agent_verification/src/connection/event.rs | 2 + agent_verification/src/connection/queries.rs | 4 + agent_verification/src/generic_oid4vc.rs | 93 +++++++++ agent_verification/src/lib.rs | 1 + agent_verification/src/services.rs | 23 ++- 27 files changed, 590 insertions(+), 170 deletions(-) create mode 100644 agent_verification/presentation_definitions/presentation_definition.json create mode 100644 agent_verification/src/generic_oid4vc.rs diff --git a/Cargo.lock b/Cargo.lock index aeb3825d..7687dd6f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -73,6 +73,7 @@ dependencies = [ "oid4vc-core", "oid4vc-manager", "oid4vci", + "oid4vp", "rstest", "serde", "serde_json", @@ -149,7 +150,7 @@ dependencies = [ "did_manager", "futures", "jsonschema", - "jsonwebtoken", + "jsonwebtoken 8.3.0", "lazy_static", "oid4vc-core", "oid4vc-manager", @@ -226,11 +227,17 @@ dependencies = [ "cqrs-es", "did_manager", "futures", + "identity_core", + "identity_credential", + "jsonwebtoken 9.3.0", "lazy_static", "oid4vc-core", "oid4vc-manager", + "oid4vci", + "oid4vp", "rstest", "serde", + "serde_json", "serial_test", "siopv2", "thiserror", @@ -1814,7 +1821,7 @@ dependencies = [ [[package]] name = "dif-presentation-exchange" version = "0.1.0" -source = "git+https://git@github.com/impierce/openid4vc.git?rev=a932af7#a932af797117bd8f042335bf29695e00905800e8" +source = "git+https://git@github.com/impierce/openid4vc.git?rev=ce4e3fd#ce4e3fdcc5781660ab8900b22d8026a4b7ca9932" dependencies = [ "getset", "jsonpath_lib", @@ -3668,13 +3675,28 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6971da4d9c3aa03c3d8f3ff0f4155b534aad021292003895a469716b2a230378" dependencies = [ "base64 0.21.7", - "pem", + "pem 1.1.1", "ring 0.16.20", "serde", "serde_json", "simple_asn1 0.6.2", ] +[[package]] +name = "jsonwebtoken" +version = "9.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9ae10193d25051e74945f1ea2d0b42e03cc3b890f7e4cc5faa44997d808193f" +dependencies = [ + "base64 0.21.7", + "js-sys", + "pem 3.0.4", + "ring 0.17.8", + "serde", + "serde_json", + "simple_asn1 0.6.2", +] + [[package]] name = "k256" version = "0.13.3" @@ -4279,7 +4301,7 @@ dependencies = [ [[package]] name = "oid4vc-core" version = "0.1.0" -source = "git+https://git@github.com/impierce/openid4vc.git?rev=a932af7#a932af797117bd8f042335bf29695e00905800e8" +source = "git+https://git@github.com/impierce/openid4vc.git?rev=ce4e3fd#ce4e3fdcc5781660ab8900b22d8026a4b7ca9932" dependencies = [ "anyhow", "async-trait", @@ -4290,7 +4312,7 @@ dependencies = [ "ed25519-dalek 2.1.1", "getset", "is_empty", - "jsonwebtoken", + "jsonwebtoken 8.3.0", "lazy_static", "rand 0.8.5", "serde", @@ -4303,7 +4325,7 @@ dependencies = [ [[package]] name = "oid4vc-manager" version = "0.1.0" -source = "git+https://git@github.com/impierce/openid4vc.git?rev=a932af7#a932af797117bd8f042335bf29695e00905800e8" +source = "git+https://git@github.com/impierce/openid4vc.git?rev=ce4e3fd#ce4e3fdcc5781660ab8900b22d8026a4b7ca9932" dependencies = [ "anyhow", "async-trait", @@ -4316,7 +4338,7 @@ dependencies = [ "getset", "identity_core", "identity_credential", - "jsonwebtoken", + "jsonwebtoken 8.3.0", "oid4vc-core", "oid4vci", "oid4vp", @@ -4335,13 +4357,13 @@ dependencies = [ [[package]] name = "oid4vci" version = "0.1.0" -source = "git+https://git@github.com/impierce/openid4vc.git?rev=a932af7#a932af797117bd8f042335bf29695e00905800e8" +source = "git+https://git@github.com/impierce/openid4vc.git?rev=ce4e3fd#ce4e3fdcc5781660ab8900b22d8026a4b7ca9932" dependencies = [ "anyhow", "derivative", "dif-presentation-exchange", "getset", - "jsonwebtoken", + "jsonwebtoken 8.3.0", "lazy_static", "oid4vc-core", "paste", @@ -4358,7 +4380,7 @@ dependencies = [ [[package]] name = "oid4vp" version = "0.1.0" -source = "git+https://git@github.com/impierce/openid4vc.git?rev=a932af7#a932af797117bd8f042335bf29695e00905800e8" +source = "git+https://git@github.com/impierce/openid4vc.git?rev=ce4e3fd#ce4e3fdcc5781660ab8900b22d8026a4b7ca9932" dependencies = [ "anyhow", "chrono", @@ -4368,7 +4390,7 @@ dependencies = [ "identity_core", "identity_credential", "is_empty", - "jsonwebtoken", + "jsonwebtoken 8.3.0", "monostate", "oid4vc-core", "oid4vci", @@ -4638,6 +4660,16 @@ dependencies = [ "base64 0.13.1", ] +[[package]] +name = "pem" +version = "3.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e459365e590736a54c3fa561947c84837534b8e9af6fc5bf781307e82658fae" +dependencies = [ + "base64 0.22.1", + "serde", +] + [[package]] name = "pem-rfc7468" version = "0.3.1" @@ -6147,7 +6179,7 @@ dependencies = [ [[package]] name = "siopv2" version = "0.1.0" -source = "git+https://git@github.com/impierce/openid4vc.git?rev=a932af7#a932af797117bd8f042335bf29695e00905800e8" +source = "git+https://git@github.com/impierce/openid4vc.git?rev=ce4e3fd#ce4e3fdcc5781660ab8900b22d8026a4b7ca9932" dependencies = [ "anyhow", "async-trait", @@ -6158,7 +6190,7 @@ dependencies = [ "futures", "getset", "is_empty", - "jsonwebtoken", + "jsonwebtoken 8.3.0", "monostate", "oid4vc-core", "reqwest 0.11.27", diff --git a/Cargo.toml b/Cargo.toml index 342cb865..4815eaa4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,10 +18,11 @@ rust-version = "1.76.0" [workspace.dependencies] did_manager = { git = "https://git@github.com/impierce/did-manager.git", rev = "c70c0f1" } -siopv2 = { git = "https://git@github.com/impierce/openid4vc.git", rev = "a932af7" } -oid4vci = { git = "https://git@github.com/impierce/openid4vc.git", rev = "a932af7" } -oid4vc-core = { git = "https://git@github.com/impierce/openid4vc.git", rev = "a932af7" } -oid4vc-manager = { git = "https://git@github.com/impierce/openid4vc.git", rev = "a932af7" } +siopv2 = { git = "https://git@github.com/impierce/openid4vc.git", rev = "ce4e3fd" } +oid4vci = { git = "https://git@github.com/impierce/openid4vc.git", rev = "ce4e3fd" } +oid4vc-core = { git = "https://git@github.com/impierce/openid4vc.git", rev = "ce4e3fd" } +oid4vc-manager = { git = "https://git@github.com/impierce/openid4vc.git", rev = "ce4e3fd" } +oid4vp = { git = "https://git@github.com/impierce/openid4vc.git", rev = "ce4e3fd" } async-trait = "0.1" axum = { version = "0.7", features = ["tracing"] } diff --git a/agent_api_rest/Cargo.toml b/agent_api_rest/Cargo.toml index 35750b4e..088f0906 100644 --- a/agent_api_rest/Cargo.toml +++ b/agent_api_rest/Cargo.toml @@ -16,6 +16,7 @@ http-api-problem = "0.57" hyper = { version = "1.2" } oid4vc-core.workspace = true oid4vci.workspace = true +oid4vp.workspace = true serde.workspace = true serde_json.workspace = true siopv2.workspace = true diff --git a/agent_api_rest/openapi.yaml b/agent_api_rest/openapi.yaml index 4e35502e..897bbb7b 100644 --- a/agent_api_rest/openapi.yaml +++ b/agent_api_rest/openapi.yaml @@ -160,6 +160,12 @@ paths: nonce: type: string example: "0d520cbe176ab9e1f7888c70888020d84a69672a4baabd3ce1c6aaad8f6420c0" + state: + type: string + example: "84266fdbd31d4c2c6d0665f7e8380fa3" + presentation_definition_id: + type: string + example: "presentation_definition" required: - nonce responses: diff --git a/agent_api_rest/postman/ssi-agent.postman_collection.json b/agent_api_rest/postman/ssi-agent.postman_collection.json index 3f6764c9..ef85d4cb 100644 --- a/agent_api_rest/postman/ssi-agent.postman_collection.json +++ b/agent_api_rest/postman/ssi-agent.postman_collection.json @@ -23,7 +23,8 @@ "}", "" ], - "type": "text/javascript" + "type": "text/javascript", + "packages": {} } } ], @@ -251,7 +252,7 @@ "header": [], "body": { "mode": "raw", - "raw": "{\n \"nonce\": \"this is a nonce\"\n}", + "raw": "{\n \"nonce\": \"this is a nonce\",\n \"presentation_definition_id\": \"presentation_definition\"\n}", "options": { "raw": { "language": "json" diff --git a/agent_api_rest/src/issuance/credential_issuer/credential.rs b/agent_api_rest/src/issuance/credential_issuer/credential.rs index a003a308..7a2e7dc5 100644 --- a/agent_api_rest/src/issuance/credential_issuer/credential.rs +++ b/agent_api_rest/src/issuance/credential_issuer/credential.rs @@ -120,7 +120,9 @@ mod tests { use crate::{ app, - issuance::{credential_issuer::token::tests::token, credentials::CredentialsRequest, offers::tests::offers}, + issuance::{ + credential_issuer::token::tests::token, credentials::CredentialsEndpointRequest, offers::tests::offers, + }, tests::{BASE_URL, OFFER_ID}, }; @@ -170,14 +172,14 @@ mod tests { // The 'backend' server can either opt for an already signed credential... let credentials_endpoint_request = if is_self_signed { - CredentialsRequest { + CredentialsEndpointRequest { offer_id: offer_id.clone(), credential: json!("eyJ0eXAiOiJKV1QiLCJhbGciOiJFZERTQSIsImtpZCI6ImRpZDprZXk6ejZNa2lpZXlvTE1TVnNKQVp2N0pqZTV3V1NrREV5bVVna3lGOGtiY3JqWnBYM3FkI3o2TWtpaWV5b0xNU1ZzSkFadjdKamU1d1dTa0RFeW1VZ2t5RjhrYmNyalpwWDNxZCJ9.eyJpc3MiOiJkaWQ6a2V5Ono2TWtpaWV5b0xNU1ZzSkFadjdKamU1d1dTa0RFeW1VZ2t5RjhrYmNyalpwWDNxZCIsInN1YiI6ImRpZDprZXk6ejZNa2lpZXlvTE1TVnNKQVp2N0pqZTV3V1NrREV5bVVna3lGOGtiY3JqWnBYM3FkIiwiZXhwIjo5OTk5OTk5OTk5LCJpYXQiOjAsInZjIjp7IkBjb250ZXh0IjpbImh0dHBzOi8vd3d3LnczLm9yZy8yMDE4L2NyZWRlbnRpYWxzL3YxIiwiaHR0cHM6Ly9wdXJsLmltc2dsb2JhbC5vcmcvc3BlYy9vYi92M3AwL2NvbnRleHQtMy4wLjIuanNvbiJdLCJpZCI6Imh0dHA6Ly9leGFtcGxlLmNvbS9jcmVkZW50aWFscy8zNTI3IiwidHlwZSI6WyJWZXJpZmlhYmxlQ3JlZGVudGlhbCIsIk9wZW5CYWRnZUNyZWRlbnRpYWwiXSwiaXNzdWVyIjoiZGlkOmtleTp6Nk1raWlleW9MTVNWc0pBWnY3SmplNXdXU2tERXltVWdreUY4a2JjcmpacFgzcWQiLCJpc3N1YW5jZURhdGUiOiIyMDEwLTAxLTAxVDAwOjAwOjAwWiIsIm5hbWUiOiJUZWFtd29yayBCYWRnZSIsImNyZWRlbnRpYWxTdWJqZWN0Ijp7ImZpcnN0X25hbWUiOiJGZXJyaXMiLCJsYXN0X25hbWUiOiJSdXN0YWNlYW4iLCJpZCI6ImRpZDprZXk6ejZNa2lpZXlvTE1TVnNKQVp2N0pqZTV3V1NrREV5bVVna3lGOGtiY3JqWnBYM3FkIn19fQ.r7T_zOXP7E2k7eAPq5EF20shwrnPKK0mOCfNaB0phPEXVkYSG_sf6QygUDuJ8-P0yU4EEajgE0dxJuRfdMVDAQ"), is_signed: true, } } else { // ...or else, submitting the data that will be signed inside `UniCore`. - CredentialsRequest { + CredentialsEndpointRequest { offer_id: offer_id.clone(), credential: json!({ "credentialSubject": { diff --git a/agent_api_rest/src/issuance/credentials.rs b/agent_api_rest/src/issuance/credentials.rs index 71d15db2..9b8aab44 100644 --- a/agent_api_rest/src/issuance/credentials.rs +++ b/agent_api_rest/src/issuance/credentials.rs @@ -29,7 +29,7 @@ pub(crate) async fn get_credentials(State(state): State, Path(cre #[derive(Deserialize, Serialize)] #[serde(rename_all = "camelCase")] -pub struct CredentialsRequest { +pub struct CredentialsEndpointRequest { pub offer_id: String, pub credential: Value, #[serde(default)] @@ -43,7 +43,7 @@ pub(crate) async fn credentials( ) -> Response { info!("Request Body: {}", payload); - let Ok(CredentialsRequest { + let Ok(CredentialsEndpointRequest { offer_id, credential: data, is_signed, diff --git a/agent_api_rest/src/issuance/offers.rs b/agent_api_rest/src/issuance/offers.rs index 0202206d..a6f727c0 100644 --- a/agent_api_rest/src/issuance/offers.rs +++ b/agent_api_rest/src/issuance/offers.rs @@ -16,7 +16,7 @@ use tracing::info; #[derive(Deserialize, Serialize)] #[serde(rename_all = "camelCase")] -pub struct OffersRequest { +pub struct OffersEndpointRequest { pub offer_id: String, } @@ -24,7 +24,7 @@ pub struct OffersRequest { pub(crate) async fn offers(State(state): State, Json(payload): Json) -> Response { info!("Request Body: {}", payload); - let Ok(OffersRequest { offer_id }) = serde_json::from_value(payload) else { + let Ok(OffersEndpointRequest { offer_id }) = serde_json::from_value(payload) else { return (StatusCode::BAD_REQUEST, "invalid payload").into_response(); }; diff --git a/agent_api_rest/src/verification/authorization_requests.rs b/agent_api_rest/src/verification/authorization_requests.rs index 7f892815..ee81dd31 100644 --- a/agent_api_rest/src/verification/authorization_requests.rs +++ b/agent_api_rest/src/verification/authorization_requests.rs @@ -13,6 +13,7 @@ use axum::{ Json, }; use hyper::header; +use serde::{Deserialize, Serialize}; use serde_json::Value; use tracing::info; @@ -24,14 +25,21 @@ pub(crate) async fn get_authorization_requests( // Get the authorization request if it exists. match query_handler(&authorization_request_id, &state.query.authorization_request).await { Ok(Some(AuthorizationRequestView { - siopv2_authorization_request: Some(siopv2_authorization_request), + authorization_request: Some(authorization_request), .. - })) => (StatusCode::OK, Json(siopv2_authorization_request)).into_response(), + })) => (StatusCode::OK, Json(authorization_request)).into_response(), Ok(None) => StatusCode::NOT_FOUND.into_response(), _ => StatusCode::INTERNAL_SERVER_ERROR.into_response(), } } +#[derive(Deserialize, Serialize)] +pub struct AuthorizationRequestsEndpointRequest { + pub nonce: String, + pub state: Option, + pub presentation_definition_id: Option, +} + #[axum_macros::debug_handler] pub(crate) async fn authorization_requests( State(verification_state): State, @@ -39,17 +47,36 @@ pub(crate) async fn authorization_requests( ) -> Response { info!("Request Body: {}", payload); - let nonce = if let Some(nonce) = payload["nonce"].as_str() { - nonce - } else { - return (StatusCode::BAD_REQUEST, "nonce is required").into_response(); + let Ok(AuthorizationRequestsEndpointRequest { + nonce, + state, + presentation_definition_id, + }) = serde_json::from_value(payload) + else { + return (StatusCode::BAD_REQUEST, "invalid payload").into_response(); }; - let state = generate_random_string(); + let state = state.unwrap_or(generate_random_string()); + + // TODO: This needs to be properly fixed instead of reading the presentation definitions from the file system + // everytime a request is made. `PresentationDefinition`'s should be implemented as a proper `Aggregate`. This + // current suboptimal solution requires the `./tmp:/app/agent_api_rest` volume to be mounted in the `docker-compose.yml`. + let presentation_definition = presentation_definition_id.map(|presentation_definition_id| { + let project_root_dir = env!("CARGO_MANIFEST_DIR"); + + serde_json::from_reader( + std::fs::File::open(format!( + "{project_root_dir}/../agent_verification/presentation_definitions/{presentation_definition_id}.json" + )) + .unwrap(), + ) + .unwrap() + }); let command = AuthorizationRequestCommand::CreateAuthorizationRequest { nonce: nonce.to_string(), state: state.clone(), + presentation_definition, }; // Create the authorization request. @@ -75,7 +102,7 @@ pub(crate) async fn authorization_requests( // Return the credential. match query_handler(&state, &verification_state.query.authorization_request).await { Ok(Some(AuthorizationRequestView { - form_url_encoded_authorization_request, + form_url_encoded_authorization_request: Some(form_url_encoded_authorization_request), .. })) => ( StatusCode::CREATED, @@ -114,7 +141,8 @@ pub mod tests { .header(http::header::CONTENT_TYPE, mime::APPLICATION_JSON.as_ref()) .body(Body::from( serde_json::to_vec(&json!({ - "nonce": "nonce" + "nonce": "nonce", + "presentation_definition_id": "presentation_definition" })) .unwrap(), )) diff --git a/agent_api_rest/src/verification/relying_party/redirect.rs b/agent_api_rest/src/verification/relying_party/redirect.rs index 865ba513..3490fbab 100644 --- a/agent_api_rest/src/verification/relying_party/redirect.rs +++ b/agent_api_rest/src/verification/relying_party/redirect.rs @@ -1,7 +1,7 @@ use agent_shared::handlers::{command_handler, query_handler}; use agent_verification::{ authorization_request::queries::AuthorizationRequestView, connection::command::ConnectionCommand, - state::VerificationState, + generic_oid4vc::GenericAuthorizationResponse, state::VerificationState, }; use axum::{ extract::State, @@ -9,42 +9,40 @@ use axum::{ response::{IntoResponse, Response}, Form, }; -use oid4vc_core::authorization_response::AuthorizationResponse; -use siopv2::siopv2::SIOPv2; #[axum_macros::debug_handler] pub(crate) async fn redirect( State(verification_state): State, - Form(siopv2_authorization_response): Form>, + Form(authorization_response): Form, ) -> Response { - let authorization_request_id = if let Some(state) = siopv2_authorization_response.state.as_ref() { + let authorization_request_id = if let Some(state) = authorization_response.state() { state.clone() } else { return StatusCode::INTERNAL_SERVER_ERROR.into_response(); }; - // Retrieve the SIOPv2 authorization request. - let siopv2_authorization_request = match query_handler( + // Retrieve the authorization request. + let authorization_request = match query_handler( &authorization_request_id, &verification_state.query.authorization_request, ) .await { Ok(Some(AuthorizationRequestView { - siopv2_authorization_request: Some(siopv2_authorization_request), + authorization_request: Some(authorization_request), .. - })) => siopv2_authorization_request, + })) => authorization_request, _ => return StatusCode::INTERNAL_SERVER_ERROR.into_response(), }; - let connection_id = siopv2_authorization_request.body.client_id.clone(); + let connection_id = authorization_request.client_id(); - let command = ConnectionCommand::VerifySIOPv2AuthorizationResponse { - siopv2_authorization_request, - siopv2_authorization_response, + let command = ConnectionCommand::VerifyAuthorizationResponse { + authorization_request, + authorization_response, }; - // Verify the SIOPv2 authorization response. + // Verify the authorization response. if command_handler(&connection_id, &verification_state.command.connection, command) .await .is_err() @@ -80,7 +78,7 @@ pub mod tests { DidMethod, SubjectSyntaxType, }; use oid4vc_manager::ProviderManager; - use siopv2::authorization_request::ClientMetadataParameters; + use siopv2::{authorization_request::ClientMetadataParameters, siopv2::SIOPv2}; use tower::Service; use wiremock::{ matchers::{method, path}, diff --git a/agent_application/docker/docker-compose.yml b/agent_application/docker/docker-compose.yml index 24629618..cfd27ee9 100644 --- a/agent_application/docker/docker-compose.yml +++ b/agent_application/docker/docker-compose.yml @@ -45,3 +45,7 @@ services: volumes: - ../../agent_secret_manager/tests/res/test.stronghold:/app/res/stronghold - ../../agent_event_publisher_http/config.yml:/app/agent_event_publisher_http/config.yml + - ../../agent_verification/presentation_definitions:/app/agent_verification/presentation_definitions + # TODO: Remove this. This is a workaround that ensures that the `agent_verification/presentation_definitions` + # folder can be accessed by the agent from the `fn authorization_requests` endpoint. + - ./tmp:/app/agent_api_rest diff --git a/agent_application/src/main.rs b/agent_application/src/main.rs index 77262c83..d39f48a8 100644 --- a/agent_application/src/main.rs +++ b/agent_application/src/main.rs @@ -8,7 +8,7 @@ use agent_shared::config; use agent_store::{in_memory, postgres, EventPublisher}; use agent_verification::services::VerificationServices; use oid4vc_core::{client_metadata::ClientMetadataResource, DidMethod, SubjectSyntaxType}; -use siopv2::authorization_request::ClientMetadataParameters; +use serde_json::json; use tracing::info; use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; @@ -34,12 +34,21 @@ async fn main() { ClientMetadataResource::ClientMetadata { client_name: None, logo_uri: None, - extension: ClientMetadataParameters { + extension: siopv2::authorization_request::ClientMetadataParameters { subject_syntax_types_supported: vec![SubjectSyntaxType::Did( DidMethod::from_str(&default_did_method).unwrap(), )], }, }, + ClientMetadataResource::ClientMetadata { + client_name: None, + logo_uri: None, + // TODO: fix this once `vp_formats` is public. + extension: serde_json::from_value(json!({ + "vp_formats": {} + })) + .unwrap(), + }, &default_did_method, )); diff --git a/agent_verification/Cargo.toml b/agent_verification/Cargo.toml index da15997a..30de9e9b 100644 --- a/agent_verification/Cargo.toml +++ b/agent_verification/Cargo.toml @@ -15,7 +15,9 @@ cqrs-es.workspace = true futures.workspace = true oid4vc-core.workspace = true oid4vc-manager.workspace = true +oid4vp.workspace = true serde.workspace = true +serde_json.workspace = true siopv2.workspace = true thiserror.workspace = true tracing.workspace = true @@ -27,7 +29,15 @@ agent_shared = { path = "../agent_shared", features = ["test"] } agent_verification = { path = ".", features = ["test"] } did_manager.workspace = true +identity_core = "1.2.0" +identity_credential = { version = "1.2.0", default-features = false, features = [ + "validator", + "credential", + "presentation" +] } +jsonwebtoken = "9.3" lazy_static.workspace = true +oid4vci.workspace = true rstest.workspace = true serial_test = "3.0" diff --git a/agent_verification/presentation_definitions/presentation_definition.json b/agent_verification/presentation_definitions/presentation_definition.json new file mode 100644 index 00000000..5cf21a9c --- /dev/null +++ b/agent_verification/presentation_definitions/presentation_definition.json @@ -0,0 +1,23 @@ +{ + "id":"Verifiable Presentation request for sign-on", + "input_descriptors":[ + { + "id":"Request for Verifiable Credential", + "constraints":{ + "fields":[ + { + "path":[ + "$.vc.type" + ], + "filter":{ + "type":"array", + "contains":{ + "const":"VerifiableCredential" + } + } + } + ] + } + } + ] +} diff --git a/agent_verification/src/authorization_request/aggregate.rs b/agent_verification/src/authorization_request/aggregate.rs index 87f06b6a..0dd3e828 100644 --- a/agent_verification/src/authorization_request/aggregate.rs +++ b/agent_verification/src/authorization_request/aggregate.rs @@ -1,25 +1,20 @@ -use std::sync::Arc; - +use super::{command::AuthorizationRequestCommand, error::AuthorizationRequestError, event::AuthorizationRequestEvent}; +use crate::{ + generic_oid4vc::{GenericAuthorizationRequest, OID4VPAuthorizationRequest, SIOPv2AuthorizationRequest}, + services::VerificationServices, +}; use agent_shared::config; use async_trait::async_trait; use cqrs_es::Aggregate; -use oid4vc_core::{ - authorization_request::{ByReference, Object}, - scope::Scope, -}; +use oid4vc_core::{authorization_request::ByReference, scope::Scope}; +use oid4vp::authorization_request::ClientIdScheme; use serde::{Deserialize, Serialize}; -use siopv2::siopv2::SIOPv2; +use std::sync::Arc; use tracing::info; -use crate::services::VerificationServices; - -use super::{command::AuthorizationRequestCommand, error::AuthorizationRequestError, event::AuthorizationRequestEvent}; - -pub type SIOPv2AuthorizationRequest = oid4vc_core::authorization_request::AuthorizationRequest>; - #[derive(Debug, Serialize, Deserialize, Default)] pub struct AuthorizationRequest { - authorization_request: Option, + authorization_request: Option, form_url_encoded_authorization_request: Option, signed_authorization_request_object: Option, } @@ -37,12 +32,17 @@ impl Aggregate for AuthorizationRequest { async fn handle(&self, command: Self::Command, services: &Self::Services) -> Result, Self::Error> { use AuthorizationRequestCommand::*; + use AuthorizationRequestError::*; use AuthorizationRequestEvent::*; info!("Handling command: {:?}", command); match command { - CreateAuthorizationRequest { state, nonce } => { + CreateAuthorizationRequest { + state, + nonce, + presentation_definition, + } => { let default_subject_syntax_type = services.relying_party.default_subject_syntax_type().to_string(); let verifier = &services.verifier; let verifier_did = verifier.identifier(&default_subject_syntax_type).unwrap(); @@ -51,18 +51,35 @@ impl Aggregate for AuthorizationRequest { let request_uri = format!("{url}/request/{state}").parse().unwrap(); let redirect_uri = format!("{url}/redirect").parse::().unwrap(); - let authorization_request = Box::new( - SIOPv2AuthorizationRequest::builder() - .client_id(verifier_did.clone()) - .scope(Scope::openid()) - .redirect_uri(redirect_uri) - .response_mode("direct_post".to_string()) - .client_metadata(services.client_metadata.clone()) - .state(state) - .nonce(nonce) - .build() - .unwrap(), - ); + let authorization_request = Box::new(if let Some(presentation_definition) = presentation_definition { + GenericAuthorizationRequest::OID4VP(Box::new( + OID4VPAuthorizationRequest::builder() + .client_id(verifier_did.clone()) + .client_id_scheme(ClientIdScheme::Did) + .scope(Scope::openid()) + .redirect_uri(redirect_uri) + .response_mode("direct_post".to_string()) + .presentation_definition(presentation_definition) + .client_metadata(services.oid4vp_client_metadata.clone()) + .state(state) + .nonce(nonce) + .build() + .map_err(AuthorizationRequestBuilderError)?, + )) + } else { + GenericAuthorizationRequest::SIOPv2(Box::new( + SIOPv2AuthorizationRequest::builder() + .client_id(verifier_did.clone()) + .scope(Scope::openid()) + .redirect_uri(redirect_uri) + .response_mode("direct_post".to_string()) + .client_metadata(services.siopv2_client_metadata.clone()) + .state(state) + .nonce(nonce) + .build() + .map_err(AuthorizationRequestBuilderError)?, + )) + }); let form_url_encoded_authorization_request = oid4vc_core::authorization_request::AuthorizationRequest { custom_url_scheme: "openid".to_string(), @@ -83,10 +100,23 @@ impl Aggregate for AuthorizationRequest { SignAuthorizationRequestObject => { let relying_party = &services.relying_party; - // TODO: Add error handling - let signed_authorization_request_object = relying_party - .encode(self.authorization_request.as_ref().unwrap()) - .unwrap(); + // TODO(oid4vc): This functionality should be moved to the `oid4vc-manager` crate. + let authorization_request = self.authorization_request.as_ref().ok_or(MissingAuthorizationRequest)?; + let signed_authorization_request_object = if let Some(siopv2_authorization_request) = + authorization_request.as_siopv2_authorization_request() + { + relying_party + .encode(siopv2_authorization_request) + .map_err(AuthorizationRequestSigningError)? + } else if let Some(oid4vp_authorization_request) = + authorization_request.as_oid4vp_authorization_request() + { + relying_party + .encode(oid4vp_authorization_request) + .map_err(AuthorizationRequestSigningError)? + } else { + unreachable!("`GenericAuthorizationRequest` cannot be `None`") + }; Ok(vec![AuthorizationRequestObjectSigned { signed_authorization_request_object, @@ -130,8 +160,9 @@ pub mod tests { use lazy_static::lazy_static; use oid4vc_core::Subject as _; use oid4vc_core::{client_metadata::ClientMetadataResource, DidMethod, SubjectSyntaxType}; + use oid4vp::PresentationDefinition; use rstest::rstest; - use siopv2::authorization_request::ClientMetadataParameters; + use serde_json::json; use crate::services::test_utils::test_verification_services; @@ -149,10 +180,11 @@ pub mod tests { .when(AuthorizationRequestCommand::CreateAuthorizationRequest { state: "state".to_string(), nonce: "nonce".to_string(), + presentation_definition: None, }) .then_expect_events(vec![ AuthorizationRequestEvent::AuthorizationRequestCreated { - authorization_request: Box::new(siopv2_authorization_request(verifier_did_method)), + authorization_request: Box::new(authorization_request("id_token", verifier_did_method)), }, AuthorizationRequestEvent::FormUrlEncodedAuthorizationRequestCreated { form_url_encoded_authorization_request: form_url_encoded_authorization_request(verifier_did_method), @@ -168,7 +200,7 @@ pub mod tests { AuthorizationRequestTestFramework::with(verification_services) .given(vec![ AuthorizationRequestEvent::AuthorizationRequestCreated { - authorization_request: Box::new(siopv2_authorization_request(verifier_did_method)), + authorization_request: Box::new(authorization_request("id_token", verifier_did_method)), }, AuthorizationRequestEvent::FormUrlEncodedAuthorizationRequestCreated { form_url_encoded_authorization_request: form_url_encoded_authorization_request(verifier_did_method), @@ -180,31 +212,64 @@ pub mod tests { }]); } - fn verifier_did(did_method: &str) -> String { + pub fn verifier_did(did_method: &str) -> String { VERIFIER.identifier(did_method).unwrap() } - pub fn client_metadata(did_method: &str) -> ClientMetadataResource { + pub fn siopv2_client_metadata( + did_method: &str, + ) -> ClientMetadataResource { ClientMetadataResource::ClientMetadata { client_name: None, logo_uri: None, - extension: ClientMetadataParameters { + extension: siopv2::authorization_request::ClientMetadataParameters { subject_syntax_types_supported: vec![SubjectSyntaxType::Did(DidMethod::from_str(did_method).unwrap())], }, } } - pub fn siopv2_authorization_request(did_method: &str) -> SIOPv2AuthorizationRequest { - SIOPv2AuthorizationRequest::builder() - .client_id(verifier_did(did_method)) - .scope(Scope::openid()) - .redirect_uri(REDIRECT_URI.clone()) - .response_mode("direct_post".to_string()) - .client_metadata(client_metadata(did_method)) - .nonce("nonce".to_string()) - .state("state".to_string()) - .build() - .unwrap() + pub fn oid4vp_client_metadata() -> ClientMetadataResource { + ClientMetadataResource::ClientMetadata { + client_name: None, + logo_uri: None, + // TODO: fix this once `vp_formats` is public. + extension: serde_json::from_value(json!({ + "vp_formats": {} + })) + .unwrap(), + } + } + + pub fn authorization_request(response_type: &str, did_method: &str) -> GenericAuthorizationRequest { + match response_type { + "id_token" => GenericAuthorizationRequest::SIOPv2(Box::new( + SIOPv2AuthorizationRequest::builder() + .client_id(verifier_did(did_method)) + .scope(Scope::openid()) + .redirect_uri(REDIRECT_URI.clone()) + .response_mode("direct_post".to_string()) + .client_metadata(siopv2_client_metadata(did_method)) + .nonce("nonce".to_string()) + .state("state".to_string()) + .build() + .unwrap(), + )), + "vp_token" => GenericAuthorizationRequest::OID4VP(Box::new( + OID4VPAuthorizationRequest::builder() + .client_id(verifier_did(did_method)) + .client_id_scheme(ClientIdScheme::Did) + .scope(Scope::openid()) + .redirect_uri(REDIRECT_URI.clone()) + .response_mode("direct_post".to_string()) + .presentation_definition(PRESENTATION_DEFINITION.clone()) + .client_metadata(oid4vp_client_metadata()) + .nonce("nonce".to_string()) + .state("state".to_string()) + .build() + .unwrap(), + )), + _ => unimplemented!(), + } } pub fn form_url_encoded_authorization_request(did_method: &str) -> String { @@ -226,6 +291,31 @@ pub mod tests { lazy_static! { static ref VERIFIER: Subject = futures::executor::block_on(async { Subject { secret_manager: secret_manager().await } }); pub static ref REDIRECT_URI: url::Url = "https://my-domain.example.org/redirect".parse::().unwrap(); + pub static ref PRESENTATION_DEFINITION: PresentationDefinition = serde_json::from_value(json!( + { + "id":"Verifiable Presentation request for sign-on", + "input_descriptors":[ + { + "id":"Request for Verifiable Credential", + "constraints":{ + "fields":[ + { + "path":[ + "$.vc.type" + ], + "filter":{ + "type":"array", + "contains":{ + "const":"TestCredential" + } + } + } + ] + } + } + ] + } + )).unwrap(); static ref FORM_URL_ENCODED_AUTHORIZATION_REQUEST_DID_KEY: String = "\ openid://?\ client_id=did%3Akey%3Az6MkiieyoLMSVsJAZv7Jje5wWSkDEymUgkyF8kbcrjZpX3qd&\ diff --git a/agent_verification/src/authorization_request/command.rs b/agent_verification/src/authorization_request/command.rs index c55a8864..c48ab99b 100644 --- a/agent_verification/src/authorization_request/command.rs +++ b/agent_verification/src/authorization_request/command.rs @@ -1,8 +1,13 @@ +use oid4vp::PresentationDefinition; use serde::Deserialize; #[derive(Debug, Deserialize)] #[serde(untagged)] pub enum AuthorizationRequestCommand { - CreateAuthorizationRequest { state: String, nonce: String }, + CreateAuthorizationRequest { + state: String, + nonce: String, + presentation_definition: Option, + }, SignAuthorizationRequestObject, } diff --git a/agent_verification/src/authorization_request/error.rs b/agent_verification/src/authorization_request/error.rs index bf74f0f1..af2077aa 100644 --- a/agent_verification/src/authorization_request/error.rs +++ b/agent_verification/src/authorization_request/error.rs @@ -1,4 +1,11 @@ use thiserror::Error; #[derive(Error, Debug)] -pub enum AuthorizationRequestError {} +pub enum AuthorizationRequestError { + #[error("Failed to create authorization request: {0}")] + AuthorizationRequestBuilderError(#[source] anyhow::Error), + #[error("Missing authorization request error")] + MissingAuthorizationRequest, + #[error("Failed to sign authorization request: {0}")] + AuthorizationRequestSigningError(#[source] anyhow::Error), +} diff --git a/agent_verification/src/authorization_request/event.rs b/agent_verification/src/authorization_request/event.rs index 43aea48e..2d36fee5 100644 --- a/agent_verification/src/authorization_request/event.rs +++ b/agent_verification/src/authorization_request/event.rs @@ -1,11 +1,11 @@ -use crate::connection::aggregate::SIOPv2AuthorizationRequest; +use crate::generic_oid4vc::GenericAuthorizationRequest; use cqrs_es::DomainEvent; use serde::{Deserialize, Serialize}; #[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] pub enum AuthorizationRequestEvent { AuthorizationRequestCreated { - authorization_request: Box, + authorization_request: Box, }, FormUrlEncodedAuthorizationRequestCreated { form_url_encoded_authorization_request: String, diff --git a/agent_verification/src/authorization_request/queries.rs b/agent_verification/src/authorization_request/queries.rs index 6df10180..5939f211 100644 --- a/agent_verification/src/authorization_request/queries.rs +++ b/agent_verification/src/authorization_request/queries.rs @@ -1,16 +1,12 @@ +use super::aggregate::AuthorizationRequest; +use crate::generic_oid4vc::GenericAuthorizationRequest; use cqrs_es::{EventEnvelope, View}; -use oid4vc_core::authorization_request::Object; use serde::{Deserialize, Serialize}; -use siopv2::siopv2::SIOPv2; - -use super::aggregate::AuthorizationRequest; - -pub type SIOPv2AuthorizationRequest = oid4vc_core::authorization_request::AuthorizationRequest>; #[derive(Debug, Default, Serialize, Deserialize, Clone)] pub struct AuthorizationRequestView { - pub siopv2_authorization_request: Option, - pub form_url_encoded_authorization_request: String, + pub authorization_request: Option, + pub form_url_encoded_authorization_request: Option, pub signed_authorization_request_object: Option, } @@ -20,14 +16,14 @@ impl View for AuthorizationRequestView { match &event.payload { AuthorizationRequestCreated { authorization_request } => { - self.siopv2_authorization_request - .replace(*authorization_request.clone()); + self.authorization_request.replace(*authorization_request.clone()); } FormUrlEncodedAuthorizationRequestCreated { form_url_encoded_authorization_request, - } => self - .form_url_encoded_authorization_request - .clone_from(form_url_encoded_authorization_request), + } => { + self.form_url_encoded_authorization_request + .replace(form_url_encoded_authorization_request.clone()); + } AuthorizationRequestObjectSigned { signed_authorization_request_object, } => { diff --git a/agent_verification/src/connection/aggregate.rs b/agent_verification/src/connection/aggregate.rs index 2aa4b981..bb4b7b54 100644 --- a/agent_verification/src/connection/aggregate.rs +++ b/agent_verification/src/connection/aggregate.rs @@ -1,21 +1,17 @@ +use super::{command::ConnectionCommand, error::ConnectionError, event::ConnectionEvent}; +use crate::{generic_oid4vc::GenericAuthorizationResponse, services::VerificationServices}; use async_trait::async_trait; use cqrs_es::Aggregate; -use oid4vc_core::authorization_request::Object; +use oid4vp::Oid4vpParams; use serde::{Deserialize, Serialize}; -use siopv2::siopv2::SIOPv2; use std::{sync::Arc, vec}; use tracing::info; -use crate::services::VerificationServices; - -use super::{command::ConnectionCommand, error::ConnectionError, event::ConnectionEvent}; - -pub type SIOPv2AuthorizationRequest = oid4vc_core::authorization_request::AuthorizationRequest>; - #[derive(Debug, Serialize, Deserialize, Default)] pub struct Connection { // TODO: Does user data need to be stored in UniCore at all? - id_token: String, + id_token: Option, + vp_token: Option, } #[async_trait] @@ -37,23 +33,38 @@ impl Aggregate for Connection { info!("Handling command: {:?}", command); match command { - VerifySIOPv2AuthorizationResponse { + VerifyAuthorizationResponse { // TODO: use this once `RelyingPartyManager` uses the official SIOPv2 validation logic. - siopv2_authorization_request: _, - siopv2_authorization_response, + authorization_request: _, + authorization_response, } => { let relying_party = &services.relying_party; - let _ = relying_party - .validate_response(&siopv2_authorization_response) - .await - .map_err(InvalidSIOPv2AuthorizationResponse)?; - - let id_token = siopv2_authorization_response.extension.id_token.clone(); - - Ok(vec![SIOPv2AuthorizationResponseVerified { - id_token: id_token.clone(), - }]) + match authorization_response { + GenericAuthorizationResponse::SIOPv2(authorization_response) => { + let _ = relying_party + .validate_response(&authorization_response) + .await + .map_err(InvalidSIOPv2AuthorizationResponse)?; + + let id_token = authorization_response.extension.id_token.clone(); + + Ok(vec![SIOPv2AuthorizationResponseVerified { id_token }]) + } + GenericAuthorizationResponse::OID4VP(oid4vp_authorization_response) => { + let _ = relying_party + .validate_response(&oid4vp_authorization_response) + .await + .map_err(InvalidOID4VPAuthorizationResponse)?; + + let vp_token = match oid4vp_authorization_response.extension.oid4vp_parameters { + Oid4vpParams::Params { vp_token, .. } => vp_token, + Oid4vpParams::Jwt { .. } => return Err(UnsupportedJwtParameterError), + }; + + Ok(vec![OID4VPAuthorizationResponseVerified { vp_token }]) + } + } } } } @@ -65,7 +76,10 @@ impl Aggregate for Connection { match event { SIOPv2AuthorizationResponseVerified { id_token } => { - self.id_token = id_token; + self.id_token.replace(id_token); + } + OID4VPAuthorizationResponseVerified { vp_token } => { + self.vp_token.replace(vp_token); } } } @@ -78,11 +92,19 @@ pub mod tests { use agent_secret_manager::secret_manager; use agent_secret_manager::subject::Subject; use cqrs_es::test::TestFramework; - use oid4vc_core::authorization_response::AuthorizationResponse; + use identity_credential::credential::Jwt; + use identity_credential::presentation::Presentation; + + use oid4vc_manager::managers::presentation::create_presentation_submission; use oid4vc_manager::ProviderManager; + use oid4vci::VerifiableCredentialJwt; + use oid4vp::oid4vp::AuthorizationResponseInput; use rstest::rstest; - use crate::authorization_request::aggregate::tests::siopv2_authorization_request; + use crate::authorization_request::aggregate::tests::{ + authorization_request, verifier_did, PRESENTATION_DEFINITION, + }; + use crate::generic_oid4vc::GenericAuthorizationRequest; use crate::services::test_utils::test_verification_services; use super::*; @@ -91,31 +113,36 @@ pub mod tests { #[rstest] #[serial_test::serial] - fn test_verify_siopv2_authorization_response( + fn test_verify_authorization_response( + // "id_token" represents the `SIOPv2` flow, and "vp_token" represents the `OID4VP` flow. + #[values("id_token", "vp_token")] response_type: &str, // TODO: add `did:web`, check for other tests as well. Probably should be moved to E2E test. #[values("did:key", "did:jwk")] verifier_did_method: &str, #[values("did:key", "did:jwk")] provider_did_method: &str, ) { let verification_services = test_verification_services(verifier_did_method); - let siopv2_authorization_request = siopv2_authorization_request(verifier_did_method); - let siopv2_authorization_response = - siopv2_authorization_response(provider_did_method, &siopv2_authorization_request); - let id_token = siopv2_authorization_response.extension.id_token.clone(); + let authorization_request = authorization_request(response_type, verifier_did_method); + let authorization_response = authorization_response(provider_did_method, &authorization_request); + let token = authorization_response.token(); ConnectionTestFramework::with(verification_services) .given_no_previous_events() - .when(ConnectionCommand::VerifySIOPv2AuthorizationResponse { - siopv2_authorization_request, - siopv2_authorization_response, + .when(ConnectionCommand::VerifyAuthorizationResponse { + authorization_request, + authorization_response, }) - .then_expect_events(vec![ConnectionEvent::SIOPv2AuthorizationResponseVerified { id_token }]); + .then_expect_events(vec![match response_type { + "id_token" => ConnectionEvent::SIOPv2AuthorizationResponseVerified { id_token: token }, + "vp_token" => ConnectionEvent::OID4VPAuthorizationResponseVerified { vp_token: token }, + _ => unreachable!("Invalid response type."), + }]); } - fn siopv2_authorization_response( + fn authorization_response( did_method: &str, - siopv2_authorization_request: &SIOPv2AuthorizationRequest, - ) -> AuthorizationResponse { + authorization_request: &GenericAuthorizationRequest, + ) -> GenericAuthorizationResponse { let provider_manager = ProviderManager::new( Arc::new(futures::executor::block_on(async { Subject { @@ -125,8 +152,76 @@ pub mod tests { did_method, ) .unwrap(); - provider_manager - .generate_response(siopv2_authorization_request, Default::default()) - .unwrap() + + match authorization_request { + GenericAuthorizationRequest::SIOPv2(siopv2_authorization_request) => GenericAuthorizationResponse::SIOPv2( + provider_manager + .generate_response(siopv2_authorization_request, Default::default()) + .unwrap(), + ), + GenericAuthorizationRequest::OID4VP(oid4vp_authorization_request) => { + // TODO: implement test fixture for subject and issuer instead of using the same did as verifier. + // Fixtures can be implemented using the `rstest` crate as described here: https://docs.rs/rstest/latest/rstest/attr.fixture.html + let issuer_did = verifier_did(did_method); + let subject_did = issuer_did.clone(); + + // Create a new verifiable credential. + let verifiable_credential = VerifiableCredentialJwt::builder() + .sub(&subject_did) + .iss(&issuer_did) + .iat(0) + .exp(9999999999i64) + .verifiable_credential(serde_json::json!({ + "@context": [ + "https://www.w3.org/2018/credentials/v1", + "https://www.w3.org/2018/credentials/examples/v1" + ], + "type": [ + "VerifiableCredential", + "TestCredential" + ], + "issuanceDate": "2022-01-01T00:00:00Z", + "issuer": issuer_did, + "credentialSubject": { + "id": subject_did, + "givenName": "Ferris", + "familyName": "Crabman", + "email": "ferris.crabman@crabmail.com", + "birthdate": "1985-05-21" + } + })) + .build() + .unwrap(); + + // Encode the verifiable credential as a JWT. + let jwt = "eyJ0eXAiOiJKV1QiLCJhbGciOiJFZERTQSIsImtpZCI6ImRpZDprZXk6ejZNa2lpZXlvTE1TVnNKQVp2N0pqZTV3V1NrREV5bVVna3lGOGtiY3JqWnBYM3FkI3o2TWtpaWV5b0xNU1ZzSkFadjdKamU1d1dTa0RFeW1VZ2t5RjhrYmNyalpwWDNxZCJ9.eyJpc3MiOiJkaWQ6a2V5Ono2TWtpaWV5b0xNU1ZzSkFadjdKamU1d1dTa0RFeW1VZ2t5RjhrYmNyalpwWDNxZCIsInN1YiI6ImRpZDprZXk6ejZNa2lpZXlvTE1TVnNKQVp2N0pqZTV3V1NrREV5bVVna3lGOGtiY3JqWnBYM3FkIiwiZXhwIjo5OTk5OTk5OTk5LCJpYXQiOjAsInZjIjp7IkBjb250ZXh0IjpbImh0dHBzOi8vd3d3LnczLm9yZy8yMDE4L2NyZWRlbnRpYWxzL3YxIiwiaHR0cHM6Ly93d3cudzMub3JnLzIwMTgvY3JlZGVudGlhbHMvZXhhbXBsZXMvdjEiXSwidHlwZSI6WyJWZXJpZmlhYmxlQ3JlZGVudGlhbCIsIlRlc3RDcmVkZW50aWFsIl0sImlzc3VhbmNlRGF0ZSI6IjIwMjItMDEtMDFUMDA6MDA6MDBaIiwiaXNzdWVyIjoiZGlkOmtleTp6Nk1raWlleW9MTVNWc0pBWnY3SmplNXdXU2tERXltVWdreUY4a2JjcmpacFgzcWQiLCJjcmVkZW50aWFsU3ViamVjdCI6eyJpZCI6ImRpZDprZXk6ejZNa2lpZXlvTE1TVnNKQVp2N0pqZTV3V1NrREV5bVVna3lGOGtiY3JqWnBYM3FkIiwiZ2l2ZW5OYW1lIjoiRmVycmlzIiwiZmFtaWx5TmFtZSI6IkNyYWJtYW4iLCJlbWFpbCI6ImZlcnJpcy5jcmFibWFuQGNyYWJtYWlsLmNvbSIsImJpcnRoZGF0ZSI6IjE5ODUtMDUtMjEifX19.6guSHngBj_QQYom3kXKmxKrHExoyW1eObBsBg8ACYn-H30YD6eub56zsWnnMzw8IznGDYAguuo3V1D37-A_vCQ".to_string(); + + // Create presentation submission using the presentation definition and the verifiable credential. + let presentation_submission = create_presentation_submission( + &PRESENTATION_DEFINITION, + &[serde_json::to_value(&verifiable_credential).unwrap()], + ) + .unwrap(); + + // Create a verifiable presentation using the JWT. + let verifiable_presentation = + Presentation::builder(subject_did.parse().unwrap(), identity_core::common::Object::new()) + .credential(Jwt::from(jwt)) + .build() + .unwrap(); + + GenericAuthorizationResponse::OID4VP( + provider_manager + .generate_response( + oid4vp_authorization_request, + AuthorizationResponseInput { + verifiable_presentation, + presentation_submission, + }, + ) + .unwrap(), + ) + } + } } } diff --git a/agent_verification/src/connection/command.rs b/agent_verification/src/connection/command.rs index fc51496e..07a590e2 100644 --- a/agent_verification/src/connection/command.rs +++ b/agent_verification/src/connection/command.rs @@ -1,14 +1,11 @@ -use oid4vc_core::authorization_response::AuthorizationResponse; +use crate::generic_oid4vc::{GenericAuthorizationRequest, GenericAuthorizationResponse}; use serde::Deserialize; -use siopv2::siopv2::SIOPv2; - -use super::aggregate::SIOPv2AuthorizationRequest; #[derive(Debug, Deserialize)] #[serde(untagged)] pub enum ConnectionCommand { - VerifySIOPv2AuthorizationResponse { - siopv2_authorization_request: SIOPv2AuthorizationRequest, - siopv2_authorization_response: AuthorizationResponse, + VerifyAuthorizationResponse { + authorization_request: GenericAuthorizationRequest, + authorization_response: GenericAuthorizationResponse, }, } diff --git a/agent_verification/src/connection/error.rs b/agent_verification/src/connection/error.rs index 8b3588fd..9ec29a54 100644 --- a/agent_verification/src/connection/error.rs +++ b/agent_verification/src/connection/error.rs @@ -4,4 +4,8 @@ use thiserror::Error; pub enum ConnectionError { #[error("Invalid SIOPv2 authorization response: {0}")] InvalidSIOPv2AuthorizationResponse(#[source] anyhow::Error), + #[error("Invalid OID4VP authorization response: {0}")] + InvalidOID4VPAuthorizationResponse(#[source] anyhow::Error), + #[error("`jwt` parameter is not supported yet")] + UnsupportedJwtParameterError, } diff --git a/agent_verification/src/connection/event.rs b/agent_verification/src/connection/event.rs index 4043033b..5a38010d 100644 --- a/agent_verification/src/connection/event.rs +++ b/agent_verification/src/connection/event.rs @@ -4,6 +4,7 @@ use serde::{Deserialize, Serialize}; #[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] pub enum ConnectionEvent { SIOPv2AuthorizationResponseVerified { id_token: String }, + OID4VPAuthorizationResponseVerified { vp_token: String }, } impl DomainEvent for ConnectionEvent { @@ -12,6 +13,7 @@ impl DomainEvent for ConnectionEvent { let event_type: &str = match self { SIOPv2AuthorizationResponseVerified { .. } => "SIOPv2AuthorizationResponseVerified", + OID4VPAuthorizationResponseVerified { .. } => "OID4VPAuthorizationResponseVerified", }; event_type.to_string() } diff --git a/agent_verification/src/connection/queries.rs b/agent_verification/src/connection/queries.rs index f1c3e82f..6553fe9f 100644 --- a/agent_verification/src/connection/queries.rs +++ b/agent_verification/src/connection/queries.rs @@ -10,6 +10,7 @@ pub type SIOPv2AuthorizationRequest = oid4vc_core::authorization_request::Author #[derive(Debug, Default, Serialize, Deserialize, Clone)] pub struct ConnectionView { id_token: Option, + vp_token: Option, } impl View for ConnectionView { @@ -20,6 +21,9 @@ impl View for ConnectionView { SIOPv2AuthorizationResponseVerified { id_token } => { self.id_token.replace(id_token.clone()); } + OID4VPAuthorizationResponseVerified { vp_token } => { + self.vp_token.replace(vp_token.clone()); + } } } } diff --git a/agent_verification/src/generic_oid4vc.rs b/agent_verification/src/generic_oid4vc.rs new file mode 100644 index 00000000..11263303 --- /dev/null +++ b/agent_verification/src/generic_oid4vc.rs @@ -0,0 +1,93 @@ +use oid4vc_core::authorization_request::{Body, Object}; +use oid4vp::oid4vp::OID4VP; +use serde::{Deserialize, Serialize}; +use siopv2::siopv2::SIOPv2; + +// TODO(oid4vc): All types and functionalities in this file should be implemented properly in the `oid4vc` crates. + +pub type SIOPv2AuthorizationResponse = oid4vc_core::authorization_response::AuthorizationResponse; +pub type OID4VPAuthorizationResponse = oid4vc_core::authorization_response::AuthorizationResponse; +pub type SIOPv2AuthorizationRequest = oid4vc_core::authorization_request::AuthorizationRequest>; +pub type OID4VPAuthorizationRequest = oid4vc_core::authorization_request::AuthorizationRequest>; + +/// This enum serves as an abstraction over the different types of authorization responses UniCore can provide +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] +#[serde(untagged)] +pub enum GenericAuthorizationResponse { + SIOPv2(SIOPv2AuthorizationResponse), + OID4VP(OID4VPAuthorizationResponse), +} + +impl GenericAuthorizationResponse { + pub fn as_siopv2_authorization_response(&self) -> Option<&SIOPv2AuthorizationResponse> { + match self { + GenericAuthorizationResponse::SIOPv2(authorization_response) => Some(authorization_response), + _ => None, + } + } + + pub fn as_oid4vp_authorization_response(&self) -> Option<&OID4VPAuthorizationResponse> { + match self { + GenericAuthorizationResponse::OID4VP(authorization_response) => Some(authorization_response), + _ => None, + } + } + + pub fn state(&self) -> Option<&String> { + match self { + GenericAuthorizationResponse::SIOPv2(authorization_response) => authorization_response.state.as_ref(), + GenericAuthorizationResponse::OID4VP(authorization_response) => authorization_response.state.as_ref(), + } + } +} + +#[cfg(test)] +impl GenericAuthorizationResponse { + pub fn token(&self) -> String { + match self { + GenericAuthorizationResponse::SIOPv2(authorization_response) => { + authorization_response.extension.id_token.clone() + } + GenericAuthorizationResponse::OID4VP(authorization_response) => { + match &authorization_response.extension.oid4vp_parameters { + oid4vp::Oid4vpParams::Params { vp_token, .. } => vp_token.clone(), + _ => unimplemented!(), + } + } + } + } +} + +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] +#[serde(untagged)] +pub enum GenericAuthorizationRequest { + SIOPv2(Box), + OID4VP(Box), +} + +impl GenericAuthorizationRequest { + pub fn as_siopv2_authorization_request(&self) -> Option<&SIOPv2AuthorizationRequest> { + match self { + GenericAuthorizationRequest::SIOPv2(authorization_request) => Some(authorization_request), + _ => None, + } + } + + pub fn as_oid4vp_authorization_request(&self) -> Option<&OID4VPAuthorizationRequest> { + match self { + GenericAuthorizationRequest::OID4VP(authorization_request) => Some(authorization_request), + _ => None, + } + } + + pub fn client_id(&self) -> String { + match self { + GenericAuthorizationRequest::SIOPv2(authorization_request) => { + authorization_request.body.client_id().clone() + } + GenericAuthorizationRequest::OID4VP(authorization_request) => { + authorization_request.body.client_id().clone() + } + } + } +} diff --git a/agent_verification/src/lib.rs b/agent_verification/src/lib.rs index a440ff75..37d478cc 100644 --- a/agent_verification/src/lib.rs +++ b/agent_verification/src/lib.rs @@ -1,4 +1,5 @@ pub mod authorization_request; pub mod connection; +pub mod generic_oid4vc; pub mod services; pub mod state; diff --git a/agent_verification/src/services.rs b/agent_verification/src/services.rs index 2fe4dc60..677660f8 100644 --- a/agent_verification/src/services.rs +++ b/agent_verification/src/services.rs @@ -2,25 +2,27 @@ use std::sync::Arc; use oid4vc_core::{client_metadata::ClientMetadataResource, Subject}; use oid4vc_manager::RelyingPartyManager; -use siopv2::authorization_request::ClientMetadataParameters; /// Verification services. This struct is used to generate authorization requests and validate authorization responses. pub struct VerificationServices { pub verifier: Arc, pub relying_party: RelyingPartyManager, - pub client_metadata: ClientMetadataResource, + pub siopv2_client_metadata: ClientMetadataResource, + pub oid4vp_client_metadata: ClientMetadataResource, } impl VerificationServices { pub fn new( verifier: Arc, - client_metadata: ClientMetadataResource, + siopv2_client_metadata: ClientMetadataResource, + oid4vp_client_metadata: ClientMetadataResource, default_did_method: &str, ) -> Self { Self { verifier: verifier.clone(), relying_party: RelyingPartyManager::new(verifier, default_did_method).unwrap(), - client_metadata, + siopv2_client_metadata, + oid4vp_client_metadata, } } } @@ -32,7 +34,7 @@ pub mod test_utils { use agent_secret_manager::secret_manager; use agent_secret_manager::subject::Subject; use oid4vc_core::{DidMethod, SubjectSyntaxType}; - use siopv2::authorization_request::ClientMetadataParameters; + use serde_json::json; use super::*; @@ -46,12 +48,21 @@ pub mod test_utils { ClientMetadataResource::ClientMetadata { client_name: None, logo_uri: None, - extension: ClientMetadataParameters { + extension: siopv2::authorization_request::ClientMetadataParameters { subject_syntax_types_supported: vec![SubjectSyntaxType::Did( DidMethod::from_str(default_did_method).unwrap(), )], }, }, + ClientMetadataResource::ClientMetadata { + client_name: None, + logo_uri: None, + // TODO: fix this once `vp_formats` is public. + extension: serde_json::from_value(json!({ + "vp_formats": {} + })) + .unwrap(), + }, default_did_method, )) }