Skip to content

Commit

Permalink
frost-client: add encryption and authentication
Browse files Browse the repository at this point in the history
  • Loading branch information
conradoplg committed Oct 24, 2024
1 parent 37765ce commit 4829845
Show file tree
Hide file tree
Showing 13 changed files with 414 additions and 40 deletions.
4 changes: 4 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions coordinator/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,11 @@ exitcode = "1.1.2"
clap = { version = "4.5.20", features = ["derive"] }
reqwest = { version = "0.12.8", features = ["json"] }
server = { path = "../server" }
participant = { path = "../participant" }
tokio = { version = "1", features = ["full"] }
message-io = "0.18"
rpassword = "7.3.1"
snow = "0.9.6"

[features]
default = []
17 changes: 16 additions & 1 deletion coordinator/src/args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ use std::{
error::Error,
fs,
io::{BufRead, Write},
rc::Rc,
};

use clap::Parser;
Expand Down Expand Up @@ -87,7 +88,7 @@ pub struct Args {
pub port: u16,
}

#[derive(Clone, Debug)]
#[derive(Clone)]
pub struct ProcessedArgs<C: Ciphersuite> {
/// CLI mode. If enabled, it will prompt for inputs from stdin
/// and print values to stdout, ignoring other flags.
Expand Down Expand Up @@ -136,6 +137,18 @@ pub struct ProcessedArgs<C: Ciphersuite> {
/// Port to bind to, if using socket comms.
/// Port to connect to, if using HTTP mode.
pub port: u16,

/// The coordinator's communication private key. Specifying this along with
/// `comm_participant_pubkey_getter` enables encryption.
pub comm_privkey: Option<Vec<u8>>,

/// A function that returns the public key for a given username, or None
/// if not available.
// It is a `Rc<dyn Fn>` to make it easier to use;
// using `fn()` would preclude using closures and using generics would
// require a lot of code change for something simple.
#[allow(clippy::type_complexity)]
pub comm_participant_pubkey_getter: Option<Rc<dyn Fn(&str) -> Option<Vec<u8>>>>,
}

impl<C: Ciphersuite + 'static> ProcessedArgs<C> {
Expand Down Expand Up @@ -193,6 +206,8 @@ impl<C: Ciphersuite + 'static> ProcessedArgs<C> {
ip: args.ip.clone(),
port: args.port,
authentication_token: None,
comm_privkey: None,
comm_participant_pubkey_getter: None,
})
}
}
Expand Down
129 changes: 114 additions & 15 deletions coordinator/src/comms/http.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,13 @@ use std::{
};

use async_trait::async_trait;
use eyre::eyre;
use eyre::{eyre, OptionExt};
use frost_core::{
keys::PublicKeyPackage, round1::SigningCommitments, round2::SignatureShare, Ciphersuite,
Identifier, SigningPackage,
};
use itertools::Itertools;

use participant::comms::http::Noise;
use server::{Msg, SendCommitmentsArgs, SendSignatureSharesArgs, SendSigningPackageArgs, Uuid};

use super::Comms;
Expand Down Expand Up @@ -268,6 +268,10 @@ pub struct HTTPComms<C: Ciphersuite> {
state: SessionState<C>,
usernames: HashMap<String, Identifier<C>>,
should_logout: bool,
// The "send" Noise objects by username of recipients.
send_noise: Option<HashMap<String, Noise>>,
// The "receive" Noise objects by username of senders.
recv_noise: Option<HashMap<String, Noise>>,
_phantom: PhantomData<C>,
}

Expand All @@ -284,9 +288,51 @@ impl<C: Ciphersuite> HTTPComms<C> {
state: SessionState::new(args.messages.len(), args.num_signers as usize),
usernames: Default::default(),
should_logout: args.authentication_token.is_none(),
send_noise: None,
recv_noise: None,
_phantom: Default::default(),
})
}

// Encrypts a message for a given recipient if encryption is enabled.
fn encrypt_if_needed(
&mut self,
recipient: &str,
msg: Vec<u8>,
) -> Result<Vec<u8>, Box<dyn Error>> {
if let Some(noise_map) = &mut self.send_noise {
let noise = noise_map
.get_mut(recipient)
.ok_or_eyre("unknown recipient")?;
let mut encrypted = vec![0; 65535];
let len = noise.write_message(&msg, &mut encrypted)?;
encrypted.truncate(len);
Ok(encrypted)
} else {
Ok(msg)
}
}

