Skip to content

Commit

Permalink
feat: add additional formats for parsing and outputting Out Of Band I…
Browse files Browse the repository at this point in the history
…nvitations (#1281)

* feat: added invitation parsing from json, base64url, and url

Signed-off-by: James Ebert <[email protected]>

* feat: added invitation to json, base64url, and url

Signed-off-by: James Ebert <[email protected]>

* feat: add tests, fix minor output issues

Signed-off-by: James Ebert <[email protected]>

* feat: added additional formats to outofbandsender, added tests

Signed-off-by: James Ebert <[email protected]>

* chore: fix formatting/clippy

Signed-off-by: James Ebert <[email protected]>

* chore: fix clippy issue

Signed-off-by: James Ebert <[email protected]>

* chore: adjust function/method names, added no invitation padding parsing test

Signed-off-by: James Ebert <[email protected]>

* chore: fix dead code needed by testing

Signed-off-by: James Ebert <[email protected]>

---------

Signed-off-by: James Ebert <[email protected]>
  • Loading branch information
JamesKEbert authored Aug 22, 2024
1 parent 8187925 commit 6887dc2
Show file tree
Hide file tree
Showing 4 changed files with 337 additions and 13 deletions.
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)?,
)?)
}

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

0 comments on commit 6887dc2

Please sign in to comment.