diff --git a/Cargo.lock b/Cargo.lock index b4a17e058..eaddd5838 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -579,7 +579,7 @@ dependencies = [ "http", "http-body", "lazy_static", - "percent-encoding 2.3.0", + "percent-encoding 2.3.1", "pin-project-lite", "tracing", ] @@ -611,12 +611,12 @@ dependencies = [ "http", "http-body", "once_cell", - "percent-encoding 2.3.0", + "percent-encoding 2.3.1", "regex", "tokio-stream", "tower", "tracing", - "url 2.4.1", + "url 2.5.0", ] [[package]] @@ -698,7 +698,7 @@ dependencies = [ "hmac 0.12.1", "http", "once_cell", - "percent-encoding 2.3.0", + "percent-encoding 2.3.1", "regex", "sha2 0.10.8", "time", @@ -789,7 +789,7 @@ dependencies = [ "http-body", "hyper", "once_cell", - "percent-encoding 2.3.0", + "percent-encoding 2.3.1", "pin-project-lite", "pin-utils", "tokio", @@ -903,7 +903,7 @@ dependencies = [ "matchit", "memchr", "mime", - "percent-encoding 2.3.0", + "percent-encoding 2.3.1", "pin-project-lite", "rustversion", "serde", @@ -1191,7 +1191,7 @@ dependencies = [ "thiserror", "tokio", "tokio-util 0.7.10", - "url 2.4.1", + "url 2.5.0", "winapi", ] @@ -1527,6 +1527,30 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e" +[[package]] +name = "chacha20" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3613f74bd2eac03dad61bd53dbe620703d4371614fe0bc3b9f04dd36fe4e818" +dependencies = [ + "cfg-if 1.0.0", + "cipher 0.4.4", + "cpufeatures", +] + +[[package]] +name = "chacha20poly1305" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10cd79432192d1c0f4e1a0fef9527696cc039165d729fb41b3f4f4f354c2dc35" +dependencies = [ + "aead", + "chacha20", + "cipher 0.4.4", + "poly1305", + "zeroize", +] + [[package]] name = "chrono" version = "0.4.31" @@ -1559,6 +1583,7 @@ checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" dependencies = [ "crypto-common", "inout", + "zeroize", ] [[package]] @@ -1716,7 +1741,7 @@ version = "0.16.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e859cd57d0710d9e06c381b550c06e76992472a8c6d527aecd2fc673dcc231fb" dependencies = [ - "percent-encoding 2.3.0", + "percent-encoding 2.3.1", "time", "version_check", ] @@ -1735,7 +1760,7 @@ dependencies = [ "serde_derive", "serde_json", "time", - "url 2.4.1", + "url 2.5.0", ] [[package]] @@ -1893,7 +1918,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ddc25c87ebf29b249e801de5eed820f0c9ba001054bf73008df884690a03e6eb" dependencies = [ "cryptoxide", - "curve25519-dalek", + "curve25519-dalek 3.2.0", "digest 0.9.0", "ff-zeroize", "generic-array 0.14.7", @@ -1903,7 +1928,7 @@ dependencies = [ "merkle-cbt", "num-integer", "num-traits", - "p256", + "p256 0.9.0", "pairing-plus", "rand 0.6.5", "rand 0.7.3", @@ -1933,6 +1958,32 @@ dependencies = [ "zeroize", ] +[[package]] +name = "curve25519-dalek" +version = "4.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e89b8c6a2e4b1f45971ad09761aafb85514a84744b67a95e32c3cc1352d1f65c" +dependencies = [ + "cfg-if 1.0.0", + "cpufeatures", + "curve25519-dalek-derive", + "fiat-crypto", + "platforms", + "rustc_version", + "subtle", +] + +[[package]] +name = "curve25519-dalek-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.39", +] + [[package]] name = "darling" version = "0.13.4" @@ -2040,9 +2091,9 @@ dependencies = [ [[package]] name = "data-encoding" -version = "2.4.0" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2e66c9d817f1720209181c316d28635c050fa304f9c79e47a520882661b7308" +checksum = "7e962a19be5cfc3f3bf6dd8f61eb50107f356ad6270fbb3ed41476571db78be5" [[package]] name = "debugid" @@ -2272,7 +2323,7 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c762bae6dcaf24c4c84667b8579785430908723d5c889f469d76a41d59cc7a9d" dependencies = [ - "curve25519-dalek", + "curve25519-dalek 3.2.0", "ed25519", "rand 0.7.3", "serde", @@ -2325,6 +2376,7 @@ dependencies = [ "ff 0.13.0", "generic-array 0.14.7", "group 0.13.0", + "hkdf", "pkcs8 0.10.2", "rand_core 0.6.4", "sec1", @@ -2516,6 +2568,12 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "fiat-crypto" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27573eac26f4dd11e2b1916c3fe1baa56407c83c71a773a8ba17ec0bca03b6b7" + [[package]] name = "filetime" version = "0.2.22" @@ -2591,11 +2649,11 @@ checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" [[package]] name = "form_urlencoded" -version = "1.2.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a62bc1cf6f830c2ec14a513a9fb124d0a213a629668a4186f329db21fe045652" +checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" dependencies = [ - "percent-encoding 2.3.0", + "percent-encoding 2.3.1", ] [[package]] @@ -2759,6 +2817,7 @@ version = "0.14.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" dependencies = [ + "serde", "typenum", "version_check", "zeroize", @@ -2915,7 +2974,7 @@ dependencies = [ "tokio", "tokio-tungstenite", "tungstenite", - "url 2.4.1", + "url 2.5.0", ] [[package]] @@ -3073,6 +3132,15 @@ dependencies = [ "serde", ] +[[package]] +name = "hkdf" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "791a029f6b9fc27657f6f188ec6e5e43f6911f6f878e0dc5501396e09809d437" +dependencies = [ + "hmac 0.12.1", +] + [[package]] name = "hmac" version = "0.11.0" @@ -3101,6 +3169,29 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "hpke" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e04a5933a381bb81f00b083fce6b4528e16d735dbeecbb2bdb45e0dbbf3f7e17" +dependencies = [ + "aead", + "aes-gcm", + "byteorder", + "chacha20poly1305", + "digest 0.10.7", + "generic-array 0.14.7", + "hkdf", + "hmac 0.12.1", + "p256 0.13.2", + "rand_core 0.6.4", + "serde", + "sha2 0.10.8", + "subtle", + "x25519-dalek", + "zeroize", +] + [[package]] name = "http" version = "0.2.11" @@ -3303,9 +3394,9 @@ dependencies = [ [[package]] name = "idna" -version = "0.4.0" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d20d6b07bfbc108882d88ed8e37d39636dcc260e15e30c45e6ba089610b917c" +checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" dependencies = [ "unicode-bidi", "unicode-normalization", @@ -3744,6 +3835,17 @@ dependencies = [ "serde", ] +[[package]] +name = "mpc-keys" +version = "0.1.0" +dependencies = [ + "borsh 0.9.3", + "hex 0.4.3", + "hpke", + "rand 0.8.5", + "serde", +] + [[package]] name = "mpc-recovery" version = "0.1.0" @@ -3820,6 +3922,7 @@ dependencies = [ "hyper", "k256", "mpc-contract", + "mpc-keys", "mpc-recovery", "mpc-recovery-node", "multi-party-eddsa", @@ -3865,6 +3968,7 @@ dependencies = [ "k256", "local-ip-address", "mpc-contract", + "mpc-keys", "near-crypto 0.17.0", "near-fetch", "near-lake-framework", @@ -3880,7 +3984,7 @@ dependencies = [ "tokio-retry", "tracing", "tracing-subscriber", - "url 2.4.1", + "url 2.5.0", ] [[package]] @@ -4007,7 +4111,7 @@ dependencies = [ "borsh 0.9.3", "bs58 0.4.0", "c2-chacha", - "curve25519-dalek", + "curve25519-dalek 3.2.0", "derive_more", "ed25519-dalek", "near-account-id 0.14.0", @@ -4032,7 +4136,7 @@ dependencies = [ "borsh 0.10.3", "bs58 0.4.0", "c2-chacha", - "curve25519-dalek", + "curve25519-dalek 3.2.0", "derive_more", "ed25519-dalek", "hex 0.4.3", @@ -4136,7 +4240,7 @@ dependencies = [ [[package]] name = "near-lake-context-derive" version = "0.8.0-beta.2" -source = "git+https://github.com/near/near-lake-framework-rs.git?branch=daniyar/reproduce#6b09538e8e84e12c38d99fa02d9f8bfd27b18af8" +source = "git+https://github.com/near/near-lake-framework-rs.git?branch=daniyar/upgrade-sdk#e23e138314cea1842bbbd58808471f6716ee0ad2" dependencies = [ "quote", "syn 2.0.39", @@ -4145,7 +4249,7 @@ dependencies = [ [[package]] name = "near-lake-framework" version = "0.8.0-beta.2" -source = "git+https://github.com/near/near-lake-framework-rs.git?branch=daniyar/reproduce#6b09538e8e84e12c38d99fa02d9f8bfd27b18af8" +source = "git+https://github.com/near/near-lake-framework-rs.git?branch=daniyar/upgrade-sdk#e23e138314cea1842bbbd58808471f6716ee0ad2" dependencies = [ "async-stream", "async-trait", @@ -4168,7 +4272,7 @@ dependencies = [ [[package]] name = "near-lake-primitives" version = "0.8.0-beta.2" -source = "git+https://github.com/near/near-lake-framework-rs.git?branch=daniyar/reproduce#6b09538e8e84e12c38d99fa02d9f8bfd27b18af8" +source = "git+https://github.com/near/near-lake-framework-rs.git?branch=daniyar/upgrade-sdk#e23e138314cea1842bbbd58808471f6716ee0ad2" dependencies = [ "anyhow", "near-crypto 0.17.0", @@ -4531,7 +4635,7 @@ dependencies = [ "tokio", "tokio-retry", "tracing", - "url 2.4.1", + "url 2.5.0", ] [[package]] @@ -4816,7 +4920,7 @@ dependencies = [ "futures-util", "js-sys", "lazy_static", - "percent-encoding 2.3.0", + "percent-encoding 2.3.1", "pin-project", "rand 0.8.5", "thiserror", @@ -4946,7 +5050,7 @@ dependencies = [ "once_cell", "opentelemetry_api", "ordered-float", - "percent-encoding 2.3.0", + "percent-encoding 2.3.1", "rand 0.8.5", "regex", "serde_json", @@ -4993,6 +5097,16 @@ dependencies = [ "sha2 0.9.9", ] +[[package]] +name = "p256" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b" +dependencies = [ + "elliptic-curve 0.13.8", + "primeorder", +] + [[package]] name = "pairing-plus" version = "0.19.0" @@ -5141,9 +5255,9 @@ checksum = "31010dd2e1ac33d5b46a5b413495239882813e0369f8ed8a5e266f173602f831" [[package]] name = "percent-encoding" -version = "2.3.0" +version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94" +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" [[package]] name = "petgraph" @@ -5261,6 +5375,12 @@ version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" +[[package]] +name = "platforms" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14e6ab3f592e6fb464fc9712d8d6e6912de6473954635fd76a589d832cffcbb0" + [[package]] name = "polling" version = "2.8.0" @@ -5291,6 +5411,17 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "poly1305" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf" +dependencies = [ + "cpufeatures", + "opaque-debug 0.3.0", + "universal-hash", +] + [[package]] name = "polyval" version = "0.6.1" @@ -5331,6 +5462,15 @@ dependencies = [ "yansi", ] +[[package]] +name = "primeorder" +version = "0.13.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "353e1ca18966c16d9deb1c69278edbc5f194139612772bd9537af60ac231e1e6" +dependencies = [ + "elliptic-curve 0.13.8", +] + [[package]] name = "primitive-types" version = "0.10.1" @@ -5868,7 +6008,7 @@ dependencies = [ "mime", "native-tls", "once_cell", - "percent-encoding 2.3.0", + "percent-encoding 2.3.1", "pin-project-lite", "rustls 0.21.9", "rustls-pemfile", @@ -5881,7 +6021,7 @@ dependencies = [ "tokio-rustls 0.24.1", "tokio-util 0.7.10", "tower-service", - "url 2.4.1", + "url 2.5.0", "wasm-bindgen", "wasm-bindgen-futures", "web-sys", @@ -6297,9 +6437,9 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.192" +version = "1.0.193" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bca2a08484b285dcb282d0f67b26cadc0df8b19f8c12502c13d966bf9482f001" +checksum = "25dd9975e68d0cb5aa1120c288333fc98731bd1dd12f561e468ea4728c042b89" dependencies = [ "serde_derive", ] @@ -6315,9 +6455,9 @@ dependencies = [ [[package]] name = "serde_derive" -version = "1.0.192" +version = "1.0.193" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6c7207fbec9faa48073f3e3074cbe553af6ea512d7c21ba46e434e70ea9fbc1" +checksum = "43576ca501357b9b071ac53cdc7da8ef0cbd9493d8df094cd821777ea6e894d3" dependencies = [ "proc-macro2", "quote", @@ -7286,7 +7426,7 @@ dependencies = [ "http-body", "hyper", "hyper-timeout", - "percent-encoding 2.3.0", + "percent-encoding 2.3.1", "pin-project", "prost 0.9.0", "prost-derive 0.9.0", @@ -7317,7 +7457,7 @@ dependencies = [ "http-body", "hyper", "hyper-timeout", - "percent-encoding 2.3.0", + "percent-encoding 2.3.1", "pin-project", "prost 0.11.9", "tokio", @@ -7558,7 +7698,7 @@ dependencies = [ "rand 0.8.5", "sha1", "thiserror", - "url 2.4.1", + "url 2.5.0", "utf-8", ] @@ -7637,9 +7777,9 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] name = "ureq" -version = "2.8.0" +version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f5ccd538d4a604753ebc2f17cd9946e89b77bf87f6a8e2309667c6f2e87855e3" +checksum = "7830e33f6e25723d41a63f77e434159dad02919f18f55a512b5f16f3b1d77138" dependencies = [ "base64 0.21.5", "flate2", @@ -7647,7 +7787,7 @@ dependencies = [ "once_cell", "rustls 0.21.9", "rustls-webpki", - "url 2.4.1", + "url 2.5.0", "webpki-roots", ] @@ -7664,13 +7804,13 @@ dependencies = [ [[package]] name = "url" -version = "2.4.1" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "143b538f18257fac9cad154828a57c6bf5157e1aa604d4816b5995bf6de87ae5" +checksum = "31e6302e3bb753d46e83516cae55ae196fc0c309407cf11ab35cc51a4c2a4633" dependencies = [ "form_urlencoded", - "idna 0.4.0", - "percent-encoding 2.3.0", + "idna 0.5.0", + "percent-encoding 2.3.1", "serde", ] @@ -7843,9 +7983,9 @@ dependencies = [ [[package]] name = "webpki-roots" -version = "0.25.2" +version = "0.25.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14247bb57be4f377dfb94c72830b8ce8fc6beac03cf4bf7b9732eadd414123fc" +checksum = "1778a42e8b3b90bff8d0f5032bf22250792889a5cdc752aa0020c84abe3aaf10" [[package]] name = "wee_alloc" @@ -8077,6 +8217,16 @@ dependencies = [ "tap", ] +[[package]] +name = "x25519-dalek" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb66477291e7e8d2b0ff1bcb900bf29489a9692816d79874bea351e7a8b6de96" +dependencies = [ + "curve25519-dalek 4.1.1", + "rand_core 0.6.4", +] + [[package]] name = "xattr" version = "1.0.1" @@ -8119,7 +8269,7 @@ dependencies = [ "hyper-rustls 0.24.2", "itertools 0.10.5", "log", - "percent-encoding 2.3.0", + "percent-encoding 2.3.1", "rustls 0.21.9", "rustls-pemfile", "seahash", @@ -8128,7 +8278,7 @@ dependencies = [ "time", "tokio", "tower-service", - "url 2.4.1", + "url 2.5.0", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 58403271a..f6b465a7d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,6 +5,7 @@ members = [ "node", "integration-tests", "load-tests", + "keys", "test-oidc-provider", ] diff --git a/Dockerfile.multichain b/Dockerfile.multichain index 439d58e45..65714ef60 100644 --- a/Dockerfile.multichain +++ b/Dockerfile.multichain @@ -8,12 +8,14 @@ RUN apt-get update \ RUN echo "fn main() {}" > dummy.rs COPY node/Cargo.toml Cargo.toml RUN sed -i 's#src/main.rs#dummy.rs#' Cargo.toml +RUN sed -i 's#mpc-keys = { path = "../keys" }##' Cargo.toml RUN sed -i 's#mpc-contract = { path = "../contract" }##' Cargo.toml RUN cargo build COPY . . RUN sed -i 's#"mpc-recovery",##' Cargo.toml RUN sed -i 's#"integration-tests",##' Cargo.toml RUN sed -i 's#"load-tests",##' Cargo.toml +RUN sed -i 's#"keys",##' Cargo.toml RUN cargo build --package mpc-recovery-node FROM debian:stable-slim as runtime diff --git a/contract/src/lib.rs b/contract/src/lib.rs index 78039848f..4434b116f 100644 --- a/contract/src/lib.rs +++ b/contract/src/lib.rs @@ -5,6 +5,10 @@ use std::collections::{BTreeMap, HashSet}; type ParticipantId = u32; +pub mod hpke { + pub type PublicKey = [u8; 32]; +} + #[derive( Serialize, Deserialize, @@ -22,6 +26,10 @@ pub struct ParticipantInfo { pub id: ParticipantId, pub account_id: AccountId, pub url: String, + /// The public key used for encrypting messages. + pub cipher_pk: hpke::PublicKey, + /// The public key used for verifying messages. + pub sign_pk: PublicKey, } #[derive(BorshDeserialize, BorshSerialize, Serialize, Deserialize, Debug)] @@ -34,6 +42,7 @@ pub struct InitializingContractState { #[derive(BorshDeserialize, BorshSerialize, Serialize, Deserialize, Debug)] pub struct RunningContractState { pub epoch: u64, + // TODO: why is this account id for participants instead of participant id? pub participants: BTreeMap, pub threshold: usize, pub public_key: PublicKey, @@ -83,7 +92,13 @@ impl MpcContract { self.protocol_state } - pub fn join(&mut self, participant_id: ParticipantId, url: String) { + pub fn join( + &mut self, + participant_id: ParticipantId, + url: String, + cipher_pk: hpke::PublicKey, + sign_pk: PublicKey, + ) { match &mut self.protocol_state { ProtocolContractState::Running(RunningContractState { participants, @@ -100,6 +115,8 @@ impl MpcContract { id: participant_id, account_id, url, + cipher_pk, + sign_pk, }, ); } diff --git a/infra/modules/signer/main.tf b/infra/modules/signer/main.tf index 7800b806d..038574a2c 100644 --- a/infra/modules/signer/main.tf +++ b/infra/modules/signer/main.tf @@ -8,9 +8,13 @@ resource "google_cloud_run_v2_service" "signer" { annotations = var.metadata_annotations == null ? null : var.metadata_annotations - vpc_access { - connector = var.connector_id == null ? null : var.connector_id - egress = "PRIVATE_RANGES_ONLY" + // Conditional block in case connector_id is present. See https://stackoverflow.com/a/69891235 + dynamic "vpc_access" { + for_each = var.connector_id == null ? [] : [1] + content { + connector = var.connector_id == null ? null : var.connector_id + egress = "PRIVATE_RANGES_ONLY" + } } scaling { diff --git a/infra/mpc-recovery-dev/terraform-dev.tfvars b/infra/mpc-recovery-dev/terraform-dev.tfvars index a85c406aa..e4879bcc0 100644 --- a/infra/mpc-recovery-dev/terraform-dev.tfvars +++ b/infra/mpc-recovery-dev/terraform-dev.tfvars @@ -1,6 +1,6 @@ env = "dev" project = "pagoda-discovery-platform-dev" -docker_image = "us-east1-docker.pkg.dev/pagoda-discovery-platform-dev/mpc-recovery/mpc-recovery-dev:3b95ef518e320cf35438bbc2e39be71040769d99" +docker_image = "us-east1-docker.pkg.dev/pagoda-discovery-platform-dev/mpc-recovery/mpc-recovery-dev:latest" account_creator_id = "mpc-recovery-dev-creator.testnet" account_creator_sk_secret_id = "mpc-recovery-account-creator-sk-dev" diff --git a/infra/mpc-recovery-prod/backend-config-prod.tfvars b/infra/mpc-recovery-prod/backend-config-prod.tfvars deleted file mode 100644 index 85d7be82f..000000000 --- a/infra/mpc-recovery-prod/backend-config-prod.tfvars +++ /dev/null @@ -1 +0,0 @@ -bucket = "mpc-recovery-terraform-prod" diff --git a/infra/mpc-recovery-prod/main.tf b/infra/mpc-recovery-prod/main.tf index aa4545bf7..fcbc2e929 100644 --- a/infra/mpc-recovery-prod/main.tf +++ b/infra/mpc-recovery-prod/main.tf @@ -17,20 +17,10 @@ locals { client_email = jsondecode(local.credentials).client_email client_id = jsondecode(local.credentials).client_id - env = { - defaults = { - near_rpc = "https://rpc.testnet.near.org" - near_root_account = "testnet" - } - testnet = { - } - mainnet = { - near_rpc = "https://rpc.mainnet.near.org" - near_root_account = "near" - } + workspace = { + near_rpc = "https://rpc.mainnet.near.org" + near_root_account = "near" } - - workspace = merge(local.env["defaults"], contains(keys(local.env), terraform.workspace) ? local.env[terraform.workspace] : local.env["defaults"]) } data "external" "git_checkout" { @@ -39,7 +29,6 @@ data "external" "git_checkout" { provider "google" { credentials = local.credentials - # credentials = file("~/.config/gcloud/application_default_credentials.json") project = var.project region = var.region @@ -50,8 +39,8 @@ provider "google" { * Create brand new service account with basic IAM */ resource "google_service_account" "service_account" { - account_id = "mpc-recovery-prod" - display_name = "MPC Recovery prod Account" + account_id = "mpc-recovery-mainnet" + display_name = "MPC Recovery mainnet Account" } resource "google_service_account_iam_binding" "serivce-account-iam" { @@ -59,8 +48,7 @@ resource "google_service_account_iam_binding" "serivce-account-iam" { role = "roles/iam.serviceAccountUser" members = [ - "serviceAccount:${local.client_email}", - # "serviceAccount:mpc-recovery@pagoda-discovery-platform-prod.iam.gserviceaccount.com" + "serviceAccount:${local.client_email}" ] } @@ -130,7 +118,7 @@ module "signer-mainnet" { count = length(var.signer_configs) source = "../modules/signer" - env = "prod" + env = "mainnet" service_name = "mpc-recovery-signer-${count.index}-mainnet" project = var.project region = var.region @@ -157,7 +145,7 @@ module "signer-mainnet" { module "leader-mainnet" { source = "../modules/leader" - env = "prod" + env = "mainnet" service_name = "mpc-recovery-leader-mainnet" project = var.project region = var.region diff --git a/infra/mpc-recovery-prod/variables.tf b/infra/mpc-recovery-prod/variables.tf index 84737ca10..80a9b7106 100644 --- a/infra/mpc-recovery-prod/variables.tf +++ b/infra/mpc-recovery-prod/variables.tf @@ -1,6 +1,3 @@ -variable "env" { -} - variable "project" { } diff --git a/infra/mpc-recovery-testnet/backend-config-prod.tfvars b/infra/mpc-recovery-testnet/backend-config-prod.tfvars deleted file mode 100644 index 85d7be82f..000000000 --- a/infra/mpc-recovery-testnet/backend-config-prod.tfvars +++ /dev/null @@ -1 +0,0 @@ -bucket = "mpc-recovery-terraform-prod" diff --git a/infra/mpc-recovery-testnet/main.tf b/infra/mpc-recovery-testnet/main.tf index 72ade796c..796afc561 100644 --- a/infra/mpc-recovery-testnet/main.tf +++ b/infra/mpc-recovery-testnet/main.tf @@ -17,20 +17,10 @@ locals { client_email = jsondecode(local.credentials).client_email client_id = jsondecode(local.credentials).client_id - env = { - defaults = { - near_rpc = "https://rpc.testnet.near.org" - near_root_account = "testnet" - } - testnet = { - } - mainnet = { - near_rpc = "https://rpc.mainnet.near.org" - near_root_account = "near" - } + workspace = { + near_rpc = "https://rpc.mainnet.near.org" + near_root_account = "near" } - - workspace = merge(local.env["defaults"], contains(keys(local.env), terraform.workspace) ? local.env[terraform.workspace] : local.env["defaults"]) } data "external" "git_checkout" { @@ -39,7 +29,6 @@ data "external" "git_checkout" { provider "google" { credentials = local.credentials - # credentials = file("~/.config/gcloud/application_default_credentials.json") project = var.project region = var.region @@ -50,8 +39,8 @@ provider "google" { * Create brand new service account with basic IAM */ resource "google_service_account" "service_account" { - account_id = "mpc-recovery-prod" - display_name = "MPC Recovery prod Account" + account_id = "mpc-recovery-testnet" + display_name = "MPC Recovery testnet Account" } resource "google_service_account_iam_binding" "serivce-account-iam" { @@ -60,7 +49,6 @@ resource "google_service_account_iam_binding" "serivce-account-iam" { members = [ "serviceAccount:${local.client_email}", - # "serviceAccount:mpc-recovery@pagoda-discovery-platform-prod.iam.gserviceaccount.com" ] } diff --git a/infra/mpc-recovery-testnet/variables.tf b/infra/mpc-recovery-testnet/variables.tf index 84737ca10..80a9b7106 100644 --- a/infra/mpc-recovery-testnet/variables.tf +++ b/infra/mpc-recovery-testnet/variables.tf @@ -1,6 +1,3 @@ -variable "env" { -} - variable "project" { } diff --git a/infra/partner/main.tf b/infra/partner/main.tf index ec25be594..59f0b1c64 100644 --- a/infra/partner/main.tf +++ b/infra/partner/main.tf @@ -21,14 +21,6 @@ provider "google" { zone = var.zone } -provider "docker" { - registry_auth { - address = "${var.region}-docker.pkg.dev" - username = "_json_key" - password = local.credentials - } -} - /* * Create brand new service account with basic IAM */ @@ -70,31 +62,6 @@ resource "google_secret_manager_secret_iam_member" "secret_share_secret_access" member = "serviceAccount:${google_service_account.service_account.email}" } -resource "google_secret_manager_secret_iam_member" "oidc_providers_secret_access" { - secret_id = var.oidc_providers_secret_id - role = "roles/secretmanager.secretAccessor" - member = "serviceAccount:${google_service_account.service_account.email}" -} - -/* - * Create Artifact Registry repo, tag existing Docker image and push to the repo - */ -resource "google_artifact_registry_repository" "mpc_recovery" { - repository_id = "mpc-recovery-partner-${var.env}" - format = "DOCKER" -} - -resource "google_secret_manager_secret_iam_member" "secret_share_secret_access" { - secret_id = var.sk_share_secret_id - role = "roles/secretmanager.secretAccessor" - member = "serviceAccount:${google_service_account.service_account.email}" -} - -resource "docker_tag" "mpc_recovery" { - source_image = var.docker_image - target_image = "${var.region}-docker.pkg.dev/${var.project}/${google_artifact_registry_repository.mpc_recovery.name}/mpc-recovery-${var.env}" -} - /* * Create a partner signer node */ @@ -102,7 +69,7 @@ module "signer" { source = "../modules/signer" env = var.env - service_name = "partner-service-name" + service_name = var.service_name project = var.project region = var.region zone = var.zone @@ -117,9 +84,7 @@ module "signer" { connector_id = var.connector_id depends_on = [ - docker_registry_image.mpc_recovery, google_secret_manager_secret_iam_member.cipher_key_secret_access, - google_secret_manager_secret_iam_member.secret_share_secret_access, - google_secret_manager_secret_iam_member.oidc_providers_secret_access + google_secret_manager_secret_iam_member.secret_share_secret_access ] } diff --git a/infra/partner/template.tfvars b/infra/partner/template.tfvars index a9a77bf3b..2a48e43d9 100644 --- a/infra/partner/template.tfvars +++ b/infra/partner/template.tfvars @@ -6,6 +6,8 @@ zone = "us-east1-c" docker_image = "near/mpc-recovery" node_id = "0" -oidc_providers_secret_id = "mpc-recovery-allowed-oidc-providers-0-dev" -cipher_key_secret_id = "mpc-recovery-encryption-cipher-0-dev" -sk_share_secret_id = "mpc-recovery-secret-share-0-dev" +cipher_key_secret_id = "mpc-recovery-encryption-cipher-0-dev" +sk_share_secret_id = "mpc-recovery-secret-share-0-dev" + +jwt_signature_pk_url = "https://www.googleapis.com/robot/v1/metadata/x509/securetoken@system.gserviceaccount.com" +service_name = "mpc-recovery-signer-0-partner-dev" diff --git a/infra/partner/variables.tf b/infra/partner/variables.tf index 668b91ce8..0bd8a31ff 100644 --- a/infra/partner/variables.tf +++ b/infra/partner/variables.tf @@ -33,10 +33,10 @@ variable "sk_share_secret_id" { type = string } -variable "oidc_providers_secret_id" { - type = string -} - variable "jwt_signature_pk_url" { -} \ No newline at end of file +} + +variable "service_name" { + type = string +} diff --git a/integration-tests/Cargo.toml b/integration-tests/Cargo.toml index de7c1d45b..3711db44c 100644 --- a/integration-tests/Cargo.toml +++ b/integration-tests/Cargo.toml @@ -39,11 +39,12 @@ near-crypto = "0.17" near-fetch = "0.0.12" near-jsonrpc-client = "0.6" near-primitives = "0.17" -near-lake-framework = { git = "https://github.com/near/near-lake-framework-rs.git", branch = "daniyar/reproduce" } -near-lake-primitives = { git = "https://github.com/near/near-lake-framework-rs.git", branch = "daniyar/reproduce" } +near-lake-framework = { git = "https://github.com/near/near-lake-framework-rs.git", branch = "daniyar/upgrade-sdk" } +near-lake-primitives = { git = "https://github.com/near/near-lake-framework-rs.git", branch = "daniyar/upgrade-sdk" } near-units = "0.2.0" mpc-contract = { path = "../contract" } +mpc-keys = { path = "../keys" } mpc-recovery = { path = "../mpc-recovery" } mpc-recovery-node = { path = "../node" } @@ -57,8 +58,6 @@ tracing-log = "0.1.3" tokio-util = { version = "0.7", features = ["full"] } reqwest = "0.11.16" -mpc-contract = { path = "../contract" } - [features] default = [] docker-test = [] diff --git a/integration-tests/src/multichain/containers.rs b/integration-tests/src/multichain/containers.rs index f0f2794a8..8b2670e1c 100644 --- a/integration-tests/src/multichain/containers.rs +++ b/integration-tests/src/multichain/containers.rs @@ -1,4 +1,5 @@ use ed25519_dalek::ed25519::signature::digest::{consts::U32, generic_array::GenericArray}; +use mpc_keys::hpke; use multi_party_eddsa::protocols::ExpandedKeyPair; use near_workspaces::AccountId; use testcontainers::{ @@ -11,6 +12,9 @@ pub struct Node<'a> { pub container: Container<'a, GenericImage>, pub address: String, pub local_address: String, + pub cipher_pk: hpke::PublicKey, + pub cipher_sk: hpke::SecretKey, + pub sign_pk: near_workspaces::types::PublicKey, } pub struct NodeApi { @@ -33,6 +37,7 @@ impl<'a> Node<'a> { account_sk: &near_workspaces::types::SecretKey, ) -> anyhow::Result> { tracing::info!(node_id, "running node container"); + let (cipher_sk, cipher_pk) = hpke::generate(); let args = mpc_recovery_node::cli::Cli::Start { node_id: node_id.into(), near_rpc: ctx.lake_indexer.rpc_host_address.clone(), @@ -40,6 +45,8 @@ impl<'a> Node<'a> { account: account.clone(), account_sk: account_sk.to_string().parse()?, web_port: Self::CONTAINER_PORT, + cipher_pk: hex::encode(cipher_pk.to_bytes()), + cipher_sk: hex::encode(cipher_sk.to_bytes()), indexer_options: mpc_recovery_node::indexer::Options { s3_bucket: ctx.localstack.s3_bucket.clone(), s3_region: ctx.localstack.s3_region.clone(), @@ -73,6 +80,9 @@ impl<'a> Node<'a> { container, address: full_address, local_address: format!("http://localhost:{host_port}"), + cipher_pk, + cipher_sk, + sign_pk: account_sk.public_key(), }) } } diff --git a/integration-tests/src/multichain/local.rs b/integration-tests/src/multichain/local.rs index 807b72057..4bb3451d1 100644 --- a/integration-tests/src/multichain/local.rs +++ b/integration-tests/src/multichain/local.rs @@ -1,5 +1,6 @@ use crate::{mpc, util}; use async_process::Child; +use mpc_keys::hpke; use near_workspaces::AccountId; #[allow(dead_code)] @@ -7,7 +8,9 @@ pub struct Node { pub address: String, node_id: usize, account: AccountId, - account_sk: near_workspaces::types::SecretKey, + pub account_sk: near_workspaces::types::SecretKey, + pub cipher_pk: hpke::PublicKey, + cipher_sk: hpke::SecretKey, // process held so it's not dropped. Once dropped, process will be killed. #[allow(unused)] @@ -22,6 +25,7 @@ impl Node { account_sk: &near_workspaces::types::SecretKey, ) -> anyhow::Result { let web_port = util::pick_unused_port().await?; + let (cipher_sk, cipher_pk) = hpke::generate(); let cli = mpc_recovery_node::cli::Cli::Start { node_id: node_id.into(), near_rpc: ctx.lake_indexer.rpc_host_address.clone(), @@ -29,6 +33,8 @@ impl Node { account: account.clone(), account_sk: account_sk.to_string().parse()?, web_port, + cipher_pk: hex::encode(cipher_pk.to_bytes()), + cipher_sk: hex::encode(cipher_sk.to_bytes()), indexer_options: mpc_recovery_node::indexer::Options { s3_bucket: ctx.localstack.s3_bucket.clone(), s3_region: ctx.localstack.s3_region.clone(), @@ -49,6 +55,8 @@ impl Node { node_id: node_id as usize, account: account.clone(), account_sk: account_sk.clone(), + cipher_pk, + cipher_sk, process, }) } diff --git a/integration-tests/src/multichain/mod.rs b/integration-tests/src/multichain/mod.rs index 9226907c2..49d4b89bb 100644 --- a/integration-tests/src/multichain/mod.rs +++ b/integration-tests/src/multichain/mod.rs @@ -144,6 +144,8 @@ pub async fn docker(nodes: usize, docker_client: &DockerClient) -> anyhow::Resul id: i as u32, account_id: account.id().to_string().parse().unwrap(), url: node.address.clone(), + cipher_pk: node.cipher_pk.to_bytes(), + sign_pk: node.sign_pk.to_string().parse().unwrap(), }, ) }) @@ -193,6 +195,8 @@ pub async fn host(nodes: usize, docker_client: &DockerClient) -> anyhow::Result< id: i as u32, account_id: account.id().to_string().parse().unwrap(), url: node.address.clone(), + cipher_pk: node.cipher_pk.to_bytes(), + sign_pk: node.account_sk.public_key().to_string().parse().unwrap(), }, ) }) diff --git a/keys/Cargo.toml b/keys/Cargo.toml new file mode 100644 index 000000000..b45de18bd --- /dev/null +++ b/keys/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "mpc-keys" +version = "0.1.0" +edition = "2021" + +[lib] +crate-type = ["cdylib", "lib"] + +[dependencies] +borsh = { version = "0.9.3" } +hpke = { version = "0.11", features = ["serde_impls", "std"] } +serde = { version = "1", features = ["derive"] } +rand = { version = "0.8" } + +[dev-dependencies] +hex = "*" diff --git a/keys/src/hpke.rs b/keys/src/hpke.rs new file mode 100644 index 000000000..e19058e00 --- /dev/null +++ b/keys/src/hpke.rs @@ -0,0 +1,158 @@ +use borsh::{self, BorshDeserialize, BorshSerialize}; +use hpke::{ + aead::{AeadTag, ChaCha20Poly1305}, + kdf::HkdfSha384, + kem::X25519HkdfSha256, + OpModeR, +}; +use serde::{Deserialize, Serialize}; + +/// This can be used to customize the generated key. This will be used as a sort of +/// versioning mechanism for the key. It's additional context about who is encrypting +/// the key. This is used to prevent a key from being used in a context it was not +/// supposed to be used for. +const INFO_ENTROPY: &[u8] = b"mpc-key-v1"; + +// Interchangeable type parameters for the HPKE context. +pub type Kem = X25519HkdfSha256; +pub type Aead = ChaCha20Poly1305; +pub type Kdf = HkdfSha384; + +#[derive(Serialize, Deserialize)] +pub struct Ciphered { + pub encapped_key: EncappedKey, + pub text: CipherText, + pub tag: Tag, +} + +#[derive(Serialize, Deserialize)] +pub struct Tag(AeadTag); + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct PublicKey(::PublicKey); + +// NOTE: Arc is used to hack up the fact that the internal private key does not have Send constraint. +#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct SecretKey(::PrivateKey); + +#[derive(Clone, Serialize, Deserialize)] +pub struct EncappedKey(::EncappedKey); + +// Series of bytes that have been previously encoded/encrypted. +pub type CipherText = Vec; + +impl PublicKey { + pub fn to_bytes(&self) -> [u8; 32] { + hpke::Serializable::to_bytes(&self.0).into() + } + + pub fn try_from_bytes(bytes: &[u8]) -> Result { + Ok(Self(hpke::Deserializable::from_bytes(bytes)?)) + } + + /// Assumes the bytes are correctly formatted. + pub fn from_bytes(bytes: &[u8]) -> Self { + Self::try_from_bytes(bytes).expect("invalid bytes") + } + + pub fn encrypt(&self, msg: &[u8], associated_data: &[u8]) -> Result { + let mut csprng = ::from_entropy(); + + // Encapsulate a key and use the resulting shared secret to encrypt a message. The AEAD context + // is what you use to encrypt. + let (encapped_key, mut sender_ctx) = hpke::setup_sender::( + &hpke::OpModeS::Base, + &self.0, + INFO_ENTROPY, + &mut csprng, + )?; + + // On success, seal_in_place_detached() will encrypt the plaintext in place + let mut ciphertext = msg.to_vec(); + let tag = sender_ctx.seal_in_place_detached(&mut ciphertext, associated_data)?; + Ok(Ciphered { + encapped_key: EncappedKey(encapped_key), + text: ciphertext, + tag: Tag(tag), + }) + } +} + +impl BorshSerialize for PublicKey { + fn serialize(&self, writer: &mut W) -> std::io::Result<()> { + BorshSerialize::serialize(&self.to_bytes(), writer) + } +} + +impl BorshDeserialize for PublicKey { + fn deserialize(buf: &mut &[u8]) -> std::io::Result { + Ok(Self::from_bytes( + & as BorshDeserialize>::deserialize(buf)?, + )) + } +} + +impl SecretKey { + pub fn to_bytes(&self) -> [u8; 32] { + hpke::Serializable::to_bytes(&self.0).into() + } + + pub fn try_from_bytes(bytes: &[u8]) -> Result { + Ok(Self(hpke::Deserializable::from_bytes(bytes)?)) + } + + pub fn decrypt( + &self, + cipher: &Ciphered, + associated_data: &[u8], + ) -> Result, hpke::HpkeError> { + // Decapsulate and derive the shared secret. This creates a shared AEAD context. + let mut receiver_ctx = hpke::setup_receiver::( + &OpModeR::Base, + &self.0, + &cipher.encapped_key.0, + INFO_ENTROPY, + )?; + + // On success, open_in_place_detached() will decrypt the ciphertext in place + let mut plaintext = cipher.text.to_vec(); + receiver_ctx.open_in_place_detached(&mut plaintext, associated_data, &cipher.tag.0)?; + Ok(plaintext) + } + + /// Get the public key associated with this secret key. + pub fn public_key(&self) -> PublicKey { + PublicKey(::sk_to_pk(&self.0)) + } +} + +pub fn generate() -> (SecretKey, PublicKey) { + let mut csprng = ::from_entropy(); + let (sk, pk) = ::gen_keypair(&mut csprng); + (SecretKey(sk), PublicKey(pk)) +} + +#[cfg(test)] +mod tests { + #[test] + fn test_encrypt_decrypt() { + let (sk, pk) = super::generate(); + let msg = b"hello world"; + let associated_data = b"associated data"; + + let cipher = pk.encrypt(msg, associated_data).unwrap(); + let decrypted = sk.decrypt(&cipher, associated_data).unwrap(); + + assert_eq!(msg, &decrypted[..]); + } + + #[test] + fn test_serialization_format() { + let sk_hex = "cf3df427dc1377914349b592cfff8deb4b9f8ab1cc4baa8e8e004b6502ac1ca0"; + let pk_hex = "0e6d143bff1d67f297ac68cb9be3667e38f1dc2b244be48bf1d6c6bd7d367c3c"; + + let sk = super::SecretKey::try_from_bytes(&hex::decode(sk_hex).unwrap()).unwrap(); + let pk = super::PublicKey::try_from_bytes(&hex::decode(pk_hex).unwrap()).unwrap(); + assert_eq!(sk.public_key(), pk); + } +} diff --git a/keys/src/lib.rs b/keys/src/lib.rs new file mode 100644 index 000000000..a486f3543 --- /dev/null +++ b/keys/src/lib.rs @@ -0,0 +1 @@ +pub mod hpke; diff --git a/node/Cargo.toml b/node/Cargo.toml index e42c0f2fc..c8217422a 100644 --- a/node/Cargo.toml +++ b/node/Cargo.toml @@ -35,9 +35,10 @@ url = { version = "2.4.0", features = ["serde"] } near-crypto = "0.17" near-fetch = "0.0.12" -near-lake-framework = { git = "https://github.com/near/near-lake-framework-rs.git", branch = "daniyar/reproduce" } -near-lake-primitives = { git = "https://github.com/near/near-lake-framework-rs.git", branch = "daniyar/reproduce" } +near-lake-framework = { git = "https://github.com/near/near-lake-framework-rs.git", branch = "daniyar/upgrade-sdk" } +near-lake-primitives = { git = "https://github.com/near/near-lake-framework-rs.git", branch = "daniyar/upgrade-sdk" } near-primitives = "0.17" near-sdk = "4.1.1" mpc-contract = { path = "../contract" } +mpc-keys = { path = "../keys" } diff --git a/node/src/cli.rs b/node/src/cli.rs index 053faedf2..a65cd7d78 100644 --- a/node/src/cli.rs +++ b/node/src/cli.rs @@ -11,6 +11,8 @@ use tokio::sync::{mpsc, RwLock}; use tracing_subscriber::EnvFilter; use url::Url; +use mpc_keys::hpke; + #[derive(Parser, Debug)] pub enum Cli { Start { @@ -36,6 +38,13 @@ pub enum Cli { /// The web port for this server #[arg(long, env("MPC_RECOVERY_WEB_PORT"))] web_port: u16, + // TODO: need to add in CipherPK type for parsing. + /// The cipher public key used to encrypt messages between nodes. + #[arg(long, env("MPC_RECOVERY_CIPHER_PK"))] + cipher_pk: String, + /// The cipher secret key used to decrypt messages between nodes. + #[arg(long, env("MPC_RECOVERY_CIPHER_SK"))] + cipher_sk: String, /// NEAR Lake Indexer options #[clap(flatten)] indexer_options: indexer::Options, @@ -57,6 +66,8 @@ impl Cli { account, account_sk, web_port, + cipher_pk, + cipher_sk, indexer_options, } => { let mut args = vec![ @@ -73,6 +84,10 @@ impl Cli { account_sk.to_string(), "--web-port".to_string(), web_port.to_string(), + "--cipher-pk".to_string(), + cipher_pk, + "--cipher-sk".to_string(), + cipher_sk, ]; args.extend(indexer_options.into_str_args()); args @@ -102,6 +117,8 @@ pub fn run(cmd: Cli) -> anyhow::Result<()> { mpc_contract_id, account, account_sk, + cipher_pk, + cipher_sk, indexer_options, } => { let sign_queue = Arc::new(RwLock::new(SignQueue::new())); @@ -132,6 +149,7 @@ pub fn run(cmd: Cli) -> anyhow::Result<()> { signer.clone(), receiver, sign_queue.clone(), + hpke::PublicKey::try_from_bytes(&hex::decode(cipher_pk)?).unwrap(), ); tracing::debug!("protocol initialized"); let protocol_handle = tokio::spawn(async move { @@ -139,6 +157,8 @@ pub fn run(cmd: Cli) -> anyhow::Result<()> { }); tracing::debug!("protocol thread spawned"); let mpc_contract_id_cloned = mpc_contract_id.clone(); + let cipher_sk = + hpke::SecretKey::try_from_bytes(&hex::decode(cipher_sk)?).unwrap(); let web_handle = tokio::spawn(async move { web::run( web_port, @@ -146,6 +166,7 @@ pub fn run(cmd: Cli) -> anyhow::Result<()> { rpc_client, signer, sender, + cipher_sk, protocol_state, ) .await diff --git a/node/src/http_client.rs b/node/src/http_client.rs index 7538b2855..d54454fc1 100644 --- a/node/src/http_client.rs +++ b/node/src/http_client.rs @@ -1,5 +1,7 @@ +use crate::protocol::message::SignedMessage; use crate::protocol::MpcMessage; use cait_sith::protocol::Participant; +use mpc_keys::hpke; use reqwest::{Client, IntoUrl}; use std::str::Utf8Error; use tokio_retry::strategy::{jitter, ExponentialBackoff}; @@ -9,19 +11,30 @@ use tokio_retry::Retry; pub enum SendError { #[error("http request was unsuccessful: {0}")] Unsuccessful(String), + #[error("serialization unsuccessful: {0}")] + DataConversionError(serde_json::Error), #[error("http client error: {0}")] ReqwestClientError(reqwest::Error), #[error("http response could not be parsed: {0}")] ReqwestBodyError(reqwest::Error), #[error("http response body is not valid utf-8: {0}")] MalformedResponse(Utf8Error), + #[error("encryption error: {0}")] + EncryptionError(String), } -pub async fn message( +pub async fn send_encrypted( + participant: Participant, + cipher_pk: &hpke::PublicKey, + sign_sk: &near_crypto::SecretKey, client: &Client, url: U, message: MpcMessage, ) -> Result<(), SendError> { + let encrypted = SignedMessage::encrypt(message, participant, sign_sk, cipher_pk) + .map_err(|err| SendError::EncryptionError(err.to_string()))?; + tracing::debug!(?participant, ciphertext = ?encrypted.text, "sending encrypted"); + let _span = tracing::info_span!("message_request"); let mut url = url.into_url().unwrap(); url.set_path("msg"); @@ -30,7 +43,7 @@ pub async fn message( let response = client .post(url.clone()) .header("content-type", "application/json") - .json(&message) + .json(&encrypted) .send() .await .map_err(SendError::ReqwestClientError)?; @@ -89,3 +102,29 @@ pub async fn join(client: &Client, url: U, me: &Participant) -> Resu let retry_strategy = ExponentialBackoff::from_millis(10).map(jitter).take(3); Retry::spawn(retry_strategy, action).await } + +#[cfg(test)] +mod tests { + use crate::protocol::message::GeneratingMessage; + use crate::protocol::MpcMessage; + + #[test] + fn test_sending_encrypted_message() { + let associated_data = b""; + let (sk, pk) = mpc_keys::hpke::generate(); + let starting_message = MpcMessage::Generating(GeneratingMessage { + from: cait_sith::protocol::Participant::from(0), + data: vec![], + }); + + let message = serde_json::to_vec(&starting_message).unwrap(); + let message = pk.encrypt(&message, associated_data).unwrap(); + + let message = serde_json::to_vec(&message).unwrap(); + let cipher = serde_json::from_slice(&message).unwrap(); + let message = sk.decrypt(&cipher, associated_data).unwrap(); + let message: MpcMessage = serde_json::from_slice(&message).unwrap(); + + assert_eq!(starting_message, message); + } +} diff --git a/node/src/protocol/consensus.rs b/node/src/protocol/consensus.rs index 9729b7132..ae1b5adb9 100644 --- a/node/src/protocol/consensus.rs +++ b/node/src/protocol/consensus.rs @@ -14,6 +14,7 @@ use crate::{http_client, rpc_client}; use async_trait::async_trait; use cait_sith::protocol::{InitializationError, Participant}; use k256::Secp256k1; +use mpc_keys::hpke; use near_crypto::InMemorySigner; use near_primitives::transaction::{Action, FunctionCallAction}; use near_primitives::types::AccountId; @@ -30,6 +31,8 @@ pub trait ConsensusCtx { fn mpc_contract_id(&self) -> &AccountId; fn my_address(&self) -> &Url; fn sign_queue(&self) -> Arc>; + fn cipher_pk(&self) -> &hpke::PublicKey; + fn sign_pk(&self) -> near_crypto::PublicKey; } #[derive(thiserror::Error, Debug)] @@ -101,24 +104,28 @@ impl ConsensusProtocol for StartedState { private_share, public_key, sign_queue: ctx.sign_queue(), - triple_manager: TripleManager::new( + triple_manager: Arc::new(RwLock::new(TripleManager::new( participants_vec.clone(), ctx.me(), contract_state.threshold, epoch, - ), - presignature_manager: PresignatureManager::new( - participants_vec.clone(), - ctx.me(), - contract_state.threshold, - epoch, - ), - signature_manager: SignatureManager::new( - participants_vec, - ctx.me(), - contract_state.public_key, - epoch, - ), + ))), + presignature_manager: Arc::new(RwLock::new( + PresignatureManager::new( + participants_vec.clone(), + ctx.me(), + contract_state.threshold, + epoch, + ), + )), + signature_manager: Arc::new(RwLock::new( + SignatureManager::new( + participants_vec, + ctx.me(), + contract_state.public_key, + epoch, + ), + )), })) } else { Ok(NodeState::Joining(JoiningState { public_key })) @@ -154,7 +161,7 @@ impl ConsensusProtocol for StartedState { if contract_state.participants.contains_key(&ctx.me()) { tracing::info!("starting key generation as a part of the participant set"); let participants = contract_state.participants; - let protocol = cait_sith::keygen( + let protocol = cait_sith::keygen::( &participants.keys().cloned().collect::>(), ctx.me(), contract_state.threshold, @@ -162,7 +169,7 @@ impl ConsensusProtocol for StartedState { Ok(NodeState::Generating(GeneratingState { participants, threshold: contract_state.threshold, - protocol: Box::new(protocol), + protocol: Arc::new(RwLock::new(protocol)), })) } else { tracing::info!("we are not a part of the initial participant set, waiting for key generation to complete"); @@ -289,24 +296,24 @@ impl ConsensusProtocol for WaitingForConsensusState { private_share: self.private_share, public_key: self.public_key, sign_queue: ctx.sign_queue(), - triple_manager: TripleManager::new( + triple_manager: Arc::new(RwLock::new(TripleManager::new( participants_vec.clone(), ctx.me(), self.threshold, self.epoch, - ), - presignature_manager: PresignatureManager::new( + ))), + presignature_manager: Arc::new(RwLock::new(PresignatureManager::new( participants_vec.clone(), ctx.me(), self.threshold, self.epoch, - ), - signature_manager: SignatureManager::new( + ))), + signature_manager: Arc::new(RwLock::new(SignatureManager::new( participants_vec, ctx.me(), self.public_key, self.epoch, - ), + ))), })) } }, @@ -522,11 +529,11 @@ impl ConsensusProtocol for JoiningState { votes_to_go = contract_state.threshold - voted.len(), "trying to get participants to vote for us" ); - for (p, url) in contract_state.participants { + for (p, info) in contract_state.participants { if voted.contains(&p) { continue; } - http_client::join(ctx.http_client(), url, &ctx.me()) + http_client::join(ctx.http_client(), info.url, &ctx.me()) .await .unwrap() } @@ -536,6 +543,8 @@ impl ConsensusProtocol for JoiningState { let args = serde_json::json!({ "participant_id": ctx.me(), "url": ctx.my_address(), + "cipher_pk": ctx.cipher_pk().to_bytes(), + "sign_pk": ctx.sign_pk(), }); ctx.rpc_client() .send_tx( @@ -616,6 +625,6 @@ fn start_resharing( new_participants: contract_state.new_participants, threshold: contract_state.threshold, public_key: contract_state.public_key, - protocol: Box::new(protocol), + protocol: Arc::new(RwLock::new(protocol)), })) } diff --git a/node/src/protocol/contract.rs b/node/src/protocol/contract.rs index 84592902d..0cb284f9a 100644 --- a/node/src/protocol/contract.rs +++ b/node/src/protocol/contract.rs @@ -1,15 +1,41 @@ use crate::types::PublicKey; use crate::util::NearPublicKeyExt; use cait_sith::protocol::Participant; -use mpc_contract::{ParticipantInfo, ProtocolContractState}; +use mpc_contract::ProtocolContractState; +use mpc_keys::hpke; +use near_primitives::borsh::BorshDeserialize; use near_sdk::AccountId; use serde::{Deserialize, Serialize}; use std::collections::{BTreeMap, HashSet}; -use url::Url; + +type ParticipantId = u32; + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] +pub struct ParticipantInfo { + pub id: ParticipantId, + pub account_id: AccountId, + pub url: String, + /// The public key used for encrypting messages. + pub cipher_pk: hpke::PublicKey, + /// The public key used for verifying messages. + pub sign_pk: near_crypto::PublicKey, +} + +impl From for ParticipantInfo { + fn from(value: mpc_contract::ParticipantInfo) -> Self { + ParticipantInfo { + id: value.id, + account_id: value.account_id, + url: value.url, + cipher_pk: hpke::PublicKey::from_bytes(&value.cipher_pk), + sign_pk: BorshDeserialize::try_from_slice(value.sign_pk.as_bytes()).unwrap(), + } + } +} #[derive(Serialize, Deserialize, Debug)] pub struct InitializingContractState { - pub participants: BTreeMap, + pub participants: BTreeMap, pub threshold: usize, pub pk_votes: BTreeMap>, } @@ -41,7 +67,7 @@ impl From for InitializingContractState #[derive(Serialize, Deserialize, Debug)] pub struct RunningContractState { pub epoch: u64, - pub participants: BTreeMap, + pub participants: BTreeMap, pub threshold: usize, pub public_key: PublicKey, pub candidates: BTreeMap, @@ -59,7 +85,7 @@ impl From for RunningContractState { candidates: value .candidates .into_iter() - .map(|(p, p_info)| (Participant::from(p), p_info)) + .map(|(p, p_info)| (Participant::from(p), p_info.into())) .collect(), join_votes: value .join_votes @@ -88,8 +114,8 @@ impl From for RunningContractState { #[derive(Serialize, Deserialize, Debug)] pub struct ResharingContractState { pub old_epoch: u64, - pub old_participants: BTreeMap, - pub new_participants: BTreeMap, + pub old_participants: BTreeMap, + pub new_participants: BTreeMap, pub threshold: usize, pub public_key: PublicKey, pub finished_votes: HashSet, @@ -120,7 +146,7 @@ pub enum ProtocolState { } impl ProtocolState { - pub fn participants(&self) -> &BTreeMap { + pub fn participants(&self) -> &BTreeMap { match self { ProtocolState::Initializing(InitializingContractState { participants, .. }) => { participants @@ -164,15 +190,10 @@ impl TryFrom for ProtocolState { } fn contract_participants_into_cait_participants( - participants: BTreeMap, -) -> BTreeMap { + participants: BTreeMap, +) -> BTreeMap { participants .into_values() - .map(|p| { - ( - Participant::from(p.id), - Url::try_from(p.url.as_str()).unwrap(), - ) - }) + .map(|p| (Participant::from(p.id), p.into())) .collect() } diff --git a/node/src/protocol/cryptography.rs b/node/src/protocol/cryptography.rs index 2d8f929fc..b77735347 100644 --- a/node/src/protocol/cryptography.rs +++ b/node/src/protocol/cryptography.rs @@ -1,3 +1,5 @@ +use std::sync::PoisonError; + use super::state::{GeneratingState, NodeState, ResharingState, RunningState}; use crate::http_client::{self, SendError}; use crate::protocol::message::{GeneratingMessage, ResharingMessage}; @@ -15,6 +17,7 @@ pub trait CryptographicCtx { fn rpc_client(&self) -> &near_fetch::Client; fn signer(&self) -> &InMemorySigner; fn mpc_contract_id(&self) -> &AccountId; + fn sign_sk(&self) -> &near_crypto::SecretKey; } #[derive(thiserror::Error, Debug)] @@ -29,6 +32,21 @@ pub enum CryptographicError { CaitSithInitializationError(#[from] InitializationError), #[error("cait-sith protocol error: {0}")] CaitSithProtocolError(#[from] ProtocolError), + #[error("sync failed: {0}")] + SyncError(String), + #[error(transparent)] + DataConversion(#[from] serde_json::Error), + #[error("encryption failed: {0}")] + Encryption(String), + #[error("more than one writing to state: {0}")] + InvalidStateHandle(String), +} + +impl From> for CryptographicError { + fn from(_: PoisonError) -> Self { + let typename = std::any::type_name::(); + Self::SyncError(format!("PoisonError: {typename}")) + } } #[async_trait] @@ -46,23 +64,28 @@ impl CryptographicProtocol for GeneratingState { ctx: C, ) -> Result { tracing::info!("progressing key generation"); + let mut protocol = self.protocol.write().await; loop { - let action = self.protocol.poke()?; + let action = protocol.poke()?; match action { Action::Wait => { + drop(protocol); tracing::debug!("waiting"); return Ok(NodeState::Generating(self)); } Action::SendMany(m) => { tracing::debug!("sending a message to many participants"); - for (p, url) in &self.participants { + for (p, info) in &self.participants { if p == &ctx.me() { // Skip yourself, cait-sith never sends messages to oneself continue; } - http_client::message( + http_client::send_encrypted( + ctx.me(), + &info.cipher_pk, + ctx.sign_sk(), ctx.http_client(), - url.clone(), + info.url.clone(), MpcMessage::Generating(GeneratingMessage { from: ctx.me(), data: m.clone(), @@ -74,10 +97,13 @@ impl CryptographicProtocol for GeneratingState { Action::SendPrivate(to, m) => { tracing::debug!("sending a private message to {to:?}"); match self.participants.get(&to) { - Some(url) => { - http_client::message( + Some(info) => { + http_client::send_encrypted( + ctx.me(), + &info.cipher_pk, + ctx.sign_sk(), ctx.http_client(), - url.clone(), + info.url.clone(), MpcMessage::Generating(GeneratingMessage { from: ctx.me(), data: m.clone(), @@ -115,23 +141,28 @@ impl CryptographicProtocol for ResharingState { ctx: C, ) -> Result { tracing::info!("progressing key reshare"); + let mut protocol = self.protocol.write().await; loop { - let action = self.protocol.poke()?; + let action = protocol.poke()?; match action { Action::Wait => { + drop(protocol); tracing::debug!("waiting"); return Ok(NodeState::Resharing(self)); } Action::SendMany(m) => { tracing::debug!("sending a message to all participants"); - for (p, url) in &self.new_participants { + for (p, info) in &self.new_participants { if p == &ctx.me() { // Skip yourself, cait-sith never sends messages to oneself continue; } - http_client::message( + http_client::send_encrypted( + ctx.me(), + &info.cipher_pk, + ctx.sign_sk(), ctx.http_client(), - url.clone(), + info.url.clone(), MpcMessage::Resharing(ResharingMessage { epoch: self.old_epoch, from: ctx.me(), @@ -144,10 +175,13 @@ impl CryptographicProtocol for ResharingState { Action::SendPrivate(to, m) => { tracing::debug!("sending a private message to {to:?}"); match self.new_participants.get(&to) { - Some(url) => { - http_client::message( + Some(info) => { + http_client::send_encrypted( + ctx.me(), + &info.cipher_pk, + ctx.sign_sk(), ctx.http_client(), - url.clone(), + info.url.clone(), MpcMessage::Resharing(ResharingMessage { epoch: self.old_epoch, from: ctx.me(), @@ -180,20 +214,34 @@ impl CryptographicProtocol for RunningState { mut self, ctx: C, ) -> Result { - if self.triple_manager.my_len() < 2 { - self.triple_manager.generate()?; + let mut triple_manager = self.triple_manager.write().await; + if triple_manager.my_len() < 2 { + triple_manager.generate()?; } - for (p, msg) in self.triple_manager.poke()? { - let url = self.participants.get(&p).unwrap(); - http_client::message(ctx.http_client(), url.clone(), MpcMessage::Triple(msg)).await?; + for (p, msg) in triple_manager.poke()? { + let info = self + .participants + .get(&p) + .ok_or(CryptographicError::UnknownParticipant(p))?; + + http_client::send_encrypted( + ctx.me(), + &info.cipher_pk, + ctx.sign_sk(), + ctx.http_client(), + info.url.clone(), + MpcMessage::Triple(msg), + ) + .await?; } - if self.presignature_manager.potential_len() < 2 { + let mut presignature_manager = self.presignature_manager.write().await; + if presignature_manager.potential_len() < 2 { // To ensure there is no contention between different nodes we are only using triples // that we proposed. This way in a non-BFT environment we are guaranteed to never try // to use the same triple as any other node. - if let Some((triple0, triple1)) = self.triple_manager.take_mine_twice() { - self.presignature_manager.generate( + if let Some((triple0, triple1)) = triple_manager.take_mine_twice() { + presignature_manager.generate( triple0, triple1, &self.public_key, @@ -203,29 +251,34 @@ impl CryptographicProtocol for RunningState { tracing::debug!("we don't have enough triples to generate a presignature"); } } - for (p, msg) in self.presignature_manager.poke()? { - let url = self.participants.get(&p).unwrap(); - http_client::message( + drop(triple_manager); + for (p, msg) in presignature_manager.poke()? { + let info = self.participants.get(&p).unwrap(); + http_client::send_encrypted( + ctx.me(), + &info.cipher_pk, + ctx.sign_sk(), ctx.http_client(), - url.clone(), + info.url.clone(), MpcMessage::Presignature(msg), ) .await?; } let mut sign_queue = self.sign_queue.write().await; + let mut signature_manager = self.signature_manager.write().await; sign_queue.organize(&self, ctx.me()); let my_requests = sign_queue.my_requests(ctx.me()); - while self.presignature_manager.my_len() > 0 { + while presignature_manager.my_len() > 0 { let Some((receipt_id, _)) = my_requests.iter().next() else { break; }; - let Some(presignature) = self.presignature_manager.take_mine() else { + let Some(presignature) = presignature_manager.take_mine() else { break; }; let receipt_id = *receipt_id; let my_request = my_requests.remove(&receipt_id).unwrap(); - self.signature_manager.generate( + signature_manager.generate( receipt_id, presignature, self.public_key, @@ -233,14 +286,23 @@ impl CryptographicProtocol for RunningState { )?; } drop(sign_queue); - for (p, msg) in self.signature_manager.poke()? { - let url = self.participants.get(&p).unwrap(); - http_client::message(ctx.http_client(), url.clone(), MpcMessage::Signature(msg)) - .await?; + drop(presignature_manager); + for (p, msg) in signature_manager.poke()? { + let info = self.participants.get(&p).unwrap(); + http_client::send_encrypted( + ctx.me(), + &info.cipher_pk, + ctx.sign_sk(), + ctx.http_client(), + info.url.clone(), + MpcMessage::Signature(msg), + ) + .await?; } - self.signature_manager + signature_manager .publish(ctx.rpc_client(), ctx.signer(), ctx.mpc_contract_id()) .await?; + drop(signature_manager); Ok(NodeState::Running(self)) } diff --git a/node/src/protocol/message.rs b/node/src/protocol/message.rs index c5d7d530e..db4ef2e2a 100644 --- a/node/src/protocol/message.rs +++ b/node/src/protocol/message.rs @@ -1,30 +1,36 @@ +use super::cryptography::CryptographicError; use super::presignature::{self, PresignatureId}; use super::state::{GeneratingState, NodeState, ResharingState, RunningState}; use super::triple::TripleId; +use crate::http_client::SendError; use async_trait::async_trait; use cait_sith::protocol::{InitializationError, MessageData, Participant, ProtocolError}; +use mpc_keys::hpke::{self, Ciphered}; +use near_crypto::Signature; use near_primitives::hash::CryptoHash; use serde::{Deserialize, Serialize}; use std::collections::{HashMap, VecDeque}; +use std::sync::Arc; +use tokio::sync::RwLock; pub trait MessageCtx { fn me(&self) -> Participant; } -#[derive(Serialize, Deserialize, Debug)] +#[derive(Serialize, Deserialize, Debug, PartialEq, Eq)] pub struct GeneratingMessage { pub from: Participant, pub data: MessageData, } -#[derive(Serialize, Deserialize, Debug)] +#[derive(Serialize, Deserialize, Debug, PartialEq, Eq)] pub struct ResharingMessage { pub epoch: u64, pub from: Participant, pub data: MessageData, } -#[derive(Serialize, Deserialize, Debug)] +#[derive(Serialize, Deserialize, Debug, PartialEq, Eq)] pub struct TripleMessage { pub id: u64, pub epoch: u64, @@ -32,7 +38,7 @@ pub struct TripleMessage { pub data: MessageData, } -#[derive(Serialize, Deserialize, Debug)] +#[derive(Serialize, Deserialize, Debug, PartialEq, Eq)] pub struct PresignatureMessage { pub id: u64, pub triple0: TripleId, @@ -42,7 +48,7 @@ pub struct PresignatureMessage { pub data: MessageData, } -#[derive(Serialize, Deserialize, Debug)] +#[derive(Serialize, Deserialize, Debug, PartialEq, Eq)] pub struct SignatureMessage { pub receipt_id: CryptoHash, pub proposer: Participant, @@ -53,7 +59,7 @@ pub struct SignatureMessage { pub data: MessageData, } -#[derive(Serialize, Deserialize, Debug)] +#[derive(Serialize, Deserialize, Debug, PartialEq, Eq)] pub enum MpcMessage { Generating(GeneratingMessage), Resharing(ResharingMessage), @@ -111,6 +117,38 @@ pub enum MessageHandleError { CaitSithInitializationError(#[from] InitializationError), #[error("cait-sith protocol error: {0}")] CaitSithProtocolError(#[from] ProtocolError), + #[error("sync failed: {0}")] + SyncError(String), + #[error("failed to send a message: {0}")] + SendError(SendError), + #[error("unknown participant: {0:?}")] + UnknownParticipant(Participant), + #[error(transparent)] + DataConversion(#[from] serde_json::Error), + #[error("encryption failed: {0}")] + Encryption(String), + #[error("invalid state")] + InvalidStateHandle(String), + #[error("rpc error: {0}")] + RpcError(#[from] near_fetch::Error), +} + +impl From for MessageHandleError { + fn from(value: CryptographicError) -> Self { + match value { + CryptographicError::CaitSithInitializationError(e) => { + Self::CaitSithInitializationError(e) + } + CryptographicError::CaitSithProtocolError(e) => Self::CaitSithProtocolError(e), + CryptographicError::SyncError(e) => Self::SyncError(e), + CryptographicError::SendError(e) => Self::SendError(e), + CryptographicError::UnknownParticipant(e) => Self::UnknownParticipant(e), + CryptographicError::DataConversion(e) => Self::DataConversion(e), + CryptographicError::Encryption(e) => Self::Encryption(e), + CryptographicError::InvalidStateHandle(e) => Self::InvalidStateHandle(e), + CryptographicError::RpcError(e) => Self::RpcError(e), + } + } } #[async_trait] @@ -129,9 +167,10 @@ impl MessageHandler for GeneratingState { _ctx: C, queue: &mut MpcMessageQueue, ) -> Result<(), MessageHandleError> { + let mut protocol = self.protocol.write().await; while let Some(msg) = queue.generating.pop_front() { tracing::debug!("handling new generating message"); - self.protocol.message(msg.from, msg.data); + protocol.message(msg.from, msg.data); } Ok(()) } @@ -145,9 +184,10 @@ impl MessageHandler for ResharingState { queue: &mut MpcMessageQueue, ) -> Result<(), MessageHandleError> { let q = queue.resharing_bins.entry(self.old_epoch).or_default(); + let mut protocol = self.protocol.write().await; while let Some(msg) = q.pop_front() { tracing::debug!("handling new resharing message"); - self.protocol.message(msg.from, msg.data); + protocol.message(msg.from, msg.data); } Ok(()) } @@ -160,25 +200,36 @@ impl MessageHandler for RunningState { _ctx: C, queue: &mut MpcMessageQueue, ) -> Result<(), MessageHandleError> { + let mut triple_manager = self.triple_manager.write().await; for (id, queue) in queue.triple_bins.entry(self.epoch).or_default() { - if let Some(protocol) = self.triple_manager.get_or_generate(*id)? { + if let Some(protocol) = triple_manager.get_or_generate(*id)? { + let mut protocol = protocol + .write() + .map_err(|err| MessageHandleError::SyncError(err.to_string()))?; while let Some(message) = queue.pop_front() { protocol.message(message.from, message.data); } } } + + let mut presignature_manager = self.presignature_manager.write().await; for (id, queue) in queue.presignature_bins.entry(self.epoch).or_default() { let mut leftover_messages = Vec::new(); while let Some(message) = queue.pop_front() { - match self.presignature_manager.get_or_generate( + match presignature_manager.get_or_generate( *id, message.triple0, message.triple1, - &mut self.triple_manager, + &mut triple_manager, &self.public_key, &self.private_share, ) { - Ok(protocol) => protocol.message(message.from, message.data), + Ok(protocol) => { + let mut protocol = protocol + .write() + .map_err(|err| MessageHandleError::SyncError(err.to_string()))?; + protocol.message(message.from, message.data) + } Err(presignature::GenerationError::AlreadyGenerated) => { tracing::info!(id, "presignature already generated, nothing left to do") } @@ -199,6 +250,8 @@ impl MessageHandler for RunningState { queue.extend(leftover_messages); } } + + let mut signature_manager = self.signature_manager.write().await; for (receipt_id, queue) in queue.signature_bins.entry(self.epoch).or_default() { let mut leftover_messages = Vec::new(); while let Some(message) = queue.pop_front() { @@ -216,14 +269,19 @@ impl MessageHandler for RunningState { // continue; // }; // TODO: Validate that the message matches our sign_queue - match self.signature_manager.get_or_generate( + match signature_manager.get_or_generate( *receipt_id, message.proposer, message.presignature_id, message.msg_hash, - &mut self.presignature_manager, + &mut presignature_manager, )? { - Some(protocol) => protocol.message(message.from, message.data), + Some(protocol) => { + let mut protocol = protocol + .write() + .map_err(|err| MessageHandleError::SyncError(err.to_string()))?; + protocol.message(message.from, message.data) + } None => { // Store the message until we are ready to process it leftover_messages.push(message) @@ -260,3 +318,66 @@ impl MessageHandler for NodeState { } } } + +/// A signed message that can be encrypted. Note that the message's signature is included +/// in the encrypted message to avoid from it being tampered with without first decrypting. +#[derive(Serialize, Deserialize)] +pub struct SignedMessage { + /// The message with all it's related info. + pub msg: T, + /// The signature used to verify the authenticity of the encrypted message. + pub sig: Signature, + /// From which particpant the message was sent. + pub from: Participant, +} + +impl SignedMessage { + pub const ASSOCIATED_DATA: &'static [u8] = b""; +} + +impl SignedMessage +where + T: Serialize, +{ + pub fn encrypt( + msg: T, + from: Participant, + sign_sk: &near_crypto::SecretKey, + cipher_pk: &hpke::PublicKey, + ) -> Result { + let msg = serde_json::to_vec(&msg)?; + let sig = sign_sk.sign(&msg); + let msg = SignedMessage { msg, sig, from }; + let msg = serde_json::to_vec(&msg)?; + let ciphered = cipher_pk + .encrypt(&msg, SignedMessage::::ASSOCIATED_DATA) + .map_err(|e| CryptographicError::Encryption(e.to_string()))?; + Ok(ciphered) + } +} + +impl SignedMessage +where + T: for<'a> Deserialize<'a>, +{ + pub async fn decrypt( + cipher_sk: &hpke::SecretKey, + protocol_state: &Arc>, + encrypted: Ciphered, + ) -> Result { + let message = cipher_sk + .decrypt(&encrypted, SignedMessage::::ASSOCIATED_DATA) + .map_err(|err| CryptographicError::Encryption(err.to_string()))?; + let SignedMessage::> { msg, sig, from } = serde_json::from_slice(&message)?; + let Some(sender) = protocol_state.read().await.fetch_participant(from) else { + return Err(CryptographicError::UnknownParticipant(from)); + }; + if !sig.verify(&msg, &sender.sign_pk) { + return Err(CryptographicError::Encryption( + "invalid signature while verifying authenticity of encrypted ".to_string(), + )); + } + + Ok(serde_json::from_slice(&msg)?) + } +} diff --git a/node/src/protocol/mod.rs b/node/src/protocol/mod.rs index 2c2511955..8ba7ab0a5 100644 --- a/node/src/protocol/mod.rs +++ b/node/src/protocol/mod.rs @@ -1,13 +1,14 @@ mod consensus; mod contract; mod cryptography; -mod message; mod presignature; mod signature; -mod state; mod triple; -pub use contract::ProtocolState; +pub mod message; +pub mod state; + +pub use contract::{ParticipantInfo, ProtocolState}; pub use message::MpcMessage; pub use signature::SignQueue; pub use signature::SignRequest; @@ -29,6 +30,8 @@ use tokio::sync::mpsc::{self, error::TryRecvError}; use tokio::sync::RwLock; use url::Url; +use mpc_keys::hpke; + struct Ctx { me: Participant, my_address: Url, @@ -37,6 +40,8 @@ struct Ctx { rpc_client: near_fetch::Client, http_client: reqwest::Client, sign_queue: Arc>, + cipher_pk: hpke::PublicKey, + sign_sk: near_crypto::SecretKey, } impl ConsensusCtx for &Ctx { @@ -67,6 +72,14 @@ impl ConsensusCtx for &Ctx { fn sign_queue(&self) -> Arc> { self.sign_queue.clone() } + + fn cipher_pk(&self) -> &hpke::PublicKey { + &self.cipher_pk + } + + fn sign_pk(&self) -> near_crypto::PublicKey { + self.sign_sk.public_key() + } } impl CryptographicCtx for &Ctx { @@ -89,6 +102,10 @@ impl CryptographicCtx for &Ctx { fn mpc_contract_id(&self) -> &AccountId { &self.mpc_contract_id } + + fn sign_sk(&self) -> &near_crypto::SecretKey { + &self.sign_sk + } } impl MessageCtx for &Ctx { @@ -112,16 +129,19 @@ impl MpcSignProtocol { signer: InMemorySigner, receiver: mpsc::Receiver, sign_queue: Arc>, + cipher_pk: hpke::PublicKey, ) -> (Self, Arc>) { let state = Arc::new(RwLock::new(NodeState::Starting)); let ctx = Ctx { me, my_address: my_address.into_url().unwrap(), mpc_contract_id, - signer, rpc_client, http_client: reqwest::Client::new(), sign_queue, + cipher_pk, + sign_sk: signer.secret_key.clone(), + signer, }; let protocol = MpcSignProtocol { ctx, @@ -167,13 +187,19 @@ impl MpcSignProtocol { } } } - let mut state_guard = self.state.write().await; - let mut state = std::mem::take(&mut *state_guard); + + let mut state = { + let guard = self.state.write().await; + guard.clone() + }; state = state.progress(&self.ctx).await?; state = state.advance(&self.ctx, contract_state).await?; state.handle(&self.ctx, &mut queue).await?; - *state_guard = state; - drop(state_guard); + + let mut guard = self.state.write().await; + *guard = state; + drop(guard); + tokio::time::sleep(Duration::from_millis(1000)).await; } } diff --git a/node/src/protocol/presignature.rs b/node/src/protocol/presignature.rs index 6e66ac6c9..5d59af0f8 100644 --- a/node/src/protocol/presignature.rs +++ b/node/src/protocol/presignature.rs @@ -7,6 +7,7 @@ use cait_sith::{KeygenOutput, PresignArguments, PresignOutput}; use k256::Secp256k1; use std::collections::hash_map::Entry; use std::collections::{HashMap, VecDeque}; +use std::sync::Arc; /// Unique number used to identify a specific ongoing presignature generation protocol. /// Without `PresignatureId` it would be unclear where to route incoming cait-sith presignature @@ -98,7 +99,7 @@ impl PresignatureManager { private_share: &PrivateKeyShare, mine: bool, ) -> Result { - let protocol = Box::new(cait_sith::presign( + let protocol = Arc::new(std::sync::RwLock::new(cait_sith::presign( participants, me, PresignArguments { @@ -110,7 +111,7 @@ impl PresignatureManager { }, threshold, }, - )?); + )?)); Ok(PresignatureGenerator { protocol, triple0: triple0.id, @@ -212,7 +213,17 @@ impl PresignatureManager { let mut result = Ok(()); self.generators.retain(|id, generator| { loop { - let action = match generator.protocol.poke() { + let mut protocol = match generator.protocol.write() { + Ok(protocol) => protocol, + Err(err) => { + tracing::error!( + ?err, + "failed to acquire lock on presignature generation protocol" + ); + break false; + } + }; + let action = match protocol.poke() { Ok(action) => action, Err(e) => { result = Err(e); diff --git a/node/src/protocol/signature.rs b/node/src/protocol/signature.rs index f879575c2..3b198b64c 100644 --- a/node/src/protocol/signature.rs +++ b/node/src/protocol/signature.rs @@ -17,6 +17,7 @@ use rand::seq::{IteratorRandom, SliceRandom}; use rand::SeedableRng; use std::collections::hash_map::Entry; use std::collections::HashMap; +use std::sync::Arc; pub struct SignRequest { pub receipt_id: CryptoHash, @@ -130,13 +131,13 @@ impl SignatureManager { presignature: Presignature, msg_hash: [u8; 32], ) -> Result { - let protocol = Box::new(cait_sith::sign( + let protocol = Arc::new(std::sync::RwLock::new(cait_sith::sign( participants, me, public_key, presignature.output, Scalar::from_uint_unchecked(U256::from_le_slice(&msg_hash)), - )?); + )?)); Ok(SignatureGenerator { protocol, proposer, @@ -211,7 +212,17 @@ impl SignatureManager { let mut result = Ok(()); self.generators.retain(|receipt_id, generator| { loop { - let action = match generator.protocol.poke() { + let mut protocol = match generator.protocol.write() { + Ok(protocol) => protocol, + Err(err) => { + tracing::error!( + ?err, + "failed to acquire lock on signature generation protocol" + ); + break false; + } + }; + let action = match protocol.poke() { Ok(action) => action, Err(e) => { result = Err(e); diff --git a/node/src/protocol/state.rs b/node/src/protocol/state.rs index a6275d1d0..66af8d340 100644 --- a/node/src/protocol/state.rs +++ b/node/src/protocol/state.rs @@ -2,61 +2,68 @@ use super::presignature::PresignatureManager; use super::signature::SignatureManager; use super::triple::TripleManager; use super::SignQueue; +use crate::protocol::ParticipantInfo; use crate::types::{KeygenProtocol, PrivateKeyShare, PublicKey, ReshareProtocol}; use cait_sith::protocol::Participant; use std::collections::BTreeMap; use std::sync::Arc; use tokio::sync::RwLock; -use url::Url; +#[derive(Clone)] pub struct PersistentNodeData { pub epoch: u64, pub private_share: PrivateKeyShare, pub public_key: PublicKey, } +#[derive(Clone)] pub struct StartedState(pub Option); +#[derive(Clone)] pub struct GeneratingState { - pub participants: BTreeMap, + pub participants: BTreeMap, pub threshold: usize, pub protocol: KeygenProtocol, } +#[derive(Clone)] pub struct WaitingForConsensusState { pub epoch: u64, - pub participants: BTreeMap, + pub participants: BTreeMap, pub threshold: usize, pub private_share: PrivateKeyShare, pub public_key: PublicKey, } +#[derive(Clone)] pub struct RunningState { pub epoch: u64, - pub participants: BTreeMap, + pub participants: BTreeMap, pub threshold: usize, pub private_share: PrivateKeyShare, pub public_key: PublicKey, pub sign_queue: Arc>, - pub triple_manager: TripleManager, - pub presignature_manager: PresignatureManager, - pub signature_manager: SignatureManager, + pub triple_manager: Arc>, + pub presignature_manager: Arc>, + pub signature_manager: Arc>, } +#[derive(Clone)] pub struct ResharingState { pub old_epoch: u64, - pub old_participants: BTreeMap, - pub new_participants: BTreeMap, + pub old_participants: BTreeMap, + pub new_participants: BTreeMap, pub threshold: usize, pub public_key: PublicKey, pub protocol: ReshareProtocol, } +#[derive(Clone)] pub struct JoiningState { pub public_key: PublicKey, } -#[derive(Default)] +#[derive(Clone, Default)] #[allow(clippy::large_enum_variant)] pub enum NodeState { #[default] @@ -68,3 +75,25 @@ pub enum NodeState { Resharing(ResharingState), Joining(JoiningState), } + +impl NodeState { + pub fn fetch_participant(&self, p: Participant) -> Option { + let participants = match self { + NodeState::Running(state) => &state.participants, + NodeState::Generating(state) => &state.participants, + NodeState::WaitingForConsensus(state) => &state.participants, + NodeState::Resharing(state) => { + if let Some(info) = state.new_participants.get(&p) { + return Some(info.clone()); + } else if let Some(info) = state.old_participants.get(&p) { + return Some(info.clone()); + } else { + return None; + } + } + _ => return None, + }; + + participants.get(&p).cloned() + } +} diff --git a/node/src/protocol/triple.rs b/node/src/protocol/triple.rs index d3b2cce9b..8f34abf5c 100644 --- a/node/src/protocol/triple.rs +++ b/node/src/protocol/triple.rs @@ -1,3 +1,4 @@ +use super::cryptography::CryptographicError; use super::message::TripleMessage; use crate::types::TripleProtocol; use crate::util::AffinePointExt; @@ -6,6 +7,7 @@ use cait_sith::triples::{TriplePub, TripleShare}; use k256::Secp256k1; use std::collections::hash_map::Entry; use std::collections::{HashMap, VecDeque}; +use std::sync::Arc; /// Unique number used to identify a specific ongoing triple generation protocol. /// Without `TripleId` it would be unclear where to route incoming cait-sith triple generation @@ -74,18 +76,20 @@ impl TripleManager { /// Returns the number of unspent triples we will have in the manager once /// all ongoing generation protocols complete. pub fn potential_len(&self) -> usize { - self.triples.len() + self.generators.len() + self.len() + self.generators.len() } /// Starts a new Beaver triple generation protocol. pub fn generate(&mut self) -> Result<(), InitializationError> { let id = rand::random(); tracing::debug!(id, "starting protocol to generate a new triple"); - let protocol: TripleProtocol = Box::new(cait_sith::triples::generate_triple( - &self.participants, - self.me, - self.threshold, - )?); + let protocol: TripleProtocol = Arc::new(std::sync::RwLock::new( + cait_sith::triples::generate_triple::( + &self.participants, + self.me, + self.threshold, + )?, + )); self.generators.insert( id, TripleGenerator { @@ -142,18 +146,20 @@ impl TripleManager { pub fn get_or_generate( &mut self, id: TripleId, - ) -> Result, InitializationError> { + ) -> Result, CryptographicError> { if self.triples.contains_key(&id) { Ok(None) } else { match self.generators.entry(id) { Entry::Vacant(e) => { tracing::debug!(id, "joining protocol to generate a new triple"); - let protocol = Box::new(cait_sith::triples::generate_triple( - &self.participants, - self.me, - self.threshold, - )?); + let protocol = Arc::new(std::sync::RwLock::new( + cait_sith::triples::generate_triple::( + &self.participants, + self.me, + self.threshold, + )?, + )); let generator = e.insert(TripleGenerator { protocol, mine: false, @@ -174,13 +180,25 @@ impl TripleManager { let mut result = Ok(()); self.generators.retain(|id, generator| { loop { - let action = match generator.protocol.poke() { + let mut protocol = match generator.protocol.write() { + Ok(protocol) => protocol, + Err(err) => { + tracing::error!( + ?err, + "failed to acquire lock on triple generation protocol" + ); + break false; + } + }; + + let action = match protocol.poke() { Ok(action) => action, Err(e) => { result = Err(e); break false; } }; + match action { Action::Wait => { tracing::debug!("waiting"); @@ -225,6 +243,7 @@ impl TripleManager { public: output.1, }, ); + if generator.mine { self.mine.push_back(*id); } diff --git a/node/src/types.rs b/node/src/types.rs index a69b56ba5..685120c37 100644 --- a/node/src/types.rs +++ b/node/src/types.rs @@ -1,13 +1,18 @@ +use std::sync::Arc; + use cait_sith::triples::TripleGenerationOutput; use cait_sith::{protocol::Protocol, KeygenOutput}; use cait_sith::{FullSignature, PresignOutput}; use k256::{elliptic_curve::CurveArithmetic, Secp256k1}; +use tokio::sync::RwLock; pub type PrivateKeyShare = ::Scalar; pub type PublicKey = ::AffinePoint; -pub type KeygenProtocol = Box> + Send + Sync>; -pub type ReshareProtocol = Box + Send + Sync>; +pub type KeygenProtocol = Arc> + Send + Sync>>; +pub type ReshareProtocol = Arc + Send + Sync>>; pub type TripleProtocol = - Box> + Send + Sync>; -pub type PresignatureProtocol = Box> + Send + Sync>; -pub type SignatureProtocol = Box> + Send + Sync>; + Arc> + Send + Sync>>; +pub type PresignatureProtocol = + Arc> + Send + Sync>>; +pub type SignatureProtocol = + Arc> + Send + Sync>>; diff --git a/node/src/web/mod.rs b/node/src/web/mod.rs index 1cb31d637..631b14522 100644 --- a/node/src/web/mod.rs +++ b/node/src/web/mod.rs @@ -1,12 +1,14 @@ mod error; use self::error::MpcSignError; +use crate::protocol::message::SignedMessage; use crate::protocol::{MpcMessage, NodeState}; use axum::http::StatusCode; use axum::routing::{get, post}; use axum::{Extension, Json, Router}; use axum_extra::extract::WithRejection; use cait_sith::protocol::Participant; +use mpc_keys::hpke::{self, Ciphered}; use near_crypto::InMemorySigner; use near_primitives::transaction::{Action, FunctionCallAction}; use near_primitives::types::AccountId; @@ -20,6 +22,7 @@ struct AxumState { signer: InMemorySigner, sender: Sender, protocol_state: Arc>, + cipher_sk: hpke::SecretKey, } pub async fn run( @@ -28,6 +31,7 @@ pub async fn run( rpc_client: near_fetch::Client, signer: InMemorySigner, sender: Sender, + cipher_sk: hpke::SecretKey, protocol_state: Arc>, ) -> anyhow::Result<()> { tracing::debug!("running a node"); @@ -37,6 +41,7 @@ pub async fn run( signer, sender, protocol_state, + cipher_sk, }; let app = Router::new() @@ -72,13 +77,22 @@ pub struct MsgRequest { #[tracing::instrument(level = "debug", skip_all)] async fn msg( Extension(state): Extension>, - WithRejection(Json(message), _): WithRejection, MpcSignError>, + WithRejection(Json(encrypted), _): WithRejection, MpcSignError>, ) -> StatusCode { - tracing::debug!(?message, "received"); + tracing::debug!(ciphertext = ?encrypted.text, "received encrypted"); + let message = + match SignedMessage::decrypt(&state.cipher_sk, &state.protocol_state, encrypted).await { + Ok(msg) => msg, + Err(err) => { + tracing::error!(?err, "failed to decrypt or verify an encrypted message"); + return StatusCode::BAD_REQUEST; + } + }; + match state.sender.send(message).await { Ok(()) => StatusCode::OK, Err(e) => { - tracing::error!("failed to send a protocol message: {e}"); + tracing::error!("failed to send an encrypted protocol message: {e}"); StatusCode::INTERNAL_SERVER_ERROR } } @@ -144,13 +158,16 @@ async fn state(Extension(state): Extension>) -> (StatusCode, Json let protocol_state = state.protocol_state.read().await; match &*protocol_state { NodeState::Running(state) => { + let triple_count = state.triple_manager.read().await.len(); + let presignature_count = state.presignature_manager.read().await.len(); + tracing::debug!("not running, state unavailable"); ( StatusCode::OK, Json(StateView::Running { participants: state.participants.keys().cloned().collect(), - triple_count: state.triple_manager.len(), - presignature_count: state.presignature_manager.len(), + triple_count, + presignature_count, }), ) }