Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: secure p2p #354

Merged
merged 24 commits into from
Nov 23, 2023
Merged
Show file tree
Hide file tree
Changes from 17 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
184df97
implement beaver triple generation
itegulov Oct 26, 2023
968c1d8
Initial setup
ChaoticTempest Nov 1, 2023
58beb05
cipher pk/sk added
ChaoticTempest Nov 3, 2023
f82b2c0
Sending private messages works now
ChaoticTempest Nov 3, 2023
bee9805
Rename + associated data
ChaoticTempest Nov 3, 2023
822afbc
Working reshare messaging
ChaoticTempest Nov 13, 2023
4d40f3d
Utilize HPKE serde feature flag
ChaoticTempest Nov 13, 2023
75d0eb8
Clippy fixes
ChaoticTempest Nov 13, 2023
1f9b253
Added tests
ChaoticTempest Nov 13, 2023
a062f78
Encrypt/decrypt now pass on errors
ChaoticTempest Nov 13, 2023
2c5fafb
Verify encrypted message instead
ChaoticTempest Nov 13, 2023
6ef49a9
Merge branch 'develop' of github.com:near/mpc-recovery into phuong/fe…
ChaoticTempest Nov 13, 2023
9e6d1ef
Fixing some unwraps
ChaoticTempest Nov 13, 2023
b4b04be
TripleManager triples are not cloned during clone
ChaoticTempest Nov 13, 2023
3bd5077
Merge branch 'develop' of github.com:near/mpc-recovery into phuong/fe…
ChaoticTempest Nov 13, 2023
4ca4fe6
Fix docker
ChaoticTempest Nov 13, 2023
60b5154
Merge branch 'develop' of github.com:near/mpc-recovery into phuong/fe…
ChaoticTempest Nov 15, 2023
d260b29
Have signature be apart of the encrypted message
ChaoticTempest Nov 16, 2023
255cfce
Merge branch 'develop' of github.com:near/mpc-recovery into phuong/fe…
ChaoticTempest Nov 16, 2023
c1220b0
Remove feature=wasm for key crate
ChaoticTempest Nov 16, 2023
9c195cc
Add more info on info parameter
ChaoticTempest Nov 16, 2023
1e0da10
Made all messaging encrypted
ChaoticTempest Nov 16, 2023
3b5352b
Merge branch 'develop' of github.com:near/mpc-recovery into phuong/fe…
ChaoticTempest Nov 16, 2023
1d2f233
Merge branch 'develop' of github.com:near/mpc-recovery into phuong/fe…
ChaoticTempest Nov 18, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
816 changes: 423 additions & 393 deletions Cargo.lock

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ members = [
"node",
"integration-tests",
"load-tests",
"keys",
"test-oidc-provider",
]

Expand Down
2 changes: 2 additions & 0 deletions Dockerfile.multichain
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
19 changes: 18 additions & 1 deletion contract/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ use std::collections::{HashMap, HashSet};

type ParticipantId = u32;

pub mod hpke {
pub type PublicKey = [u8; 32];
ChaoticTempest marked this conversation as resolved.
Show resolved Hide resolved
}

#[derive(
Serialize,
Deserialize,
Expand All @@ -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.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would rather have longer names here and in other places, like msg_encryption_pk, msg_signature_pk. People and other developers can confuse these with key shares.

pub sign_pk: PublicKey,
}

#[derive(BorshDeserialize, BorshSerialize, Serialize, Deserialize, Debug)]
Expand All @@ -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?
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is kind of like a preparation for #330
Ideally nodes should be identified by the account id they use for interacting with the contract and the participant id should be given to them at runtime by the contract

pub participants: HashMap<AccountId, ParticipantInfo>,
pub threshold: usize,
pub public_key: PublicKey,
Expand Down Expand Up @@ -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,
Expand All @@ -100,6 +115,8 @@ impl MpcContract {
id: participant_id,
account_id,
url,
cipher_pk,
sign_pk,
},
);
}
Expand Down
3 changes: 1 addition & 2 deletions integration-tests/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ futures = "0.3"
hex = "0.4.3"
hyper = { version = "0.14", features = ["full"] }
mpc-contract = { path = "../contract" }
mpc-keys = { path = "../keys" }
mpc-recovery = { path = "../mpc-recovery" }
mpc-recovery-node = { path = "../node" }
multi-party-eddsa = { git = "https://github.com/DavidM-D/multi-party-eddsa.git", rev = "25ae4fdc5ff7819ae70e73ab4afacf1c24fc4da1" }
Expand Down Expand Up @@ -46,8 +47,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 = []
Expand Down
10 changes: 10 additions & 0 deletions integration-tests/src/multichain/containers.rs
Original file line number Diff line number Diff line change
@@ -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::{
Expand All @@ -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 {
Expand All @@ -33,13 +37,16 @@ impl<'a> Node<'a> {
account_sk: &near_workspaces::types::SecretKey,
) -> anyhow::Result<Node<'a>> {
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(),
mpc_contract_id: ctx.mpc_contract.id().clone(),
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_host_address.clone(),
s3_region: ctx.localstack.s3_region.clone(),
Expand Down Expand Up @@ -72,6 +79,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(),
})
}
}
10 changes: 9 additions & 1 deletion integration-tests/src/multichain/local.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
use crate::{mpc, util};
use async_process::Child;
use mpc_keys::hpke;
use near_workspaces::AccountId;

