Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[FIX] DIDExchange handlers should do more signature checking #1226 #1232

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
edf4718
move existing into v1_0 section
gmulhearn-anonyome Jun 18, 2024
ab67558
duplicate types
gmulhearn-anonyome Jun 18, 2024
0354cb8
static generic types for messages created and piped thru all layers
gmulhearn-anonyome Jun 18, 2024
807e425
simplify generics
gmulhearn-anonyome Jun 19, 2024
e8b4e05
change approach to use runtime versioning rather than generics
gmulhearn-anonyome Jun 19, 2024
a020458
v1_1 branch processing, and some clippy
gmulhearn-anonyome Jun 19, 2024
f870743
remove old todos
gmulhearn-anonyome Jun 19, 2024
b1cbe84
fixes for aath with self for 4/7 performance on RFC0793 & 4/7 on 0023
gmulhearn-anonyome Jun 19, 2024
b913674
smalls patches from acapy testing
gmulhearn-anonyome Jun 19, 2024
5c1f6da
fix up mimetype handling as a result of testing acapy (text/string)
gmulhearn-anonyome Jun 19, 2024
494f8c8
handle multikey (acapy uses this)
gmulhearn-anonyome Jun 19, 2024
6ec33a4
make invite handshake 1.1
gmulhearn-anonyome Jun 19, 2024
a5ae36f
include invitation id
gmulhearn-anonyome Jun 19, 2024
83a2510
pthid in response (for acapy)
gmulhearn-anonyome Jun 19, 2024
98cfd54
Merge branch 'main' into gm/1228-did-exch-1_1
gmulhearn-anonyome Jun 19, 2024
5a5e327
merge fix and add hack for local aath testing
gmulhearn-anonyome Jun 19, 2024
c173cda
fixes for didpeer2
gmulhearn-anonyome Jun 20, 2024
55a7991
improve VM handling to understand more DIDDoc styles (acapy AATH test…
gmulhearn-anonyome Jun 20, 2024
3121644
fmt
gmulhearn-anonyome Jun 21, 2024
ba3570a
clean switcher
gmulhearn-anonyome Jun 21, 2024
9866a2a
label pass, and some fixes
gmulhearn-anonyome Jun 21, 2024
7ad8670
test fixes
gmulhearn-anonyome Jun 21, 2024
a03f6ae
response sign verification mostly impled
gmulhearn-anonyome Jun 24, 2024
ed46a35
fix did rotate content
gmulhearn-anonyome Jun 24, 2024
f54aea5
Merge branch 'gm/1228-did-exch-1_1' into gm/1226-didexchange-signatur…
gmulhearn-anonyome Jun 24, 2024
40f1f28
negative test
gmulhearn-anonyome Jun 24, 2024
732f260
small patch re acapy testing
gmulhearn-anonyome Jun 24, 2024
5f5133d
pass in handshake ver
gmulhearn-anonyome Jun 24, 2024
e4c2939
Merge branch 'gm/1228-did-exch-1_1' into gm/1226-didexchange-signatur…
gmulhearn-anonyome Jun 24, 2024
ec59ed9
clippy and rebase
gmulhearn-anonyome Jun 24, 2024
e6924c5
unnecessary mockall dep
gmulhearn-anonyome Jun 25, 2024
6a054c6
any-wrapper approach
gmulhearn-anonyome Jun 26, 2024
8265ec1
lint
gmulhearn-anonyome Jun 26, 2024
9f7d789
Merge branch 'gm/1228-did-exch-1_1' into gm/1226-didexchange-signatur…
gmulhearn-anonyome Jun 26, 2024
8838804
borrow registries instead
gmulhearn-anonyome Jun 26, 2024
83e89dc
Merge branch 'main' into gm/1226-didexchange-signature-checks
gmulhearn-anonyome Jun 26, 2024
e181f4f
jws testing
gmulhearn-anonyome Jul 4, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

16 changes: 12 additions & 4 deletions aries/agents/aries-vcx-agent/src/handlers/did_exchange.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -73,7 +73,7 @@ impl<T: BaseWallet> DidcommHandlerDidExchange<T> {

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,
Expand Down Expand Up @@ -170,7 +170,7 @@ impl<T: BaseWallet> DidcommHandlerDidExchange<T> {

let (responder, response) = GenericDidExchange::handle_request(
self.wallet.as_ref(),
self.resolver_registry.clone(),
&self.resolver_registry,
request,
&our_peer_did,
invitation_key,
Expand Down Expand Up @@ -224,8 +224,16 @@ impl<T: BaseWallet> DidcommHandlerDidExchange<T> {

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();
Expand Down
28 changes: 25 additions & 3 deletions aries/aries_vcx/src/protocols/did_exchange/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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)?;
JamesKEbert marked this conversation as resolved.
Show resolved Hide resolved
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<Key, AriesVcxError> {
// 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()?)
}
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ impl GenericDidExchange {
}

pub async fn construct_request(
resolver_registry: Arc<ResolverRegistry>,
resolver_registry: &Arc<ResolverRegistry>,
invitation_id: Option<String>,
their_did: &Did,
our_peer_did: &PeerDid<Numalgo4>,
Expand All @@ -115,7 +115,7 @@ impl GenericDidExchange {

pub async fn handle_request(
wallet: &impl BaseWallet,
resolver_registry: Arc<ResolverRegistry>,
resolver_registry: &Arc<ResolverRegistry>,
request: AnyRequest,
our_peer_did: &PeerDid<Numalgo4>,
invitation_key: Option<Key>,
Expand All @@ -137,14 +137,16 @@ impl GenericDidExchange {

pub async fn handle_response(
self,
wallet: &impl BaseWallet,
invitation_key: &Key,
response: AnyResponse,
resolver_registry: Arc<ResolverRegistry>,
resolver_registry: &Arc<ResolverRegistry>,
) -> 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((
Expand Down
223 changes: 185 additions & 38 deletions aries/aries_vcx/src/protocols/did_exchange/state_machine/helpers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Attachment, AriesVcxError> {
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<u8> = 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<bool, AriesVcxError> {
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)
}
JamesKEbert marked this conversation as resolved.
Show resolved Hide resolved

// TODO - ideally this should be resilient to the case where the attachment is a legacy aries DIDDoc
Expand Down Expand Up @@ -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<dyn Error>> {
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<dyn Error>> {
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(())
}
}
Loading
Loading