diff --git a/rosenpass/src/api/config.rs b/rosenpass/src/api/config.rs index 0a410a4..2bf1b60 100644 --- a/rosenpass/src/api/config.rs +++ b/rosenpass/src/api/config.rs @@ -38,4 +38,12 @@ impl ApiConfig { Ok(()) } + + pub fn count_api_sources(&self) -> usize { + self.listen_path.len() + self.listen_fd.len() + self.stream_fd.len() + } + + pub fn has_api_sources(&self) -> bool { + self.count_api_sources() > 0 + } } diff --git a/rosenpass/src/app_server.rs b/rosenpass/src/app_server.rs index ecdfdab..9b5a5e8 100644 --- a/rosenpass/src/app_server.rs +++ b/rosenpass/src/app_server.rs @@ -514,8 +514,7 @@ impl HostPathDiscoveryEndpoint { impl AppServer { pub fn new( - sk: SSk, - pk: SPk, + keypair: Option<(SSk, SPk)>, addrs: Vec, verbosity: Verbosity, test_helpers: Option, @@ -605,10 +604,13 @@ impl AppServer { )?; } - // TODO use mio::net::UnixStream together with std::os::unix::net::UnixStream for Linux + let crypto_site = match keypair { + Some((sk, pk)) => ConstructionSite::from_product(CryptoServer::new(sk, pk)), + None => ConstructionSite::new(BuildCryptoServer::empty()), + }; Ok(Self { - crypto_site: ConstructionSite::from_product(CryptoServer::new(sk, pk)), + crypto_site, peers: Vec::new(), verbosity, sockets, diff --git a/rosenpass/src/cli.rs b/rosenpass/src/cli.rs index 39ee896..f9dbb88 100644 --- a/rosenpass/src/cli.rs +++ b/rosenpass/src/cli.rs @@ -1,4 +1,4 @@ -use anyhow::{bail, ensure}; +use anyhow::{bail, ensure, Context}; use clap::{Parser, Subcommand}; use rosenpass_cipher_traits::Kem; use rosenpass_ciphers::kem::StaticKem; @@ -303,8 +303,11 @@ impl CliArgs { ); let config = config::Rosenpass::load(config_file)?; + let keypair = config + .keypair + .context("Config file present, but no keypair is specified.")?; - (config.public_key, config.secret_key) + (keypair.public_key, keypair.secret_key) } (_, Some(pkf), Some(skf)) => (pkf.clone(), skf.clone()), _ => { @@ -343,6 +346,7 @@ impl CliArgs { let mut config = config::Rosenpass::load(config_file)?; config.validate()?; self.apply_to_config(&mut config)?; + config.check_usefullness()?; Self::event_loop(config, broker_interface, test_helpers)?; } @@ -363,6 +367,7 @@ impl CliArgs { } config.validate()?; self.apply_to_config(&mut config)?; + config.check_usefullness()?; Self::event_loop(config, broker_interface, test_helpers)?; } @@ -394,13 +399,19 @@ impl CliArgs { const MAX_PSK_SIZE: usize = 1000; // load own keys - let sk = SSk::load(&config.secret_key)?; - let pk = SPk::load(&config.public_key)?; + let keypair = config + .keypair + .as_ref() + .map(|kp| -> anyhow::Result<_> { + let sk = SSk::load(&kp.secret_key)?; + let pk = SPk::load(&kp.public_key)?; + Ok((sk, pk)) + }) + .transpose()?; // start an application server let mut srv = std::boxed::Box::::new(AppServer::new( - sk, - pk, + keypair, config.listen.clone(), config.verbosity, test_helpers, diff --git a/rosenpass/src/config.rs b/rosenpass/src/config.rs index e7538d3..2b25924 100644 --- a/rosenpass/src/config.rs +++ b/rosenpass/src/config.rs @@ -23,11 +23,10 @@ use crate::app_server::AppServer; #[derive(Debug, Serialize, Deserialize)] pub struct Rosenpass { - /// path to the public key file - pub public_key: PathBuf, - - /// path to the secret key file - pub secret_key: PathBuf, + // TODO: Raise error if secret key or public key alone is set during deserialization + // SEE: https://github.com/serde-rs/serde/issues/2793 + #[serde(flatten)] + pub keypair: Option, /// Location of the API listen sockets #[cfg(feature = "experiment_api")] @@ -58,6 +57,26 @@ pub struct Rosenpass { pub config_file_path: PathBuf, } +#[derive(Debug, Deserialize, Serialize, PartialEq, Eq, Clone)] +pub struct Keypair { + /// path to the public key file + pub public_key: PathBuf, + + /// path to the secret key file + pub secret_key: PathBuf, +} + +impl Keypair { + pub fn new, Sk: AsRef>(public_key: Pk, secret_key: Sk) -> Self { + let public_key = public_key.as_ref().to_path_buf(); + let secret_key = secret_key.as_ref().to_path_buf(); + Self { + public_key, + secret_key, + } + } +} + /// ## TODO /// - replace this type with [`log::LevelFilter`], also see #[derive(Debug, PartialEq, Eq, Serialize, Deserialize, Copy, Clone)] @@ -113,6 +132,12 @@ pub struct WireGuard { pub extra_params: Vec, } +impl Default for Rosenpass { + fn default() -> Self { + Self::empty() + } +} + impl Rosenpass { /// load configuration from a TOML file /// @@ -128,8 +153,10 @@ impl Rosenpass { // resolve `~` (see https://github.com/rosenpass/rosenpass/issues/237) use util::resolve_path_with_tilde; - resolve_path_with_tilde(&mut config.public_key); - resolve_path_with_tilde(&mut config.secret_key); + if let Some(ref mut keypair) = config.keypair { + resolve_path_with_tilde(&mut keypair.public_key); + resolve_path_with_tilde(&mut keypair.secret_key); + } for peer in config.peers.iter_mut() { resolve_path_with_tilde(&mut peer.public_key); if let Some(ref mut psk) = &mut peer.pre_shared_key { @@ -175,19 +202,21 @@ impl Rosenpass { /// - check that files do not just exist but are also readable /// - warn if neither out_key nor exchange_command of a peer is defined (v.i.) pub fn validate(&self) -> anyhow::Result<()> { - // check the public key file exists - ensure!( - self.public_key.is_file(), - "could not find public-key file {:?}: no such file", - self.public_key - ); + if let Some(ref keypair) = self.keypair { + // check the public key file exists + ensure!( + keypair.public_key.is_file(), + "could not find public-key file {:?}: no such file", + keypair.public_key + ); - // check the secret-key file exists - ensure!( - self.secret_key.is_file(), - "could not find secret-key file {:?}: no such file", - self.secret_key - ); + // check the secret-key file exists + ensure!( + keypair.secret_key.is_file(), + "could not find secret-key file {:?}: no such file", + keypair.secret_key + ); + } for (i, peer) in self.peers.iter().enumerate() { // check peer's public-key file exists @@ -212,11 +241,33 @@ impl Rosenpass { Ok(()) } + pub fn check_usefullness(&self) -> anyhow::Result<()> { + #[cfg(not(feature = "experiment_api"))] + ensure!(self.keypair.is_some(), "Server keypair missing."); + + #[cfg(feature = "experiment_api")] + ensure!( + self.keypair.is_some() || self.api.has_api_sources(), + "{}{}", + "Specify a server keypair or some API connections to configure the keypair with.", + "Without a keypair, rosenpass can not operate." + ); + + Ok(()) + } + + pub fn empty() -> Self { + Self::new(None) + } + + pub fn from_sk_pk, Pk: AsRef>(sk: Sk, pk: Pk) -> Self { + Self::new(Some(Keypair::new(pk, sk))) + } + /// Creates a new configuration - pub fn new, P2: AsRef>(public_key: P1, secret_key: P2) -> Self { + pub fn new(keypair: Option) -> Self { Self { - public_key: PathBuf::from(public_key.as_ref()), - secret_key: PathBuf::from(secret_key.as_ref()), + keypair, listen: vec![], #[cfg(feature = "experiment_api")] api: crate::api::config::ApiConfig::default(), @@ -242,7 +293,7 @@ impl Rosenpass { /// from chaotic args /// Quest: the grammar is undecideable, what do we do here? pub fn parse_args(args: Vec) -> anyhow::Result { - let mut config = Self::new("", ""); + let mut config = Self::new(Some(Keypair::new("", ""))); #[derive(Debug, Hash, PartialEq, Eq)] enum State { @@ -303,7 +354,7 @@ impl Rosenpass { already_set.insert(OwnPublicKey), "public-key was already set" ); - config.public_key = pk.into(); + config.keypair.as_mut().unwrap().public_key = pk.into(); Own } (OwnSecretKey, sk, None) => { @@ -311,7 +362,7 @@ impl Rosenpass { already_set.insert(OwnSecretKey), "secret-key was already set" ); - config.secret_key = sk.into(); + config.keypair.as_mut().unwrap().secret_key = sk.into(); Own } (OwnListen, l, None) => { @@ -446,10 +497,12 @@ impl Rosenpass { }; Self { - public_key: "/path/to/rp-public-key".into(), - secret_key: "/path/to/rp-secret-key".into(), + keypair: Some(Keypair { + public_key: "/path/to/rp-public-key".into(), + secret_key: "/path/to/rp-secret-key".into(), + }), peers: vec![peer], - ..Self::new("", "") + ..Self::new(None) } } } @@ -462,13 +515,119 @@ impl Default for Verbosity { #[cfg(test)] mod test { + use super::*; - use std::net::IpAddr; + use std::{borrow::Borrow, net::IpAddr}; + + fn toml_des>(s: S) -> Result { + toml::from_str(s.borrow()) + } + + fn toml_ser(s: S) -> Result { + toml::Table::try_from(s) + } + + fn assert_toml>(l: L, r: R, info: &str) -> anyhow::Result<()> { + fn lines_prepend(prefix: &str, s: &str) -> anyhow::Result { + use std::fmt::Write; + + let mut buf = String::new(); + for line in s.lines() { + writeln!(&mut buf, "{prefix}{line}")?; + } + Ok(buf) + } + + let l = toml_ser(l)?; + let r = toml_des(r.borrow())?; + ensure!( + l == r, + "{}{}TOML value mismatch.\n Have:\n{}\n Expected:\n{}", + info, + if info.is_empty() { "" } else { ": " }, + lines_prepend(" ", &toml::to_string_pretty(&l)?)?, + lines_prepend(" ", &toml::to_string_pretty(&r)?)? + ); + Ok(()) + } + + fn assert_toml_round<'de, L: Serialize + Deserialize<'de>, R: Borrow>( + l: L, + r: R, + ) -> anyhow::Result<()> { + let l = toml_ser(l)?; + assert_toml(&l, r.borrow(), "Straight deserialization")?; + + let l: L = l.try_into().unwrap(); + let l = toml_ser(l).unwrap(); + assert_toml(l, r.borrow(), "Roundtrip deserialization")?; + + Ok(()) + } fn split_str(s: &str) -> Vec { s.split(' ').map(|s| s.to_string()).collect() } + #[test] + fn toml_serialization() -> anyhow::Result<()> { + #[cfg(feature = "experiment_api")] + assert_toml_round( + Rosenpass::empty(), + r#" + listen = [] + verbosity = "Quiet" + peers = [] + + [api] + listen_path = [] + listen_fd = [] + stream_fd = [] + "#, + )?; + + #[cfg(not(feature = "experiment_api"))] + assert_toml_round( + Rosenpass::empty(), + r#" + listen = [] + verbosity = "Quiet" + peers = [] + "#, + )?; + + #[cfg(feature = "experiment_api")] + assert_toml_round( + Rosenpass::from_sk_pk("/my/sk", "/my/pk"), + r#" + public_key = "/my/pk" + secret_key = "/my/sk" + listen = [] + verbosity = "Quiet" + peers = [] + + [api] + listen_path = [] + listen_fd = [] + stream_fd = [] + "#, + )?; + + #[cfg(not(feature = "experiment_api"))] + assert_toml_round( + Rosenpass::from_sk_pk("/my/sk", "/my/pk"), + r#" + public_key = "/my/pk" + secret_key = "/my/sk" + listen = [] + verbosity = "Quiet" + peers = [] + "#, + )?; + + Ok(()) + } + #[test] fn test_simple_cli_parse() { let args = split_str( @@ -479,8 +638,10 @@ mod test { let config = Rosenpass::parse_args(args).unwrap(); - assert_eq!(config.public_key, PathBuf::from("/my/public-key")); - assert_eq!(config.secret_key, PathBuf::from("/my/secret-key")); + assert_eq!( + config.keypair, + Some(Keypair::new("/my/public-key", "/my/secret-key")) + ); assert_eq!(config.verbosity, Verbosity::Verbose); assert_eq!( &config.listen, @@ -509,8 +670,10 @@ mod test { let config = Rosenpass::parse_args(args).unwrap(); - assert_eq!(config.public_key, PathBuf::from("/my/public-key")); - assert_eq!(config.secret_key, PathBuf::from("/my/secret-key")); + assert_eq!( + config.keypair, + Some(Keypair::new("/my/public-key", "/my/secret-key")) + ); assert_eq!(config.verbosity, Verbosity::Verbose); assert!(&config.listen.is_empty()); assert_eq!( diff --git a/rosenpass/tests/api-integration-tests.rs b/rosenpass/tests/api-integration-tests.rs index da9c84a..cc777a7 100644 --- a/rosenpass/tests/api-integration-tests.rs +++ b/rosenpass/tests/api-integration-tests.rs @@ -37,10 +37,11 @@ fn api_integration_test() -> anyhow::Result<()> { let peer_b_osk = tempfile!("b.osk"); use rosenpass::config; + + let peer_a_keypair = config::Keypair::new(tempfile!("a.pk"), tempfile!("a.sk")); let peer_a = config::Rosenpass { config_file_path: tempfile!("a.config"), - secret_key: tempfile!("a.sk"), - public_key: tempfile!("a.pk"), + keypair: Some(peer_a_keypair.clone()), listen: peer_a_endpoint.to_socket_addrs()?.collect(), // TODO: This could collide by accident verbosity: config::Verbosity::Verbose, api: api::config::ApiConfig { @@ -57,10 +58,10 @@ fn api_integration_test() -> anyhow::Result<()> { }], }; + let peer_b_keypair = config::Keypair::new(tempfile!("b.pk"), tempfile!("b.sk")); let peer_b = config::Rosenpass { config_file_path: tempfile!("b.config"), - secret_key: tempfile!("b.sk"), - public_key: tempfile!("b.pk"), + keypair: Some(peer_b_keypair.clone()), listen: vec![], verbosity: config::Verbosity::Verbose, api: api::config::ApiConfig { @@ -79,12 +80,12 @@ fn api_integration_test() -> anyhow::Result<()> { // Generate the keys rosenpass::cli::testing::generate_and_save_keypair( - peer_a.secret_key.clone(), - peer_a.public_key.clone(), + peer_a_keypair.secret_key.clone(), + peer_a_keypair.public_key.clone(), )?; rosenpass::cli::testing::generate_and_save_keypair( - peer_b.secret_key.clone(), - peer_b.public_key.clone(), + peer_b_keypair.secret_key.clone(), + peer_b_keypair.public_key.clone(), )?; // Write the configuration files diff --git a/rp/src/exchange.rs b/rp/src/exchange.rs index abef57a..4f8739f 100644 --- a/rp/src/exchange.rs +++ b/rp/src/exchange.rs @@ -188,8 +188,7 @@ pub async fn exchange(options: ExchangeOptions) -> Result<()> { let pk = SPk::load(&pqpk)?; let mut srv = Box::new(AppServer::new( - sk, - pk, + Some((sk, pk)), if let Some(listen) = options.listen { vec![listen] } else {