Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

feat: add additional formats for parsing and outputting Out Of Band Invitations #1281

Merged
merged 10 commits into from
Aug 22, 2024
2 changes: 1 addition & 1 deletion aries/agents/aries-vcx-agent/src/handlers/out_of_band.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ impl<T: BaseWallet> ServiceOutOfBand<T> {
GenericOutOfBand::Sender(sender.to_owned()),
)?;

Ok(sender.to_aries_message())
Ok(sender.invitation_to_aries_message())
}

pub fn receive_invitation(&self, invitation: AriesMessage) -> AgentResult<String> {
Expand Down
22 changes: 21 additions & 1 deletion aries/aries_vcx/src/errors/mapping_others.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
use std::{num::ParseIntError, sync::PoisonError};
use std::{num::ParseIntError, string::FromUtf8Error, sync::PoisonError};

use base64::DecodeError;
use did_doc::schema::{types::uri::UriWrapperError, utils::error::DidDocumentLookupError};
use shared::errors::http_error::HttpError;
use url::ParseError;

use crate::{
errors::error::{AriesVcxError, AriesVcxErrorKind},
Expand Down Expand Up @@ -92,3 +94,21 @@ impl From<ParseIntError> for AriesVcxError {
AriesVcxError::from_msg(AriesVcxErrorKind::InvalidInput, err.to_string())
}
}

impl From<DecodeError> for AriesVcxError {
fn from(err: DecodeError) -> Self {
AriesVcxError::from_msg(AriesVcxErrorKind::InvalidInput, err.to_string())
}
}

impl From<FromUtf8Error> for AriesVcxError {
fn from(err: FromUtf8Error) -> Self {
AriesVcxError::from_msg(AriesVcxErrorKind::InvalidInput, err.to_string())
}
}

impl From<ParseError> for AriesVcxError {
fn from(err: ParseError) -> Self {
AriesVcxError::from_msg(AriesVcxErrorKind::InvalidInput, err.to_string())
}
}
214 changes: 208 additions & 6 deletions aries/aries_vcx/src/handlers/out_of_band/receiver.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,11 @@ use messages::{
};
use serde::Deserialize;
use serde_json::Value;
use url::Url;

use crate::{errors::error::prelude::*, handlers::util::AttachmentId};
use crate::{
errors::error::prelude::*, handlers::util::AttachmentId, utils::base64::URL_SAFE_LENIENT,
};

#[derive(Debug, PartialEq, Clone)]
pub struct OutOfBandReceiver {
Expand All @@ -38,6 +41,23 @@ impl OutOfBandReceiver {
}
}

pub fn create_from_json_encoded_oob(oob_json: &str) -> VcxResult<Self> {
Ok(Self {
oob: extract_encoded_invitation_from_json_string(oob_json)?,
})
}

pub fn create_from_url_encoded_oob(oob_url_string: &str) -> VcxResult<Self> {
// TODO - URL Shortening
Ok(Self {
oob: extract_encoded_invitation_from_json_string(
&extract_encoded_invitation_from_base64_url(&extract_encoded_invitation_from_url(
oob_url_string,
)?)?,
)?,
})
}

pub fn get_id(&self) -> String {
self.oob.id.clone()
}
Expand All @@ -58,17 +78,53 @@ impl OutOfBandReceiver {
}
}

