From a8046d9622b8c44e8d4849cb31362c12b8b6d9c4 Mon Sep 17 00:00:00 2001 From: Sam Greenbury Date: Tue, 5 Sep 2023 14:41:50 +0100 Subject: [PATCH 01/21] Initial testing --- trustchain-ion/Cargo.toml | 3 ++ trustchain-ion/src/lib.rs | 1 + trustchain-ion/src/mnemonic.rs | 70 ++++++++++++++++++++++++++++++++++ 3 files changed, 74 insertions(+) create mode 100644 trustchain-ion/src/mnemonic.rs diff --git a/trustchain-ion/Cargo.toml b/trustchain-ion/Cargo.toml index 8ee38ff8..8501b280 100644 --- a/trustchain-ion/Cargo.toml +++ b/trustchain-ion/Cargo.toml @@ -8,11 +8,14 @@ edition = "2021" [dependencies] trustchain-core = { path = "../trustchain-core" } async-trait = "0.1" +bip32 = {version="*", features = ["secp256k1"]} +bip39 = "2.0.0" bitcoin = "0.29.2" bitcoincore-rpc = "0.16.0" canonical_json = "0.4.0" clap = { version = "~4.0", features=["derive", "cargo"] } did-ion="0.1.0" +ed25519-hd-key = "0.3.0" flate2 = "1.0.24" futures = "0.3.21" hex = "0.4.3" diff --git a/trustchain-ion/src/lib.rs b/trustchain-ion/src/lib.rs index 246dc4dc..5b7ad739 100644 --- a/trustchain-ion/src/lib.rs +++ b/trustchain-ion/src/lib.rs @@ -6,6 +6,7 @@ pub mod config; pub mod controller; pub mod create; pub mod data; +pub mod mnemonic; pub mod sidetree; pub mod utils; pub mod verifier; diff --git a/trustchain-ion/src/mnemonic.rs b/trustchain-ion/src/mnemonic.rs new file mode 100644 index 00000000..c11b8031 --- /dev/null +++ b/trustchain-ion/src/mnemonic.rs @@ -0,0 +1,70 @@ +use bip39::Mnemonic; +use ed25519_hd_key; +use ssi::jwk::{Base64urlUInt, OctetParams, Params, JWK}; + +// See: https://github.com/alepop/dart-ed25519-hd-key/blob/f785c73b1248037df58f8d582e6f71c480e49d39/lib/src/hd_key.dart#L45-L56 +fn with_zero_byte(public_key: [u8; 32]) -> [u8; 33] { + let mut new_public_key = [0u8; 33]; + new_public_key[1..33].copy_from_slice(public_key.as_slice()); + new_public_key +} + +#[cfg(test)] +mod tests { + use bip32::{Prefix, XPrv}; + use ssi::jwk::ECParams; + + use super::*; + + #[test] + fn test_mnemonic_secp256k1() -> Result<(), Box> { + let phrase = "state draft moral repeat knife trend animal pretty delay collect fall adjust"; + let mnemonic = Mnemonic::parse(phrase).unwrap(); + let seed = mnemonic.to_seed(""); + // Update key is 1/0 + let path = "m/1'/0'"; + // Derive the root `XPrv` from the `seed` value + let root_xprv = XPrv::new(&seed)?; + let private_key = root_xprv.private_key().to_bytes(); + let public_key = root_xprv.public_key().to_bytes(); + println!("{:?}", private_key); + println!("{:?}", public_key); + println!("{:?}", root_xprv.to_string(Prefix::XPRV)); + // TODO: how to convert into EC params? + // let jwk = JWK::from(Params::EC(ECParams { + // curve: "secp256k1".to_string(), + // public_key: Base64urlUInt(public_key.to_vec()), + // private_key: Some(Base64urlUInt(private_key.to_vec())), + // })); + // println!("{}", serde_json::to_string_pretty(&jwk).unwrap()); + Ok(()) + } + + #[test] + fn test_mnemonic_ed22519() { + // Test case: + // https://github.com/alan-turing-institute/trustchain-mobile/blob/1b735645fd140b94bf1360bd5546643214c423b6/test/app/key_tests.dart + let phrase = "state draft moral repeat knife trend animal pretty delay collect fall adjust"; + let mnemonic = Mnemonic::parse(phrase).unwrap(); + let seed = mnemonic.to_seed(""); + let path = "m/0'/0'"; + let (private_key, _chain_code) = ed25519_hd_key::derive_from_path(path, &seed); + let public_key = ed25519_hd_key::get_public_key(&private_key); + // For some reason zero byte is required despite the false arg here: + // https://github.com/alan-turing-institute/trustchain-mobile/blob/1b735645fd140b94bf1360bd5546643214c423b6/lib/app/shared/key_generation.dart#L12 + let public_key = with_zero_byte(public_key); + + // Compare to test case JWK: + let expected = r#"{"kty":"OKP","crv":"Ed25519","d":"wHwSUdy4a00qTxAhnuOHeWpai4ERjdZGslaou-Lig5g=","x":"AI4pdGWalv3JXZcatmtBM8OfSIBCFC0o_RNzTg-mEAh6"}"#; + let expected_jwk: JWK = serde_json::from_str(expected).unwrap(); + // Make a JWK from bytes: https://docs.rs/ssi/0.4.0/src/ssi/jwk.rs.html#251-265 + let jwk = JWK::from(Params::OKP(OctetParams { + curve: "Ed25519".to_string(), + public_key: Base64urlUInt(public_key.to_vec()), + private_key: Some(Base64urlUInt(private_key.to_vec())), + })); + println!("{}", serde_json::to_string_pretty(&jwk).unwrap()); + println!("{}", serde_json::to_string_pretty(&expected_jwk).unwrap()); + assert_eq!(jwk, expected_jwk); + } +} From 7f03d3b2ab15121be15774310f8a6730a9f8d646 Mon Sep 17 00:00:00 2001 From: Sam Greenbury Date: Wed, 6 Sep 2023 10:30:26 +0100 Subject: [PATCH 02/21] Initial conversion to JWK --- trustchain-ion/Cargo.toml | 2 ++ trustchain-ion/src/mnemonic.rs | 60 ++++++++++++++++++++++++---------- 2 files changed, 45 insertions(+), 17 deletions(-) diff --git a/trustchain-ion/Cargo.toml b/trustchain-ion/Cargo.toml index 8501b280..6165d2f0 100644 --- a/trustchain-ion/Cargo.toml +++ b/trustchain-ion/Cargo.toml @@ -21,9 +21,11 @@ futures = "0.3.21" hex = "0.4.3" ipfs-api-backend-hyper = {version="0.6", features = ["with-send-sync"]} ipfs-hasher = "0.13.0" +k256 = "0.9.6" lazy_static="1.4.0" mongodb = "2.3.1" reqwest = "0.11" +secp256k1 = "0.27.0" serde = { version = "1.0", features = ["derive"] } serde_jcs = "0.1.0" serde_json = "1.0" diff --git a/trustchain-ion/src/mnemonic.rs b/trustchain-ion/src/mnemonic.rs index c11b8031..74a13f44 100644 --- a/trustchain-ion/src/mnemonic.rs +++ b/trustchain-ion/src/mnemonic.rs @@ -11,10 +11,12 @@ fn with_zero_byte(public_key: [u8; 32]) -> [u8; 33] { #[cfg(test)] mod tests { - use bip32::{Prefix, XPrv}; - use ssi::jwk::ECParams; - use super::*; + use bip32::{DerivationPath, Prefix, PrivateKey, XPrv}; + use bitcoin::Address; + use k256::elliptic_curve::sec1::ToEncodedPoint; + // use lazy_static::__Deref; + use ssi::jwk::ECParams; #[test] fn test_mnemonic_secp256k1() -> Result<(), Box> { @@ -22,21 +24,44 @@ mod tests { let mnemonic = Mnemonic::parse(phrase).unwrap(); let seed = mnemonic.to_seed(""); // Update key is 1/0 - let path = "m/1'/0'"; + let path = "m/0'/0'"; + // Derive the root `XPrv` from the `seed` value - let root_xprv = XPrv::new(&seed)?; - let private_key = root_xprv.private_key().to_bytes(); - let public_key = root_xprv.public_key().to_bytes(); - println!("{:?}", private_key); - println!("{:?}", public_key); - println!("{:?}", root_xprv.to_string(Prefix::XPRV)); - // TODO: how to convert into EC params? - // let jwk = JWK::from(Params::EC(ECParams { - // curve: "secp256k1".to_string(), - // public_key: Base64urlUInt(public_key.to_vec()), - // private_key: Some(Base64urlUInt(private_key.to_vec())), - // })); - // println!("{}", serde_json::to_string_pretty(&jwk).unwrap()); + // let root_xprv = XPrv::new(seed)?; + let path: DerivationPath = path.parse()?; + let root_xprv = XPrv::derive_from_path(seed, &path)?; + let private_key = root_xprv.private_key(); + let mut private_key_32_bytes: [u8; 32] = [0; 32]; + private_key_32_bytes.copy_from_slice(&private_key.to_bytes()); + + let secret_key = k256::SecretKey::from_bytes(private_key_32_bytes)?; + // Copied from SSI try_from conversion but leads to identical bytes to removing. + // let sk_bytes: &[u8] = secret_key.as_scalar_bytes().as_ref(); + // assert_eq!(sk_bytes, private_key_32_bytes); + + let public_key = secret_key.public_key(); + let mut ec_params = ECParams::try_from(&public_key)?; + ec_params.ecc_private_key = Some(Base64urlUInt(private_key_32_bytes.to_vec())); + let jwk = JWK::from(Params::EC(ec_params)); + println!("{}", serde_json::to_string_pretty(&jwk).unwrap()); + + let public_key_bytes = public_key.to_encoded_point(false).as_bytes(); + // let hash = sha2::digest(public_key_bytes); + // let bitcoin_pk = bitcoin::PublicKey::new_uncompressed(); + + // use secp256k1::{PublicKey, Secp256k1, SecretKey}; + // let secp = Secp256k1::new(); + // // let secret_key = SecretKey::from_slice(&[0xcd; 32]).expect("32 bytes, within curve order"); + // let secret_key = + // SecretKey::from_slice(&private_key_32_bytes).expect("32 bytes, within curve order"); + // let public_key = PublicKey::from_secret_key(&secp, &secret_key); + + // Address::p2pkh(&public_key_bytes, bitcoin::Network::Bitcoin); + + // Test cases: https://learnmeabitcoin.com/technical/derivation-paths + // With above phrase + // m/0h/0h: 1KtK31vM2RaK9vKkV8e16yBfBGEKF8tNb4 + Ok(()) } @@ -57,6 +82,7 @@ mod tests { // Compare to test case JWK: let expected = r#"{"kty":"OKP","crv":"Ed25519","d":"wHwSUdy4a00qTxAhnuOHeWpai4ERjdZGslaou-Lig5g=","x":"AI4pdGWalv3JXZcatmtBM8OfSIBCFC0o_RNzTg-mEAh6"}"#; let expected_jwk: JWK = serde_json::from_str(expected).unwrap(); + // Make a JWK from bytes: https://docs.rs/ssi/0.4.0/src/ssi/jwk.rs.html#251-265 let jwk = JWK::from(Params::OKP(OctetParams { curve: "Ed25519".to_string(), From 6495989a196b434df02a2c9b744a0395072d676e Mon Sep 17 00:00:00 2001 From: Tim Hobson Date: Wed, 6 Sep 2023 22:48:23 +0100 Subject: [PATCH 03/21] Fix test_mnemonic_secp256k1 using rust bitcoin --- trustchain-ion/src/mnemonic.rs | 75 +++++++++++++++++++--------------- 1 file changed, 42 insertions(+), 33 deletions(-) diff --git a/trustchain-ion/src/mnemonic.rs b/trustchain-ion/src/mnemonic.rs index 74a13f44..3723e6b5 100644 --- a/trustchain-ion/src/mnemonic.rs +++ b/trustchain-ion/src/mnemonic.rs @@ -12,55 +12,64 @@ fn with_zero_byte(public_key: [u8; 32]) -> [u8; 33] { #[cfg(test)] mod tests { use super::*; - use bip32::{DerivationPath, Prefix, PrivateKey, XPrv}; + + // use bip32::{DerivationPath, Prefix, PrivateKey, XPrv}; + // note: the above import conflicts with the following: + use bitcoin::util::bip32::{DerivationPath, ExtendedPrivKey}; + + use bitcoin::secp256k1::Secp256k1; use bitcoin::Address; - use k256::elliptic_curve::sec1::ToEncodedPoint; - // use lazy_static::__Deref; use ssi::jwk::ECParams; + use ssi::jwk::JWK; + use std::str::FromStr; #[test] fn test_mnemonic_secp256k1() -> Result<(), Box> { let phrase = "state draft moral repeat knife trend animal pretty delay collect fall adjust"; let mnemonic = Mnemonic::parse(phrase).unwrap(); let seed = mnemonic.to_seed(""); - // Update key is 1/0 let path = "m/0'/0'"; // Derive the root `XPrv` from the `seed` value // let root_xprv = XPrv::new(seed)?; - let path: DerivationPath = path.parse()?; - let root_xprv = XPrv::derive_from_path(seed, &path)?; - let private_key = root_xprv.private_key(); - let mut private_key_32_bytes: [u8; 32] = [0; 32]; - private_key_32_bytes.copy_from_slice(&private_key.to_bytes()); - - let secret_key = k256::SecretKey::from_bytes(private_key_32_bytes)?; - // Copied from SSI try_from conversion but leads to identical bytes to removing. - // let sk_bytes: &[u8] = secret_key.as_scalar_bytes().as_ref(); - // assert_eq!(sk_bytes, private_key_32_bytes); - - let public_key = secret_key.public_key(); - let mut ec_params = ECParams::try_from(&public_key)?; - ec_params.ecc_private_key = Some(Base64urlUInt(private_key_32_bytes.to_vec())); - let jwk = JWK::from(Params::EC(ec_params)); - println!("{}", serde_json::to_string_pretty(&jwk).unwrap()); - let public_key_bytes = public_key.to_encoded_point(false).as_bytes(); - // let hash = sha2::digest(public_key_bytes); - // let bitcoin_pk = bitcoin::PublicKey::new_uncompressed(); + // // Using bip32 crate (works): + // let path: DerivationPath = path.parse()?; + // let root_xprv = XPrv::derive_from_path(seed, &path)?; + // let private_key = root_xprv.private_key(); + // let mut private_key_32_bytes: [u8; 32] = [0; 32]; + // private_key_32_bytes.copy_from_slice(&private_key.to_bytes()); + // let secret_key = k256::SecretKey::from_bytes(private_key_32_bytes)?; - // use secp256k1::{PublicKey, Secp256k1, SecretKey}; - // let secp = Secp256k1::new(); - // // let secret_key = SecretKey::from_slice(&[0xcd; 32]).expect("32 bytes, within curve order"); - // let secret_key = - // SecretKey::from_slice(&private_key_32_bytes).expect("32 bytes, within curve order"); - // let public_key = PublicKey::from_secret_key(&secp, &secret_key); + // let public_key = secret_key.public_key(); + // let mut ec_params = ECParams::try_from(&public_key)?; + // ec_params.ecc_private_key = Some(Base64urlUInt(private_key_32_bytes.to_vec())); + // let jwk = JWK::from(Params::EC(ec_params)); + // println!("{}", serde_json::to_string_pretty(&jwk).unwrap()); - // Address::p2pkh(&public_key_bytes, bitcoin::Network::Bitcoin); + // Using rust bitcoin crate: + let m = ExtendedPrivKey::new_master(bitcoin::Network::Bitcoin, &seed).unwrap(); + let secp = Secp256k1::new(); + let derivation_path = DerivationPath::from_str(path).unwrap(); + let xpriv = m.derive_priv(&secp, &derivation_path).unwrap(); - // Test cases: https://learnmeabitcoin.com/technical/derivation-paths - // With above phrase - // m/0h/0h: 1KtK31vM2RaK9vKkV8e16yBfBGEKF8tNb4 + let private_key = xpriv.to_priv(); + let public_key: bitcoin::util::key::PublicKey = private_key.public_key(&secp); + let address = Address::p2pkh(&public_key, bitcoin::Network::Bitcoin); + + // This matches the address generated at https://learnmeabitcoin.com/technical/derivation-paths + println!("{}", address); // Yay!! + let expected_address = "1KtK31vM2RaK9vKkV8e16yBfBGEKF8tNb4"; + assert_eq!(address.to_string(), expected_address); + + // Now convert the bitcoin::util::bip32::ExtendedPrivKey into a JWK. + let k256_secret_key = k256::SecretKey::from_bytes(private_key.to_bytes()).unwrap(); + let k256_public_key = k256_secret_key.public_key(); + + let mut ec_params = ECParams::try_from(&k256_public_key)?; + ec_params.ecc_private_key = Some(Base64urlUInt(private_key.to_bytes().to_vec())); + let jwk = JWK::from(Params::EC(ec_params)); + println!("{}", serde_json::to_string_pretty(&jwk).unwrap()); // Fine Ok(()) } From 4cd7e53e47848d98e3f085326ebc95da67a6c78a Mon Sep 17 00:00:00 2001 From: Tim Hobson Date: Thu, 7 Sep 2023 10:49:10 +0100 Subject: [PATCH 04/21] Tidy up mnemonic to secp256k1 test --- trustchain-ion/src/mnemonic.rs | 40 +++++++++++----------------------- 1 file changed, 13 insertions(+), 27 deletions(-) diff --git a/trustchain-ion/src/mnemonic.rs b/trustchain-ion/src/mnemonic.rs index 3723e6b5..e2e13261 100644 --- a/trustchain-ion/src/mnemonic.rs +++ b/trustchain-ion/src/mnemonic.rs @@ -13,11 +13,8 @@ fn with_zero_byte(public_key: [u8; 32]) -> [u8; 33] { mod tests { use super::*; - // use bip32::{DerivationPath, Prefix, PrivateKey, XPrv}; - // note: the above import conflicts with the following: - use bitcoin::util::bip32::{DerivationPath, ExtendedPrivKey}; - use bitcoin::secp256k1::Secp256k1; + use bitcoin::util::bip32::{DerivationPath, ExtendedPrivKey}; use bitcoin::Address; use ssi::jwk::ECParams; use ssi::jwk::JWK; @@ -30,46 +27,35 @@ mod tests { let seed = mnemonic.to_seed(""); let path = "m/0'/0'"; - // Derive the root `XPrv` from the `seed` value - // let root_xprv = XPrv::new(seed)?; - - // // Using bip32 crate (works): - // let path: DerivationPath = path.parse()?; - // let root_xprv = XPrv::derive_from_path(seed, &path)?; - // let private_key = root_xprv.private_key(); - // let mut private_key_32_bytes: [u8; 32] = [0; 32]; - // private_key_32_bytes.copy_from_slice(&private_key.to_bytes()); - // let secret_key = k256::SecretKey::from_bytes(private_key_32_bytes)?; - - // let public_key = secret_key.public_key(); - // let mut ec_params = ECParams::try_from(&public_key)?; - // ec_params.ecc_private_key = Some(Base64urlUInt(private_key_32_bytes.to_vec())); - // let jwk = JWK::from(Params::EC(ec_params)); - // println!("{}", serde_json::to_string_pretty(&jwk).unwrap()); - // Using rust bitcoin crate: - let m = ExtendedPrivKey::new_master(bitcoin::Network::Bitcoin, &seed).unwrap(); + let m = ExtendedPrivKey::new_master(bitcoin::Network::Bitcoin, &seed)?; let secp = Secp256k1::new(); - let derivation_path = DerivationPath::from_str(path).unwrap(); - let xpriv = m.derive_priv(&secp, &derivation_path).unwrap(); + let derivation_path = DerivationPath::from_str(path)?; + let xpriv = m.derive_priv(&secp, &derivation_path)?; let private_key = xpriv.to_priv(); let public_key: bitcoin::util::key::PublicKey = private_key.public_key(&secp); let address = Address::p2pkh(&public_key, bitcoin::Network::Bitcoin); // This matches the address generated at https://learnmeabitcoin.com/technical/derivation-paths - println!("{}", address); // Yay!! let expected_address = "1KtK31vM2RaK9vKkV8e16yBfBGEKF8tNb4"; assert_eq!(address.to_string(), expected_address); // Now convert the bitcoin::util::bip32::ExtendedPrivKey into a JWK. - let k256_secret_key = k256::SecretKey::from_bytes(private_key.to_bytes()).unwrap(); + let k256_secret_key = k256::SecretKey::from_bytes(private_key.to_bytes())?; let k256_public_key = k256_secret_key.public_key(); let mut ec_params = ECParams::try_from(&k256_public_key)?; ec_params.ecc_private_key = Some(Base64urlUInt(private_key.to_bytes().to_vec())); let jwk = JWK::from(Params::EC(ec_params)); - println!("{}", serde_json::to_string_pretty(&jwk).unwrap()); // Fine + let expected_jwk = r#"{ + "kty": "EC", + "crv": "secp256k1", + "x": "czAsjE4ifEsU-QO-nkz4WNWxlEWBqBIqg2Wn1hxJ7bg", + "y": "lnBcn6tVS9_O2PHR5Lr1Qim0gDryEHyErTaRx4to8-k", + "d": "8dJUi9adMsRYkVDVJR49kr38cSKLeMYahdNYsZalTns" + }"#; + assert_eq!(jwk, serde_json::from_str::(expected_jwk)?); Ok(()) } From 9f86f6236aea52eee6bb4971f59ea0ce9a4ca588 Mon Sep 17 00:00:00 2001 From: Tim Hobson Date: Thu, 7 Sep 2023 16:30:08 +0100 Subject: [PATCH 05/21] Add functions to generate keys from a mnemonic phrase --- trustchain-ion/Cargo.toml | 1 - trustchain-ion/src/lib.rs | 5 + trustchain-ion/src/mnemonic.rs | 231 ++++++++++++++++++++++++++++++++- 3 files changed, 233 insertions(+), 4 deletions(-) diff --git a/trustchain-ion/Cargo.toml b/trustchain-ion/Cargo.toml index 6165d2f0..c9e8c287 100644 --- a/trustchain-ion/Cargo.toml +++ b/trustchain-ion/Cargo.toml @@ -8,7 +8,6 @@ edition = "2021" [dependencies] trustchain-core = { path = "../trustchain-core" } async-trait = "0.1" -bip32 = {version="*", features = ["secp256k1"]} bip39 = "2.0.0" bitcoin = "0.29.2" bitcoincore-rpc = "0.16.0" diff --git a/trustchain-ion/src/lib.rs b/trustchain-ion/src/lib.rs index 5b7ad739..69d6d067 100644 --- a/trustchain-ion/src/lib.rs +++ b/trustchain-ion/src/lib.rs @@ -144,3 +144,8 @@ pub const NONCE_KEY: &str = "nonce"; // Minimum number of zeros for PoW block hash of root // TODO: set differently for mainnet and testnet with features pub const MIN_POW_ZEROS: usize = 14; + +// BIP32 +pub const SIGNING_KEY_DERIVATION_PATH: &str = "m/0h"; +pub const UPDATE_KEY_DERIVATION_PATH: &str = "m/1h"; +pub const RECOVERY_KEY_DERIVATION_PATH: &str = "m/2h"; diff --git a/trustchain-ion/src/mnemonic.rs b/trustchain-ion/src/mnemonic.rs index e2e13261..4299e5f4 100644 --- a/trustchain-ion/src/mnemonic.rs +++ b/trustchain-ion/src/mnemonic.rs @@ -1,6 +1,40 @@ use bip39::Mnemonic; +use bitcoin::secp256k1::Secp256k1; +use bitcoin::util::bip32::{DerivationPath, ExtendedPrivKey}; use ed25519_hd_key; -use ssi::jwk::{Base64urlUInt, OctetParams, Params, JWK}; +use ssi::jwk::{Base64urlUInt, ECParams, OctetParams, Params, JWK}; +use std::str::FromStr; +use thiserror::Error; + +use crate::{ + RECOVERY_KEY_DERIVATION_PATH, SIGNING_KEY_DERIVATION_PATH, UPDATE_KEY_DERIVATION_PATH, +}; + +/// An error relating to key generation from a mnemonic seed phrase. +#[derive(Error, Debug)] +pub enum MnemonicError { + /// Invalid BIP32 derivation path. + #[error("Invalid BIP32 derivation path.")] + InvalidDerivationPath(bitcoin::util::bip32::Error), + /// Failed to deserialize private scalar. + #[error("Failed to deserialize private scalar bytes.")] + FailedToDeserializeScalar(k256::elliptic_curve::Error), + /// Failed to convert elliptic curve parameters. + #[error("Failed to convert elliptic curve parameters.")] + FailedToConvertECParams, +} + +impl From for MnemonicError { + fn from(err: bitcoin::util::bip32::Error) -> Self { + MnemonicError::InvalidDerivationPath(err) + } +} + +impl From for MnemonicError { + fn from(err: k256::elliptic_curve::Error) -> Self { + MnemonicError::FailedToDeserializeScalar(err) + } +} // See: https://github.com/alepop/dart-ed25519-hd-key/blob/f785c73b1248037df58f8d582e6f71c480e49d39/lib/src/hd_key.dart#L45-L56 fn with_zero_byte(public_key: [u8; 32]) -> [u8; 33] { @@ -9,6 +43,105 @@ fn with_zero_byte(public_key: [u8; 32]) -> [u8; 33] { new_public_key } +/// Generates a signing key on the ed25519 elliptic curve from a mnemonic seed phrase. +fn generate_ed25519_signing_key( + mnemonic: &Mnemonic, + index: Option, +) -> Result { + let seed = mnemonic.to_seed(""); + let path = SIGNING_KEY_DERIVATION_PATH; + let derivation_path = derivation_path(path, index)?; + let (private_key, _chain_code) = + ed25519_hd_key::derive_from_path(&derivation_path.to_string(), &seed); + let public_key = ed25519_hd_key::get_public_key(&private_key); + // For some reason zero byte is required despite the false arg here: + // https://github.com/alan-turing-institute/trustchain-mobile/blob/1b735645fd140b94bf1360bd5546643214c423b6/lib/app/shared/key_generation.dart#L12 + let public_key = with_zero_byte(public_key); + // Make a JWK from bytes: https://docs.rs/ssi/0.4.0/src/ssi/jwk.rs.html#251-265 + Ok(JWK::from(Params::OKP(OctetParams { + curve: "Ed25519".to_string(), + public_key: Base64urlUInt(public_key.to_vec()), + private_key: Some(Base64urlUInt(private_key.to_vec())), + }))) +} + +/// Generates a key on the secp256k1 elliptic curve from a mnemonic seed phrase. +fn generate_secp256k1_key( + mnemonic: &Mnemonic, + path: &str, + index: Option, +) -> Result { + let seed = mnemonic.to_seed(""); + let m = ExtendedPrivKey::new_master(bitcoin::Network::Bitcoin, &seed)?; + let secp = Secp256k1::new(); + let derivation_path = derivation_path(path, index)?; + let xpriv = m.derive_priv(&secp, &derivation_path)?; + let private_key = xpriv.to_priv(); + // let public_key: bitcoin::util::key::PublicKey = private_key.public_key(&secp); + + // Now convert the bitcoin::util::bip32::ExtendedPrivKey into a JWK. + let k256_secret_key = k256::SecretKey::from_bytes(private_key.to_bytes())?; + let k256_public_key = k256_secret_key.public_key(); + let mut ec_params = match ECParams::try_from(&k256_public_key) { + Ok(params) => params, + Err(_) => return Err(MnemonicError::FailedToConvertECParams), + }; + ec_params.ecc_private_key = Some(Base64urlUInt(private_key.to_bytes().to_vec())); + Ok(JWK::from(Params::EC(ec_params))) +} + +fn derivation_path( + path: &str, + index: Option, +) -> Result { + let index = match index { + Some(i) => i, + None => 0, + }; + // Handle case index > 2^31 - 1. + if index > 2u32.pow(31) - 1 { + return Err(bitcoin::util::bip32::Error::InvalidChildNumber(index)); + } + let mut derivation_path = path.to_string(); + derivation_path.push_str("/"); + derivation_path.push_str(&index.to_string()); + derivation_path.push_str("'"); + DerivationPath::from_str(&derivation_path) +} + +/// Generates a DID update key on the secp256k1 elliptic curve from a mnemonic seed phrase. +fn generate_secp256k1_update_key( + mnemonic: &Mnemonic, + index: Option, +) -> Result { + generate_secp256k1_key(mnemonic, UPDATE_KEY_DERIVATION_PATH, index) +} + +/// Generates a DID recovery key on the secp256k1 elliptic curve from a mnemonic seed phrase. +fn generate_secp256k1_recovery_key( + mnemonic: &Mnemonic, + index: Option, +) -> Result { + generate_secp256k1_key(mnemonic, RECOVERY_KEY_DERIVATION_PATH, index) +} +pub struct IONKeys { + signing_key: JWK, + update_key: JWK, + recovery_key: JWK, +} + +/// Generates a set of signing, update and recovery keys from a mnemonic phrase and child index. +pub fn generate_keys(mnemonic: &Mnemonic, index: Option) -> Result { + let signing_key = generate_ed25519_signing_key(mnemonic, index)?; + let update_key = generate_secp256k1_update_key(mnemonic, index)?; + let recovery_key = generate_secp256k1_recovery_key(mnemonic, index)?; + Ok(IONKeys { + signing_key, + update_key, + recovery_key, + }) +} + #[cfg(test)] mod tests { use super::*; @@ -20,6 +153,98 @@ mod tests { use ssi::jwk::JWK; use std::str::FromStr; + fn get_test_mnemonic() -> Mnemonic { + let phrase = "state draft moral repeat knife trend animal pretty delay collect fall adjust"; + Mnemonic::parse(phrase).unwrap() + } + + #[test] + fn test_derivation_path() { + let path = "m/1h"; + let expected = DerivationPath::from_str("m/1h/0h"); + assert_eq!(expected, derivation_path(path, None)); + + let path = "m/1h"; + let index: u32 = 0; + let expected = DerivationPath::from_str("m/1h/0h"); + assert_eq!(expected, derivation_path(path, Some(index))); + + let path = "m/2h"; + let index: u32 = 2147483647; + let expected = DerivationPath::from_str("m/2h/2147483647h"); + assert_eq!(expected, derivation_path(path, Some(index))); + + let path = "m/2h"; + let index: u32 = 2147483647; + let expected = DerivationPath::from_str("m/1h/2147483647h"); + assert_ne!(expected, derivation_path(path, Some(index))); + + let path = "m/1'"; + let index: u32 = 0; + let expected = DerivationPath::from_str("m/1h/0h"); + assert_eq!(expected, derivation_path(path, Some(index))); + + let path = "m/0'"; + let index: u32 = 0; + let expected = DerivationPath::from_str("m/1h/0h"); + assert_ne!(expected, derivation_path(path, Some(index))); + + let derivation_path = DerivationPath::from_str("m/1h/0h").unwrap(); + let expected = "m/1'/0'"; + assert_eq!(expected, derivation_path.to_string()); + + let derivation_path = DerivationPath::from_str("m/1'/0'").unwrap(); + let expected = "m/1'/0'"; + assert_eq!(expected, derivation_path.to_string()); + } + + #[test] + fn test_generate_ed25519_signing_key() -> Result<(), Box> { + let mnemonic = get_test_mnemonic(); + let result = generate_ed25519_signing_key(&mnemonic, Some(22))?; + let expected = r#" + { + "kty": "OKP", + "crv": "Ed25519", + "x": "AFPB4OPrkVLsxAxppQ2QrqcX1vK_dLP1pJfNuwUA8WGA", + "d": "065j_kBmgaYT5JCMbIebOSRkkneHJ83JKkjVrogSamI" + }"#; + assert_eq!(result, serde_json::from_str::(expected)?); + Ok(()) + } + + #[test] + fn test_generate_secp256k1_update_key() -> Result<(), Box> { + let mnemonic = get_test_mnemonic(); + let result = generate_secp256k1_update_key(&mnemonic, None)?; + let expected = r#" + { + "kty": "EC", + "crv": "secp256k1", + "x": "pALnEGubf31SDdZQsbjSXqqDRivTSQXteERlggq3vYI", + "y": "sWjJLyW3dDbqXnYNNPnyeuQGBkBmBH2K_XV3LVCDCDQ", + "d": "LxQYTvQ2naxEl-XsTWxekhMroP4LtkTW5mdOXuoyS0E" + }"#; + assert_eq!(result, serde_json::from_str::(expected)?); + Ok(()) + } + + #[test] + fn test_generate_secp256k1_recovery_key() -> Result<(), Box> { + let mnemonic = get_test_mnemonic(); + let result = generate_secp256k1_recovery_key(&mnemonic, Some(2))?; + let expected = r#" + { + "kty": "EC", + "crv": "secp256k1", + "x": "3SKbKH_8zBKfWi-5_xgsiVlOmnWIOCHcP27VpndhDp8", + "y": "aTNRpMlDjVglXVb4G8PqqAd0Akf95aCyVqPzN636fA8", + "d": "0fJzxc3TJ5T1xx8ZnY90uLJT99QN8Y4pn57i2n0SZH8" + }"#; + assert_eq!(result, serde_json::from_str::(expected)?); + Ok(()) + } + #[test] fn test_mnemonic_secp256k1() -> Result<(), Box> { let phrase = "state draft moral repeat knife trend animal pretty delay collect fall adjust"; @@ -84,8 +309,8 @@ mod tests { public_key: Base64urlUInt(public_key.to_vec()), private_key: Some(Base64urlUInt(private_key.to_vec())), })); - println!("{}", serde_json::to_string_pretty(&jwk).unwrap()); - println!("{}", serde_json::to_string_pretty(&expected_jwk).unwrap()); + // println!("{}", serde_json::to_string_pretty(&jwk).unwrap()); + // println!("{}", serde_json::to_string_pretty(&expected_jwk).unwrap()); assert_eq!(jwk, expected_jwk); } } From 2aab123b37d613d24f26daae963e18bbb8bbea4a Mon Sep 17 00:00:00 2001 From: Sam Greenbury Date: Thu, 7 Sep 2023 17:07:44 +0100 Subject: [PATCH 06/21] Add ION create operation from keys and mboile FFI --- trustchain-ffi/Cargo.toml | 1 + trustchain-ffi/src/mobile.rs | 50 +++++++++++++++++++++++++- trustchain-ffi/src/mobile_bridge.io.rs | 15 ++++++++ trustchain-ffi/src/mobile_bridge.rs | 33 +++++++++++++++++ trustchain-ion/src/create.rs | 27 +++++++++++++- trustchain-ion/src/mnemonic.rs | 6 ++-- 6 files changed, 127 insertions(+), 5 deletions(-) diff --git a/trustchain-ffi/Cargo.toml b/trustchain-ffi/Cargo.toml index 705a2df2..1f4939ba 100644 --- a/trustchain-ffi/Cargo.toml +++ b/trustchain-ffi/Cargo.toml @@ -15,6 +15,7 @@ trustchain-ion = { path = "../trustchain-ion" } trustchain-api = { path = "../trustchain-api" } anyhow = "1.0" +bip39 = "2.0.0" chrono = "0.4.26" did-ion="0.1.0" flutter_rust_bridge = "1" diff --git a/trustchain-ffi/src/mobile.rs b/trustchain-ffi/src/mobile.rs index 665514f3..7af2a2ce 100644 --- a/trustchain-ffi/src/mobile.rs +++ b/trustchain-ffi/src/mobile.rs @@ -1,7 +1,13 @@ // TODO: add module doc comments for mobile FFI use crate::config::FFIConfig; use anyhow::Result; +use bip39::Mnemonic; use chrono::{DateTime, Utc}; +use did_ion::{ + sidetree::{CreateOperation, PublicKeyEntry, PublicKeyJwk, Sidetree, SidetreeDID}, + ION, +}; +use serde::{Deserialize, Serialize}; use ssi::{ jwk::JWK, ldp::now_ms, @@ -15,7 +21,7 @@ use trustchain_api::{ TrustchainAPI, }; use trustchain_core::{resolver::ResolverError, vc::CredentialError, verifier::VerifierError}; -use trustchain_ion::{get_ion_resolver, verifier::IONVerifier}; +use trustchain_ion::{create::create_operation_from_keys, get_ion_resolver, verifier::IONVerifier}; /// A speicfic error for FFI mobile making handling easier. #[derive(Error, Debug)] @@ -163,6 +169,41 @@ pub fn vp_issue_presentation( // todo!() // } +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +struct CreateOperationAndDID { + create_operation: CreateOperation, + did: String, +} + +pub fn ion_create_operation(phrase: String) -> Result { + // 1. Generate keys + let mnemonic = Mnemonic::parse(phrase)?; + let ion_keys = trustchain_ion::mnemonic::generate_keys(&mnemonic, None)?; + ION::validate_key(&ion_keys.update_key)?; + ION::validate_key(&ion_keys.recovery_key)?; + let signing_public_key = PublicKeyEntry::try_from(ion_keys.signing_key.clone())?; + let update_public_key = PublicKeyJwk::try_from(ion_keys.update_key.to_public())?; + let recovery_public_key = PublicKeyJwk::try_from(ion_keys.recovery_key.to_public())?; + + // 2. Call create with keys as args + let create_operation = create_operation_from_keys( + &signing_public_key, + &update_public_key, + &recovery_public_key, + ) + .unwrap(); + // 3. Get DID from create operation + // Get DID information + let did = SidetreeDID::::from_create_operation(&create_operation)?.to_string(); + let did = did.rsplit_once(':').unwrap().0.to_string(); + // 4. Return DID and create operation as JSON + Ok(serde_json::to_string_pretty(&CreateOperationAndDID { + create_operation, + did, + })?) +} + #[cfg(test)] mod tests { use crate::config::parse_toml; @@ -250,4 +291,11 @@ mod tests { // // TODO: implement once verifiable presentations are included in API // #[test] // fn test_vc_verify_presentation() {} + + #[test] + fn test_ion_create_operation() { + let phrase = "state draft moral repeat knife trend animal pretty delay collect fall adjust"; + let create_op_and_did = ion_create_operation(phrase.to_string()).unwrap(); + println!("{}", create_op_and_did); + } } diff --git a/trustchain-ffi/src/mobile_bridge.io.rs b/trustchain-ffi/src/mobile_bridge.io.rs index 100321d7..a1fc9d09 100644 --- a/trustchain-ffi/src/mobile_bridge.io.rs +++ b/trustchain-ffi/src/mobile_bridge.io.rs @@ -33,6 +33,21 @@ pub extern "C" fn wire_vc_verify_credential( wire_vc_verify_credential_impl(port_, credential, opts) } +#[no_mangle] +pub extern "C" fn wire_vp_issue_presentation( + port_: i64, + presentation: *mut wire_uint_8_list, + opts: *mut wire_uint_8_list, + jwk_json: *mut wire_uint_8_list, +) { + wire_vp_issue_presentation_impl(port_, presentation, opts, jwk_json) +} + +#[no_mangle] +pub extern "C" fn wire_ion_create_operation(port_: i64, phrase: *mut wire_uint_8_list) { + wire_ion_create_operation_impl(port_, phrase) +} + // Section: allocate functions #[no_mangle] diff --git a/trustchain-ffi/src/mobile_bridge.rs b/trustchain-ffi/src/mobile_bridge.rs index 418b8e27..0222a520 100644 --- a/trustchain-ffi/src/mobile_bridge.rs +++ b/trustchain-ffi/src/mobile_bridge.rs @@ -85,6 +85,39 @@ fn wire_vc_verify_credential_impl( }, ) } +fn wire_vp_issue_presentation_impl( + port_: MessagePort, + presentation: impl Wire2Api + UnwindSafe, + opts: impl Wire2Api + UnwindSafe, + jwk_json: impl Wire2Api + UnwindSafe, +) { + FLUTTER_RUST_BRIDGE_HANDLER.wrap( + WrapInfo { + debug_name: "vp_issue_presentation", + port: Some(port_), + mode: FfiCallMode::Normal, + }, + move || { + let api_presentation = presentation.wire2api(); + let api_opts = opts.wire2api(); + let api_jwk_json = jwk_json.wire2api(); + move |task_callback| vp_issue_presentation(api_presentation, api_opts, api_jwk_json) + }, + ) +} +fn wire_ion_create_operation_impl(port_: MessagePort, phrase: impl Wire2Api + UnwindSafe) { + FLUTTER_RUST_BRIDGE_HANDLER.wrap( + WrapInfo { + debug_name: "ion_create_operation", + port: Some(port_), + mode: FfiCallMode::Normal, + }, + move || { + let api_phrase = phrase.wire2api(); + move |task_callback| ion_create_operation(api_phrase) + }, + ) +} // Section: wrapper structs // Section: static checks diff --git a/trustchain-ion/src/create.rs b/trustchain-ion/src/create.rs index 4d3436d5..aa138f11 100644 --- a/trustchain-ion/src/create.rs +++ b/trustchain-ion/src/create.rs @@ -1,7 +1,7 @@ //! ION operation for DID creation. use crate::attestor::{AttestorData, IONAttestor}; use crate::controller::{ControllerData, IONController}; -use did_ion::sidetree::DIDStatePatch; +use did_ion::sidetree::{CreateOperation, DIDStatePatch}; use did_ion::sidetree::{DocumentState, PublicKeyEntry, PublicKeyJwk}; use did_ion::sidetree::{Operation, Sidetree, SidetreeDID, SidetreeOperation}; use did_ion::ION; @@ -11,6 +11,31 @@ use ssi::one_or_many::OneOrMany; use std::convert::TryFrom; use trustchain_core::utils::{generate_key, get_operations_path}; +/// Makes a new DID given public signing, update and recovery keys. +pub fn create_operation_from_keys( + signing_public_key: &PublicKeyEntry, + update_public_key: &PublicKeyJwk, + recovery_public_key: &PublicKeyJwk, +) -> Result> { + // Create operation: Make the create patch from scratch or passed file + let document_state = DocumentState { + public_keys: Some(vec![signing_public_key.to_owned()]), + services: None, + }; + let patches = vec![DIDStatePatch::Replace { + document: document_state, + }]; + // Make the create operation from patches + let operation = ION::create_existing(update_public_key, recovery_public_key, patches).unwrap(); + // Verify operation + operation.clone().partial_verify::()?; + let create_operation = match operation.clone() { + Operation::Create(x) => x, + _ => panic!(), + }; + Ok(create_operation) +} + /// Makes a new DID subject to be controlled with correspondong create operation written to file. pub fn create_operation( document_state: Option, diff --git a/trustchain-ion/src/mnemonic.rs b/trustchain-ion/src/mnemonic.rs index 4299e5f4..6a36746f 100644 --- a/trustchain-ion/src/mnemonic.rs +++ b/trustchain-ion/src/mnemonic.rs @@ -125,9 +125,9 @@ fn generate_secp256k1_recovery_key( generate_secp256k1_key(mnemonic, RECOVERY_KEY_DERIVATION_PATH, index) } pub struct IONKeys { - signing_key: JWK, - update_key: JWK, - recovery_key: JWK, + pub signing_key: JWK, + pub update_key: JWK, + pub recovery_key: JWK, } /// Generates a set of signing, update and recovery keys from a mnemonic phrase and child index. From 9993b0307e310151ea55934b5a3010fc4bb207d2 Mon Sep 17 00:00:00 2001 From: Sam Greenbury Date: Sat, 30 Sep 2023 10:03:41 +0100 Subject: [PATCH 07/21] Add test case --- trustchain-ffi/src/mobile.rs | 45 +++++++++++++++++++++++++++++++++++- 1 file changed, 44 insertions(+), 1 deletion(-) diff --git a/trustchain-ffi/src/mobile.rs b/trustchain-ffi/src/mobile.rs index 17ae3bb4..43a013f7 100644 --- a/trustchain-ffi/src/mobile.rs +++ b/trustchain-ffi/src/mobile.rs @@ -216,6 +216,7 @@ pub fn ion_create_operation(phrase: String) -> Result { #[cfg(test)] mod tests { use ssi::vc::CredentialOrJWT; + use trustchain_core::utils::canonicalize_str; use crate::config::parse_toml; @@ -261,6 +262,45 @@ mod tests { } "#; + const TEST_ION_CREATE_OPERATION: &str = r#" + { + "createOperation": { + "delta": { + "patches": [ + { + "action": "replace", + "document": { + "publicKeys": [ + { + "id": "--3rT8-n9BDn51S_rkdxR3G6J_a8YQlsQ8OIAx1Qqkk", + "publicKeyJwk": { + "crv": "Ed25519", + "kty": "OKP", + "x": "AI4pdGWalv3JXZcatmtBM8OfSIBCFC0o_RNzTg-mEAh6" + }, + "purposes": [ + "assertionMethod", + "authentication", + "keyAgreement", + "capabilityInvocation", + "capabilityDelegation" + ], + "type": "JsonWebSignature2020" + } + ] + } + } + ], + "updateCommitment": "EiA2gSveT83s4DD4kJp6tLJuPfy_M3m_m6NtRJzjwtrlDg" + }, + "suffixData": { + "deltaHash": "EiCzptro682kdNxcWXZ7xBnSzZqKu9jphkAfsXmU-7UA-g", + "recoveryCommitment": "EiDKEn4lG5ETCoQpQxAsMVahzuerhlk0rtqtuoHPYKEEog" + } + }, + "did": "did:ion:test:EiB_YFqM3CcO93dnjlNWfljYesSUQxP_tzxEOQsXE3T4MQ" + }"#; + #[test] #[ignore = "integration test requires ION, MongoDB, IPFS and Bitcoin RPC"] fn test_did_resolve() { @@ -335,6 +375,9 @@ mod tests { fn test_ion_create_operation() { let phrase = "state draft moral repeat knife trend animal pretty delay collect fall adjust"; let create_op_and_did = ion_create_operation(phrase.to_string()).unwrap(); - println!("{}", create_op_and_did); + assert_eq!( + canonicalize_str::(&create_op_and_did).unwrap(), + canonicalize_str::(TEST_ION_CREATE_OPERATION).unwrap() + ); } } From 1d8c0cbbf9c63940a528a37ce477bc1bf7d7312e Mon Sep 17 00:00:00 2001 From: Sam Greenbury Date: Sun, 1 Oct 2023 20:15:50 +0100 Subject: [PATCH 08/21] Add create from phrase and trait, refactor create to remove repeated code --- trustchain-ffi/src/mobile.rs | 45 +++---- trustchain-ffi/src/mobile_bridge.io.rs | 4 +- trustchain-ffi/src/mobile_bridge.rs | 9 +- trustchain-ion/src/create.rs | 179 ++++++++++++++++++------- trustchain-ion/src/mnemonic.rs | 4 + 5 files changed, 158 insertions(+), 83 deletions(-) diff --git a/trustchain-ffi/src/mobile.rs b/trustchain-ffi/src/mobile.rs index 43a013f7..027a7ecb 100644 --- a/trustchain-ffi/src/mobile.rs +++ b/trustchain-ffi/src/mobile.rs @@ -1,12 +1,8 @@ //! Mobile FFI. use crate::config::FFIConfig; use anyhow::Result; -use bip39::Mnemonic; use chrono::{DateTime, Utc}; -use did_ion::{ - sidetree::{CreateOperation, PublicKeyEntry, PublicKeyJwk, Sidetree, SidetreeDID}, - ION, -}; +use did_ion::sidetree::CreateOperation; use serde::{Deserialize, Serialize}; use ssi::{ jwk::JWK, @@ -23,7 +19,11 @@ use trustchain_api::{ use trustchain_core::{ resolver::ResolverError, vc::CredentialError, verifier::VerifierError, vp::PresentationError, }; -use trustchain_ion::{create::create_operation_from_keys, get_ion_resolver, verifier::IONVerifier}; +use trustchain_ion::{ + create::{phrase_to_create_and_keys, OperationDID}, + get_ion_resolver, + verifier::IONVerifier, +}; /// A speicfic error for FFI mobile making handling easier. #[derive(Error, Debug)] @@ -46,6 +46,8 @@ pub enum FFIMobileError { FutureProofCreatedTime(DateTime, DateTime), #[error("Failed to issue presentation error: {0}.")] FailedToIssuePresentation(PresentationError), + #[error("Failed to make create operation from phrase: {0}.")] + FailedCreateOperation(String), } /// Example greet function. @@ -184,32 +186,17 @@ struct CreateOperationAndDID { did: String, } -pub fn ion_create_operation(phrase: String) -> Result { - // Generate keys from mnemonic - let mnemonic = Mnemonic::parse(phrase)?; - let ion_keys = trustchain_ion::mnemonic::generate_keys(&mnemonic, None)?; - ION::validate_key(&ion_keys.update_key)?; - ION::validate_key(&ion_keys.recovery_key)?; - let signing_public_key = PublicKeyEntry::try_from(ion_keys.signing_key.clone())?; - let update_public_key = PublicKeyJwk::try_from(ion_keys.update_key.to_public())?; - let recovery_public_key = PublicKeyJwk::try_from(ion_keys.recovery_key.to_public())?; - - // Call create with keys as args - let create_operation = create_operation_from_keys( - &signing_public_key, - &update_public_key, - &recovery_public_key, - ) - .unwrap(); - - // Get DID from create operation - let did = SidetreeDID::::from_create_operation(&create_operation)?.to_string(); - let did = did.rsplit_once(':').unwrap().0.to_string(); +/// Makes a new ION DID from a mnemonic phrase. +// TODO: consider optional index in API +pub fn create_operation_phrase(phrase: String) -> Result { + // Generate create operation from phrase + let (create_operation, _) = phrase_to_create_and_keys(&phrase, 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, - did, })?) } @@ -374,7 +361,7 @@ mod tests { #[test] fn test_ion_create_operation() { let phrase = "state draft moral repeat knife trend animal pretty delay collect fall adjust"; - let create_op_and_did = ion_create_operation(phrase.to_string()).unwrap(); + let create_op_and_did = create_operation_phrase(phrase.to_string()).unwrap(); assert_eq!( canonicalize_str::(&create_op_and_did).unwrap(), canonicalize_str::(TEST_ION_CREATE_OPERATION).unwrap() diff --git a/trustchain-ffi/src/mobile_bridge.io.rs b/trustchain-ffi/src/mobile_bridge.io.rs index a1fc9d09..625175fd 100644 --- a/trustchain-ffi/src/mobile_bridge.io.rs +++ b/trustchain-ffi/src/mobile_bridge.io.rs @@ -44,8 +44,8 @@ pub extern "C" fn wire_vp_issue_presentation( } #[no_mangle] -pub extern "C" fn wire_ion_create_operation(port_: i64, phrase: *mut wire_uint_8_list) { - wire_ion_create_operation_impl(port_, phrase) +pub extern "C" fn wire_create_operation_phrase(port_: i64, phrase: *mut wire_uint_8_list) { + wire_create_operation_phrase_impl(port_, phrase) } // Section: allocate functions diff --git a/trustchain-ffi/src/mobile_bridge.rs b/trustchain-ffi/src/mobile_bridge.rs index 0222a520..6f96395c 100644 --- a/trustchain-ffi/src/mobile_bridge.rs +++ b/trustchain-ffi/src/mobile_bridge.rs @@ -105,16 +105,19 @@ fn wire_vp_issue_presentation_impl( }, ) } -fn wire_ion_create_operation_impl(port_: MessagePort, phrase: impl Wire2Api + UnwindSafe) { +fn wire_create_operation_phrase_impl( + port_: MessagePort, + phrase: impl Wire2Api + UnwindSafe, +) { FLUTTER_RUST_BRIDGE_HANDLER.wrap( WrapInfo { - debug_name: "ion_create_operation", + debug_name: "create_operation_phrase", port: Some(port_), mode: FfiCallMode::Normal, }, move || { let api_phrase = phrase.wire2api(); - move |task_callback| ion_create_operation(api_phrase) + move |task_callback| create_operation_phrase(api_phrase) }, ) } diff --git a/trustchain-ion/src/create.rs b/trustchain-ion/src/create.rs index aa138f11..50027e5a 100644 --- a/trustchain-ion/src/create.rs +++ b/trustchain-ion/src/create.rs @@ -1,6 +1,8 @@ //! ION operation for DID creation. use crate::attestor::{AttestorData, IONAttestor}; use crate::controller::{ControllerData, IONController}; +use crate::mnemonic::IONKeys; +use bip39::Mnemonic; use did_ion::sidetree::{CreateOperation, DIDStatePatch}; use did_ion::sidetree::{DocumentState, PublicKeyEntry, PublicKeyJwk}; use did_ion::sidetree::{Operation, Sidetree, SidetreeDID, SidetreeOperation}; @@ -9,10 +11,77 @@ use serde_json::to_string_pretty as to_json; use ssi::jwk::JWK; use ssi::one_or_many::OneOrMany; use std::convert::TryFrom; +use trustchain_core::controller::Controller; use trustchain_core::utils::{generate_key, get_operations_path}; +/// Collection of methods to return DID information from an operation. +pub trait OperationDID { + /// Associated type for DID method specification such as `ION` for `Sidetree`. + type T; + /// Returns the DID suffix. + fn to_did_suffix(&self) -> String; + /// Returns the short-form DID. + fn to_did(&self) -> String; + /// Returns the long-form DID. + fn to_did_long(&self) -> String; +} + +impl OperationDID for CreateOperation { + type T = ION; + fn to_did_suffix(&self) -> String { + Self::T::serialize_suffix_data(&self.suffix_data) + .unwrap() + .to_string() + } + fn to_did(&self) -> String { + self.to_did_long().rsplit_once(':').unwrap().0.to_string() + } + fn to_did_long(&self) -> String { + SidetreeDID::::from_create_operation(self) + .unwrap() + .to_string() + } +} + +/// Writes attestor, controller and create operation. +fn write_create_operation( + create_operation: CreateOperation, + signing_key: Option, + update_key: JWK, + recovery_key: JWK, +) -> Result> { + // Get DID + let controlled_did = create_operation.to_did(); + + // Make attestor + if let Some(signing_key) = signing_key { + IONAttestor::try_from(AttestorData::new( + controlled_did.to_string(), + OneOrMany::One(signing_key), + ))?; + } + + // Write controller data: DID is arbitrarily set to contolled_did in creation + let controller = IONController::try_from(ControllerData::new( + controlled_did.to_string(), + controlled_did.to_string(), + update_key, + recovery_key, + )) + .unwrap(); + + // Write create operation to push to ION server + let path = get_operations_path().unwrap(); + let filename = format!( + "create_operation_{}.json", + controller.controlled_did_suffix() + ); + std::fs::write(path.join(&filename), to_json(&create_operation).unwrap())?; + Ok(filename) +} + /// Makes a new DID given public signing, update and recovery keys. -pub fn create_operation_from_keys( +fn create_operation_from_keys( signing_public_key: &PublicKeyEntry, update_public_key: &PublicKeyJwk, recovery_public_key: &PublicKeyJwk, @@ -36,7 +105,7 @@ pub fn create_operation_from_keys( Ok(create_operation) } -/// Makes a new DID subject to be controlled with correspondong create operation written to file. +/// Makes a new DID subject to be controlled with corresponding create operation written to file. pub fn create_operation( document_state: Option, verbose: bool, @@ -74,69 +143,81 @@ pub fn create_operation( ) }; + // Construct patches let patches = vec![DIDStatePatch::Replace { document: document_state, }]; // Make the create operation from patches - let operation = ION::create_existing(&update_pk, &recovery_pk, patches).unwrap(); + let operation = ION::create_existing(&update_pk, &recovery_pk, patches)?; - // Verify operation - let partially_verified_create_operation = operation.clone().partial_verify::(); - if verbose { - println!( - "Partially verified create: {}", - partially_verified_create_operation.is_ok() - ); - } + // Partially verify operation + operation.clone().partial_verify::()?; - let create_operation = match operation.clone() { - Operation::Create(x) => Some(x), - _ => None, + // Extract data from operation + let create_operation = match operation { + Operation::Create(x) => x, + _ => panic!("Operation is not expected 'Create' type."), }; + // Get DID information + let controlled_did_suffix = create_operation.to_did_suffix(); + let controlled_did_long = create_operation.to_did_long(); + let controlled_did = create_operation.to_did(); + + // Verbose output if verbose { println!("Create operation:"); println!("{}", to_json(&create_operation).unwrap()); - } - - // Get DID information - let controlled_did_suffix = - ION::serialize_suffix_data(&create_operation.clone().unwrap().suffix_data) - .unwrap() - .to_string(); - let controlled_did_long = SidetreeDID::::from_create_operation(&create_operation.unwrap()) - .unwrap() - .to_string(); - let controlled_did = controlled_did_long.rsplit_once(':').unwrap().0; - if verbose { println!("Controlled DID suffix: {:?}", controlled_did_suffix); println!("Controlled DID (short-form): {:?}", controlled_did); println!("Controlled DID (long-form) : {:?}", controlled_did_long); } - - // If a signing key has been generated, IONAttestor needs to be saved - if let Some(signing_key) = generated_signing_key { - IONAttestor::try_from(AttestorData::new( - controlled_did.to_string(), - OneOrMany::One(signing_key), - ))?; - } - - // Write controller data: DID is arbitrarily set to contolled_did in creation - IONController::try_from(ControllerData::new( - controlled_did.to_string(), - controlled_did.to_string(), + // Write operation and keys + write_create_operation( + create_operation, + generated_signing_key, update_key, recovery_key, - ))?; + ) +} - // Write create operation to push to ION server - let path = get_operations_path()?; - let filename = format!("create_operation_{}.json", controlled_did_suffix); - std::fs::write(path.join(&filename), to_json(&operation).unwrap())?; +/// Generates a create operation and corresponding keys from a mnemonic phrase. +pub fn phrase_to_create_and_keys( + phrase: &str, + index: Option, +) -> Result<(CreateOperation, IONKeys), Box> { + let ion_keys = crate::mnemonic::generate_keys(&Mnemonic::parse(phrase)?, index)?; + let signing_public_key = PublicKeyEntry::try_from(ion_keys.signing_key.clone())?; + let update_public_key = PublicKeyJwk::try_from(ion_keys.update_key.to_public())?; + let recovery_public_key = PublicKeyJwk::try_from(ion_keys.recovery_key.to_public())?; - Ok(filename) + // Construct create operation + let create_operation = create_operation_from_keys( + &signing_public_key, + &update_public_key, + &recovery_public_key, + ) + .map_err(|err| err.to_string())?; + Ok((create_operation, ion_keys)) +} + +/// Makes a new DID subject to be controlled with corresponding create operation written to file +/// from a mnemonic phrase. +pub fn create_operation_phrase( + phrase: &str, + index: Option, +) -> Result> { + // Generate operation and keys + let (create_operation, ion_keys) = phrase_to_create_and_keys(phrase, index)?; + + // Write create operation + write_create_operation( + create_operation, + Some(ion_keys.signing_key), + ion_keys.update_key, + ion_keys.recovery_key, + ) } #[cfg(test)] @@ -146,7 +227,7 @@ mod test { use trustchain_core::utils::init; // Test document state for making a create operation from - const TEST_DOC_STATE: &str = r##"{ + const TEST_DOC_STATE: &str = r#"{ "publicKeys": [ { "id": "Mz94EfSCueClM5qv62SXxtLWRj4Ti7rR2wLWmW37aCs", @@ -176,10 +257,10 @@ mod test { } } ] - }"##; + }"#; #[test] - fn test_main_create() -> Result<(), Box> { + fn test_create() -> Result<(), Box> { init(); // 1. Run create with no document state passed @@ -189,7 +270,7 @@ mod test { let doc_state: DocumentState = serde_json::from_reader(TEST_DOC_STATE.as_bytes())?; create_operation(Some(doc_state), false)?; - // Try to read outputted create operations and check they deserialize + // Try to read outputted create operations and check they deserialize let path = get_operations_path()?; let pattern = path.join("create_operation_*.json"); let pattern = pattern.into_os_string().into_string().unwrap(); diff --git a/trustchain-ion/src/mnemonic.rs b/trustchain-ion/src/mnemonic.rs index 6a36746f..ab641b0a 100644 --- a/trustchain-ion/src/mnemonic.rs +++ b/trustchain-ion/src/mnemonic.rs @@ -1,6 +1,8 @@ use bip39::Mnemonic; use bitcoin::secp256k1::Secp256k1; use bitcoin::util::bip32::{DerivationPath, ExtendedPrivKey}; +use did_ion::sidetree::Sidetree; +use did_ion::ION; use ed25519_hd_key; use ssi::jwk::{Base64urlUInt, ECParams, OctetParams, Params, JWK}; use std::str::FromStr; @@ -135,6 +137,8 @@ pub fn generate_keys(mnemonic: &Mnemonic, index: Option) -> Result Date: Mon, 2 Oct 2023 09:15:44 +0100 Subject: [PATCH 09/21] Add ion operations route --- trustchain-http/src/errors.rs | 6 +++++ trustchain-http/src/ion.rs | 42 +++++++++++++++++++++++++++++++++++ trustchain-http/src/lib.rs | 1 + trustchain-http/src/server.rs | 9 +++++++- 4 files changed, 57 insertions(+), 1 deletion(-) create mode 100644 trustchain-http/src/ion.rs diff --git a/trustchain-http/src/errors.rs b/trustchain-http/src/errors.rs index a13b460a..c2330432 100644 --- a/trustchain-http/src/errors.rs +++ b/trustchain-http/src/errors.rs @@ -24,6 +24,8 @@ pub enum TrustchainHTTPError { CredentialDoesNotExist, #[error("No issuer available.")] NoCredentialIssuer, + #[error("Wrapped reqwest error: {0}")] + ReqwestError(reqwest::Error), } impl From for TrustchainHTTPError { @@ -83,6 +85,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(), + ), }; let body = Json(json!({ "error": err_message })); (status, body).into_response() diff --git a/trustchain-http/src/ion.rs b/trustchain-http/src/ion.rs new file mode 100644 index 00000000..7c957678 --- /dev/null +++ b/trustchain-http/src/ion.rs @@ -0,0 +1,42 @@ +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, + _app_state: Arc, +) -> impl IntoResponse { + info!("Received ION operation: {}", operation); + let client = reqwest::Client::new(); + client + // TODO: Add config for ION URL + .post("http://localhost:3000/operations") + .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::*; + // TODO: add test by setting up: + // - a mock server as the ION server + // - a test server with the `/operations` route + // And then sending a POST reqwest and testing response +} diff --git a/trustchain-http/src/lib.rs b/trustchain-http/src/lib.rs index 4cfd47b5..1138d54e 100644 --- a/trustchain-http/src/lib.rs +++ b/trustchain-http/src/lib.rs @@ -1,5 +1,6 @@ pub mod config; pub mod errors; +pub mod ion; pub mod issuer; pub mod middleware; pub mod qrcode; diff --git a/trustchain-http/src/server.rs b/trustchain-http/src/server.rs index 07c94cfb..f48bc1fa 100644 --- a/trustchain-http/src/server.rs +++ b/trustchain-http/src/server.rs @@ -1,6 +1,6 @@ use crate::middleware::validate_did; use crate::{config::HTTPConfig, issuer, resolver, state::AppState, static_handlers, verifier}; -use axum::routing::IntoMakeService; +use axum::routing::{post, IntoMakeService}; use axum::{middleware, routing::get, Router}; use hyper::server::conn::AddrIncoming; use std::sync::Arc; @@ -65,6 +65,13 @@ impl TrustchainRouter { "/did/bundle/:id", get(resolver::TrustchainHTTPHandler::get_verification_bundle), ) + .route( + "/operations", + post({ + let state = shared_state.clone(); + move |operation| crate::ion::post_operation(operation, state) + }), + ) .with_state(shared_state), } } From 5980e3b0d4d8fe4350b70098deed5e89bc061073 Mon Sep 17 00:00:00 2001 From: Sam Greenbury Date: Fri, 13 Oct 2023 07:42:52 +0100 Subject: [PATCH 10/21] Add create with mnemonic to cli --- trustchain-cli/src/bin/main.rs | 29 +++++++++++++++++++---------- 1 file changed, 19 insertions(+), 10 deletions(-) diff --git a/trustchain-cli/src/bin/main.rs b/trustchain-cli/src/bin/main.rs index f8f8748c..f6496613 100644 --- a/trustchain-cli/src/bin/main.rs +++ b/trustchain-cli/src/bin/main.rs @@ -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_phrase}, + get_ion_resolver, + verifier::IONVerifier, }; fn cli() -> Command { @@ -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 ).required(false)), ) .subcommand( @@ -93,16 +97,21 @@ async fn main() -> Result<(), Box> { Some(("create", sub_matches)) => { let file_path = sub_matches.get_one::("file_path"); let verbose = matches!(sub_matches.get_one::("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::("mnemonic"), Some(true)); + 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_phrase(&mnemonic, None)?; + } } Some(("attest", sub_matches)) => { let did = sub_matches.get_one::("did").unwrap(); From eb508b6be148e707e76617cadb53d3551f3d62cc Mon Sep 17 00:00:00 2001 From: Sam Greenbury Date: Fri, 20 Oct 2023 11:14:33 +0100 Subject: [PATCH 11/21] Update to use ed25519_dalek_bip32 crate --- trustchain-ffi/src/mobile.rs | 7 +- trustchain-http/Cargo.toml | 2 +- trustchain-http/src/server.rs | 2 +- trustchain-ion/Cargo.toml | 4 +- trustchain-ion/src/create.rs | 6 +- trustchain-ion/src/mnemonic.rs | 127 ++++++++++++++++++++------------- 6 files changed, 92 insertions(+), 56 deletions(-) diff --git a/trustchain-ffi/src/mobile.rs b/trustchain-ffi/src/mobile.rs index c1c6c095..a0aab098 100644 --- a/trustchain-ffi/src/mobile.rs +++ b/trustchain-ffi/src/mobile.rs @@ -2,7 +2,7 @@ use crate::config::FFIConfig; use anyhow::Result; use chrono::{DateTime, Utc}; -use did_ion::sidetree::CreateOperation; +use did_ion::sidetree::Operation; use serde::{Deserialize, Serialize}; use ssi::{ jsonld::ContextLoader, @@ -184,7 +184,7 @@ pub fn vp_issue_presentation( #[derive(Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] struct CreateOperationAndDID { - create_operation: CreateOperation, + create_operation: Operation, did: String, } @@ -198,7 +198,7 @@ pub fn create_operation_phrase(phrase: String) -> Result { // Return DID and create operation as JSON Ok(serde_json::to_string_pretty(&CreateOperationAndDID { did: create_operation.to_did(), - create_operation, + create_operation: Operation::Create(create_operation), })?) } @@ -254,6 +254,7 @@ mod tests { const TEST_ION_CREATE_OPERATION: &str = r#" { "createOperation": { + "type": "create", "delta": { "patches": [ { diff --git a/trustchain-http/Cargo.toml b/trustchain-http/Cargo.toml index fad55fc8..b62f9ba6 100644 --- a/trustchain-http/Cargo.toml +++ b/trustchain-http/Cargo.toml @@ -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" diff --git a/trustchain-http/src/server.rs b/trustchain-http/src/server.rs index 4256a17a..82546dc9 100644 --- a/trustchain-http/src/server.rs +++ b/trustchain-http/src/server.rs @@ -3,7 +3,7 @@ use crate::middleware::validate_did; use crate::{ config::HTTPConfig, issuer, resolver, root, state::AppState, static_handlers, verifier, }; -use axum::routing::{IntoMakeService, post}; +use axum::routing::{post, IntoMakeService}; use axum::{middleware, routing::get, Router}; use axum_server::tls_rustls::RustlsConfig; use hyper::server::conn::AddrIncoming; diff --git a/trustchain-ion/Cargo.toml b/trustchain-ion/Cargo.toml index 75a35508..03859bcd 100644 --- a/trustchain-ion/Cargo.toml +++ b/trustchain-ion/Cargo.toml @@ -17,13 +17,13 @@ canonical_json = "0.4.0" chrono = "0.4" clap = { version = "^4.1", features=["derive", "cargo"] } did-ion = {git="https://github.com/alan-turing-institute/ssi.git", branch="modify-encode-sign-jwt"} -ed25519-hd-key = "0.3.0" +ed25519-dalek-bip32 = "0.3.0" flate2 = "1.0.24" futures = "0.3.21" hex = "0.4.3" ipfs-api-backend-hyper = {version="0.6", features = ["with-send-sync"]} ipfs-hasher = "0.13.0" -k256 = "0.9.6" +k256 = "0.13.1" lazy_static="1.4.0" mongodb = "2.3.1" reqwest = "0.11" diff --git a/trustchain-ion/src/create.rs b/trustchain-ion/src/create.rs index 07731f53..39d7fe16 100644 --- a/trustchain-ion/src/create.rs +++ b/trustchain-ion/src/create.rs @@ -76,7 +76,10 @@ fn write_create_operation( "create_operation_{}.json", controller.controlled_did_suffix() ); - std::fs::write(path.join(&filename), to_json(&create_operation).unwrap())?; + std::fs::write( + path.join(&filename), + to_json(&Operation::Create(create_operation)).unwrap(), + )?; Ok(filename) } @@ -280,6 +283,7 @@ mod test { for path in paths { if let Ok(path_buf) = path { let operation_string = std::fs::read_to_string(path_buf)?; + println!("{}", operation_string); let _operation: Operation = serde_json::from_str(&operation_string)?; operation_count += 1; } else { diff --git a/trustchain-ion/src/mnemonic.rs b/trustchain-ion/src/mnemonic.rs index ab641b0a..8a7ebc53 100644 --- a/trustchain-ion/src/mnemonic.rs +++ b/trustchain-ion/src/mnemonic.rs @@ -3,7 +3,7 @@ use bitcoin::secp256k1::Secp256k1; use bitcoin::util::bip32::{DerivationPath, ExtendedPrivKey}; use did_ion::sidetree::Sidetree; use did_ion::ION; -use ed25519_hd_key; +use ed25519_dalek_bip32::ExtendedSigningKey; use ssi::jwk::{Base64urlUInt, ECParams, OctetParams, Params, JWK}; use std::str::FromStr; use thiserror::Error; @@ -16,10 +16,16 @@ use crate::{ #[derive(Error, Debug)] pub enum MnemonicError { /// Invalid BIP32 derivation path. - #[error("Invalid BIP32 derivation path.")] + #[error("Invalid BIP32 derivation path: {0}")] InvalidDerivationPath(bitcoin::util::bip32::Error), + /// Invalid BIP32 derivation path. + #[error("Wrapped ed25519_dalek_bip32 error: {0}")] + Ed25519DalekBip32Error(ed25519_dalek_bip32::Error), + /// Invalid ed25519 BIP32 derivation path. + #[error("Invalid ed25519 BIP32 derivation path: {0}")] + InvalidDerivationPathEd25519(ed25519_dalek_bip32::derivation_path::DerivationPathParseError), /// Failed to deserialize private scalar. - #[error("Failed to deserialize private scalar bytes.")] + #[error("Failed to deserialize private scalar bytes: {0}")] FailedToDeserializeScalar(k256::elliptic_curve::Error), /// Failed to convert elliptic curve parameters. #[error("Failed to convert elliptic curve parameters.")] @@ -38,6 +44,19 @@ impl From for MnemonicError { } } +// TODO: add imports to module +impl From for MnemonicError { + fn from(err: ed25519_dalek_bip32::derivation_path::DerivationPathParseError) -> Self { + MnemonicError::InvalidDerivationPathEd25519(err) + } +} + +impl From for MnemonicError { + fn from(err: ed25519_dalek_bip32::Error) -> Self { + MnemonicError::Ed25519DalekBip32Error(err) + } +} + // See: https://github.com/alepop/dart-ed25519-hd-key/blob/f785c73b1248037df58f8d582e6f71c480e49d39/lib/src/hd_key.dart#L45-L56 fn with_zero_byte(public_key: [u8; 32]) -> [u8; 33] { let mut new_public_key = [0u8; 33]; @@ -51,11 +70,10 @@ fn generate_ed25519_signing_key( index: Option, ) -> Result { let seed = mnemonic.to_seed(""); - let path = SIGNING_KEY_DERIVATION_PATH; - let derivation_path = derivation_path(path, index)?; - let (private_key, _chain_code) = - ed25519_hd_key::derive_from_path(&derivation_path.to_string(), &seed); - let public_key = ed25519_hd_key::get_public_key(&private_key); + let extended_secret_key = ExtendedSigningKey::from_seed(&seed)?; + let derivation_path = ed25519_derivation_path(SIGNING_KEY_DERIVATION_PATH, index)?; + let private_key = extended_secret_key.derive(&derivation_path)?; + let public_key = private_key.verifying_key().to_bytes(); // For some reason zero byte is required despite the false arg here: // https://github.com/alan-turing-institute/trustchain-mobile/blob/1b735645fd140b94bf1360bd5546643214c423b6/lib/app/shared/key_generation.dart#L12 let public_key = with_zero_byte(public_key); @@ -63,7 +81,7 @@ fn generate_ed25519_signing_key( Ok(JWK::from(Params::OKP(OctetParams { curve: "Ed25519".to_string(), public_key: Base64urlUInt(public_key.to_vec()), - private_key: Some(Base64urlUInt(private_key.to_vec())), + private_key: Some(Base64urlUInt(private_key.signing_key.to_bytes().to_vec())), }))) } @@ -82,7 +100,8 @@ fn generate_secp256k1_key( // let public_key: bitcoin::util::key::PublicKey = private_key.public_key(&secp); // Now convert the bitcoin::util::bip32::ExtendedPrivKey into a JWK. - let k256_secret_key = k256::SecretKey::from_bytes(private_key.to_bytes())?; + + let k256_secret_key = k256::SecretKey::from_slice(&private_key.to_bytes())?; let k256_public_key = k256_secret_key.public_key(); let mut ec_params = match ECParams::try_from(&k256_public_key) { Ok(params) => params, @@ -96,19 +115,31 @@ fn derivation_path( path: &str, index: Option, ) -> Result { - let index = match index { - Some(i) => i, - None => 0, - }; + let index = index.unwrap_or(0); // Handle case index > 2^31 - 1. if index > 2u32.pow(31) - 1 { return Err(bitcoin::util::bip32::Error::InvalidChildNumber(index)); } - let mut derivation_path = path.to_string(); - derivation_path.push_str("/"); - derivation_path.push_str(&index.to_string()); - derivation_path.push_str("'"); - DerivationPath::from_str(&derivation_path) + DerivationPath::from_str(&format!("{path}/{index}'")) +} + +fn ed25519_derivation_path( + path: &str, + index: Option, +) -> Result { + let index = index.unwrap_or(0); + // Handle case index > 2^31 - 1. + if index > 2u32.pow(31) - 1 { + return Err(MnemonicError::InvalidDerivationPath( + bitcoin::util::bip32::Error::InvalidChildNumber(index), + )); + } + Ok( + ed25519_dalek_bip32::derivation_path::DerivationPath::from_str(&format!( + "{}/{index}'", + path.replace('h', "'") + ))?, + ) } /// Generates a DID update key on the secp256k1 elliptic curve from a mnemonic seed phrase. @@ -271,7 +302,7 @@ mod tests { assert_eq!(address.to_string(), expected_address); // Now convert the bitcoin::util::bip32::ExtendedPrivKey into a JWK. - let k256_secret_key = k256::SecretKey::from_bytes(private_key.to_bytes())?; + let k256_secret_key = k256::SecretKey::from_slice(&private_key.to_bytes())?; let k256_public_key = k256_secret_key.public_key(); let mut ec_params = ECParams::try_from(&k256_public_key)?; @@ -289,32 +320,32 @@ mod tests { Ok(()) } - #[test] - fn test_mnemonic_ed22519() { - // Test case: - // https://github.com/alan-turing-institute/trustchain-mobile/blob/1b735645fd140b94bf1360bd5546643214c423b6/test/app/key_tests.dart - let phrase = "state draft moral repeat knife trend animal pretty delay collect fall adjust"; - let mnemonic = Mnemonic::parse(phrase).unwrap(); - let seed = mnemonic.to_seed(""); - let path = "m/0'/0'"; - let (private_key, _chain_code) = ed25519_hd_key::derive_from_path(path, &seed); - let public_key = ed25519_hd_key::get_public_key(&private_key); - // For some reason zero byte is required despite the false arg here: - // https://github.com/alan-turing-institute/trustchain-mobile/blob/1b735645fd140b94bf1360bd5546643214c423b6/lib/app/shared/key_generation.dart#L12 - let public_key = with_zero_byte(public_key); - - // Compare to test case JWK: - let expected = r#"{"kty":"OKP","crv":"Ed25519","d":"wHwSUdy4a00qTxAhnuOHeWpai4ERjdZGslaou-Lig5g=","x":"AI4pdGWalv3JXZcatmtBM8OfSIBCFC0o_RNzTg-mEAh6"}"#; - let expected_jwk: JWK = serde_json::from_str(expected).unwrap(); - - // Make a JWK from bytes: https://docs.rs/ssi/0.4.0/src/ssi/jwk.rs.html#251-265 - let jwk = JWK::from(Params::OKP(OctetParams { - curve: "Ed25519".to_string(), - public_key: Base64urlUInt(public_key.to_vec()), - private_key: Some(Base64urlUInt(private_key.to_vec())), - })); - // println!("{}", serde_json::to_string_pretty(&jwk).unwrap()); - // println!("{}", serde_json::to_string_pretty(&expected_jwk).unwrap()); - assert_eq!(jwk, expected_jwk); - } + // #[test] + // fn test_mnemonic_ed22519() { + // // Test case: + // // https://github.com/alan-turing-institute/trustchain-mobile/blob/1b735645fd140b94bf1360bd5546643214c423b6/test/app/key_tests.dart + // let phrase = "state draft moral repeat knife trend animal pretty delay collect fall adjust"; + // let mnemonic = Mnemonic::parse(phrase).unwrap(); + // let seed = mnemonic.to_seed(""); + // let path = "m/0'/0'"; + // let (private_key, _chain_code) = ed25519_hd_key::derive_from_path(path, &seed); + // let public_key = ed25519_hd_key::get_public_key(&private_key); + // // For some reason zero byte is required despite the false arg here: + // // https://github.com/alan-turing-institute/trustchain-mobile/blob/1b735645fd140b94bf1360bd5546643214c423b6/lib/app/shared/key_generation.dart#L12 + // let public_key = with_zero_byte(public_key); + + // // Compare to test case JWK: + // let expected = r#"{"kty":"OKP","crv":"Ed25519","d":"wHwSUdy4a00qTxAhnuOHeWpai4ERjdZGslaou-Lig5g=","x":"AI4pdGWalv3JXZcatmtBM8OfSIBCFC0o_RNzTg-mEAh6"}"#; + // let expected_jwk: JWK = serde_json::from_str(expected).unwrap(); + + // // Make a JWK from bytes: https://docs.rs/ssi/0.4.0/src/ssi/jwk.rs.html#251-265 + // let jwk = JWK::from(Params::OKP(OctetParams { + // curve: "Ed25519".to_string(), + // public_key: Base64urlUInt(public_key.to_vec()), + // private_key: Some(Base64urlUInt(private_key.to_vec())), + // })); + // // println!("{}", serde_json::to_string_pretty(&jwk).unwrap()); + // // println!("{}", serde_json::to_string_pretty(&expected_jwk).unwrap()); + // assert_eq!(jwk, expected_jwk); + // } } From 4f9cf2e727804ea5cf58b66123247ed459f25be1 Mon Sep 17 00:00:00 2001 From: Sam Greenbury Date: Mon, 23 Oct 2023 11:41:36 +0100 Subject: [PATCH 12/21] Update methods and imports --- trustchain-ion/src/mnemonic.rs | 114 ++++++++++++++++++++++++--------- 1 file changed, 82 insertions(+), 32 deletions(-) diff --git a/trustchain-ion/src/mnemonic.rs b/trustchain-ion/src/mnemonic.rs index 8a7ebc53..6e0fdc42 100644 --- a/trustchain-ion/src/mnemonic.rs +++ b/trustchain-ion/src/mnemonic.rs @@ -3,6 +3,8 @@ use bitcoin::secp256k1::Secp256k1; use bitcoin::util::bip32::{DerivationPath, ExtendedPrivKey}; use did_ion::sidetree::Sidetree; use did_ion::ION; +use ed25519_dalek_bip32::derivation_path::DerivationPath as Ed25519DerivationPath; +use ed25519_dalek_bip32::derivation_path::DerivationPathParseError; use ed25519_dalek_bip32::ExtendedSigningKey; use ssi::jwk::{Base64urlUInt, ECParams, OctetParams, Params, JWK}; use std::str::FromStr; @@ -23,7 +25,7 @@ pub enum MnemonicError { Ed25519DalekBip32Error(ed25519_dalek_bip32::Error), /// Invalid ed25519 BIP32 derivation path. #[error("Invalid ed25519 BIP32 derivation path: {0}")] - InvalidDerivationPathEd25519(ed25519_dalek_bip32::derivation_path::DerivationPathParseError), + InvalidDerivationPathEd25519(DerivationPathParseError), /// Failed to deserialize private scalar. #[error("Failed to deserialize private scalar bytes: {0}")] FailedToDeserializeScalar(k256::elliptic_curve::Error), @@ -44,9 +46,8 @@ impl From for MnemonicError { } } -// TODO: add imports to module -impl From for MnemonicError { - fn from(err: ed25519_dalek_bip32::derivation_path::DerivationPathParseError) -> Self { +impl From for MnemonicError { + fn from(err: DerivationPathParseError) -> Self { MnemonicError::InvalidDerivationPathEd25519(err) } } @@ -94,7 +95,7 @@ fn generate_secp256k1_key( let seed = mnemonic.to_seed(""); let m = ExtendedPrivKey::new_master(bitcoin::Network::Bitcoin, &seed)?; let secp = Secp256k1::new(); - let derivation_path = derivation_path(path, index)?; + let derivation_path = secp256k1_derivation_path(path, index)?; let xpriv = m.derive_priv(&secp, &derivation_path)?; let private_key = xpriv.to_priv(); // let public_key: bitcoin::util::key::PublicKey = private_key.public_key(&secp); @@ -111,35 +112,29 @@ fn generate_secp256k1_key( Ok(JWK::from(Params::EC(ec_params))) } -fn derivation_path( - path: &str, - index: Option, -) -> Result { +fn derivation_path(path: &str, index: Option) -> Result { let index = index.unwrap_or(0); // Handle case index > 2^31 - 1. if index > 2u32.pow(31) - 1 { return Err(bitcoin::util::bip32::Error::InvalidChildNumber(index)); } - DerivationPath::from_str(&format!("{path}/{index}'")) + Ok(format!("{}/{index}'", path.replace('h', "'"))) +} + +fn secp256k1_derivation_path( + path: &str, + index: Option, +) -> Result { + DerivationPath::from_str(&derivation_path(path, index)?) } fn ed25519_derivation_path( path: &str, index: Option, -) -> Result { - let index = index.unwrap_or(0); - // Handle case index > 2^31 - 1. - if index > 2u32.pow(31) - 1 { - return Err(MnemonicError::InvalidDerivationPath( - bitcoin::util::bip32::Error::InvalidChildNumber(index), - )); - } - Ok( - ed25519_dalek_bip32::derivation_path::DerivationPath::from_str(&format!( - "{}/{index}'", - path.replace('h', "'") - ))?, - ) +) -> Result { + Ok(Ed25519DerivationPath::from_str(&derivation_path( + path, index, + )?)?) } /// Generates a DID update key on the secp256k1 elliptic curve from a mnemonic seed phrase. @@ -194,35 +189,35 @@ mod tests { } #[test] - fn test_derivation_path() { + fn test_secp256k1_derivation_path() { let path = "m/1h"; - let expected = DerivationPath::from_str("m/1h/0h"); - assert_eq!(expected, derivation_path(path, None)); + let expected = DerivationPath::from_str("m/1'/0'"); + assert_eq!(expected, secp256k1_derivation_path(path, None)); let path = "m/1h"; let index: u32 = 0; let expected = DerivationPath::from_str("m/1h/0h"); - assert_eq!(expected, derivation_path(path, Some(index))); + assert_eq!(expected, secp256k1_derivation_path(path, Some(index))); let path = "m/2h"; let index: u32 = 2147483647; let expected = DerivationPath::from_str("m/2h/2147483647h"); - assert_eq!(expected, derivation_path(path, Some(index))); + assert_eq!(expected, secp256k1_derivation_path(path, Some(index))); let path = "m/2h"; let index: u32 = 2147483647; let expected = DerivationPath::from_str("m/1h/2147483647h"); - assert_ne!(expected, derivation_path(path, Some(index))); + assert_ne!(expected, secp256k1_derivation_path(path, Some(index))); let path = "m/1'"; let index: u32 = 0; let expected = DerivationPath::from_str("m/1h/0h"); - assert_eq!(expected, derivation_path(path, Some(index))); + assert_eq!(expected, secp256k1_derivation_path(path, Some(index))); let path = "m/0'"; let index: u32 = 0; let expected = DerivationPath::from_str("m/1h/0h"); - assert_ne!(expected, derivation_path(path, Some(index))); + assert_ne!(expected, secp256k1_derivation_path(path, Some(index))); let derivation_path = DerivationPath::from_str("m/1h/0h").unwrap(); let expected = "m/1'/0'"; @@ -233,6 +228,61 @@ mod tests { assert_eq!(expected, derivation_path.to_string()); } + #[test] + fn test_ed25519_derivation_path() { + let path = "m/1h"; + let expected = Ed25519DerivationPath::from_str("m/1'/0'").unwrap(); + assert_eq!(expected, ed25519_derivation_path(path, None).unwrap()); + + let path = "m/1h"; + let index: u32 = 0; + let expected = Ed25519DerivationPath::from_str("m/1'/0'").unwrap(); + assert_eq!( + expected, + ed25519_derivation_path(path, Some(index)).unwrap() + ); + + let path = "m/2h"; + let index: u32 = 2147483647; + let expected = Ed25519DerivationPath::from_str("m/2'/2147483647'").unwrap(); + assert_eq!( + expected, + ed25519_derivation_path(path, Some(index)).unwrap() + ); + + let path = "m/2h"; + let index: u32 = 2147483647; + let expected = Ed25519DerivationPath::from_str("m/1'/2147483647'").unwrap(); + assert_ne!( + expected, + ed25519_derivation_path(path, Some(index)).unwrap() + ); + + let path = "m/1'"; + let index: u32 = 0; + let expected = Ed25519DerivationPath::from_str("m/1'/0'").unwrap(); + assert_eq!( + expected, + ed25519_derivation_path(path, Some(index)).unwrap() + ); + + let path = "m/0'"; + let index: u32 = 0; + let expected = Ed25519DerivationPath::from_str("m/1'/0'").unwrap(); + assert_ne!( + expected, + ed25519_derivation_path(path, Some(index)).unwrap() + ); + + let derivation_path = Ed25519DerivationPath::from_str("m/1'/0'").unwrap(); + let expected = "m/1'/0'"; + assert_eq!(expected, derivation_path.to_string()); + + let derivation_path = Ed25519DerivationPath::from_str("m/1'/0'").unwrap(); + let expected = "m/1'/0'"; + assert_eq!(expected, derivation_path.to_string()); + } + #[test] fn test_generate_ed25519_signing_key() -> Result<(), Box> { let mnemonic = get_test_mnemonic(); From 8fc68b855ed57b275452ed1f37f55d6f63264a80 Mon Sep 17 00:00:00 2001 From: Sam Greenbury Date: Wed, 25 Oct 2023 17:40:25 +0100 Subject: [PATCH 13/21] Add condition for did create flags and comment --- trustchain-cli/src/bin/main.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/trustchain-cli/src/bin/main.rs b/trustchain-cli/src/bin/main.rs index 3932694c..1c1b329e 100644 --- a/trustchain-cli/src/bin/main.rs +++ b/trustchain-cli/src/bin/main.rs @@ -99,6 +99,9 @@ async fn main() -> Result<(), Box> { let file_path = sub_matches.get_one::("file_path"); let verbose = matches!(sub_matches.get_one::("verbose"), Some(true)); let mnemonic = matches!(sub_matches.get_one::("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 { @@ -124,6 +127,8 @@ async fn main() -> Result<(), Box> { // 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::("did").unwrap(); let _verbose = matches!(sub_matches.get_one::("verbose"), Some(true)); From 71c51b3287b1b4c4a7f78b102c0212fcc71fb825 Mon Sep 17 00:00:00 2001 From: Sam Greenbury Date: Wed, 25 Oct 2023 21:48:41 +0100 Subject: [PATCH 14/21] Use only mnemonic, remove obsolete test --- trustchain-cli/src/bin/main.rs | 4 +- trustchain-ffi/src/mobile.rs | 17 +++++---- trustchain-ffi/src/mobile_bridge.io.rs | 4 +- trustchain-ffi/src/mobile_bridge.rs | 10 ++--- trustchain-ion/src/create.rs | 16 ++++---- trustchain-ion/src/mnemonic.rs | 52 ++++++-------------------- 6 files changed, 37 insertions(+), 66 deletions(-) diff --git a/trustchain-cli/src/bin/main.rs b/trustchain-cli/src/bin/main.rs index 1c1b329e..b8ceb029 100644 --- a/trustchain-cli/src/bin/main.rs +++ b/trustchain-cli/src/bin/main.rs @@ -14,7 +14,7 @@ use trustchain_cli::config::cli_config; use trustchain_core::{vc::CredentialError, verifier::Verifier}; use trustchain_ion::{ attest::attest_operation, - create::{create_operation, create_operation_phrase}, + create::{create_operation, create_operation_mnemonic}, get_ion_resolver, verifier::IONVerifier, }; @@ -114,7 +114,7 @@ async fn main() -> Result<(), Box> { let mut mnemonic = String::new(); println!("Enter a mnemonic:"); std::io::stdin().read_line(&mut mnemonic).unwrap(); - create_operation_phrase(&mnemonic, None)?; + create_operation_mnemonic(&mnemonic, None)?; } } Some(("attest", sub_matches)) => { diff --git a/trustchain-ffi/src/mobile.rs b/trustchain-ffi/src/mobile.rs index 7737d3a2..ff36e456 100644 --- a/trustchain-ffi/src/mobile.rs +++ b/trustchain-ffi/src/mobile.rs @@ -21,7 +21,7 @@ use trustchain_core::{ resolver::ResolverError, vc::CredentialError, verifier::VerifierError, vp::PresentationError, }; use trustchain_ion::{ - create::{phrase_to_create_and_keys, OperationDID}, + create::{mnemonic_to_create_and_keys, OperationDID}, get_ion_resolver, verifier::IONVerifier, }; @@ -47,7 +47,7 @@ pub enum FFIMobileError { FutureProofCreatedTime(DateTime, DateTime), #[error("Failed to issue presentation error: {0}.")] FailedToIssuePresentation(PresentationError), - #[error("Failed to make create operation from phrase: {0}.")] + #[error("Failed to make create operation from mnemonic: {0}.")] FailedCreateOperation(String), } @@ -188,11 +188,11 @@ struct CreateOperationAndDID { did: String, } -/// Makes a new ION DID from a mnemonic phrase. +/// Makes a new ION DID from a mnemonic. // TODO: consider optional index in API -pub fn create_operation_phrase(phrase: String) -> Result { - // Generate create operation from phrase - let (create_operation, _) = phrase_to_create_and_keys(&phrase, None) +pub fn create_operation_mnemonic(mnemonic: String) -> Result { + // 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 @@ -361,8 +361,9 @@ mod tests { #[test] fn test_ion_create_operation() { - let phrase = "state draft moral repeat knife trend animal pretty delay collect fall adjust"; - let create_op_and_did = create_operation_phrase(phrase.to_string()).unwrap(); + 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::(&create_op_and_did).unwrap(), canonicalize_str::(TEST_ION_CREATE_OPERATION).unwrap() diff --git a/trustchain-ffi/src/mobile_bridge.io.rs b/trustchain-ffi/src/mobile_bridge.io.rs index 625175fd..ee2682fc 100644 --- a/trustchain-ffi/src/mobile_bridge.io.rs +++ b/trustchain-ffi/src/mobile_bridge.io.rs @@ -44,8 +44,8 @@ pub extern "C" fn wire_vp_issue_presentation( } #[no_mangle] -pub extern "C" fn wire_create_operation_phrase(port_: i64, phrase: *mut wire_uint_8_list) { - wire_create_operation_phrase_impl(port_, phrase) +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 diff --git a/trustchain-ffi/src/mobile_bridge.rs b/trustchain-ffi/src/mobile_bridge.rs index 6f96395c..474dfdfa 100644 --- a/trustchain-ffi/src/mobile_bridge.rs +++ b/trustchain-ffi/src/mobile_bridge.rs @@ -105,19 +105,19 @@ fn wire_vp_issue_presentation_impl( }, ) } -fn wire_create_operation_phrase_impl( +fn wire_create_operation_mnemonic_impl( port_: MessagePort, - phrase: impl Wire2Api + UnwindSafe, + mnemonic: impl Wire2Api + UnwindSafe, ) { FLUTTER_RUST_BRIDGE_HANDLER.wrap( WrapInfo { - debug_name: "create_operation_phrase", + debug_name: "create_operation_mnemonic", port: Some(port_), mode: FfiCallMode::Normal, }, move || { - let api_phrase = phrase.wire2api(); - move |task_callback| create_operation_phrase(api_phrase) + let api_mnemonic = mnemonic.wire2api(); + move |task_callback| create_operation_mnemonic(api_mnemonic) }, ) } diff --git a/trustchain-ion/src/create.rs b/trustchain-ion/src/create.rs index 39d7fe16..256c23bc 100644 --- a/trustchain-ion/src/create.rs +++ b/trustchain-ion/src/create.rs @@ -185,12 +185,12 @@ pub fn create_operation( ) } -/// Generates a create operation and corresponding keys from a mnemonic phrase. -pub fn phrase_to_create_and_keys( - phrase: &str, +/// Generates a create operation and corresponding keys from a mnemonic. +pub fn mnemonic_to_create_and_keys( + mnemonic: &str, index: Option, ) -> Result<(CreateOperation, IONKeys), Box> { - let ion_keys = crate::mnemonic::generate_keys(&Mnemonic::parse(phrase)?, index)?; + let ion_keys = crate::mnemonic::generate_keys(&Mnemonic::parse(mnemonic)?, index)?; let signing_public_key = PublicKeyEntry::try_from(ion_keys.signing_key.clone())?; let update_public_key = PublicKeyJwk::try_from(ion_keys.update_key.to_public())?; let recovery_public_key = PublicKeyJwk::try_from(ion_keys.recovery_key.to_public())?; @@ -206,13 +206,13 @@ pub fn phrase_to_create_and_keys( } /// Makes a new DID subject to be controlled with corresponding create operation written to file -/// from a mnemonic phrase. -pub fn create_operation_phrase( - phrase: &str, +/// from a mnemonic. +pub fn create_operation_mnemonic( + mnemonic: &str, index: Option, ) -> Result> { // Generate operation and keys - let (create_operation, ion_keys) = phrase_to_create_and_keys(phrase, index)?; + let (create_operation, ion_keys) = mnemonic_to_create_and_keys(mnemonic, index)?; // Write create operation write_create_operation( diff --git a/trustchain-ion/src/mnemonic.rs b/trustchain-ion/src/mnemonic.rs index 6e0fdc42..e7a3beb6 100644 --- a/trustchain-ion/src/mnemonic.rs +++ b/trustchain-ion/src/mnemonic.rs @@ -14,7 +14,7 @@ use crate::{ RECOVERY_KEY_DERIVATION_PATH, SIGNING_KEY_DERIVATION_PATH, UPDATE_KEY_DERIVATION_PATH, }; -/// An error relating to key generation from a mnemonic seed phrase. +/// An error relating to key generation from a mnemonic. #[derive(Error, Debug)] pub enum MnemonicError { /// Invalid BIP32 derivation path. @@ -65,7 +65,7 @@ fn with_zero_byte(public_key: [u8; 32]) -> [u8; 33] { new_public_key } -/// Generates a signing key on the ed25519 elliptic curve from a mnemonic seed phrase. +/// Generates a signing key on the ed25519 elliptic curve from a mnemonic. fn generate_ed25519_signing_key( mnemonic: &Mnemonic, index: Option, @@ -86,7 +86,7 @@ fn generate_ed25519_signing_key( }))) } -/// Generates a key on the secp256k1 elliptic curve from a mnemonic seed phrase. +/// Generates a key on the secp256k1 elliptic curve from a mnemonic. fn generate_secp256k1_key( mnemonic: &Mnemonic, path: &str, @@ -98,10 +98,8 @@ fn generate_secp256k1_key( let derivation_path = secp256k1_derivation_path(path, index)?; let xpriv = m.derive_priv(&secp, &derivation_path)?; let private_key = xpriv.to_priv(); - // let public_key: bitcoin::util::key::PublicKey = private_key.public_key(&secp); // Now convert the bitcoin::util::bip32::ExtendedPrivKey into a JWK. - let k256_secret_key = k256::SecretKey::from_slice(&private_key.to_bytes())?; let k256_public_key = k256_secret_key.public_key(); let mut ec_params = match ECParams::try_from(&k256_public_key) { @@ -137,7 +135,7 @@ fn ed25519_derivation_path( )?)?) } -/// Generates a DID update key on the secp256k1 elliptic curve from a mnemonic seed phrase. +/// Generates a DID update key on the secp256k1 elliptic curve from a mnemonic. fn generate_secp256k1_update_key( mnemonic: &Mnemonic, index: Option, @@ -145,7 +143,7 @@ fn generate_secp256k1_update_key( generate_secp256k1_key(mnemonic, UPDATE_KEY_DERIVATION_PATH, index) } -/// Generates a DID recovery key on the secp256k1 elliptic curve from a mnemonic seed phrase. +/// Generates a DID recovery key on the secp256k1 elliptic curve from a mnemonic. fn generate_secp256k1_recovery_key( mnemonic: &Mnemonic, index: Option, @@ -158,7 +156,7 @@ pub struct IONKeys { pub recovery_key: JWK, } -/// Generates a set of signing, update and recovery keys from a mnemonic phrase and child index. +/// Generates a set of signing, update and recovery keys from a mnemonic and child index. pub fn generate_keys(mnemonic: &Mnemonic, index: Option) -> Result { let signing_key = generate_ed25519_signing_key(mnemonic, index)?; let update_key = generate_secp256k1_update_key(mnemonic, index)?; @@ -184,8 +182,10 @@ mod tests { use std::str::FromStr; fn get_test_mnemonic() -> Mnemonic { - let phrase = "state draft moral repeat knife trend animal pretty delay collect fall adjust"; - Mnemonic::parse(phrase).unwrap() + Mnemonic::parse( + "state draft moral repeat knife trend animal pretty delay collect fall adjust", + ) + .unwrap() } #[test] @@ -332,8 +332,7 @@ mod tests { #[test] fn test_mnemonic_secp256k1() -> Result<(), Box> { - let phrase = "state draft moral repeat knife trend animal pretty delay collect fall adjust"; - let mnemonic = Mnemonic::parse(phrase).unwrap(); + let mnemonic = get_test_mnemonic(); let seed = mnemonic.to_seed(""); let path = "m/0'/0'"; @@ -369,33 +368,4 @@ mod tests { Ok(()) } - - // #[test] - // fn test_mnemonic_ed22519() { - // // Test case: - // // https://github.com/alan-turing-institute/trustchain-mobile/blob/1b735645fd140b94bf1360bd5546643214c423b6/test/app/key_tests.dart - // let phrase = "state draft moral repeat knife trend animal pretty delay collect fall adjust"; - // let mnemonic = Mnemonic::parse(phrase).unwrap(); - // let seed = mnemonic.to_seed(""); - // let path = "m/0'/0'"; - // let (private_key, _chain_code) = ed25519_hd_key::derive_from_path(path, &seed); - // let public_key = ed25519_hd_key::get_public_key(&private_key); - // // For some reason zero byte is required despite the false arg here: - // // https://github.com/alan-turing-institute/trustchain-mobile/blob/1b735645fd140b94bf1360bd5546643214c423b6/lib/app/shared/key_generation.dart#L12 - // let public_key = with_zero_byte(public_key); - - // // Compare to test case JWK: - // let expected = r#"{"kty":"OKP","crv":"Ed25519","d":"wHwSUdy4a00qTxAhnuOHeWpai4ERjdZGslaou-Lig5g=","x":"AI4pdGWalv3JXZcatmtBM8OfSIBCFC0o_RNzTg-mEAh6"}"#; - // let expected_jwk: JWK = serde_json::from_str(expected).unwrap(); - - // // Make a JWK from bytes: https://docs.rs/ssi/0.4.0/src/ssi/jwk.rs.html#251-265 - // let jwk = JWK::from(Params::OKP(OctetParams { - // curve: "Ed25519".to_string(), - // public_key: Base64urlUInt(public_key.to_vec()), - // private_key: Some(Base64urlUInt(private_key.to_vec())), - // })); - // // println!("{}", serde_json::to_string_pretty(&jwk).unwrap()); - // // println!("{}", serde_json::to_string_pretty(&expected_jwk).unwrap()); - // assert_eq!(jwk, expected_jwk); - // } } From 638742a323d6684f64e202b304ca4e5e51f9dec5 Mon Sep 17 00:00:00 2001 From: Sam Greenbury Date: Thu, 26 Oct 2023 10:11:21 +0100 Subject: [PATCH 15/21] Add comments, use MnemonicError --- trustchain-ion/src/mnemonic.rs | 53 ++++++++++++++++++++++++---------- 1 file changed, 37 insertions(+), 16 deletions(-) diff --git a/trustchain-ion/src/mnemonic.rs b/trustchain-ion/src/mnemonic.rs index e7a3beb6..6ab5efb2 100644 --- a/trustchain-ion/src/mnemonic.rs +++ b/trustchain-ion/src/mnemonic.rs @@ -110,22 +110,27 @@ fn generate_secp256k1_key( Ok(JWK::from(Params::EC(ec_params))) } -fn derivation_path(path: &str, index: Option) -> Result { +/// Generates derivation path. +fn derivation_path(path: &str, index: Option) -> Result { let index = index.unwrap_or(0); // Handle case index > 2^31 - 1. if index > 2u32.pow(31) - 1 { - return Err(bitcoin::util::bip32::Error::InvalidChildNumber(index)); + return Err(MnemonicError::InvalidDerivationPath( + bitcoin::util::bip32::Error::InvalidChildNumber(index), + )); } Ok(format!("{}/{index}'", path.replace('h', "'"))) } +/// Generates derivation path. fn secp256k1_derivation_path( path: &str, index: Option, -) -> Result { - DerivationPath::from_str(&derivation_path(path, index)?) +) -> Result { + Ok(DerivationPath::from_str(&derivation_path(path, index)?)?) } +/// Generates an ed25519_dalek_bip32 derivation path. fn ed25519_derivation_path( path: &str, index: Option, @@ -150,6 +155,7 @@ fn generate_secp256k1_recovery_key( ) -> Result { generate_secp256k1_key(mnemonic, RECOVERY_KEY_DERIVATION_PATH, index) } +/// A type for the set of JWK required for an ION create operation. pub struct IONKeys { pub signing_key: JWK, pub update_key: JWK, @@ -191,33 +197,48 @@ mod tests { #[test] fn test_secp256k1_derivation_path() { let path = "m/1h"; - let expected = DerivationPath::from_str("m/1'/0'"); - assert_eq!(expected, secp256k1_derivation_path(path, None)); + let expected = DerivationPath::from_str("m/1'/0'").unwrap(); + assert_eq!(expected, secp256k1_derivation_path(path, None).unwrap()); let path = "m/1h"; let index: u32 = 0; - let expected = DerivationPath::from_str("m/1h/0h"); - assert_eq!(expected, secp256k1_derivation_path(path, Some(index))); + let expected = DerivationPath::from_str("m/1h/0h").unwrap(); + assert_eq!( + expected, + secp256k1_derivation_path(path, Some(index)).unwrap() + ); let path = "m/2h"; let index: u32 = 2147483647; - let expected = DerivationPath::from_str("m/2h/2147483647h"); - assert_eq!(expected, secp256k1_derivation_path(path, Some(index))); + let expected = DerivationPath::from_str("m/2h/2147483647h").unwrap(); + assert_eq!( + expected, + secp256k1_derivation_path(path, Some(index)).unwrap() + ); let path = "m/2h"; let index: u32 = 2147483647; - let expected = DerivationPath::from_str("m/1h/2147483647h"); - assert_ne!(expected, secp256k1_derivation_path(path, Some(index))); + let expected = DerivationPath::from_str("m/1h/2147483647h").unwrap(); + assert_ne!( + expected, + secp256k1_derivation_path(path, Some(index)).unwrap() + ); let path = "m/1'"; let index: u32 = 0; - let expected = DerivationPath::from_str("m/1h/0h"); - assert_eq!(expected, secp256k1_derivation_path(path, Some(index))); + let expected = DerivationPath::from_str("m/1h/0h").unwrap(); + assert_eq!( + expected, + secp256k1_derivation_path(path, Some(index)).unwrap() + ); let path = "m/0'"; let index: u32 = 0; - let expected = DerivationPath::from_str("m/1h/0h"); - assert_ne!(expected, secp256k1_derivation_path(path, Some(index))); + let expected = DerivationPath::from_str("m/1h/0h").unwrap(); + assert_ne!( + expected, + secp256k1_derivation_path(path, Some(index)).unwrap() + ); let derivation_path = DerivationPath::from_str("m/1h/0h").unwrap(); let expected = "m/1'/0'"; From 5a05fcd95c01900593ff44a0cde8abf197364bd5 Mon Sep 17 00:00:00 2001 From: Sam Greenbury Date: Thu, 26 Oct 2023 11:15:17 +0100 Subject: [PATCH 16/21] Remove zero byte prefix, add test --- trustchain-ffi/src/mobile.rs | 12 ++++++------ trustchain-ion/src/mnemonic.rs | 29 +++++++++++++++-------------- 2 files changed, 21 insertions(+), 20 deletions(-) diff --git a/trustchain-ffi/src/mobile.rs b/trustchain-ffi/src/mobile.rs index ff36e456..7dd62da7 100644 --- a/trustchain-ffi/src/mobile.rs +++ b/trustchain-ffi/src/mobile.rs @@ -252,7 +252,6 @@ mod tests { const TEST_ION_CREATE_OPERATION: &str = r#" { "createOperation": { - "type": "create", "delta": { "patches": [ { @@ -260,11 +259,11 @@ mod tests { "document": { "publicKeys": [ { - "id": "--3rT8-n9BDn51S_rkdxR3G6J_a8YQlsQ8OIAx1Qqkk", + "id": "CIMzmuW8XaQoc2DyccLwMZ35GyLhPj4yG2k38JNw5P4", "publicKeyJwk": { "crv": "Ed25519", "kty": "OKP", - "x": "AI4pdGWalv3JXZcatmtBM8OfSIBCFC0o_RNzTg-mEAh6" + "x": "jil0ZZqW_cldlxq2a0Ezw59IgEIULSj9E3NOD6YQCHo" }, "purposes": [ "assertionMethod", @@ -282,11 +281,12 @@ mod tests { "updateCommitment": "EiA2gSveT83s4DD4kJp6tLJuPfy_M3m_m6NtRJzjwtrlDg" }, "suffixData": { - "deltaHash": "EiCzptro682kdNxcWXZ7xBnSzZqKu9jphkAfsXmU-7UA-g", + "deltaHash": "EiBtaFhQ3mbpKXwOXD2wr7so32FvbZDGvRyGJ-yOfforGQ", "recoveryCommitment": "EiDKEn4lG5ETCoQpQxAsMVahzuerhlk0rtqtuoHPYKEEog" - } + }, + "type": "create" }, - "did": "did:ion:test:EiB_YFqM3CcO93dnjlNWfljYesSUQxP_tzxEOQsXE3T4MQ" + "did": "did:ion:test:EiA1dZD7jVkS5ZP7JJO01t6HgTU3eeLpbKEV1voOFWJV0g" }"#; #[test] diff --git a/trustchain-ion/src/mnemonic.rs b/trustchain-ion/src/mnemonic.rs index 6ab5efb2..3e1b4550 100644 --- a/trustchain-ion/src/mnemonic.rs +++ b/trustchain-ion/src/mnemonic.rs @@ -58,13 +58,6 @@ impl From for MnemonicError { } } -// See: https://github.com/alepop/dart-ed25519-hd-key/blob/f785c73b1248037df58f8d582e6f71c480e49d39/lib/src/hd_key.dart#L45-L56 -fn with_zero_byte(public_key: [u8; 32]) -> [u8; 33] { - let mut new_public_key = [0u8; 33]; - new_public_key[1..33].copy_from_slice(public_key.as_slice()); - new_public_key -} - /// Generates a signing key on the ed25519 elliptic curve from a mnemonic. fn generate_ed25519_signing_key( mnemonic: &Mnemonic, @@ -75,9 +68,6 @@ fn generate_ed25519_signing_key( let derivation_path = ed25519_derivation_path(SIGNING_KEY_DERIVATION_PATH, index)?; let private_key = extended_secret_key.derive(&derivation_path)?; let public_key = private_key.verifying_key().to_bytes(); - // For some reason zero byte is required despite the false arg here: - // https://github.com/alan-turing-institute/trustchain-mobile/blob/1b735645fd140b94bf1360bd5546643214c423b6/lib/app/shared/key_generation.dart#L12 - let public_key = with_zero_byte(public_key); // Make a JWK from bytes: https://docs.rs/ssi/0.4.0/src/ssi/jwk.rs.html#251-265 Ok(JWK::from(Params::OKP(OctetParams { curve: "Ed25519".to_string(), @@ -307,13 +297,13 @@ mod tests { #[test] fn test_generate_ed25519_signing_key() -> Result<(), Box> { let mnemonic = get_test_mnemonic(); - let result = generate_ed25519_signing_key(&mnemonic, Some(22))?; + let result = generate_ed25519_signing_key(&mnemonic, Some(0))?; let expected = r#" { "kty": "OKP", "crv": "Ed25519", - "x": "AFPB4OPrkVLsxAxppQ2QrqcX1vK_dLP1pJfNuwUA8WGA", - "d": "065j_kBmgaYT5JCMbIebOSRkkneHJ83JKkjVrogSamI" + "x": "jil0ZZqW_cldlxq2a0Ezw59IgEIULSj9E3NOD6YQCHo", + "d": "wHwSUdy4a00qTxAhnuOHeWpai4ERjdZGslaou-Lig5g" }"#; assert_eq!(result, serde_json::from_str::(expected)?); Ok(()) @@ -330,7 +320,7 @@ mod tests { "x": "pALnEGubf31SDdZQsbjSXqqDRivTSQXteERlggq3vYI", "y": "sWjJLyW3dDbqXnYNNPnyeuQGBkBmBH2K_XV3LVCDCDQ", "d": "LxQYTvQ2naxEl-XsTWxekhMroP4LtkTW5mdOXuoyS0E" - }"#; + }"#; assert_eq!(result, serde_json::from_str::(expected)?); Ok(()) } @@ -389,4 +379,15 @@ mod tests { Ok(()) } + + #[test] + fn test_ed25519_signing_key_signature() { + let key = generate_ed25519_signing_key(&get_test_mnemonic(), None).unwrap(); + let algorithm = key.get_algorithm().unwrap(); + let payload = "payload"; + let signed = ssi::jws::encode_sign(algorithm, payload, &key).unwrap(); + let verified = ssi::jws::decode_verify(&signed, &key.to_public()); + assert!(verified.is_ok()); + assert_eq!(payload, String::from_utf8(verified.unwrap().1).unwrap()); + } } From 0fea1aca18cd7fe1b09620cb86ef5f9d186b8ce2 Mon Sep 17 00:00:00 2001 From: Sam Greenbury Date: Thu, 26 Oct 2023 11:19:30 +0100 Subject: [PATCH 17/21] Fix unused import --- trustchain-ion/src/utils.rs | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/trustchain-ion/src/utils.rs b/trustchain-ion/src/utils.rs index 2616c19c..abf1a257 100644 --- a/trustchain-ion/src/utils.rs +++ b/trustchain-ion/src/utils.rs @@ -6,7 +6,7 @@ use bitcoin::{BlockHash, BlockHeader, Transaction}; use bitcoincore_rpc::{bitcoincore_rpc_json::BlockStatsFields, RpcApi}; use chrono::NaiveDate; use flate2::read::GzDecoder; -use futures::{StreamExt, TryStreamExt}; +use futures::TryStreamExt; use ipfs_api_backend_hyper::{IpfsApi, IpfsClient}; use mongodb::{bson::doc, options::ClientOptions, Cursor}; use serde_json::{json, Value}; @@ -387,18 +387,15 @@ pub fn block_height_range_on_date( #[cfg(test)] mod tests { - use core::panic; - use std::io::Read; - use std::str::FromStr; - use super::*; use crate::sidetree::CoreIndexFile; use flate2::read::GzDecoder; - + use futures::StreamExt; use ssi::{ did::{Document, ServiceEndpoint}, jwk::Params, }; + use std::str::FromStr; use trustchain_core::{ data::{ TEST_SIDETREE_DOCUMENT_MULTIPLE_KEYS, TEST_SIDETREE_DOCUMENT_SERVICE_AND_PROOF, From 1e40f563d77a1313662a4aa5abdc6aee76f69166 Mon Sep 17 00:00:00 2001 From: Sam Greenbury Date: Thu, 26 Oct 2023 17:52:52 +0100 Subject: [PATCH 18/21] Modify post operation to get address from config, add test --- trustchain-http/src/config.rs | 8 ++++ trustchain-http/src/ion.rs | 70 +++++++++++++++++++++++++++++++---- 2 files changed, 71 insertions(+), 7 deletions(-) diff --git a/trustchain-http/src/config.rs b/trustchain-http/src/config.rs index 67cf238c..3d3a5d55 100644 --- a/trustchain-http/src/config.rs +++ b/trustchain-http/src/config.rs @@ -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, /// Flag indicating whether server uses https @@ -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, @@ -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 diff --git a/trustchain-http/src/ion.rs b/trustchain-http/src/ion.rs index 7c957678..b33e22ef 100644 --- a/trustchain-http/src/ion.rs +++ b/trustchain-http/src/ion.rs @@ -11,13 +11,17 @@ use std::sync::Arc; /// Receives ION operation POST request and forwards to local ION node. pub async fn post_operation( Json(operation): Json, - _app_state: Arc, + app_state: Arc, ) -> 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("http://localhost:3000/operations") + .post(address) .json(&operation) .send() .await @@ -34,9 +38,61 @@ pub async fn post_operation( #[cfg(test)] mod tests { - // use super::*; - // TODO: add test by setting up: - // - a mock server as the ION server - // - a test server with the `/operations` route - // And then sending a POST reqwest and testing response + 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) -> impl IntoResponse { + Html(format!("Response: {}", json)) + } + + #[tokio::test] + 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 = 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() + ); + } } From 250edc5ca6478c6ac752311e60bef309bafd5a6d Mon Sep 17 00:00:00 2001 From: Sam Greenbury Date: Mon, 30 Oct 2023 11:11:01 +0000 Subject: [PATCH 19/21] Add ignore for test given config dependency --- trustchain-http/src/ion.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/trustchain-http/src/ion.rs b/trustchain-http/src/ion.rs index b33e22ef..3646293f 100644 --- a/trustchain-http/src/ion.rs +++ b/trustchain-http/src/ion.rs @@ -50,6 +50,7 @@ mod tests { } #[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(); From 3b5e94c21391139231689893208e3d4ac7887ff4 Mon Sep 17 00:00:00 2001 From: Sam Greenbury Date: Mon, 30 Oct 2023 13:52:26 +0000 Subject: [PATCH 20/21] Remove unused dependency --- trustchain-ffi/Cargo.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/trustchain-ffi/Cargo.toml b/trustchain-ffi/Cargo.toml index b61fbc22..278ed386 100644 --- a/trustchain-ffi/Cargo.toml +++ b/trustchain-ffi/Cargo.toml @@ -15,7 +15,6 @@ trustchain-ion = { path = "../trustchain-ion" } trustchain-api = { path = "../trustchain-api" } anyhow = "1.0" -bip39 = "2.0.0" 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: `flutter_rust_bridge_codegen@1.64.0` From 0cad0f97af70e21d2b7e7d2af8c7c605c9e64fa9 Mon Sep 17 00:00:00 2001 From: Sam Greenbury <50113363+sgreenbury@users.noreply.github.com> Date: Mon, 30 Oct 2023 14:18:40 +0000 Subject: [PATCH 21/21] Remove print from test --- trustchain-ion/src/create.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/trustchain-ion/src/create.rs b/trustchain-ion/src/create.rs index 256c23bc..c8be24bd 100644 --- a/trustchain-ion/src/create.rs +++ b/trustchain-ion/src/create.rs @@ -283,7 +283,6 @@ mod test { for path in paths { if let Ok(path_buf) = path { let operation_string = std::fs::read_to_string(path_buf)?; - println!("{}", operation_string); let _operation: Operation = serde_json::from_str(&operation_string)?; operation_count += 1; } else {