Skip to content

Commit

Permalink
Merge pull request #122 from alan-turing-institute/62-ffi-v1-ion-create
Browse files Browse the repository at this point in the history
ION create operation with mnemonic for CLI and mobile FFI (#115)
  • Loading branch information
sgreenbury authored Nov 1, 2023
2 parents d3c7906 + 0cad0f9 commit fb4a170
Show file tree
Hide file tree
Showing 15 changed files with 812 additions and 66 deletions.
34 changes: 24 additions & 10 deletions trustchain-cli/src/bin/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,10 @@ use trustchain_api::{
use trustchain_cli::config::cli_config;
use trustchain_core::{vc::CredentialError, verifier::Verifier};
use trustchain_ion::{
attest::attest_operation, create::create_operation, get_ion_resolver, verifier::IONVerifier,
attest::attest_operation,
create::{create_operation, create_operation_mnemonic},
get_ion_resolver,
verifier::IONVerifier,
};

fn cli() -> Command {
Expand All @@ -34,6 +37,7 @@ fn cli() -> Command {
Command::new("create")
.about("Creates a new controlled DID from a document state.")
.arg(arg!(-v - -verbose).action(ArgAction::SetTrue))
.arg(arg!(-m - -mnemonic).action(ArgAction::SetTrue))
.arg(arg!(-f --file_path <FILE_PATH>).required(false)),
)
.subcommand(
Expand Down Expand Up @@ -94,16 +98,24 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
Some(("create", sub_matches)) => {
let file_path = sub_matches.get_one::<String>("file_path");
let verbose = matches!(sub_matches.get_one::<bool>("verbose"), Some(true));

// Read doc state from file path
let doc_state = if let Some(file_path) = file_path {
Some(serde_json::from_reader(File::open(file_path)?)?)
let mnemonic = matches!(sub_matches.get_one::<bool>("mnemonic"), Some(true));
if mnemonic && file_path.is_some() {
panic!("Please use only one of '--file_path' and '--mnemonic'.")
}
if !mnemonic {
// Read doc state from file path
let doc_state = if let Some(file_path) = file_path {
Some(serde_json::from_reader(File::open(file_path)?)?)
} else {
None
};
create_operation(doc_state, verbose)?;
} else {
None
};

// Read from the file path to a "Reader"
create_operation(doc_state, verbose)?;
let mut mnemonic = String::new();
println!("Enter a mnemonic:");
std::io::stdin().read_line(&mut mnemonic).unwrap();
create_operation_mnemonic(&mnemonic, None)?;
}
}
Some(("attest", sub_matches)) => {
let did = sub_matches.get_one::<String>("did").unwrap();
Expand All @@ -115,6 +127,8 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
// TODO: pass optional key_id
attest_operation(did, controlled_did, verbose).await?;
}
// TODO: add a flag for update operation with a mnemonic to add a
// key generated on mobile to the DID.
Some(("resolve", sub_matches)) => {
let did = sub_matches.get_one::<String>("did").unwrap();
let _verbose = matches!(sub_matches.get_one::<bool>("verbose"), Some(true));
Expand Down
83 changes: 82 additions & 1 deletion trustchain-ffi/src/mobile.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
use crate::config::FFIConfig;
use anyhow::Result;
use chrono::{DateTime, Utc};
use did_ion::sidetree::Operation;
use serde::{Deserialize, Serialize};
use ssi::{
jsonld::ContextLoader,
jwk::JWK,
Expand All @@ -18,7 +20,11 @@ use trustchain_api::{
use trustchain_core::{
resolver::ResolverError, vc::CredentialError, verifier::VerifierError, vp::PresentationError,
};
use trustchain_ion::{get_ion_resolver, verifier::IONVerifier};
use trustchain_ion::{
create::{mnemonic_to_create_and_keys, OperationDID},
get_ion_resolver,
verifier::IONVerifier,
};

/// A speicfic error for FFI mobile making handling easier.
#[derive(Error, Debug)]
Expand All @@ -41,6 +47,8 @@ pub enum FFIMobileError {
FutureProofCreatedTime(DateTime<Utc>, DateTime<Utc>),
#[error("Failed to issue presentation error: {0}.")]
FailedToIssuePresentation(PresentationError),
#[error("Failed to make create operation from mnemonic: {0}.")]
FailedCreateOperation(String),
}

/// Example greet function.
Expand Down Expand Up @@ -173,9 +181,31 @@ pub fn vp_issue_presentation(
// todo!()
// }

#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
struct CreateOperationAndDID {
create_operation: Operation,
did: String,
}

/// Makes a new ION DID from a mnemonic.
// TODO: consider optional index in API
pub fn create_operation_mnemonic(mnemonic: String) -> Result<String> {
// Generate create operation from mnemonic
let (create_operation, _) = mnemonic_to_create_and_keys(&mnemonic, None)
.map_err(|err| FFIMobileError::FailedCreateOperation(err.to_string()))?;

// Return DID and create operation as JSON
Ok(serde_json::to_string_pretty(&CreateOperationAndDID {
did: create_operation.to_did(),
create_operation: Operation::Create(create_operation),
})?)
}

#[cfg(test)]
mod tests {
use ssi::vc::CredentialOrJWT;
use trustchain_core::utils::canonicalize_str;

use crate::config::parse_toml;

Expand Down Expand Up @@ -219,6 +249,46 @@ mod tests {
}
"#;

const TEST_ION_CREATE_OPERATION: &str = r#"
{
"createOperation": {
"delta": {
"patches": [
{
"action": "replace",
"document": {
"publicKeys": [
{
"id": "CIMzmuW8XaQoc2DyccLwMZ35GyLhPj4yG2k38JNw5P4",
"publicKeyJwk": {
"crv": "Ed25519",
"kty": "OKP",
"x": "jil0ZZqW_cldlxq2a0Ezw59IgEIULSj9E3NOD6YQCHo"
},
"purposes": [
"assertionMethod",
"authentication",
"keyAgreement",
"capabilityInvocation",
"capabilityDelegation"
],
"type": "JsonWebSignature2020"
}
]
}
}
],
"updateCommitment": "EiA2gSveT83s4DD4kJp6tLJuPfy_M3m_m6NtRJzjwtrlDg"
},
"suffixData": {
"deltaHash": "EiBtaFhQ3mbpKXwOXD2wr7so32FvbZDGvRyGJ-yOfforGQ",
"recoveryCommitment": "EiDKEn4lG5ETCoQpQxAsMVahzuerhlk0rtqtuoHPYKEEog"
},
"type": "create"
},
"did": "did:ion:test:EiA1dZD7jVkS5ZP7JJO01t6HgTU3eeLpbKEV1voOFWJV0g"
}"#;

#[test]
#[ignore = "integration test requires ION, MongoDB, IPFS and Bitcoin RPC"]
fn test_did_resolve() {
Expand Down Expand Up @@ -288,4 +358,15 @@ mod tests {
// // TODO: implement once verifiable presentations are included in API
// #[test]
// fn test_vc_verify_presentation() {}

#[test]
fn test_ion_create_operation() {
let mnemonic =
"state draft moral repeat knife trend animal pretty delay collect fall adjust";
let create_op_and_did = create_operation_mnemonic(mnemonic.to_string()).unwrap();
assert_eq!(
canonicalize_str::<CreateOperationAndDID>(&create_op_and_did).unwrap(),
canonicalize_str::<CreateOperationAndDID>(TEST_ION_CREATE_OPERATION).unwrap()
);
}
}
5 changes: 5 additions & 0 deletions trustchain-ffi/src/mobile_bridge.io.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,11 @@ pub extern "C" fn wire_vp_issue_presentation(
wire_vp_issue_presentation_impl(port_, presentation, opts, jwk_json)
}

#[no_mangle]
pub extern "C" fn wire_create_operation_mnemonic(port_: i64, mnemonic: *mut wire_uint_8_list) {
wire_create_operation_mnemonic_impl(port_, mnemonic)
}

// Section: allocate functions

#[no_mangle]
Expand Down
16 changes: 16 additions & 0 deletions trustchain-ffi/src/mobile_bridge.rs
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,22 @@ fn wire_vp_issue_presentation_impl(
},
)
}
fn wire_create_operation_mnemonic_impl(
port_: MessagePort,
mnemonic: impl Wire2Api<String> + UnwindSafe,
) {
FLUTTER_RUST_BRIDGE_HANDLER.wrap(
WrapInfo {
debug_name: "create_operation_mnemonic",
port: Some(port_),
mode: FfiCallMode::Normal,
},
move || {
let api_mnemonic = mnemonic.wire2api();
move |task_callback| create_operation_mnemonic(api_mnemonic)
},
)
}
// Section: wrapper structs

// Section: static checks
Expand Down
2 changes: 1 addition & 1 deletion trustchain-http/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ image = "0.23.14"
lazy_static="1.4.0"
log = "0.4"
qrcode = "0.12.0"
reqwest = "0.11.16"
reqwest = {version="0.11.16", features=["stream"]}
serde = { version = "1.0", features = ["derive"] }
serde_jcs = "0.1.0"
serde_json = "1.0"
Expand Down
8 changes: 8 additions & 0 deletions trustchain-http/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@ pub struct HTTPConfig {
pub host_display: String,
/// Port for server
pub port: u16,
/// ION host
pub ion_host: IpAddr,
/// ION port
pub ion_port: u16,
/// Optional server DID if issuing or verifying
pub server_did: Option<String>,
/// Flag indicating whether server uses https
Expand All @@ -49,6 +53,8 @@ impl Default for HTTPConfig {
host: IpAddr::from_str(DEFAULT_HOST).unwrap(),
host_display: DEFAULT_HOST.to_string(),
port: DEFAULT_PORT,
ion_host: IpAddr::from_str(DEFAULT_HOST).unwrap(),
ion_port: 3000,
server_did: None,
https: false,
https_path: None,
Expand Down Expand Up @@ -116,6 +122,8 @@ mod tests {
host = "127.0.0.1"
host_display = "127.0.0.1"
port = 8081
ion_host = "127.0.0.1"
ion_port = 3000
server_did = "did:ion:test:EiBcLZcELCKKtmun_CUImSlb2wcxK5eM8YXSq3MrqNe5wA"
https = false
Expand Down
6 changes: 6 additions & 0 deletions trustchain-http/src/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ pub enum TrustchainHTTPError {
CredentialDoesNotExist,
#[error("No issuer available.")]
NoCredentialIssuer,
#[error("Wrapped reqwest error: {0}")]
ReqwestError(reqwest::Error),
#[error("Failed to verify credential.")]
FailedToVerifyCredential,
#[error("Invalid signature.")]
Expand Down Expand Up @@ -118,6 +120,10 @@ impl IntoResponse for TrustchainHTTPError {
err @ TrustchainHTTPError::NoCredentialIssuer => {
(StatusCode::BAD_REQUEST, err.to_string())
}
TrustchainHTTPError::ReqwestError(err) => (
err.status().unwrap_or(StatusCode::INTERNAL_SERVER_ERROR),
err.to_string(),
),
ref err @ TrustchainHTTPError::RootError(ref variant) => match variant {
TrustchainRootError::NoUniqueRootEvent(_) => {
(StatusCode::BAD_REQUEST, err.to_string())
Expand Down
99 changes: 99 additions & 0 deletions trustchain-http/src/ion.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
use crate::{errors::TrustchainHTTPError, state::AppState};
use axum::{
response::{IntoResponse, Response},
Json,
};
use hyper::Body;
use log::info;
use serde_json::Value;
use std::sync::Arc;

/// Receives ION operation POST request and forwards to local ION node.
pub async fn post_operation(
Json(operation): Json<Value>,
app_state: Arc<AppState>,
) -> impl IntoResponse {
info!("Received ION operation: {}", operation);
let client = reqwest::Client::new();
let address = format!(
"http://{}:{}/operations",
app_state.config.ion_host, app_state.config.ion_port
);
client
// TODO: Add config for ION URL
.post(address)
.json(&operation)
.send()
.await
.map_err(TrustchainHTTPError::ReqwestError)
.map(|response| {
// See example: https://github.com/tokio-rs/axum/blob/8854e660e9ab07404e5bb8e30b92311d3848de05/examples/reqwest-response/src/main.rs
let mut response_builder = Response::builder().status(response.status());
*response_builder.headers_mut().unwrap() = response.headers().clone();
response_builder
.body(Body::wrap_stream(response.bytes_stream()))
.unwrap()
})
}

#[cfg(test)]
mod tests {
use super::*;
use crate::{config::HTTPConfig, server::TrustchainRouter};
use axum::{response::Html, routing::post, Router};
use hyper::{Server, StatusCode};
use std::{collections::HashMap, net::TcpListener};
use tower::make::Shared;

async fn mock_post_operation_handler(Json(json): Json<Value>) -> impl IntoResponse {
Html(format!("Response: {}", json))
}

#[tokio::test]
#[ignore = "requires reading trustchain_config.toml"]
async fn test_post_operation() {
let listener = TcpListener::bind("127.0.0.1:0").expect("Could not bind ephemeral socket");
let trustchain_addr = listener.local_addr().unwrap();
let trustchain_port = trustchain_addr.port();
let ion_listener =
TcpListener::bind("127.0.0.1:0").expect("Could not bind ephemeral socket");
let ion_addr = ion_listener.local_addr().unwrap();
let ion_port = ion_addr.port();
let http_config = HTTPConfig {
port: trustchain_port,
ion_port,
..Default::default()
};
assert_eq!(
http_config.host.to_string(),
trustchain_addr.ip().to_string()
);

// Run server
tokio::spawn(async move {
let trustchain_server = Server::from_tcp(listener).unwrap().serve(Shared::new(
TrustchainRouter::from(http_config).into_router(),
));
trustchain_server.await.expect("server error");
});

// Run mock ION server
tokio::spawn(async move {
let ion_server = Server::from_tcp(ion_listener).unwrap().serve(Shared::new(
Router::new().route("/operations", post(mock_post_operation_handler)),
));
ion_server.await.expect("server error");
});

// Send POST request to server
let client = reqwest::Client::new();
let addr = format!("http://127.0.0.1:{trustchain_port}/operations");
let map: HashMap<String, String> = serde_json::from_str(r#"{"key": "value"}"#).unwrap();
let response = client.post(&addr).json(&map).send().await.unwrap();
assert_eq!(response.status(), StatusCode::OK);
assert_eq!(
r#"Response: {"key":"value"}"#,
response.text().await.unwrap()
);
}
}
1 change: 1 addition & 0 deletions trustchain-http/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
pub mod config;
pub mod errors;
pub mod ion;
pub mod issuer;
pub mod middleware;
pub mod qrcode;
Expand Down
Loading

0 comments on commit fb4a170

Please sign in to comment.