pub fn to_aries_message(&self) -> AriesMessage {
pub fn invitation_to_aries_message(&self) -> AriesMessage {
self.oob.clone().into()
}

pub fn from_string(oob_data: &str) -> VcxResult<Self> {
Ok(Self {
oob: serde_json::from_str(oob_data)?,
})
pub fn invitation_to_json_string(&self) -> String {
self.invitation_to_aries_message().to_string()
}

fn invitation_to_base64_url(&self) -> String {
URL_SAFE_LENIENT.encode(self.invitation_to_json_string())
}

pub fn invitation_to_url(&self, domain_path: &str) -> VcxResult<Url> {
let oob_url = Url::parse(domain_path)?
.query_pairs_mut()
.append_pair("oob", &self.invitation_to_base64_url())
.finish()
.to_owned();
Ok(oob_url)
}
}

fn extract_encoded_invitation_from_json_string(oob_json: &str) -> VcxResult<Invitation> {
Ok(serde_json::from_str(oob_json)?)
}

fn extract_encoded_invitation_from_base64_url(base64_url_encoded_oob: &str) -> VcxResult<String> {
Ok(String::from_utf8(
URL_SAFE_LENIENT.decode(base64_url_encoded_oob)?,
JamesKEbert marked this conversation as resolved.
Show resolved Hide resolved
)?)
}

fn extract_encoded_invitation_from_url(oob_url_string: &str) -> VcxResult<String> {
let oob_url = Url::parse(oob_url_string)?;
let (_oob_query, base64_url_encoded_oob) = oob_url
.query_pairs()
.find(|(name, _value)| name == "oob")
.ok_or_else(|| {
AriesVcxError::from_msg(
AriesVcxErrorKind::InvalidInput,
"OutOfBand Invitation URL is missing 'oob' query parameter",
)
})?;

Ok(base64_url_encoded_oob.into_owned())
}

impl Display for OutOfBandReceiver {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", json!(AriesMessage::from(self.oob.clone())))
Expand Down Expand Up @@ -136,3 +192,149 @@ fn attachment_to_aries_message(attach: &Attachment) -> VcxResult<Option<AriesMes
)),
}
}

