diff --git a/Cargo.lock b/Cargo.lock index b6b1d8127..79ab5eac7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -134,9 +134,9 @@ checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" [[package]] name = "axum" -version = "0.6.14" +version = "0.6.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e5abc76e6ef57bf438c067d2898481475994102eeb5d662a0b2162d0c2fdcf1" +checksum = "3b32c5ea3aabaf4deb5f5ced2d688ec0844c881c9e6c696a8b769a05fc691e62" dependencies = [ "async-trait", "axum-core", @@ -220,6 +220,15 @@ version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" +[[package]] +name = "block-buffer" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4" +dependencies = [ + "generic-array", +] + [[package]] name = "block-buffer" version = "0.10.4" @@ -428,6 +437,19 @@ dependencies = [ "typenum", ] +[[package]] +name = "curve25519-dalek" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b9fdf9972b2bd6af2d913799d9ebc165ea4d2e65878e329d9c6b372c4491b61" +dependencies = [ + "byteorder", + "digest 0.9.0", + "rand_core 0.5.1", + "subtle", + "zeroize", +] + [[package]] name = "cxx" version = "1.0.94" @@ -483,17 +505,49 @@ dependencies = [ "zeroize", ] +[[package]] +name = "digest" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066" +dependencies = [ + "generic-array", +] + [[package]] name = "digest" version = "0.10.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8168378f4e5023e7218c89c891c0fd8ecdb5e5e4f18cb78f38cf245dd021e76f" dependencies = [ - "block-buffer", + "block-buffer 0.10.4", "const-oid", "crypto-common", ] +[[package]] +name = "ed25519" +version = "1.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91cff35c70bba8a626e3185d8cd48cc11b5437e1a5bcd15b9b5fa3c64b6dfee7" +dependencies = [ + "signature 1.6.4", +] + +[[package]] +name = "ed25519-dalek" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c762bae6dcaf24c4c84667b8579785430908723d5c889f469d76a41d59cc7a9d" +dependencies = [ + "curve25519-dalek", + "ed25519", + "rand 0.7.3", + "serde", + "sha2 0.9.9", + "zeroize", +] + [[package]] name = "either" version = "1.8.1" @@ -1178,6 +1232,7 @@ dependencies = [ "axum", "chrono", "clap", + "ed25519-dalek", "futures", "hex", "hyper", @@ -1215,6 +1270,7 @@ version = "0.1.0" dependencies = [ "anyhow", "bollard", + "ed25519-dalek", "futures", "hex", "hyper", @@ -1355,7 +1411,7 @@ dependencies = [ "serde", "serde_json", "serde_path_to_error", - "sha2", + "sha2 0.10.6", "thiserror", "url", ] @@ -1375,6 +1431,12 @@ version = "1.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b7e5500299e16ebb147ae15a00a942af264cf3688f47923b8fc2cd5858f23ad3" +[[package]] +name = "opaque-debug" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" + [[package]] name = "openssl" version = "0.10.50" @@ -1837,7 +1899,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "55a77d189da1fee555ad95b7e50e7457d91c0e089ec68ca69ad2989413bbdab4" dependencies = [ "byteorder", - "digest", + "digest 0.10.6", "num-bigint-dig", "num-integer", "num-iter", @@ -1845,7 +1907,7 @@ dependencies = [ "pkcs1", "pkcs8", "rand_core 0.6.4", - "signature", + "signature 2.1.0", "subtle", "zeroize", ] @@ -2078,6 +2140,19 @@ dependencies = [ "time 0.3.20", ] +[[package]] +name = "sha2" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d58a1e1bf39749807d89cf2d98ac2dfa0ff1cb3faa38fbb64dd88ac8013d800" +dependencies = [ + "block-buffer 0.9.0", + "cfg-if", + "cpufeatures", + "digest 0.9.0", + "opaque-debug", +] + [[package]] name = "sha2" version = "0.10.6" @@ -2086,7 +2161,7 @@ checksum = "82e6b795fe2e3b1e845bafcb27aa35405c4d47cdfc92af5fc8d3002f76cebdc0" dependencies = [ "cfg-if", "cpufeatures", - "digest", + "digest 0.10.6", ] [[package]] @@ -2107,13 +2182,19 @@ dependencies = [ "libc", ] +[[package]] +name = "signature" +version = "1.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74233d3b3b2f6d4b006dc19dee745e73e2a6bfb6f93607cd3b02bd5b00797d7c" + [[package]] name = "signature" version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e1788eed21689f9cf370582dfc467ef36ed9c707f073528ddafa8d83e3b8500" dependencies = [ - "digest", + "digest 0.10.6", "rand_core 0.6.4", ] @@ -2178,9 +2259,9 @@ checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" [[package]] name = "subtle" -version = "2.4.1" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601" +checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc" [[package]] name = "syn" @@ -3023,3 +3104,17 @@ name = "zeroize" version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a0956f1ba7c7909bfb66c2e9e4124ab6f6482560f6628b5aaeba39207c9aad9" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.14", +] diff --git a/DEPLOY.md b/DEPLOY.md new file mode 100644 index 000000000..347793128 --- /dev/null +++ b/DEPLOY.md @@ -0,0 +1,36 @@ +# Manually Deploying mpc-recovery to GCP + +GCP Project ID: pagoda-discovery-platform-dev +Service account: mpc-recovery@pagoda-discovery-platform-dev.iam.gserviceaccount.com + +First, if you don't have credentials, go to [here](https://console.cloud.google.com/iam-admin/serviceaccounts/details/106859519072057593233;edit=true/keys?project=pagoda-discovery-platform-dev) and generate a new one for yourself. + +Now, assuming you saved it as `mpc-recovery-creds.json` in the current working directory: + +```bash +$ cat pagoda-discovery-platform-dev-92b300563d36.json | docker login -u _json_key --password-stdin https://us-east1-docker.pkg.dev +``` + +This will log you into the GCP Artifact Repository. + +Build the mpc-recovery docker image like you usually would, but tag it with this image name: + +```bash +$ docker build . -t us-east1-docker.pkg.dev/pagoda-discovery-platform-dev/mpc-recovery-tmp/mpc-recovery +``` + +Push the image to GCP Artifact Registry: + +```bash +$ docker push us-east1-docker.pkg.dev/pagoda-discovery-platform-dev/mpc-recovery-tmp/mpc-recovery +``` + + You can check that the image has been successfully uploaded [here](https://console.cloud.google.com/artifacts/docker/pagoda-discovery-platform-dev/us-east1/mpc-recovery-tmp?project=pagoda-discovery-platform-dev). + + Now reset the VM instance: + + ```bash + $ gcloud compute instances reset mpc-recovery-tmp-0 + ``` + + The API should be available shortly on `http://34.139.85.130:3000`. diff --git a/integration-tests/Cargo.toml b/integration-tests/Cargo.toml index 15c8117e1..5b45924e5 100644 --- a/integration-tests/Cargo.toml +++ b/integration-tests/Cargo.toml @@ -7,6 +7,7 @@ publish = false [dev-dependencies] anyhow = "1.0" bollard = "0.14" +ed25519-dalek = "1" futures = "0.3" hex = "0.4" hyper = { version = "0.14", features = ["full"] } diff --git a/integration-tests/tests/lib.rs b/integration-tests/tests/lib.rs index 8d9b3b979..6cd97cb0e 100644 --- a/integration-tests/tests/lib.rs +++ b/integration-tests/tests/lib.rs @@ -2,6 +2,7 @@ use bollard::container::{AttachContainerOptions, AttachContainerResults, Config} use bollard::network::CreateNetworkOptions; use bollard::service::{HostConfig, Ipam, PortBinding}; use bollard::Docker; +use ed25519_dalek::SecretKey; use futures::StreamExt; use hyper::{Body, Client, Method, Request}; use mpc_recovery::msg::LeaderResponse; @@ -110,6 +111,7 @@ async fn start_mpc_leader_node( pk_set: &PublicKeySet, sk_share: &SecretKeyShare, sign_nodes: Vec, + root_secret_key: &SecretKey, ) -> anyhow::Result { let web_port = portpicker::pick_unused_port().expect("no free ports"); @@ -123,6 +125,8 @@ async fn start_mpc_leader_node( serde_json::to_string(&SerdeSecret(sk_share))?, "--web-port".to_string(), web_port.to_string(), + "--root-secret-key".to_string(), + hex::encode(root_secret_key), ]; for sign_node in sign_nodes { cmd.push("--sign-nodes".to_string()); @@ -194,14 +198,22 @@ async fn test_trio() -> anyhow::Result<()> { // This test creates 4 sk shares with a threshold of 2 (i.e. minimum 3 required to sign), // but only instantiates 3 nodes. - let (pk_set, sk_shares) = mpc_recovery::generate(4, 3)?; + let (pk_set, sk_shares, root_secret_key) = mpc_recovery::generate(4, 3)?; let mut sign_nodes = Vec::new(); for i in 2..=3 { let addr = start_mpc_sign_node(&docker, i as u64, &pk_set, &sk_shares[i - 1]).await?; sign_nodes.push(addr); } - let leader_node = start_mpc_leader_node(&docker, 1, &pk_set, &sk_shares[0], sign_nodes).await?; + let leader_node = start_mpc_leader_node( + &docker, + 1, + &pk_set, + &sk_shares[0], + sign_nodes, + &root_secret_key, + ) + .await?; // Wait until all nodes initialize tokio::time::sleep(Duration::from_millis(2000)).await; diff --git a/mpc-recovery/Cargo.toml b/mpc-recovery/Cargo.toml index e3ab2bce3..4aedc8dcc 100644 --- a/mpc-recovery/Cargo.toml +++ b/mpc-recovery/Cargo.toml @@ -23,6 +23,7 @@ async-trait = "0.1" axum = "0.6" chrono = "0.4.24" clap = { version = "4.2", features = ["derive", "env"] } +ed25519-dalek = "1" futures = "0.3" hex = "0.4" hyper = { version = "0.14", features = ["full"] } diff --git a/mpc-recovery/src/leader_node/mod.rs b/mpc-recovery/src/leader_node/mod.rs index 3ebf939d0..16b1bb1f7 100644 --- a/mpc-recovery/src/leader_node/mod.rs +++ b/mpc-recovery/src/leader_node/mod.rs @@ -1,7 +1,11 @@ -use crate::msg::{LeaderRequest, LeaderResponse, SigShareRequest, SigShareResponse}; +use crate::msg::{ + AddRecoveryMethodRequest, AddRecoveryMethodResponse, LeaderRequest, LeaderResponse, + RecoverAccountRequest, RecoverAccountResponse, SigShareRequest, SigShareResponse, +}; use crate::oauth::{OAuthTokenVerifier, UniversalTokenVerifier}; use crate::NodeId; use axum::{extract::State, http::StatusCode, routing::post, Json, Router}; +use ed25519_dalek::SecretKey; use futures::stream::FuturesUnordered; use hyper::client::ResponseFuture; use hyper::{Body, Client, Method, Request}; @@ -10,13 +14,15 @@ use std::collections::BTreeMap; use std::net::SocketAddr; use threshold_crypto::{PublicKeySet, SecretKeyShare}; -#[tracing::instrument(level = "debug", skip(pk_set, sk_share, sign_nodes))] +#[tracing::instrument(level = "debug", skip(pk_set, sk_share, sign_nodes, root_secret_key))] pub async fn run( id: NodeId, pk_set: PublicKeySet, sk_share: SecretKeyShare, port: u16, sign_nodes: Vec, + // TODO: temporary solution + root_secret_key: SecretKey, ) { tracing::debug!(?sign_nodes, "running a leader node"); @@ -30,10 +36,19 @@ pub async fn run( pk_set, sk_share, sign_nodes, + root_secret_key, }; let app = Router::new() .route("/submit", post(submit::)) + .route( + "/add_recovery_method", + post(add_recovery_method::), + ) + .route( + "/recover_account", + post(recover_account::), + ) .with_state(state); let addr = SocketAddr::from(([0, 0, 0, 0], port)); @@ -44,12 +59,25 @@ pub async fn run( .unwrap(); } -#[derive(Clone)] struct LeaderState { id: NodeId, pk_set: PublicKeySet, sk_share: SecretKeyShare, sign_nodes: Vec, + // TODO: temporary solution + root_secret_key: SecretKey, +} + +impl Clone for LeaderState { + fn clone(&self) -> Self { + Self { + id: self.id, + pk_set: self.pk_set.clone(), + sk_share: self.sk_share.clone(), + sign_nodes: self.sign_nodes.clone(), + root_secret_key: SecretKey::from_bytes(self.root_secret_key.as_bytes()).unwrap(), + } + } } async fn parse(response_future: ResponseFuture) -> anyhow::Result { @@ -58,6 +86,67 @@ async fn parse(response_future: ResponseFuture) -> anyhow::Result( + State(state): State, + Json(request): Json, +) -> (StatusCode, Json) { + tracing::info!( + access_token = format!("{:.5}...", request.access_token), + "new request" + ); + + match T::verify_token(&request.access_token).await { + Ok(_) => { + tracing::info!("access token is valid"); + ( + StatusCode::OK, + Json(AddRecoveryMethodResponse::Ok { + public_key: (&state.root_secret_key).into(), + }), + ) + } + Err(_) => { + tracing::error!("access token verification failed"); + ( + StatusCode::UNAUTHORIZED, + Json(AddRecoveryMethodResponse::Err { + msg: "access token verification failed".into(), + }), + ) + } + } +} + +#[tracing::instrument(level = "debug", skip_all, fields(id = state.id))] +async fn recover_account( + State(state): State, + Json(request): Json, +) -> (StatusCode, Json) { + tracing::info!( + access_token = format!("{:.5}...", request.access_token), + public_key = hex::encode(request.public_key), + "new request" + ); + + match T::verify_token(&request.access_token).await { + Ok(_) => { + tracing::info!("access token is valid"); + // TODO: create and submit a transaction + (StatusCode::OK, Json(RecoverAccountResponse::Ok)) + } + Err(_) => { + tracing::error!("access token verification failed"); + ( + StatusCode::UNAUTHORIZED, + Json(RecoverAccountResponse::Err { + msg: "access token verification failed".into(), + }), + ) + } + } +} + #[tracing::instrument(level = "debug", skip_all, fields(id = state.id))] async fn submit( State(state): State, diff --git a/mpc-recovery/src/lib.rs b/mpc-recovery/src/lib.rs index b21d640cc..6d16a5627 100644 --- a/mpc-recovery/src/lib.rs +++ b/mpc-recovery/src/lib.rs @@ -1,3 +1,5 @@ +use ed25519_dalek::SecretKey; +use rand::rngs::OsRng; use threshold_crypto::{PublicKeySet, SecretKeySet, SecretKeyShare}; mod leader_node; @@ -11,7 +13,10 @@ pub use leader_node::run as run_leader_node; pub use sign_node::run as run_sign_node; #[tracing::instrument(level = "debug", skip_all, fields(n = n, threshold = t))] -pub fn generate(n: usize, t: usize) -> anyhow::Result<(PublicKeySet, Vec)> { +pub fn generate( + n: usize, + t: usize, +) -> anyhow::Result<(PublicKeySet, Vec, SecretKey)> { let sk_set = SecretKeySet::random(t - 1, &mut rand::thread_rng()); let pk_set = sk_set.public_keys(); tracing::debug!(public_key = ?pk_set.public_key()); @@ -22,5 +27,8 @@ pub fn generate(n: usize, t: usize) -> anyhow::Result<(PublicKeySet, Vec, + /// TEMPORARY - Root ed25519 secret key + #[arg(long, env("MPC_RECOVERY_ROOT_SECRET_KEY"))] + root_secret_key: String, }, StartSign { /// Node ID @@ -57,7 +61,7 @@ async fn main() -> anyhow::Result<()> { match Cli::parse() { Cli::Generate { n, t } => { - let (pk_set, sk_shares) = mpc_recovery::generate(n, t)?; + let (pk_set, sk_shares, root_secret_key) = mpc_recovery::generate(n, t)?; println!("Public key set: {}", serde_json::to_string(&pk_set)?); for (i, sk_share) in sk_shares.iter().enumerate() { println!( @@ -66,6 +70,8 @@ async fn main() -> anyhow::Result<()> { serde_json::to_string(&SerdeSecret(sk_share))? ); } + + println!("Root private key: {}", hex::encode(root_secret_key)); } Cli::StartLeader { node_id, @@ -73,13 +79,23 @@ async fn main() -> anyhow::Result<()> { sk_share, web_port, sign_nodes, + root_secret_key, } => { let sk_share = load_sh_skare(node_id, sk_share).await?; let pk_set: PublicKeySet = serde_json::from_str(&pk_set).unwrap(); let sk_share: SecretKeyShare = serde_json::from_str(&sk_share).unwrap(); + let root_secret_key = SecretKey::from_bytes(&hex::decode(root_secret_key)?)?; - mpc_recovery::run_leader_node(node_id, pk_set, sk_share, web_port, sign_nodes).await; + mpc_recovery::run_leader_node( + node_id, + pk_set, + sk_share, + web_port, + sign_nodes, + root_secret_key, + ) + .await; } Cli::StartSign { node_id, diff --git a/mpc-recovery/src/msg.rs b/mpc-recovery/src/msg.rs index 9053c6911..8c757ffb1 100644 --- a/mpc-recovery/src/msg.rs +++ b/mpc-recovery/src/msg.rs @@ -1,7 +1,41 @@ +use crate::NodeId; +use ed25519_dalek::PublicKey; use serde::{Deserialize, Serialize}; use threshold_crypto::{Signature, SignatureShare}; -use crate::NodeId; +#[derive(Serialize, Deserialize)] +pub struct AddRecoveryMethodRequest { + pub access_token: String, + pub account_id: String, +} + +#[derive(Serialize, Deserialize)] +#[serde(tag = "type")] +#[serde(rename_all = "snake_case")] +pub enum AddRecoveryMethodResponse { + Ok { + #[serde(with = "hex_public_key")] + public_key: PublicKey, + }, + Err { + msg: String, + }, +} + +#[derive(Serialize, Deserialize)] +pub struct RecoverAccountRequest { + pub access_token: String, + #[serde(with = "hex_public_key")] + pub public_key: PublicKey, +} + +#[derive(Serialize, Deserialize)] +#[serde(tag = "type")] +#[serde(rename_all = "snake_case")] +pub enum RecoverAccountResponse { + Ok, + Err { msg: String }, +} #[derive(Serialize, Deserialize)] pub struct LeaderRequest { @@ -65,3 +99,25 @@ mod hex_sig_share { .map_err(serde::de::Error::custom) } } + +mod hex_public_key { + use ed25519_dalek::PublicKey; + use serde::{Deserialize, Deserializer, Serializer}; + + pub fn serialize(sig_share: &PublicKey, serializer: S) -> Result + where + S: Serializer, + { + let s = hex::encode(sig_share.to_bytes()); + serializer.serialize_str(&s) + } + + pub fn deserialize<'de, D>(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let s = String::deserialize(deserializer)?; + PublicKey::from_bytes(&hex::decode(s).map_err(serde::de::Error::custom)?) + .map_err(serde::de::Error::custom) + } +}