diff --git a/.github/workflows/checks.yaml b/.github/workflows/checks.yaml index 45928f1..bd14fe7 100644 --- a/.github/workflows/checks.yaml +++ b/.github/workflows/checks.yaml @@ -5,11 +5,12 @@ on: types: - opened - synchronize - + jobs: rust-pipeline: uses: affinidi/pipeline-rust/.github/workflows/checks.yaml@main secrets: inherit with: - auditIgnore: "RUSTSEC-2022-0040,RUSTSEC-2023-0071,RUSTSEC-2024-0373" + auditIgnore: 'RUSTSEC-2022-0040,RUSTSEC-2023-0071,RUSTSEC-2024-0373' coverage: 10 + useRedis: true diff --git a/Cargo.lock b/Cargo.lock index 1f4678c..56a6534 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -143,6 +143,7 @@ dependencies = [ "http 1.1.0", "itertools 0.13.0", "jsonwebtoken", + "lazy_static", "rand", "rcgen", "redis", diff --git a/affinidi-messaging-mediator/Cargo.toml b/affinidi-messaging-mediator/Cargo.toml index 9873f07..1a772f2 100644 --- a/affinidi-messaging-mediator/Cargo.toml +++ b/affinidi-messaging-mediator/Cargo.toml @@ -56,4 +56,5 @@ rcgen = { version = "0.13", default-features = false, features = [ "aws_lc_rs", "pem", ] } +lazy_static = "1.4.0" time = "0.3" diff --git a/affinidi-messaging-mediator/tests/common.rs b/affinidi-messaging-mediator/tests/common.rs new file mode 100644 index 0000000..5681056 --- /dev/null +++ b/affinidi-messaging-mediator/tests/common.rs @@ -0,0 +1,43 @@ +use lazy_static::lazy_static; +use serde_json::{json, Value}; + +pub const MY_DID: &str = "did:peer:2.Vz6MkgWJfVmPELozq6aCycK3CpxHN8Upphn3WSuQkWY6iqsjF.EzQ3shfb7vwQaTJqFkt8nRfo7Nu98tmeYpdDfWgrqQitDaqXRz"; +pub const MEDIATOR_API: &str = "https://localhost:7037/mediator/v1"; +pub const BOB_DID: &str = "did:peer:2.Vz6Mkihn2R3M8nY62EFJ7MAVXu7YxsTnuS5iAhmn3qKJbkdFf.EzQ3shpZRBUtewwzYiueXgDqs1bvGNkSyGoRgsbZJXt3TTb9jD.SeyJ0IjoiZG0iLCJzIjp7InVyaSI6Imh0dHBzOi8vbG9jYWxob3N0OjcwMzcvIiwiYWNjZXB0IjpbImRpZGNvbW0vdjIiXSwicm91dGluZ19rZXlzIjpbXX0sImlkIjpudWxsfQ"; +pub const SECRETS_PATH: &str = "../affinidi-messaging-mediator/conf/secrets.json"; +pub const CONFIG_PATH: &str = "../affinidi-messaging-mediator/conf/mediator.toml"; + +lazy_static! { +// Signing and verification key +pub static ref MY_V1: Value = json!({ + "crv": "Ed25519", + "d": "LLWCf83n8VsUYq31zlZRe0NNMCcn1N4Dh85dGpIqSFw", + "kty": "OKP", + "x": "Hn8T4ZjjT0oJ6rjhqox8AykwC3GDFsJF6KkaYZExwQo" +}); + +// Encryption key +pub static ref MY_E1: Value = json!({ + "crv": "secp256k1", + "d": "oi-dXG4EqfNODFPjv2vkieoLdbQZH9k6dwPDV8HDoms", + "kty": "EC", + "x": "DhfaXbhwo0KkOiyA5V1K1RZx6Ikr86h_lX5GOwxjmjE", + "y": "PpYqybOwMsm64vftt-7gBCQPIUbglMmyy_6rloSSAPk" +}); + +pub static ref BOB_V1: Value = json!({ + "crv": "Ed25519", + "d": "FZMJijqdcp7PCQShgtFj6Ud3vjZY7jFZBVvahziaMMM", + "kty": "OKP", + "x": "PybG95kyeSfGRebp4T7hzA7JQuysc6mZ97nM2ety6Vo" +}); + +pub static ref BOB_E1: Value = json!({ + "crv": "secp256k1", + "d": "ai7B5fgT3pCBHec0I4Y1xXpSyrEHlTy0hivSlddWHZE", + "kty": "EC", + "x": "k2FhEi8WMxr4Ztr4u2xjKzDESqVnGg_WKrN1820wPeA", + "y": "fq0DnZ_duPWyeFK0k93bAzjNJVVHEjHFRlGOJXKDS18" +}); + +} diff --git a/affinidi-messaging-mediator/tests/integration_test.rs b/affinidi-messaging-mediator/tests/integration_test.rs new file mode 100644 index 0000000..db5988a --- /dev/null +++ b/affinidi-messaging-mediator/tests/integration_test.rs @@ -0,0 +1,860 @@ +use affinidi_did_resolver_cache_sdk::{config::ClientConfigBuilder, DIDCacheClient}; +use affinidi_messaging_didcomm::{ + secrets::{Secret, SecretsResolver}, + Message, PackEncryptedOptions, +}; +use affinidi_messaging_mediator::{resolvers::affinidi_secrets::AffinidiSecrets, server::start}; +use affinidi_messaging_sdk::{ + config::Config, + conversions::secret_from_str, + errors::ATMError, + messages::{ + fetch::FetchOptions, sending::InboundMessageResponse, AuthenticationChallenge, + AuthorizationResponse, DeleteMessageRequest, DeleteMessageResponse, Folder, + GenericDataStruct, GetMessagesRequest, GetMessagesResponse, MessageList, + MessageListElement, SuccessResponse, + }, + transports::SendMessageResponse, +}; +use common::{ + BOB_DID, BOB_E1, BOB_V1, CONFIG_PATH, MEDIATOR_API, MY_DID, MY_E1, MY_V1, SECRETS_PATH, +}; +use core::panic; +use message_builders::{ + build_delivery_request_message, build_forward_request_message, build_message_received_message, + build_ping_message, build_status_request_message, create_auth_challenge_response, +}; +use reqwest::{Certificate, Client, ClientBuilder}; +use response_validations::{ + validate_forward_request_response, validate_get_message_response, validate_list_messages, + validate_message_delivery, validate_message_received_status_reply, validate_status_reply, +}; +use sha256::digest; +use std::{ + fs::{self, File}, + io::{self, BufRead, BufReader}, + path::Path, + process::Command, + str, + time::Duration, +}; +use tokio::time::sleep; + +mod common; +mod message_builders; +mod response_validations; + +#[tokio::test] +async fn test_mediator_server() { + // Generate secrets and did for mediator if not existing + if !fs::metadata(SECRETS_PATH).is_ok() { + println!("Generating secrets"); + _generate_keys(); + _generate_secrets(); + let mediator_did = _get_did_from_secrets(SECRETS_PATH.into()); + _inject_did_into_config(CONFIG_PATH, &mediator_did); + println!("Secrets generated and did injected to mediator.toml"); + } + + _start_mediator_server().await; + + // Allow some time for the server to start + sleep(Duration::from_millis(2000)).await; + + let config = Config::builder() + .with_ssl_certificates(&mut vec![ + "../affinidi-messaging-mediator/conf/keys/client.chain".into(), + ]) + .build() + .unwrap(); + + let did_resolver = DIDCacheClient::new(ClientConfigBuilder::default().build()) + .await + .unwrap(); + let my_secrets_resolver = AffinidiSecrets::new(vec![ + secret_from_str(&format!("{}#key-1", MY_DID), &MY_V1), + secret_from_str(&format!("{}#key-2", MY_DID), &MY_E1), + ]); + let bob_secrets_resolver = AffinidiSecrets::new(vec![ + secret_from_str(&format!("{}#key-1", BOB_DID), &BOB_V1), + secret_from_str(&format!("{}#key-2", BOB_DID), &BOB_E1), + ]); + + let client = init_client(config.clone()); + + let mediator_did = _well_known(client.clone()).await; + + // Start Authentication + let my_authentication_challenge = _authenticate_challenge(client.clone(), MY_DID).await; + let bob_authentication_challenge = _authenticate_challenge(client.clone(), BOB_DID).await; + + // /authenticate/challenge + let my_auth_response_msg = + create_auth_challenge_response(&my_authentication_challenge, MY_DID, &mediator_did); + + let bob_auth_response_msg = + create_auth_challenge_response(&bob_authentication_challenge, BOB_DID, &mediator_did); + + // /authenticate + let my_authentication_response = _authenticate( + client.clone(), + my_auth_response_msg, + MY_DID, + &mediator_did, + &did_resolver, + &my_secrets_resolver, + ) + .await; + let bob_authentication_response = _authenticate( + client.clone(), + bob_auth_response_msg, + BOB_DID, + &mediator_did, + &did_resolver, + &bob_secrets_resolver, + ) + .await; + + assert!(!my_authentication_response.access_token.is_empty()); + assert!(!my_authentication_response.refresh_token.is_empty()); + assert!(!bob_authentication_response.access_token.is_empty()); + assert!(!bob_authentication_response.refresh_token.is_empty()); + + // POST /inbound + // MessageType=TrustPing + // Send signed ping and expecting response + let (signed_ping_msg, mut signed_ping_msg_info) = build_ping_message( + &mediator_did, + MY_DID.into(), + true, + true, + &did_resolver, + &my_secrets_resolver, + ) + .await; + + let signed_ping_res: SendMessageResponse = _send_inbound_message( + client.clone(), + my_authentication_response.clone(), + &signed_ping_msg, + true, + 200, + ) + .await; + + signed_ping_msg_info.response = + if let SendMessageResponse::RestAPI(Some(InboundMessageResponse::Stored(m))) = + signed_ping_res + { + if let Some((_, msg_id)) = m.messages.first() { + Some(msg_id.to_owned()) + } else { + None + } + } else { + None + }; + + let pong_msg_id = signed_ping_msg_info.response.unwrap(); + assert!(!pong_msg_id.is_empty()); + + // Send anonymous ping + let (anon_ping_msg, _) = build_ping_message( + &mediator_did, + MY_DID.into(), + false, + false, + &did_resolver, + &my_secrets_resolver, + ) + .await; + let _anon_ping_res: SendMessageResponse = _send_inbound_message( + client.clone(), + my_authentication_response.clone(), + &anon_ping_msg, + false, + 500, + ) + .await; + + // MessageType=MessagePickupStatusRequest + let status_request_msg = build_status_request_message( + &mediator_did, + MY_DID.into(), + &did_resolver, + &my_secrets_resolver, + ) + .await; + let status_reply: SendMessageResponse = _send_inbound_message( + client.clone(), + my_authentication_response.clone(), + &status_request_msg, + false, + 200, + ) + .await; + validate_status_reply( + status_reply, + MY_DID.into(), + &did_resolver, + &my_secrets_resolver, + ) + .await; + + // MessageType=MessagePickupDeliveryRequest + let delivery_request_msg = build_delivery_request_message( + &mediator_did, + MY_DID.into(), + &did_resolver, + &my_secrets_resolver, + ) + .await; + let message_delivery: SendMessageResponse = _send_inbound_message( + client.clone(), + my_authentication_response.clone(), + &delivery_request_msg, + true, + 200, + ) + .await; + let message_received_ids = validate_message_delivery( + message_delivery, + &did_resolver, + &my_secrets_resolver, + &pong_msg_id, + ) + .await; + + // MessageType=MessagePickupMessagesReceived + let message_received_msg = build_message_received_message( + &mediator_did, + MY_DID.into(), + &did_resolver, + &my_secrets_resolver, + message_received_ids.clone(), + ) + .await; + let message_received_status_reply: SendMessageResponse = + _send_inbound_message( + client.clone(), + my_authentication_response.clone(), + &message_received_msg, + true, + 200, + ) + .await; + + validate_message_received_status_reply( + message_received_status_reply, + MY_DID.into(), + &did_resolver, + &my_secrets_resolver, + ) + .await; + + // MessageType=ForwardRequest + let forward_request_msg = build_forward_request_message( + &mediator_did, + MY_DID.into(), + BOB_DID.into(), + &did_resolver, + &bob_secrets_resolver, + ) + .await; + + let forward_request_response: SendMessageResponse = + _send_inbound_message( + client.clone(), + my_authentication_response.clone(), + &forward_request_msg, + true, + 200, + ) + .await; + + let forwarded_msg_id = validate_forward_request_response(forward_request_response).await; + + // /outbound + // delete messages: FALSE + let get_message_no_delete_request = GetMessagesRequest { + message_ids: vec![forwarded_msg_id.clone()], + delete: false, + }; + let msg_list = _outbound_message( + client.clone(), + &get_message_no_delete_request, + my_authentication_response.clone(), + 200, + false, + ) + .await; + + validate_get_message_response(msg_list, MY_DID, &did_resolver, &my_secrets_resolver).await; + + // delete messages: TRUE + let get_message_delete_request = GetMessagesRequest { + message_ids: vec![forwarded_msg_id.clone()], + delete: true, + }; + let msg_list = _outbound_message( + client.clone(), + &get_message_delete_request, + my_authentication_response.clone(), + 200, + false, + ) + .await; + + validate_get_message_response(msg_list, MY_DID, &did_resolver, &my_secrets_resolver).await; + + // get message should return not found + let _msg_list = _outbound_message( + client.clone(), + &get_message_delete_request, + my_authentication_response.clone(), + 200, + true, + ) + .await; + + // Sending messages to list/fetch + for _ in 0..3 { + let (signed_ping_msg, _) = build_ping_message( + &mediator_did, + MY_DID.into(), + true, + true, + &did_resolver, + &my_secrets_resolver, + ) + .await; + + let _: SendMessageResponse = _send_inbound_message( + client.clone(), + my_authentication_response.clone(), + &signed_ping_msg, + true, + 200, + ) + .await; + } + + // /list/:did_hash/:folder + // /list/:did_hash/Inbox + let msgs_list = list_messages( + client.clone(), + my_authentication_response.clone(), + 200, + MY_DID, + Folder::Inbox, + ) + .await; + validate_list_messages(msgs_list, &mediator_did); + + // /list/:did_hash/Outbox + let msgs_list = list_messages( + client.clone(), + my_authentication_response.clone(), + 200, + MY_DID, + Folder::Outbox, + ) + .await; + assert_eq!(msgs_list.len(), 0); + + // /fetch + let messages = _fetch_messages( + client.clone(), + my_authentication_response.clone(), + 200, + &FetchOptions { + limit: 10, + start_id: None, + delete_policy: affinidi_messaging_sdk::messages::FetchDeletePolicy::DoNotDelete, + }, + ) + .await; + assert_eq!(messages.success.len(), 3); + + let msg_ids: Vec = messages + .success + .iter() + .map(|msg| msg.msg_id.clone()) + .collect(); + + let deleted_msgs = _delete_messages( + client.clone(), + my_authentication_response.clone(), + 200, + &DeleteMessageRequest { + message_ids: msg_ids, + }, + ) + .await; + assert_eq!(deleted_msgs.success.len(), 3); +} + +async fn _start_mediator_server() { + tokio::spawn(async move { start().await }); + println!("Server running"); +} + +fn init_client(config: Config<'_>) -> Client { + // Set up the HTTPS client + let mut client = ClientBuilder::new() + .use_rustls_tls() + .https_only(true) + .user_agent("Affinidi Trusted Messaging"); + + for cert in config.get_ssl_certificates() { + client = + client.add_root_certificate(Certificate::from_der(cert.to_vec().as_slice()).unwrap()); + } + + let client = match client.build() { + Ok(client) => client, + Err(e) => { + assert!(false, "{:?}", e); + panic!(); + } + }; + client +} + +async fn _well_known(client: Client) -> String { + let well_known_did_atm_api = format!("{}/.well-known/did", MEDIATOR_API); + + let res = client + .get(well_known_did_atm_api) + .header("Content-Type", "application/json") + .send() + .await + .unwrap(); + + let status = res.status(); + assert_eq!(status, 200); + println!("API response: status({})", status); + let body = res.text().await.unwrap(); + + let body = serde_json::from_str::>(&body) + .ok() + .unwrap(); + let did = if let Some(did) = body.data { + did + } else { + panic!("Not able to fetch mediator did"); + }; + assert!(!did.is_empty()); + + did +} + +async fn _authenticate_challenge(client: Client, did: &str) -> AuthenticationChallenge { + let res = client + .post(format!("{}/authenticate/challenge", MEDIATOR_API)) + .header("Content-Type", "application/json") + .body(format!("{{\"did\": \"{}\"}}", did).to_string()) + .send() + .await + .unwrap(); + + let status = res.status(); + assert_eq!(status, 200); + + let body = res.text().await.unwrap(); + + if !status.is_success() { + println!("Failed to get authentication challenge. Body: {:?}", body); + assert!( + false, + "Failed to get authentication challenge. Body: {:?}", + body + ); + } + let body = serde_json::from_str::>(&body) + .ok() + .unwrap(); + + let challenge = if let Some(challenge) = body.data { + challenge + } else { + panic!("No challenge received from ATM"); + }; + assert!(!challenge.challenge.is_empty()); + assert!(!challenge.session_id.is_empty()); + + challenge +} + +async fn _authenticate<'sr>( + client: Client, + auth_response: Message, + my_did: &str, + atm_did: &str, + did_resolver: &DIDCacheClient, + secrets_resolver: &'sr (dyn SecretsResolver + 'sr + Sync), +) -> AuthorizationResponse { + let (auth_msg, _) = auth_response + .pack_encrypted( + atm_did, + Some(my_did), + Some(my_did), + did_resolver, + secrets_resolver, + &PackEncryptedOptions::default(), + ) + .await + .map_err(|e| { + ATMError::MsgSendError(format!( + "Couldn't pack authentication response message: {:?}", + e + )) + }) + .unwrap(); + + let res = client + .post(format!("{}/authenticate", MEDIATOR_API)) + .header("Content-Type", "application/json") + .body(auth_msg) + .send() + .await + .map_err(|e| { + ATMError::TransportError(format!("Could not post authentication response: {:?}", e)) + }) + .unwrap(); + + let status = res.status(); + println!("Authentication response: status({})", status); + + let body = res.text().await.unwrap(); + + assert!(status.is_success(), "Received status code: {}", status); + + if !status.is_success() { + println!("Failed to get authentication response. Body: {:?}", body); + panic!("Failed to get authentication response"); + } + let body = serde_json::from_str::>(&body).unwrap(); + + if let Some(tokens) = body.data { + return tokens.clone(); + } else { + panic!("No tokens received from ATM"); + } +} + +async fn _send_inbound_message( + client: Client, + tokens: AuthorizationResponse, + message: &str, + return_response: bool, + expected_status_code: u16, +) -> SendMessageResponse +where + T: GenericDataStruct, +{ + let msg = message.to_owned(); + + let res = client + .post(format!("{}/inbound", MEDIATOR_API)) + .header("Content-Type", "application/json") + .header("Authorization", format!("Bearer {}", tokens.access_token)) + .body(msg) + .send() + .await + .unwrap(); + + let status = res.status(); + println!("API response: status({})", status); + assert_eq!(status, expected_status_code); + + let body = res.text().await.unwrap(); + + let http_response: Option = if return_response { + let r: SuccessResponse = serde_json::from_str(&body).unwrap(); + r.data + } else { + None + }; + + SendMessageResponse::RestAPI(http_response) +} + +async fn _outbound_message( + client: Client, + messages: &GetMessagesRequest, + tokens: AuthorizationResponse, + expected_status_code: u16, + expecting_get_errors: bool, +) -> GetMessagesResponse { + let body = serde_json::to_string(messages) + .map_err(|e| { + ATMError::TransportError(format!("Could not serialize get message request: {:?}", e)) + }) + .unwrap(); + + let res = client + .post(format!("{}/outbound", MEDIATOR_API)) + .header("Content-Type", "application/json") + .header("Authorization", format!("Bearer {}", tokens.access_token)) + .body(body) + .send() + .await + .map_err(|e| { + ATMError::TransportError(format!("Could not send get_messages request: {:?}", e)) + }) + .unwrap(); + + let status = res.status(); + println!("API response: status({})", status); + assert_eq!(status, expected_status_code); + let body = res + .text() + .await + .map_err(|e| ATMError::TransportError(format!("Couldn't get body: {:?}", e))) + .unwrap(); + + let body = serde_json::from_str::>(&body) + .ok() + .unwrap(); + + let list = if let Some(list) = body.data { + list + } else { + panic!("No messages found") + }; + + if expecting_get_errors { + assert!(!list.get_errors.is_empty()) + } else { + assert!(list.get_errors.is_empty()) + } + + if !list.get_errors.is_empty() { + for (msg, err) in &list.get_errors { + println!("failed get: msg({}) error({})", msg, err); + } + } + if !list.delete_errors.is_empty() { + for (msg, err) in &list.delete_errors { + println!("failed delete: msg({}) error({})", msg, err); + } + } + list +} + +async fn list_messages( + client: Client, + tokens: AuthorizationResponse, + expected_status_code: u16, + my_did: &str, + folder: Folder, +) -> Vec { + let res = client + .get(format!( + "{}/list/{}/{}", + MEDIATOR_API, + digest(my_did), + folder, + )) + .header("Content-Type", "application/json") + .header("Authorization", format!("Bearer {}", tokens.access_token)) + .send() + .await + .map_err(|e| { + ATMError::TransportError(format!("Could not send list_messages request: {:?}", e)) + }) + .unwrap(); + + let status = res.status(); + println!("API response: status({})", status); + assert_eq!(status, expected_status_code); + + let body = res + .text() + .await + .map_err(|e| ATMError::TransportError(format!("Couldn't get body: {:?}", e))) + .unwrap(); + + let body = serde_json::from_str::>(&body) + .ok() + .unwrap(); + + let list = if let Some(list) = body.data { + list + } else { + panic!("No messages found"); + }; + + list +} + +async fn _fetch_messages( + client: Client, + tokens: AuthorizationResponse, + expected_status_code: u16, + options: &FetchOptions, +) -> GetMessagesResponse { + let body = serde_json::to_string(options) + .map_err(|e| { + ATMError::TransportError(format!( + "Could not serialize fetch_message() options: {:?}", + e + )) + }) + .unwrap(); + + let res = client + .post(format!("{}/fetch", MEDIATOR_API)) + .header("Content-Type", "application/json") + .header("Authorization", format!("Bearer {}", tokens.access_token)) + .body(body) + .send() + .await + .map_err(|e| { + ATMError::TransportError(format!("Could not send list_messages request: {:?}", e)) + }) + .unwrap(); + + let status = res.status(); + println!("API response: status({})", status); + assert_eq!(status, expected_status_code); + + let body = res + .text() + .await + .map_err(|e| ATMError::TransportError(format!("Couldn't get body: {:?}", e))) + .unwrap(); + + let body = serde_json::from_str::>(&body) + .ok() + .unwrap(); + + let list = if let Some(list) = body.data { + list + } else { + panic!("No messages found"); + }; + list +} + +async fn _delete_messages( + client: Client, + tokens: AuthorizationResponse, + expected_status_code: u16, + messages: &DeleteMessageRequest, +) -> DeleteMessageResponse { + let msg = serde_json::to_string(messages) + .map_err(|e| { + ATMError::TransportError(format!( + "Could not serialize delete message request: {:?}", + e + )) + }) + .unwrap(); + + let res = client + .delete(format!("{}/delete", MEDIATOR_API)) + .header("Content-Type", "application/json") + .header("Authorization", format!("Bearer {}", tokens.access_token)) + .body(msg) + .send() + .await + .map_err(|e| { + ATMError::TransportError(format!("Could not send delete_messages request: {:?}", e)) + }) + .unwrap(); + + let status = res.status(); + println!("API response: status({})", status); + assert_eq!(status, expected_status_code); + + let body = res + .text() + .await + .map_err(|e| ATMError::TransportError(format!("Couldn't get body: {:?}", e))) + .unwrap(); + + let body = serde_json::from_str::>(&body) + .ok() + .unwrap(); + + let list = if let Some(list) = body.data { + list + } else { + panic!("No messages found"); + }; + + if !list.errors.is_empty() { + for (msg, err) in &list.errors { + println!("failed: msg({}) error({})", msg, err); + } + panic!("Failed to delete above messages") + } + + list +} + +fn _generate_secrets() { + let output = Command::new("cargo") + .args(&["run", "--example", "generate_secrets"]) + .output() + .expect("Failed to generate secrets"); + assert!(output.status.success()); + let source_path = "../affinidi-messaging-mediator/conf/secrets.json-generated"; + + let _ = match fs::copy(source_path, SECRETS_PATH) { + Ok(_) => println!("Copied {} to {}", source_path, SECRETS_PATH), + Err(e) => panic!("Failed with error: {e:?}"), + }; +} + +fn _generate_keys() { + let output = Command::new("cargo") + .args(&["run", "--example", "create_local_certs"]) + .output() + .expect("Failed to create local certs"); + assert!(output.status.success()); +} + +fn _get_did_from_secrets(path: String) -> String { + let file = File::open(path).unwrap(); + let reader = BufReader::new(file); + + // Parse the JSON file + let config: Vec = serde_json::from_reader(reader).unwrap(); + let id_split: Vec<&str> = config.first().unwrap().id.split("#").collect(); + let did = *id_split.first().unwrap(); + did.into() +} + +fn _inject_did_into_config

(file_name: P, did: &str) +where + P: AsRef, +{ + let file = File::open(file_name.as_ref()) + .map_err(|err| { + panic!( + "{}", + format!( + "Could not open file({}). {}", + file_name.as_ref().display(), + err + ) + ); + }) + .unwrap(); + + let mut lines: Vec = Vec::new(); + for mut line in io::BufReader::new(file).lines().map_while(Result::ok) { + // Strip comments out + if line.starts_with("mediator_did =") { + let line_split: Vec<&str> = line.split("//").collect(); + let line_beginning = *line_split.first().unwrap(); + line = format!("{}{}{}{}", line_beginning, "//", did, "}\""); + } + lines.push(line); + } + let config_file = lines.join("\n"); + fs::write(file_name, config_file).expect("Failed to write to file"); +} diff --git a/affinidi-messaging-mediator/tests/message_builders.rs b/affinidi-messaging-mediator/tests/message_builders.rs new file mode 100644 index 0000000..36e9cf0 --- /dev/null +++ b/affinidi-messaging-mediator/tests/message_builders.rs @@ -0,0 +1,252 @@ +use std::time::SystemTime; + +use affinidi_did_resolver_cache_sdk::DIDCacheClient; +use affinidi_messaging_didcomm::{ + secrets::SecretsResolver, Attachment, Message, PackEncryptedOptions, +}; +use affinidi_messaging_sdk::{ + messages::AuthenticationChallenge, + protocols::{message_pickup::MessagePickupDeliveryRequest, trust_ping::TrustPingSent}, +}; +use serde_json::{json, Value}; +use sha256::digest; +use uuid::Uuid; + +pub fn create_auth_challenge_response( + body: &AuthenticationChallenge, + my_did: &str, + atm_did: &str, +) -> Message { + let now = _get_time_now(); + + Message::build( + Uuid::new_v4().into(), + "https://affinidi.com/atm/1.0/authenticate".to_owned(), + json!(body), + ) + .to(atm_did.to_owned()) + .from(my_did.to_owned()) + .created_time(now) + .expires_time(now + 60) + .finalize() +} + +pub async fn build_ping_message<'sr>( + to_did: &str, + my_did: String, + signed: bool, + expect_response: bool, + did_resolver: &DIDCacheClient, + secrets_resolver: &'sr (dyn SecretsResolver + 'sr + Sync), +) -> (String, TrustPingSent) { + let now = _get_time_now(); + + let mut msg = Message::build( + Uuid::new_v4().into(), + "https://didcomm.org/trust-ping/2.0/ping".to_owned(), + json!({"response_requested": expect_response}), + ) + .to(to_did.to_owned()); + + let from_did = if !signed { + // Can support anonymous pings + None + } else { + msg = msg.from(my_did.clone()); + Some(my_did.clone()) + }; + let msg = msg.created_time(now).expires_time(now + 300).finalize(); + let mut msg_info = TrustPingSent { + message_id: msg.id.clone(), + message_hash: "".to_string(), + bytes: 0, + response: None, + }; + let (msg, _) = msg + .pack_encrypted( + to_did, + from_did.as_deref(), + from_did.as_deref(), + did_resolver, + secrets_resolver, + &PackEncryptedOptions::default(), + ) + .await + .unwrap(); + + msg_info.message_hash = digest(&msg).to_string(); + msg_info.bytes = msg.len() as u32; + + (msg, msg_info) +} + +pub async fn build_status_request_message<'sr>( + mediator_did: &str, + recipient_did: String, + did_resolver: &DIDCacheClient, + secrets_resolver: &'sr (dyn SecretsResolver + 'sr + Sync), +) -> String { + let mut msg = Message::build( + Uuid::new_v4().into(), + "https://didcomm.org/messagepickup/3.0/status-request".to_owned(), + json!({}), + ) + .header("return_route".into(), Value::String("all".into())); + + msg = msg.body(json!({"recipient_did": recipient_did })); + + let to_did = mediator_did; + + msg = msg.to(to_did.to_owned()); + + msg = msg.from(recipient_did.clone().into()); + let now = _get_time_now(); + let msg = msg.created_time(now).expires_time(now + 300).finalize(); + let (msg, _) = msg + .pack_encrypted( + &to_did, + Some(&recipient_did), + Some(&recipient_did), + &did_resolver, + secrets_resolver, + &PackEncryptedOptions::default(), + ) + .await + .unwrap(); + + msg +} + +pub async fn build_delivery_request_message<'sr>( + mediator_did: &str, + recipient_did: String, + did_resolver: &DIDCacheClient, + secrets_resolver: &'sr (dyn SecretsResolver + 'sr + Sync), +) -> String { + let body = MessagePickupDeliveryRequest { + recipient_did: recipient_did.clone(), + limit: 10, + }; + + let mut msg = Message::build( + Uuid::new_v4().into(), + "https://didcomm.org/messagepickup/3.0/delivery-request".to_owned(), + serde_json::to_value(body).unwrap(), + ) + .header("return_route".into(), Value::String("all".into())); + + let to_did = mediator_did; + msg = msg.to(to_did.to_owned()); + + msg = msg.from(recipient_did.clone().to_owned()); + let now = _get_time_now(); + let msg = msg.created_time(now).expires_time(now + 300).finalize(); + + // Pack the message + let (msg, _) = msg + .pack_encrypted( + &to_did, + Some(&recipient_did), + Some(&recipient_did), + &did_resolver, + secrets_resolver, + &PackEncryptedOptions::default(), + ) + .await + .unwrap(); + + msg +} + +pub async fn build_message_received_message<'sr>( + mediator_did: &str, + recipient_did: String, + did_resolver: &DIDCacheClient, + secrets_resolver: &'sr (dyn SecretsResolver + 'sr + Sync), + to_delete_list: Vec, +) -> String { + let mut msg = Message::build( + Uuid::new_v4().into(), + "https://didcomm.org/messagepickup/3.0/messages-received".to_owned(), + json!({"message_id_list": to_delete_list}), + ) + .header("return_route".into(), Value::String("all".into())); + + let to_did = mediator_did; + msg = msg.to(to_did.to_owned()); + + msg = msg.from(recipient_did.clone().into()); + let now = SystemTime::now() + .duration_since(SystemTime::UNIX_EPOCH) + .unwrap() + .as_secs(); + let msg = msg.created_time(now).expires_time(now + 300).finalize(); + + // Pack the message + let (msg, _) = msg + .pack_encrypted( + &to_did, + Some(&recipient_did), + Some(&recipient_did), + &did_resolver, + secrets_resolver, + &PackEncryptedOptions::default(), + ) + .await + .unwrap(); + + msg +} + +pub async fn build_forward_request_message<'sr>( + mediator_did: &str, + recipient_did: String, + actor_did: String, + did_resolver: &DIDCacheClient, + secrets_resolver: &'sr (dyn SecretsResolver + 'sr + Sync), +) -> String { + let now = _get_time_now(); + + let recipient_did = recipient_did; + + let msg = Message::build( + Uuid::new_v4().into(), + "https://didcomm.org/routing/2.0/forward".to_owned(), + json!({ "next": recipient_did }), + ) + .to(mediator_did.to_owned()) + .from(actor_did.clone()) + .attachment( + Attachment::json(json!({ "message": "plaintext attachment, mediator can read this" })) + .finalize(), + ) + .attachment( + Attachment::base64(String::from( + "ciphertext and iv which is encrypted by the recipient public key", + )) + .finalize(), + ); + + let msg = msg.created_time(now).expires_time(now + 300).finalize(); + + // Pack the message + let (msg, _) = msg + .pack_encrypted( + &mediator_did.to_owned(), + Some(&actor_did.clone()), + Some(&actor_did.clone()), + &did_resolver, + secrets_resolver, + &PackEncryptedOptions::default(), + ) + .await + .unwrap(); + msg +} + +fn _get_time_now() -> u64 { + SystemTime::now() + .duration_since(SystemTime::UNIX_EPOCH) + .unwrap() + .as_secs() +} diff --git a/affinidi-messaging-mediator/tests/response_validations.rs b/affinidi-messaging-mediator/tests/response_validations.rs new file mode 100644 index 0000000..0cb7a5c --- /dev/null +++ b/affinidi-messaging-mediator/tests/response_validations.rs @@ -0,0 +1,223 @@ +use affinidi_did_resolver_cache_sdk::DIDCacheClient; +use affinidi_messaging_didcomm::{ + envelope::MetaEnvelope, secrets::SecretsResolver, AttachmentData, Message, UnpackMetadata, + UnpackOptions, +}; +use affinidi_messaging_sdk::{ + messages::{sending::InboundMessageResponse, GetMessagesResponse, MessageListElement}, + protocols::message_pickup::MessagePickupStatusReply, + transports::SendMessageResponse, +}; +use base64::{prelude::BASE64_URL_SAFE_NO_PAD, Engine}; +use sha256::digest; + +pub async fn validate_status_reply( + status_reply: SendMessageResponse, + recipient_did: String, + did_resolver: &DIDCacheClient, + secrets_resolver: &S, +) where + S: SecretsResolver + Send, +{ + if let SendMessageResponse::RestAPI(Some(InboundMessageResponse::Ephemeral(message))) = + status_reply + { + let (message, _) = Message::unpack_string( + &message, + &did_resolver, + secrets_resolver, + &UnpackOptions::default(), + ) + .await + .unwrap(); + let status: MessagePickupStatusReply = + serde_json::from_value(message.body.clone()).unwrap(); + assert!(!status.live_delivery); + assert!(status.longest_waited_seconds.unwrap() > 0); + assert!(status.message_count == 1); + assert!(status.recipient_did == recipient_did); + assert!(status.total_bytes > 0); + } +} + +pub async fn validate_message_delivery( + message_delivery: SendMessageResponse, + did_resolver: &DIDCacheClient, + secrets_resolver: &S, + pong_msg_id: &str, +) -> Vec +where + S: SecretsResolver + Send, +{ + if let SendMessageResponse::RestAPI(Some(InboundMessageResponse::Ephemeral(message))) = + message_delivery + { + let (message, _) = Message::unpack_string( + &message, + &did_resolver, + secrets_resolver, + &UnpackOptions::default(), + ) + .await + .unwrap(); + + let messages = _handle_delivery(&message, did_resolver, secrets_resolver).await; + let mut to_delete_ids: Vec = Vec::new(); + + assert_eq!(messages.first().unwrap().0.id, pong_msg_id); + + for (message, _) in messages { + to_delete_ids.push(message.id.clone()); + } + to_delete_ids + } else { + vec![] + } +} + +async fn _handle_delivery( + message: &Message, + did_resolver: &DIDCacheClient, + secrets_resolver: &S, +) -> Vec<(Message, UnpackMetadata)> +where + S: SecretsResolver + Send, +{ + let mut response: Vec<(Message, UnpackMetadata)> = Vec::new(); + + if let Some(attachments) = &message.attachments { + for attachment in attachments { + match &attachment.data { + AttachmentData::Base64 { value } => { + let decoded = match BASE64_URL_SAFE_NO_PAD.decode(value.base64.clone()) { + Ok(decoded) => match String::from_utf8(decoded) { + Ok(decoded) => decoded, + Err(e) => { + assert!(false, "{:?}", e); + "".into() + } + }, + Err(e) => { + assert!(false, "{:?}", e); + continue; + } + }; + let mut envelope = + match MetaEnvelope::new(&decoded, &did_resolver, secrets_resolver).await { + Ok(envelope) => envelope, + Err(e) => { + assert!(false, "{:?}", e); + continue; + } + }; + + match Message::unpack( + &mut envelope, + did_resolver, + secrets_resolver, + &UnpackOptions::default(), + ) + .await + { + Ok((mut m, u)) => { + if let Some(attachment_id) = &attachment.id { + m.id = attachment_id.to_string(); + } + response.push((m, u)) + } + Err(e) => { + assert!(false, "{:?}", e); + continue; + } + }; + } + _ => { + assert!(false); + continue; + } + }; + } + } + + response +} + +pub async fn validate_message_received_status_reply( + status_reply: SendMessageResponse, + recipient_did: String, + did_resolver: &DIDCacheClient, + secrets_resolver: &S, +) where + S: SecretsResolver + Send, +{ + if let SendMessageResponse::RestAPI(Some(InboundMessageResponse::Ephemeral(message))) = + status_reply + { + let (message, _) = Message::unpack_string( + &message, + &did_resolver, + secrets_resolver, + &UnpackOptions::default(), + ) + .await + .unwrap(); + let status: MessagePickupStatusReply = + serde_json::from_value(message.body.clone()).unwrap(); + + assert!(!status.live_delivery); + assert!(status.longest_waited_seconds.is_none()); + assert!(status.message_count == 0); + assert!(status.recipient_did == recipient_did); + assert!(status.total_bytes == 0); + } +} + +pub async fn validate_forward_request_response( + forward_request_response: SendMessageResponse, +) -> String { + let msg_id = if let SendMessageResponse::RestAPI(Some(InboundMessageResponse::Stored(m))) = + forward_request_response + { + if let Some((_, msg_id)) = m.messages.first() { + Some(msg_id.to_owned()) + } else { + None + } + } else { + None + }; + + assert!(!msg_id.is_none()); + + msg_id.unwrap() +} + +pub async fn validate_get_message_response( + list: GetMessagesResponse, + my_did: &str, + did_resolver: &DIDCacheClient, + secrets_resolver: &S, +) where + S: SecretsResolver + Send, +{ + for msg in list.success { + assert_eq!(msg.to_address.unwrap(), digest(my_did)); + let _ = Message::unpack_string( + &msg.msg.unwrap(), + did_resolver, + secrets_resolver, + &UnpackOptions::default(), + ) + .await + .unwrap(); + println!("Msg id: {}", msg.msg_id); + } +} + +pub fn validate_list_messages(list: Vec, mediator_did: &str) { + assert_eq!(list.len(), 3); + + for msg in list { + assert_eq!(msg.from_address.unwrap(), mediator_did); + } +}