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

ION create operation FFI #122

Merged
merged 25 commits into from
Nov 1, 2023
Merged
Show file tree
Hide file tree
Changes from 23 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
a8046d9
Initial testing
sgreenbury Sep 5, 2023
7f03d3b
Initial conversion to JWK
sgreenbury Sep 6, 2023
6495989
Fix test_mnemonic_secp256k1 using rust bitcoin
thobson88 Sep 6, 2023
4cd7e53
Tidy up mnemonic to secp256k1 test
thobson88 Sep 7, 2023
9f86f62
Add functions to generate keys from a mnemonic phrase
thobson88 Sep 7, 2023
68faf4a
Merge branch '117-mnemonic-create' into 62-ffi-v1-ion-create
sgreenbury Sep 7, 2023
2aab123
Add ION create operation from keys and mboile FFI
sgreenbury Sep 7, 2023
7549dca
Merge branch 'main' into 62-ffi-v1-ion-create
sgreenbury Sep 30, 2023
9993b03
Add test case
sgreenbury Sep 30, 2023
1d8c0cb
Add create from phrase and trait, refactor create to remove repeated …
sgreenbury Oct 1, 2023
9990960
Add ion operations route
sgreenbury Oct 2, 2023
5980e3b
Add create with mnemonic to cli
sgreenbury Oct 13, 2023
733a122
Merge branch 'main' into 62-ffi-v1-ion-create
sgreenbury Oct 13, 2023
eb508b6
Update to use ed25519_dalek_bip32 crate
sgreenbury Oct 20, 2023
4f9cf2e
Update methods and imports
sgreenbury Oct 23, 2023
aafcba0
Merge branch 'main' into 62-ffi-v1-ion-create
sgreenbury Oct 23, 2023
8fc68b8
Add condition for did create flags and comment
sgreenbury Oct 25, 2023
71c51b3
Use only mnemonic, remove obsolete test
sgreenbury Oct 25, 2023
638742a
Add comments, use MnemonicError
sgreenbury Oct 26, 2023
5a05fcd
Remove zero byte prefix, add test
sgreenbury Oct 26, 2023
0fea1ac
Fix unused import
sgreenbury Oct 26, 2023
1e40f56
Modify post operation to get address from config, add test
sgreenbury Oct 26, 2023
250edc5
Add ignore for test given config dependency
sgreenbury Oct 30, 2023
3b5e94c
Remove unused dependency
sgreenbury Oct 30, 2023
0cad0f9
Remove print from test
sgreenbury Oct 30, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
1 change: 1 addition & 0 deletions trustchain-ffi/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ trustchain-ion = { path = "../trustchain-ion" }
trustchain-api = { path = "../trustchain-api" }

anyhow = "1.0"
bip39 = "2.0.0"
sgreenbury marked this conversation as resolved.
Show resolved Hide resolved
chrono = "0.4.26"
did-ion = {git="https://github.com/alan-turing-institute/ssi.git", branch="modify-encode-sign-jwt"}
# Fixed to same version used to generate bridge: `[email protected]`
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
Loading