#[cfg(test)]
mod tests {
use messages::{
msg_fields::protocols::out_of_band::{
invitation::{Invitation, InvitationContent, InvitationDecorators, OobService},
OobGoalCode,
},
msg_types::{
connection::{ConnectionType, ConnectionTypeV1},
protocols::did_exchange::{DidExchangeType, DidExchangeTypeV1},
Protocol,
},
};
use shared::maybe_known::MaybeKnown;

use super::*;

// Example invite formats referenced (with change to use OOB 1.1) from example invite in RFC 0434 - https://github.com/hyperledger/aries-rfcs/tree/main/features/0434-outofband
const JSON_OOB_INVITE: &str = r#"{
"@type": "https://didcomm.org/out-of-band/1.1/invitation",
"@id": "69212a3a-d068-4f9d-a2dd-4741bca89af3",
"label": "Faber College",
"goal_code": "issue-vc",
"goal": "To issue a Faber College Graduate credential",
"handshake_protocols": ["https://didcomm.org/didexchange/1.0", "https://didcomm.org/connections/1.0"],
"services": ["did:sov:LjgpST2rjsoxYegQDRm7EL"]
}"#;
const JSON_OOB_INVITE_NO_WHITESPACE: &str = r#"{"@type":"https://didcomm.org/out-of-band/1.1/invitation","@id":"69212a3a-d068-4f9d-a2dd-4741bca89af3","label":"Faber College","goal_code":"issue-vc","goal":"To issue a Faber College Graduate credential","handshake_protocols":["https://didcomm.org/didexchange/1.0","https://didcomm.org/connections/1.0"],"services":["did:sov:LjgpST2rjsoxYegQDRm7EL"]}"#;
const OOB_BASE64_URL_ENCODED: &str = "eyJAdHlwZSI6Imh0dHBzOi8vZGlkY29tbS5vcmcvb3V0LW9mLWJhbmQvMS4xL2ludml0YXRpb24iLCJAaWQiOiI2OTIxMmEzYS1kMDY4LTRmOWQtYTJkZC00NzQxYmNhODlhZjMiLCJsYWJlbCI6IkZhYmVyIENvbGxlZ2UiLCJnb2FsX2NvZGUiOiJpc3N1ZS12YyIsImdvYWwiOiJUbyBpc3N1ZSBhIEZhYmVyIENvbGxlZ2UgR3JhZHVhdGUgY3JlZGVudGlhbCIsImhhbmRzaGFrZV9wcm90b2NvbHMiOlsiaHR0cHM6Ly9kaWRjb21tLm9yZy9kaWRleGNoYW5nZS8xLjAiLCJodHRwczovL2RpZGNvbW0ub3JnL2Nvbm5lY3Rpb25zLzEuMCJdLCJzZXJ2aWNlcyI6WyJkaWQ6c292OkxqZ3BTVDJyanNveFllZ1FEUm03RUwiXX0";
const OOB_URL: &str = "http://example.com/ssi?oob=eyJAdHlwZSI6Imh0dHBzOi8vZGlkY29tbS5vcmcvb3V0LW9mLWJhbmQvMS4xL2ludml0YXRpb24iLCJAaWQiOiI2OTIxMmEzYS1kMDY4LTRmOWQtYTJkZC00NzQxYmNhODlhZjMiLCJsYWJlbCI6IkZhYmVyIENvbGxlZ2UiLCJnb2FsX2NvZGUiOiJpc3N1ZS12YyIsImdvYWwiOiJUbyBpc3N1ZSBhIEZhYmVyIENvbGxlZ2UgR3JhZHVhdGUgY3JlZGVudGlhbCIsImhhbmRzaGFrZV9wcm90b2NvbHMiOlsiaHR0cHM6Ly9kaWRjb21tLm9yZy9kaWRleGNoYW5nZS8xLjAiLCJodHRwczovL2RpZGNvbW0ub3JnL2Nvbm5lY3Rpb25zLzEuMCJdLCJzZXJ2aWNlcyI6WyJkaWQ6c292OkxqZ3BTVDJyanNveFllZ1FEUm03RUwiXX0";
const OOB_URL_WITH_PADDING: &str = "http://example.com/ssi?oob=eyJAdHlwZSI6Imh0dHBzOi8vZGlkY29tbS5vcmcvb3V0LW9mLWJhbmQvMS4xL2ludml0YXRpb24iLCJAaWQiOiI2OTIxMmEzYS1kMDY4LTRmOWQtYTJkZC00NzQxYmNhODlhZjMiLCJsYWJlbCI6IkZhYmVyIENvbGxlZ2UiLCJnb2FsX2NvZGUiOiJpc3N1ZS12YyIsImdvYWwiOiJUbyBpc3N1ZSBhIEZhYmVyIENvbGxlZ2UgR3JhZHVhdGUgY3JlZGVudGlhbCIsImhhbmRzaGFrZV9wcm90b2NvbHMiOlsiaHR0cHM6Ly9kaWRjb21tLm9yZy9kaWRleGNoYW5nZS8xLjAiLCJodHRwczovL2RpZGNvbW0ub3JnL2Nvbm5lY3Rpb25zLzEuMCJdLCJzZXJ2aWNlcyI6WyJkaWQ6c292OkxqZ3BTVDJyanNveFllZ1FEUm03RUwiXX0%3D";
const OOB_URL_WITH_PADDING_NOT_PERCENT_ENCODED: &str = "http://example.com/ssi?oob=eyJAdHlwZSI6Imh0dHBzOi8vZGlkY29tbS5vcmcvb3V0LW9mLWJhbmQvMS4xL2ludml0YXRpb24iLCJAaWQiOiI2OTIxMmEzYS1kMDY4LTRmOWQtYTJkZC00NzQxYmNhODlhZjMiLCJsYWJlbCI6IkZhYmVyIENvbGxlZ2UiLCJnb2FsX2NvZGUiOiJpc3N1ZS12YyIsImdvYWwiOiJUbyBpc3N1ZSBhIEZhYmVyIENvbGxlZ2UgR3JhZHVhdGUgY3JlZGVudGlhbCIsImhhbmRzaGFrZV9wcm90b2NvbHMiOlsiaHR0cHM6Ly9kaWRjb21tLm9yZy9kaWRleGNoYW5nZS8xLjAiLCJodHRwczovL2RpZGNvbW0ub3JnL2Nvbm5lY3Rpb25zLzEuMCJdLCJzZXJ2aWNlcyI6WyJkaWQ6c292OkxqZ3BTVDJyanNveFllZ1FEUm03RUwiXX0=";