// Decrypts a message if encryption is enabled.
// Note that this authenticates the `sender` in the `Msg` struct; if the
// sender is tampered with, the message would fail to decrypt.
fn decrypt_if_needed(&mut self, msg: Msg) -> Result<Msg, Box<dyn Error>> {
if let Some(noise_map) = &mut self.recv_noise {
let noise = noise_map
.get_mut(&msg.sender)
.ok_or_eyre("unknown sender")?;
let mut decrypted = vec![0; 65535];
decrypted.resize(65535, 0);
let len = noise.read_message(&msg.msg, &mut decrypted)?;
decrypted.truncate(len);
Ok(Msg {
sender: msg.sender,
msg: decrypted,
})
} else {
Ok(msg)
}
}
}

#[async_trait(?Send)]
Expand Down Expand Up @@ -336,6 +382,49 @@ impl<C: Ciphersuite + 'static> Comms<C> for HTTPComms<C> {
}
self.session_id = Some(r.session_id);
self.num_signers = num_signers;

// If encryption is enabled, create the Noise objects
(self.send_noise, self.recv_noise) = if let (
Some(comm_privkey),
Some(comm_participant_pubkey_getter),
) = (
&self.args.comm_privkey,
&self.args.comm_participant_pubkey_getter,
) {
let mut send_noise_map = HashMap::new();
let mut recv_noise_map = HashMap::new();
for username in &self.args.signers {
let comm_participant_pubkey = comm_participant_pubkey_getter(username).ok_or_eyre("A participant in specified FROST session is not registered in the coordinator's address book")?;
let builder = snow::Builder::new(
"Noise_K_25519_ChaChaPoly_BLAKE2s"
.parse()
.expect("should be a valid cipher"),
);
let send_noise = Noise::new(
builder
.local_private_key(comm_privkey)
.remote_public_key(&comm_participant_pubkey)
.build_initiator()?,
);
let builder = snow::Builder::new(
"Noise_K_25519_ChaChaPoly_BLAKE2s"
.parse()
.expect("should be a valid cipher"),
);
let recv_noise = Noise::new(
builder
.local_private_key(comm_privkey)
.remote_public_key(&comm_participant_pubkey)
.build_responder()?,
);
send_noise_map.insert(username.clone(), send_noise);
recv_noise_map.insert(username.clone(), recv_noise);
}
(Some(send_noise_map), Some(recv_noise_map))
} else {
(None, None)
};

eprint!("Waiting for participants to send their commitments...");

loop {
Expand All @@ -352,6 +441,7 @@ impl<C: Ciphersuite + 'static> Comms<C> for HTTPComms<C> {
.json::<server::ReceiveOutput>()
.await?;
for msg in r.msgs {
let msg = self.decrypt_if_needed(msg)?;
self.state.recv(msg)?;
}
tokio::time::sleep(Duration::from_secs(2)).await;
Expand Down Expand Up @@ -382,19 +472,27 @@ impl<C: Ciphersuite + 'static> Comms<C> for HTTPComms<C> {
aux_msg: Default::default(),
randomizer: randomizer.map(|r| vec![r]).unwrap_or_default(),
};
let _r = self
.client
.post(format!("{}/send", self.host_port))
.bearer_auth(&self.access_token)
.json(&server::SendArgs {
session_id: self.session_id.unwrap(),
recipients: self.usernames.keys().cloned().collect_vec(),
msg: serde_json::to_vec(&send_signing_package_args)?,
})
.send()
.await?
.bytes()
.await?;
// We need to send a message separately for each recipient even if the
// message is the same, because they are (possibly) encrypted
// individually for each recipient.
let usernames: Vec<_> = self.usernames.keys().cloned().collect();
for recipient in usernames {
let msg = self
.encrypt_if_needed(&recipient, serde_json::to_vec(&send_signing_package_args)?)?;
let _r = self
.client
.post(format!("{}/send", self.host_port))
.bearer_auth(&self.access_token)
.json(&server::SendArgs {
session_id: self.session_id.unwrap(),
recipients: vec![recipient.clone()],
msg,
})
.send()
.await?
.bytes()
.await?;
}

eprintln!("Waiting for participants to send their SignatureShares...");

