diff --git a/Cargo.lock b/Cargo.lock index 13141df045..9973c131cc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -340,9 +340,11 @@ dependencies = [ "lazy_static", "log", "messages", + "mockall", "num-bigint", "rand 0.8.5", "regex", + "reqwest", "serde", "serde_derive", "serde_json", diff --git a/aries_vcx/Cargo.toml b/aries_vcx/Cargo.toml index 566d5a602e..800799572b 100644 --- a/aries_vcx/Cargo.toml +++ b/aries_vcx/Cargo.toml @@ -57,3 +57,5 @@ android_logger = "0.13.3" wallet_migrator = { path = "../wallet_migrator" } async-channel = "1.7.1" tokio = { version = "1.20", features = ["rt", "macros", "rt-multi-thread"] } +mockall = "0.11.4" +reqwest = "0.11.18" # TODO - DELETE ONLY FOR TEMPORARY TEST!! diff --git a/aries_vcx/src/handlers/util.rs b/aries_vcx/src/handlers/util.rs index a31af6808a..546d68980d 100644 --- a/aries_vcx/src/handlers/util.rs +++ b/aries_vcx/src/handlers/util.rs @@ -1,4 +1,6 @@ +use base64::{engine::general_purpose, Engine}; use messages::{ + decorators::attachment::{Attachment, AttachmentType}, msg_fields::protocols::{ connection::{invitation::Invitation, Connection}, cred_issuance::{v1::CredentialIssuanceV1, v2::CredentialIssuanceV2, CredentialIssuance}, @@ -19,6 +21,15 @@ use strum_macros::{AsRefStr, EnumString}; use crate::errors::error::{AriesVcxError, AriesVcxErrorKind, VcxResult}; +macro_rules! get_thread_id_or_message_id { + ($msg:expr) => { + $msg.decorators + .thread + .as_ref() + .map_or($msg.id.clone(), |t| t.thid.clone()) + }; +} + macro_rules! matches_thread_id { ($msg:expr, $id:expr) => { $msg.decorators.thread.thid == $id || $msg.decorators.thread.pthid.as_deref() == Some($id) @@ -71,10 +82,50 @@ macro_rules! make_attach_from_str { } pub(crate) use get_attach_as_string; +pub(crate) use get_thread_id_or_message_id; pub(crate) use make_attach_from_str; pub(crate) use matches_opt_thread_id; pub(crate) use matches_thread_id; +/// Extract/decode the inner data of an [Attachment] as a [Vec], regardless of whether the inner +/// data is encoded as base64 or JSON. +pub fn extract_attachment_data(attachment: &Attachment) -> VcxResult> { + let data = match &attachment.data.content { + AttachmentType::Base64(encoded_attach) => general_purpose::URL_SAFE + .decode(encoded_attach) + .map_err(|_| { + AriesVcxError::from_msg( + AriesVcxErrorKind::EncodeError, + format!("Message attachment is not base64 as expected: {attachment:?}"), + ) + })?, + AttachmentType::Json(json_attach) => serde_json::to_vec(json_attach)?, + _ => { + return Err(AriesVcxError::from_msg( + AriesVcxErrorKind::InvalidMessageFormat, + format!("Message attachment is not base64 or JSON: {attachment:?}"), + )) + } + }; + + Ok(data) +} + +/// Retrieve the first [Attachment] from a list, where the [Attachment] as an `id` matching the +/// supplied id. Returning an error if no attachment is found. +pub fn get_attachment_with_id<'a>( + attachments: &'a Vec, + id: &String, +) -> VcxResult<&'a Attachment> { + attachments + .iter() + .find(|attachment| attachment.id.as_ref() == Some(id)) + .ok_or(AriesVcxError::from_msg( + AriesVcxErrorKind::InvalidMessageFormat, + format!("Message is missing an attachment with the expected ID : {id}."), + )) +} + pub fn verify_thread_id(thread_id: &str, message: &AriesMessage) -> VcxResult<()> { let is_match = match message { AriesMessage::BasicMessage(msg) => matches_opt_thread_id!(msg, thread_id), diff --git a/aries_vcx/src/protocols/issuance/holder/state_machine.rs b/aries_vcx/src/protocols/issuance/holder/state_machine.rs index 68e2c0a5d0..3bba8100b4 100644 --- a/aries_vcx/src/protocols/issuance/holder/state_machine.rs +++ b/aries_vcx/src/protocols/issuance/holder/state_machine.rs @@ -512,7 +512,7 @@ pub fn parse_cred_def_id_from_cred_offer(cred_offer: &str) -> VcxResult Ok(cred_def_id.to_string()) } -fn _parse_rev_reg_id_from_credential(credential: &str) -> VcxResult> { +pub fn _parse_rev_reg_id_from_credential(credential: &str) -> VcxResult> { trace!("Holder::_parse_rev_reg_id_from_credential >>>"); let parsed_credential: serde_json::Value = serde_json::from_str(credential).map_err(|err| { diff --git a/aries_vcx/src/protocols/issuance_v2/formats/holder/hyperledger_indy.rs b/aries_vcx/src/protocols/issuance_v2/formats/holder/hyperledger_indy.rs new file mode 100644 index 0000000000..9cce926fbf --- /dev/null +++ b/aries_vcx/src/protocols/issuance_v2/formats/holder/hyperledger_indy.rs @@ -0,0 +1,240 @@ +use std::marker::PhantomData; + +use aries_vcx_core::{ + anoncreds::base_anoncreds::BaseAnonCreds, ledger::base_ledger::AnoncredsLedgerRead, +}; +use async_trait::async_trait; +use messages::msg_fields::protocols::cred_issuance::v2::{ + issue_credential::{IssueCredentialAttachmentFormatType, IssueCredentialV2}, + offer_credential::{OfferCredentialAttachmentFormatType, OfferCredentialV2}, + propose_credential::ProposeCredentialAttachmentFormatType, + request_credential::RequestCredentialAttachmentFormatType, +}; +use shared_vcx::maybe_known::MaybeKnown; + +use super::HolderCredentialIssuanceFormat; +use crate::{ + errors::error::{AriesVcxError, AriesVcxErrorKind, VcxResult}, + protocols::issuance::holder::state_machine::{ + _parse_rev_reg_id_from_credential, create_anoncreds_credential_request, + parse_cred_def_id_from_cred_offer, + }, +}; + +/// Structure which implements [HolderCredentialIssuanceFormat] functionality for the `hlindy/...` +/// family of issue-credential-v2 attachment formats. +/// +/// This implementation expects and creates attachments of the following types: +/// * [ProposeCredentialAttachmentFormatType::HyperledgerIndyCredentialFilter2_0] +/// * [RequestCredentialAttachmentFormatType::HyperledgerIndyCredentialRequest2_0] +/// * [OfferCredentialAttachmentFormatType::HyperledgerIndyCredentialAbstract2_0] +/// * [IssueCredentialAttachmentFormatType::HyperledgerIndyCredential2_0] +/// +/// This is done in accordance to the Aries RFC 0592 Spec: +/// +/// https://github.com/hyperledger/aries-rfcs/blob/b3a3942ef052039e73cd23d847f42947f8287da2/features/0592-indy-attachments/README.md +pub struct HyperledgerIndyHolderCredentialIssuanceFormat<'a, R, A> +where + R: AnoncredsLedgerRead, + A: BaseAnonCreds, +{ + _data: &'a PhantomData<()>, + _ledger_read: PhantomData, + _anoncreds: PhantomData, +} + +pub struct HyperledgerIndyCreateProposalInput { + pub cred_filter: HyperledgerIndyCredentialFilter, +} + +#[derive(Default, Clone, PartialEq, Debug, Serialize, Deserialize, Builder)] +#[builder(setter(into, strip_option), default)] +pub struct HyperledgerIndyCredentialFilter { + #[serde(skip_serializing_if = "Option::is_none")] + pub schema_issuer_did: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub schema_name: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub schema_version: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub schema_id: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub issuer_did: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub cred_def_id: Option, +} + +// Simplified cred abstract, for purpose of easy viewing for consumer +// https://github.com/hyperledger/aries-rfcs/blob/main/features/0592-indy-attachments/README.md#cred-abstract-format +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct HyperledgerIndyOfferDetails { + pub schema_id: String, + pub cred_def_id: String, +} + +pub struct HyperledgerIndyCreateRequestInput<'a, R, A> +where + R: AnoncredsLedgerRead, + A: BaseAnonCreds, +{ + pub entropy_did: String, + pub ledger: &'a R, + pub anoncreds: &'a A, +} + +#[derive(Clone, Debug)] +pub struct HyperledgerIndyCreatedRequestMetadata { + credential_request_metadata: String, + credential_def_json: String, +} + +pub struct HyperledgerIndyStoreCredentialInput<'a, R, A> +where + R: AnoncredsLedgerRead, + A: BaseAnonCreds, +{ + pub ledger: &'a R, + pub anoncreds: &'a A, +} + +#[derive(Clone, Debug)] +pub struct HyperledgerIndyStoredCredentialMetadata { + pub credential_id: String, +} + +#[async_trait] +impl<'a, R, A> HolderCredentialIssuanceFormat + for HyperledgerIndyHolderCredentialIssuanceFormat<'a, R, A> +where + R: AnoncredsLedgerRead + 'a, + A: BaseAnonCreds + 'a, +{ + type CreateProposalInput = HyperledgerIndyCreateProposalInput; + + type OfferDetails = HyperledgerIndyOfferDetails; + + type CreateRequestInput = HyperledgerIndyCreateRequestInput<'a, R, A>; + type CreatedRequestMetadata = HyperledgerIndyCreatedRequestMetadata; + + type StoreCredentialInput = HyperledgerIndyStoreCredentialInput<'a, R, A>; + type StoredCredentialMetadata = HyperledgerIndyStoredCredentialMetadata; + + fn supports_request_independent_of_offer() -> bool { + false + } + + fn get_proposal_attachment_format() -> MaybeKnown { + MaybeKnown::Known(ProposeCredentialAttachmentFormatType::HyperledgerIndyCredentialFilter2_0) + } + fn get_request_attachment_format() -> MaybeKnown { + MaybeKnown::Known( + RequestCredentialAttachmentFormatType::HyperledgerIndyCredentialRequest2_0, + ) + } + fn get_offer_attachment_format() -> MaybeKnown { + MaybeKnown::Known(OfferCredentialAttachmentFormatType::HyperledgerIndyCredentialAbstract2_0) + } + fn get_credential_attachment_format() -> MaybeKnown { + MaybeKnown::Known(IssueCredentialAttachmentFormatType::HyperledgerIndyCredential2_0) + } + + async fn create_proposal_attachment_content( + data: &HyperledgerIndyCreateProposalInput, + ) -> VcxResult> { + let filter_bytes = serde_json::to_vec(&data.cred_filter)?; + + Ok(filter_bytes) + } + + fn extract_offer_details( + offer_message: &OfferCredentialV2, + ) -> VcxResult { + let attachment = Self::extract_offer_attachment_content(offer_message)?; + + Ok(serde_json::from_slice(&attachment)?) + } + + async fn create_request_attachment_content( + offer_message: &OfferCredentialV2, + data: &Self::CreateRequestInput, + ) -> VcxResult<(Vec, HyperledgerIndyCreatedRequestMetadata)> { + let offer_bytes = Self::extract_offer_attachment_content(&offer_message)?; + let offer_payload = String::from_utf8(offer_bytes).map_err(|_| { + AriesVcxError::from_msg( + AriesVcxErrorKind::EncodeError, + "Expected payload to be a utf8 string", + ) + })?; + + let cred_def_id = parse_cred_def_id_from_cred_offer(&offer_payload)?; + let entropy = &data.entropy_did; + let ledger = data.ledger; + let anoncreds = data.anoncreds; + + let (credential_request, credential_request_metadata, _, credential_def_json) = + create_anoncreds_credential_request( + ledger, + anoncreds, + &cred_def_id, + &entropy, + &offer_payload, + ) + .await?; + + Ok(( + credential_request.into(), + HyperledgerIndyCreatedRequestMetadata { + credential_request_metadata, + credential_def_json, + }, + )) + } + + async fn create_request_attachment_content_independent_of_offer( + _: &Self::CreateRequestInput, + ) -> VcxResult<(Vec, Self::CreatedRequestMetadata)> { + Err(AriesVcxError::from_msg( + AriesVcxErrorKind::ActionNotSupported, + "Anoncreds cannot create request payload independent of an offer", + )) + } + + async fn process_and_store_credential( + issue_credential_message: &IssueCredentialV2, + user_input: &HyperledgerIndyStoreCredentialInput, + request_metadata: &HyperledgerIndyCreatedRequestMetadata, + ) -> VcxResult { + let cred_bytes = Self::extract_credential_attachment_content(&issue_credential_message)?; + let credential_payload = String::from_utf8(cred_bytes).map_err(|_| { + AriesVcxError::from_msg( + AriesVcxErrorKind::EncodeError, + "Expected payload to be a utf8 string", + ) + })?; + + let ledger = user_input.ledger; + let anoncreds = user_input.anoncreds; + + let rev_reg_id = _parse_rev_reg_id_from_credential(&credential_payload)?; + let rev_reg_def_json = if let Some(rev_reg_id) = rev_reg_id { + let json = ledger.get_rev_reg_def_json(&rev_reg_id).await?; + Some(json) + } else { + None + }; + + let cred_id = anoncreds + .prover_store_credential( + None, + &request_metadata.credential_request_metadata, + &credential_payload, + &request_metadata.credential_def_json, + rev_reg_def_json.as_deref(), + ) + .await?; + + Ok(HyperledgerIndyStoredCredentialMetadata { + credential_id: cred_id, + }) + } +} diff --git a/aries_vcx/src/protocols/issuance_v2/formats/holder/ld_proof_vc.rs b/aries_vcx/src/protocols/issuance_v2/formats/holder/ld_proof_vc.rs new file mode 100644 index 0000000000..e9d08d1a40 --- /dev/null +++ b/aries_vcx/src/protocols/issuance_v2/formats/holder/ld_proof_vc.rs @@ -0,0 +1,75 @@ +use async_trait::async_trait; +use messages::msg_fields::protocols::cred_issuance::v2::{ + issue_credential::{IssueCredentialAttachmentFormatType, IssueCredentialV2}, + offer_credential::{OfferCredentialAttachmentFormatType, OfferCredentialV2}, + propose_credential::ProposeCredentialAttachmentFormatType, + request_credential::RequestCredentialAttachmentFormatType, +}; +use shared_vcx::maybe_known::MaybeKnown; + +use super::HolderCredentialIssuanceFormat; +use crate::errors::error::VcxResult; + +// TODO - delete, this is just a mock +pub struct LdProofHolderCredentialIssuanceFormat; + +#[async_trait] +impl HolderCredentialIssuanceFormat for LdProofHolderCredentialIssuanceFormat { + type CreateProposalInput = (); + + type OfferDetails = (); + + type CreateRequestInput = (); + type CreatedRequestMetadata = (); + + type StoreCredentialInput = (); + type StoredCredentialMetadata = (); + + fn supports_request_independent_of_offer() -> bool { + true + } + + fn get_proposal_attachment_format() -> MaybeKnown { + MaybeKnown::Known(ProposeCredentialAttachmentFormatType::AriesLdProofVcDetail1_0) + } + fn get_request_attachment_format() -> MaybeKnown { + MaybeKnown::Known(RequestCredentialAttachmentFormatType::AriesLdProofVcDetail1_0) + } + fn get_offer_attachment_format() -> MaybeKnown { + MaybeKnown::Known(OfferCredentialAttachmentFormatType::AriesLdProofVcDetail1_0) + } + fn get_credential_attachment_format() -> MaybeKnown { + MaybeKnown::Known(IssueCredentialAttachmentFormatType::AriesLdProofVc1_0) + } + + async fn create_proposal_attachment_content( + _data: &Self::CreateProposalInput, + ) -> VcxResult> { + Ok("mock".to_owned().into()) + } + + fn extract_offer_details(_: &OfferCredentialV2) -> VcxResult { + Ok(()) + } + + async fn create_request_attachment_content( + _offer_message: &OfferCredentialV2, + _data: &Self::CreateRequestInput, + ) -> VcxResult<(Vec, Self::CreatedRequestMetadata)> { + Ok(("mock".to_owned().into(), ())) + } + + async fn create_request_attachment_content_independent_of_offer( + _data: &Self::CreateRequestInput, + ) -> VcxResult<(Vec, Self::CreatedRequestMetadata)> { + Ok(("mock".to_owned().into(), ())) + } + + async fn process_and_store_credential( + _issue_credential_message: &IssueCredentialV2, + _user_input: &Self::StoreCredentialInput, + _request_metadata: &Self::CreatedRequestMetadata, + ) -> VcxResult { + Ok(()) + } +} diff --git a/aries_vcx/src/protocols/issuance_v2/formats/holder/mod.rs b/aries_vcx/src/protocols/issuance_v2/formats/holder/mod.rs new file mode 100644 index 0000000000..a497077ec3 --- /dev/null +++ b/aries_vcx/src/protocols/issuance_v2/formats/holder/mod.rs @@ -0,0 +1,182 @@ +use async_trait::async_trait; +use messages::msg_fields::protocols::cred_issuance::v2::{ + issue_credential::{IssueCredentialAttachmentFormatType, IssueCredentialV2}, + offer_credential::{OfferCredentialAttachmentFormatType, OfferCredentialV2}, + propose_credential::ProposeCredentialAttachmentFormatType, + request_credential::RequestCredentialAttachmentFormatType, +}; +use shared_vcx::maybe_known::MaybeKnown; + +use crate::{ + errors::error::{AriesVcxError, AriesVcxErrorKind, VcxResult}, + handlers::util::{extract_attachment_data, get_attachment_with_id}, +}; + +pub mod hyperledger_indy; +pub mod ld_proof_vc; + +/// Trait representing some issue-credential-v2 format family, containing methods required by an +/// holder of this format to create attachments of this format. +#[async_trait] +pub trait HolderCredentialIssuanceFormat { + type CreateProposalInput; + + type OfferDetails; + + type CreateRequestInput; + type CreatedRequestMetadata; + + type StoreCredentialInput; + type StoredCredentialMetadata; + + fn supports_request_independent_of_offer() -> bool; + + /// Retrieve the format type that an implementation uses/expects for credential proposal + /// attachments. + /// + /// See formats here: https://github.com/hyperledger/aries-rfcs/blob/main/features/0453-issue-credential-v2/README.md#propose-attachment-registry + fn get_proposal_attachment_format() -> MaybeKnown; + + /// Retrieve the format type that an implementation uses/expects for credential offer + /// attachments. + /// + /// See formats here: https://github.com/hyperledger/aries-rfcs/blob/main/features/0453-issue-credential-v2/README.md#offer-attachment-registry + fn get_offer_attachment_format() -> MaybeKnown; + + /// Retrieve the format type that an implementation uses/expects for credential request + /// attachments. + /// + /// See formats here: https://github.com/hyperledger/aries-rfcs/blob/main/features/0453-issue-credential-v2/README.md#request-attachment-registry + fn get_request_attachment_format() -> MaybeKnown; + + /// Retrieve the format type that an implementation uses/expects for credential attachments. + /// + /// See formats here: https://github.com/hyperledger/aries-rfcs/blob/main/features/0453-issue-credential-v2/README.md#credentials-attachment-registry + fn get_credential_attachment_format() -> MaybeKnown; + + async fn create_proposal_attachment_content( + data: &Self::CreateProposalInput, + ) -> VcxResult>; + + fn extract_offer_attachment_content(offer_message: &OfferCredentialV2) -> VcxResult> { + let attachment_id = offer_message + .content + .formats + .iter() + .find_map(|format| { + (format.format == Self::get_offer_attachment_format()).then_some(&format.attach_id) + }) + .ok_or(AriesVcxError::from_msg( + AriesVcxErrorKind::InvalidMessageFormat, + "Message does not containing an attachment with the expected format.", + ))?; + + let attachment = + get_attachment_with_id(&offer_message.content.offers_attach, attachment_id)?; + + extract_attachment_data(attachment) + } + + fn extract_offer_details(offer_message: &OfferCredentialV2) -> VcxResult; + + async fn create_request_attachment_content( + offer_message: &OfferCredentialV2, + data: &Self::CreateRequestInput, + ) -> VcxResult<(Vec, Self::CreatedRequestMetadata)>; + + async fn create_request_attachment_content_independent_of_offer( + data: &Self::CreateRequestInput, + ) -> VcxResult<(Vec, Self::CreatedRequestMetadata)>; + + fn extract_credential_attachment_content( + issue_credential_message: &IssueCredentialV2, + ) -> VcxResult> { + let attachment_id = issue_credential_message + .content + .formats + .iter() + .find_map(|format| { + (format.format == Self::get_credential_attachment_format()) + .then_some(&format.attach_id) + }) + .ok_or(AriesVcxError::from_msg( + AriesVcxErrorKind::InvalidMessageFormat, + "Message does not containing an attachment with the expected format.", + ))?; + + let attachment = get_attachment_with_id( + &issue_credential_message.content.credentials_attach, + attachment_id, + )?; + + extract_attachment_data(attachment) + } + + async fn process_and_store_credential( + issue_credential_message: &IssueCredentialV2, + data: &Self::StoreCredentialInput, + request_metadata: &Self::CreatedRequestMetadata, + ) -> VcxResult; +} + +#[cfg(test)] +pub(crate) mod mocks { + use async_trait::async_trait; + use messages::msg_fields::protocols::cred_issuance::v2::{ + issue_credential::{IssueCredentialAttachmentFormatType, IssueCredentialV2}, + offer_credential::{OfferCredentialAttachmentFormatType, OfferCredentialV2}, + propose_credential::ProposeCredentialAttachmentFormatType, + request_credential::RequestCredentialAttachmentFormatType, + }; + use mockall::mock; + use shared_vcx::maybe_known::MaybeKnown; + + use super::HolderCredentialIssuanceFormat; + use crate::errors::error::VcxResult; + + mock! { + pub HolderCredentialIssuanceFormat {} + #[async_trait] + impl HolderCredentialIssuanceFormat for HolderCredentialIssuanceFormat { + type CreateProposalInput = String; + + type OfferDetails = String; + + type CreateRequestInput = String; + type CreatedRequestMetadata = String; + + type StoreCredentialInput = String; + type StoredCredentialMetadata = String; + + fn supports_request_independent_of_offer() -> bool; + + fn get_proposal_attachment_format() -> MaybeKnown; + fn get_offer_attachment_format() -> MaybeKnown; + fn get_request_attachment_format() -> MaybeKnown; + fn get_credential_attachment_format() -> MaybeKnown; + + async fn create_proposal_attachment_content( + data: &String, + ) -> VcxResult>; + + fn extract_offer_details( + offer_message: &OfferCredentialV2, + ) -> VcxResult; + + async fn create_request_attachment_content( + offer_message: &OfferCredentialV2, + data: &String, + ) -> VcxResult<(Vec, String)>; + + async fn create_request_attachment_content_independent_of_offer( + data: &String, + ) -> VcxResult<(Vec, String)>; + + async fn process_and_store_credential( + issue_credential_message: &IssueCredentialV2, + data: &String, + request_metadata: &String, + ) -> VcxResult; + } + } +} diff --git a/aries_vcx/src/protocols/issuance_v2/formats/issuer/hyperledger_indy.rs b/aries_vcx/src/protocols/issuance_v2/formats/issuer/hyperledger_indy.rs new file mode 100644 index 0000000000..31e503f432 --- /dev/null +++ b/aries_vcx/src/protocols/issuance_v2/formats/issuer/hyperledger_indy.rs @@ -0,0 +1,201 @@ +use std::{collections::HashMap, marker::PhantomData}; + +use aries_vcx_core::anoncreds::base_anoncreds::BaseAnonCreds; +use async_trait::async_trait; +use messages::msg_fields::protocols::cred_issuance::v2::{ + issue_credential::IssueCredentialAttachmentFormatType, + offer_credential::OfferCredentialAttachmentFormatType, + propose_credential::{ProposeCredentialAttachmentFormatType, ProposeCredentialV2}, + request_credential::{RequestCredentialAttachmentFormatType, RequestCredentialV2}, +}; +use shared_vcx::maybe_known::MaybeKnown; + +use super::IssuerCredentialIssuanceFormat; +use crate::{ + errors::error::{AriesVcxError, AriesVcxErrorKind, VcxResult}, + protocols::issuance_v2::formats::holder::hyperledger_indy::HyperledgerIndyCredentialFilter, + utils::openssl::encode, +}; + +/// Structure which implements [IssuerCredentialIssuanceFormat] functionality for the `hlindy/...` +/// family of issue-credential-v2 attachment formats. +/// +/// This implementation expects and creates attachments of the following types: +/// * [ProposeCredentialAttachmentFormatType::HyperledgerIndyCredentialFilter2_0] +/// * [RequestCredentialAttachmentFormatType::HyperledgerIndyCredentialRequest2_0] +/// * [OfferCredentialAttachmentFormatType::HyperledgerIndyCredentialAbstract2_0] +/// * [IssueCredentialAttachmentFormatType::HyperledgerIndyCredential2_0] +/// +/// This is done in accordance to the Aries RFC 0592 Spec: +/// +/// https://github.com/hyperledger/aries-rfcs/blob/b3a3942ef052039e73cd23d847f42947f8287da2/features/0592-indy-attachments/README.md + +pub struct HyperledgerIndyIssuerCredentialIssuanceFormat<'a, A> +where + A: BaseAnonCreds, +{ + _marker: &'a PhantomData<()>, + _anoncreds: PhantomData, +} + +pub struct HyperledgerIndyCreateOfferInput<'a, A> { + pub anoncreds: &'a A, + pub cred_def_id: String, +} + +#[derive(Clone)] +pub struct HyperledgerIndyCreatedOfferMetadata { + pub offer_json: String, +} + +pub struct HyperledgerIndyCreateCredentialInput<'a, A> { + pub anoncreds: &'a A, + pub credential_attributes: HashMap, + pub revocation_info: Option, +} + +#[derive(Clone)] +pub struct HyperledgerIndyCreateCredentialRevocationInfoInput { + pub registry_id: String, + pub tails_directory: String, +} + +#[derive(Clone)] +pub struct HyperledgerIndyCreatedCredentialMetadata { + pub credential_revocation_id: Option, +} + +#[async_trait] +impl<'a, A> IssuerCredentialIssuanceFormat for HyperledgerIndyIssuerCredentialIssuanceFormat<'a, A> +where + A: BaseAnonCreds + 'a, +{ + type ProposalDetails = HyperledgerIndyCredentialFilter; + + type CreateOfferInput = HyperledgerIndyCreateOfferInput<'a, A>; + type CreatedOfferMetadata = HyperledgerIndyCreatedOfferMetadata; + + type CreateCredentialInput = HyperledgerIndyCreateCredentialInput<'a, A>; + type CreatedCredentialMetadata = HyperledgerIndyCreatedCredentialMetadata; + + fn supports_request_independent_of_offer() -> bool { + false + } + + fn get_proposal_attachment_format() -> MaybeKnown { + MaybeKnown::Known(ProposeCredentialAttachmentFormatType::HyperledgerIndyCredentialFilter2_0) + } + + fn get_request_attachment_format() -> MaybeKnown { + MaybeKnown::Known( + RequestCredentialAttachmentFormatType::HyperledgerIndyCredentialRequest2_0, + ) + } + fn get_offer_attachment_format() -> MaybeKnown { + MaybeKnown::Known(OfferCredentialAttachmentFormatType::HyperledgerIndyCredentialAbstract2_0) + } + fn get_credential_attachment_format() -> MaybeKnown { + MaybeKnown::Known(IssueCredentialAttachmentFormatType::HyperledgerIndyCredential2_0) + } + + fn extract_proposal_details( + proposal_message: &ProposeCredentialV2, + ) -> VcxResult { + let attachment = Self::extract_proposal_attachment_content(proposal_message)?; + + Ok(serde_json::from_slice(&attachment)?) + } + + async fn create_offer_attachment_content( + data: &HyperledgerIndyCreateOfferInput, + ) -> VcxResult<(Vec, HyperledgerIndyCreatedOfferMetadata)> { + let cred_offer = data + .anoncreds + .issuer_create_credential_offer(&data.cred_def_id) + .await?; + + Ok(( + cred_offer.clone().into_bytes(), + HyperledgerIndyCreatedOfferMetadata { + offer_json: cred_offer, + }, + )) + } + + async fn create_credential_attachment_content( + offer_metadata: &HyperledgerIndyCreatedOfferMetadata, + request_message: &RequestCredentialV2, + data: &HyperledgerIndyCreateCredentialInput, + ) -> VcxResult<(Vec, HyperledgerIndyCreatedCredentialMetadata)> { + let offer = &offer_metadata.offer_json; + + let request_bytes = Self::extract_request_attachment_content(&request_message)?; + let request_payload = String::from_utf8(request_bytes).map_err(|_| { + AriesVcxError::from_msg( + AriesVcxErrorKind::EncodeError, + "Expected payload to be a utf8 string", + ) + })?; + + let encoded_credential_attributes = encode_attributes(&data.credential_attributes)?; + let encoded_credential_attributes_json = + serde_json::to_string(&encoded_credential_attributes)?; + + let (rev_reg_id, tails_dir) = data.revocation_info.as_ref().map_or((None, None), |info| { + ( + Some(info.registry_id.to_owned()), + Some(info.tails_directory.to_owned()), + ) + }); + + let (credential, cred_rev_id, _) = data + .anoncreds + .issuer_create_credential( + offer, + &request_payload, + &encoded_credential_attributes_json, + rev_reg_id, + tails_dir, + ) + .await?; + + let metadata = HyperledgerIndyCreatedCredentialMetadata { + credential_revocation_id: cred_rev_id, + }; + + Ok((credential.into_bytes(), metadata)) + } + + async fn create_credential_attachment_content_independent_of_offer( + _: &RequestCredentialV2, + _: &Self::CreateCredentialInput, + ) -> VcxResult<(Vec, HyperledgerIndyCreatedCredentialMetadata)> { + return Err(AriesVcxError::from_msg( + AriesVcxErrorKind::ActionNotSupported, + "Creating a credential independent of an offer is unsupported for this format", + )); + } +} + +fn encode_attributes( + attributes: &HashMap, +) -> VcxResult> { + let mut encoded = HashMap::::new(); + for (k, v) in attributes.into_iter() { + encoded.insert( + k.to_owned(), + RawAndEncoded { + raw: v.to_owned(), + encoded: encode(&v)?, + }, + ); + } + + Ok(encoded) +} + +#[derive(Serialize)] +struct RawAndEncoded { + raw: String, + encoded: String, +} diff --git a/aries_vcx/src/protocols/issuance_v2/formats/issuer/ld_proof_vc.rs b/aries_vcx/src/protocols/issuance_v2/formats/issuer/ld_proof_vc.rs new file mode 100644 index 0000000000..18b2533ceb --- /dev/null +++ b/aries_vcx/src/protocols/issuance_v2/formats/issuer/ld_proof_vc.rs @@ -0,0 +1,68 @@ +// TODO - delete, this is a mock + +use async_trait::async_trait; +use messages::msg_fields::protocols::cred_issuance::v2::{ + issue_credential::IssueCredentialAttachmentFormatType, + offer_credential::OfferCredentialAttachmentFormatType, + propose_credential::{ProposeCredentialAttachmentFormatType, ProposeCredentialV2}, + request_credential::{RequestCredentialAttachmentFormatType, RequestCredentialV2}, +}; +use shared_vcx::maybe_known::MaybeKnown; + +use super::IssuerCredentialIssuanceFormat; +use crate::errors::error::VcxResult; + +pub struct LdProofIssuerCredentialIssuanceFormat; + +#[async_trait] +impl IssuerCredentialIssuanceFormat for LdProofIssuerCredentialIssuanceFormat { + type ProposalDetails = (); + + type CreateOfferInput = (); + type CreatedOfferMetadata = (); + + type CreateCredentialInput = (); + type CreatedCredentialMetadata = (); + + fn supports_request_independent_of_offer() -> bool { + true + } + + fn get_proposal_attachment_format() -> MaybeKnown { + MaybeKnown::Known(ProposeCredentialAttachmentFormatType::AriesLdProofVcDetail1_0) + } + fn get_request_attachment_format() -> MaybeKnown { + MaybeKnown::Known(RequestCredentialAttachmentFormatType::AriesLdProofVcDetail1_0) + } + fn get_offer_attachment_format() -> MaybeKnown { + MaybeKnown::Known(OfferCredentialAttachmentFormatType::AriesLdProofVcDetail1_0) + } + fn get_credential_attachment_format() -> MaybeKnown { + MaybeKnown::Known(IssueCredentialAttachmentFormatType::AriesLdProofVc1_0) + } + + fn extract_proposal_details(_: &ProposeCredentialV2) -> VcxResult { + Ok(()) + } + + async fn create_offer_attachment_content( + _: &Self::CreateOfferInput, + ) -> VcxResult<(Vec, ())> { + Ok(("mock data".into(), ())) + } + + async fn create_credential_attachment_content( + _offer_metadata: &(), + _request_message: &RequestCredentialV2, + _data: &Self::CreateCredentialInput, + ) -> VcxResult<(Vec, ())> { + Ok(("mock data".into(), ())) + } + + async fn create_credential_attachment_content_independent_of_offer( + _request_message: &RequestCredentialV2, + _data: &Self::CreateCredentialInput, + ) -> VcxResult<(Vec, ())> { + Ok(("mock data".into(), ())) + } +} diff --git a/aries_vcx/src/protocols/issuance_v2/formats/issuer/mod.rs b/aries_vcx/src/protocols/issuance_v2/formats/issuer/mod.rs new file mode 100644 index 0000000000..a2e9515f76 --- /dev/null +++ b/aries_vcx/src/protocols/issuance_v2/formats/issuer/mod.rs @@ -0,0 +1,117 @@ +pub mod hyperledger_indy; +pub mod ld_proof_vc; + +use async_trait::async_trait; +use messages::msg_fields::protocols::cred_issuance::v2::{ + issue_credential::IssueCredentialAttachmentFormatType, + offer_credential::OfferCredentialAttachmentFormatType, + propose_credential::{ProposeCredentialAttachmentFormatType, ProposeCredentialV2}, + request_credential::{RequestCredentialAttachmentFormatType, RequestCredentialV2}, +}; +use shared_vcx::maybe_known::MaybeKnown; + +use crate::{ + errors::error::{AriesVcxError, AriesVcxErrorKind, VcxResult}, + handlers::util::{extract_attachment_data, get_attachment_with_id}, +}; + +/// Trait representing some issue-credential-v2 format family, containing methods required by an +/// issuer of this format to create attachments of this format. +#[async_trait] +pub trait IssuerCredentialIssuanceFormat { + type ProposalDetails; + + type CreateOfferInput; + type CreatedOfferMetadata; + + type CreateCredentialInput; + type CreatedCredentialMetadata; + + fn supports_request_independent_of_offer() -> bool; + + /// Retrieve the format type that an implementation uses/expects for credential proposal + /// attachments. + /// + /// See formats here: https://github.com/hyperledger/aries-rfcs/blob/main/features/0453-issue-credential-v2/README.md#propose-attachment-registry + fn get_proposal_attachment_format() -> MaybeKnown; + + /// Retrieve the format type that an implementation uses/expects for credential offer + /// attachments. + /// + /// See formats here: https://github.com/hyperledger/aries-rfcs/blob/main/features/0453-issue-credential-v2/README.md#offer-attachment-registry + fn get_offer_attachment_format() -> MaybeKnown; + + /// Retrieve the format type that an implementation uses/expects for credential request + /// attachments. + /// + /// See formats here: https://github.com/hyperledger/aries-rfcs/blob/main/features/0453-issue-credential-v2/README.md#request-attachment-registry + fn get_request_attachment_format() -> MaybeKnown; + + /// Retrieve the format type that an implementation uses/expects for credential attachments. + /// + /// See formats here: https://github.com/hyperledger/aries-rfcs/blob/main/features/0453-issue-credential-v2/README.md#credentials-attachment-registry + fn get_credential_attachment_format() -> MaybeKnown; + + fn extract_proposal_attachment_content( + proposal_message: &ProposeCredentialV2, + ) -> VcxResult> { + let attachment_id = proposal_message + .content + .formats + .iter() + .find_map(|format| { + (format.format == Self::get_proposal_attachment_format()) + .then_some(&format.attach_id) + }) + .ok_or(AriesVcxError::from_msg( + AriesVcxErrorKind::InvalidMessageFormat, + "Message does not containing an attachment with the expected format.", + ))?; + + let attachment = + get_attachment_with_id(&proposal_message.content.filters_attach, attachment_id)?; + + extract_attachment_data(attachment) + } + + fn extract_proposal_details( + proposal_message: &ProposeCredentialV2, + ) -> VcxResult; + + async fn create_offer_attachment_content( + data: &Self::CreateOfferInput, + ) -> VcxResult<(Vec, Self::CreatedOfferMetadata)>; + + fn extract_request_attachment_content( + request_message: &RequestCredentialV2, + ) -> VcxResult> { + let attachment_id = request_message + .content + .formats + .iter() + .find_map(|format| { + (format.format == Self::get_request_attachment_format()) + .then_some(&format.attach_id) + }) + .ok_or(AriesVcxError::from_msg( + AriesVcxErrorKind::InvalidMessageFormat, + "Message does not containing an attachment with the expected format.", + ))?; + + let attachment = + get_attachment_with_id(&request_message.content.requests_attach, attachment_id)?; + + extract_attachment_data(attachment) + } + + async fn create_credential_attachment_content( + offer_metadata: &Self::CreatedOfferMetadata, + request_message: &RequestCredentialV2, + data: &Self::CreateCredentialInput, + ) -> VcxResult<(Vec, Self::CreatedCredentialMetadata)>; + + async fn create_credential_attachment_content_independent_of_offer( + request_message: &RequestCredentialV2, + data: &Self::CreateCredentialInput, + ) -> VcxResult<(Vec, Self::CreatedCredentialMetadata)>; +} diff --git a/aries_vcx/src/protocols/issuance_v2/formats/mod.rs b/aries_vcx/src/protocols/issuance_v2/formats/mod.rs new file mode 100644 index 0000000000..edadb99165 --- /dev/null +++ b/aries_vcx/src/protocols/issuance_v2/formats/mod.rs @@ -0,0 +1,2 @@ +pub mod holder; +pub mod issuer; diff --git a/aries_vcx/src/protocols/issuance_v2/mod.rs b/aries_vcx/src/protocols/issuance_v2/mod.rs new file mode 100644 index 0000000000..174847e614 --- /dev/null +++ b/aries_vcx/src/protocols/issuance_v2/mod.rs @@ -0,0 +1,35 @@ +use messages::AriesMessage; + +use crate::errors::error::{AriesVcxError, AriesVcxErrorKind}; + +pub mod formats; +pub mod processing; +pub mod state_machines; + +// TODO - better name? +pub struct RecoveredSMError { + pub error: AriesVcxError, + pub state_machine: T, +} + +impl std::fmt::Debug for RecoveredSMError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("RecoveredSMError") + .field("error", &self.error) + .finish() + } +} + +// TODO - impl Error for RecoveredSMError? + +type VcxSMTransitionResult = Result>; + +fn unmatched_thread_id_error(msg: AriesMessage, expected_thid: &str) -> AriesVcxError { + AriesVcxError::from_msg( + AriesVcxErrorKind::InvalidJson, + format!( + "Cannot handle message {:?}: thread id does not match, expected {:?}", + msg, expected_thid + ), + ) +} diff --git a/aries_vcx/src/protocols/issuance_v2/processing/holder.rs b/aries_vcx/src/protocols/issuance_v2/processing/holder.rs new file mode 100644 index 0000000000..ca87c3b112 --- /dev/null +++ b/aries_vcx/src/protocols/issuance_v2/processing/holder.rs @@ -0,0 +1,64 @@ +use messages::{ + decorators::thread::Thread, + msg_fields::protocols::cred_issuance::v2::{ + propose_credential::{ + ProposeCredentialAttachmentFormatType, ProposeCredentialV2, ProposeCredentialV2Content, + ProposeCredentialV2Decorators, + }, + request_credential::{ + RequestCredentialAttachmentFormatType, RequestCredentialV2, RequestCredentialV2Content, + RequestCredentialV2Decorators, + }, + CredentialPreviewV2, + }, +}; +use shared_vcx::maybe_known::MaybeKnown; +use uuid::Uuid; + +use super::create_attachments_and_formats; + +pub fn create_proposal_message_from_attachments( + attachments_format_and_data: Vec<(MaybeKnown, Vec)>, + preview: Option, + thread_id: Option, +) -> ProposeCredentialV2 { + let (attachments, formats) = create_attachments_and_formats(attachments_format_and_data); + + let content = ProposeCredentialV2Content::builder() + .formats(formats) + .filters_attach(attachments) + .credential_preview(preview) + .build(); + + let decorators = ProposeCredentialV2Decorators::builder() + .thread(thread_id.map(|id| Thread::builder().thid(id).build())) + .build(); + + ProposeCredentialV2::builder() + .id(Uuid::new_v4().to_string()) + .content(content) + .decorators(decorators) + .build() +} + +pub fn create_request_message_from_attachments( + attachments_format_and_data: Vec<(MaybeKnown, Vec)>, + thread_id: Option, +) -> RequestCredentialV2 { + let (attachments, formats) = create_attachments_and_formats(attachments_format_and_data); + + let content = RequestCredentialV2Content::builder() + .formats(formats) + .requests_attach(attachments) + .build(); + + let decorators = RequestCredentialV2Decorators::builder() + .thread(thread_id.map(|id| Thread::builder().thid(id).build())) + .build(); + + RequestCredentialV2::builder() + .id(Uuid::new_v4().to_string()) + .content(content) + .decorators(decorators) + .build() +} diff --git a/aries_vcx/src/protocols/issuance_v2/processing/issuer.rs b/aries_vcx/src/protocols/issuance_v2/processing/issuer.rs new file mode 100644 index 0000000000..aba964890f --- /dev/null +++ b/aries_vcx/src/protocols/issuance_v2/processing/issuer.rs @@ -0,0 +1,73 @@ +use messages::{ + decorators::{ + please_ack::{AckOn, PleaseAck}, + thread::Thread, + }, + msg_fields::protocols::cred_issuance::v2::{ + issue_credential::{ + IssueCredentialAttachmentFormatType, IssueCredentialV2, IssueCredentialV2Content, + IssueCredentialV2Decorators, + }, + offer_credential::{ + OfferCredentialAttachmentFormatType, OfferCredentialV2, OfferCredentialV2Content, + OfferCredentialV2Decorators, + }, + CredentialPreviewV2, + }, +}; +use shared_vcx::maybe_known::MaybeKnown; +use uuid::Uuid; + +use super::create_attachments_and_formats; + +pub fn create_offer_message_from_attachments( + attachments_format_and_data: Vec<(MaybeKnown, Vec)>, + preview: CredentialPreviewV2, + replacement_id: Option, + thread_id: Option, +) -> OfferCredentialV2 { + let (attachments, formats) = create_attachments_and_formats(attachments_format_and_data); + + let content = OfferCredentialV2Content::builder() + .credential_preview(preview) + .formats(formats) + .offers_attach(attachments) + .replacement_id(replacement_id) + .build(); + + let decorators = OfferCredentialV2Decorators::builder() + .thread(thread_id.map(|id| Thread::builder().thid(id).build())) + .build(); + + OfferCredentialV2::builder() + .id(Uuid::new_v4().to_string()) + .content(content) + .decorators(decorators) + .build() +} + +pub fn create_credential_message_from_attachments( + attachments_format_and_data: Vec<(MaybeKnown, Vec)>, + please_ack: bool, + thread_id: String, + replacement_id: Option, +) -> IssueCredentialV2 { + let (attachments, formats) = create_attachments_and_formats(attachments_format_and_data); + + let content = IssueCredentialV2Content::builder() + .formats(formats) + .credentials_attach(attachments) + .replacement_id(replacement_id) + .build(); + + let decorators = IssueCredentialV2Decorators::builder() + .thread(Thread::builder().thid(thread_id).build()) + .please_ack(please_ack.then_some(PleaseAck::builder().on(vec![AckOn::Outcome]).build())) + .build(); + + IssueCredentialV2::builder() + .id(Uuid::new_v4().to_string()) + .content(content) + .decorators(decorators) + .build() +} diff --git a/aries_vcx/src/protocols/issuance_v2/processing/mod.rs b/aries_vcx/src/protocols/issuance_v2/processing/mod.rs new file mode 100644 index 0000000000..eaf363541f --- /dev/null +++ b/aries_vcx/src/protocols/issuance_v2/processing/mod.rs @@ -0,0 +1,43 @@ +use base64::{engine::general_purpose, Engine}; +use messages::{ + decorators::attachment::{Attachment, AttachmentData, AttachmentType}, + misc::MimeType, + msg_fields::protocols::cred_issuance::v2::AttachmentFormatSpecifier, +}; +use shared_vcx::maybe_known::MaybeKnown; +use uuid::Uuid; + +pub mod holder; +pub mod issuer; + +fn create_attachments_and_formats( + attachments_format_and_data: Vec<(MaybeKnown, Vec)>, +) -> (Vec, Vec>) { + let mut attachments = vec![]; + let mut formats = vec![]; + + for (format, attachment_data) in attachments_format_and_data { + let attachment_content = + AttachmentType::Base64(general_purpose::URL_SAFE.encode(&attachment_data)); + let attach_id = Uuid::new_v4().to_string(); + let attachment = Attachment::builder() + .id(attach_id.clone()) + .mime_type(MimeType::Json) + .data( + AttachmentData::builder() + .content(attachment_content) + .build(), + ) + .build(); + + let format_specifier = AttachmentFormatSpecifier::builder() + .attach_id(attach_id) + .format(format) + .build(); + + attachments.push(attachment); + formats.push(format_specifier); + } + + (attachments, formats) +} diff --git a/aries_vcx/src/protocols/issuance_v2/state_machines/holder/mod.rs b/aries_vcx/src/protocols/issuance_v2/state_machines/holder/mod.rs new file mode 100644 index 0000000000..f366660a34 --- /dev/null +++ b/aries_vcx/src/protocols/issuance_v2/state_machines/holder/mod.rs @@ -0,0 +1,459 @@ +pub mod states; + +use std::error::Error; + +use messages::{ + decorators::thread::Thread, + msg_fields::protocols::{ + cred_issuance::v2::{ + ack::AckCredentialV2, issue_credential::IssueCredentialV2, + offer_credential::OfferCredentialV2, problem_report::CredIssuanceProblemReportV2, + propose_credential::ProposeCredentialV2, request_credential::RequestCredentialV2, + CredentialPreviewV2, + }, + notification::ack::{AckContent, AckDecorators, AckStatus}, + report_problem::{Description, ProblemReportContent, ProblemReportDecorators}, + }, +}; +use uuid::Uuid; + +use self::states::{ + completed::Completed, credential_received::CredentialReceived, failed::Failed, + offer_received::OfferReceived, proposal_prepared::ProposalPrepared, + request_prepared::RequestPrepared, +}; +use crate::{ + errors::error::VcxResult, + handlers::util::{get_thread_id_or_message_id, matches_thread_id}, + protocols::issuance_v2::{ + formats::holder::HolderCredentialIssuanceFormat, + processing::holder::{ + create_proposal_message_from_attachments, create_request_message_from_attachments, + }, + unmatched_thread_id_error, RecoveredSMError, VcxSMTransitionResult, + }, +}; + +/// Represents a type-state machine which walks through issue-credential-v2 from the Holder +/// perspective. https://github.com/hyperledger/aries-rfcs/blob/main/features/0453-issue-credential-v2/README.md +/// +/// States in the HolderV2 APIs require knowledge of the credential format being used. As such, this +/// API only supports usage of a single credential format being used throughout a single protocol +/// flow. +/// +/// To indicate which credential format should be used by [HolderV2], an implementation of +/// [HolderCredentialIssuanceFormat] should be used as the generic argument when required. +/// +/// For instance, the following will bootstrap a [HolderV2] into the [ProposalPrepared] state, +/// with the `HyperledgerIndyHolderCredentialIssuanceFormat` format. +/// +/// ```no_run +/// let holder = +/// HolderV2::>::with_proposal( +/// &proposal_input, +/// Some(proposal_preview.clone()), +/// ) +/// .await +/// .unwrap(); +/// ``` +/// +/// For more information about formats, see [HolderCredentialIssuanceFormat] documentation. +pub struct HolderV2 { + state: S, + thread_id: String, +} + +impl HolderV2 { + pub fn from_parts(thread_id: String, state: S) -> Self { + Self { state, thread_id } + } + + pub fn into_parts(self) -> (String, S) { + (self.thread_id, self.state) + } + + /// Get the thread ID that is being used for this protocol instance. + pub fn get_thread_id(&self) -> &str { + &self.thread_id + } + + pub fn get_state(&self) -> &S { + &self.state + } +} + +impl HolderV2> { + /// Initiate a new [HolderV2] by preparing a proposal message from the provided input for + /// creating a proposal with the choosen [HolderCredentialIssuanceFormat]. + /// + /// Additionally, a [CredentialPreviewV2] can be provided to attach more proposal information + /// in the proposal message payload. + pub async fn with_proposal( + input_data: &T::CreateProposalInput, + preview: Option, + ) -> VcxResult<(Self, ProposeCredentialV2)> { + let attachment_data = T::create_proposal_attachment_content(input_data).await?; + let attachments_format_and_data = + vec![(T::get_proposal_attachment_format(), attachment_data)]; + let proposal = + create_proposal_message_from_attachments(attachments_format_and_data, preview, None); + + let holder = HolderV2 { + thread_id: get_thread_id_or_message_id!(proposal), + state: ProposalPrepared::new(), + }; + + Ok((holder, proposal)) + } + + /// Receive an incoming [OfferCredentialV2] message for this protocol. On success, the + /// [HolderV2] transitions into the [OfferReceived] state. + /// + /// This API should only be used for offers which are in response to an ongoing [HolderV2] + /// protocol thread. New offers should be received via [HolderV2::from_offer]. + /// + /// In the event of failure, an error is returned which contains the reason for failure + /// and the state machine before any transitions. Consumers should decide whether the failure + /// is terminal, in which case they should prepare a problem report. + pub fn receive_offer( + self, + offer: OfferCredentialV2, + ) -> VcxSMTransitionResult>, Self> { + let is_match = offer + .decorators + .thread + .as_ref() + .map_or(false, |t| t.thid == self.thread_id); + if !is_match { + return Err(RecoveredSMError { + error: unmatched_thread_id_error(offer.into(), &self.thread_id), + state_machine: self, + }); + } + + let new_state = OfferReceived::new(offer); + + Ok(HolderV2 { + state: new_state, + thread_id: self.thread_id, + }) + } +} + +impl HolderV2> { + /// Initialize a [HolderV2] protocol from a new incoming [OfferCredentialV2] message. + /// + /// The [HolderCredentialIssuanceFormat] used during initialization should be suitable for + /// the attachments within the [OfferCredentialV2] message, or else the [HolderV2] will not + /// be able to transition forward without failure. + /// + /// This API should only be used for offers which are initializing a NEW issue-credential-v2 + /// thread. [OfferCredentialV2] messages which are in response to an ongoing protocol thread + /// should be handled via [HolderV2::receive_offer]. + pub fn from_offer(offer: OfferCredentialV2) -> Self { + Self { + thread_id: get_thread_id_or_message_id!(offer), + state: OfferReceived::new(offer), + } + } + + /// Get the details and credential preview of the offer that was received. The returned + /// [HolderCredentialIssuanceFormat::OfferDetails] data will contain data specific to the + /// format being used. + pub fn get_offer_details(&self) -> VcxResult<(T::OfferDetails, &CredentialPreviewV2)> { + let offer_msg = self.state.get_offer(); + let details = T::extract_offer_details(offer_msg)?; + let preview = &offer_msg.content.credential_preview; + + Ok((details, preview)) + } + + /// Respond to an offer by preparing a new proposal. This API can be used repeatedly to + /// negotiate the offer with the issuer until an agreement is reached. + /// + /// A proposal is prepared in the format of [HolderCredentialIssuanceFormat], using the provided + /// input data to create it. Additionally, a [CredentialPreviewV2] can be attached to give + /// further details to the issuer about the proposal. + /// + /// In the event of failure, an error is returned which contains the reason for failure + /// and the state machine before any transitions. Consumers should decide whether the failure + /// is terminal, in which case they should prepare a problem report. + pub async fn prepare_proposal( + self, + input_data: &T::CreateProposalInput, + preview: Option, + ) -> VcxSMTransitionResult<(HolderV2>, ProposeCredentialV2), Self> { + let attachment_data = match T::create_proposal_attachment_content(input_data).await { + Ok(msg) => msg, + Err(error) => { + return Err(RecoveredSMError { + error, + state_machine: self, + }) + } + }; + let attachments_format_and_data = + vec![(T::get_proposal_attachment_format(), attachment_data)]; + let proposal = create_proposal_message_from_attachments( + attachments_format_and_data, + preview, + Some(self.thread_id.clone()), + ); + + let holder = HolderV2 { + state: ProposalPrepared::new(), + thread_id: self.thread_id, + }; + + Ok((holder, proposal)) + } + + /// Respond to an offer by preparing a request (to accept the offer). The request is prepared in + /// the format of [HolderCredentialIssuanceFormat] using the input data to create it. If the + /// request is successfully prepared, the [HolderV2] will transition to [RequestPrepared] where + /// the request message can be sent. + /// + /// In the event of failure, an error is returned which contains the reason for failure + /// and the state machine before any transitions. Consumers should decide whether the failure + /// is terminal, in which case they should prepare a problem report. + pub async fn prepare_credential_request( + self, + input_data: &T::CreateRequestInput, + ) -> VcxSMTransitionResult<(HolderV2>, RequestCredentialV2), Self> { + let offer_message = self.state.get_offer(); + + let (attachment_data, output_metadata) = + match T::create_request_attachment_content(offer_message, input_data).await { + Ok((data, meta)) => (data, meta), + Err(error) => { + return Err(RecoveredSMError { + error, + state_machine: self, + }) + } + }; + + let attachments_format_and_data = + vec![(T::get_request_attachment_format(), attachment_data)]; + let request = create_request_message_from_attachments( + attachments_format_and_data, + Some(self.thread_id.clone()), + ); + + let new_state = RequestPrepared::new(output_metadata); + + let holder = HolderV2 { + state: new_state, + thread_id: self.thread_id, + }; + + Ok((holder, request)) + } +} + +impl HolderV2> { + /// Initialize a [HolderV2] by preparing a request. This API should only be used to create + /// standalone requests that are not in response to an ongoing protocol thread (i.e. in + /// response to an offer). + /// + /// To create a request in response to an ongoing protocol thread, the + /// [HolderV2::prepare_credential_request] method should be used. + /// + /// The request is prepared in the [HolderCredentialIssuanceFormat] using the input data to + /// create it. Note that the [HolderCredentialIssuanceFormat] MUST support standalone request + /// creation for this function to succeed, some formats (such as hlindy or anoncreds) do not + /// support this. + pub async fn with_request( + input_data: &T::CreateRequestInput, + ) -> VcxResult<(HolderV2>, RequestCredentialV2)> { + let (attachment_data, output_metadata) = + T::create_request_attachment_content_independent_of_offer(input_data).await?; + + let attachments_format_and_data = + vec![(T::get_request_attachment_format(), attachment_data)]; + let request = create_request_message_from_attachments(attachments_format_and_data, None); + + let thread_id = get_thread_id_or_message_id!(request); + + let new_state = RequestPrepared::new(output_metadata); + + let holder = HolderV2 { + thread_id, + state: new_state, + }; + + Ok((holder, request)) + } + + /// Receive a credential in response to a request message that was sent to the issuer. + /// The received credential is processed and stored in accordance to the + /// [HolderCredentialIssuanceFormat] being used. + /// + /// In the event of failure, an error is returned which contains the reason for failure + /// and the state machine before any transitions. Consumers should decide whether the failure + /// is terminal, in which case they should prepare a problem report. + pub async fn receive_credential( + self, + credential: IssueCredentialV2, + input_data: &T::StoreCredentialInput, + ) -> VcxSMTransitionResult>, Self> { + let is_match = matches_thread_id!(credential, self.thread_id.as_str()); + if !is_match { + return Err(RecoveredSMError { + error: unmatched_thread_id_error(credential.into(), &self.thread_id), + state_machine: self, + }); + } + let credential_received_metadata = match T::process_and_store_credential( + &credential, + input_data, + self.state.get_request_preparation_metadata(), + ) + .await + { + Ok(data) => data, + Err(error) => { + return Err(RecoveredSMError { + error, + state_machine: self, + }) + } + }; + + let should_ack = credential.decorators.please_ack.is_some(); + let new_state = CredentialReceived::new(credential_received_metadata, should_ack); + Ok(HolderV2 { + state: new_state, + thread_id: self.thread_id, + }) + } +} + +impl HolderV2> { + /// Get details about the credential that was received and stored. + /// The details are specific to the [HolderCredentialIssuanceFormat] being used. + pub fn get_stored_credential_metadata(&self) -> &T::StoredCredentialMetadata { + self.state.get_stored_credential_metadata() + } + + // TODO - consider enum variants for (HolderV2, HoldverV2) + /// Transition into the [Complete] state, by preparing an Ack message, only if required. + pub fn prepare_ack_if_required(self) -> (HolderV2>, Option) { + let should_ack = self.state.get_should_ack(); + + let ack = if should_ack { + Some( + AckCredentialV2::builder() + .id(uuid::Uuid::new_v4().to_string()) + .content(AckContent::builder().status(AckStatus::Ok).build()) + .decorators( + AckDecorators::builder() + .thread(Thread::builder().thid(self.thread_id.clone()).build()) + .build(), + ) + .build(), + ) + } else { + None + }; + let holder = HolderV2 { + state: Completed::new(), + thread_id: self.thread_id, + }; + + (holder, ack) + } +} + +impl HolderV2 { + /// Get the prepared [CredIssuanceProblemReportV2] to be sent to the issuer to report a failure. + pub fn get_problem_report(&self) -> &CredIssuanceProblemReportV2 { + &self.state.get_problem_report() + } +} + +impl HolderV2 { + /// Transition into the [Failed] state by preparing a problem report message for the issuer. + /// The problem report message is generated by using details from the provided [Error]. + pub fn prepare_problem_report_with_error(self, err: &E) -> HolderV2 + where + E: Error, + { + let content = ProblemReportContent::builder() + .description(Description::builder().code(err.to_string()).build()) + .build(); + + let decorators = ProblemReportDecorators::builder() + .thread(Thread::builder().thid(self.thread_id.clone()).build()) + .build(); + + let report = CredIssuanceProblemReportV2::builder() + .id(Uuid::new_v4().to_string()) + .content(content) + .decorators(decorators) + .build(); + + let new_state = Failed::new(report); + + HolderV2 { + state: new_state, + thread_id: self.thread_id, + } + } +} + +#[cfg(test)] +mod tests { + use base64::{engine::general_purpose, Engine}; + use messages::decorators::attachment::AttachmentType; + use shared_vcx::maybe_known::MaybeKnown; + + use crate::protocols::issuance_v2::{ + formats::holder::mocks::MockHolderCredentialIssuanceFormat, + state_machines::holder::{states::proposal_prepared::ProposalPrepared, HolderV2}, + }; + + #[tokio::test] + async fn test_with_proposal_creates_message_with_attachments() { + // note synchronization issues. might need to just set this once globally and use constant + // data + let ctx = MockHolderCredentialIssuanceFormat::create_proposal_attachment_content_context(); + + ctx.expect() + .returning(|_| Ok(String::from("data").into_bytes())); + + let ctx2 = MockHolderCredentialIssuanceFormat::get_proposal_attachment_format_context(); + ctx2.expect() + .returning(|| MaybeKnown::Unknown(String::from("format"))); + + let (_holder, proposal) = + HolderV2::>::with_proposal( + &String::from("in"), + None, + ) + .await + .unwrap(); + + let formats = proposal.content.formats.clone(); + let attachments = proposal.content.filters_attach.clone(); + + assert_eq!(formats.len(), 1); + assert_eq!(attachments.len(), 1); + + assert_eq!(formats[0].attach_id, attachments[0].id.clone().unwrap()); + assert_eq!( + formats[0].format, + MaybeKnown::Unknown(String::from("format")) + ); + + let AttachmentType::Base64(b64_content) = attachments[0].data.content.clone() else { + panic!("wrong attachment type") + }; + + let decoded = general_purpose::URL_SAFE.decode(&b64_content).unwrap(); + + assert_eq!(String::from_utf8(decoded).unwrap(), String::from("data")); + } + + // TODO - unit test all when we're happy with the layout +} diff --git a/aries_vcx/src/protocols/issuance_v2/state_machines/holder/states/completed.rs b/aries_vcx/src/protocols/issuance_v2/state_machines/holder/states/completed.rs new file mode 100644 index 0000000000..b80a8f7bd1 --- /dev/null +++ b/aries_vcx/src/protocols/issuance_v2/state_machines/holder/states/completed.rs @@ -0,0 +1,15 @@ +use std::marker::PhantomData; + +use crate::protocols::issuance_v2::formats::holder::HolderCredentialIssuanceFormat; + +pub struct Completed { + _marker: PhantomData, +} + +impl Completed { + pub fn new() -> Self { + Self { + _marker: PhantomData, + } + } +} diff --git a/aries_vcx/src/protocols/issuance_v2/state_machines/holder/states/credential_received.rs b/aries_vcx/src/protocols/issuance_v2/state_machines/holder/states/credential_received.rs new file mode 100644 index 0000000000..4dc4f28f16 --- /dev/null +++ b/aries_vcx/src/protocols/issuance_v2/state_machines/holder/states/credential_received.rs @@ -0,0 +1,23 @@ +use crate::protocols::issuance_v2::formats::holder::HolderCredentialIssuanceFormat; + +pub struct CredentialReceived { + stored_credential_metadata: T::StoredCredentialMetadata, + should_ack: bool, +} + +impl CredentialReceived { + pub fn new(stored_credential_metadata: T::StoredCredentialMetadata, should_ack: bool) -> Self { + Self { + stored_credential_metadata, + should_ack, + } + } + + pub fn get_stored_credential_metadata(&self) -> &T::StoredCredentialMetadata { + &self.stored_credential_metadata + } + + pub fn get_should_ack(&self) -> bool { + self.should_ack + } +} diff --git a/aries_vcx/src/protocols/issuance_v2/state_machines/holder/states/failed.rs b/aries_vcx/src/protocols/issuance_v2/state_machines/holder/states/failed.rs new file mode 100644 index 0000000000..a6790f48fb --- /dev/null +++ b/aries_vcx/src/protocols/issuance_v2/state_machines/holder/states/failed.rs @@ -0,0 +1,15 @@ +use messages::msg_fields::protocols::cred_issuance::v2::problem_report::CredIssuanceProblemReportV2; + +pub struct Failed { + problem_report: CredIssuanceProblemReportV2, +} + +impl Failed { + pub fn new(problem_report: CredIssuanceProblemReportV2) -> Self { + Self { problem_report } + } + + pub fn get_problem_report(&self) -> &CredIssuanceProblemReportV2 { + &self.problem_report + } +} diff --git a/aries_vcx/src/protocols/issuance_v2/state_machines/holder/states/mod.rs b/aries_vcx/src/protocols/issuance_v2/state_machines/holder/states/mod.rs new file mode 100644 index 0000000000..1c4cfcc27a --- /dev/null +++ b/aries_vcx/src/protocols/issuance_v2/state_machines/holder/states/mod.rs @@ -0,0 +1,6 @@ +pub mod completed; +pub mod credential_received; +pub mod failed; +pub mod offer_received; +pub mod proposal_prepared; +pub mod request_prepared; diff --git a/aries_vcx/src/protocols/issuance_v2/state_machines/holder/states/offer_received.rs b/aries_vcx/src/protocols/issuance_v2/state_machines/holder/states/offer_received.rs new file mode 100644 index 0000000000..1f91e45614 --- /dev/null +++ b/aries_vcx/src/protocols/issuance_v2/state_machines/holder/states/offer_received.rs @@ -0,0 +1,23 @@ +use std::marker::PhantomData; + +use messages::msg_fields::protocols::cred_issuance::v2::offer_credential::OfferCredentialV2; + +use crate::protocols::issuance_v2::formats::holder::HolderCredentialIssuanceFormat; + +pub struct OfferReceived { + offer: OfferCredentialV2, + _marker: PhantomData, +} + +impl OfferReceived { + pub fn new(offer: OfferCredentialV2) -> Self { + Self { + offer, + _marker: PhantomData, + } + } + + pub fn get_offer(&self) -> &OfferCredentialV2 { + &self.offer + } +} diff --git a/aries_vcx/src/protocols/issuance_v2/state_machines/holder/states/proposal_prepared.rs b/aries_vcx/src/protocols/issuance_v2/state_machines/holder/states/proposal_prepared.rs new file mode 100644 index 0000000000..b44f05fe3d --- /dev/null +++ b/aries_vcx/src/protocols/issuance_v2/state_machines/holder/states/proposal_prepared.rs @@ -0,0 +1,15 @@ +use std::marker::PhantomData; + +use crate::protocols::issuance_v2::formats::holder::HolderCredentialIssuanceFormat; + +pub struct ProposalPrepared { + _marker: PhantomData, +} + +impl ProposalPrepared { + pub fn new() -> Self { + Self { + _marker: PhantomData, + } + } +} diff --git a/aries_vcx/src/protocols/issuance_v2/state_machines/holder/states/request_prepared.rs b/aries_vcx/src/protocols/issuance_v2/state_machines/holder/states/request_prepared.rs new file mode 100644 index 0000000000..886a372314 --- /dev/null +++ b/aries_vcx/src/protocols/issuance_v2/state_machines/holder/states/request_prepared.rs @@ -0,0 +1,17 @@ +use crate::protocols::issuance_v2::formats::holder::HolderCredentialIssuanceFormat; + +pub struct RequestPrepared { + request_preparation_metadata: T::CreatedRequestMetadata, +} + +impl RequestPrepared { + pub fn new(request_preparation_metadata: T::CreatedRequestMetadata) -> Self { + Self { + request_preparation_metadata, + } + } + + pub fn get_request_preparation_metadata(&self) -> &T::CreatedRequestMetadata { + &self.request_preparation_metadata + } +} diff --git a/aries_vcx/src/protocols/issuance_v2/state_machines/issuer/mod.rs b/aries_vcx/src/protocols/issuance_v2/state_machines/issuer/mod.rs new file mode 100644 index 0000000000..67eb9935f2 --- /dev/null +++ b/aries_vcx/src/protocols/issuance_v2/state_machines/issuer/mod.rs @@ -0,0 +1,453 @@ +pub mod states; + +use std::error::Error; + +use messages::{ + decorators::thread::Thread, + msg_fields::protocols::{ + cred_issuance::v2::{ + ack::AckCredentialV2, issue_credential::IssueCredentialV2, + offer_credential::OfferCredentialV2, problem_report::CredIssuanceProblemReportV2, + propose_credential::ProposeCredentialV2, request_credential::RequestCredentialV2, + CredentialPreviewV2, + }, + report_problem::{Description, ProblemReportContent, ProblemReportDecorators}, + }, +}; +use uuid::Uuid; + +use self::states::{ + completed::Completed, credential_prepared::CredentialPrepared, failed::Failed, + offer_prepared::OfferPrepared, proposal_received::ProposalReceived, + request_received::RequestReceived, +}; +use crate::{ + errors::error::{AriesVcxError, AriesVcxErrorKind, VcxResult}, + handlers::util::{get_thread_id_or_message_id, matches_thread_id}, + protocols::issuance_v2::{ + formats::issuer::IssuerCredentialIssuanceFormat, + processing::issuer::{ + create_credential_message_from_attachments, create_offer_message_from_attachments, + }, + unmatched_thread_id_error, RecoveredSMError, VcxSMTransitionResult, + }, +}; + +/// Represents a type-state machine which walks through issue-credential-v2 from the Issuer +/// perspective. https://github.com/hyperledger/aries-rfcs/blob/main/features/0453-issue-credential-v2/README.md +/// +/// States in the [IssuerV2] APIs require knowledge of the credential format being used. As such, +/// this API only supports usage of a single credential format being used throughout a single +/// protocol flow. +/// +/// To indicate which credential format should be used by [IssuerV2], an implementation of +/// [IssuerCredentialIssuanceFormat] should be used as the generic argument when required. +/// +/// For instance, the following will bootstrap a [IssuerV2] into the [ProposalPrepared] state, +/// with the `HyperledgerIndyIssuerCredentialIssuanceFormat` format. +/// +/// ```no_run +/// let issuer = +/// IssuerV2::>::with_offer( +/// &offer_data, +/// offer_preview, +/// None, +/// ) +/// .await +/// .unwrap(); +/// ``` +/// +/// For more information about formats, see [IssuerCredentialIssuanceFormat] documentation. +pub struct IssuerV2 { + state: S, + thread_id: String, +} + +impl IssuerV2 { + pub fn from_parts(thread_id: String, state: S) -> Self { + Self { state, thread_id } + } + + pub fn into_parts(self) -> (String, S) { + (self.thread_id, self.state) + } + + /// Get the thread ID that is being used for this protocol instance. + pub fn get_thread_id(&self) -> &str { + &self.thread_id + } + + pub fn get_state(&self) -> &S { + &self.state + } +} + +impl IssuerV2> { + /// Initialize a new [IssuerV2] by receiving an incoming [ProposeCredentialV2] message from a + /// holder. + /// + /// The [IssuerCredentialIssuanceFormat] used during initialization should be suitable + /// for the attachments within the [ProposeCredentialV2] message, or else the [IssuerV2] will + /// not be able to transition forward without failure. + /// + /// This API should only be used for standalone proposals that aren't apart of an existing + /// protocol thread. Proposals in response to an ongoing thread should be handled via + /// [HolderV2::receive_proposal]. + pub fn from_proposal(proposal: ProposeCredentialV2) -> Self { + IssuerV2 { + thread_id: get_thread_id_or_message_id!(proposal), + state: ProposalReceived::new(proposal), + } + } + + /// Get the details and credential preview (if any) of the proposal that was received. The + /// returned [IssuerCredentialIssuanceFormat::ProposalDetails] data will contain data + /// specific to the format being used. + pub fn get_proposal_details( + &self, + ) -> VcxResult<(T::ProposalDetails, Option<&CredentialPreviewV2>)> { + let propsoal_msg = self.state.get_proposal(); + let details = T::extract_proposal_details(propsoal_msg)?; + let preview = propsoal_msg.content.credential_preview.as_ref(); + + Ok((details, preview)) + } + + /// Respond to a proposal by preparing a new offer. This API can be used repeatedly to negotiate + /// the offer with the holder until an agreement is reached. + /// + /// An offer is prepared in the format of [IssuerCredentialIssuanceFormat], using the provided + /// input data to create it. Additionally, a [CredentialPreviewV2] is attached to give further + /// details to the holder about the offer. + /// + /// In the event of failure, an error is returned which contains the reason for failure + /// and the state machine before any transitions. Consumers should decide whether the failure + /// is terminal, in which case they should prepare a problem report. + pub async fn prepare_offer( + self, + input_data: &T::CreateOfferInput, + preview: CredentialPreviewV2, + replacement_id: Option, + ) -> VcxSMTransitionResult<(IssuerV2>, OfferCredentialV2), Self> { + let (attachment_data, offer_metadata) = + match T::create_offer_attachment_content(input_data).await { + Ok(data) => data, + Err(error) => { + return Err(RecoveredSMError { + error, + state_machine: self, + }) + } + }; + let attachments_format_and_data = vec![(T::get_offer_attachment_format(), attachment_data)]; + let offer = create_offer_message_from_attachments( + attachments_format_and_data, + preview, + replacement_id, + Some(self.thread_id.clone()), + ); + + let new_state = OfferPrepared::new(offer_metadata); + + let issuer = IssuerV2 { + state: new_state, + thread_id: self.thread_id, + }; + + Ok((issuer, offer)) + } +} + +impl IssuerV2> { + /// Initiate a new [IssuerV2] by preparing a offer message from the provided input for + /// creating a offer with the choosen [IssuerCredentialIssuanceFormat]. + /// + /// Additionally, a [CredentialPreviewV2] is provided to attach more credential information + /// in the offer message payload. + pub async fn with_offer( + input_data: &T::CreateOfferInput, + preview: CredentialPreviewV2, + replacement_id: Option, + ) -> VcxResult<(Self, OfferCredentialV2)> { + let (attachment_data, offer_metadata) = + T::create_offer_attachment_content(input_data).await?; + + let attachments_format_and_data = vec![(T::get_offer_attachment_format(), attachment_data)]; + let offer = create_offer_message_from_attachments( + attachments_format_and_data, + preview, + replacement_id, + None, + ); + + let thread_id = get_thread_id_or_message_id!(offer); + + let new_state = OfferPrepared::new(offer_metadata); + + let issuer = IssuerV2 { + state: new_state, + thread_id, + }; + + Ok((issuer, offer)) + } + + /// Receive an incoming [ProposeCredentialV2] message for this protocol. On success, the + /// [IssuerV2] transitions into the [ProposalReceived] state. + /// + /// This API should only be used for proposals which are in response to an ongoing [IssuerV2] + /// protocol thread. New proposals should be received via [IssuerV2::from_proposal]. + /// + /// In the event of failure, an error is returned which contains the reason for failure + /// and the state machine before any transitions. Consumers should decide whether the failure + /// is terminal, in which case they should prepare a problem report. + pub fn receive_proposal( + self, + proposal: ProposeCredentialV2, + ) -> VcxSMTransitionResult>, Self> { + let is_match = proposal + .decorators + .thread + .as_ref() + .map_or(false, |t| t.thid == self.thread_id); + if !is_match { + return Err(RecoveredSMError { + error: unmatched_thread_id_error(proposal.into(), &self.thread_id), + state_machine: self, + }); + } + + let new_state = ProposalReceived::new(proposal); + + Ok(IssuerV2 { + state: new_state, + thread_id: self.thread_id, + }) + } + + /// Receive a request in response to an offer that was sent to the holder. + /// + /// This API should only be used for requests that are in response to an ongoing [IssuerV2] + /// protocol thread. To receive new standalone requests, [IssuerV2::from_request] should be + /// used. + /// + /// In the event of failure, an error is returned which contains the reason for failure + /// and the state machine before any transitions. Consumers should decide whether the failure + /// is terminal, in which case they should prepare a problem report. + pub fn receive_request( + self, + request: RequestCredentialV2, + ) -> VcxSMTransitionResult>, Self> { + let is_match = request + .decorators + .thread + .as_ref() + .map_or(false, |t| t.thid == self.thread_id); + if !is_match { + return Err(RecoveredSMError { + error: unmatched_thread_id_error(request.into(), &self.thread_id), + state_machine: self, + }); + } + + let new_state = RequestReceived::new(Some(self.state.into_parts()), request); + + Ok(IssuerV2 { + state: new_state, + thread_id: self.thread_id, + }) + } +} + +impl IssuerV2> { + /// Initialize an [IssuerV2] by receiving a standalone request message from a holder. This API + /// should only be used for standalone requests not in response to an ongoing protocol thread. + /// + /// To receive a request in response to an ongoing protocol thread, the + /// [IssuerV2::receive_request] method should be used. + /// + /// The request should contain an attachment in the suitable [IssuerCredentialIssuanceFormat] + /// format, and the [IssuerCredentialIssuanceFormat] MUST support receiving standalone requests + /// for this function to succeed. Some formats (such as hlindy or anoncreds) do not + /// support this. + pub fn from_request(request: RequestCredentialV2) -> VcxResult { + if !T::supports_request_independent_of_offer() { + return Err(AriesVcxError::from_msg( + AriesVcxErrorKind::ActionNotSupported, + "Receiving a request independent of an offer is unsupported for this format", + )); + } + + let thread_id = get_thread_id_or_message_id!(request); + + let new_state = RequestReceived::new(None, request); + + Ok(Self { + state: new_state, + thread_id, + }) + } + + /// Prepare a credential message in response to a received request. The prepared credential will + /// be in the [IssuerCredentialIssuanceFormat] format, and will be created using the associated + /// input data. + /// + /// Additionally other flags can be attached to the prepared message for the holder. Notably: + /// * `please_ack` - whether the holder should acknowledge that they receive the credential + /// * `replacement_id` - a unique ID which can be used across credential issuances to indicate + /// that this credential should effectively 'replace' the last credential that this issuer + /// issued to them with the same `replacement_id`. + /// + /// In the event of failure, an error is returned which contains the reason for failure + /// and the state machine before any transitions. Consumers should decide whether the failure + /// is terminal, in which case they should prepare a problem report. + pub async fn prepare_credential( + self, + input_data: &T::CreateCredentialInput, + please_ack: Option, // defaults to false + replacement_id: Option, + ) -> VcxSMTransitionResult<(IssuerV2>, IssueCredentialV2), Self> { + let request = self.state.get_request(); + + let res = match self.state.get_from_offer_metadata() { + Some(offer) => { + T::create_credential_attachment_content(offer, request, input_data).await + } + None => { + T::create_credential_attachment_content_independent_of_offer(request, input_data) + .await + } + }; + + let (attachment_data, cred_metadata) = match res { + Ok(data) => data, + Err(error) => { + return Err(RecoveredSMError { + error, + state_machine: self, + }) + } + }; + let attachments_format_and_data = + vec![(T::get_credential_attachment_format(), attachment_data)]; + + let please_ack = please_ack.unwrap_or(false); + let credential = create_credential_message_from_attachments( + attachments_format_and_data, + please_ack, + self.thread_id.clone(), + replacement_id, + ); + + let new_state = + CredentialPrepared::new(self.state.into_parts().0, cred_metadata, please_ack); + + let issuer = IssuerV2 { + state: new_state, + thread_id: self.thread_id, + }; + + Ok((issuer, credential)) + } +} + +impl IssuerV2> { + /// Get details about the credential that was prepared. + /// The details are specific to the [IssuerCredentialIssuanceFormat] being used. + pub fn get_credential_creation_metadata(&self) -> &T::CreatedCredentialMetadata { + self.state.get_credential_metadata() + } + + /// Whether or not this [IssuerV2] is expecting an Ack message to complete. + pub fn is_expecting_ack(&self) -> bool { + self.state.get_please_ack() + } + + /// Transition into a completed state without receiving an ack message from the holder. + /// + /// In the case where the [IssuerV2] was expecting an ack, this method will fail. + /// + /// In the event of failure, an error is returned which contains the reason for failure + /// and the state machine before any transitions. Consumers should decide whether the failure + /// is terminal, in which case they should prepare a problem report. + pub fn complete_without_ack(self) -> VcxSMTransitionResult>, Self> { + if self.is_expecting_ack() { + return Err(RecoveredSMError { + error: AriesVcxError::from_msg( + AriesVcxErrorKind::ActionNotSupported, + "Cannot transition until ACK is received", + ), + state_machine: self, + }); + } + + let new_state = Completed::new(None); + + Ok(IssuerV2 { + state: new_state, + thread_id: self.thread_id, + }) + } + + /// Transition into a completed state by receiving an incoming ack message from the holder. + /// + /// In the event of failure, an error is returned which contains the reason for failure + /// and the state machine before any transitions. Consumers should decide whether the failure + /// is terminal, in which case they should prepare a problem report. + pub fn complete_with_ack( + self, + ack: AckCredentialV2, + ) -> VcxSMTransitionResult>, Self> { + let is_match = matches_thread_id!(ack, self.thread_id.as_str()); + if !is_match { + return Err(RecoveredSMError { + error: unmatched_thread_id_error(ack.into(), &self.thread_id), + state_machine: self, + }); + } + + let new_state = Completed::new(Some(ack)); + + Ok(IssuerV2 { + state: new_state, + thread_id: self.thread_id, + }) + } +} + +impl IssuerV2 { + /// Get the prepared [CredIssuanceProblemReportV2] to be sent to the holder to report a failure. + pub fn get_problem_report(&self) -> &CredIssuanceProblemReportV2 { + &self.state.get_problem_report() + } +} + +impl IssuerV2 { + /// Transition into the [Failed] state by preparing a problem report message for the holder. + /// The problem report message is generated by using details from the provided [Error]. + pub fn prepare_problem_report_with_error(self, err: &E) -> IssuerV2 + where + E: Error, + { + let content = ProblemReportContent::builder() + .description(Description::builder().code(err.to_string()).build()) + .build(); + + let decorators = ProblemReportDecorators::builder() + .thread(Thread::builder().thid(self.thread_id.clone()).build()) + .build(); + + let report = CredIssuanceProblemReportV2::builder() + .id(Uuid::new_v4().to_string()) + .content(content) + .decorators(decorators) + .build(); + + let new_state = Failed::new(report); + + IssuerV2 { + state: new_state, + thread_id: self.thread_id, + } + } +} diff --git a/aries_vcx/src/protocols/issuance_v2/state_machines/issuer/states/completed.rs b/aries_vcx/src/protocols/issuance_v2/state_machines/issuer/states/completed.rs new file mode 100644 index 0000000000..114b767519 --- /dev/null +++ b/aries_vcx/src/protocols/issuance_v2/state_machines/issuer/states/completed.rs @@ -0,0 +1,23 @@ +use std::marker::PhantomData; + +use messages::msg_fields::protocols::cred_issuance::v2::ack::AckCredentialV2; + +use crate::protocols::issuance_v2::formats::issuer::IssuerCredentialIssuanceFormat; + +pub struct Completed { + ack: Option, + _marker: PhantomData, +} + +impl Completed { + pub fn new(ack: Option) -> Self { + Self { + ack, + _marker: PhantomData, + } + } + + pub fn get_ack(&self) -> Option<&AckCredentialV2> { + self.ack.as_ref() + } +} diff --git a/aries_vcx/src/protocols/issuance_v2/state_machines/issuer/states/credential_prepared.rs b/aries_vcx/src/protocols/issuance_v2/state_machines/issuer/states/credential_prepared.rs new file mode 100644 index 0000000000..3573a6db39 --- /dev/null +++ b/aries_vcx/src/protocols/issuance_v2/state_machines/issuer/states/credential_prepared.rs @@ -0,0 +1,33 @@ +use crate::protocols::issuance_v2::formats::issuer::IssuerCredentialIssuanceFormat; + +pub struct CredentialPrepared { + from_offer_metadata: Option, + credential_metadata: T::CreatedCredentialMetadata, + please_ack: bool, +} + +impl CredentialPrepared { + pub fn new( + from_offer_metadata: Option, + credential_metadata: T::CreatedCredentialMetadata, + please_ack: bool, + ) -> Self { + Self { + from_offer_metadata, + credential_metadata, + please_ack, + } + } + + pub fn get_from_offer_metadata(&self) -> Option<&T::CreatedOfferMetadata> { + self.from_offer_metadata.as_ref() + } + + pub fn get_credential_metadata(&self) -> &T::CreatedCredentialMetadata { + &self.credential_metadata + } + + pub fn get_please_ack(&self) -> bool { + self.please_ack + } +} diff --git a/aries_vcx/src/protocols/issuance_v2/state_machines/issuer/states/failed.rs b/aries_vcx/src/protocols/issuance_v2/state_machines/issuer/states/failed.rs new file mode 100644 index 0000000000..a6790f48fb --- /dev/null +++ b/aries_vcx/src/protocols/issuance_v2/state_machines/issuer/states/failed.rs @@ -0,0 +1,15 @@ +use messages::msg_fields::protocols::cred_issuance::v2::problem_report::CredIssuanceProblemReportV2; + +pub struct Failed { + problem_report: CredIssuanceProblemReportV2, +} + +impl Failed { + pub fn new(problem_report: CredIssuanceProblemReportV2) -> Self { + Self { problem_report } + } + + pub fn get_problem_report(&self) -> &CredIssuanceProblemReportV2 { + &self.problem_report + } +} diff --git a/aries_vcx/src/protocols/issuance_v2/state_machines/issuer/states/mod.rs b/aries_vcx/src/protocols/issuance_v2/state_machines/issuer/states/mod.rs new file mode 100644 index 0000000000..fdfb38a1e2 --- /dev/null +++ b/aries_vcx/src/protocols/issuance_v2/state_machines/issuer/states/mod.rs @@ -0,0 +1,6 @@ +pub mod completed; +pub mod credential_prepared; +pub mod failed; +pub mod offer_prepared; +pub mod proposal_received; +pub mod request_received; diff --git a/aries_vcx/src/protocols/issuance_v2/state_machines/issuer/states/offer_prepared.rs b/aries_vcx/src/protocols/issuance_v2/state_machines/issuer/states/offer_prepared.rs new file mode 100644 index 0000000000..75c9c64ed1 --- /dev/null +++ b/aries_vcx/src/protocols/issuance_v2/state_machines/issuer/states/offer_prepared.rs @@ -0,0 +1,19 @@ +use crate::protocols::issuance_v2::formats::issuer::IssuerCredentialIssuanceFormat; + +pub struct OfferPrepared { + offer_metadata: T::CreatedOfferMetadata, +} + +impl OfferPrepared { + pub fn new(offer_metadata: T::CreatedOfferMetadata) -> Self { + Self { offer_metadata } + } + + pub fn into_parts(self) -> T::CreatedOfferMetadata { + self.offer_metadata + } + + pub fn get_offer_metadata(&self) -> &T::CreatedOfferMetadata { + &self.offer_metadata + } +} diff --git a/aries_vcx/src/protocols/issuance_v2/state_machines/issuer/states/proposal_received.rs b/aries_vcx/src/protocols/issuance_v2/state_machines/issuer/states/proposal_received.rs new file mode 100644 index 0000000000..acea2176f4 --- /dev/null +++ b/aries_vcx/src/protocols/issuance_v2/state_machines/issuer/states/proposal_received.rs @@ -0,0 +1,23 @@ +use std::marker::PhantomData; + +use messages::msg_fields::protocols::cred_issuance::v2::propose_credential::ProposeCredentialV2; + +use crate::protocols::issuance_v2::formats::issuer::IssuerCredentialIssuanceFormat; + +pub struct ProposalReceived { + proposal: ProposeCredentialV2, + _marker: PhantomData, +} + +impl ProposalReceived { + pub fn new(proposal: ProposeCredentialV2) -> Self { + Self { + proposal, + _marker: PhantomData, + } + } + + pub fn get_proposal(&self) -> &ProposeCredentialV2 { + &self.proposal + } +} diff --git a/aries_vcx/src/protocols/issuance_v2/state_machines/issuer/states/request_received.rs b/aries_vcx/src/protocols/issuance_v2/state_machines/issuer/states/request_received.rs new file mode 100644 index 0000000000..cc21faae06 --- /dev/null +++ b/aries_vcx/src/protocols/issuance_v2/state_machines/issuer/states/request_received.rs @@ -0,0 +1,32 @@ +use messages::msg_fields::protocols::cred_issuance::v2::request_credential::RequestCredentialV2; + +use crate::protocols::issuance_v2::formats::issuer::IssuerCredentialIssuanceFormat; + +pub struct RequestReceived { + from_offer_metadata: Option, + request: RequestCredentialV2, +} + +impl RequestReceived { + pub fn new( + from_offer_metadata: Option, + request: RequestCredentialV2, + ) -> Self { + Self { + from_offer_metadata, + request, + } + } + + pub fn into_parts(self) -> (Option, RequestCredentialV2) { + (self.from_offer_metadata, self.request) + } + + pub fn get_from_offer_metadata(&self) -> Option<&T::CreatedOfferMetadata> { + self.from_offer_metadata.as_ref() + } + + pub fn get_request(&self) -> &RequestCredentialV2 { + &self.request + } +} diff --git a/aries_vcx/src/protocols/issuance_v2/state_machines/mod.rs b/aries_vcx/src/protocols/issuance_v2/state_machines/mod.rs new file mode 100644 index 0000000000..edadb99165 --- /dev/null +++ b/aries_vcx/src/protocols/issuance_v2/state_machines/mod.rs @@ -0,0 +1,2 @@ +pub mod holder; +pub mod issuer; diff --git a/aries_vcx/src/protocols/mod.rs b/aries_vcx/src/protocols/mod.rs index 9cc569363f..853ba26f27 100644 --- a/aries_vcx/src/protocols/mod.rs +++ b/aries_vcx/src/protocols/mod.rs @@ -7,6 +7,7 @@ use crate::errors::error::VcxResult; pub mod common; pub mod connection; pub mod issuance; +pub mod issuance_v2; pub mod mediated_connection; pub mod oob; pub mod proof_presentation; diff --git a/aries_vcx/src/utils/openssl.rs b/aries_vcx/src/utils/openssl.rs index 904d9acab7..10865562d9 100644 --- a/aries_vcx/src/utils/openssl.rs +++ b/aries_vcx/src/utils/openssl.rs @@ -3,6 +3,7 @@ use sha2::{Digest, Sha256}; use crate::errors::error::prelude::*; +// TODO - does not have to be a result... pub fn encode(s: &str) -> VcxResult { match s.parse::() { Ok(val) => Ok(val.to_string()), diff --git a/aries_vcx/tests/test_credential_issuance_v2.rs b/aries_vcx/tests/test_credential_issuance_v2.rs new file mode 100644 index 0000000000..fa4b78df74 --- /dev/null +++ b/aries_vcx/tests/test_credential_issuance_v2.rs @@ -0,0 +1,565 @@ +use std::{collections::HashMap, sync::Arc, time::Duration}; + +use agency_client::httpclient::post_message; +use aries_vcx::{ + common::test_utils::{create_and_write_test_cred_def, create_and_write_test_schema}, + core::profile::{ledger::VcxPoolConfig, modular_libs_profile::ModularLibsProfile, Profile}, + errors::error::VcxResult, + global::settings, + protocols::{ + connection::Connection, + issuance_v2::{ + formats::{ + holder::{ + hyperledger_indy::{ + HyperledgerIndyCreateProposalInput, HyperledgerIndyCreateRequestInput, + HyperledgerIndyCredentialFilterBuilder, + HyperledgerIndyHolderCredentialIssuanceFormat, + HyperledgerIndyStoreCredentialInput, + }, + HolderCredentialIssuanceFormat, + }, + issuer::{ + hyperledger_indy::{ + HyperledgerIndyCreateCredentialInput, HyperledgerIndyCreateOfferInput, + HyperledgerIndyIssuerCredentialIssuanceFormat, + }, + IssuerCredentialIssuanceFormat, + }, + }, + processing::{ + holder::create_request_message_from_attachments, + issuer::{ + create_credential_message_from_attachments, + create_offer_message_from_attachments, + }, + }, + state_machines::{ + holder::{ + states::{offer_received::OfferReceived, proposal_prepared::ProposalPrepared}, + HolderV2, + }, + issuer::{states::proposal_received::ProposalReceived, IssuerV2}, + }, + }, + mediated_connection::pairwise_info::PairwiseInfo, + }, + run_setup, + transport::Transport, + utils::mockdata::profile::{mock_anoncreds::MockAnoncreds, mock_ledger::MockLedger}, +}; +use aries_vcx_core::{ + anoncreds::base_anoncreds::BaseAnonCreds, + wallet::{ + base_wallet::BaseWallet, + indy::{wallet::create_and_open_wallet, IndySdkWallet, WalletConfigBuilder}, + }, +}; +use async_trait::async_trait; +use messages::msg_fields::protocols::{ + connection::response::Response, + cred_issuance::{ + common::CredentialAttr, + v2::{ + issue_credential::IssueCredentialV2, offer_credential::OfferCredentialV2, + CredentialPreviewV2, + }, + }, +}; +use serde::de::DeserializeOwned; +use serde_json::{json, Value}; +use url::Url; + +type HLIndyIssuerFormat<'a, A> = HyperledgerIndyIssuerCredentialIssuanceFormat<'a, A>; +type HLIndyHolderFormat<'a, R, A> = HyperledgerIndyHolderCredentialIssuanceFormat<'a, R, A>; + +#[tokio::test] +#[ignore] +async fn test_hlindy_credential_issuance_v2_with_processing_functions() { + run_setup!(|setup| async move { + let anoncreds = setup.profile.anoncreds(); + let ledger_read = setup.profile.ledger_read(); + + let schema = create_and_write_test_schema( + anoncreds, + setup.profile.ledger_write(), + &setup.institution_did, + aries_vcx::utils::constants::DEFAULT_SCHEMA_ATTRS, + ) + .await; + let cred_def = create_and_write_test_cred_def( + anoncreds, + ledger_read, + setup.profile.ledger_write(), + &setup.institution_did, + &schema.schema_id, + false, + ) + .await; + + let offer_preview = CredentialPreviewV2::new(vec![ + CredentialAttr::builder() + .name(String::from("address1")) + .value(String::from("123 Main St")) + .build(), + CredentialAttr::builder() + .name(String::from("address2")) + .value(String::from("Suite 3")) + .build(), + CredentialAttr::builder() + .name(String::from("city")) + .value(String::from("Draper")) + .build(), + CredentialAttr::builder() + .name(String::from("state")) + .value(String::from("UT")) + .build(), + CredentialAttr::builder() + .name(String::from("zip")) + .value(String::from("84000")) + .build(), + ]); + + // TODO - how bad is it bad that this requires an impl generic? IndyCredxAnonCreds + let attachment_format = HLIndyIssuerFormat::::get_offer_attachment_format(); + let (attachment_data, offer_metadata) = + HLIndyIssuerFormat::create_offer_attachment_content(&HyperledgerIndyCreateOfferInput { + anoncreds, + cred_def_id: cred_def.get_cred_def_id(), + }) + .await + .unwrap(); + let attachments_format_and_data = vec![(attachment_format, attachment_data)]; + + let offer_msg = create_offer_message_from_attachments( + attachments_format_and_data, + offer_preview, + None, + None, + ); + + let thread_id = offer_msg + .decorators + .thread + .as_ref() + .map(|th| th.thid.clone()) + .unwrap_or(offer_msg.id.clone()); + + let offer_details = + HLIndyHolderFormat::::extract_offer_details(&offer_msg) + .unwrap(); + assert_eq!(offer_details.schema_id, cred_def.get_schema_id()); + assert_eq!(offer_details.cred_def_id, cred_def.get_cred_def_id()); + + let attachment_format = + HLIndyHolderFormat::::get_request_attachment_format(); + let (request_attach, request_metadata) = + HLIndyHolderFormat::<_, _>::create_request_attachment_content( + &offer_msg, + &HyperledgerIndyCreateRequestInput { + entropy_did: setup.institution_did, // not realistic + ledger: ledger_read, + anoncreds, + }, + ) + .await + .unwrap(); + let attachments_format_and_data = vec![(attachment_format, request_attach)]; + let request_msg = create_request_message_from_attachments( + attachments_format_and_data, + Some(thread_id.clone()), + ); + + let attachment_format = + HLIndyIssuerFormat::::get_credential_attachment_format(); + let (attachment_data, created_cred_metadata) = + HLIndyIssuerFormat::<_>::create_credential_attachment_content( + &offer_metadata, + &request_msg, + &HyperledgerIndyCreateCredentialInput { + anoncreds, + credential_attributes: HashMap::from([ + (String::from("address1"), String::from("123 Main St")), + (String::from("address2"), String::from("Suite 3")), + (String::from("city"), String::from("Draper")), + (String::from("state"), String::from("UT")), + (String::from("zip"), String::from("84000")), + ]), + revocation_info: None, + }, + ) + .await + .unwrap(); + let attachments_format_and_data = vec![(attachment_format, attachment_data)]; + let cred_msg = create_credential_message_from_attachments( + attachments_format_and_data, + false, + thread_id.clone(), + None, + ); + + let stored_cred_metadata = HLIndyHolderFormat::<_, _>::process_and_store_credential( + &cred_msg, + &HyperledgerIndyStoreCredentialInput { + ledger: ledger_read, + anoncreds, + }, + &request_metadata, + ) + .await + .unwrap(); + + assert!(created_cred_metadata.credential_revocation_id.is_none()); + assert!(!stored_cred_metadata.credential_id.is_empty()); + + let stored_cred = anoncreds + .prover_get_credential(&stored_cred_metadata.credential_id) + .await + .unwrap(); + + assert!(!stored_cred.is_empty()); + }) + .await; +} + +#[tokio::test] +#[ignore] +async fn test_hlindy_non_revocable_credential_issuance_v2_from_proposal() { + run_setup!(|setup| async move { + let anoncreds = setup.profile.anoncreds(); + let ledger_read = setup.profile.ledger_read(); + + let schema = create_and_write_test_schema( + anoncreds, + setup.profile.ledger_write(), + &setup.institution_did, + aries_vcx::utils::constants::DEFAULT_SCHEMA_ATTRS, + ) + .await; + let cred_def = create_and_write_test_cred_def( + anoncreds, + ledger_read, + setup.profile.ledger_write(), + &setup.institution_did, + &schema.schema_id, + false, + ) + .await; + + let proposal_input = HyperledgerIndyCreateProposalInput { + cred_filter: HyperledgerIndyCredentialFilterBuilder::default() + .cred_def_id(cred_def.get_cred_def_id()) + .build() + .unwrap(), + }; + let proposal_preview = CredentialPreviewV2::new(vec![CredentialAttr::builder() + .name(String::from("address")) + .value(String::from("123 Main St")) + .build()]); + + let (holder, proposal_msg) = HolderV2::< + ProposalPrepared>, + >::with_proposal( + &proposal_input, Some(proposal_preview.clone()) + ) + .await + .unwrap(); + + let issuer = IssuerV2::< + ProposalReceived>, + >::from_proposal(proposal_msg); + + // issuer checks details of the proposal + let (received_filter, received_proposal_preview) = issuer.get_proposal_details().unwrap(); + assert_eq!(received_filter, proposal_input.cred_filter); + assert_eq!(received_proposal_preview.unwrap(), &proposal_preview); + + let offer_data = HyperledgerIndyCreateOfferInput { + anoncreds: anoncreds, + cred_def_id: cred_def.get_cred_def_id(), + }; + let offer_preview = CredentialPreviewV2::new(vec![ + CredentialAttr::builder() + .name(String::from("address1")) + .value(String::from("123 Main St")) + .build(), + CredentialAttr::builder() + .name(String::from("address2")) + .value(String::from("Suite 3")) + .build(), + CredentialAttr::builder() + .name(String::from("city")) + .value(String::from("Draper")) + .build(), + CredentialAttr::builder() + .name(String::from("state")) + .value(String::from("UT")) + .build(), + CredentialAttr::builder() + .name(String::from("zip")) + .value(String::from("84000")) + .build(), + ]); + let (issuer, offer_msg) = issuer + .prepare_offer(&offer_data, offer_preview.clone(), None) + .await + .unwrap(); + + let holder = holder.receive_offer(offer_msg).unwrap(); + + // holder checks details of the offer + let (received_offer_details, received_offer_preview) = holder.get_offer_details().unwrap(); + assert_eq!( + received_offer_details.cred_def_id, + cred_def.get_cred_def_id() + ); + assert_eq!(received_offer_details.schema_id, cred_def.get_schema_id()); + assert_eq!(received_offer_preview, &offer_preview); + + // usually this would be the DID from the connection, but does not really matter + let pw = PairwiseInfo::create(setup.profile.wallet()).await.unwrap(); + let request_input = HyperledgerIndyCreateRequestInput { + entropy_did: pw.pw_did, + ledger: ledger_read, + anoncreds: anoncreds, + }; + + let (holder, request_msg) = holder + .prepare_credential_request(&request_input) + .await + .unwrap(); + + let issuer = issuer.receive_request(request_msg).unwrap(); + + let cred_data = HyperledgerIndyCreateCredentialInput { + anoncreds: anoncreds, + credential_attributes: HashMap::from([ + (String::from("address1"), String::from("123 Main St")), + (String::from("address2"), String::from("Suite 3")), + (String::from("city"), String::from("Draper")), + (String::from("state"), String::from("UT")), + (String::from("zip"), String::from("84000")), + ]), + revocation_info: None, + }; + + let (issuer, cred_msg) = issuer + .prepare_credential(&cred_data, Some(true), None) + .await + .unwrap(); + let issuer_cred_metadata = issuer.get_credential_creation_metadata().clone(); + + let receive_input = HyperledgerIndyStoreCredentialInput { + ledger: ledger_read, + anoncreds: anoncreds, + }; + + let holder = holder + .receive_credential(cred_msg, &receive_input) + .await + .unwrap(); + let holder_cred_metadata = holder.get_stored_credential_metadata().clone(); + let (_holder, ack_msg) = holder.prepare_ack_if_required(); + + let _issuer = issuer.complete_with_ack(ack_msg.unwrap()); + + // check final states + assert!(issuer_cred_metadata.credential_revocation_id.is_none()); + + let holder_cred_id = holder_cred_metadata.credential_id; + let cred = anoncreds + .prover_get_credential(&holder_cred_id) + .await + .unwrap(); + assert!(!cred.is_empty()); + }) + .await +} + +// TODO -DELETE BELOW +#[tokio::test] +#[ignore] +async fn manual_test_holder_against_acapy() { + let relay_external_endpoint = + String::from("https://fa5b-203-123-120-210.ngrok-free.app/send_user_message/user-123"); + let relay_internal_endpoint = + String::from("https://fa5b-203-123-120-210.ngrok-free.app/pop_user_message/user-123"); + + fn fix_malformed_thread_decorator(msg: &mut Value) { + // remove thread decorator if it is empty (acapy sends it empty) + let Some(thread) = msg.get_mut("~thread") else { + return; + }; + + if thread.as_object().unwrap().is_empty() { + thread.take(); + } + } + + async fn get_next_aries_msg( + relay: &str, + wallet: &impl BaseWallet, + ) -> VcxResult { + let enc_bytes = reqwest::get(relay) + .await + .unwrap() + .bytes() + .await + .unwrap() + .to_vec(); + + let unpacked = wallet.unpack_message(&enc_bytes).await?; + let mut msg = serde_json::from_str(&unpacked.message)?; + fix_malformed_thread_decorator(&mut msg); + Ok(serde_json::from_value(msg)?) + } + + async fn await_next_aries_msg(relay: &str, wallet: &impl BaseWallet) -> T { + loop { + match get_next_aries_msg(relay, wallet).await { + Ok(data) => return data, + Err(e) => println!("failed to fetch msg, trying again: {e:?}"), + } + + std::thread::sleep(Duration::from_millis(500)) + } + } + + let config_wallet = WalletConfigBuilder::default() + .wallet_name("wallet1") + .wallet_key(settings::DEFAULT_WALLET_KEY) + .wallet_key_derivation(settings::WALLET_KDF_RAW) + .build() + .unwrap(); + + let wh = create_and_open_wallet(&config_wallet).await.unwrap(); + let wallet = Arc::new(IndySdkWallet::new(wh)); + let vcx_pool_config = VcxPoolConfig { + genesis_file_path: String::from("/Users/gmulhearne/Documents/dev/rust/aries-vcx/testnet"), + indy_vdr_config: None, + response_cache_config: None, + }; + let profile = ModularLibsProfile::init(wallet, vcx_pool_config).unwrap(); + let wallet = profile.wallet(); + let indy_read = profile.ledger_read(); + let anoncreds_read = profile.ledger_read(); + let anoncreds = profile.anoncreds(); + + anoncreds + .prover_create_link_secret(settings::DEFAULT_LINK_SECRET_ALIAS) + .await + .ok(); + + let pairwise_info = PairwiseInfo::create(wallet).await.unwrap(); + let inviter = Connection::new_invitee(String::from("Mr Vcx"), pairwise_info.clone()); + + // acccept invite + let invitation_json = json!( + { + "@type": "did:sov:BzCbsNYhMrjHiqZDTUASHg;spec/connections/1.0/invitation", + "@id": "ade68e30-6880-47e7-9dae-b5588e41b815", + "label": "Bob3", + "recipientKeys": [ + "Ab2L1WaK5rhTqZFCb2RHyjQjVzygf6xAo3jAayH1r8XM" + ], + "serviceEndpoint": "http://localhost:8200" + } + ); + let invitation = serde_json::from_value(invitation_json).unwrap(); + + let inviter = inviter + .accept_invitation(indy_read, invitation) + .await + .unwrap(); + + let inviter = inviter + .prepare_request(relay_external_endpoint.parse().unwrap(), vec![]) + .await + .unwrap(); + let request_msg = inviter.get_request().clone(); + inviter + .send_message(wallet, &request_msg.into(), &HttpClient) + .await + .unwrap(); + + // get and accept response + let response = await_next_aries_msg::(&relay_internal_endpoint, wallet).await; + let conn = inviter + .handle_response(wallet, response.try_into().unwrap()) + .await + .unwrap(); + + // send back an ack + conn.send_message(wallet, &conn.get_ack().into(), &HttpClient) + .await + .unwrap(); + + println!("CONN ESTABLISHED"); + + // start the credential fun :) + + // get offer + println!("WAITING FOR CRED OFFER, GO DO IT"); + + let offer = await_next_aries_msg::(&relay_internal_endpoint, wallet).await; + println!("{offer:?}"); + println!("{}", serde_json::to_string(&offer).unwrap()); + + let holder = + HolderV2::>>::from_offer( + offer, + ); + + println!("{:?}", holder.get_offer_details().unwrap()); + + // send request + + let (holder, msg) = holder + .prepare_credential_request(&HyperledgerIndyCreateRequestInput { + entropy_did: pairwise_info.pw_did, + ledger: anoncreds_read, + anoncreds: anoncreds, + }) + .await + .unwrap(); + + conn.send_message(wallet, &msg.into(), &HttpClient) + .await + .unwrap(); + + // get cred + let cred = await_next_aries_msg::(&relay_internal_endpoint, wallet).await; + println!("{cred:?}"); + println!("{}", serde_json::to_string(&cred).unwrap()); + + let holder = holder + .receive_credential( + cred, + &HyperledgerIndyStoreCredentialInput { + ledger: anoncreds_read, + anoncreds: anoncreds, + }, + ) + .await + .unwrap(); + + println!("{:?}", holder.get_stored_credential_metadata()); + + // check cred made in wallet! + let stored_cred = anoncreds + .prover_get_credential(&holder.get_stored_credential_metadata().credential_id) + .await + .unwrap(); + + println!("{stored_cred}"); +} + +// TODO - DELETE ME, for acapy test +pub struct HttpClient; +#[async_trait] +impl Transport for HttpClient { + async fn send_message(&self, msg: Vec, service_endpoint: Url) -> VcxResult<()> { + post_message(msg, service_endpoint).await?; + Ok(()) + } +} diff --git a/messages/src/msg_fields/protocols/cred_issuance/v1/mod.rs b/messages/src/msg_fields/protocols/cred_issuance/v1/mod.rs index a85709a63d..a46f6676a7 100644 --- a/messages/src/msg_fields/protocols/cred_issuance/v1/mod.rs +++ b/messages/src/msg_fields/protocols/cred_issuance/v1/mod.rs @@ -177,24 +177,33 @@ impl Serialize for CredentialPreviewV1MsgType { transit_to_aries_msg!( OfferCredentialV1Content: OfferCredentialV1Decorators, - CredentialIssuanceV1, CredentialIssuance + CredentialIssuanceV1, + CredentialIssuance ); transit_to_aries_msg!( ProposeCredentialV1Content: ProposeCredentialV1Decorators, - CredentialIssuanceV1, CredentialIssuance + CredentialIssuanceV1, + CredentialIssuance ); transit_to_aries_msg!( RequestCredentialV1Content: RequestCredentialV1Decorators, - CredentialIssuanceV1, CredentialIssuance + CredentialIssuanceV1, + CredentialIssuance ); transit_to_aries_msg!( IssueCredentialV1Content: IssueCredentialV1Decorators, - CredentialIssuanceV1, CredentialIssuance + CredentialIssuanceV1, + CredentialIssuance +); +transit_to_aries_msg!( + AckCredentialV1Content: AckDecorators, + CredentialIssuanceV1, + CredentialIssuance ); -transit_to_aries_msg!(AckCredentialV1Content: AckDecorators, CredentialIssuanceV1, CredentialIssuance); transit_to_aries_msg!( CredIssuanceV1ProblemReportContent: ProblemReportDecorators, - CredentialIssuanceV1, CredentialIssuance + CredentialIssuanceV1, + CredentialIssuance ); into_msg_with_type!( diff --git a/messages/src/msg_fields/protocols/cred_issuance/v2/mod.rs b/messages/src/msg_fields/protocols/cred_issuance/v2/mod.rs index 8f9d100c62..a5e6e55500 100644 --- a/messages/src/msg_fields/protocols/cred_issuance/v2/mod.rs +++ b/messages/src/msg_fields/protocols/cred_issuance/v2/mod.rs @@ -178,30 +178,39 @@ impl Serialize for CredentialPreviewV2MsgType { #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, TypedBuilder)] #[serde(rename_all = "snake_case")] pub struct AttachmentFormatSpecifier { - attach_id: String, - format: MaybeKnown, + pub attach_id: String, + pub format: MaybeKnown, } transit_to_aries_msg!( OfferCredentialV2Content: OfferCredentialV2Decorators, - CredentialIssuanceV2, CredentialIssuance + CredentialIssuanceV2, + CredentialIssuance ); transit_to_aries_msg!( ProposeCredentialV2Content: ProposeCredentialV2Decorators, - CredentialIssuanceV2, CredentialIssuance + CredentialIssuanceV2, + CredentialIssuance ); transit_to_aries_msg!( RequestCredentialV2Content: RequestCredentialV2Decorators, - CredentialIssuanceV2, CredentialIssuance + CredentialIssuanceV2, + CredentialIssuance ); transit_to_aries_msg!( IssueCredentialV2Content: IssueCredentialV2Decorators, - CredentialIssuanceV2, CredentialIssuance + CredentialIssuanceV2, + CredentialIssuance +); +transit_to_aries_msg!( + AckCredentialV2Content: AckDecorators, + CredentialIssuanceV2, + CredentialIssuance ); -transit_to_aries_msg!(AckCredentialV2Content: AckDecorators, CredentialIssuanceV2, CredentialIssuance); transit_to_aries_msg!( CredIssuanceV2ProblemReportContent: ProblemReportDecorators, - CredentialIssuanceV2, CredentialIssuance + CredentialIssuanceV2, + CredentialIssuance ); into_msg_with_type!( diff --git a/testnet b/testnet new file mode 100644 index 0000000000..eb4a6ecae4 --- /dev/null +++ b/testnet @@ -0,0 +1,7 @@ +{"reqSignature":{},"txn":{"data":{"data":{"alias":"OpsNode","blskey":"4i39oJqm7fVX33gnYEbFdGurMtwYQJgDEYfXdYykpbJMWogByocaXxKbuXdrg3k9LP33Tamq64gUwnm4oA7FkxqJ5h4WfKH6qyVLvmBu5HgeV8Rm1GJ33mKX6LWPbm1XE9TfzpQXJegKyxHQN9ABquyBVAsfC6NSM4J5t1QGraJBfZi","blskey_pop":"Qq3CzhSfugsCJotxSCRAnPjmNDJidDz7Ra8e4xvLTEzQ5w3ppGray9KynbGPH8T7XnUTU1ioZadTbjXaRY26xd4hQ3DxAyR4GqBymBn3UBomLRJHmj7ukcdJf9WE6tu1Fp1EhxmyaMqHv13KkDrDfCthgd2JjAWvSgMGWwAAzXEow5","client_ip":"13.58.197.208","client_port":"9702","node_ip":"3.135.134.42","node_port":"9701","services":["VALIDATOR"]},"dest":"EVwxHoKXUy2rnRzVdVKnJGWFviamxMwLvUso7KMjjQNH"},"metadata":{"from":"Pms5AZzgPWHSj6nNmJDfmo"},"type":"0"},"txnMetadata":{"seqNo":1,"txnId":"77ad6682f320be9969f70a37d712344afed8e3fba8d43fa5602c81b578d26088"},"ver":"1"} +{"reqSignature":{},"txn":{"data":{"data":{"alias":"cynjanode","blskey":"32DLSweyJRxVMcVKGjUeNkVF1fwyFfRcFqGU9x7qL2ox2STpF6VxZkbxoLkGMPnt3gywRaY6jAjqgC8XMkf3webMJ4SEViPtBKZJjCCFTf4tGXfEsMwinummaPja85GgTALf7DddCNyCojmkXWHpgjrLx3626Z2MiNxVbaMapG2taFX","blskey_pop":"RQRU8GVYSYZeu9dfH6myhzZ2qfxeVpCL3bTzgto1bRbx3QCt3mFFQQBVbgrqui2JpXhcWXxoDzp1WyYbSZwYqYQbRmvK7PPG82VAvVagv1n83Qa3cdyGwCevZdEzxuETiiXBRWSPfb4JibAXPKkLZHyQHWCEHcAEVeXtx7FRS1wjTd","client_ip":"3.17.103.221","client_port":"9702","node_ip":"3.17.215.226","node_port":"9701","services":["VALIDATOR"]},"dest":"iTq944JTtwHnst7rucfsRA4m26x9i6zCKKohETBCiWu"},"metadata":{"from":"QC174PGaL4zA9YHYqofPH2"},"type":"0"},"txnMetadata":{"seqNo":2,"txnId":"ce7361e44ec10a275899ece1574f6e38f2f3c7530c179fa07a2924e55775759b"},"ver":"1"} +{"reqSignature":{},"txn":{"data":{"data":{"alias":"GlobaliD","blskey":"4Behdr1KJfLTAPNospghtL7iWdCHca6MZDxAtzYNXq35QCUr4aqpLu6p4Sgu9wNbTACB3DbwmVgE2L7hX6UsasuvZautqUpf4nC5viFpH7X6mHyqLreBJTBH52tSwifQhRjuFAySbbfyRK3wb6R2Emxun9GY7MFNuy792LXYg4C6sRJ","blskey_pop":"RKYDRy8oTxKnyAV3HocapavH2jkw3PVe54JcEekxXz813DFbEy87N3i3BNqwHB7MH93qhtTRb7EZMaEiYhm92uaLKyubUMo5Rqjve2jbEdYEYVRmgNJWpxFKCmUBa5JwBWYuGunLMZZUTU3qjbdDXkJ9UNMQxDULCPU5gzLTy1B5kb","client_ip":"13.56.175.126","client_port":"9702","node_ip":"50.18.84.131","node_port":"9701","services":["VALIDATOR"]},"dest":"2ErWxamsNGBfhkFnwYgs4UW4aApct1kHUvu7jbkA1xX4"},"metadata":{"from":"4H8us7B1paLW9teANv8nam"},"type":"0"},"txnMetadata":{"seqNo":3,"txnId":"0c3b33b77e0419d6883be35d14b389c3936712c38a469ac5320a3cae68be1293"},"ver":"1"} +{"reqSignature":{},"txn":{"data":{"data":{"alias":"IdRamp","blskey":"LoYzqUMPDZEfRshwGSzkgATxcM5FAS1LYx896zHnMfXP7duDsCQ6CBG2akBkZzgH3tBMvnjhs2z7PFc2gFeaKUF9fKDHhtbVqPofxH3ebcRfA959qU9mgvmkUwMUgwd21puRU6BebUwBiYxMxcE5ChReBnAkdAv19gVorm3prBMk94","blskey_pop":"R1DjpsG7UxgwstuF7WDUL17a9Qq64vCozwJZ88bTrSDPwC1cdRn3WmhqJw5LpEhFQJosDSVVT6tS8dAZrrssRv2YsELbfGEJ7ZGjhNjZHwhqg4qeustZ7PZZE3Vr1ALSHY4Aa6KpNzGodxu1XymYZWXAFokPAs3Kho8mKcJwLCHn3h","client_ip":"207.126.128.12","client_port":"9702","node_ip":"207.126.129.12","node_port":"9701","services":["VALIDATOR"]},"dest":"5Zj5Aec6Kt9ki1runrXu87wZ522mnm3zwmaoHLUcHLx9"},"metadata":{"from":"AFLDFPoJuDQUHqnfmg8U7i"},"type":"0"},"txnMetadata":{"seqNo":4,"txnId":"c9df105558333ac8016610d9da5aad1e9a5dd50b9d9cc5684e94f439fa10f836"},"ver":"1"} +{"reqSignature":{},"txn":{"data":{"data":{"alias":"idlab-node01","blskey":"2fjJVi33U1tCTjW77cJaf1NLz7EzWkVNzR9BEQpVVK64MJpRKNUzt6k7Td2U8yqU5hGyAFH5N7ZymSB55TnpC3rJYLVTcGXZeXpmrQx3mwnXNyfTDnxfTpdQ1KMoFeZoDPZ8acfaH8GWeW2jL1qREE52tetBf4tXTeshmWzGkEN7r4y","blskey_pop":"RSjiM6dYUmN2rv2ca7dUCmEKrivq12rhxhXUKHdmSwUxbCmcijsgoERjYG7MqxhKLjSAJ5715K23fVEc6uK1kTenKmYCcCts8MLMAQG8Upb22nfgHJ3py8RwRoACeAjFF3myAMNRJJPhUdv96drJdwkGRv7f6JjvoB5KWVQYTNgheP","client_ip":"205.159.92.17","client_port":"9702","node_ip":"205.159.92.16","node_port":"9701","services":["VALIDATOR"]},"dest":"8czYgwmLDazVrBHuo53Tyx7Tw8ZhvnoC2BfhQGir4r8F"},"metadata":{"from":"PN8wFxLKjdkwyxoEEXwyz2"},"type":"0"},"txnMetadata":{"seqNo":5,"txnId":"9237eca7d2a203f6e1779f63064d2f22cf28e1bcd4e6fe5d791b15e82969acdc"},"ver":"1"} +{"reqSignature":{},"txn":{"data":{"data":{"alias":"lorica-identity-node1","blskey":"wUh24sVCQ8PHDgSb343g2eLxjD5vwxsrETfuV2sbwMNnYon9nhbaK5jcWTekvXtyiwxHxuiCCoZwKS97MQEAeC2oLbbMeKjYm212QwSnm7aKLEqTStXht35VqZvZLT7Q3mPQRYLjMGixdn4ocNHrBTMwPUQYycEqwaHWgE1ncDueXY","blskey_pop":"R2sMwF7UW6AaD4ALa1uB1YVPuP6JsdJ7LsUoViM9oySFqFt34C1x1tdHDysS9wwruzaaEFui6xNPqJ8eu3UBqcFKkoWhdsMqCALwe63ytxPwvtLtCffJLhHAcgrPC7DorXYdqhdG2cevdqc5oqFEAaKoFDBf12p5SsbbM4PYWCmVCb","client_ip":"35.225.220.151","client_port":"9702","node_ip":"35.224.26.110","node_port":"9701","services":["VALIDATOR"]},"dest":"k74ZsZuUaJEcB8RRxMwkCwdE5g1r9yzA3nx41qvYqYf"},"metadata":{"from":"Ex6hzsJFYzNJ7kzbfncNeU"},"type":"0"},"txnMetadata":{"seqNo":6,"txnId":"6880673ce4ae4a2352f103d2a6ae20469dd070f2027283a1da5e62a64a59d688"},"ver":"1"} +{"reqSignature":{},"txn":{"data":{"data":{"alias":"cysecure-itn","blskey":"GdCvMLkkBYevRFi93b6qaj9G2u1W6Vnbg8QhRD1chhrWR8vRE8x9x7KXVeUBPFf6yW5qq2JCfA2frc8SGni2RwjtTagezfwAwnorLhVJqS5ZxTi4pgcw6smebnt4zWVhTkh6ugDHEypHwNQBcw5WhBZcEJKgNbyVLnHok9ob6cfr3u","blskey_pop":"RbH9mY7M5p3UB3oj4sT1skYwMkxjoUnja8eTYfcm83VcNbxC9zR9pCiRhk4q1dJT3wkDBPGNKnk2p83vaJYLcgMuJtzoWoJAWAxjb3Mcq8Agf6cgQpBuzBq2uCzFPuQCAhDS4Kv9iwA6FsRnfvoeFTs1hhgSJVxQzDWMVTVAD9uCqu","client_ip":"35.169.19.171","client_port":"9702","node_ip":"54.225.56.21","node_port":"9701","services":["VALIDATOR"]},"dest":"4ETBDmHzx8iDQB6Xygmo9nNXtMgq9f6hxGArNhQ6Hh3u"},"metadata":{"from":"uSXXXEdBicPHMMhr3ddNF"},"type":"0"},"txnMetadata":{"seqNo":7,"txnId":"3c21718b07806b2f193b35953dda5b68b288efd551dce4467ce890703d5ba549"},"ver":"1"}