// Params mimic example invitation in RFC 0434 - https://github.com/hyperledger/aries-rfcs/tree/main/features/0434-outofband
fn _create_invitation() -> Invitation {
let id = "69212a3a-d068-4f9d-a2dd-4741bca89af3";
let did = "did:sov:LjgpST2rjsoxYegQDRm7EL";
let service = OobService::Did(did.to_string());
let handshake_protocols = vec![
MaybeKnown::Known(Protocol::DidExchangeType(DidExchangeType::V1(
DidExchangeTypeV1::new_v1_0(),
))),
MaybeKnown::Known(Protocol::ConnectionType(ConnectionType::V1(
ConnectionTypeV1::new_v1_0(),
))),
];
let content = InvitationContent::builder()
.services(vec![service])
.goal("To issue a Faber College Graduate credential".to_string())
.goal_code(MaybeKnown::Known(OobGoalCode::IssueVC))
.label("Faber College".to_string())
.handshake_protocols(handshake_protocols)
.build();
let decorators = InvitationDecorators::default();

let invitation: Invitation = Invitation::builder()
.id(id.to_string())
.content(content)
.decorators(decorators)
.build();

invitation
}

#[test]
fn receive_invitation_by_json() {
let base_invite = _create_invitation();
let parsed_invite = OutOfBandReceiver::create_from_json_encoded_oob(JSON_OOB_INVITE)
.unwrap()
.oob;
assert_eq!(base_invite, parsed_invite);
}

#[test]
fn receive_invitation_by_json_no_whitespace() {
let base_invite = _create_invitation();
let parsed_invite =
OutOfBandReceiver::create_from_json_encoded_oob(JSON_OOB_INVITE_NO_WHITESPACE)
.unwrap()
.oob;
assert_eq!(base_invite, parsed_invite);
}

#[test]
fn receive_invitation_by_url() {
let base_invite = _create_invitation();
let parsed_invite = OutOfBandReceiver::create_from_url_encoded_oob(OOB_URL)
.unwrap()
.oob;
assert_eq!(base_invite, parsed_invite);
}

#[test]
fn receive_invitation_by_url_with_padding() {
let base_invite = _create_invitation();
let parsed_invite = OutOfBandReceiver::create_from_url_encoded_oob(OOB_URL_WITH_PADDING)
.unwrap()
.oob;
assert_eq!(base_invite, parsed_invite);
}

#[test]
fn receive_invitation_by_url_with_padding_no_percent_encoding() {
let base_invite = _create_invitation();
let parsed_invite = OutOfBandReceiver::create_from_url_encoded_oob(
OOB_URL_WITH_PADDING_NOT_PERCENT_ENCODED,
)
.unwrap()
.oob;
assert_eq!(base_invite, parsed_invite);
}

#[test]
fn invitation_to_json() {
let out_of_band_receiver =
OutOfBandReceiver::create_from_json_encoded_oob(JSON_OOB_INVITE).unwrap();

let json_invite = out_of_band_receiver.invitation_to_json_string();

assert_eq!(JSON_OOB_INVITE_NO_WHITESPACE, json_invite);
}

#[test]
fn invitation_to_base64_url() {
let out_of_band_receiver =
OutOfBandReceiver::create_from_json_encoded_oob(JSON_OOB_INVITE).unwrap();

let base64_url_invite = out_of_band_receiver.invitation_to_base64_url();

assert_eq!(OOB_BASE64_URL_ENCODED, base64_url_invite);
}

#[test]
fn invitation_to_url() {
let out_of_band_receiver =
OutOfBandReceiver::create_from_json_encoded_oob(JSON_OOB_INVITE).unwrap();

let oob_url = out_of_band_receiver
.invitation_to_url("http://example.com/ssi")
.unwrap()
.to_string();

assert_eq!(OOB_URL, oob_url);
}
}
Loading
Loading