diff --git a/Cargo.toml b/Cargo.toml index fa01da07..5d3ec491 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,9 +1,17 @@ [package] -name = "didcomm-mediator" -version = "0.1.0" +name = "didcomm-mediator" +version = "0.1.0" +authors = ["adorsys GmbH Co. KG"] +license = "Apache-2.0" +description = "A Rust Mediator for DIDComm messaging, supporting secure and decentralized communication." +repository = "https://github.com/adorsys/didcomm-mediator-rs" +homepage = "https://github.com/adorsys/didcomm-mediator-rs/blob/main/README.md" +documentation = "https://github.com/adorsys/didcomm-mediator-rs/tree/main/docs" +keywords = ["DIDComm", "Mediator", "DIDComm Mediator", "DIDComm Mediation", "DIDCOMM Messaging", "Decentralized Identity", "Rust Mediator"] +categories = ["cryptography", "decentralized-systems"] edition = "2021" -description = "A mediator for DIDComm messages" -authors = ["adorsys GmbH Co. KG"] +readme = "README.md" + [workspace] diff --git a/README.md b/README.md index c0a17e65..d8c1caac 100644 --- a/README.md +++ b/README.md @@ -19,12 +19,12 @@ For further understanding checkout the [docs](docs/mediator-doc.md)
|[Pickup Protocol](https://didcomm.org/messagepickup/3.0/)| ADOPTED | ✅ | |[DID Rotation](https://didcomm.org/book/v2/didrotation) | ACCEPTED | ✅ | |[Cross-Domain Messaging/ Routing Protocol](https://identity.foundation/didcomm-messaging/spec/#routing-protocol-20) | ADOPTED | ✅| -|[Trust Ping Ptotocol](https://identity.foundation/didcomm-messaging/spec/#trust-ping-protocol-20) | ADOPTED|⚪| +|[Trust Ping Ptotocol](https://identity.foundation/didcomm-messaging/spec/#trust-ping-protocol-20) | ADOPTED|✅| |[Discover Features Protocol](https://didcomm.org/discover-features/2.0/) | ADOPTED | ⚪ | |[Out of band Messaging](https://identity.foundation/didcomm-messaging/spec/#out-of-band-messages) | ADOPTED | ⚪ +|[Basic Message Protocol](https://didcomm.org/basicmessage/2.0/#:~:text=The%20BasicMessage%20protocol%20describes%20a,message%20type%20used%20to%20communicate.) | ADOPTED|⚪| |[Acks](https://github.com/hyperledger/aries-rfcs/tree/main/features/0015-acks)| ADOPTED |❌ | |[Present Proof Protocol](https://didcomm.org/present-proof/3.0/)| ADOPTED | ❌| -|[Basic Message Protocol](https://didcomm.org/basicmessage/2.0/#:~:text=The%20BasicMessage%20protocol%20describes%20a,message%20type%20used%20to%20communicate.) | ADOPTED|❌| ## Building and testing diff --git a/crates/web-plugins/didcomm-messaging/Cargo.toml b/crates/web-plugins/didcomm-messaging/Cargo.toml index 7acb4eb5..8da6d9bf 100644 --- a/crates/web-plugins/didcomm-messaging/Cargo.toml +++ b/crates/web-plugins/didcomm-messaging/Cargo.toml @@ -13,6 +13,7 @@ plugin-api.workspace = true filesystem.workspace = true forward.workspace = true pickup.workspace = true +trust-ping.workspace = true mediator-coordination.workspace = true mongodb.workspace = true diff --git a/crates/web-plugins/didcomm-messaging/protocols/forward/src/error.rs b/crates/web-plugins/didcomm-messaging/protocols/forward/src/error.rs index 2595acdf..67fa1b18 100644 --- a/crates/web-plugins/didcomm-messaging/protocols/forward/src/error.rs +++ b/crates/web-plugins/didcomm-messaging/protocols/forward/src/error.rs @@ -1,25 +1,29 @@ -use axum::Json; -use serde_json::{json, Value}; +use axum::{response::IntoResponse, Json}; +use hyper::StatusCode; use thiserror::Error; #[derive(Debug, Error)] -pub enum RoutingError { +pub enum ForwardError { #[error("message body is malformed")] MalformedBody, - #[error("Repository not set")] - RepostitoryError -} -impl RoutingError { - /// Converts the error to an axum JSON representation. - pub fn json(&self) -> Json { - Json(json!({ - "error": self.to_string() - })) - } + #[error("Uncoordinated sender")] + UncoordinatedSender, + #[error("Internal server error")] + InternalServerError, } -impl From for Json { - fn from(error: RoutingError) -> Self { - error.json() +impl IntoResponse for ForwardError { + fn into_response(self) -> axum::response::Response { + let status_code = match self { + ForwardError::MalformedBody => StatusCode::BAD_REQUEST, + ForwardError::UncoordinatedSender => StatusCode::UNAUTHORIZED, + ForwardError::InternalServerError => StatusCode::INTERNAL_SERVER_ERROR, + }; + + let body = Json(serde_json::json!({ + "error": self.to_string(), + })); + + (status_code, body).into_response() } } diff --git a/crates/web-plugins/didcomm-messaging/protocols/forward/src/lib.rs b/crates/web-plugins/didcomm-messaging/protocols/forward/src/lib.rs index 82f5807c..e17bcec6 100644 --- a/crates/web-plugins/didcomm-messaging/protocols/forward/src/lib.rs +++ b/crates/web-plugins/didcomm-messaging/protocols/forward/src/lib.rs @@ -1,3 +1,5 @@ mod error; - pub mod web; + +// Re-exports +pub use error::ForwardError; diff --git a/crates/web-plugins/didcomm-messaging/protocols/forward/src/web/handler.rs b/crates/web-plugins/didcomm-messaging/protocols/forward/src/web/handler.rs index eb3f40ad..92413149 100644 --- a/crates/web-plugins/didcomm-messaging/protocols/forward/src/web/handler.rs +++ b/crates/web-plugins/didcomm-messaging/protocols/forward/src/web/handler.rs @@ -1,14 +1,14 @@ -use super::routing::handler; -use axum::response::Response; +use crate::{web::routing::handler, ForwardError}; use didcomm::Message; use shared::state::AppState; +use std::sync::Arc; /// Mediator receives forwarded messages, extract the next field in the message body, and the attachments in the message /// then stores the attachment with the next field as key for pickup pub async fn mediator_forward_process( - state: &AppState, + state: Arc, payload: Message, -) -> Result { - let result = handler(state, payload).await.unwrap(); +) -> Result, ForwardError> { + let result = handler(state.clone(), payload).await.unwrap(); Ok(result) } diff --git a/crates/web-plugins/didcomm-messaging/protocols/forward/src/web/routing.rs b/crates/web-plugins/didcomm-messaging/protocols/forward/src/web/routing.rs index 1778da99..999400a6 100644 --- a/crates/web-plugins/didcomm-messaging/protocols/forward/src/web/routing.rs +++ b/crates/web-plugins/didcomm-messaging/protocols/forward/src/web/routing.rs @@ -1,12 +1,9 @@ -use crate::error::RoutingError; -use axum::response::{IntoResponse, Response}; +use crate::error::ForwardError; use database::Repository; use didcomm::{AttachmentData, Message}; -use hyper::StatusCode; use mongodb::bson::doc; use serde_json::{json, Value}; use shared::{ - errors::MediationError, repository::entity::{Connection, RoutedMessage}, state::{AppState, AppStateRepository}, }; @@ -15,35 +12,27 @@ use std::sync::Arc; async fn checks( message: &Message, connection_repository: &Arc>, -) -> Result { +) -> Result { let next = message.body.get("next").and_then(Value::as_str); match next { Some(next) => next, - None => { - let response = (StatusCode::BAD_REQUEST, RoutingError::MalformedBody.json()); - return Err(response.into_response()); - } + None => return Err(ForwardError::MalformedBody), }; // Check if the client's did in mediator's keylist let _connection = match connection_repository .find_one_by(doc! {"keylist": doc!{ "$elemMatch": { "$eq": &next}}}) .await - .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR.into_response())? + .map_err(|_| ForwardError::InternalServerError)? { Some(connection) => connection, - None => { - let response = ( - StatusCode::UNAUTHORIZED, - MediationError::UncoordinatedSender.json(), - ); - return Err(response.into_response()); - } + None => return Err(ForwardError::UncoordinatedSender), }; + Ok(next.unwrap().to_string()) } -pub(crate) async fn handler(state: &AppState, message: Message) -> Result { +pub(crate) async fn handler(state: Arc, message: Message) -> Result, ForwardError> { let AppStateRepository { message_repository, connection_repository, @@ -51,11 +40,11 @@ pub(crate) async fn handler(state: &AppState, message: Message) -> Result Ok(next), - None => Err(MediationError::RepostitoryError), + None => Err(ForwardError::InternalServerError), }; let attachments = message.attachments.unwrap_or_default(); @@ -72,9 +61,9 @@ pub(crate) async fn handler(state: &AppState, message: Message) -> Result Vec { let _recipient_did = _recipient_did(); @@ -125,14 +114,14 @@ mod test { // simulate sender forwarding process let mut state = tests::setup().clone(); let state = Arc::make_mut(&mut state); - + let mock_connections = MockConnectionRepository::from(_initial_connections()); state.repository = Some(AppStateRepository { connection_repository: Arc::new(mock_connections), message_repository: Arc::new(MockMessagesRepository::from(vec![])), keystore: Arc::new(MockKeyStore::new(vec![])), }); - + let msg = Message::build( Uuid::new_v4().to_string(), "example/v1".to_owned(), @@ -153,10 +142,6 @@ mod test { ) .await .expect("Unable pack_encrypted"); - println!("Encryption metadata is\n{:?}\n", _metadata); - - // --- Sending message by Alice --- - println!("Alice is sending message \n{}\n", packed_forward_msg); let msg = wrap_in_forward( &packed_forward_msg, @@ -169,7 +154,6 @@ mod test { .await .expect("Unable wrap_in_forward"); - println!(" wraped in forward\n{}\n", msg); let (msg, _metadata) = Message::unpack( &msg, &state.did_resolver, @@ -179,14 +163,7 @@ mod test { .await .expect("Unable unpack"); - println!("Mediator1 received message is \n{:?}\n", msg); - - println!( - "Mediator1 received message unpack metadata is \n{:?}\n", - _metadata - ); - - let msg = mediator_forward_process(state, msg).await.unwrap(); + let msg = mediator_forward_process(Arc::new(state.clone()), msg).await.unwrap(); println!("Mediator1 is forwarding message \n{:?}\n", msg); } diff --git a/crates/web-plugins/didcomm-messaging/protocols/mediator-coordination/src/errors.rs b/crates/web-plugins/didcomm-messaging/protocols/mediator-coordination/src/errors.rs new file mode 100644 index 00000000..ed25318d --- /dev/null +++ b/crates/web-plugins/didcomm-messaging/protocols/mediator-coordination/src/errors.rs @@ -0,0 +1,34 @@ +use axum::{response::IntoResponse, Json}; +use hyper::StatusCode; +use thiserror::Error; + +/// Represents errors that can occur during mediation. +#[derive(Debug, Error, PartialEq, Eq)] +pub enum MediationError { + #[error("No return route all decoration")] + NoReturnRouteAllDecoration, + #[error("invalid message type")] + InvalidMessageType, + #[error("uncoordinated sender")] + UncoordinatedSender, + #[error("could not parse into expected message format")] + UnexpectedMessageFormat, +} + +impl IntoResponse for MediationError { + fn into_response(self) -> axum::response::Response { + let status_code = match self { + MediationError::NoReturnRouteAllDecoration | MediationError::InvalidMessageType => { + StatusCode::BAD_REQUEST + } + MediationError::UncoordinatedSender => StatusCode::UNAUTHORIZED, + MediationError::UnexpectedMessageFormat => StatusCode::BAD_REQUEST, + }; + + let body = Json(serde_json::json!({ + "error": self.to_string(), + })); + + (status_code, body).into_response() + } +} diff --git a/crates/web-plugins/didcomm-messaging/protocols/mediator-coordination/src/lib.rs b/crates/web-plugins/didcomm-messaging/protocols/mediator-coordination/src/lib.rs index 49cc1258..33872a31 100644 --- a/crates/web-plugins/didcomm-messaging/protocols/mediator-coordination/src/lib.rs +++ b/crates/web-plugins/didcomm-messaging/protocols/mediator-coordination/src/lib.rs @@ -1,5 +1,9 @@ +mod errors; +mod jose; +mod model; + pub mod client; pub mod web; -mod jose; -mod model; +// Re-exports +pub use errors::MediationError; diff --git a/crates/web-plugins/didcomm-messaging/protocols/mediator-coordination/src/web/handler/midlw.rs b/crates/web-plugins/didcomm-messaging/protocols/mediator-coordination/src/web/handler/midlw.rs index 54fcde1c..dfa894e6 100644 --- a/crates/web-plugins/didcomm-messaging/protocols/mediator-coordination/src/web/handler/midlw.rs +++ b/crates/web-plugins/didcomm-messaging/protocols/mediator-coordination/src/web/handler/midlw.rs @@ -1,36 +1,13 @@ -use axum::{ - http::StatusCode, - response::{IntoResponse, Response}, -}; +use crate::errors::MediationError; use didcomm::Message; use serde_json::{json, Value}; - -use shared::{ - constants::{MEDIATE_REQUEST_2_0, MEDIATE_REQUEST_DIC_1_0}, - errors::MediationError, -}; - -macro_rules! run { - ($expression:expr) => { - match $expression { - Ok(res) => res, - - Err(err) => return Err(err), - } - }; -} - -pub(crate) use run; - +use shared::constants::{MEDIATE_REQUEST_2_0, MEDIATE_REQUEST_DIC_1_0}; /// Validate that JWM's indicative body type is a mediation request -pub(crate) fn ensure_jwm_type_is_mediation_request(message: &Message) -> Result<(), Response> { +pub(crate) fn ensure_jwm_type_is_mediation_request( + message: &Message, +) -> Result<(), MediationError> { if ![MEDIATE_REQUEST_2_0, MEDIATE_REQUEST_DIC_1_0].contains(&message.type_.as_str()) { - let response = ( - StatusCode::BAD_REQUEST, - MediationError::InvalidMessageType.json(), - ); - - return Err(response.into_response()); + return Err(MediationError::InvalidMessageType); } Ok(()) @@ -41,14 +18,9 @@ pub(crate) fn ensure_jwm_type_is_mediation_request(message: &Message) -> Result< pub fn ensure_mediation_request_type( mediation_request: &Value, message_type: &str, -) -> Result<(), Response> { +) -> Result<(), MediationError> { if mediation_request.get("type") != Some(&json!(message_type)) { - let response = ( - StatusCode::BAD_REQUEST, - MediationError::InvalidMessageType.json(), - ); - - return Err(response.into_response()); + return Err(MediationError::InvalidMessageType); } Ok(()) @@ -57,7 +29,7 @@ pub fn ensure_mediation_request_type( #[cfg(test)] mod tests { use super::*; - use shared::{utils::tests_utils::tests::*, midlw::tests::assert_error}; + use shared::utils::tests_utils::tests::*; #[cfg(feature = "stateless")] use crate::model::stateless::coord::{ @@ -75,8 +47,6 @@ mod tests { "urn:uuid:8f8208ae-6e16-4275-bde8-7b7cb81ffa59".to_owned(), MEDIATE_REQUEST_2_0.to_string(), json!({ - "id": "id_alice_mediation_request", - "type": MEDIATE_REQUEST_2_0, "did": "did:key:alice_identity_pub@alice_mediator", "services": ["inbox", "outbox"] }), @@ -91,8 +61,6 @@ mod tests { "urn:uuid:8f8208ae-6e16-4275-bde8-7b7cb81ffa59".to_owned(), MEDIATE_REQUEST_DIC_1_0.to_string(), json!({ - "id": "id_alice_mediation_request", - "type": MEDIATE_REQUEST_DIC_1_0, "did": "did:key:alice_identity_pub@alice_mediator", "services": ["inbox", "outbox"] }), @@ -109,8 +77,6 @@ mod tests { "urn:uuid:8f8208ae-6e16-4275-bde8-7b7cb81ffa59".to_owned(), "invalid-type".to_string(), json!({ - "id": "id_alice_mediation_request", - "type": MEDIATE_REQUEST_2_0, "did": "did:key:alice_identity_pub@alice_mediator", "services": ["inbox", "outbox"] }), @@ -119,12 +85,10 @@ mod tests { .from(_edge_did()) .finalize(); - assert_error( + assert_eq!( ensure_jwm_type_is_mediation_request(&msg).unwrap_err(), - StatusCode::BAD_REQUEST, - &MediationError::InvalidMessageType.json().0, - ) - .await; + MediationError::InvalidMessageType, + ); } #[cfg(feature = "stateless")] diff --git a/crates/web-plugins/didcomm-messaging/protocols/mediator-coordination/src/web/handler/stateful.rs b/crates/web-plugins/didcomm-messaging/protocols/mediator-coordination/src/web/handler/stateful.rs index ee81b390..b37077cc 100644 --- a/crates/web-plugins/didcomm-messaging/protocols/mediator-coordination/src/web/handler/stateful.rs +++ b/crates/web-plugins/didcomm-messaging/protocols/mediator-coordination/src/web/handler/stateful.rs @@ -1,6 +1,11 @@ -use axum::{ - http::StatusCode, - response::{IntoResponse, Response}, +use crate::{ + errors::MediationError, + model::stateful::coord::{ + Keylist, KeylistBody, KeylistEntry, KeylistUpdateAction, KeylistUpdateBody, + KeylistUpdateConfirmation, KeylistUpdateResponseBody, KeylistUpdateResult, MediationDeny, + MediationGrant, MediationGrantBody, + }, + web::handler::midlw::ensure_jwm_type_is_mediation_request, }; use did_utils::{ crypto::{Ed25519KeyPair, Generate, ToMultikey, X25519KeyPair}, @@ -9,41 +14,29 @@ use did_utils::{ methods::{DidPeer, Purpose, PurposedKey}, }; use didcomm::{did::DIDResolver, Message}; +use keystore::Secrets; use mongodb::bson::doc; use serde_json::json; -use std::sync::Arc; -use uuid::Uuid; - -use crate::{ - model::stateful::coord::{ - Keylist, KeylistBody, KeylistEntry, KeylistUpdateAction, KeylistUpdateBody, - KeylistUpdateConfirmation, KeylistUpdateResponseBody, KeylistUpdateResult, MediationDeny, - MediationGrant, MediationGrantBody, - }, - web::handler::midlw::{self, ensure_jwm_type_is_mediation_request}, -}; - -use keystore::Secrets; use shared::{ constants::{KEYLIST_2_0, KEYLIST_UPDATE_RESPONSE_2_0, MEDIATE_DENY_2_0, MEDIATE_GRANT_2_0}, - errors::MediationError, midlw::ensure_transport_return_route_is_decorated_all, repository::entity::Connection, state::{AppState, AppStateRepository}, }; +use std::sync::Arc; +use uuid::Uuid; /// Process a DIDComm mediate request pub async fn process_mediate_request( - state: &AppState, + state: Arc, plain_message: &Message, -) -> Result { +) -> Result, MediationError> { // This is to Check message type compliance - midlw::run!(ensure_jwm_type_is_mediation_request(&plain_message)); + ensure_jwm_type_is_mediation_request(&plain_message)?; // This is to Check explicit agreement to HTTP responding - midlw::run!(ensure_transport_return_route_is_decorated_all( - &plain_message - )); + ensure_transport_return_route_is_decorated_all(&plain_message) + .map_err(|_| MediationError::NoReturnRouteAllDecoration)?; let mediator_did = &state.diddoc.id; @@ -69,18 +62,20 @@ pub async fn process_mediate_request( .unwrap() { println!("Sending mediate deny."); - return Ok(Message::build( - format!("urn:uuid:{}", Uuid::new_v4()), - MEDIATE_DENY_2_0.to_string(), - json!(MediationDeny { - id: format!("urn:uuid:{}", Uuid::new_v4()), - message_type: MEDIATE_DENY_2_0.to_string(), - ..Default::default() - }), - ) - .to(sender_did.clone()) - .from(mediator_did.clone()) - .finalize()); + return Ok(Some( + Message::build( + format!("urn:uuid:{}", Uuid::new_v4()), + MEDIATE_DENY_2_0.to_string(), + json!(MediationDeny { + id: format!("urn:uuid:{}", Uuid::new_v4()), + message_type: MEDIATE_DENY_2_0.to_string(), + ..Default::default() + }), + ) + .to(sender_did.clone()) + .from(mediator_did.clone()) + .finalize(), + )); } else { /* Issue mediate grant response */ println!("Sending mediate grant."); @@ -148,14 +143,16 @@ pub async fn process_mediate_request( Err(error) => eprintln!("Error storing connection: {:?}", error), } - Ok(Message::build( - format!("urn:uuid:{}", Uuid::new_v4()), - mediation_grant.message_type.clone(), - json!(mediation_grant), - ) - .to(sender_did.clone()) - .from(mediator_did.clone()) - .finalize()) + Ok(Some( + Message::build( + format!("urn:uuid:{}", Uuid::new_v4()), + mediation_grant.message_type.clone(), + json!(mediation_grant), + ) + .to(sender_did.clone()) + .from(mediator_did.clone()) + .finalize(), + )) } } @@ -208,7 +205,7 @@ fn generate_did_peer(service_endpoint: String) -> (String, Ed25519KeyPair, X2551 pub async fn process_plain_keylist_update_message( state: Arc, message: Message, -) -> Result { +) -> Result, MediationError> { // Extract message sender let sender = message @@ -217,17 +214,8 @@ pub async fn process_plain_keylist_update_message( // Parse message body into keylist update - let keylist_update_body: KeylistUpdateBody = match serde_json::from_value(message.body) { - Ok(serialized) => serialized, - Err(_) => { - let response = ( - StatusCode::BAD_REQUEST, - MediationError::UnexpectedMessageFormat.json(), - ); - - return Err(response.into_response()); - } - }; + let keylist_update_body: KeylistUpdateBody = serde_json::from_value(message.body) + .map_err(|_| MediationError::UnexpectedMessageFormat)?; // Retrieve repository to connection entities @@ -241,21 +229,11 @@ pub async fn process_plain_keylist_update_message( // Find connection for this keylist update - let connection = match connection_repository + let connection = connection_repository .find_one_by(doc! { "client_did": &sender }) .await .unwrap() - { - Some(connection) => connection, - None => { - let response = ( - StatusCode::UNAUTHORIZED, - MediationError::UncoordinatedSender.json(), - ); - - return Err(response.into_response()); - } - }; + .ok_or_else(|| MediationError::UncoordinatedSender)?; // Prepare handles to relevant collections @@ -338,22 +316,24 @@ pub async fn process_plain_keylist_update_message( let mediator_did = &state.diddoc.id; - Ok(Message::build( - format!("urn:uuid:{}", Uuid::new_v4()), - KEYLIST_UPDATE_RESPONSE_2_0.to_string(), - json!(KeylistUpdateResponseBody { - updated: confirmations - }), - ) - .to(sender) - .from(mediator_did.to_owned()) - .finalize()) + Ok(Some( + Message::build( + format!("urn:uuid:{}", Uuid::new_v4()), + KEYLIST_UPDATE_RESPONSE_2_0.to_string(), + json!(KeylistUpdateResponseBody { + updated: confirmations + }), + ) + .to(sender) + .from(mediator_did.to_owned()) + .finalize(), + )) } pub async fn process_plain_keylist_query_message( state: Arc, message: Message, -) -> Result { +) -> Result, MediationError> { println!("Processing keylist query..."); let sender = message .from @@ -367,21 +347,11 @@ pub async fn process_plain_keylist_query_message( .as_ref() .expect("missing persistence layer"); - let connection = match connection_repository + let connection = connection_repository .find_one_by(doc! { "client_did": &sender }) .await .unwrap() - { - Some(connection) => connection, - None => { - let response = ( - StatusCode::UNAUTHORIZED, - MediationError::UncoordinatedSender.json(), - ); - - return Err(response.into_response()); - } - }; + .ok_or_else(|| MediationError::UncoordinatedSender)?; println!("keylist: {:?}", connection); @@ -418,13 +388,11 @@ pub async fn process_plain_keylist_query_message( println!("message: {:?}", message); - Ok(message) + Ok(Some(message)) } #[cfg(test)] mod tests { - use serde_json::Value; - use super::*; use shared::{ @@ -465,7 +433,8 @@ mod tests { // Process request let response = process_plain_keylist_query_message(Arc::clone(&state), message) .await - .unwrap(); + .unwrap() + .expect("Response should not be None"); assert_eq!(response.type_, KEYLIST_2_0); assert_eq!(response.from.unwrap(), global::_mediator_did(&state)); @@ -490,12 +459,7 @@ mod tests { .await .unwrap_err(); // Assert issued error for uncoordinated sender - _assert_delegate_handler_err( - err, - StatusCode::UNAUTHORIZED, - MediationError::UncoordinatedSender, - ) - .await; + assert_eq!(err, MediationError::UncoordinatedSender,); } #[tokio::test] async fn test_keylist_update() { @@ -528,7 +492,8 @@ mod tests { let response = process_plain_keylist_update_message(Arc::clone(&state), message) .await - .unwrap(); + .unwrap() + .expect("Response should not be None"); let response = response; // Assert metadata @@ -626,9 +591,8 @@ mod tests { let response = process_plain_keylist_update_message(Arc::clone(&state), message) .await - .unwrap(); - - let response = response; + .unwrap() + .expect("Response should not be None"); // Assert updates assert_eq!( @@ -691,8 +655,8 @@ mod tests { let response = process_plain_keylist_update_message(Arc::clone(&state), message) .await - .unwrap(); - let response = response; + .unwrap() + .expect("Response should not be None"); // Assert updates assert_eq!( @@ -751,8 +715,8 @@ mod tests { let response = process_plain_keylist_update_message(Arc::clone(&state), message) .await - .unwrap(); - let response = response; + .unwrap() + .expect("Response should not be None"); // Assert updates @@ -791,12 +755,7 @@ mod tests { .unwrap_err(); // Assert issued error - _assert_delegate_handler_err( - err, - StatusCode::BAD_REQUEST, - MediationError::UnexpectedMessageFormat, - ) - .await; + assert_eq!(err, MediationError::UnexpectedMessageFormat,); } #[tokio::test] @@ -842,12 +801,7 @@ mod tests { .unwrap_err(); // Assert issued error - _assert_delegate_handler_err( - err, - StatusCode::UNAUTHORIZED, - MediationError::UncoordinatedSender, - ) - .await; + assert_eq!(err, MediationError::UncoordinatedSender,); } //---------------------------------------------------------------------------------------------- @@ -882,22 +836,6 @@ mod tests { .unwrap() } - async fn _assert_delegate_handler_err( - err: Response, - status: StatusCode, - mediation_error: MediationError, - ) { - assert_eq!(err.status(), status); - - let body = hyper::body::to_bytes(err.into_body()).await.unwrap(); - let body: Value = serde_json::from_slice(&body).unwrap(); - - assert_eq!( - json_canon::to_string(&body).unwrap(), - json_canon::to_string(&mediation_error.json().0).unwrap() - ); - } - use did_utils::crypto::{KeyMaterial, BYTES_LENGTH_32}; #[test] diff --git a/crates/web-plugins/didcomm-messaging/protocols/pickup/src/error.rs b/crates/web-plugins/didcomm-messaging/protocols/pickup/src/error.rs index 55ea8750..037bcd4d 100644 --- a/crates/web-plugins/didcomm-messaging/protocols/pickup/src/error.rs +++ b/crates/web-plugins/didcomm-messaging/protocols/pickup/src/error.rs @@ -1,29 +1,29 @@ use axum::{http::StatusCode, response::IntoResponse, Json}; -use serde::Serialize; use thiserror::Error; -#[derive(Debug, Serialize, Error)] -pub enum PickupError<'a> { +#[derive(Debug, Error, PartialEq)] +pub enum PickupError { #[error("Missing sender DID")] MissingSenderDID, #[error("{0}")] - InternalError(&'a str), + InternalError(String), #[error("No client connection found")] MissingClientConnection, #[error("Malformed request. {0}")] - MalformedRequest(&'a str), + MalformedRequest(String), } -impl IntoResponse for PickupError<'_> { +impl IntoResponse for PickupError { fn into_response(self) -> axum::response::Response { let status_code = match self { - PickupError::MissingSenderDID => StatusCode::BAD_REQUEST, + PickupError::MissingSenderDID | PickupError::MalformedRequest(_) => { + StatusCode::BAD_REQUEST + } PickupError::InternalError(_) => StatusCode::INTERNAL_SERVER_ERROR, PickupError::MissingClientConnection => StatusCode::UNAUTHORIZED, - PickupError::MalformedRequest(_) => StatusCode::BAD_REQUEST, }; let body = Json(serde_json::json!({ diff --git a/crates/web-plugins/didcomm-messaging/protocols/pickup/src/handler.rs b/crates/web-plugins/didcomm-messaging/protocols/pickup/src/handler.rs index 372cbca2..0530c114 100644 --- a/crates/web-plugins/didcomm-messaging/protocols/pickup/src/handler.rs +++ b/crates/web-plugins/didcomm-messaging/protocols/pickup/src/handler.rs @@ -5,7 +5,6 @@ use crate::{ LiveDeliveryChange, StatusResponse, }, }; -use axum::response::{IntoResponse, Response}; use didcomm::{Attachment, Message, MessageBuilder}; use mongodb::bson::{doc, oid::ObjectId}; use serde_json::Value; @@ -22,9 +21,10 @@ use uuid::Uuid; pub async fn handle_status_request( state: Arc, message: Message, -) -> Result { +) -> Result, PickupError> { // Validate the return_route header - ensure_transport_return_route_is_decorated_all(&message)?; + ensure_transport_return_route_is_decorated_all(&message) + .map_err(|_| PickupError::MalformedRequest("Missing return_route header".to_owned()))?; let mediator_did = &state.diddoc.id; let recipient_did = message @@ -56,16 +56,17 @@ pub async fn handle_status_request( .from(mediator_did.to_owned()) .finalize(); - Ok(response) + Ok(Some(response)) } // Process pickup delivery request pub async fn handle_delivery_request( state: Arc, message: Message, -) -> Result { +) -> Result, PickupError> { // Validate the return_route header - ensure_transport_return_route_is_decorated_all(&message)?; + ensure_transport_return_route_is_decorated_all(&message) + .map_err(|_| PickupError::MalformedRequest("Missing return_route header".to_owned()))?; let mediator_did = &state.diddoc.id; let recipient_did = message @@ -79,9 +80,7 @@ pub async fn handle_delivery_request( .body .get("limit") .and_then(Value::as_u64) - .ok_or_else(|| { - PickupError::MalformedRequest("Invalid \"limit\" specifier").into_response() - })?; + .ok_or_else(|| PickupError::MalformedRequest("Invalid \"limit\" specifier".to_owned()))?; let repository = repository(state.clone())?; let connection = client_connection(&repository, sender_did).await?; @@ -110,9 +109,8 @@ pub async fn handle_delivery_request( let attached = Attachment::json(message.message) .id(message.id.map(|id| id.to_string()).ok_or_else(|| { PickupError::InternalError( - "Failed to load requested messages. Please try again later.", + "Failed to load requested messages. Please try again later.".to_owned(), ) - .into_response() })?) .finalize(); @@ -134,16 +132,17 @@ pub async fn handle_delivery_request( .from(mediator_did.to_owned()) .finalize(); - Ok(response) + Ok(Some(response)) } // Process pickup messages acknowledgement pub async fn handle_message_acknowledgement( state: Arc, message: Message, -) -> Result { +) -> Result, PickupError> { // Validate the return_route header - ensure_transport_return_route_is_decorated_all(&message)?; + ensure_transport_return_route_is_decorated_all(&message) + .map_err(|_| PickupError::MalformedRequest("Missing return_route header".to_owned()))?; let mediator_did = &state.diddoc.id; let repository = repository(state.clone())?; @@ -157,24 +156,24 @@ pub async fn handle_message_acknowledgement( .and_then(|v| v.as_array()) .map(|a| a.iter().filter_map(|v| v.as_str()).collect::>()) .ok_or_else(|| { - PickupError::MalformedRequest("Invalid \"message_id_list\" specifier").into_response() + PickupError::MalformedRequest("Invalid \"message_id_list\" specifier".to_owned()) })?; for id in message_id_list { let msg_id = ObjectId::from_str(id); if msg_id.is_err() { - return Err(PickupError::MalformedRequest( - format!("Invalid message id: {id}").as_str(), - ) - .into_response()); + return Err(PickupError::MalformedRequest(format!( + "Invalid message id: {id}" + ))); } repository .message_repository .delete_one(msg_id.unwrap()) .await .map_err(|_| { - PickupError::InternalError("Failed to process the request. Please try again later.") - .into_response() + PickupError::InternalError( + "Failed to process the request. Please try again later.".to_owned(), + ) })?; } @@ -197,42 +196,47 @@ pub async fn handle_message_acknowledgement( .from(mediator_did.to_owned()) .finalize(); - Ok(response) + Ok(Some(response)) } // Process live delivery change request pub async fn handle_live_delivery_change( state: Arc, message: Message, -) -> Result { +) -> Result, PickupError> { // Validate the return_route header - ensure_transport_return_route_is_decorated_all(&message)?; + ensure_transport_return_route_is_decorated_all(&message) + .map_err(|_| PickupError::MalformedRequest("Missing return_route header".to_owned()))?; + + match message.body.get("live_delivery").and_then(Value::as_bool) { + Some(true) => { + let mediator_did = &state.diddoc.id; + let sender_did = sender_did(&message)?; + let id = Uuid::new_v4().urn().to_string(); + let pthid = message.thid.as_deref().unwrap_or(id.as_str()); + + let response_builder: MessageBuilder = LiveDeliveryChange { + id: id.as_str(), + pthid, + type_: PROBLEM_REPORT_2_0, + body: BodyLiveDeliveryChange { + code: "e.m.live-mode-not-supported", + comment: "Connection does not support Live Delivery", + }, + } + .into(); - if let Some(_live_delivery) = message.body.get("live_delivery").and_then(Value::as_bool) { - let mediator_did = &state.diddoc.id; - let sender_did = sender_did(&message)?; - let id = Uuid::new_v4().urn().to_string(); - let pthid = message.thid.as_deref().unwrap_or(id.as_str()); + let response = response_builder + .to(sender_did.to_owned()) + .from(mediator_did.to_owned()) + .finalize(); - let response_builder: MessageBuilder = LiveDeliveryChange { - id: id.as_str(), - pthid, - type_: PROBLEM_REPORT_2_0, - body: BodyLiveDeliveryChange { - code: "e.m.live-mode-not-supported", - comment: "Connection does not support Live Delivery", - }, + Ok(Some(response)) } - .into(); - - let response = response_builder - .to(sender_did.to_owned()) - .from(mediator_did.to_owned()) - .finalize(); - - Ok(response) - } else { - Err(PickupError::MalformedRequest("Missing \"live_delivery\" specifier").into_response()) + Some(false) => Ok(None), + None => Err(PickupError::MalformedRequest( + "Missing \"live_delivery\" specifier".to_owned(), + )), } } @@ -240,7 +244,7 @@ async fn count_messages( repository: AppStateRepository, recipient_did: Option<&str>, connection: Connection, -) -> Result { +) -> Result { let recipients = recipients(recipient_did, &connection); let count = repository @@ -248,8 +252,9 @@ async fn count_messages( .count_by(doc! { "recipient_did": { "$in": recipients } }) .await .map_err(|_| { - PickupError::InternalError("Failed to process the request. Please try again later.") - .into_response() + PickupError::InternalError( + "Failed to process the request. Please try again later.".to_owned(), + ) })?; Ok(count) @@ -260,7 +265,7 @@ async fn messages( recipient_did: Option<&str>, connection: Connection, limit: usize, -) -> Result, Response> { +) -> Result, PickupError> { let recipients = recipients(recipient_did, &connection); let routed_messages = repository @@ -271,8 +276,9 @@ async fn messages( ) .await .map_err(|_| { - PickupError::InternalError("Failed to process the request. Please try again later.") - .into_response() + PickupError::InternalError( + "Failed to process the request. Please try again later.".to_owned(), + ) })?; Ok(routed_messages) @@ -292,49 +298,47 @@ fn recipients<'a>(recipient_did: Option<&'a str>, connection: &'a Connection) -> } #[inline] -fn sender_did(message: &Message) -> Result<&str, Response> { +fn sender_did(message: &Message) -> Result<&str, PickupError> { message .from .as_ref() .map(|did| did.as_str()) - .ok_or(PickupError::MissingSenderDID.into_response()) + .ok_or(PickupError::MissingSenderDID) } #[inline] -fn repository(state: Arc) -> Result { - state.repository.clone().ok_or( - PickupError::InternalError("Internal server error. Please try again later.") - .into_response(), - ) +fn repository(state: Arc) -> Result { + state.repository.clone().ok_or(PickupError::InternalError( + "Internal server error. Please try again later.".to_owned(), + )) } #[inline] async fn client_connection( repository: &AppStateRepository, client_did: &str, -) -> Result { +) -> Result { repository .connection_repository .find_one_by(doc! { "client_did": client_did }) .await .map_err(|_| { - PickupError::InternalError("Failed to process the request. Please try again later.") - .into_response() + PickupError::InternalError( + "Failed to process the request. Please try again later.".to_owned(), + ) })? - .ok_or(PickupError::MissingClientConnection.into_response()) + .ok_or(PickupError::MissingClientConnection) } #[cfg(test)] mod tests { use super::*; - use hyper::StatusCode; use serde_json::json; use shared::{ constants::{ DELIVERY_REQUEST_3_0, LIVE_MODE_CHANGE_3_0, MESSAGE_DELIVERY_3_0, MESSAGE_RECEIVED_3_0, PROBLEM_REPORT_2_0, STATUS_REQUEST_3_0, STATUS_RESPONSE_3_0, }, - midlw::tests::assert_error, repository::tests::{MockConnectionRepository, MockMessagesRepository}, utils::tests_utils::tests as global, }; @@ -401,7 +405,8 @@ mod tests { let response = handle_status_request(Arc::clone(&state), request) .await - .unwrap(); + .unwrap() + .expect("Response should not be None"); assert_eq!(response.type_, STATUS_RESPONSE_3_0); assert_eq!(response.from.unwrap(), global::_mediator_did(&state)); @@ -429,7 +434,8 @@ mod tests { let response = handle_status_request(Arc::clone(&state), request) .await - .unwrap(); + .unwrap() + .expect("Response should not be None"); assert_eq!( response.body, @@ -455,7 +461,8 @@ mod tests { let response = handle_status_request(Arc::clone(&state), request) .await - .unwrap(); + .unwrap() + .expect("Response should not be None"); assert_eq!( response.body, @@ -482,12 +489,7 @@ mod tests { .await .unwrap_err(); - assert_error( - error, - StatusCode::UNAUTHORIZED, - &json!({"error": PickupError::MissingClientConnection.to_string()}), - ) - .await; + assert_eq!(error, PickupError::MissingClientConnection); } #[tokio::test] @@ -506,7 +508,8 @@ mod tests { let response = handle_delivery_request(Arc::clone(&state), request) .await - .unwrap(); + .unwrap() + .expect("Response should not be None"); let expected_attachments = vec![ Attachment::json(json!("test1")) @@ -548,7 +551,10 @@ mod tests { // When the specified recipient did is not in the keylist, // it should return a status response with a message count of 0 - let response = handle_delivery_request(state, request).await.unwrap(); + let response = handle_delivery_request(state, request) + .await + .unwrap() + .expect("Response should not be None"); assert_eq!(response.type_, STATUS_RESPONSE_3_0); assert_eq!( @@ -574,7 +580,10 @@ mod tests { // When the limit is set to 0, it should return all the messages in the queue // and since the recipient did is not specified, it should return the messages // for all the dids in the keylist for that sender connection - let response = handle_delivery_request(state, request).await.unwrap(); + let response = handle_delivery_request(state, request) + .await + .unwrap() + .expect("Response should not be None"); let expected_attachments = vec![ Attachment::json(json!("test1")) @@ -611,7 +620,10 @@ mod tests { // Since the recipient did is not specified, it should return the messages // for all the dids in the keylist for that sender connection (2 in this case) // The limit is set to 1 so it should return the first message in the queue - let response = handle_delivery_request(state, request).await.unwrap(); + let response = handle_delivery_request(state, request) + .await + .unwrap() + .expect("Response should not be None"); let expected_attachments = vec![Attachment::json(json!("test1")) .id(ObjectId::from_str("6580701fd2d92bb3cd291b2a") @@ -641,7 +653,8 @@ mod tests { // Should return 2 since these ids are not associated with any message let response = handle_message_acknowledgement(state, request) .await - .unwrap(); + .unwrap() + .expect("Response should not be None"); assert_eq!(response.type_, STATUS_RESPONSE_3_0); assert_eq!( @@ -668,7 +681,8 @@ mod tests { // to the first message in the queue and then will be deleted let response = handle_message_acknowledgement(state, request) .await - .unwrap(); + .unwrap() + .expect("Response should not be None"); assert_eq!(response.type_, STATUS_RESPONSE_3_0); assert_eq!( @@ -692,7 +706,10 @@ mod tests { .from(global::_edge_did()) .finalize(); - let response = handle_live_delivery_change(state, request).await.unwrap(); + let response = handle_live_delivery_change(state, request) + .await + .unwrap() + .expect("Response should not be None"); assert_eq!(response.pthid.unwrap(), "123"); assert_eq!(response.type_, PROBLEM_REPORT_2_0); @@ -705,6 +722,26 @@ mod tests { ); } + #[tokio::test] + async fn test_handle_live_delivery_change_false() { + let state = setup(test_connections(), test_messages()); + + let request = Message::build( + "id_alice_live_delivery_change".to_owned(), + LIVE_MODE_CHANGE_3_0.to_owned(), + json!({"live_delivery": false}), + ) + .thid("123".to_owned()) + .header("return_route".into(), json!("all")) + .to(global::_mediator_did(&state)) + .from(global::_edge_did()) + .finalize(); + + let response = handle_live_delivery_change(state, request).await.unwrap(); + + assert_eq!(response, None); + } + #[tokio::test] async fn test_handle_live_delivery_change_with_invalid_body() { let state = setup(test_connections(), test_messages()); @@ -724,11 +761,9 @@ mod tests { .await .unwrap_err(); - assert_error( + assert_eq!( error, - StatusCode::BAD_REQUEST, - &json!({"error": PickupError::MalformedRequest("Missing \"live_delivery\" specifier").to_string()}) - ) - .await; + PickupError::MalformedRequest("Missing \"live_delivery\" specifier".to_owned()) + ); } } diff --git a/crates/web-plugins/didcomm-messaging/protocols/pickup/src/lib.rs b/crates/web-plugins/didcomm-messaging/protocols/pickup/src/lib.rs index ca8b21f0..1c7cc076 100644 --- a/crates/web-plugins/didcomm-messaging/protocols/pickup/src/lib.rs +++ b/crates/web-plugins/didcomm-messaging/protocols/pickup/src/lib.rs @@ -2,3 +2,6 @@ mod error; mod model; pub mod handler; + +// Re-exports +pub use error::PickupError; diff --git a/crates/web-plugins/didcomm-messaging/protocols/trust-ping/src/error.rs b/crates/web-plugins/didcomm-messaging/protocols/trust-ping/src/error.rs index 0d107f98..2c8d4741 100644 --- a/crates/web-plugins/didcomm-messaging/protocols/trust-ping/src/error.rs +++ b/crates/web-plugins/didcomm-messaging/protocols/trust-ping/src/error.rs @@ -2,16 +2,16 @@ use axum::{http::StatusCode, response::IntoResponse, Json}; use serde::Serialize; use thiserror::Error; -#[derive(Debug, Serialize, Error)] -pub(crate) enum TrustPingError<'a> { +#[derive(Debug, Serialize, Error, PartialEq, Eq)] +pub enum TrustPingError { #[error("Missing sender DID")] MissingSenderDID, #[error("Malformed request. {0}")] - MalformedRequest(&'a str), + MalformedRequest(String), } -impl IntoResponse for TrustPingError<'_> { +impl IntoResponse for TrustPingError { fn into_response(self) -> axum::response::Response { let status_code = match self { TrustPingError::MissingSenderDID => StatusCode::BAD_REQUEST, diff --git a/crates/web-plugins/didcomm-messaging/protocols/trust-ping/src/handler.rs b/crates/web-plugins/didcomm-messaging/protocols/trust-ping/src/handler.rs index b1ad7a04..9d468d8d 100644 --- a/crates/web-plugins/didcomm-messaging/protocols/trust-ping/src/handler.rs +++ b/crates/web-plugins/didcomm-messaging/protocols/trust-ping/src/handler.rs @@ -1,6 +1,4 @@ -use axum::response::{IntoResponse, Response}; use didcomm::{Message, MessageBuilder}; -use hyper::StatusCode; use std::sync::Arc; use uuid::Uuid; @@ -12,12 +10,12 @@ use shared::constants::TRUST_PING_RESPONSE_2_0; pub async fn handle_trust_ping( state: Arc, message: Message, -) -> Result { +) -> Result, TrustPingError> { let mediator_did = &state.diddoc.id; let sender_did = message .from .as_ref() - .ok_or(TrustPingError::MissingSenderDID.into_response())?; + .ok_or(TrustPingError::MissingSenderDID)?; match message .body @@ -36,41 +34,22 @@ pub async fn handle_trust_ping( .to(sender_did.to_owned()) .from(mediator_did.to_owned()) .finalize(); - Ok(response) + Ok(Some(response)) } - Some(false) => Err(StatusCode::OK.into_response()), - None => Err( - TrustPingError::MalformedRequest("Missing \"response_requested\" field.") - .into_response(), - ), + Some(false) => Ok(None), + None => Err(TrustPingError::MalformedRequest( + "Missing \"response_requested\" field.".to_owned(), + )), } } #[cfg(test)] mod tests { - use serde_json::{json, Value}; + use serde_json::json; use super::*; use shared::{constants::TRUST_PING_2_0, utils::tests_utils::tests as global}; - async fn assert_error( - response: Response, - status: StatusCode, - error: Option>, - ) { - assert_eq!(response.status(), status); - - if let Some(error) = error { - let body = hyper::body::to_bytes(response.into_body()).await.unwrap(); - let body: Value = serde_json::from_slice(&body).unwrap(); - - assert_eq!( - json_canon::to_string(&body).unwrap(), - json_canon::to_string(&json!({"error": error.to_string()})).unwrap() - ); - } - } - #[tokio::test] async fn test_request_trust_ping_response() { let state = global::setup(); @@ -86,7 +65,8 @@ mod tests { let response = handle_trust_ping(Arc::clone(&state), request) .await - .unwrap(); + .unwrap() + .expect("Response should not be None"); assert_eq!(response.type_, TRUST_PING_RESPONSE_2_0); assert_eq!(response.from.unwrap(), global::_mediator_did(&state)); @@ -109,9 +89,9 @@ mod tests { let response = handle_trust_ping(Arc::clone(&state), request) .await - .unwrap_err(); + .unwrap(); - assert_error(response, StatusCode::OK, None).await; + assert_eq!(response, None); } #[tokio::test] @@ -130,11 +110,6 @@ mod tests { .await .unwrap_err(); - assert_error( - response, - StatusCode::BAD_REQUEST, - Some(TrustPingError::MissingSenderDID), - ) - .await; + assert_eq!(response, TrustPingError::MissingSenderDID); } } diff --git a/crates/web-plugins/didcomm-messaging/protocols/trust-ping/src/lib.rs b/crates/web-plugins/didcomm-messaging/protocols/trust-ping/src/lib.rs index ca8b21f0..3cdf318e 100644 --- a/crates/web-plugins/didcomm-messaging/protocols/trust-ping/src/lib.rs +++ b/crates/web-plugins/didcomm-messaging/protocols/trust-ping/src/lib.rs @@ -2,3 +2,6 @@ mod error; mod model; pub mod handler; + +// Re-exports +pub use error::TrustPingError; diff --git a/crates/web-plugins/didcomm-messaging/shared/src/errors.rs b/crates/web-plugins/didcomm-messaging/shared/src/errors.rs index 33a0cf97..bded5211 100644 --- a/crates/web-plugins/didcomm-messaging/shared/src/errors.rs +++ b/crates/web-plugins/didcomm-messaging/shared/src/errors.rs @@ -1,60 +1,7 @@ -use axum::Json; -use didcomm::error::ErrorKind as DidcommErrorKind; -use serde_json::{json, Value}; use thiserror::Error; -/// Represents errors that can occur during mediation. -#[derive(Debug, Error)] -pub enum MediationError { - #[error("message must not be anoncrypt'd")] - AnonymousPacker, - #[error("anti spam check failure")] - AntiSpamCheckFailure, - #[error("duplicate command")] - DuplicateCommand, - #[error("generic: {0}")] - Generic(String), - #[error("invalid message type")] - InvalidMessageType, - #[error("assumed didcomm-encrypted message is malformed")] - MalformedDidcommEncrypted, - #[error("could not unpack message")] - MessageUnpackingFailure, - #[error("could not pack message: {0}")] - MessagePackingFailure(DidcommErrorKind), +#[derive(Debug, Error, PartialEq, Eq)] +pub enum SharedError { #[error("message must be decorated with return route all extension")] NoReturnRouteAllDecoration, - #[error("unsupported content-type, only accept application/didcomm-encrypted+json")] - NotDidcommEncryptedPayload, - #[error("uncoordinated sender")] - UncoordinatedSender, - #[error("could not parse into expected message format")] - UnexpectedMessageFormat, - #[error("unparseable payload")] - UnparseablePayload, - #[error("unsupported did method")] - UnsupportedDidMethod, - #[error("unsupported operation")] - UnsupportedOperation, - #[error("Could not store Message")] - PersisenceError, - #[error("Could not deserialize Message")] - DeserializationError, - #[error("Repository not set")] - RepostitoryError, -} - -impl MediationError { - /// Converts the error to an axum JSON representation. - pub fn json(&self) -> Json { - Json(json!({ - "error": self.to_string() - })) - } -} - -impl From for Json { - fn from(error: MediationError) -> Self { - error.json() - } } diff --git a/crates/web-plugins/didcomm-messaging/shared/src/midlw.rs b/crates/web-plugins/didcomm-messaging/shared/src/midlw.rs index a9d98e13..08bd0211 100644 --- a/crates/web-plugins/didcomm-messaging/shared/src/midlw.rs +++ b/crates/web-plugins/didcomm-messaging/shared/src/midlw.rs @@ -1,59 +1,27 @@ -use crate::errors::MediationError; -use axum::{ - http::StatusCode, - response::{IntoResponse, Response}, -}; +use crate::errors::SharedError; use didcomm::Message; use serde_json::Value; /// Validate explicit decoration on message to receive response on same route. -pub fn ensure_transport_return_route_is_decorated_all(message: &Message) -> Result<(), Response> { +pub fn ensure_transport_return_route_is_decorated_all( + message: &Message, +) -> Result<(), SharedError> { if message .extra_headers .get("return_route") .and_then(Value::as_str) != Some("all") { - let response = ( - StatusCode::BAD_REQUEST, - MediationError::NoReturnRouteAllDecoration.json(), - ); - - return Err(response.into_response()); + return Err(SharedError::NoReturnRouteAllDecoration); } Ok(()) } -#[cfg(any(test, feature = "test-utils"))] -pub mod tests { - use super::*; - use serde::Serialize; - use serde_json::Value; - - pub async fn assert_error( - response: Response, - status: StatusCode, - error: &T, - ) { - assert_eq!(response.status(), status); - - let body = hyper::body::to_bytes(response.into_body()).await.unwrap(); - let body: Value = serde_json::from_slice(&body).unwrap(); - - assert_eq!( - json_canon::to_string(&body).unwrap(), - json_canon::to_string(error).unwrap() - ); - } -} - #[cfg(test)] mod midlw_test { use super::*; - use crate::{ - constants::MEDIATE_REQUEST_2_0, midlw::tests::assert_error, utils::tests_utils::tests, - }; + use crate::{constants::MEDIATE_REQUEST_2_0, utils::tests_utils::tests}; use serde_json::{json, Value}; #[tokio::test] @@ -66,8 +34,6 @@ mod midlw_test { "urn:uuid:8f8208ae-6e16-4275-bde8-7b7cb81ffa59".to_owned(), MEDIATE_REQUEST_2_0.to_string(), json!({ - "id": "id_alice_mediation_request", - "type": MEDIATE_REQUEST_2_0, "did": "did:key:alice_identity_pub@alice_mediator", "services": ["inbox", "outbox"] }), @@ -89,22 +55,18 @@ mod midlw_test { let msg = unfinalized_msg!().finalize(); - assert_error( + assert_eq!( ensure_transport_return_route_is_decorated_all(&msg).unwrap_err(), - StatusCode::BAD_REQUEST, - &MediationError::NoReturnRouteAllDecoration.json().0, - ) - .await; + SharedError::NoReturnRouteAllDecoration + ); let msg = unfinalized_msg!() .header("return_route".into(), Value::String("none".into())) .finalize(); - assert_error( + assert_eq!( ensure_transport_return_route_is_decorated_all(&msg).unwrap_err(), - StatusCode::BAD_REQUEST, - &MediationError::NoReturnRouteAllDecoration.json().0, - ) - .await; + SharedError::NoReturnRouteAllDecoration + ); } } diff --git a/crates/web-plugins/didcomm-messaging/src/did_rotation/did_rotation.rs b/crates/web-plugins/didcomm-messaging/src/did_rotation/did_rotation.rs index 04331aa2..9ccd1ff6 100644 --- a/crates/web-plugins/didcomm-messaging/src/did_rotation/did_rotation.rs +++ b/crates/web-plugins/didcomm-messaging/src/did_rotation/did_rotation.rs @@ -377,8 +377,6 @@ mod test { .await .unwrap(); - println!("{:?}", msg); - // Assert response's metadata assert_eq!(response.status(), StatusCode::ACCEPTED); assert_eq!( diff --git a/crates/web-plugins/didcomm-messaging/src/error.rs b/crates/web-plugins/didcomm-messaging/src/error.rs new file mode 100644 index 00000000..a4eeec02 --- /dev/null +++ b/crates/web-plugins/didcomm-messaging/src/error.rs @@ -0,0 +1,36 @@ +use axum::Json; +use didcomm::error::ErrorKind as DidcommErrorKind; +use serde_json::{json, Value}; +use thiserror::Error; + +/// Represents errors that can occur during mediation. +#[derive(Debug, Error)] +pub(crate) enum Error { + #[error("message must not be anoncrypt'd")] + AnonymousPacker, + #[error("assumed didcomm-encrypted message is malformed")] + MalformedDidcommEncrypted, + #[error("could not unpack message")] + MessageUnpackingFailure, + #[error("could not pack message: {0}")] + MessagePackingFailure(DidcommErrorKind), + #[error("unsupported content-type, only accept application/didcomm-encrypted+json")] + NotDidcommEncryptedPayload, + #[error("unparseable payload")] + UnparseablePayload, +} + +impl Error { + /// Converts the error to an axum JSON representation. + pub fn json(&self) -> Json { + Json(json!({ + "error": self.to_string() + })) + } +} + +impl From for Json { + fn from(error: Error) -> Self { + error.json() + } +} diff --git a/crates/web-plugins/didcomm-messaging/src/lib.rs b/crates/web-plugins/didcomm-messaging/src/lib.rs index c077952b..2c16146f 100644 --- a/crates/web-plugins/didcomm-messaging/src/lib.rs +++ b/crates/web-plugins/didcomm-messaging/src/lib.rs @@ -1,4 +1,5 @@ mod did_rotation; +mod error; mod manager; mod midlw; mod protocols; diff --git a/crates/web-plugins/didcomm-messaging/src/midlw.rs b/crates/web-plugins/didcomm-messaging/src/midlw.rs index ee36d19c..055e0395 100644 --- a/crates/web-plugins/didcomm-messaging/src/midlw.rs +++ b/crates/web-plugins/didcomm-messaging/src/midlw.rs @@ -10,10 +10,9 @@ use serde_json::Value; use std::sync::Arc; // use super::{error::MediationError, AppState}; -use crate::did_rotation::did_rotation::did_rotation; +use crate::{did_rotation::did_rotation::did_rotation, error::Error}; use shared::{ constants::{DIDCOMM_ENCRYPTED_MIME_TYPE, DIDCOMM_ENCRYPTED_SHORT_MIME_TYPE}, - errors::MediationError, state::{AppState, AppStateRepository}, utils::resolvers::{LocalDIDResolver, LocalSecretsResolver}, }; @@ -41,7 +40,7 @@ pub async fn unpack_didcomm_message( Err(_) => { let response = ( StatusCode::BAD_REQUEST, - MediationError::UnparseablePayload.json(), + Error::UnparseablePayload.json(), ); return response.into_response(); @@ -92,7 +91,7 @@ fn content_type_is_didcomm_encrypted(content_type: Option<&str>) -> Result<(), R { let response = ( StatusCode::BAD_REQUEST, - MediationError::NotDidcommEncryptedPayload.json(), + Error::NotDidcommEncryptedPayload.json(), ); return Err(response.into_response()); @@ -118,7 +117,7 @@ async fn unpack_payload( let (plain_message, metadata) = res.map_err(|_| { let response = ( StatusCode::BAD_REQUEST, - MediationError::MessageUnpackingFailure.json(), + Error::MessageUnpackingFailure.json(), ); response.into_response() @@ -127,7 +126,7 @@ async fn unpack_payload( if !metadata.encrypted { let response = ( StatusCode::BAD_REQUEST, - MediationError::MalformedDidcommEncrypted.json(), + Error::MalformedDidcommEncrypted.json(), ); return Err(response.into_response()); @@ -136,7 +135,7 @@ async fn unpack_payload( if plain_message.from.is_none() || !metadata.authenticated || metadata.anonymous_sender { let response = ( StatusCode::BAD_REQUEST, - MediationError::AnonymousPacker.json(), + Error::AnonymousPacker.json(), ); return Err(response.into_response()); @@ -157,7 +156,7 @@ pub async fn pack_response_message( if from.is_none() || to.is_none() { let response = ( StatusCode::INTERNAL_SERVER_ERROR, - MediationError::MessagePackingFailure(DidcommErrorKind::Malformed).json(), + Error::MessagePackingFailure(DidcommErrorKind::Malformed).json(), ); return Err(response.into_response()); @@ -176,7 +175,7 @@ pub async fn pack_response_message( .map_err(|err| { let response = ( StatusCode::INTERNAL_SERVER_ERROR, - MediationError::MessagePackingFailure(err.kind()).json(), + Error::MessagePackingFailure(err.kind()).json(), ); response.into_response() @@ -240,7 +239,7 @@ mod tests { .await .unwrap_err(), StatusCode::INTERNAL_SERVER_ERROR, - MediationError::MessagePackingFailure(DidcommErrorKind::Malformed), + Error::MessagePackingFailure(DidcommErrorKind::Malformed), ) .await; } @@ -266,7 +265,7 @@ mod tests { .await .unwrap_err(), StatusCode::INTERNAL_SERVER_ERROR, - MediationError::MessagePackingFailure(DidcommErrorKind::Unsupported), + Error::MessagePackingFailure(DidcommErrorKind::Unsupported), ) .await; } @@ -275,7 +274,7 @@ mod tests { // Helpers ------------------------------------------------------------------------------------- //---------------------------------------------------------------------------------------------- - async fn _assert_midlw_err(err: Response, status: StatusCode, mediation_error: MediationError) { + async fn _assert_midlw_err(err: Response, status: StatusCode, mediation_error: Error) { assert_eq!(err.status(), status); let body = hyper::body::to_bytes(err.into_body()).await.unwrap(); diff --git a/crates/web-plugins/didcomm-messaging/src/web/dispatcher.rs b/crates/web-plugins/didcomm-messaging/src/web/dispatcher.rs index 47c92848..7d89540d 100644 --- a/crates/web-plugins/didcomm-messaging/src/web/dispatcher.rs +++ b/crates/web-plugins/didcomm-messaging/src/web/dispatcher.rs @@ -4,16 +4,14 @@ use axum::{ Extension, Json, }; use didcomm::Message; -use forward::web::handler::mediator_forward_process; use hyper::{header::CONTENT_TYPE, StatusCode}; use mediator_coordination::web; use shared::{ constants::{ DELIVERY_REQUEST_3_0, DIDCOMM_ENCRYPTED_MIME_TYPE, KEYLIST_QUERY_2_0, KEYLIST_UPDATE_2_0, LIVE_MODE_CHANGE_3_0, MEDIATE_FORWARD_2_0, MEDIATE_REQUEST_2_0, MESSAGE_RECEIVED_3_0, - STATUS_REQUEST_3_0, + STATUS_REQUEST_3_0, TRUST_PING_2_0, }, - errors::MediationError, state::AppState, }; use std::sync::Arc; @@ -23,67 +21,85 @@ pub(crate) async fn process_didcomm_message( State(state): State>, Extension(message): Extension, ) -> Response { - if message.type_ == MEDIATE_FORWARD_2_0 { - return match mediator_forward_process(&state, message).await { - Ok(_message) => StatusCode::ACCEPTED.into_response(), - Err(response) => response, - }; - } - let response = match message.type_.as_str() { - KEYLIST_UPDATE_2_0 => { - web::handler::stateful::process_plain_keylist_update_message( - Arc::clone(&state), - message, - ) - .await - } - KEYLIST_QUERY_2_0 => { - web::handler::stateful::process_plain_keylist_query_message(Arc::clone(&state), message) + let response: Result, Response> = match message.type_.as_str() { + MEDIATE_FORWARD_2_0 => { + forward::web::handler::mediator_forward_process(state.clone(), message) .await + .map_err(|e| e.into_response()) } MEDIATE_REQUEST_2_0 => { - web::handler::stateful::process_mediate_request(&state, &message).await + web::handler::stateful::process_mediate_request(state.clone(), &message) + .await + .map_err(|e| e.into_response()) } - STATUS_REQUEST_3_0 => pickup::handler::handle_status_request(state.clone(), message).await, - DELIVERY_REQUEST_3_0 => { - pickup::handler::handle_delivery_request(state.clone(), message).await + + KEYLIST_UPDATE_2_0 => web::handler::stateful::process_plain_keylist_update_message( + Arc::clone(&state), + message, + ) + .await + .map_err(|e| e.into_response()), + + KEYLIST_QUERY_2_0 => { + web::handler::stateful::process_plain_keylist_query_message(state.clone(), message) + .await + .map_err(|e| e.into_response()) } + + STATUS_REQUEST_3_0 => pickup::handler::handle_status_request(state.clone(), message) + .await + .map_err(|e| e.into_response()), + + DELIVERY_REQUEST_3_0 => pickup::handler::handle_delivery_request(state.clone(), message) + .await + .map_err(|e| e.into_response()), + MESSAGE_RECEIVED_3_0 => { - pickup::handler::handle_message_acknowledgement(state.clone(), message).await + pickup::handler::handle_message_acknowledgement(state.clone(), message) + .await + .map_err(|e| e.into_response()) } + LIVE_MODE_CHANGE_3_0 => { - pickup::handler::handle_live_delivery_change(state.clone(), message).await - } - _ => { - let response = ( - StatusCode::BAD_REQUEST, - MediationError::UnsupportedOperation.json(), - ); - return response.into_response(); + pickup::handler::handle_live_delivery_change(state.clone(), message) + .await + .map_err(|e| e.into_response()) } + + TRUST_PING_2_0 => trust_ping::handler::handle_trust_ping(state.clone(), message) + .await + .map_err(|e| e.into_response()), + + _ => return (StatusCode::BAD_REQUEST, "Unsupported operation".to_string()).into_response(), }; process_response(state, response).await } -async fn process_response(state: Arc, response: Result) -> Response { +async fn process_response( + state: Arc, + response: Result, Response>, +) -> Response { match response { - Ok(message) => crate::midlw::pack_response_message( - &message, - &state.did_resolver, - &state.secrets_resolver, - ) - .await - .map(|packed| { - ( - StatusCode::ACCEPTED, - [(CONTENT_TYPE, DIDCOMM_ENCRYPTED_MIME_TYPE)], - Json(packed), + Ok(message) => match message { + Some(message) => crate::midlw::pack_response_message( + &message, + &state.did_resolver, + &state.secrets_resolver, ) - .into_response() - }) - .unwrap_or_else(|err| err.into_response()), + .await + .map(|packed| { + ( + StatusCode::ACCEPTED, + [(CONTENT_TYPE, DIDCOMM_ENCRYPTED_MIME_TYPE)], + Json(packed), + ) + .into_response() + }) + .unwrap_or_else(|err| err.into_response()), + None => StatusCode::ACCEPTED.into_response(), + }, Err(response) => response, } } diff --git a/docs/edge-device-doc.md b/docs/edge-device-doc.md new file mode 100644 index 00000000..12ae320c --- /dev/null +++ b/docs/edge-device-doc.md @@ -0,0 +1,71 @@ +# DIDComm Messaging for Edge Device Registration and Message Exchange +## Introduction +With the growth of IoT and connected devices, secure, private communication between devices like smart sensors, appliances, and any device has become essential. Decentralized Identifier Communication (DIDComm) provides a protocol that ensures private messaging, enabling devices to communicate without intermediaries directly accessing data. This makes DIDComm ideal for IoT networks where privacy, security, and decentralization are paramount. + + +![sample cloud services](work-flow.png) + +### Benefits of DIDComm Messaging for Edge Devices +* **Enhanced Security:** Encryption protects message content, even when routed through intermediaries. +* **Privacy Preservation:** Only routing information is visible to intermediaries, keeping message content confidential. +* **Decentralization:** Devices control the communication flow without reliance on centralized systems. +* **Reliable Message Delivery:** Coordinated through a Mediator, communication remains effective even in complex networks. +This guide outlines the DIDComm messaging process, including participant roles, secure key exchange, and message flow, with an emphasis on privacy and security. + +### Key Roles in DIDComm Messaging + * **Mediator:** +The Mediator facilitates message routing between devices, handling tasks such as forwarding, error management, and key updates. This support allows devices to connect securely, even in networks with limited reachability. + +### Edge Device 1 +Edge Device 1 initiates communication with Edge Device 2. Before beginning the message exchange, it connects with the Mediator to obtain essential routing details, establishing a secure, private channel. + +### Edge Device 2 +Edge Device 2 is the intended recipient. However, it can also initiate communication, allowing for dynamic role interchangeability between devices. + +## DIDComm Messaging Phases +The DIDComm process for edge device communication consists of three main phases: + +* **Phase 1: Out-of-Band (OOB) Setup and Establishing Mediation** +Before DIDComm communication begins, Edge Device 1 and Edge Device 2 perform an Out-of-Band (OOB) setup to establish a secure channel. They exchange essential information, such as: + + * **Decentralized Identifiers (DIDs)** to represent each device. + * **Routing Information** such as the Mediator’s routing DID. + * **Secure Key Exchange:** During this setup, both devices securely exchange cryptographic keys, which will be used to encrypt all subsequent communication. This ensures that the initial link between the devices is both private and verifiable. + The OOB setup establishes a trusted foundation, enabling Edge Device 1 and Edge Device 2 to communicate securely without the risk of exposing sensitive information. + + * **Mediation Request:** + Edge Device 1 sends a mediation request to the Mediator, signaling its intent to use the Mediator’s services for secure message routing. + + * **Mediation Grant:** + The Mediator grants the request, providing Edge Device 1 with a routing identifier (DID) and creating a private connection for secure messaging with Edge Device 2. + +* **Phase 2: Message Exchange Preparation** +With the mediation relationship in place, Edge Device 1 prepares to send a secure message to Edge Device 2 by ensuring that cryptographic keys are up-to-date and properly aligned with the Mediator’s setup. + + * **Key Management:** Edge Device 1 reviews its encryption keys, updating them if necessary due to device identity changes or new recipient requirements. It then sends a keylist-update message to the Mediator, specifying any additions, removals, or changes to its encryption keys. + + * **Secure Key Exchange:** Edge Device 1 and the Mediator exchange and update any necessary public keys. This maintains an encrypted path for all messages, allowing the Mediator to route messages securely, without access to their content. + + With secure keys in place, Edge Device 1 can now initiate or respond to messages, confident in the privacy and security of the communication pathway. + +* **Phase 3: Message Pickup and Delivery** +With routing and encryption set, Edge Device 1 is ready to exchange messages with Edge Device 2 via the Mediator. This phase involves requests, deliveries, and acknowledgments to ensure reliable message exchange. + + * **Message Pickup:** Edge Device 1 periodically checks with the Mediator for any incoming messages from Edge Device 2. This provides asynchronous communication, allowing messages to be retrieved as needed. + + * **Message Creation and Delivery:** Edge Device 1 encrypts a message for Edge Device 2, using DIDComm’s layered encryption, or “onion encryption,” for added security. It includes Edge Device 2’s routing DID (provided by the Mediator) as part of the routing information, specifying how the Mediator should forward the message. + + * **Message Forwarding by the Mediator:** Upon receiving the message, the Mediator unpacks only the outer “onion” layer to reveal routing details. This allows the Mediator to forward the message to Edge Device 2 without accessing its content, preserving privacy. + + * **Message Pickup by Edge Device 2:** The Mediator routes Edge Device 1’s message to Edge Device 2, which decrypts it using its private key. This ensures secure and confidential message retrieval. + + * **Acknowledgment of Receipt:** After decrypting and reading Edge Device 1’s message, Edge Device 2 sends an acknowledgment back through the Mediator. This confirmation helps Edge Device 1 verify that its message has successfully reached the intended recipient, adding reliability and traceability to the exchange. + +### Security, Privacy, and Decentralization in DIDComm +DIDComm’s decentralized (a system without a central authority, where decision-making and control are distributed among multiple nodes, using peer-to-peer communication and decentralized identifier management) architecture ensures that communication remains private and secure, without relying on centralized intermediaries + +* **Role Interchangeability:** Edge Device 1 and Edge Device 2 can dynamically shift between sender and recipient roles based on communication needs, enabling either device to initiate or respond to messages as situations evolve. +* **Privacy Preservation:** The Mediator only unpacks routing information, keeping the inner message content confidential. Each encryption layer reinforces privacy, allowing the Mediator to route messages securely. + +### Conclusion +DIDComm messaging provides a robust framework for private, secure communication between devices. By coordinating through a Mediator, devices like Edge Device 1 and Edge Device 2 can reliably connect and exchange messages, even in complex network environments. diff --git a/docs/work-flow.png b/docs/work-flow.png new file mode 100644 index 00000000..881fd4c0 Binary files /dev/null and b/docs/work-flow.png differ