frost-client: add encryption and authentication (#336)
This commit is contained in:
parent
a4011301ce
commit
36772857ca
|
@ -621,6 +621,7 @@ dependencies = [
|
|||
"hex",
|
||||
"itertools",
|
||||
"message-io",
|
||||
"participant",
|
||||
"rand",
|
||||
"reddsa",
|
||||
"reqwest",
|
||||
|
@ -628,6 +629,7 @@ dependencies = [
|
|||
"serde_json",
|
||||
"serdect",
|
||||
"server",
|
||||
"snow",
|
||||
"thiserror 2.0.3",
|
||||
"tokio",
|
||||
]
|
||||
|
@ -811,7 +813,7 @@ checksum = "74ef43543e701c01ad77d3a5922755c6a1d71b22d942cb8042be4994b380caff"
|
|||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.72",
|
||||
"syn 2.0.87",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -1056,6 +1058,7 @@ dependencies = [
|
|||
"frost-ed25519",
|
||||
"frost-rerandomized",
|
||||
"hex",
|
||||
"itertools",
|
||||
"participant",
|
||||
"postcard",
|
||||
"rand",
|
||||
|
@ -2028,6 +2031,7 @@ dependencies = [
|
|||
"serde_json",
|
||||
"serdect",
|
||||
"server",
|
||||
"snow",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
|
|
|
@ -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 = []
|
||||
|
|
|
@ -3,6 +3,7 @@ use std::{
|
|||
error::Error,
|
||||
fs,
|
||||
io::{BufRead, Write},
|
||||
rc::Rc,
|
||||
};
|
||||
|
||||
use clap::Parser;
|
||||
|
@ -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.
|
||||
|
@ -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> {
|
||||
|
@ -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,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
@ -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>,
|
||||
}
|
||||
|
||||
|
@ -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)]
|
||||
|
@ -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 {
|
||||
|
@ -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;
|
||||
|
@ -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...");
|
||||
|
||||
|
@ -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;
|
||||
|
|
|
@ -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"
|
|
@ -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).
|
||||
|
@ -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)]
|
||||
|
@ -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)]
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
use std::error::Error;
|
||||
use std::rc::Rc;
|
||||
|
||||
use coordinator::cli::cli_for_processed_args;
|
||||
use eyre::eyre;
|
||||
|
@ -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")?;
|
||||
|
||||
|
@ -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,
|
||||
|
@ -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?;
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
use std::error::Error;
|
||||
use std::rc::Rc;
|
||||
|
||||
use eyre::eyre;
|
||||
use eyre::Context;
|
||||
|
@ -53,6 +54,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")?;
|
||||
|
||||
|
@ -61,6 +64,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 = participant::args::ProcessedArgs {
|
||||
cli: false,
|
||||
http: true,
|
||||
|
@ -79,6 +83,19 @@ pub(crate) async fn run_for_ciphersuite<C: RandomizedCiphersuite + 'static>(
|
|||
.ok_or_eyre("Not logged in in the given server")?,
|
||||
),
|
||||
session_id: String::new(),
|
||||
comm_privkey: Some(
|
||||
config
|
||||
.communication_key
|
||||
.ok_or_eyre("user not initialized")?
|
||||
.privkey
|
||||
.clone(),
|
||||
),
|
||||
comm_coordinator_pubkey_getter: Some(Rc::new(move |coordinator_username| {
|
||||
group_participants
|
||||
.values()
|
||||
.find(|p| p.username == Some(coordinator_username.to_string()))
|
||||
.map(|p| p.pubkey.clone())
|
||||
})),
|
||||
};
|
||||
|
||||
cli_for_processed_args(pargs, &mut input, &mut output).await?;
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
use std::{collections::BTreeMap, error::Error};
|
||||
|
||||
use eyre::{eyre, OptionExt};
|
||||
use itertools::izip;
|
||||
use rand::thread_rng;
|
||||
|
||||
use frost_core::{keys::KeyPackage, Ciphersuite};
|
||||
|
@ -36,6 +37,8 @@ pub(crate) fn trusted_dealer_for_ciphersuite<C: Ciphersuite + MaybeIntoEvenY + '
|
|||
threshold,
|
||||
num_signers,
|
||||
names,
|
||||
usernames,
|
||||
server_url,
|
||||
} = (*args).clone()
|
||||
else {
|
||||
panic!("invalid Command");
|
||||
|
@ -64,16 +67,28 @@ pub(crate) fn trusted_dealer_for_ciphersuite<C: Ciphersuite + MaybeIntoEvenY + '
|
|||
// First pass over configs; create participants map
|
||||
let mut participants = BTreeMap::new();
|
||||
let mut contacts = Vec::new();
|
||||
for ((identifier, path), name) in shares.keys().zip(config.iter()).zip(names.iter()) {
|
||||
for (idx, (identifier, path, name)) in
|
||||
izip!(shares.keys(), config.iter(), names.iter()).enumerate()
|
||||
{
|
||||
let config = Config::read(Some(path.to_string()))?;
|
||||
let pubkey = config
|
||||
.communication_key
|
||||
.ok_or_eyre("config not initialized")?
|
||||
.pubkey;
|
||||
let username = if server_url.is_some() {
|
||||
Some(
|
||||
usernames
|
||||
.get(idx)
|
||||
.ok_or_eyre("must specify usernames of all users")?
|
||||
.clone(),
|
||||
)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let participant = Participant {
|
||||
identifier: identifier.serialize(),
|
||||
pubkey: pubkey.clone(),
|
||||
username: None,
|
||||
username,
|
||||
};
|
||||
participants.insert(hex::encode(identifier.serialize()), participant);
|
||||
let contact = Contact {
|
||||
|
@ -95,7 +110,7 @@ pub(crate) fn trusted_dealer_for_ciphersuite<C: Ciphersuite + MaybeIntoEvenY + '
|
|||
key_package: postcard::to_allocvec(&key_package)?,
|
||||
public_key_package: postcard::to_allocvec(&public_key_package)?,
|
||||
participant: participants.clone(),
|
||||
server_url: None,
|
||||
server_url: server_url.clone(),
|
||||
};
|
||||
config.group.insert(
|
||||
hex::encode(public_key_package.verifying_key().serialize()?),
|
||||
|
|
|
@ -24,6 +24,7 @@ message-io = "0.18"
|
|||
reqwest = { version = "0.12.8", features = ["json"] }
|
||||
server = { path = "../server" }
|
||||
rpassword = "7.3.1"
|
||||
snow = "0.9.6"
|
||||
|
||||
[features]
|
||||
default = []
|
||||
|
|
|
@ -2,6 +2,7 @@ use std::{
|
|||
env,
|
||||
error::Error,
|
||||
io::{BufRead, Write},
|
||||
rc::Rc,
|
||||
};
|
||||
|
||||
use clap::Parser;
|
||||
|
@ -58,7 +59,7 @@ pub struct Args {
|
|||
pub session_id: String,
|
||||
}
|
||||
|
||||
#[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.
|
||||
|
@ -92,6 +93,18 @@ pub struct ProcessedArgs<C: Ciphersuite> {
|
|||
|
||||
/// Optional Session ID
|
||||
pub session_id: String,
|
||||
|
||||
/// The participant's communication private key. Specifying this along with
|
||||
/// `comm_coordinator_pubkey_getter` enables encryption.
|
||||
pub comm_privkey: Option<Vec<u8>>,
|
||||
|
||||
/// A function that returns the public key for the given username of the
|
||||
/// coordinator, 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_coordinator_pubkey_getter: Option<Rc<dyn Fn(&str) -> Option<Vec<u8>>>>,
|
||||
}
|
||||
|
||||
impl<C: Ciphersuite + 'static> ProcessedArgs<C> {
|
||||
|
@ -128,6 +141,8 @@ impl<C: Ciphersuite + 'static> ProcessedArgs<C> {
|
|||
port: args.port,
|
||||
authentication_token: None,
|
||||
session_id: args.session_id.clone(),
|
||||
comm_privkey: None,
|
||||
comm_coordinator_pubkey_getter: None,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,22 +8,105 @@ use std::{
|
|||
};
|
||||
|
||||
use async_trait::async_trait;
|
||||
use eyre::eyre;
|
||||
use eyre::{eyre, OptionExt};
|
||||
use frost_core::{
|
||||
self as frost, round1::SigningCommitments, round2::SignatureShare, Ciphersuite, Identifier,
|
||||
};
|
||||
use snow::{HandshakeState, TransportState};
|
||||
|
||||
use super::Comms;
|
||||
use crate::args::ProcessedArgs;
|
||||
|
||||
/// A Noise state.
|
||||
///
|
||||
/// This abstracts away some awkwardness in the `snow` crate API, which
|
||||
/// requires explicitly marking the handshake as finished and switching
|
||||
/// to a new state object after the first message is sent.
|
||||
pub struct Noise {
|
||||
// These should ideally be a enum, but that makes the implementation much
|
||||
// more awkward so I went with easier option which is using two Options.
|
||||
// Only one of them must has a value at any given time.
|
||||
/// The handshake state; None after handshake is complete.
|
||||
handshake_state: Option<HandshakeState>,
|
||||
/// The transport state; None before handshake is complete.
|
||||
transport_state: Option<TransportState>,
|
||||
}
|
||||
|
||||
impl Noise {
|
||||
/// Create a new Noise state from a HandshakeState created with the `snow`
|
||||
/// crate.
|
||||
pub fn new(handshake_state: HandshakeState) -> Self {
|
||||
Self {
|
||||
handshake_state: Some(handshake_state),
|
||||
transport_state: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Write (i.e. encrypts) a message following the same API as `snow`'s
|
||||
/// [`HandshakeState::write_message()`] and
|
||||
/// [`TransportState::write_message()`].
|
||||
pub fn write_message(
|
||||
&mut self,
|
||||
payload: &[u8],
|
||||
message: &mut [u8],
|
||||
) -> Result<usize, snow::Error> {
|
||||
if let Some(handshake_state) = &mut self.handshake_state {
|
||||
// This does the handshake and also writes a first message.
|
||||
let r = handshake_state.write_message(payload, message);
|
||||
// This `if`` should always be true, we do the check regardless for safety.
|
||||
if handshake_state.is_handshake_finished() {
|
||||
// Get the transport state from the handshake state and update
|
||||
// the struct accordingly.
|
||||
let handshake_state = self
|
||||
.handshake_state
|
||||
.take()
|
||||
.expect("there must be a handshake state set");
|
||||
self.transport_state = Some(handshake_state.into_transport_mode()?);
|
||||
}
|
||||
r
|
||||
} else if let Some(transport_state) = &mut self.transport_state {
|
||||
transport_state.write_message(payload, message)
|
||||
} else {
|
||||
panic!("invalid state");
|
||||
}
|
||||
}
|
||||
|
||||
/// Reads (i.e. decrypts) a message following the same API as `snow`'s
|
||||
/// [`HandshakeState::read_message()`] and
|
||||
/// [`TransportState::read_message()`].
|
||||
pub fn read_message(
|
||||
&mut self,
|
||||
payload: &[u8],
|
||||
message: &mut [u8],
|
||||
) -> Result<usize, snow::Error> {
|
||||
// See comments in [`Self::write_message()`].
|
||||
if let Some(handshake_state) = &mut self.handshake_state {
|
||||
let r = handshake_state.read_message(payload, message);
|
||||
if handshake_state.is_handshake_finished() {
|
||||
let handshake_state = self
|
||||
.handshake_state
|
||||
.take()
|
||||
.expect("there must be a handshake state set");
|
||||
self.transport_state = Some(handshake_state.into_transport_mode()?);
|
||||
}
|
||||
r
|
||||
} else if let Some(transport_state) = &mut self.transport_state {
|
||||
transport_state.read_message(payload, message)
|
||||
} else {
|
||||
panic!("invalid state");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct HTTPComms<C: Ciphersuite> {
|
||||
client: reqwest::Client,
|
||||
host_port: String,
|
||||
session_id: Option<Uuid>,
|
||||
username: String,
|
||||
password: String,
|
||||
access_token: String,
|
||||
should_logout: bool,
|
||||
args: ProcessedArgs<C>,
|
||||
send_noise: Option<Noise>,
|
||||
recv_noise: Option<Noise>,
|
||||
_phantom: PhantomData<C>,
|
||||
}
|
||||
|
||||
|
@ -40,13 +123,39 @@ where
|
|||
client,
|
||||
host_port: format!("http://{}:{}", args.ip, args.port),
|
||||
session_id: Uuid::parse_str(&args.session_id).ok(),
|
||||
username: args.username.clone(),
|
||||
password: args.password.clone(),
|
||||
access_token: args.authentication_token.clone().unwrap_or_default(),
|
||||
should_logout: args.authentication_token.is_none(),
|
||||
args: args.clone(),
|
||||
send_noise: None,
|
||||
recv_noise: None,
|
||||
_phantom: Default::default(),
|
||||
})
|
||||
}
|
||||
|
||||
// Encrypts a message for the coordinator if encryption is enabled.
|
||||
fn encrypt_if_needed(&mut self, msg: Vec<u8>) -> Result<Vec<u8>, Box<dyn Error>> {
|
||||
if let Some(noise) = &mut self.send_noise {
|
||||
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 from the coordinator if encryption is enabled.
|
||||
fn decrypt_if_needed(&mut self, msg: Vec<u8>) -> Result<Vec<u8>, Box<dyn Error>> {
|
||||
if let Some(noise) = &mut self.recv_noise {
|
||||
let mut decrypted = vec![0; 65535];
|
||||
decrypted.resize(65535, 0);
|
||||
let len = noise.read_message(&msg, &mut decrypted)?;
|
||||
decrypted.truncate(len);
|
||||
Ok(decrypted)
|
||||
} else {
|
||||
Ok(msg)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait(?Send)]
|
||||
|
@ -73,8 +182,8 @@ where
|
|||
.client
|
||||
.post(format!("{}/login", self.host_port))
|
||||
.json(&server::LoginArgs {
|
||||
username: self.username.clone(),
|
||||
password: self.password.clone(),
|
||||
username: self.args.username.clone(),
|
||||
password: self.args.password.clone(),
|
||||
})
|
||||
.send()
|
||||
.await?
|
||||
|
@ -106,11 +215,60 @@ where
|
|||
};
|
||||
self.session_id = Some(session_id);
|
||||
|
||||
// If encryption is enabled, create the Noise objects
|
||||
(self.send_noise, self.recv_noise) = if let (
|
||||
Some(comm_privkey),
|
||||
Some(comm_coordinator_pubkey_getter),
|
||||
) = (
|
||||
&self.args.comm_privkey,
|
||||
&self.args.comm_coordinator_pubkey_getter,
|
||||
) {
|
||||
// We need to know what is the username of the coordinator in order
|
||||
// to encrypt message to them.
|
||||
let session_info = self
|
||||
.client
|
||||
.post(format!("{}/get_session_info", self.host_port))
|
||||
.json(&server::GetSessionInfoArgs { session_id })
|
||||
.bearer_auth(&self.access_token)
|
||||
.send()
|
||||
.await?
|
||||
.json::<server::GetSessionInfoOutput>()
|
||||
.await?;
|
||||
|
||||
let comm_coordinator_pubkey = comm_coordinator_pubkey_getter(&session_info.coordinator).ok_or_eyre("The coordinator for the specified FROST session is not registered in the user'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_coordinator_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_coordinator_pubkey)
|
||||
.build_responder()?,
|
||||
);
|
||||
(Some(send_noise), Some(recv_noise))
|
||||
} else {
|
||||
(None, None)
|
||||
};
|
||||
|
||||
// Send Commitments to Server
|
||||
let send_commitments_args = SendCommitmentsArgs {
|
||||
identifier,
|
||||
commitments: vec![commitments],
|
||||
};
|
||||
let msg = self.encrypt_if_needed(serde_json::to_vec(&send_commitments_args)?)?;
|
||||
self.client
|
||||
.post(format!("{}/send", self.host_port))
|
||||
.bearer_auth(&self.access_token)
|
||||
|
@ -118,7 +276,7 @@ where
|
|||
session_id,
|
||||
// Empty recipients: Coordinator
|
||||
recipients: vec![],
|
||||
msg: serde_json::to_vec(&send_commitments_args)?,
|
||||
msg,
|
||||
})
|
||||
.send()
|
||||
.await?;
|
||||
|
@ -145,8 +303,9 @@ where
|
|||
eprint!(".");
|
||||
} else {
|
||||
eprintln!("\nSigning package received");
|
||||
eprintln!("\n{}", String::from_utf8(r.msgs[0].msg.clone()).unwrap());
|
||||
break serde_json::from_slice(&r.msgs[0].msg)?;
|
||||
let msg = self.decrypt_if_needed(r.msgs[0].msg.clone())?;
|
||||
eprintln!("\n{}", String::from_utf8_lossy(&msg.clone()));
|
||||
break serde_json::from_slice(&msg)?;
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -180,6 +339,8 @@ where
|
|||
signature_share: vec![signature_share],
|
||||
};
|
||||
|
||||
let msg = self.encrypt_if_needed(serde_json::to_vec(&send_signature_shares_args)?)?;
|
||||
|
||||
let _r = self
|
||||
.client
|
||||
.post(format!("{}/send", self.host_port))
|
||||
|
@ -188,7 +349,7 @@ where
|
|||
session_id: self.session_id.unwrap(),
|
||||
// Empty recipients: Coordinator
|
||||
recipients: vec![],
|
||||
msg: serde_json::to_vec(&send_signature_shares_args)?,
|
||||
msg,
|
||||
})
|
||||
.send()
|
||||
.await?;
|
||||
|
|
|
@ -464,6 +464,8 @@ fn test_snow() -> Result<(), Box<dyn Error>> {
|
|||
.build_initiator()
|
||||
.unwrap();
|
||||
|
||||
println!("{}", anoise.is_handshake_finished());
|
||||
|
||||
let mut encrypted = [0u8; 65535];
|
||||
let len = anoise
|
||||
.write_message("hello world".as_bytes(), &mut encrypted)
|
||||
|
@ -481,6 +483,22 @@ fn test_snow() -> Result<(), Box<dyn Error>> {
|
|||
let len = bnoise.read_message(encrypted, &mut decrypted).unwrap();
|
||||
let decrypted = &decrypted[0..len];
|
||||
|
||||
let mut anoise = anoise.into_transport_mode()?;
|
||||
let mut bnoise = bnoise.into_transport_mode()?;
|
||||
|
||||
println!("{}", str::from_utf8(decrypted).unwrap());
|
||||
|
||||
let mut encrypted = [0u8; 65535];
|
||||
let len = anoise
|
||||
.write_message("hello world".as_bytes(), &mut encrypted)
|
||||
.unwrap();
|
||||
let encrypted = &encrypted[0..len];
|
||||
|
||||
let mut decrypted = [0u8; 65535];
|
||||
let len = bnoise.read_message(encrypted, &mut decrypted).unwrap();
|
||||
let decrypted = &decrypted[0..len];
|
||||
|
||||
println!("{}", str::from_utf8(decrypted).unwrap());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue