From a5b723e83f5109862fef4ae17fc049847d75116a Mon Sep 17 00:00:00 2001 From: Nander Stabel Date: Fri, 22 Mar 2024 11:58:44 +0100 Subject: [PATCH] feat: add `agent_verification` (#35) * refactor: move `ApplicationState` to `agent_shared` * feat: add `agent_verification` * style: rename `AuthorizationRequestTestFramework` to `ConnectionTestFramework` * refactor: remove `ApplicationState` struct and replace it for a tuple * fix: remove `ConnectionNotificationSent` Instead of using the `VerificationServices` for sending connection notifications we will probably need to utilize a `Query` that will function as an outgoing adapter. * fix: set `AGENT_VERIFICATION_URL` env variable using `AGENT_APPLICATION_URL` * fix: remove println statement * fix: fix `event_type` for `SIOPv2AuthorizationResponseVerified` --- Cargo.lock | 142 +++++++++-- Cargo.toml | 3 + agent_application/src/main.rs | 2 + agent_shared/Cargo.toml | 3 +- agent_shared/src/lib.rs | 3 + agent_shared/src/secret_manager.rs | 14 ++ agent_shared/tests/.env.test | 3 + agent_verification/Cargo.toml | 31 +++ .../src/authorization_request/aggregate.rs | 220 ++++++++++++++++++ .../src/authorization_request/command.rs | 13 ++ .../src/authorization_request/error.rs | 4 + .../src/authorization_request/event.rs | 33 +++ .../src/authorization_request/mod.rs | 5 + .../src/authorization_request/queries.rs | 37 +++ .../src/connection/aggregate.rs | 117 ++++++++++ agent_verification/src/connection/command.rs | 14 ++ agent_verification/src/connection/error.rs | 4 + agent_verification/src/connection/event.rs | 22 ++ agent_verification/src/connection/mod.rs | 5 + agent_verification/src/connection/queries.rs | 25 ++ agent_verification/src/lib.rs | 4 + agent_verification/src/services.rs | 32 +++ agent_verification/src/state.rs | 46 ++++ 23 files changed, 757 insertions(+), 25 deletions(-) create mode 100644 agent_shared/src/secret_manager.rs create mode 100644 agent_verification/Cargo.toml create mode 100644 agent_verification/src/authorization_request/aggregate.rs create mode 100644 agent_verification/src/authorization_request/command.rs create mode 100644 agent_verification/src/authorization_request/error.rs create mode 100644 agent_verification/src/authorization_request/event.rs create mode 100644 agent_verification/src/authorization_request/mod.rs create mode 100644 agent_verification/src/authorization_request/queries.rs create mode 100644 agent_verification/src/connection/aggregate.rs create mode 100644 agent_verification/src/connection/command.rs create mode 100644 agent_verification/src/connection/error.rs create mode 100644 agent_verification/src/connection/event.rs create mode 100644 agent_verification/src/connection/mod.rs create mode 100644 agent_verification/src/connection/queries.rs create mode 100644 agent_verification/src/lib.rs create mode 100644 agent_verification/src/services.rs create mode 100644 agent_verification/src/state.rs diff --git a/Cargo.lock b/Cargo.lock index 3f45e2cb..afc71118 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -152,6 +152,7 @@ dependencies = [ "axum 0.7.4", "config", "cqrs-es", + "did_manager", "dotenvy", "rand 0.8.5", "time", @@ -173,6 +174,29 @@ dependencies = [ "tokio", ] +[[package]] +name = "agent_verification" +version = "0.1.0" +dependencies = [ + "agent_shared", + "agent_verification", + "async-trait", + "axum 0.7.4", + "cqrs-es", + "did_manager", + "futures", + "lazy_static", + "oid4vc-core", + "oid4vc-manager", + "serde", + "serial_test", + "siopv2", + "thiserror", + "tokio", + "tracing", + "url", +] + [[package]] name = "ahash" version = "0.7.8" @@ -1037,11 +1061,12 @@ dependencies = [ [[package]] name = "config" -version = "0.13.4" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23738e11972c7643e4ec947840fc463b6a571afcd3e735bdfce7d03c7a784aca" +checksum = "7328b20597b53c2454f0b1919720c25c7339051c02b72b7e05409e00b14132be" dependencies = [ "async-trait", + "convert_case 0.6.0", "json5", "lazy_static", "nom", @@ -1066,6 +1091,26 @@ version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" +[[package]] +name = "const-random" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87e00182fe74b066627d63b85fd550ac2998d4b0bd86bfed477a0ae4c7c71359" +dependencies = [ + "const-random-macro", +] + +[[package]] +name = "const-random-macro" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e" +dependencies = [ + "getrandom 0.2.12", + "once_cell", + "tiny-keccak", +] + [[package]] name = "constant_time_eq" version = "0.1.5" @@ -1104,6 +1149,15 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" +[[package]] +name = "convert_case" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec182b0ca2f35d8fc196cf3404988fd8b8c739a4d270ff118a398feb0cbec1ca" +dependencies = [ + "unicode-segmentation", +] + [[package]] name = "core-foundation" version = "0.9.4" @@ -1506,7 +1560,7 @@ version = "0.99.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4fb810d30a7c1953f91334de7244731fc3f3c10d7fe163338a35b9f640960321" dependencies = [ - "convert_case", + "convert_case 0.4.0", "proc-macro2", "quote", "rustc_version", @@ -1742,9 +1796,12 @@ dependencies = [ [[package]] name = "dlv-list" -version = "0.3.0" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0688c2a7f92e427f44895cd63841bff7b29f8d7a1648b9e7e07a4a365b2e1257" +checksum = "442039f5147480ba31067cb00ada1adae6892028e40e45fc5de7b7df6dcc1b5f" +dependencies = [ + "const-random", +] [[package]] name = "dotenvy" @@ -4367,12 +4424,12 @@ dependencies = [ [[package]] name = "ordered-multimap" -version = "0.4.3" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccd746e37177e1711c20dd619a1620f34f5c8b569c53590a72dedd5344d8924a" +checksum = "4ed8acf08e98e744e5384c8bc63ceb0364e68a6854187221c18df61c4797690e" dependencies = [ "dlv-list", - "hashbrown 0.12.3", + "hashbrown 0.13.2", ] [[package]] @@ -4458,7 +4515,7 @@ version = "3.6.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "be30eaf4b0a9fba5336683b38de57bb86d179a35862ba6bfcf57625d006bde5b" dependencies = [ - "proc-macro-crate 2.0.2", + "proc-macro-crate 2.0.0", "proc-macro2", "quote", "syn 1.0.109", @@ -4894,11 +4951,10 @@ dependencies = [ [[package]] name = "proc-macro-crate" -version = "2.0.2" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b00f26d3400549137f92511a46ac1cd8ce37cb5598a96d382381458b992a5d24" +checksum = "7e8366a6159044a37876a2b9817124296703c586a5c92e2c53751fa06d8d43e8" dependencies = [ - "toml_datetime", "toml_edit 0.20.2", ] @@ -5323,13 +5379,14 @@ dependencies = [ [[package]] name = "ron" -version = "0.7.1" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88073939a61e5b7680558e6be56b419e208420c2adb92be54921fa6b72283f1a" +checksum = "b91f7eff05f748767f183df4320a63d6936e9c6107d97c9e6bdd9784f4289c94" dependencies = [ - "base64 0.13.1", - "bitflags 1.3.2", + "base64 0.21.7", + "bitflags 2.4.2", "serde", + "serde_derive", ] [[package]] @@ -5407,9 +5464,9 @@ dependencies = [ [[package]] name = "rust-ini" -version = "0.18.0" +version = "0.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6d5f2436026b4f6e79dc829837d467cc7e9a55ee40e750d716713540715a2df" +checksum = "7e2a3bcec1f113553ef1c88aae6c020a369d03d55b58de9869a0908930385091" dependencies = [ "cfg-if", "ordered-multimap", @@ -5712,6 +5769,15 @@ dependencies = [ "syn 2.0.53", ] +[[package]] +name = "serde_spanned" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb3622f419d1296904700073ea6cc23ad690adbd66f13ea683df73298736f0c1" +dependencies = [ + "serde", +] + [[package]] name = "serde_urlencoded" version = "0.7.1" @@ -6741,18 +6807,24 @@ dependencies = [ [[package]] name = "toml" -version = "0.5.11" +version = "0.8.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234" +checksum = "e9dd1545e8208b4a5af1aa9bbd0b4cf7e9ea08fabc5d0a5c67fcaafa17433aa3" dependencies = [ "serde", + "serde_spanned", + "toml_datetime", + "toml_edit 0.22.8", ] [[package]] name = "toml_datetime" -version = "0.6.3" +version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b" +checksum = "3550f4e9685620ac18a50ed434eb3aec30db8ba93b0287467bca5826ea25baf1" +dependencies = [ + "serde", +] [[package]] name = "toml_edit" @@ -6762,7 +6834,7 @@ checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" dependencies = [ "indexmap 2.2.5", "toml_datetime", - "winnow", + "winnow 0.5.40", ] [[package]] @@ -6773,7 +6845,20 @@ checksum = "396e4d48bbb2b7554c944bde63101b5ae446cff6ec4a24227428f15eb72ef338" dependencies = [ "indexmap 2.2.5", "toml_datetime", - "winnow", + "winnow 0.5.40", +] + +[[package]] +name = "toml_edit" +version = "0.22.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c12219811e0c1ba077867254e5ad62ee2c9c190b0d957110750ac0cda1ae96cd" +dependencies = [ + "indexmap 2.2.5", + "serde", + "serde_spanned", + "toml_datetime", + "winnow 0.6.5", ] [[package]] @@ -7511,6 +7596,15 @@ dependencies = [ "memchr", ] +[[package]] +name = "winnow" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dffa400e67ed5a4dd237983829e66475f0a4a26938c4b04c21baede6262215b8" +dependencies = [ + "memchr", +] + [[package]] name = "winreg" version = "0.50.0" diff --git a/Cargo.toml b/Cargo.toml index c9871ad6..11ac532f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,6 +7,7 @@ members = [ "agent_secret_manager", "agent_shared", "agent_store", + "agent_verification", ] [workspace.package] @@ -16,6 +17,7 @@ rust-version = "1.76.0" [workspace.dependencies] did_manager = { git = "https://git@github.com/impierce/did-manager.git", rev = "60ba7c0" } +siopv2 = { git = "https://git@github.com/impierce/openid4vc.git", rev = "10a6bd7" } oid4vci = { git = "https://git@github.com/impierce/openid4vc.git", rev = "10a6bd7" } oid4vc-core = { git = "https://git@github.com/impierce/openid4vc.git", rev = "10a6bd7" } oid4vc-manager = { git = "https://git@github.com/impierce/openid4vc.git", rev = "10a6bd7" } @@ -23,6 +25,7 @@ oid4vc-manager = { git = "https://git@github.com/impierce/openid4vc.git", rev = async-trait = "0.1" axum = { version = "0.7", features = ["tracing"] } cqrs-es = "0.4.2" +futures = "0.3" lazy_static = "1.4" serde = { version = "1.0", default-features = false, features = ["derive"] } serde_json = { version = "1.0" } diff --git a/agent_application/src/main.rs b/agent_application/src/main.rs index 458c085a..39dc5c3f 100644 --- a/agent_application/src/main.rs +++ b/agent_application/src/main.rs @@ -24,6 +24,8 @@ async fn main() { }; let url = config!("url").expect("AGENT_APPLICATION_URL is not set"); + // TODO: Temporary solution. In the future we need to read these kinds of values from a config file. + std::env::set_var("AGENT_VERIFICATION_URL", &url); info!("Application url: {:?}", url); diff --git a/agent_shared/Cargo.toml b/agent_shared/Cargo.toml index 964ae65b..ee3d0eae 100644 --- a/agent_shared/Cargo.toml +++ b/agent_shared/Cargo.toml @@ -7,8 +7,9 @@ rust-version.workspace = true [dependencies] async-trait.workspace = true axum.workspace = true -config = { version = "0.13" } +config = { version = "0.14" } cqrs-es.workspace = true +did_manager.workspace = true dotenvy = { version = "0.15" } rand = "0.8" time = { version = "0.3" } diff --git a/agent_shared/src/lib.rs b/agent_shared/src/lib.rs index 25773e26..65ab2b70 100644 --- a/agent_shared/src/lib.rs +++ b/agent_shared/src/lib.rs @@ -4,6 +4,9 @@ pub mod generic_query; pub mod handlers; pub mod url_utils; +#[cfg(feature = "test")] +pub mod secret_manager; + pub use ::config::ConfigError; use rand::Rng; pub use url_utils::UrlAppendHelpers; diff --git a/agent_shared/src/secret_manager.rs b/agent_shared/src/secret_manager.rs new file mode 100644 index 00000000..64c6b0ce --- /dev/null +++ b/agent_shared/src/secret_manager.rs @@ -0,0 +1,14 @@ +use crate::config::config; +use did_manager::SecretManager; + +pub async fn secret_manager() -> SecretManager { + let snapshot_path = config(std::env!("CARGO_PKG_NAME")) + .get_string("stronghold_path") + .unwrap(); + let password = config(std::env!("CARGO_PKG_NAME")) + .get_string("stronghold_password") + .unwrap(); + let key_id = config(std::env!("CARGO_PKG_NAME")).get_string("issuer_key_id").unwrap(); + + SecretManager::load(snapshot_path, password, key_id).await.unwrap() +} diff --git a/agent_shared/tests/.env.test b/agent_shared/tests/.env.test index dc7ffd28..44ebcad3 100644 --- a/agent_shared/tests/.env.test +++ b/agent_shared/tests/.env.test @@ -10,3 +10,6 @@ TEST_CREDENTIAL_LOGO_URL=https://my-domain.example.org/credential_logo.png TEST_STRONGHOLD_PATH="../agent_secret_manager/tests/res/test.stronghold" TEST_STRONGHOLD_PASSWORD="secure_password" TEST_ISSUER_KEY_ID="9O66nzWqYYy1LmmiOudOlh2SMIaUWoTS" + +# AGENT_VERIFICATION +TEST_URL=https://my-domain.example.org diff --git a/agent_verification/Cargo.toml b/agent_verification/Cargo.toml new file mode 100644 index 00000000..9b800c1f --- /dev/null +++ b/agent_verification/Cargo.toml @@ -0,0 +1,31 @@ +[package] +name = "agent_verification" +version.workspace = true +edition.workspace = true +rust-version.workspace = true + +[dependencies] +agent_shared = { path = "../agent_shared" } + +async-trait.workspace = true +axum.workspace = true +cqrs-es.workspace = true +futures.workspace = true +oid4vc-core.workspace = true +oid4vc-manager.workspace = true +serde.workspace = true +siopv2.workspace = true +thiserror.workspace = true +tracing.workspace = true +url.workspace = true +tokio.workspace = true + +[dev-dependencies] +agent_verification = { path = ".", features = ["test"] } +agent_shared = { path = "../agent_shared", features = ["test"] } +did_manager.workspace = true +lazy_static.workspace = true +serial_test = "3.0" + +[features] +test = [] diff --git a/agent_verification/src/authorization_request/aggregate.rs b/agent_verification/src/authorization_request/aggregate.rs new file mode 100644 index 00000000..e8a5171f --- /dev/null +++ b/agent_verification/src/authorization_request/aggregate.rs @@ -0,0 +1,220 @@ +use std::sync::Arc; + +use agent_shared::config; +use async_trait::async_trait; +use cqrs_es::Aggregate; +use oid4vc_core::{ + authorization_request::{ByReference, Object}, + scope::Scope, +}; +use serde::{Deserialize, Serialize}; +use siopv2::siopv2::SIOPv2; +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, + form_url_encoded_authorization_request: Option, + signed_authorization_request_object: Option, +} + +#[async_trait] +impl Aggregate for AuthorizationRequest { + type Command = AuthorizationRequestCommand; + type Event = AuthorizationRequestEvent; + type Error = AuthorizationRequestError; + type Services = Arc; + + fn aggregate_type() -> String { + "authorization_request".to_string() + } + + async fn handle(&self, command: Self::Command, services: &Self::Services) -> Result, Self::Error> { + use AuthorizationRequestCommand::*; + use AuthorizationRequestEvent::*; + + info!("Handling command: {:?}", command); + + match command { + CreateAuthorizationRequest { + client_metadata, + state, + nonce, + } => { + let verifier = &services.verifier; + let verifier_did = verifier.identifier().unwrap(); + + let url = config!("url").unwrap(); + let request_uri = format!("{url}/siopv2/request/{state}").parse().unwrap(); + let redirect_uri = format!("{url}/siopv2/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(*client_metadata) + .state(state) + .nonce(nonce) + .build() + .unwrap(), + ); + + let form_url_encoded_authorization_request = oid4vc_core::authorization_request::AuthorizationRequest { + body: ByReference { + client_id: verifier_did, + request_uri, + }, + } + .to_string(); + + Ok(vec![ + AuthorizationRequestCreated { authorization_request }, + FormUrlEncodedAuthorizationRequestCreated { + form_url_encoded_authorization_request, + }, + ]) + } + 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(); + + Ok(vec![AuthorizationRequestObjectSigned { + signed_authorization_request_object, + }]) + } + } + } + + fn apply(&mut self, event: Self::Event) { + use AuthorizationRequestEvent::*; + + info!("Applying event: {:?}", event); + + match event { + AuthorizationRequestCreated { authorization_request } => { + self.authorization_request = Some(*authorization_request); + } + FormUrlEncodedAuthorizationRequestCreated { + form_url_encoded_authorization_request, + } => { + self.form_url_encoded_authorization_request = Some(form_url_encoded_authorization_request); + } + AuthorizationRequestObjectSigned { + signed_authorization_request_object, + } => { + self.signed_authorization_request_object = Some(signed_authorization_request_object); + } + } + } +} + +#[cfg(test)] +pub mod tests { + use std::str::FromStr; + + use agent_shared::secret_manager::secret_manager; + use cqrs_es::test::TestFramework; + use did_manager::SecretManager; + use lazy_static::lazy_static; + use oid4vc_core::Subject; + use oid4vc_core::{client_metadata::ClientMetadata, DidMethod, SubjectSyntaxType}; + + use crate::services::test_utils::test_verification_services; + + use super::*; + + type AuthorizationRequestTestFramework = TestFramework; + + #[test] + #[serial_test::serial] + fn test_create_authorization_request() { + let verification_services = test_verification_services(); + + AuthorizationRequestTestFramework::with(verification_services) + .given_no_previous_events() + .when(AuthorizationRequestCommand::CreateAuthorizationRequest { + client_metadata: Box::new(CLIENT_METADATA.clone()), + state: "state".to_string(), + nonce: "nonce".to_string(), + }) + .then_expect_events(vec![ + AuthorizationRequestEvent::AuthorizationRequestCreated { + authorization_request: Box::new(SIOPV2_AUTHORIZATION_REQUEST.clone()), + }, + AuthorizationRequestEvent::FormUrlEncodedAuthorizationRequestCreated { + form_url_encoded_authorization_request: FORM_URL_ENCODED_AUTHORIZATION_REQUEST.clone(), + }, + ]); + } + + #[test] + #[serial_test::serial] + fn test_sign_authorization_request_object() { + let verification_services = test_verification_services(); + + AuthorizationRequestTestFramework::with(verification_services) + .given(vec![ + AuthorizationRequestEvent::AuthorizationRequestCreated { + authorization_request: Box::new(SIOPV2_AUTHORIZATION_REQUEST.clone()), + }, + AuthorizationRequestEvent::FormUrlEncodedAuthorizationRequestCreated { + form_url_encoded_authorization_request: FORM_URL_ENCODED_AUTHORIZATION_REQUEST.clone(), + }, + ]) + .when(AuthorizationRequestCommand::SignAuthorizationRequestObject) + .then_expect_events(vec![AuthorizationRequestEvent::AuthorizationRequestObjectSigned { + signed_authorization_request_object: SIGNED_AUTHORIZATION_REQUEST_OBJECT.clone(), + }]); + } + + lazy_static! { + static ref VERIFIER: SecretManager = futures::executor::block_on(async { secret_manager().await }); + static ref VERIFIER_DID: String = VERIFIER.identifier().unwrap(); + static ref REDIRECT_URI: url::Url = "https://my-domain.example.org/siopv2/redirect" + .parse::() + .unwrap(); + static ref CLIENT_METADATA: ClientMetadata = ClientMetadata::default().with_subject_syntax_types_supported( + vec![SubjectSyntaxType::Did(DidMethod::from_str("did:test").unwrap()),] + ); + pub static ref SIOPV2_AUTHORIZATION_REQUEST: SIOPv2AuthorizationRequest = SIOPv2AuthorizationRequest::builder() + .client_id(VERIFIER_DID.clone()) + .scope(Scope::openid()) + .redirect_uri(REDIRECT_URI.clone()) + .response_mode("direct_post".to_string()) + .client_metadata(CLIENT_METADATA.clone()) + .nonce("nonce".to_string()) + .state("state".to_string()) + .build() + .unwrap(); + static ref FORM_URL_ENCODED_AUTHORIZATION_REQUEST: String = "\ + siopv2://idtoken?\ + client_id=did%3Akey%3Az6MkiieyoLMSVsJAZv7Jje5wWSkDEymUgkyF8kbcrjZpX3qd&\ + request_uri=https%3A%2F%2Fmy-domain.example.org%2Fsiopv2%2Frequest%2Fstate" + .to_string(); + static ref SIGNED_AUTHORIZATION_REQUEST_OBJECT: String = + "eyJ0eXAiOiJKV1QiLCJhbGciOiJFZERTQSIsImtpZCI6ImRpZDprZXk6ejZNa2lp\ + ZXlvTE1TVnNKQVp2N0pqZTV3V1NrREV5bVVna3lGOGtiY3JqWnBYM3FkI3o2TWtp\ + aWV5b0xNU1ZzSkFadjdKamU1d1dTa0RFeW1VZ2t5RjhrYmNyalpwWDNxZCJ9.eyJ\ + jbGllbnRfaWQiOiJkaWQ6a2V5Ono2TWtpaWV5b0xNU1ZzSkFadjdKamU1d1dTa0R\ + FeW1VZ2t5RjhrYmNyalpwWDNxZCIsInJlZGlyZWN0X3VyaSI6Imh0dHBzOi8vbXk\ + tZG9tYWluLmV4YW1wbGUub3JnL3Npb3B2Mi9yZWRpcmVjdCIsInN0YXRlIjoic3R\ + hdGUiLCJyZXNwb25zZV90eXBlIjoiaWRfdG9rZW4iLCJzY29wZSI6Im9wZW5pZCI\ + sInJlc3BvbnNlX21vZGUiOiJkaXJlY3RfcG9zdCIsIm5vbmNlIjoibm9uY2UiLCJ\ + jbGllbnRfbWV0YWRhdGEiOnsic3ViamVjdF9zeW50YXhfdHlwZXNfc3VwcG9ydGV\ + kIjpbImRpZDp0ZXN0Il19fQ.vjE-9wDbWqN8tRtnpYRZR7umZWb7M8MEMRSei28B\ + 0zmTMDJlXeEYFJaDwN4hGVgRXmkTmwD_Tg-xhsfcD8BMAw" + .to_string(); + } +} diff --git a/agent_verification/src/authorization_request/command.rs b/agent_verification/src/authorization_request/command.rs new file mode 100644 index 00000000..a8c10391 --- /dev/null +++ b/agent_verification/src/authorization_request/command.rs @@ -0,0 +1,13 @@ +use oid4vc_core::client_metadata::ClientMetadata; +use serde::Deserialize; + +#[derive(Debug, Deserialize)] +#[serde(untagged)] +pub enum AuthorizationRequestCommand { + CreateAuthorizationRequest { + client_metadata: Box, + state: String, + nonce: String, + }, + SignAuthorizationRequestObject, +} diff --git a/agent_verification/src/authorization_request/error.rs b/agent_verification/src/authorization_request/error.rs new file mode 100644 index 00000000..bf74f0f1 --- /dev/null +++ b/agent_verification/src/authorization_request/error.rs @@ -0,0 +1,4 @@ +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum AuthorizationRequestError {} diff --git a/agent_verification/src/authorization_request/event.rs b/agent_verification/src/authorization_request/event.rs new file mode 100644 index 00000000..43aea48e --- /dev/null +++ b/agent_verification/src/authorization_request/event.rs @@ -0,0 +1,33 @@ +use crate::connection::aggregate::SIOPv2AuthorizationRequest; +use cqrs_es::DomainEvent; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] +pub enum AuthorizationRequestEvent { + AuthorizationRequestCreated { + authorization_request: Box, + }, + FormUrlEncodedAuthorizationRequestCreated { + form_url_encoded_authorization_request: String, + }, + AuthorizationRequestObjectSigned { + signed_authorization_request_object: String, + }, +} + +impl DomainEvent for AuthorizationRequestEvent { + fn event_type(&self) -> String { + use AuthorizationRequestEvent::*; + + let event_type: &str = match self { + AuthorizationRequestCreated { .. } => "AuthorizationRequestCreated", + FormUrlEncodedAuthorizationRequestCreated { .. } => "FormUrlEncodedAuthorizationRequestCreated", + AuthorizationRequestObjectSigned { .. } => "AuthorizationRequestObjectSigned", + }; + event_type.to_string() + } + + fn event_version(&self) -> String { + "1".to_string() + } +} diff --git a/agent_verification/src/authorization_request/mod.rs b/agent_verification/src/authorization_request/mod.rs new file mode 100644 index 00000000..7d8a943f --- /dev/null +++ b/agent_verification/src/authorization_request/mod.rs @@ -0,0 +1,5 @@ +pub mod aggregate; +pub mod command; +pub mod error; +pub mod event; +pub mod queries; diff --git a/agent_verification/src/authorization_request/queries.rs b/agent_verification/src/authorization_request/queries.rs new file mode 100644 index 00000000..04bc6b1e --- /dev/null +++ b/agent_verification/src/authorization_request/queries.rs @@ -0,0 +1,37 @@ +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 signed_authorization_request_object: Option, +} + +impl View for AuthorizationRequestView { + fn update(&mut self, event: &EventEnvelope) { + use crate::authorization_request::event::AuthorizationRequestEvent::*; + + match &event.payload { + AuthorizationRequestCreated { authorization_request } => { + self.siopv2_authorization_request = Some(*authorization_request.clone()); + } + FormUrlEncodedAuthorizationRequestCreated { + form_url_encoded_authorization_request, + } => { + self.form_url_encoded_authorization_request = form_url_encoded_authorization_request.clone(); + } + AuthorizationRequestObjectSigned { + signed_authorization_request_object, + } => { + self.signed_authorization_request_object = Some(signed_authorization_request_object.clone()); + } + } + } +} diff --git a/agent_verification/src/connection/aggregate.rs b/agent_verification/src/connection/aggregate.rs new file mode 100644 index 00000000..3de7fe0a --- /dev/null +++ b/agent_verification/src/connection/aggregate.rs @@ -0,0 +1,117 @@ +use async_trait::async_trait; +use cqrs_es::Aggregate; +use oid4vc_core::authorization_request::Object; +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, +} + +#[async_trait] +impl Aggregate for Connection { + type Command = ConnectionCommand; + type Event = ConnectionEvent; + type Error = ConnectionError; + type Services = Arc; + + fn aggregate_type() -> String { + "connection".to_string() + } + + async fn handle(&self, command: Self::Command, services: &Self::Services) -> Result, Self::Error> { + use ConnectionCommand::*; + use ConnectionEvent::*; + + info!("Handling command: {:?}", command); + + match command { + VerifySIOPv2AuthorizationResponse { + // TODO: use this once `RelyingPartyManager` uses the official SIOPv2 validation logic. + siopv2_authorization_request: _, + siopv2_authorization_response, + } => { + let relying_party = &services.relying_party; + + let _ = relying_party + .validate_response(&siopv2_authorization_response) + .await + .unwrap(); + + let id_token = siopv2_authorization_response.extension.id_token.clone(); + + Ok(vec![SIOPv2AuthorizationResponseVerified { + id_token: id_token.clone(), + }]) + } + } + } + + fn apply(&mut self, event: Self::Event) { + use ConnectionEvent::*; + + info!("Applying event: {:?}", event); + + match event { + SIOPv2AuthorizationResponseVerified { id_token } => { + self.id_token = id_token; + } + } + } +} + +#[cfg(test)] +pub mod tests { + use std::sync::Arc; + + use agent_shared::secret_manager::secret_manager; + use cqrs_es::test::TestFramework; + use lazy_static::lazy_static; + use oid4vc_core::authorization_response::AuthorizationResponse; + use oid4vc_manager::ProviderManager; + + use crate::authorization_request::aggregate::tests::SIOPV2_AUTHORIZATION_REQUEST; + use crate::services::test_utils::test_verification_services; + + use super::*; + + type ConnectionTestFramework = TestFramework; + + #[test] + #[serial_test::serial] + fn test_verify_siopv2_authorization_response() { + let verification_services = test_verification_services(); + + ConnectionTestFramework::with(verification_services) + .given_no_previous_events() + .when(ConnectionCommand::VerifySIOPv2AuthorizationResponse { + siopv2_authorization_request: SIOPV2_AUTHORIZATION_REQUEST.clone(), + siopv2_authorization_response: SIOPV2_AUTHORIZATION_RESPONSE.clone(), + }) + .then_expect_events(vec![ConnectionEvent::SIOPv2AuthorizationResponseVerified { + id_token: ID_TOKEN.clone(), + }]); + } + + lazy_static! { + static ref SIOPV2_AUTHORIZATION_RESPONSE: AuthorizationResponse = { + let provider_manager = + ProviderManager::new([Arc::new(futures::executor::block_on(async { secret_manager().await }))]) + .unwrap(); + provider_manager + .generate_response(&SIOPV2_AUTHORIZATION_REQUEST, Default::default()) + .unwrap() + }; + static ref ID_TOKEN: String = SIOPV2_AUTHORIZATION_RESPONSE.extension.id_token.clone(); + } +} diff --git a/agent_verification/src/connection/command.rs b/agent_verification/src/connection/command.rs new file mode 100644 index 00000000..fc51496e --- /dev/null +++ b/agent_verification/src/connection/command.rs @@ -0,0 +1,14 @@ +use oid4vc_core::authorization_response::AuthorizationResponse; +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, + }, +} diff --git a/agent_verification/src/connection/error.rs b/agent_verification/src/connection/error.rs new file mode 100644 index 00000000..fa46229b --- /dev/null +++ b/agent_verification/src/connection/error.rs @@ -0,0 +1,4 @@ +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum ConnectionError {} diff --git a/agent_verification/src/connection/event.rs b/agent_verification/src/connection/event.rs new file mode 100644 index 00000000..4043033b --- /dev/null +++ b/agent_verification/src/connection/event.rs @@ -0,0 +1,22 @@ +use cqrs_es::DomainEvent; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] +pub enum ConnectionEvent { + SIOPv2AuthorizationResponseVerified { id_token: String }, +} + +impl DomainEvent for ConnectionEvent { + fn event_type(&self) -> String { + use ConnectionEvent::*; + + let event_type: &str = match self { + SIOPv2AuthorizationResponseVerified { .. } => "SIOPv2AuthorizationResponseVerified", + }; + event_type.to_string() + } + + fn event_version(&self) -> String { + "1".to_string() + } +} diff --git a/agent_verification/src/connection/mod.rs b/agent_verification/src/connection/mod.rs new file mode 100644 index 00000000..7d8a943f --- /dev/null +++ b/agent_verification/src/connection/mod.rs @@ -0,0 +1,5 @@ +pub mod aggregate; +pub mod command; +pub mod error; +pub mod event; +pub mod queries; diff --git a/agent_verification/src/connection/queries.rs b/agent_verification/src/connection/queries.rs new file mode 100644 index 00000000..3f47b094 --- /dev/null +++ b/agent_verification/src/connection/queries.rs @@ -0,0 +1,25 @@ +use cqrs_es::{EventEnvelope, View}; +use oid4vc_core::authorization_request::Object; +use serde::{Deserialize, Serialize}; +use siopv2::siopv2::SIOPv2; + +use super::aggregate::Connection; + +pub type SIOPv2AuthorizationRequest = oid4vc_core::authorization_request::AuthorizationRequest>; + +#[derive(Debug, Default, Serialize, Deserialize, Clone)] +pub struct ConnectionView { + id_token: Option, +} + +impl View for ConnectionView { + fn update(&mut self, event: &EventEnvelope) { + use crate::connection::event::ConnectionEvent::*; + + match &event.payload { + SIOPv2AuthorizationResponseVerified { id_token } => { + self.id_token = Some(id_token.clone()); + } + } + } +} diff --git a/agent_verification/src/lib.rs b/agent_verification/src/lib.rs new file mode 100644 index 00000000..a440ff75 --- /dev/null +++ b/agent_verification/src/lib.rs @@ -0,0 +1,4 @@ +pub mod authorization_request; +pub mod connection; +pub mod services; +pub mod state; diff --git a/agent_verification/src/services.rs b/agent_verification/src/services.rs new file mode 100644 index 00000000..f492daa3 --- /dev/null +++ b/agent_verification/src/services.rs @@ -0,0 +1,32 @@ +use std::sync::Arc; + +use oid4vc_core::Subject; +use oid4vc_manager::RelyingPartyManager; + +/// Verification services. This struct is used to generate authorization requests and validate authorization responses. +pub struct VerificationServices { + pub verifier: Arc, + pub relying_party: RelyingPartyManager, +} + +impl VerificationServices { + pub fn new(verifier: Arc) -> Self { + Self { + verifier: verifier.clone(), + relying_party: RelyingPartyManager::new([verifier]).unwrap(), + } + } +} + +#[cfg(feature = "test")] +pub mod test_utils { + use agent_shared::secret_manager::secret_manager; + + use super::*; + + pub fn test_verification_services() -> Arc { + Arc::new(VerificationServices::new(Arc::new(futures::executor::block_on( + async { secret_manager().await }, + )))) + } +} diff --git a/agent_verification/src/state.rs b/agent_verification/src/state.rs new file mode 100644 index 00000000..00acbddf --- /dev/null +++ b/agent_verification/src/state.rs @@ -0,0 +1,46 @@ +use agent_shared::application_state::CommandHandler; +use cqrs_es::persist::ViewRepository; +use std::sync::Arc; + +use crate::authorization_request::aggregate::AuthorizationRequest; +use crate::authorization_request::queries::AuthorizationRequestView; +use crate::connection::aggregate::Connection; +use crate::connection::queries::ConnectionView; + +#[derive(Clone)] +pub struct VerificationState { + pub command: CommandHandlers, + pub query: Queries, +} + +/// The command handlers are used to execute commands on the aggregates. +#[derive(Clone)] +pub struct CommandHandlers { + pub authorization_request: CommandHandler, + pub connection: CommandHandler, +} +/// This type is used to define the queries that are used to query the view repositories. We make use of `dyn` here, so +/// that any type of repository that implements the `ViewRepository` trait can be used, but the corresponding `View` and +/// `Aggregate` types must be the same. +type Queries = ViewRepositories< + dyn ViewRepository, + dyn ViewRepository, +>; + +pub struct ViewRepositories +where + AR: ViewRepository + ?Sized, + C: ViewRepository + ?Sized, +{ + pub authorization_request: Arc, + pub connection: Arc, +} + +impl Clone for Queries { + fn clone(&self) -> Self { + ViewRepositories { + authorization_request: self.authorization_request.clone(), + connection: self.connection.clone(), + } + } +}