From 3c547e49d2e7170f8de66cf97b356008dd04b1cb Mon Sep 17 00:00:00 2001 From: Conrado Gouvea Date: Wed, 20 Nov 2024 21:11:49 -0300 Subject: [PATCH 1/2] server: support private key authentication --- Cargo.lock | 136 ++++++++++++++++++++--- coordinator/Cargo.toml | 1 + coordinator/src/args.rs | 26 +++-- coordinator/src/comms/http.rs | 115 +++++++++++-------- frost-client/src/args.rs | 2 +- frost-client/src/coordinator.rs | 41 ++++--- frost-client/src/participant.rs | 33 +++--- participant/Cargo.toml | 1 + participant/src/args.rs | 10 +- participant/src/comms/http.rs | 59 +++++++--- server/Cargo.toml | 2 + server/src/functions.rs | 177 ++++++++++++++++++------------ server/src/lib.rs | 4 +- server/src/state.rs | 30 +++-- server/src/types.rs | 48 +++++++- server/src/user.rs | 27 +++-- server/tests/integration_tests.rs | 118 ++++++++++++-------- 17 files changed, 567 insertions(+), 263 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a4c5b23e..be8e2e25 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -632,6 +632,7 @@ dependencies = [ "snow", "thiserror 2.0.3", "tokio", + "xeddsa", ] [[package]] @@ -733,6 +734,7 @@ dependencies = [ "cfg-if", "cpufeatures", "curve25519-dalek-derive", + "digest", "fiat-crypto", "rand_core", "rustc_version", @@ -763,6 +765,17 @@ version = "0.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f400d0750c0c069e8493f2256cb4da6f604b6d2eeb69a0ca8863acde352f8400" +[[package]] +name = "delay_map" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df941644b671f05f59433e481ba0d31ac10e3667de725236a4c0d587c496fba1" +dependencies = [ + "futures", + "tokio", + "tokio-util", +] + [[package]] name = "der" version = "0.7.9" @@ -805,6 +818,17 @@ dependencies = [ "syn 2.0.87", ] +[[package]] +name = "derive_more" +version = "0.99.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f33878137e4dafd7fa914ad4e259e18a4e8e532b9617a2d0150262bf53abfce" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.87", +] + [[package]] name = "diff" version = "0.1.13" @@ -886,6 +910,30 @@ version = "0.15.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" +[[package]] +name = "ed25519" +version = "2.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" +dependencies = [ + "pkcs8", + "signature", +] + +[[package]] +name = "ed25519-dalek" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a3daa8e81a3963a60642bcc1f90a670680bd4a77535faa384e9d1c79d620871" +dependencies = [ + "curve25519-dalek", + "ed25519", + "serde", + "sha2", + "subtle", + "zeroize", +] + [[package]] name = "either" version = "1.13.0" @@ -1122,11 +1170,26 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" +[[package]] +name = "futures" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + [[package]] name = "futures-channel" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" dependencies = [ "futures-core", "futures-sink", @@ -1134,15 +1197,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" [[package]] name = "futures-executor" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a576fc72ae164fca6b9db127eaa9a9dda0d61316034f33a0a0d4eda41f02b01d" +checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" dependencies = [ "futures-core", "futures-task", @@ -1162,30 +1225,43 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" + +[[package]] +name = "futures-macro" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.87", +] [[package]] name = "futures-sink" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" [[package]] name = "futures-task" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" [[package]] name = "futures-util" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" dependencies = [ + "futures-channel", "futures-core", "futures-io", + "futures-macro", "futures-sink", "futures-task", "memchr", @@ -2022,6 +2098,7 @@ dependencies = [ "server", "snow", "tokio", + "xeddsa", ] [[package]] @@ -2687,6 +2764,7 @@ dependencies = [ "axum-test", "clap", "coordinator", + "delay_map", "derivative", "eyre", "frost-core", @@ -2707,6 +2785,7 @@ dependencies = [ "tracing", "tracing-subscriber", "uuid", + "xeddsa", ] [[package]] @@ -3384,6 +3463,7 @@ dependencies = [ "futures-core", "futures-sink", "pin-project-lite", + "slab", "tokio", ] @@ -4036,6 +4116,34 @@ dependencies = [ "tap", ] +[[package]] +name = "x25519-dalek" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7e468321c81fb07fa7f4c636c3972b9100f0346e5b6a9f2bd0603a52f7ed277" +dependencies = [ + "curve25519-dalek", + "rand_core", + "serde", + "zeroize", +] + +[[package]] +name = "xeddsa" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2460c9a9c9d1331ff6801e87badb517faa6b6758e5fb585eb27daf7622c6d5ad" +dependencies = [ + "curve25519-dalek", + "derive_more", + "ed25519", + "ed25519-dalek", + "rand", + "sha2", + "x25519-dalek", + "zeroize", +] + [[package]] name = "yansi" version = "0.5.1" diff --git a/coordinator/Cargo.toml b/coordinator/Cargo.toml index 3b206573..a3e31e50 100644 --- a/coordinator/Cargo.toml +++ b/coordinator/Cargo.toml @@ -28,6 +28,7 @@ tokio = { version = "1", features = ["full"] } message-io = "0.18" rpassword = "7.3.1" snow = "0.9.6" +xeddsa = "1.0.2" [features] default = [] diff --git a/coordinator/src/args.rs b/coordinator/src/args.rs index 8fe494b5..8dbb329c 100644 --- a/coordinator/src/args.rs +++ b/coordinator/src/args.rs @@ -109,10 +109,10 @@ pub struct ProcessedArgs { /// it will login with `password` pub authentication_token: Option, - /// The comma-separated usernames of the signers to use in HTTP mode. - /// If HTTP mode is enabled and this is empty, then the session ID - /// will be printed and will have to be shared manually. - pub signers: Vec, + /// The comma-separated keys of the signers to use in + /// HTTP mode. If HTTP mode is enabled and this is empty, then the session + /// ID will be printed and will have to be shared manually. + pub signers: Vec>, /// The number of participants. pub num_signers: u16, @@ -142,13 +142,16 @@ pub struct ProcessedArgs { /// `comm_participant_pubkey_getter` enables encryption. pub comm_privkey: Option>, - /// A function that returns the public key for a given username, or None - /// if not available. + /// The coordinator's communication public key. + pub comm_pubkey: Option>, + + /// A function that confirms if the public key of a participant is in the + /// user's contact book, returning the same public key, or None if not. // It is a `Rc` 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 Option>>>, + pub comm_participant_pubkey_getter: Option) -> Option>>>, } impl ProcessedArgs { @@ -185,6 +188,12 @@ impl ProcessedArgs { &args.public_key_package, )?; + let signers = args + .signers + .iter() + .map(|s| Ok(hex::decode(s)?.to_vec())) + .collect::>>()?; + let public_key_package: PublicKeyPackage = serde_json::from_str(&out)?; let messages = read_messages(&args.message, output, input)?; @@ -197,7 +206,7 @@ impl ProcessedArgs { http: args.http, username: args.username.clone(), password, - signers: args.signers.clone(), + signers, num_signers, public_key_package, messages, @@ -207,6 +216,7 @@ impl ProcessedArgs { port: args.port, authentication_token: None, comm_privkey: None, + comm_pubkey: None, comm_participant_pubkey_getter: None, }) } diff --git a/coordinator/src/comms/http.rs b/coordinator/src/comms/http.rs index c68d861e..c1080144 100644 --- a/coordinator/src/comms/http.rs +++ b/coordinator/src/comms/http.rs @@ -17,7 +17,9 @@ use frost_core::{ }; use participant::comms::http::Noise; +use rand::thread_rng; use server::{Msg, SendCommitmentsArgs, SendSignatureSharesArgs, SendSigningPackageArgs, Uuid}; +use xeddsa::{xed25519, Sign as _}; use super::Comms; use crate::args::ProcessedArgs; @@ -42,7 +44,7 @@ pub enum SessionState { /// Commitments sent by participants so far, for each message being /// signed. commitments: HashMap, Vec>>, - usernames: HashMap>, + pubkeys: HashMap, Identifier>, }, /// Commitments have been sent by all participants. Coordinator can create /// SigningPackage and send to participants. Waiting for participants to @@ -52,8 +54,8 @@ pub enum SessionState { args: SessionStateArgs, /// All commitments sent by participants, for each message being signed. commitments: HashMap, Vec>>, - /// Username -> Identifier mapping. - usernames: HashMap>, + /// Pubkey -> Identifier mapping. + pubkeys: HashMap, Identifier>, /// Signature shares sent by participants so far, for each message being /// signed. signature_shares: HashMap, Vec>>, @@ -78,7 +80,7 @@ impl SessionState { Self::WaitingForCommitments { args, commitments: Default::default(), - usernames: Default::default(), + pubkeys: Default::default(), } } @@ -108,13 +110,13 @@ impl SessionState { /// Handle commitments sent by a participant. fn handle_commitments( &mut self, - username: String, + pubkey: Vec, send_commitments_args: SendCommitmentsArgs, ) -> Result<(), Box> { if let SessionState::WaitingForCommitments { args, commitments, - usernames, + pubkeys: usernames, } = self { if send_commitments_args.commitments.len() != args.num_messages { @@ -129,14 +131,14 @@ impl SessionState { send_commitments_args.identifier, send_commitments_args.commitments, ); - usernames.insert(username, send_commitments_args.identifier); + usernames.insert(pubkey, send_commitments_args.identifier); // If complete, advance to next state if commitments.len() == args.num_signers { *self = SessionState::WaitingForSignatureShares { args: args.clone(), commitments: commitments.clone(), - usernames: usernames.clone(), + pubkeys: usernames.clone(), signature_shares: Default::default(), } } @@ -162,14 +164,14 @@ impl SessionState { ) -> Result< ( Vec, SigningCommitments>>, - HashMap>, + HashMap, Identifier>, ), Box, > { if let SessionState::WaitingForSignatureShares { args, commitments, - usernames, + pubkeys, .. } = self { @@ -180,7 +182,7 @@ impl SessionState { .num_messages) .map(|i| commitments.iter().map(|(id, c)| (*id, c[i])).collect()) .collect(); - Ok((commitments, usernames.clone())) + Ok((commitments, pubkeys.clone())) } else { panic!("wrong state"); } @@ -195,7 +197,7 @@ impl SessionState { /// Handle signature share sent by a participant. fn handle_signature_share( &mut self, - _username: String, + _username: Vec, send_signature_shares_args: SendSignatureSharesArgs, ) -> Result<(), Box> { if let SessionState::WaitingForSignatureShares { @@ -266,12 +268,12 @@ pub struct HTTPComms { num_signers: u16, args: ProcessedArgs, state: SessionState, - usernames: HashMap>, + pubkeys: HashMap, Identifier>, should_logout: bool, - // The "send" Noise objects by username of recipients. - send_noise: Option>, - // The "receive" Noise objects by username of senders. - recv_noise: Option>, + // The "send" Noise objects by pubkey of recipients. + send_noise: Option, Noise>>, + // The "receive" Noise objects by pubkey of senders. + recv_noise: Option, Noise>>, _phantom: PhantomData, } @@ -286,7 +288,7 @@ impl HTTPComms { num_signers: 0, args: args.clone(), state: SessionState::new(args.messages.len(), args.num_signers as usize), - usernames: Default::default(), + pubkeys: Default::default(), should_logout: args.authentication_token.is_none(), send_noise: None, recv_noise: None, @@ -297,7 +299,7 @@ impl HTTPComms { // Encrypts a message for a given recipient if encryption is enabled. fn encrypt_if_needed( &mut self, - recipient: &str, + recipient: &Vec, msg: Vec, ) -> Result, Box> { if let Some(noise_map) = &mut self.send_noise { @@ -344,28 +346,53 @@ impl Comms for HTTPComms { _pub_key_package: &PublicKeyPackage, num_signers: u16, ) -> Result, SigningCommitments>, Box> { - if self.access_token.is_empty() { - self.access_token = self - .client - .post(format!("{}/login", self.host_port)) - .json(&server::LoginArgs { - username: self.args.username.clone(), - password: self.args.password.clone(), - }) - .send() - .await? - .json::() - .await? - .access_token - .to_string(); - } + let mut rng = thread_rng(); + let challenge = self + .client + .post(format!("{}/challenge", self.host_port)) + .json(&server::ChallengeArgs {}) + .send() + .await? + .json::() + .await? + .challenge; + + let privkey = xed25519::PrivateKey::from( + &TryInto::<[u8; 32]>::try_into( + self.args + .comm_privkey + .clone() + .ok_or_eyre("comm_privkey must be specified")?, + ) + .map_err(|_| eyre!("invalid comm_privkey"))?, + ); + let signature: [u8; 64] = privkey.sign(challenge.as_bytes(), &mut rng); + + self.access_token = self + .client + .post(format!("{}/key_login", self.host_port)) + .json(&server::KeyLoginArgs { + uuid: challenge, + pubkey: self + .args + .comm_pubkey + .clone() + .ok_or_eyre("comm_pubkey must be specified")?, + signature: signature.to_vec(), + }) + .send() + .await? + .json::() + .await? + .access_token + .to_string(); let r = self .client .post(format!("{}/create_new_session", self.host_port)) .bearer_auth(&self.access_token) .json(&server::CreateNewSessionArgs { - usernames: self.args.signers.clone(), + pubkeys: self.args.signers.clone(), num_signers, message_count: 1, }) @@ -393,8 +420,8 @@ impl Comms for HTTPComms { ) { 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")?; + for pubkey in &self.args.signers { + let comm_participant_pubkey = comm_participant_pubkey_getter(pubkey).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() @@ -417,8 +444,8 @@ impl Comms for HTTPComms { .remote_public_key(&comm_participant_pubkey) .build_responder()?, ); - send_noise_map.insert(username.clone(), send_noise); - recv_noise_map.insert(username.clone(), recv_noise); + send_noise_map.insert(pubkey.clone(), send_noise); + recv_noise_map.insert(pubkey.clone(), recv_noise); } (Some(send_noise_map), Some(recv_noise_map)) } else { @@ -452,8 +479,8 @@ impl Comms for HTTPComms { } eprintln!(); - let (commitments, usernames) = self.state.commitments()?; - self.usernames = usernames; + let (commitments, pubkeys) = self.state.commitments()?; + self.pubkeys = pubkeys; // TODO: support more than 1 Ok(commitments[0].clone()) @@ -475,8 +502,8 @@ impl Comms for HTTPComms { // 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 pubkeys: Vec<_> = self.pubkeys.keys().cloned().collect(); + for recipient in pubkeys { let msg = self .encrypt_if_needed(&recipient, serde_json::to_vec(&send_signing_package_args)?)?; let _r = self @@ -485,7 +512,7 @@ impl Comms for HTTPComms { .bearer_auth(&self.access_token) .json(&server::SendArgs { session_id: self.session_id.unwrap(), - recipients: vec![recipient.clone()], + recipients: vec![server::PublicKey(recipient.clone())], msg, }) .send() diff --git a/frost-client/src/args.rs b/frost-client/src/args.rs index d3a0c4ee..d7ed1217 100644 --- a/frost-client/src/args.rs +++ b/frost-client/src/args.rs @@ -125,7 +125,7 @@ pub(crate) enum Command { /// to list) #[arg(short, long)] group: String, - /// The comma-separated usernames of the signers to use. + /// The comma-separated hex-encoded public keys of the signers to use. #[arg(short = 'S', long, value_delimiter = ',')] signers: Vec, /// The messages to sign. Each instance can be a file with the raw message, diff --git a/frost-client/src/coordinator.rs b/frost-client/src/coordinator.rs index 6e27cdce..61b764e8 100644 --- a/frost-client/src/coordinator.rs +++ b/frost-client/src/coordinator.rs @@ -58,24 +58,28 @@ pub(crate) async fn run_for_ciphersuite( 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 = if let Some(server_url) = server_url { + server_url + } else { + 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")?; - let registry = config - .registry - .get(&server_url) - .ok_or_eyre("Not registered in the given server")?; + let signers = signers + .iter() + .map(|s| Ok(hex::decode(s)?.to_vec())) + .collect::, Box>>()?; + let num_signers = signers.len() as u16; let group_participants = group.participant.clone(); let pargs = coordinator::args::ProcessedArgs { cli: false, http: true, - username: registry.username.clone(), + username: String::new(), password: String::new(), - signers: signers.clone(), - num_signers: signers.len() as u16, + signers, + num_signers, public_key_package, messages: coordinator::args::read_messages(&message, &mut output, &mut input)?, randomizers: coordinator::args::read_randomizers(&randomizer, &mut output, &mut input)?, @@ -85,23 +89,26 @@ pub(crate) async fn run_for_ciphersuite( .ok_or_eyre("host missing in URL")? .to_owned(), port: server_url_parsed.port().unwrap_or(2744), - authentication_token: Some( - registry - .token - .clone() - .ok_or_eyre("Not logged in in the given server")?, - ), + authentication_token: None, comm_privkey: Some( config .communication_key + .clone() .ok_or_eyre("user not initialized")? .privkey .clone(), ), - comm_participant_pubkey_getter: Some(Rc::new(move |participant_username| { + comm_pubkey: Some( + config + .communication_key + .ok_or_eyre("user not initialized")? + .pubkey + .clone(), + ), + comm_participant_pubkey_getter: Some(Rc::new(move |participant_pubkey| { group_participants .values() - .find(|p| p.username == Some(participant_username.to_string())) + .find(|p| p.pubkey == *participant_pubkey) .map(|p| p.pubkey.clone()) })), }; diff --git a/frost-client/src/participant.rs b/frost-client/src/participant.rs index 915218ab..2999d187 100644 --- a/frost-client/src/participant.rs +++ b/frost-client/src/participant.rs @@ -54,21 +54,19 @@ pub(crate) async fn run_for_ciphersuite( 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 = if let Some(server_url) = server_url { + server_url + } else { + 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")?; - let registry = config - .registry - .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, - username: registry.username.clone(), + username: String::new(), password: String::new(), key_package, ip: server_url_parsed @@ -76,24 +74,27 @@ pub(crate) async fn run_for_ciphersuite( .ok_or_eyre("host missing in URL")? .to_owned(), port: server_url_parsed.port().unwrap_or(2744), - authentication_token: Some( - registry - .token - .clone() - .ok_or_eyre("Not logged in in the given server")?, - ), + authentication_token: None, session_id: String::new(), comm_privkey: Some( config .communication_key + .clone() .ok_or_eyre("user not initialized")? .privkey .clone(), ), - comm_coordinator_pubkey_getter: Some(Rc::new(move |coordinator_username| { + comm_pubkey: Some( + config + .communication_key + .ok_or_eyre("user not initialized")? + .pubkey + .clone(), + ), + comm_coordinator_pubkey_getter: Some(Rc::new(move |coordinator_pubkey| { group_participants .values() - .find(|p| p.username == Some(coordinator_username.to_string())) + .find(|p| p.pubkey == *coordinator_pubkey) .map(|p| p.pubkey.clone()) })), }; diff --git a/participant/Cargo.toml b/participant/Cargo.toml index 0a65e9d1..ae2b289e 100644 --- a/participant/Cargo.toml +++ b/participant/Cargo.toml @@ -25,6 +25,7 @@ reqwest = { version = "0.12.9", features = ["json"] } server = { path = "../server" } rpassword = "7.3.1" snow = "0.9.6" +xeddsa = "1.0.2" [features] default = [] diff --git a/participant/src/args.rs b/participant/src/args.rs index 6c95ced3..6c9d8fab 100644 --- a/participant/src/args.rs +++ b/participant/src/args.rs @@ -98,13 +98,16 @@ pub struct ProcessedArgs { /// `comm_coordinator_pubkey_getter` enables encryption. pub comm_privkey: Option>, - /// A function that returns the public key for the given username of the - /// coordinator, or None if not available. + /// The participant's communication public key. + pub comm_pubkey: Option>, + + /// A function that confirms that a public key from the server is trusted by + /// the user; returns the same public key. // It is a `Rc` 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 Option>>>, + pub comm_coordinator_pubkey_getter: Option) -> Option>>>, } impl ProcessedArgs { @@ -142,6 +145,7 @@ impl ProcessedArgs { authentication_token: None, session_id: args.session_id.clone(), comm_privkey: None, + comm_pubkey: None, comm_coordinator_pubkey_getter: None, }) } diff --git a/participant/src/comms/http.rs b/participant/src/comms/http.rs index 123c0339..30775d52 100644 --- a/participant/src/comms/http.rs +++ b/participant/src/comms/http.rs @@ -12,7 +12,9 @@ use eyre::{eyre, OptionExt}; use frost_core::{ self as frost, round1::SigningCommitments, round2::SignatureShare, Ciphersuite, Identifier, }; +use rand::thread_rng; use snow::{HandshakeState, TransportState}; +use xeddsa::{xed25519, Sign as _}; use super::Comms; use crate::args::ProcessedArgs; @@ -177,21 +179,46 @@ where ), Box, > { - if self.access_token.is_empty() { - self.access_token = self - .client - .post(format!("{}/login", self.host_port)) - .json(&server::LoginArgs { - username: self.args.username.clone(), - password: self.args.password.clone(), - }) - .send() - .await? - .json::() - .await? - .access_token - .to_string(); - } + let mut rng = thread_rng(); + let challenge = self + .client + .post(format!("{}/challenge", self.host_port)) + .json(&server::ChallengeArgs {}) + .send() + .await? + .json::() + .await? + .challenge; + + let privkey = xed25519::PrivateKey::from( + &TryInto::<[u8; 32]>::try_into( + self.args + .comm_privkey + .clone() + .ok_or_eyre("comm_privkey must be specified")?, + ) + .map_err(|_| eyre!("invalid comm_privkey"))?, + ); + let signature: [u8; 64] = privkey.sign(challenge.as_bytes(), &mut rng); + + self.access_token = self + .client + .post(format!("{}/key_login", self.host_port)) + .json(&server::KeyLoginArgs { + uuid: challenge, + pubkey: self + .args + .comm_pubkey + .clone() + .ok_or_eyre("comm_pubkey must be specified")?, + signature: signature.to_vec(), + }) + .send() + .await? + .json::() + .await? + .access_token + .to_string(); let session_id = match self.session_id { Some(s) => s, @@ -235,7 +262,7 @@ where .json::() .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 comm_coordinator_pubkey = comm_coordinator_pubkey_getter(&session_info.coordinator_pubkey).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() diff --git a/server/Cargo.toml b/server/Cargo.toml index 6429c5f5..bb0818c6 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -10,6 +10,7 @@ axum = "0.7.9" axum-extra = { version = "0.9.4", features = ["typed-header"] } axum-macros = "0.4.2" clap = { version = "4.5.21", features = ["derive"] } +delay_map = "0.4.0" derivative = "2.2.0" eyre = "0.6.11" frost-core = { version = "2.0.0", features = ["serde"] } @@ -26,6 +27,7 @@ tower-http = { version = "0.6.2", features = ["trace"] } tracing = "0.1" tracing-subscriber = { version = "0.3", features = ["env-filter"] } uuid = { version = "1.11.0", features = ["v4", "fast-rng", "serde"] } +xeddsa = "1.0.2" [dev-dependencies] axum-test = "16.4.0" diff --git a/server/src/functions.rs b/server/src/functions.rs index 4779eaaf..a05bba64 100644 --- a/server/src/functions.rs +++ b/server/src/functions.rs @@ -1,13 +1,13 @@ use axum::{extract::State, http::StatusCode, Json}; use eyre::eyre; use uuid::Uuid; +use xeddsa::{xed25519, Verify as _}; use crate::{ state::{Session, SharedState}, types::*, user::{ - add_access_token, authenticate_user, create_user, delete_user, get_user, - remove_access_token, User, + add_access_token, authenticate_user, create_user, delete_user, remove_access_token, User, }, AppError, }; @@ -28,18 +28,78 @@ pub(crate) async fn register( )); } - let db = { - let state_lock = state.read().unwrap(); - state_lock.db.clone() - }; - - create_user(db, username, password, args.pubkey) + create_user(state.db.clone(), username, password, args.pubkey) .await .map_err(|e| AppError(StatusCode::INTERNAL_SERVER_ERROR, e))?; Ok(Json(())) } +/// Implement the challenge API. +#[tracing::instrument(ret, err(Debug), skip(state, _args))] +pub(crate) async fn challenge( + State(state): State, + Json(_args): Json, +) -> Result, AppError> { + // Create new challenge. + let challenge = Uuid::new_v4(); + + state.challenges.write().unwrap().insert(challenge); + + let output = ChallengeOutput { challenge }; + Ok(Json(output)) +} + +/// Implement the key_login API. +#[tracing::instrument(ret, err(Debug), skip(state, args))] +pub(crate) async fn key_login( + State(state): State, + Json(args): Json, +) -> Result, AppError> { + // Check if the user sent the credentials + if args.signature.is_empty() || args.pubkey.is_empty() { + return Err(AppError( + StatusCode::INTERNAL_SERVER_ERROR, + eyre!("empty args").into(), + )); + } + + let pubkey = TryInto::<[u8; 32]>::try_into(args.pubkey.clone()).map_err(|_| { + AppError( + StatusCode::INTERNAL_SERVER_ERROR, + eyre!("invalid pubkey").into(), + ) + })?; + let pubkey = xed25519::PublicKey(pubkey); + let signature = TryInto::<[u8; 64]>::try_into(args.signature).map_err(|_| { + AppError( + StatusCode::INTERNAL_SERVER_ERROR, + eyre!("invalid signature").into(), + ) + })?; + pubkey + .verify(args.uuid.as_bytes(), &signature) + .map_err(|_| AppError(StatusCode::UNAUTHORIZED, eyre!("invalid signature").into()))?; + + let mut challenges = state.challenges.write().unwrap(); + if !challenges.remove(&args.uuid) { + return Err(AppError( + StatusCode::UNAUTHORIZED, + eyre!("invalid challenge").into(), + )); + } + drop(challenges); + + let access_token = Uuid::new_v4(); + + let mut access_tokens = state.access_tokens.write().unwrap(); + access_tokens.insert(access_token, args.pubkey); + + let token = KeyLoginOutput { access_token }; + + Ok(Json(token)) +} + /// Implement the login API. #[tracing::instrument(ret, err(Debug), skip(state,args), fields(args.username = %args.username))] pub(crate) async fn login( @@ -54,12 +114,7 @@ pub(crate) async fn login( )); } - let db = { - let state_lock = state.read().unwrap(); - state_lock.db.clone() - }; - - let user = authenticate_user(db.clone(), &args.username, &args.password) + let user = authenticate_user(state.db.clone(), &args.username, &args.password) .await .map_err(|e| AppError(StatusCode::INTERNAL_SERVER_ERROR, e))?; @@ -73,7 +128,7 @@ pub(crate) async fn login( } }; - let access_token = add_access_token(db.clone(), user.id) + let access_token = add_access_token(state.db.clone(), user.id) .await .map_err(|e| AppError(StatusCode::INTERNAL_SERVER_ERROR, e))?; @@ -88,13 +143,14 @@ pub(crate) async fn logout( State(state): State, user: User, ) -> Result, AppError> { - let db = { - let state_lock = state.read().unwrap(); - state_lock.db.clone() - }; + state.access_tokens.write().unwrap().remove( + &user + .current_token + .expect("user is logged in so they must have a token"), + ); remove_access_token( - db.clone(), + state.db.clone(), user.current_token .expect("user is logged in so they must have a token"), ) @@ -110,12 +166,7 @@ pub(crate) async fn unregister( State(state): State, user: User, ) -> Result, AppError> { - let db = { - let state_lock = state.read().unwrap(); - state_lock.db.clone() - }; - - delete_user(db, user.id) + delete_user(state.db.clone(), user.id) .await .map_err(|e| AppError(StatusCode::INTERNAL_SERVER_ERROR, e))?; @@ -135,39 +186,24 @@ pub(crate) async fn create_new_session( eyre!("invalid message_count").into(), )); } - let db = { - let state_lock = state.read().unwrap(); - state_lock.db.clone() - }; - for username in &args.usernames { - if get_user(db.clone(), username) - .await - .map_err(|e| AppError(StatusCode::INTERNAL_SERVER_ERROR, e))? - .is_none() - { - return Err(AppError( - StatusCode::INTERNAL_SERVER_ERROR, - eyre!("invalid user").into(), - )); - } - } + // Create new session object. let id = Uuid::new_v4(); - let mut state = state.write().unwrap(); + let mut state = state.sessions.write().unwrap(); // Save session ID in global state - for username in &args.usernames { + for pubkey in &args.pubkeys { state - .sessions_by_username - .entry(username.to_string()) + .sessions_by_pubkey + .entry(pubkey.clone()) .or_default() .insert(id); } // Create Session object let session = Session { - usernames: args.usernames, - coordinator: user.username, + pubkeys: args.pubkeys, + coordinator_pubkey: user.pubkey, num_signers: args.num_signers, message_count: args.message_count, queue: Default::default(), @@ -185,11 +221,11 @@ pub(crate) async fn list_sessions( State(state): State, user: User, ) -> Result, AppError> { - let state = state.read().unwrap(); + let state = state.sessions.read().unwrap(); let session_ids = state - .sessions_by_username - .get(&user.username) + .sessions_by_pubkey + .get(&user.pubkey) .map(|s| s.iter().cloned().collect()) .unwrap_or_default(); @@ -203,7 +239,7 @@ pub(crate) async fn get_session_info( user: User, Json(args): Json, ) -> Result, AppError> { - let state_lock = state.read().unwrap(); + let state_lock = state.sessions.read().unwrap(); let session = state_lock.sessions.get(&args.session_id).ok_or(AppError( StatusCode::NOT_FOUND, @@ -213,8 +249,8 @@ pub(crate) async fn get_session_info( Ok(Json(GetSessionInfoOutput { num_signers: session.num_signers, message_count: session.message_count, - usernames: session.usernames.clone(), - coordinator: session.coordinator.clone(), + pubkeys: session.pubkeys.clone(), + coordinator_pubkey: session.coordinator_pubkey.clone(), })) } @@ -227,7 +263,7 @@ pub(crate) async fn send( Json(args): Json, ) -> Result<(), AppError> { // Get the mutex lock to read and write from the state - let mut state_lock = state.write().unwrap(); + let mut state_lock = state.sessions.write().unwrap(); let session = state_lock .sessions @@ -238,17 +274,17 @@ pub(crate) async fn send( ))?; let recipients = if args.recipients.is_empty() { - vec![String::new()] + vec![Vec::new()] } else { - args.recipients + args.recipients.into_iter().map(|p| p.0).collect() }; - for username in &recipients { + for pubkey in &recipients { session .queue - .entry(username.clone()) + .entry(pubkey.clone()) .or_default() .push_back(Msg { - sender: user.username.clone(), + sender: user.pubkey.clone(), msg: args.msg.clone(), }); } @@ -265,7 +301,7 @@ pub(crate) async fn receive( Json(args): Json, ) -> Result, AppError> { // Get the mutex lock to read and write from the state - let mut state_lock = state.write().unwrap(); + let mut state_lock = state.sessions.write().unwrap(); let session = state_lock .sessions @@ -275,18 +311,13 @@ pub(crate) async fn receive( eyre!("session ID not found").into(), ))?; - let username = if user.username == session.coordinator && args.as_coordinator { - String::new() + let pubkey = if user.pubkey == session.coordinator_pubkey && args.as_coordinator { + Vec::new() } else { - user.username + user.pubkey }; - let msgs = session - .queue - .entry(username.to_string()) - .or_default() - .drain(..) - .collect(); + let msgs = session.queue.entry(pubkey).or_default().drain(..).collect(); Ok(Json(ReceiveOutput { msgs })) } @@ -298,7 +329,7 @@ pub(crate) async fn close_session( user: User, Json(args): Json, ) -> Result, AppError> { - let mut state = state.write().unwrap(); + let mut state = state.sessions.write().unwrap(); for username in state .sessions @@ -307,10 +338,10 @@ pub(crate) async fn close_session( StatusCode::INTERNAL_SERVER_ERROR, eyre!("invalid session ID").into(), ))? - .usernames + .pubkeys .clone() { - if let Some(v) = state.sessions_by_username.get_mut(&username) { + if let Some(v) = state.sessions_by_pubkey.get_mut(&username) { v.remove(&args.session_id); } } diff --git a/server/src/lib.rs b/server/src/lib.rs index 01f24bbb..d3e6f252 100644 --- a/server/src/lib.rs +++ b/server/src/lib.rs @@ -23,7 +23,9 @@ pub fn router(shared_state: SharedState) -> Router { // Shared state that is passed to each handler by axum Router::new() .route("/register", post(functions::register)) + .route("/challenge", post(functions::challenge)) .route("/login", post(functions::login)) + .route("/key_login", post(functions::key_login)) .route("/logout", post(functions::logout)) .route("/unregister", post(functions::unregister)) .route("/create_new_session", post(functions::create_new_session)) @@ -39,7 +41,7 @@ pub fn router(shared_state: SharedState) -> Router { /// Run the server with the specified arguments. pub async fn run(args: &Args) -> Result<(), Box> { let shared_state = AppState::new(&args.database).await?; - let app = router(shared_state); + let app = router(shared_state.clone()); let addr = format!("{}:{}", args.ip, args.port); let listener = tokio::net::TcpListener::bind(addr).await?; diff --git a/server/src/state.rs b/server/src/state.rs index 7c2cb94a..66de3da4 100644 --- a/server/src/state.rs +++ b/server/src/state.rs @@ -4,6 +4,7 @@ use std::{ sync::{Arc, RwLock}, }; +use delay_map::{HashMapDelay, HashSetDelay}; use sqlx::{ sqlite::{SqliteConnectOptions, SqlitePoolOptions}, SqlitePool, @@ -15,10 +16,10 @@ use crate::Msg; /// A particular signing session. #[derive(Debug)] pub struct Session { - /// The usernames of the participants - pub(crate) usernames: Vec, - /// The username of the coordinator - pub(crate) coordinator: String, + /// The public keys of the participants + pub(crate) pubkeys: Vec>, + /// The public key of the coordinator + pub(crate) coordinator_pubkey: Vec, /// The number of signers in the session. pub(crate) num_signers: u16, /// The set of identifiers for the session. @@ -26,16 +27,23 @@ pub struct Session { /// The number of messages being simultaneously signed. pub(crate) message_count: u8, /// The message queue. - pub(crate) queue: HashMap>, + pub(crate) queue: HashMap, VecDeque>, } /// The global state of the server. #[derive(Debug)] pub struct AppState { + pub(crate) sessions: Arc>, + pub(crate) challenges: Arc>>, + pub(crate) access_tokens: Arc>>>, + pub(crate) db: SqlitePool, +} + +#[derive(Debug, Default)] +pub struct SessionState { /// Mapping of signing sessions by UUID. pub(crate) sessions: HashMap, - pub(crate) sessions_by_username: HashMap>, - pub(crate) db: SqlitePool, + pub(crate) sessions_by_pubkey: HashMap, HashSet>, } impl AppState { @@ -46,13 +54,15 @@ impl AppState { sqlx::migrate!().run(&db).await?; let state = Self { sessions: Default::default(), - sessions_by_username: Default::default(), + challenges: RwLock::new(HashSetDelay::new(std::time::Duration::from_secs(10))).into(), + access_tokens: RwLock::new(HashMapDelay::new(std::time::Duration::from_secs(60 * 60))) + .into(), db, }; - Ok(Arc::new(RwLock::new(state))) + Ok(Arc::new(state)) } } /// Type alias for the global state under a reference-counted RW mutex, /// which allows reading and writing the state across different handlers. -pub type SharedState = Arc>; +pub type SharedState = Arc; diff --git a/server/src/types.rs b/server/src/types.rs index fcc5041f..c9ec0808 100644 --- a/server/src/types.rs +++ b/server/src/types.rs @@ -16,6 +16,34 @@ pub struct RegisterArgs { pub pubkey: Vec, } +#[derive(Debug, Serialize, Deserialize)] +pub struct ChallengeArgs {} + +#[derive(Debug, Serialize, Deserialize)] +pub struct ChallengeOutput { + pub challenge: Uuid, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct KeyLoginArgs { + pub uuid: Uuid, + #[serde( + serialize_with = "serdect::slice::serialize_hex_lower_or_bin", + deserialize_with = "serdect::slice::deserialize_hex_or_bin_vec" + )] + pub pubkey: Vec, + #[serde( + serialize_with = "serdect::slice::serialize_hex_lower_or_bin", + deserialize_with = "serdect::slice::deserialize_hex_or_bin_vec" + )] + pub signature: Vec, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct KeyLoginOutput { + pub access_token: Uuid, +} + #[derive(Debug, Serialize, Deserialize)] pub struct LoginOutput { pub access_token: Uuid, @@ -29,7 +57,7 @@ pub struct LoginArgs { #[derive(Clone, Debug, Serialize, Deserialize)] pub struct CreateNewSessionArgs { - pub usernames: Vec, + pub pubkeys: Vec>, pub num_signers: u16, pub message_count: u8, } @@ -53,14 +81,24 @@ pub struct GetSessionInfoArgs { pub struct GetSessionInfoOutput { pub num_signers: u16, pub message_count: u8, - pub usernames: Vec, - pub coordinator: String, + pub pubkeys: Vec>, + pub coordinator_pubkey: Vec, } +#[derive(Clone, Debug, Serialize, Deserialize)] +#[serde(transparent)] +pub struct PublicKey( + #[serde( + serialize_with = "serdect::slice::serialize_hex_lower_or_bin", + deserialize_with = "serdect::slice::deserialize_hex_or_bin_vec" + )] + pub Vec, +); + #[derive(Clone, Debug, Serialize, Deserialize)] pub struct SendArgs { pub session_id: Uuid, - pub recipients: Vec, + pub recipients: Vec, #[serde( serialize_with = "serdect::slice::serialize_hex_lower_or_bin", deserialize_with = "serdect::slice::deserialize_hex_or_bin_vec" @@ -70,7 +108,7 @@ pub struct SendArgs { #[derive(Clone, Debug, Serialize, Deserialize)] pub struct Msg { - pub sender: String, + pub sender: Vec, #[serde( serialize_with = "serdect::slice::serialize_hex_lower_or_bin", deserialize_with = "serdect::slice::deserialize_hex_or_bin_vec" diff --git a/server/src/user.rs b/server/src/user.rs index ff3ed2ee..11ce3ae5 100644 --- a/server/src/user.rs +++ b/server/src/user.rs @@ -212,15 +212,28 @@ impl FromRequestParts for User { ) })?; - let db = { - let state_lock = state.read().unwrap(); - state_lock.db.clone() + let pubkey = state + .access_tokens + .read() + .unwrap() + .get(&access_token) + .cloned(); + + let user = if let Some(pubkey) = pubkey { + Some(User { + id: -1, + username: String::new(), + password: String::new(), + pubkey, + access_tokens: vec![], + current_token: Some(access_token), + }) + } else { + get_user_for_access_token(state.db.clone(), access_token) + .await + .map_err(|e| AppError(StatusCode::INTERNAL_SERVER_ERROR, e))? }; - let user = get_user_for_access_token(db, access_token) - .await - .map_err(|e| AppError(StatusCode::INTERNAL_SERVER_ERROR, e))?; - match user { Some(mut user) => { user.current_token = Some(access_token); diff --git a/server/tests/integration_tests.rs b/server/tests/integration_tests.rs index 298d159f..406b5e8f 100644 --- a/server/tests/integration_tests.rs +++ b/server/tests/integration_tests.rs @@ -10,6 +10,7 @@ use server::{ }; use frost_core as frost; +use xeddsa::{xed25519, Sign, Verify}; #[tokio::test] async fn test_main_router_ed25519() -> Result<(), Box> { @@ -60,42 +61,50 @@ async fn test_main_router< // it currently it doesn't really matter who the user is, users are only // used to share session IDs. This will likely change soon. + let builder = snow::Builder::new("Noise_K_25519_ChaChaPoly_BLAKE2s".parse().unwrap()); + let alice_keypair = builder.generate_keypair().unwrap(); + let bob_keypair = builder.generate_keypair().unwrap(); + let res = server - .post("/register") - .json(&server::RegisterArgs { - username: "alice".to_string(), - password: "passw0rd".to_string(), - pubkey: vec![], - }) + .post("/challenge") + .json(&server::ChallengeArgs {}) .await; res.assert_status_ok(); + let r: server::ChallengeOutput = res.json(); + let alice_challenge = r.challenge; let res = server - .post("/register") - .json(&server::RegisterArgs { - username: "bob".to_string(), - password: "passw0rd".to_string(), - pubkey: vec![], - }) + .post("/challenge") + .json(&server::ChallengeArgs {}) .await; res.assert_status_ok(); + let r: server::ChallengeOutput = res.json(); + let bob_challenge = r.challenge; + let alice_private = + xed25519::PrivateKey::from(&TryInto::<[u8; 32]>::try_into(alice_keypair.private).unwrap()); + let alice_signature: [u8; 64] = alice_private.sign(alice_challenge.as_bytes(), &mut rng); let res = server - .post("/login") - .json(&server::LoginArgs { - username: "alice".to_string(), - password: "passw0rd".to_string(), + .post("/key_login") + .json(&server::KeyLoginArgs { + uuid: alice_challenge, + pubkey: alice_keypair.public.clone(), + signature: alice_signature.to_vec(), }) .await; res.assert_status_ok(); let r: server::LoginOutput = res.json(); let alice_token = r.access_token; + let bob_private = + xed25519::PrivateKey::from(&TryInto::<[u8; 32]>::try_into(bob_keypair.private).unwrap()); + let bob_signature: [u8; 64] = bob_private.sign(bob_challenge.as_bytes(), &mut rng); let res = server - .post("/login") - .json(&server::LoginArgs { - username: "bob".to_string(), - password: "passw0rd".to_string(), + .post("/key_login") + .json(&server::KeyLoginArgs { + uuid: bob_challenge, + pubkey: bob_keypair.public.clone(), + signature: bob_signature.to_vec(), }) .await; res.assert_status_ok(); @@ -109,7 +118,7 @@ async fn test_main_router< .post("/create_new_session") .authorization_bearer(alice_token) .json(&server::CreateNewSessionArgs { - usernames: vec!["alice".to_string(), "bob".to_string()], + pubkeys: vec![alice_keypair.public.clone(), bob_keypair.public.clone()], num_signers: 2, message_count: 2, }) @@ -228,7 +237,7 @@ async fn test_main_router< .authorization_bearer(alice_token) .json(&server::SendArgs { session_id, - recipients: usernames.keys().cloned().collect(), + recipients: usernames.keys().cloned().map(server::PublicKey).collect(), msg: serde_json::to_vec(&send_signing_package_args)?, }) .await; @@ -372,6 +381,7 @@ async fn test_main_router< #[tokio::test] async fn test_http() -> Result<(), Box> { tracing_subscriber::fmt::init(); + let mut rng = thread_rng(); // Spawn server for testing tokio::spawn(async move { @@ -391,45 +401,39 @@ async fn test_http() -> Result<(), Box> { // Create a client to make requests let client = reqwest::Client::new(); - // Call register to create users - let r = client - .post("http://127.0.0.1:2744/register") - .json(&server::RegisterArgs { - username: "alice".to_string(), - password: "passw0rd".to_string(), - pubkey: vec![], - }) - .send() - .await?; - if r.status() != reqwest::StatusCode::OK { - panic!("{}", r.text().await?) - } + let builder = snow::Builder::new("Noise_K_25519_ChaChaPoly_BLAKE2s".parse().unwrap()); + let alice_keypair = builder.generate_keypair().unwrap(); + let bob_keypair = builder.generate_keypair().unwrap(); + + // Get challenges for login let r = client - .post("http://127.0.0.1:2744/register") - .json(&server::RegisterArgs { - username: "bob".to_string(), - password: "passw0rd".to_string(), - pubkey: vec![], - }) + .post("http://127.0.0.1:2744/challenge") + .json(&server::ChallengeArgs {}) .send() .await?; if r.status() != reqwest::StatusCode::OK { panic!("{}", r.text().await?) } + let r = r.json::().await?; + let alice_challenge = r.challenge; - // Call login to authenticate + // Call key_login to authenticate + let alice_private = + xed25519::PrivateKey::from(&TryInto::<[u8; 32]>::try_into(alice_keypair.private).unwrap()); + let alice_signature: [u8; 64] = alice_private.sign(alice_challenge.as_bytes(), &mut rng); let r = client - .post("http://127.0.0.1:2744/login") - .json(&server::LoginArgs { - username: "alice".to_string(), - password: "passw0rd".to_string(), + .post("http://127.0.0.1:2744/key_login") + .json(&server::KeyLoginArgs { + uuid: alice_challenge, + pubkey: alice_keypair.public.clone(), + signature: alice_signature.to_vec(), }) .send() .await?; if r.status() != reqwest::StatusCode::OK { panic!("{}", r.text().await?) } - let r = r.json::().await?; + let r = r.json::().await?; let access_token = r.access_token; // Call create_new_session @@ -437,7 +441,7 @@ async fn test_http() -> Result<(), Box> { .post("http://127.0.0.1:2744/create_new_session") .bearer_auth(access_token) .json(&server::CreateNewSessionArgs { - usernames: vec!["alice".to_string(), "bob".to_string()], + pubkeys: vec![alice_keypair.public.clone(), bob_keypair.public.clone()], message_count: 1, num_signers: 2, }) @@ -502,3 +506,21 @@ fn test_snow() -> Result<(), Box> { Ok(()) } + +/// Test if signing with a snow keypair works. +#[test] +fn test_snow_keypair() -> Result<(), Box> { + let builder = snow::Builder::new("Noise_K_25519_ChaChaPoly_BLAKE2s".parse().unwrap()); + let keypair = builder.generate_keypair().unwrap(); + + let private = + xed25519::PrivateKey::from(&TryInto::<[u8; 32]>::try_into(keypair.private).unwrap()); + let public = xed25519::PublicKey(TryInto::<[u8; 32]>::try_into(keypair.public).unwrap()); + let msg: &[u8] = b"hello"; + + let rng = thread_rng(); + let signature: [u8; 64] = private.sign(msg, rng); + public.verify(msg, &signature).unwrap(); + + Ok(()) +} From d3404b655a681f4b333b6b707a6977d32a265f53 Mon Sep 17 00:00:00 2001 From: Conrado Gouvea Date: Fri, 29 Nov 2024 13:52:49 -0300 Subject: [PATCH 2/2] use PublicKey for all instances in types.rs --- Cargo.lock | 1 + coordinator/src/comms/http.rs | 6 ++++-- server/Cargo.toml | 1 + server/src/functions.rs | 6 +++--- server/src/types.rs | 14 +++++++++++--- server/tests/integration_tests.rs | 10 ++++++++-- 6 files changed, 28 insertions(+), 10 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index be8e2e25..8cb1ad45 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2770,6 +2770,7 @@ dependencies = [ "frost-core", "frost-ed25519", "frost-rerandomized", + "hex", "password-auth", "rand", "reddsa", diff --git a/coordinator/src/comms/http.rs b/coordinator/src/comms/http.rs index c1080144..c2f6e3df 100644 --- a/coordinator/src/comms/http.rs +++ b/coordinator/src/comms/http.rs @@ -18,7 +18,9 @@ use frost_core::{ use participant::comms::http::Noise; use rand::thread_rng; -use server::{Msg, SendCommitmentsArgs, SendSignatureSharesArgs, SendSigningPackageArgs, Uuid}; +use server::{ + Msg, PublicKey, SendCommitmentsArgs, SendSignatureSharesArgs, SendSigningPackageArgs, Uuid, +}; use xeddsa::{xed25519, Sign as _}; use super::Comms; @@ -392,7 +394,7 @@ impl Comms for HTTPComms { .post(format!("{}/create_new_session", self.host_port)) .bearer_auth(&self.access_token) .json(&server::CreateNewSessionArgs { - pubkeys: self.args.signers.clone(), + pubkeys: self.args.signers.iter().cloned().map(PublicKey).collect(), num_signers, message_count: 1, }) diff --git a/server/Cargo.toml b/server/Cargo.toml index bb0818c6..3f43c912 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -28,6 +28,7 @@ tracing = "0.1" tracing-subscriber = { version = "0.3", features = ["env-filter"] } uuid = { version = "1.11.0", features = ["v4", "fast-rng", "serde"] } xeddsa = "1.0.2" +hex = "0.4.3" [dev-dependencies] axum-test = "16.4.0" diff --git a/server/src/functions.rs b/server/src/functions.rs index a05bba64..82c27408 100644 --- a/server/src/functions.rs +++ b/server/src/functions.rs @@ -196,13 +196,13 @@ pub(crate) async fn create_new_session( for pubkey in &args.pubkeys { state .sessions_by_pubkey - .entry(pubkey.clone()) + .entry(pubkey.0.clone()) .or_default() .insert(id); } // Create Session object let session = Session { - pubkeys: args.pubkeys, + pubkeys: args.pubkeys.into_iter().map(|p| p.0).collect(), coordinator_pubkey: user.pubkey, num_signers: args.num_signers, message_count: args.message_count, @@ -249,7 +249,7 @@ pub(crate) async fn get_session_info( Ok(Json(GetSessionInfoOutput { num_signers: session.num_signers, message_count: session.message_count, - pubkeys: session.pubkeys.clone(), + pubkeys: session.pubkeys.iter().cloned().map(PublicKey).collect(), coordinator_pubkey: session.coordinator_pubkey.clone(), })) } diff --git a/server/src/types.rs b/server/src/types.rs index c9ec0808..50e25236 100644 --- a/server/src/types.rs +++ b/server/src/types.rs @@ -57,7 +57,7 @@ pub struct LoginArgs { #[derive(Clone, Debug, Serialize, Deserialize)] pub struct CreateNewSessionArgs { - pub pubkeys: Vec>, + pub pubkeys: Vec, pub num_signers: u16, pub message_count: u8, } @@ -81,11 +81,11 @@ pub struct GetSessionInfoArgs { pub struct GetSessionInfoOutput { pub num_signers: u16, pub message_count: u8, - pub pubkeys: Vec>, + pub pubkeys: Vec, pub coordinator_pubkey: Vec, } -#[derive(Clone, Debug, Serialize, Deserialize)] +#[derive(Clone, Serialize, Deserialize)] #[serde(transparent)] pub struct PublicKey( #[serde( @@ -95,6 +95,14 @@ pub struct PublicKey( pub Vec, ); +impl std::fmt::Debug for PublicKey { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_tuple("PublicKey") + .field(&hex::encode(&self.0)) + .finish() + } +} + #[derive(Clone, Debug, Serialize, Deserialize)] pub struct SendArgs { pub session_id: Uuid, diff --git a/server/tests/integration_tests.rs b/server/tests/integration_tests.rs index 406b5e8f..9c2a6d94 100644 --- a/server/tests/integration_tests.rs +++ b/server/tests/integration_tests.rs @@ -118,7 +118,10 @@ async fn test_main_router< .post("/create_new_session") .authorization_bearer(alice_token) .json(&server::CreateNewSessionArgs { - pubkeys: vec![alice_keypair.public.clone(), bob_keypair.public.clone()], + pubkeys: vec![ + server::PublicKey(alice_keypair.public.clone()), + server::PublicKey(bob_keypair.public.clone()), + ], num_signers: 2, message_count: 2, }) @@ -441,7 +444,10 @@ async fn test_http() -> Result<(), Box> { .post("http://127.0.0.1:2744/create_new_session") .bearer_auth(access_token) .json(&server::CreateNewSessionArgs { - pubkeys: vec![alice_keypair.public.clone(), bob_keypair.public.clone()], + pubkeys: vec![ + server::PublicKey(alice_keypair.public.clone()), + server::PublicKey(bob_keypair.public.clone()), + ], message_count: 1, num_signers: 2, })