#[allow(dead_code)]
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)]
Expand All @@ -22,13 +25,16 @@ impl Node {
account_sk: &near_workspaces::types::SecretKey,
) -> anyhow::Result<Self> {
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(),
mpc_contract_id: ctx.mpc_contract.id().clone(),
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_host_address.clone(),
s3_region: ctx.localstack.s3_region.clone(),
Expand All @@ -48,6 +54,8 @@ impl Node {
node_id: node_id as usize,
account: account.clone(),
account_sk: account_sk.clone(),
cipher_pk,
cipher_sk,
process,
})
}
Expand Down
4 changes: 4 additions & 0 deletions integration-tests/src/multichain/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,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(),
},
)
})
Expand Down Expand Up @@ -182,6 +184,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(),
},
)
})
Expand Down
16 changes: 16 additions & 0 deletions keys/Cargo.toml
Original file line number Diff line number Diff line change
@@ -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 = "*"
156 changes: 156 additions & 0 deletions keys/src/hpke.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
use borsh::{self, BorshDeserialize, BorshSerialize};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FYI I want Michel to take a look at this to make sure we are not misusing anything here, will let you know how it goes

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sounds good

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@itegulov Are we good to go?

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.
const INFO_ENTROPY: &[u8] = b"session-key-v1";
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, can you give an example of a situation when we would want "bump" this version?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is just extra info for generating the derived key fro encryption. So it's more for contextual info. It's useful when we end up using the same key as encryption and signing, but we don't se it can just empty or static like this. I just arbitrarily made it into a versioning scheme


// 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<Aead>);

#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct PublicKey(<Kem as hpke::Kem>::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(<Kem as hpke::Kem>::PrivateKey);

#[derive(Clone, Serialize, Deserialize)]
pub struct EncappedKey(<Kem as hpke::Kem>::EncappedKey);

// Series of bytes that have been previously encoded/encrypted.
pub type CipherText = Vec<u8>;

impl PublicKey {
pub fn to_bytes(&self) -> [u8; 32] {
hpke::Serializable::to_bytes(&self.0).into()
}

pub fn try_from_bytes(bytes: &[u8]) -> Result<Self, hpke::HpkeError> {
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<Ciphered, hpke::HpkeError> {
let mut csprng = <rand::rngs::StdRng as rand::SeedableRng>::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::<Aead, Kdf, Kem, _>(
&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<W: std::io::Write>(&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<Self> {
Ok(Self::from_bytes(
&<Vec<u8> 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<Self, hpke::HpkeError> {
Ok(Self(hpke::Deserializable::from_bytes(bytes)?))
}

pub fn decrypt(
&self,
cipher: &Ciphered,
associated_data: &[u8],
) -> Result<Vec<u8>, hpke::HpkeError> {
// Decapsulate and derive the shared secret. This creates a shared AEAD context.
let mut receiver_ctx = hpke::setup_receiver::<Aead, Kdf, Kem>(
&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(<Kem as hpke::Kem>::sk_to_pk(&self.0))
}
}

pub fn generate() -> (SecretKey, PublicKey) {
let mut csprng = <rand::rngs::StdRng as rand::SeedableRng>::from_entropy();
let (sk, pk) = <Kem as hpke::Kem>::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);
}
}
24 changes: 24 additions & 0 deletions keys/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
#[cfg(not(feature = "wasm"))]
pub mod hpke;

#[cfg(feature = "wasm")]
pub mod hpke {
use borsh::{self, BorshDeserialize, BorshSerialize};
use serde::{Deserialize, Serialize};

/// HPKE public key interface for wasm contracts
#[derive(
Clone,
Debug,
Hash,
PartialEq,
Eq,
PartialOrd,
Ord,
Serialize,
Deserialize,
BorshSerialize,
BorshDeserialize,
)]
pub struct PublicKey([u8; 32]);
}
Loading
Loading