diff --git a/aries/agents/aries-vcx-agent/src/handlers/did_exchange.rs b/aries/agents/aries-vcx-agent/src/handlers/did_exchange.rs index 958f4bdd4c..b602110917 100644 --- a/aries/agents/aries-vcx-agent/src/handlers/did_exchange.rs +++ b/aries/agents/aries-vcx-agent/src/handlers/did_exchange.rs @@ -15,7 +15,7 @@ use aries_vcx::{ AriesMessage, }, protocols::did_exchange::{ - resolve_enc_key_from_invitation, + resolve_enc_key_from_did_doc, resolve_enc_key_from_invitation, state_machine::{ generic::{GenericDidExchange, ThinState}, helpers::create_peer_did_4, @@ -73,7 +73,7 @@ impl DidcommHandlerDidExchange { let their_did: Did = their_did.parse()?; let (requester, request) = GenericDidExchange::construct_request( - self.resolver_registry.clone(), + &self.resolver_registry, invitation_id, &their_did, &our_peer_did, @@ -170,7 +170,7 @@ impl DidcommHandlerDidExchange { let (responder, response) = GenericDidExchange::handle_request( self.wallet.as_ref(), - self.resolver_registry.clone(), + &self.resolver_registry, request, &our_peer_did, invitation_key, @@ -224,8 +224,16 @@ impl DidcommHandlerDidExchange { let (requester, _) = self.did_exchange.get(&thid)?; + let inviter_ddo = requester.their_did_doc(); + let inviter_key = resolve_enc_key_from_did_doc(inviter_ddo)?; + let (requester, complete) = requester - .handle_response(response, self.resolver_registry.clone()) + .handle_response( + self.wallet.as_ref(), + &inviter_key, + response, + &self.resolver_registry, + ) .await?; let ddo_their = requester.their_did_doc(); let ddo_our = requester.our_did_document(); diff --git a/aries/aries_vcx/src/protocols/did_exchange/mod.rs b/aries/aries_vcx/src/protocols/did_exchange/mod.rs index c5629831af..df8d3f4f03 100644 --- a/aries/aries_vcx/src/protocols/did_exchange/mod.rs +++ b/aries/aries_vcx/src/protocols/did_exchange/mod.rs @@ -11,7 +11,10 @@ use messages::msg_fields::protocols::out_of_band::invitation::{ }; use public_key::Key; -use crate::errors::error::{AriesVcxError, AriesVcxErrorKind, VcxResult}; +use crate::{ + errors::error::{AriesVcxError, AriesVcxErrorKind, VcxResult}, + utils::didcomm_utils::resolve_service_key_to_typed_key, +}; pub mod state_machine; pub mod states; @@ -103,11 +106,30 @@ pub async fn resolve_enc_key_from_invitation( "resolve_enc_key_from_invitation >> Resolved did document {}", output.did_document ); - let key = resolve_first_key_agreement(&output.did_document)?; - Ok(key.public_key()?) + let did_doc = output.did_document; + resolve_enc_key_from_did_doc(&did_doc) } OobService::AriesService(_service) => { unimplemented!("Embedded Aries Service not yet supported by did-exchange") } } } + +/// Attempts to resolve a [Key] in the [DidDocument] that can be used for sending encrypted +/// messages. The approach is: +/// * check the service for a recipient key, +/// * if there is none, use the first key agreement key in the DIDDoc, +/// * else fail +pub fn resolve_enc_key_from_did_doc(did_doc: &DidDocument) -> Result { + // prefer first service key if available + if let Some(service_recipient_key) = did_doc + .service() + .first() + .and_then(|s| s.extra_field_recipient_keys().into_iter().flatten().next()) + { + return resolve_service_key_to_typed_key(&service_recipient_key, did_doc); + } + + let key = resolve_first_key_agreement(did_doc)?; + Ok(key.public_key()?) +} diff --git a/aries/aries_vcx/src/protocols/did_exchange/state_machine/generic/mod.rs b/aries/aries_vcx/src/protocols/did_exchange/state_machine/generic/mod.rs index 08de2843fc..fc5adacb05 100644 --- a/aries/aries_vcx/src/protocols/did_exchange/state_machine/generic/mod.rs +++ b/aries/aries_vcx/src/protocols/did_exchange/state_machine/generic/mod.rs @@ -90,7 +90,7 @@ impl GenericDidExchange { } pub async fn construct_request( - resolver_registry: Arc, + resolver_registry: &Arc, invitation_id: Option, their_did: &Did, our_peer_did: &PeerDid, @@ -115,7 +115,7 @@ impl GenericDidExchange { pub async fn handle_request( wallet: &impl BaseWallet, - resolver_registry: Arc, + resolver_registry: &Arc, request: AnyRequest, our_peer_did: &PeerDid, invitation_key: Option, @@ -137,14 +137,16 @@ impl GenericDidExchange { pub async fn handle_response( self, + wallet: &impl BaseWallet, + invitation_key: &Key, response: AnyResponse, - resolver_registry: Arc, + resolver_registry: &Arc, ) -> Result<(Self, AnyComplete), (Self, AriesVcxError)> { match self { GenericDidExchange::Requester(requester_state) => match requester_state { RequesterState::RequestSent(request_sent_state) => { match request_sent_state - .receive_response(response, resolver_registry) + .receive_response(wallet, invitation_key, response, resolver_registry) .await { Ok(TransitionResult { state, output }) => Ok(( diff --git a/aries/aries_vcx/src/protocols/did_exchange/state_machine/helpers.rs b/aries/aries_vcx/src/protocols/did_exchange/state_machine/helpers.rs index 91dbf5c83f..bafbe57eb3 100644 --- a/aries/aries_vcx/src/protocols/did_exchange/state_machine/helpers.rs +++ b/aries/aries_vcx/src/protocols/did_exchange/state_machine/helpers.rs @@ -180,51 +180,111 @@ pub(crate) fn assemble_did_rotate_attachment(did: &Did) -> Attachment { .build() } -// TODO: Obviously, extract attachment signing -// TODO: JWS verification +// TODO: if this becomes a common method, move to a shared location. +/// Creates a JWS signature of the attachment with the provided verkey. The created JWS +/// signature is appended to the attachment, in alignment with Aries RFC 0017: +/// https://hyperledger.github.io/aries-rfcs/latest/concepts/0017-attachments/#signing-attachments. pub(crate) async fn jws_sign_attach( mut attach: Attachment, verkey: Key, wallet: &impl BaseWallet, ) -> Result { - if let AttachmentType::Base64(attach_base64) = &attach.data.content { - let did_key: DidKey = verkey.clone().try_into()?; - let verkey_b64 = base64::engine::Engine::encode(&URL_SAFE_LENIENT, verkey.key()); - - let protected_header = json!({ - "alg": "EdDSA", - "jwk": { - "kty": "OKP", - "kid": did_key.to_string(), - "crv": "Ed25519", - "x": verkey_b64 - } - }); - let unprotected_header = json!({ - // TODO: Needs to be both protected and unprotected, does it make sense? - "kid": did_key.to_string(), - }); - let b64_protected = - base64::engine::Engine::encode(&URL_SAFE_LENIENT, protected_header.to_string()); - let sign_input = format!("{}.{}", b64_protected, attach_base64).into_bytes(); - let signed = wallet.sign(&verkey, &sign_input).await?; - let signature_base64 = base64::engine::Engine::encode(&URL_SAFE_LENIENT, signed); - - let jws = { - let mut jws = HashMap::new(); - jws.insert("header".to_string(), unprotected_header); - jws.insert("protected".to_string(), Value::String(b64_protected)); - jws.insert("signature".to_string(), Value::String(signature_base64)); - jws - }; - attach.data.jws = Some(jws); - Ok(attach) - } else { - Err(AriesVcxError::from_msg( - AriesVcxErrorKind::InvalidState, + let AttachmentType::Base64(attach_base64) = &attach.data.content else { + return Err(AriesVcxError::from_msg( + AriesVcxErrorKind::InvalidInput, "Cannot sign non-base64-encoded attachment", - )) + )); + }; + if verkey.key_type() != &KeyType::Ed25519 { + return Err(AriesVcxError::from_msg( + AriesVcxErrorKind::InvalidVerkey, + "Only JWS signatures with Ed25519 based keys are currently supported.", + )); } + + let did_key: DidKey = verkey.clone().try_into()?; + let verkey_b64 = base64::engine::Engine::encode(&URL_SAFE_LENIENT, verkey.key()); + + let protected_header = json!({ + "alg": "EdDSA", + "jwk": { + "kty": "OKP", + "kid": did_key.to_string(), + "crv": "Ed25519", + "x": verkey_b64 + } + }); + let unprotected_header = json!({ + "kid": did_key.to_string(), + }); + let b64_protected = + base64::engine::Engine::encode(&URL_SAFE_LENIENT, protected_header.to_string()); + let sign_input = format!("{}.{}", b64_protected, attach_base64).into_bytes(); + let signed: Vec = wallet.sign(&verkey, &sign_input).await?; + let signature_base64 = base64::engine::Engine::encode(&URL_SAFE_LENIENT, signed); + + let jws = { + let mut jws = HashMap::new(); + jws.insert("header".to_string(), unprotected_header); + jws.insert("protected".to_string(), Value::String(b64_protected)); + jws.insert("signature".to_string(), Value::String(signature_base64)); + jws + }; + attach.data.jws = Some(jws); + Ok(attach) +} + +/// Verifies that the given has a JWS signature attached, which is a valid signature given +/// the expected signer key. +// NOTE: Does not handle attachments with multiple signatures. +// NOTE: this is the specific use case where the signer is known by the function caller. Therefore +// we do not need to attempt to decode key within the protected nor unprotected header. +pub(crate) async fn jws_verify_attachment( + attach: &Attachment, + expected_signer: &Key, + wallet: &impl BaseWallet, +) -> Result { + let AttachmentType::Base64(attach_base64) = &attach.data.content else { + return Err(AriesVcxError::from_msg( + AriesVcxErrorKind::InvalidInput, + "Cannot verify JWS of a non-base64-encoded attachment", + )); + }; + // aries attachments do not REQUIRE that the attachment has no padding, + // but JWS does, so remove it; just incase. + let attach_base64 = attach_base64.replace('=', ""); + + let Some(ref jws) = attach.data.jws else { + return Err(AriesVcxError::from_msg( + AriesVcxErrorKind::InvalidInput, + "Attachment has no JWS signature attached. Cannot verify.", + )); + }; + + let (Some(b64_protected), Some(b64_signature)) = ( + jws.get("protected").and_then(|s| s.as_str()), + jws.get("signature").and_then(|s| s.as_str()), + ) else { + return Err(AriesVcxError::from_msg( + AriesVcxErrorKind::InvalidInput, + "Attachment has an invalid JWS with missing fields. Cannot verify.", + )); + }; + + let sign_input = format!("{}.{}", b64_protected, attach_base64).into_bytes(); + let signature = + base64::engine::Engine::decode(&URL_SAFE_LENIENT, b64_signature).map_err(|_| { + AriesVcxError::from_msg( + AriesVcxErrorKind::EncodeError, + "Attachment JWS signature was not correctly base64Url encoded.", + ) + })?; + + let res = wallet + .verify(expected_signer, &sign_input, &signature) + .await?; + + Ok(res) } // TODO - ideally this should be resilient to the case where the attachment is a legacy aries DIDDoc @@ -261,3 +321,90 @@ where state, } } + +#[cfg(test)] +mod tests { + use std::error::Error; + + use aries_vcx_wallet::wallet::base_wallet::did_wallet::DidWallet; + use messages::decorators::attachment::{Attachment, AttachmentData, AttachmentType}; + use public_key::Key; + use test_utils::devsetup::build_setup_profile; + + use crate::{ + protocols::did_exchange::state_machine::helpers::{jws_sign_attach, jws_verify_attachment}, + utils::base64::URL_SAFE_LENIENT, + }; + + // assert self fulfilling + #[tokio::test] + async fn test_jws_sign_and_verify_attachment() -> Result<(), Box> { + let setup = build_setup_profile().await; + let wallet = &setup.wallet; + let signer_did = wallet.create_and_store_my_did(None, None).await?; + let signer = signer_did.verkey(); + + let content_b64 = base64::engine::Engine::encode(&URL_SAFE_LENIENT, "hello world"); + let attach = Attachment::builder() + .data( + AttachmentData::builder() + .content(AttachmentType::Base64(content_b64)) + .build(), + ) + .build(); + + let signed_attach = jws_sign_attach(attach, signer.clone(), wallet).await?; + + // should contain signed JWS + assert_eq!(signed_attach.data.jws.as_ref().unwrap().len(), 3); + + // verify + assert!(jws_verify_attachment(&signed_attach, signer, wallet).await?); + + // verify with wrong key should be false + let wrong_did = wallet.create_and_store_my_did(None, None).await?; + let wrong_signer = wrong_did.verkey(); + assert!(!jws_verify_attachment(&signed_attach, wrong_signer, wallet).await?); + + Ok(()) + } + + // test vector taken from an ACApy 0.12.1 DIDExchange response + #[tokio::test] + async fn test_jws_verify_attachment_with_acapy_test_vector() -> Result<(), Box> { + let setup = build_setup_profile().await; + let wallet = &setup.wallet; + + let json = json!({ + "@id": "18bec73c-c621-4ef2-b3d8-085c59ac9e2b", + "mime-type": "text/string", + "data": { + "jws": { + "signature": "QxC2oLxAYav-fPOvjkn4OpMLng9qOo2fjsy0MoQotDgyVM_PRjYlatsrw6_rADpRpWR_GMpBVlBskuKxpsJIBQ", + "header": { + "kid": "did:key:z6MkpNusbzt7HSBwrBiRpZmbyLiBEsNGs2fotoYhykU8Muaz" + }, + "protected": "eyJhbGciOiAiRWREU0EiLCAiandrIjogeyJrdHkiOiAiT0tQIiwgImNydiI6ICJFZDI1NTE5IiwgIngiOiAiazNlOHZRTHpSZlFhZFhzVDBMUkMxMWhpX09LUlR6VFphd29ocmxhaW1ETSIsICJraWQiOiAiZGlkOmtleTp6Nk1rcE51c2J6dDdIU0J3ckJpUnBabWJ5TGlCRXNOR3MyZm90b1loeWtVOE11YXoifX0" + }, + // NOTE: includes b64 padding, but not recommended + "base64": "ZGlkOnBlZXI6NHpRbVhza2o1Sjc3NXRyWUpkaVVFZVlaUU5mYXZZQUREb25YMzJUOHF4VHJiU05oOno2MmY5VlFROER0N1VWRXJXcmp6YTd4MUVKOG50NWVxOWlaZk1BUGoyYnpyeGJycGY4VXdUTEpXVUJTV2U4dHNoRFl4ZDhlcmVSclRhOHRqVlhKNmNEOTV0Qml5dVdRVll6QzNtZWtUckJ4MzNjeXFCb2g0c3JGamdXZm1lcE5yOEZpRFI5aEoySExxMlM3VGZNWXIxNVN4UG52OExRR2lIV24zODhzVlF3ODRURVJFaTg4OXlUejZzeVVmRXhEaXdxWHZOTk05akt1eHc4NERvbmtVUDRHYkh0Q3B4R2hKYVBKWnlUWmJVaFF2SHBENGc2YzYyWTN5ZGQ0V1BQdXBYQVFISzJScFZod2hQWlVnQWQzN1lrcW1jb3FiWGFZTWFnekZZY3kxTEJ6NkdYekV5NjRrOGQ4WGhlem5vUkpIV3F4RTV1am5LYkpOM0pRR241UzREaEtRaXJTbUZINUJOYUNvRTZqaFlWc3gzWlpEM1ZWZVVxUW9ZMmVHMkNRVVRRak1zY0ozOEdqeDFiaVVlRkhZVVRrejRRVDJFWXpXRlVEbW1URHExVmVoZExtelJDWnNQUjJKR1VpVExUVkNzdUNzZ21jd1FqWHY4WmN6ejRaZUo0ODc4S3hBRm5mam1ibk1EejV5NVJOMnZtRGtkaE42dFFMZjJEWVJuSm1vSjJ5VTNheXczU2NjV0VMVzNpWEN6UFROV1F3WmFEb2d5UFVXZFBobkw0OEVpMjI2cnRBcWoySGQxcTRua1Fwb0ZWQ1B3aXJGUmtub05Zc2NGV1dxN1JEVGVMcmlKcENrUVVFblh4WVBpU1F5S0RxbVpFN0FRVjI=" + } + }); + let mut attach: Attachment = serde_json::from_value(json)?; + let signer = Key::from_fingerprint("z6MkpNusbzt7HSBwrBiRpZmbyLiBEsNGs2fotoYhykU8Muaz")?; + + // should verify with correct signer + assert!(jws_verify_attachment(&attach, &signer, wallet).await?); + + // should not verify with wrong signer + let wrong_signer = + Key::from_fingerprint("z6Mkva1JM9mM3SMuLCtVDAXzAQTwkdtfzHXSYMKtfXK2cPye")?; + assert!(!jws_verify_attachment(&attach, &wrong_signer, wallet).await?); + + // should not verify if wrong signature + attach.data.content = AttachmentType::Base64(String::from("d3JvbmcgZGF0YQ==")); + assert!(!jws_verify_attachment(&attach, &signer, wallet).await?); + + Ok(()) + } +} diff --git a/aries/aries_vcx/src/protocols/did_exchange/state_machine/requester/request_sent/mod.rs b/aries/aries_vcx/src/protocols/did_exchange/state_machine/requester/request_sent/mod.rs index 30c351239d..a171d91087 100644 --- a/aries/aries_vcx/src/protocols/did_exchange/state_machine/requester/request_sent/mod.rs +++ b/aries/aries_vcx/src/protocols/did_exchange/state_machine/requester/request_sent/mod.rs @@ -1,32 +1,39 @@ use std::sync::Arc; +use aries_vcx_wallet::wallet::base_wallet::BaseWallet; +use base64::Engine; +use did_doc::schema::did_doc::DidDocument; use did_parser_nom::Did; use did_peer::peer_did::{numalgos::numalgo4::Numalgo4, PeerDid}; use did_resolver::traits::resolvable::resolution_output::DidResolutionOutput; use did_resolver_registry::ResolverRegistry; use messages::{ - msg_fields::protocols::did_exchange::v1_x::{ - complete::AnyComplete, request::AnyRequest, response::AnyResponse, + decorators::attachment::AttachmentType, + msg_fields::protocols::did_exchange::{ + v1_1::response::Response, + v1_x::{complete::AnyComplete, request::AnyRequest, response::AnyResponse}, }, msg_types::protocols::did_exchange::DidExchangeTypeV1, }; +use public_key::Key; use super::DidExchangeRequester; use crate::{ errors::error::{AriesVcxError, AriesVcxErrorKind}, protocols::did_exchange::{ state_machine::{ - helpers::{attachment_to_diddoc, to_transition_error}, + helpers::{attachment_to_diddoc, jws_verify_attachment, to_transition_error}, requester::helpers::{construct_didexchange_complete, construct_request}, }, states::{completed::Completed, requester::request_sent::RequestSent}, transition::{transition_error::TransitionError, transition_result::TransitionResult}, }, + utils::base64::URL_SAFE_LENIENT, }; impl DidExchangeRequester { pub async fn construct_request( - resolver_registry: Arc, + resolver_registry: &Arc, invitation_id: Option, their_did: &Did, our_peer_did: &PeerDid, @@ -69,8 +76,10 @@ impl DidExchangeRequester { pub async fn receive_response( self, + wallet: &impl BaseWallet, + invitation_key: &Key, response: AnyResponse, - resolver_registry: Arc, + resolver_registry: &Arc, ) -> Result, AnyComplete>, TransitionError> { debug!( @@ -89,28 +98,15 @@ impl DidExchangeRequester { state: self, }); } - // TODO - process differently depending on version - let did_document = if let Some(ddo) = response.content.did_doc { - debug!( - "DidExchangeRequester::receive_response >> the Response message \ - contained attached ddo" - ); - // verify JWS signature on attachment - attachment_to_diddoc(ddo).map_err(to_transition_error(self.clone()))? - } else { - debug!( - "DidExchangeRequester::receive_response >> the Response message \ - contains pairwise DID, resolving to DID Document" - ); - // verify JWS signature on attachment IF version == 1.1 - let did = - &Did::parse(response.content.did).map_err(to_transition_error(self.clone()))?; - let DidResolutionOutput { did_document, .. } = resolver_registry - .resolve(did, &Default::default()) - .await - .map_err(to_transition_error(self.clone()))?; - did_document - }; + + let did_document = extract_and_verify_responder_did_doc( + wallet, + invitation_key, + response, + resolver_registry, + ) + .await + .map_err(to_transition_error(self.clone()))?; let complete_message = construct_didexchange_complete( self.state.invitation_id, @@ -134,3 +130,96 @@ impl DidExchangeRequester { }) } } + +async fn extract_and_verify_responder_did_doc( + wallet: &impl BaseWallet, + invitation_key: &Key, + response: Response, + resolver_registry: &Arc, +) -> Result { + let their_did = response.content.did; + + if let Some(did_doc_attach) = response.content.did_doc { + debug!( + "Verifying signature of DIDDoc attached to response: {did_doc_attach:?} against \ + expected key {invitation_key:?}" + ); + let verified_signature = + jws_verify_attachment(&did_doc_attach, invitation_key, wallet).await?; + if !verified_signature { + return Err(AriesVcxError::from_msg( + AriesVcxErrorKind::InvalidInput, + "DIDExchange response did not have a valid DIDDoc signature from the expected \ + inviter", + )); + } + + let did_doc = attachment_to_diddoc(did_doc_attach)?; + if did_doc.id().to_string() != their_did { + return Err(AriesVcxError::from_msg( + AriesVcxErrorKind::InvalidInput, + "DIDExchange response had a DIDDoc which did not match the response DID", + )); + } + return Ok(did_doc); + } + + if let Some(did_rotate_attach) = response.content.did_rotate { + debug!( + "Verifying signature of DID Rotate attached to response: {did_rotate_attach:?} \ + against expected key {invitation_key:?}" + ); + let verified_signature = + jws_verify_attachment(&did_rotate_attach, invitation_key, wallet).await?; + if !verified_signature { + return Err(AriesVcxError::from_msg( + AriesVcxErrorKind::InvalidInput, + "DIDExchange response did not have a valid DID rotate signature from the expected \ + inviter", + )); + } + + let AttachmentType::Base64(signed_did_b64) = did_rotate_attach.data.content else { + return Err(AriesVcxError::from_msg( + AriesVcxErrorKind::EncodeError, + "DIDExchange response did not have a valid DID rotate attachment", + )); + }; + + let did_bytes = URL_SAFE_LENIENT.decode(signed_did_b64).map_err(|_| { + AriesVcxError::from_msg( + AriesVcxErrorKind::EncodeError, + "DIDExchange response did not have a valid base64 did rotate attachment", + ) + })?; + let signed_did = String::from_utf8(did_bytes).map_err(|_| { + AriesVcxError::from_msg( + AriesVcxErrorKind::EncodeError, + "DIDExchange response did not have a valid UTF8 did rotate attachment", + ) + })?; + + if signed_did != their_did { + return Err(AriesVcxError::from_msg( + AriesVcxErrorKind::InvalidInput, + format!( + "DIDExchange response had a DID rotate which did not match the response DID. \ + Wanted {their_did}, found {signed_did}" + ), + )); + } + + let did = &Did::parse(their_did)?; + let DidResolutionOutput { + did_document: did_doc, + .. + } = resolver_registry.resolve(did, &Default::default()).await?; + return Ok(did_doc); + } + + // default to error + Err(AriesVcxError::from_msg( + AriesVcxErrorKind::InvalidInput, + "DIDExchange response could not be verified. No DIDDoc nor DIDRotate was attached.", + )) +} diff --git a/aries/aries_vcx/src/protocols/did_exchange/state_machine/responder/response_sent/mod.rs b/aries/aries_vcx/src/protocols/did_exchange/state_machine/responder/response_sent/mod.rs index 524eee4f77..27e1ffba44 100644 --- a/aries/aries_vcx/src/protocols/did_exchange/state_machine/responder/response_sent/mod.rs +++ b/aries/aries_vcx/src/protocols/did_exchange/state_machine/responder/response_sent/mod.rs @@ -30,7 +30,7 @@ use crate::{ impl DidExchangeResponder { pub async fn receive_request( wallet: &impl BaseWallet, - resolver_registry: Arc, + resolver_registry: &Arc, request: AnyRequest, our_peer_did: &PeerDid, invitation_key: Option, @@ -44,7 +44,7 @@ impl DidExchangeResponder { let version = request.get_version(); let request = request.into_inner(); - let their_ddo = resolve_ddo_from_request(&resolver_registry, &request).await?; + let their_ddo = resolve_ddo_from_request(resolver_registry, &request).await?; let our_did_document = our_peer_did.resolve_did_doc()?; let unsigned_attachment = match version { diff --git a/aries/aries_vcx/src/protocols/did_exchange/transition/transition_result.rs b/aries/aries_vcx/src/protocols/did_exchange/transition/transition_result.rs index 15b1153054..6e4b79a165 100644 --- a/aries/aries_vcx/src/protocols/did_exchange/transition/transition_result.rs +++ b/aries/aries_vcx/src/protocols/did_exchange/transition/transition_result.rs @@ -1,5 +1,6 @@ // TODO: Somehow enforce using both #[must_use] +#[derive(Debug)] pub struct TransitionResult { pub state: T, pub output: U, diff --git a/aries/aries_vcx/src/utils/didcomm_utils.rs b/aries/aries_vcx/src/utils/didcomm_utils.rs index 2a369f26ff..50ce4efc0f 100644 --- a/aries/aries_vcx/src/utils/didcomm_utils.rs +++ b/aries/aries_vcx/src/utils/didcomm_utils.rs @@ -6,7 +6,7 @@ use public_key::{Key, KeyType}; use crate::errors::error::{AriesVcxError, AriesVcxErrorKind, VcxResult}; -fn resolve_service_key_to_typed_key( +pub(crate) fn resolve_service_key_to_typed_key( key: &ServiceKeyKind, did_document: &DidDocument, ) -> VcxResult { diff --git a/aries/aries_vcx/tests/test_did_exchange.rs b/aries/aries_vcx/tests/test_did_exchange.rs index ad1b2ae096..bb3f34c8a9 100644 --- a/aries/aries_vcx/tests/test_did_exchange.rs +++ b/aries/aries_vcx/tests/test_did_exchange.rs @@ -4,6 +4,7 @@ use std::{error::Error, sync::Arc, thread, time::Duration}; use aries_vcx::{ common::ledger::transactions::write_endpoint_from_service, + errors::error::AriesVcxErrorKind, protocols::did_exchange::{ resolve_enc_key_from_invitation, state_machine::{ @@ -19,7 +20,12 @@ use aries_vcx::{ encryption_envelope::EncryptionEnvelope, }, }; -use aries_vcx_ledger::ledger::indy_vdr_ledger::DefaultIndyLedgerRead; +use aries_vcx_anoncreds::anoncreds::base_anoncreds::BaseAnonCreds; +use aries_vcx_ledger::ledger::{ + base_ledger::{AnoncredsLedgerRead, AnoncredsLedgerWrite, IndyLedgerRead, IndyLedgerWrite}, + indy_vdr_ledger::DefaultIndyLedgerRead, +}; +use aries_vcx_wallet::wallet::base_wallet::BaseWallet; use did_doc::schema::{ did_doc::DidDocument, service::typed::{didcommv1::ServiceDidCommV1, ServiceType}, @@ -37,6 +43,7 @@ use messages::{ use pretty_assertions::assert_eq; use test_utils::devsetup::{dev_build_profile_vdr_ledger, SetupPoolDirectory}; use url::Url; +use utils::test_agent::TestAgent; use crate::utils::test_agent::{ create_test_agent, create_test_agent_endorser_2, create_test_agent_trustee, @@ -51,56 +58,29 @@ fn assert_key_agreement(a: DidDocument, b: DidDocument) { assert_eq!(a_key, b_key); } -#[tokio::test] -#[ignore] -async fn did_exchange_test() -> Result<(), Box> { - let setup = SetupPoolDirectory::init().await; +async fn did_exchange_test( + inviter_did: String, + agent_inviter: TestAgent< + impl IndyLedgerRead + AnoncredsLedgerRead, + impl IndyLedgerWrite + AnoncredsLedgerWrite, + impl BaseAnonCreds, + impl BaseWallet, + >, + agent_invitee: TestAgent< + impl IndyLedgerRead + AnoncredsLedgerRead, + impl IndyLedgerWrite + AnoncredsLedgerWrite, + impl BaseAnonCreds, + impl BaseWallet, + >, + resolver_registry: Arc, +) -> Result<(), Box> { let dummy_url: Url = "http://dummyurl.org".parse().unwrap(); - let agent_trustee = create_test_agent_trustee(setup.genesis_file_path.clone()).await; - // todo: patrik: update create_test_agent_endorser_2 to not consume trustee agent - let agent_inviter = - create_test_agent_endorser_2(&setup.genesis_file_path, agent_trustee).await?; - let create_service = ServiceDidCommV1::new( - Uri::new("#service-0").unwrap(), - dummy_url.clone(), - 0, - vec![], - vec![], - ); - write_endpoint_from_service( - &agent_inviter.wallet, - &agent_inviter.ledger_write, - &agent_inviter.institution_did, - &create_service.try_into()?, - ) - .await?; - thread::sleep(Duration::from_millis(100)); - - let agent_invitee = create_test_agent(setup.genesis_file_path.clone()).await; - - let (ledger_read_2, _) = dev_build_profile_vdr_ledger(setup.genesis_file_path); - let ledger_read_2_arc = Arc::new(ledger_read_2); - - // if we were to use, more generally, the `dev_build_featured_indy_ledger`, we would need to - // here the type based on the feature flag (indy vs proxy vdr client) which is pain - // we need to improve DidSovResolver such that Rust compiler can fully infer the return type - let did_sov_resolver: DidSovResolver, DefaultIndyLedgerRead> = - DidSovResolver::new(ledger_read_2_arc); - - let resolver_registry = Arc::new( - ResolverRegistry::new() - .register_resolver::("peer".into(), PeerDidResolver::new()) - .register_resolver("sov".into(), did_sov_resolver), - ); let invitation = Invitation::builder() .id("test_invite_id".to_owned()) .content( InvitationContent::builder() - .services(vec![OobService::Did(format!( - "did:sov:{}", - agent_inviter.institution_did - ))]) + .services(vec![OobService::Did(inviter_did)]) .build(), ) .build(); @@ -125,7 +105,7 @@ async fn did_exchange_test() -> Result<(), Box> { state: requester, output: request, } = DidExchangeRequester::::construct_request( - resolver_registry.clone(), + &resolver_registry, Some(invitation.id), &did_inviter, &requesters_peer_did, @@ -140,7 +120,7 @@ async fn did_exchange_test() -> Result<(), Box> { ); let (responders_peer_did, _our_verkey) = - create_peer_did_4(&agent_invitee.wallet, dummy_url.clone(), vec![]).await?; + create_peer_did_4(&agent_inviter.wallet, dummy_url.clone(), vec![]).await?; let responders_did_document = responders_peer_did.resolve_did_doc()?; info!("Responder prepares did document: {responders_did_document}"); @@ -152,10 +132,10 @@ async fn did_exchange_test() -> Result<(), Box> { state: responder, } = DidExchangeResponder::::receive_request( &agent_inviter.wallet, - resolver_registry.clone(), + &resolver_registry, request, &responders_peer_did, - Some(invitation_key), + Some(invitation_key.clone()), ) .await .unwrap(); @@ -164,7 +144,12 @@ async fn did_exchange_test() -> Result<(), Box> { state: requester, output: complete, } = requester - .receive_response(response, resolver_registry) + .receive_response( + &agent_invitee.wallet, + &invitation_key, + response, + &resolver_registry, + ) .await .unwrap(); @@ -208,9 +193,167 @@ async fn did_exchange_test() -> Result<(), Box> { let requesters_peer_did = requesters_peer_did.resolve_did_doc()?; let expected_sender_vk = resolve_ed25519_base58_key_agreement(&requesters_peer_did)?; let unpacked = - EncryptionEnvelope::auth_unpack(&agent_invitee.wallet, m.0, &expected_sender_vk).await?; + EncryptionEnvelope::auth_unpack(&agent_inviter.wallet, m.0, &expected_sender_vk).await?; info!("Unpacked message: {:?}", unpacked); Ok(()) } + +#[tokio::test] +#[ignore] +async fn did_exchange_test_sov_inviter() -> Result<(), Box> { + let setup = SetupPoolDirectory::init().await; + let dummy_url: Url = "http://dummyurl.org".parse().unwrap(); + let agent_trustee = create_test_agent_trustee(setup.genesis_file_path.clone()).await; + // todo: patrik: update create_test_agent_endorser_2 to not consume trustee agent + let agent_inviter = + create_test_agent_endorser_2(&setup.genesis_file_path, agent_trustee).await?; + let create_service = ServiceDidCommV1::new( + Uri::new("#service-0").unwrap(), + dummy_url.clone(), + 0, + vec![], + vec![], + ); + write_endpoint_from_service( + &agent_inviter.wallet, + &agent_inviter.ledger_write, + &agent_inviter.institution_did, + &create_service.try_into()?, + ) + .await?; + thread::sleep(Duration::from_millis(100)); + + let agent_invitee = create_test_agent(setup.genesis_file_path.clone()).await; + + let (ledger_read_2, _) = dev_build_profile_vdr_ledger(setup.genesis_file_path); + let ledger_read_2_arc = Arc::new(ledger_read_2); + + // if we were to use, more generally, the `dev_build_featured_indy_ledger`, we would need to + // here the type based on the feature flag (indy vs proxy vdr client) which is pain + // we need to improve DidSovResolver such that Rust compiler can fully infer the return type + let did_sov_resolver: DidSovResolver, DefaultIndyLedgerRead> = + DidSovResolver::new(ledger_read_2_arc); + + let resolver_registry = Arc::new( + ResolverRegistry::new() + .register_resolver::("peer".into(), PeerDidResolver::new()) + .register_resolver("sov".into(), did_sov_resolver), + ); + + did_exchange_test( + format!("did:sov:{}", agent_inviter.institution_did), + agent_inviter, + agent_invitee, + resolver_registry, + ) + .await +} + +#[tokio::test] +#[ignore] +async fn did_exchange_test_peer_to_peer() -> Result<(), Box> { + let setup = SetupPoolDirectory::init().await; + let dummy_url: Url = "http://dummyurl.org".parse().unwrap(); + + let agent_inviter = create_test_agent(setup.genesis_file_path.clone()).await; + let agent_invitee = create_test_agent(setup.genesis_file_path.clone()).await; + + let resolver_registry = Arc::new( + ResolverRegistry::new() + .register_resolver::("peer".into(), PeerDidResolver::new()), + ); + + let (inviter_peer_did, _) = + create_peer_did_4(&agent_inviter.wallet, dummy_url.clone(), vec![]).await?; + + did_exchange_test( + inviter_peer_did.to_string(), + agent_inviter, + agent_invitee, + resolver_registry, + ) + .await +} + +#[tokio::test] +#[ignore] +async fn did_exchange_test_with_invalid_rotation_signature() -> Result<(), Box> { + let setup = SetupPoolDirectory::init().await; + let dummy_url: Url = "http://dummyurl.org".parse().unwrap(); + + let agent_inviter = create_test_agent(setup.genesis_file_path.clone()).await; + let agent_invitee = create_test_agent(setup.genesis_file_path.clone()).await; + + let resolver_registry = Arc::new( + ResolverRegistry::new() + .register_resolver::("peer".into(), PeerDidResolver::new()), + ); + + let (inviter_peer_did, _) = + create_peer_did_4(&agent_inviter.wallet, dummy_url.clone(), vec![]).await?; + + let dummy_url: Url = "http://dummyurl.org".parse().unwrap(); + + let invitation = Invitation::builder() + .id("test_invite_id".to_owned()) + .content( + InvitationContent::builder() + .services(vec![OobService::Did(inviter_peer_did.to_string())]) + .build(), + ) + .build(); + let real_invitation_key = + resolve_enc_key_from_invitation(&invitation, &resolver_registry).await?; + + let (requesters_peer_did, _our_verkey) = + create_peer_did_4(&agent_invitee.wallet, dummy_url.clone(), vec![]).await?; + let did_inviter: Did = invitation_get_first_did_service(&invitation)?; + + let TransitionResult { + state: requester, + output: request, + } = DidExchangeRequester::::construct_request( + &resolver_registry, + Some(invitation.id), + &did_inviter, + &requesters_peer_did, + "some-label".to_owned(), + DidExchangeTypeV1::new_v1_1(), + ) + .await?; + + let (responders_peer_did, incorrect_invitation_key) = + create_peer_did_4(&agent_inviter.wallet, dummy_url.clone(), vec![]).await?; + + // create a response with a DID Rotate signed by the wrong key (not the original invitation key) + let TransitionResult { + output: response, + state: _, + } = DidExchangeResponder::::receive_request( + &agent_inviter.wallet, + &resolver_registry, + request, + &responders_peer_did, + // sign with NOT the invitation key + Some(incorrect_invitation_key), + ) + .await?; + + // receiving the response should fail when verifying the signature + let res = requester + .receive_response( + &agent_invitee.wallet, + &real_invitation_key, + response, + &resolver_registry, + ) + .await; + assert_eq!( + res.unwrap_err().error.kind(), + AriesVcxErrorKind::InvalidInput + ); + + Ok(()) +} diff --git a/did_core/did_methods/did_resolver_sov/Cargo.toml b/did_core/did_methods/did_resolver_sov/Cargo.toml index bd478df5c4..7bf83330b3 100644 --- a/did_core/did_methods/did_resolver_sov/Cargo.toml +++ b/did_core/did_methods/did_resolver_sov/Cargo.toml @@ -7,7 +7,6 @@ edition = "2021" did_resolver = { path = "../../did_resolver" } aries_vcx_ledger = { path = "../../../aries/aries_vcx_ledger" } async-trait = "0.1.68" -mockall = "0.11.4" serde_json = "1.0.96" serde = { version = "1.0.160", features = ["derive"] } chrono = { version = "0.4.24", default-features = false } @@ -16,6 +15,7 @@ url = "2.3.1" log = "0.4.16" [dev-dependencies] +mockall = "0.11.4" aries_vcx = { path = "../../../aries/aries_vcx" } tokio = { version = "1.38.0", default-features = false, features = ["macros", "rt"] } uuid = "1.3.1"