Expand All @@ -412,6 +510,7 @@ impl<C: Ciphersuite + 'static> Comms<C> for HTTPComms<C> {
.json::<server::ReceiveOutput>()
.await?;
for msg in r.msgs {
let msg = self.decrypt_if_needed(msg)?;
self.state.recv(msg)?;
}
tokio::time::sleep(Duration::from_secs(2)).await;
Expand Down
1 change: 1 addition & 0 deletions frost-client/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,4 @@ frost-rerandomized = { version = "2.0.0-rc.0", features = ["serde"] }
reddsa = { git = "https://github.com/ZcashFoundation/reddsa.git", rev = "ed49e9ca0699a6450f6d4a9fe62ff168f5ea1ead", features = ["frost"] }
rand = "0.8"
stable-eyre = "0.2"
itertools = "0.13.0"
27 changes: 18 additions & 9 deletions frost-client/src/args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -83,9 +83,18 @@ pub(crate) enum Command {
/// dealer process via the FROST server (TODO: this is not supported yet)
#[arg(short, long)]
config: Vec<String>,
/// The name of each participant.
/// The comma-separated name of each participant.
#[arg(short = 'N', long, value_delimiter = ',')]
names: Vec<String>,
/// The comma-separated username of each participant in the same order
/// as `names`. Note: these won't be checked in the server.
#[arg(short, long, value_delimiter = ',')]
usernames: Vec<String>,
/// The server URL, if desired. Note that this does not connect to the
/// server; it will just associated the server URL with the group in the
/// config file.
#[arg(short, long)]
server_url: Option<String>,
#[arg(short = 'C', long, default_value = "ed25519")]
ciphersuite: String,
/// The threshold (minimum number of signers).
Expand All @@ -107,11 +116,11 @@ pub(crate) enum Command {
/// $HOME/.local/frost/credentials.toml
#[arg(short, long)]
config: Option<String>,
/// The server URL to use. You can use a substring of the URL. It will
/// use the username previously logged in via the `login` subcommand for
/// the given server.
/// The server URL to use. If not specified, it will use the server URL
/// for the specified group, if any. It will use the username previously
/// logged in via the `login` subcommand for the given server.
#[arg(short, long)]
server_url: String,
server_url: Option<String>,
/// The group to use, identified by the group public key (use `groups`
/// to list)
#[arg(short, long)]
Expand Down Expand Up @@ -142,11 +151,11 @@ pub(crate) enum Command {
/// $HOME/.local/frost/credentials.toml
#[arg(short, long)]
config: Option<String>,
/// The server URL to use. You can use a substring of the URL. It will
/// use the username previously logged in via the `login` subcommand for
/// the given server.
/// The server URL to use. If not specified, it will use the server URL
/// for the specified group, if any. It will use the username previously
/// logged in via the `login` subcommand for the given server.
#[arg(short, long)]
server_url: String,
server_url: Option<String>,
/// The group to use, identified by the group public key (use `groups`
/// to list)
#[arg(short, long)]
Expand Down
17 changes: 17 additions & 0 deletions frost-client/src/coordinator.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use std::error::Error;
use std::rc::Rc;

use coordinator::cli::cli_for_processed_args;
use eyre::eyre;
Expand Down Expand Up @@ -57,6 +58,8 @@ pub(crate) async fn run_for_ciphersuite<C: RandomizedCiphersuite + 'static>(
let mut input = Box::new(std::io::stdin().lock());
let mut output = std::io::stdout();

let server_url =
server_url.unwrap_or(group.server_url.clone().ok_or_eyre("server-url required")?);
let server_url_parsed =
Url::parse(&format!("http://{}", server_url)).wrap_err("error parsing server-url")?;

Expand All @@ -65,6 +68,7 @@ pub(crate) async fn run_for_ciphersuite<C: RandomizedCiphersuite + 'static>(
.get(&server_url)
.ok_or_eyre("Not registered in the given server")?;

let group_participants = group.participant.clone();
let pargs = coordinator::args::ProcessedArgs {
cli: false,
http: true,
Expand All @@ -87,6 +91,19 @@ pub(crate) async fn run_for_ciphersuite<C: RandomizedCiphersuite + 'static>(
.clone()
.ok_or_eyre("Not logged in in the given server")?,
),
comm_privkey: Some(
config
.communication_key
.ok_or_eyre("user not initialized")?
.privkey
.clone(),
),
comm_participant_pubkey_getter: Some(Rc::new(move |participant_username| {
group_participants
.values()
.find(|p| p.username == Some(participant_username.to_string()))
.map(|p| p.pubkey.clone())
})),
};

cli_for_processed_args(pargs, &mut input, &mut output).await?;
Expand Down
Loading

0 comments on commit 4829845

Please sign in to comment.