chore(doc): Docs for rosenpass::{config, cli} (#560)

This commit is contained in:
Paul Spooren
2024-12-18 23:11:45 +01:00
committed by GitHub
8 changed files with 347 additions and 73 deletions

View File

@@ -6,7 +6,7 @@ use serde::{Deserialize, Serialize};
use crate::app_server::AppServer;
#[derive(Debug, Serialize, Deserialize, Default, Clone)]
#[derive(Debug, Serialize, Deserialize, Default, Clone, PartialEq, Eq)]
pub struct ApiConfig {
/// Where in the file-system to create the unix socket the rosenpass API will be listening for
/// connections on

View File

@@ -1,3 +1,8 @@
//! Contains the code used to parse command line parameters for rosenpass.
//!
//! [CliArgs::run] is called by the rosenpass main function and contains the
//! bulk of our boostrapping code while the main function just sets up the basic environment
use anyhow::{bail, ensure, Context};
use clap::{Parser, Subcommand};
use rosenpass_cipher_traits::Kem;
@@ -31,15 +36,25 @@ use {
std::thread,
};
/// enum representing a choice of interface to a WireGuard broker
/// How to reach a WireGuard PSK Broker
#[derive(Debug)]
pub enum BrokerInterface {
/// The PSK Broker is listening on a unix socket at the given path
Socket(PathBuf),
/// The PSK Broker broker is already connected to this process; a
/// unix socket stream can be reached at the given file descriptor.
///
/// This is generally used with file descriptor passing.
FileDescriptor(i32),
/// Create a socketpair(3p), spawn the PSK broker process from within rosenpass,
/// and hand one end of the socketpair to the broker process via file
/// descriptor passing to the subprocess
SocketPair,
}
/// struct holding all CLI arguments for `clap` crate to parse
/// Command line arguments to the Rosenpass binary.
///
/// Used for parsing with [clap].
#[derive(Parser, Debug)]
#[command(author, version, about, long_about, arg_required_else_help = true)]
pub struct CliArgs {
@@ -80,6 +95,7 @@ pub struct CliArgs {
#[arg(short, long, group = "psk-broker-specs")]
psk_broker_spawn: bool,
/// The subcommand to be invoked
#[command(subcommand)]
pub command: Option<CliCommand>,
@@ -98,6 +114,10 @@ pub struct CliArgs {
}
impl CliArgs {
/// Apply the command line parameters to the Rosenpass configuration struct
///
/// Generally the flow of control here is that all the command line parameters
/// are merged into the configuration file to avoid much code duplication.
pub fn apply_to_config(&self, _cfg: &mut config::Rosenpass) -> anyhow::Result<()> {
#[cfg(feature = "experiment_api")]
self.api.apply_to_config(_cfg)?;
@@ -123,9 +143,11 @@ impl CliArgs {
None
}
/// Return the WireGuard PSK broker interface configured.
///
/// Returns `None` if the `experiment_api` feature is disabled.
#[cfg(feature = "experiment_api")]
/// returns the broker interface set by CLI args
/// returns `None` if the `experiment_api` feature isn't enabled
pub fn get_broker_interface(&self) -> Option<BrokerInterface> {
if let Some(path_ref) = self.psk_broker_path.as_ref() {
Some(BrokerInterface::Socket(path_ref.to_path_buf()))
@@ -138,9 +160,10 @@ impl CliArgs {
}
}
/// Return the WireGuard PSK broker interface configured.
///
/// Returns `None` if the `experiment_api` feature is disabled.
#[cfg(not(feature = "experiment_api"))]
/// returns the broker interface set by CLI args
/// returns `None` if the `experiment_api` feature isn't enabled
pub fn get_broker_interface(&self) -> Option<BrokerInterface> {
None
}
@@ -244,15 +267,17 @@ pub enum CliCommand {
}
impl CliArgs {
/// Runs the command specified via CLI
/// Run Rosenpass with the given command line parameters
///
/// ## TODO
/// - This method consumes the [`CliCommand`] value. It might be wise to use a reference...
/// This contains the bulk of our startup logic with
/// the main function just setting up the basic environment
/// and then calling this function.
pub fn run(
self,
broker_interface: Option<BrokerInterface>,
test_helpers: Option<AppServerTest>,
) -> anyhow::Result<()> {
// TODO: This method consumes the [`CliCommand`] value. It might be wise to use a reference...
use CliCommand::*;
match &self.command {
Some(GenConfig { config_file, force }) => {
@@ -403,6 +428,7 @@ impl CliArgs {
Ok(())
}
/// Used by [Self::run] to start the Rosenpass key exchange server
fn event_loop(
config: config::Rosenpass,
broker_interface: Option<BrokerInterface>,
@@ -470,6 +496,19 @@ impl CliArgs {
srv.event_loop()
}
/// Create the WireGuard PSK broker to be used by
/// [crate::app_server::AppServer].
///
/// If the `experiment_api`
/// feature flag is set, then this communicates with a PSK broker
/// running in a different process as configured via
/// the `psk_broker_path`, `psk_broker_fd`, and `psk_broker_spawn`
/// fields.
///
/// If the `experiment_api`
/// feature flag is not set, then this returns a [NativeUnixBroker],
/// sending pre-shared keys directly to WireGuard from within this
/// process.
#[cfg(feature = "experiment_api")]
fn create_broker(
broker_interface: Option<BrokerInterface>,
@@ -485,6 +524,19 @@ impl CliArgs {
}
}
/// Create the WireGuard PSK broker to be used by
/// [crate::app_server::AppServer].
///
/// If the `experiment_api`
/// feature flag is set, then this communicates with a PSK broker
/// running in a different process as configured via
/// the `psk_broker_path`, `psk_broker_fd`, and `psk_broker_spawn`
/// fields.
///
/// If the `experiment_api`
/// feature flag is not set, then this returns a [NativeUnixBroker],
/// sending pre-shared keys directly to WireGuard from within this
/// process.
#[cfg(not(feature = "experiment_api"))]
fn create_broker(
_broker_interface: Option<BrokerInterface>,
@@ -492,6 +544,10 @@ impl CliArgs {
Ok(Box::new(NativeUnixBroker::new()))
}
/// Used by [Self::create_broker] if the `experiment_api` is configured
/// to set up the connection with the PSK broker process as configured
/// via the `psk_broker_path`, `psk_broker_fd`, and `psk_broker_spawn`
/// fields.
#[cfg(feature = "experiment_api")]
fn get_broker_socket(broker_interface: BrokerInterface) -> Result<UnixStream, anyhow::Error> {
// Connect to the psk broker unix socket if one was specified
@@ -549,7 +605,7 @@ impl CliArgs {
}
/// generate secret and public keys, store in files according to the paths passed as arguments
fn generate_and_save_keypair(secret_key: PathBuf, public_key: PathBuf) -> anyhow::Result<()> {
pub fn generate_and_save_keypair(secret_key: PathBuf, public_key: PathBuf) -> anyhow::Result<()> {
let mut ssk = crate::protocol::SSk::random();
let mut spk = crate::protocol::SPk::random();
StaticKem::keygen(ssk.secret_mut(), spk.deref_mut())?;

View File

@@ -4,8 +4,9 @@
//! [`Rosenpass`] which holds such a configuration.
//!
//! ## TODO
//! - support `~` in <https://github.com/rosenpass/rosenpass/issues/237>
//! - provide tooling to create config file from shell <https://github.com/rosenpass/rosenpass/issues/247>
//! - TODO: support `~` in <https://github.com/rosenpass/rosenpass/issues/237>
//! - TODO: provide tooling to create config file from shell <https://github.com/rosenpass/rosenpass/issues/247>
use crate::protocol::{SPk, SSk};
use rosenpass_util::file::LoadValue;
use std::{
@@ -31,7 +32,10 @@ fn empty_api_config() -> crate::api::config::ApiConfig {
}
}
#[derive(Debug, Serialize, Deserialize)]
/// Configuration for the Rosenpass key exchange
///
/// i.e. configuration for the `rosenpass exchange` and `rosenpass exchange-config` commands
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
pub struct Rosenpass {
// TODO: Raise error if secret key or public key alone is set during deserialization
// SEE: https://github.com/serde-rs/serde/issues/2793
@@ -46,7 +50,10 @@ pub struct Rosenpass {
/// list of [`SocketAddr`] to listen on
///
/// Examples:
/// - `0.0.0.0:123`
///
/// - `0.0.0.0:123` Listen on any interface using IPv4, port 123
/// - `[::1]:1234` Listen on IPv6 localhost, port 1234
/// - `[::]:4476` Listen on any IPv4 or IPv6 interface, port 4476
pub listen: Vec<SocketAddr>,
/// log verbosity
@@ -68,6 +75,7 @@ pub struct Rosenpass {
pub config_file_path: PathBuf,
}
/// Public key and secret key locations.
#[derive(Debug, Deserialize, Serialize, PartialEq, Eq, Clone)]
pub struct Keypair {
/// path to the public key file
@@ -78,6 +86,7 @@ pub struct Keypair {
}
impl Keypair {
/// Construct a keypair from its fields
pub fn new<Pk: AsRef<Path>, Sk: AsRef<Path>>(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();
@@ -88,62 +97,72 @@ impl Keypair {
}
}
/// ## TODO
/// - replace this type with [`log::LevelFilter`], also see <https://github.com/rosenpass/rosenpass/pull/246>
/// Level of verbosity for [crate::app_server::AppServer]
///
/// The value of the field [crate::app_server::AppServer::verbosity]. See the field documentation
/// for details.
///
/// - TODO: replace this type with [`log::LevelFilter`], also see <https://github.com/rosenpass/rosenpass/pull/246>
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize, Copy, Clone)]
pub enum Verbosity {
Quiet,
Verbose,
}
/// ## TODO
/// - examples
/// - documentation
/// Configuration data for a single Rosenpass peer
#[derive(Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct RosenpassPeer {
/// path to the public key of the peer
pub public_key: PathBuf,
/// ## TODO
/// - documentation
/// The hostname and port to connect to
///
/// Can be a
///
/// - hostname and port, e.g. `localhost:8876` or `rosenpass.eu:1427`
/// - IPv4 address and port, e.g. `1.2.3.4:7764`
/// - IPv6 address and port, e.g. `[fe80::24]:7890`
pub endpoint: Option<String>,
/// path to the pre-shared key with the peer
/// path to the pre-shared key shared with the peer
///
/// NOTE: this item can be skipped in the config if you do not use a pre-shared key with the peer
pub pre_shared_key: Option<PathBuf>,
/// ## TODO
/// - documentation
/// If this field is set to a path, the Rosenpass will write the exchanged symmetric keys
/// to the given file and write a notification to standard out to let the calling application
/// know that a new key was exchanged
#[serde(default)]
pub key_out: Option<PathBuf>,
/// ## TODO
/// - documentation
/// - make this field only available on binary builds, not on library builds <https://github.com/rosenpass/rosenpass/issues/249>
/// Information for supplying exchanged keys directly to WireGuard
#[serde(flatten)]
pub wg: Option<WireGuard>,
}
/// ## TODO
/// - documentation
/// Information for supplying exchanged keys directly to WireGuard
#[derive(Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct WireGuard {
/// ## TODO
/// - documentation
/// Name of the WireGuard interface to supply with pre-shared keys generated by the Rosenpass
/// key exchange
pub device: String,
/// ## TODO
/// - documentation
/// WireGuard public key of the peer to supply with pre-shared keys
pub peer: String,
/// ## TODO
/// - documentation
/// Extra parameters passed to the `wg` command
#[serde(default)]
pub extra_params: Vec<String>,
}
impl Default for Rosenpass {
/// Generate an empty configuration
///
/// # Examples
///
#[doc = "```ignore"]
#[doc = include_str!("../tests/config_Rosenpass_new.rs")]
#[doc = "```"]
fn default() -> Self {
Self::empty()
}
@@ -156,8 +175,15 @@ impl Rosenpass {
/// checked whether they even exist.
///
/// ## TODO
///
/// - consider using a different algorithm to determine home directory the below one may
/// behave unexpectedly on Windows
///
/// # Examples
///
#[doc = "```ignore"]
#[doc = include_str!("../tests/config_Rosenpass_store.rs")]
#[doc = "```"]
pub fn load<P: AsRef<Path>>(p: P) -> anyhow::Result<Self> {
// read file and deserialize
let mut config: Self = toml::from_str(&fs::read_to_string(&p)?)?;
@@ -185,7 +211,13 @@ impl Rosenpass {
Ok(config)
}
/// Write a config to a file
/// Encode a configuration object as toml and write it to a file
///
/// # Examples
///
#[doc = "```ignore"]
#[doc = include_str!("../tests/config_Rosenpass_store.rs")]
#[doc = "```"]
pub fn store<P: AsRef<Path>>(&self, p: P) -> anyhow::Result<()> {
let serialized_config =
toml::to_string_pretty(&self).expect("unable to serialize the default config");
@@ -194,6 +226,12 @@ impl Rosenpass {
}
/// Commit the configuration to where it came from, overwriting the original file
///
/// # Examples
///
#[doc = "```ignore"]
#[doc = include_str!("../tests/config_Rosenpass_store.rs")]
#[doc = "```"]
pub fn commit(&self) -> anyhow::Result<()> {
let mut f = fopen_w(&self.config_file_path, Visibility::Public)?;
f.write_all(toml::to_string_pretty(&self)?.as_bytes())?;
@@ -201,13 +239,21 @@ impl Rosenpass {
self.store(&self.config_file_path)
}
/// Apply the configuration in this object to the given [crate::app_server::AppServer]
pub fn apply_to_app_server(&self, _srv: &mut AppServer) -> anyhow::Result<()> {
#[cfg(feature = "experiment_api")]
self.api.apply_to_app_server(_srv)?;
Ok(())
}
/// Validate a configuration
/// Check that the configuration is sound, ensuring
/// for instance that the referenced files exist
///
/// # Examples
///
#[doc = "```ignore"]
#[doc = include_str!("../tests/config_Rosenpass_validate.rs")]
#[doc = "```"]
pub fn validate(&self) -> anyhow::Result<()> {
if let Some(ref keypair) = self.keypair {
// check the public key file exists
@@ -284,6 +330,21 @@ impl Rosenpass {
Ok(())
}
/// Check that the configuration is useful given the feature set Rosenpass was compiled with
/// and the configuration values.
///
/// This was introduced when we introduced a unix-socket API feature allowing the server
/// keypair to be supplied via the API; in this process we also made [Self::keypair] optional.
/// With respect to this particular feature, this function ensures that [Self::keypair] is set
/// when Rosenpass is compiles without the `experiment_api` flag. When `experiment_api` is
/// used, the function ensures that [Self::keypair] is only `None`, if the Rosenpass API is
/// enabled in the configuration.
///
/// # Examples
///
#[doc = "```ignore"]
#[doc = include_str!("../tests/config_Rosenpass_validate.rs")]
#[doc = "```"]
pub fn check_usefullness(&self) -> anyhow::Result<()> {
#[cfg(not(feature = "experiment_api"))]
ensure!(self.keypair.is_some(), "Server keypair missing.");
@@ -299,15 +360,38 @@ impl Rosenpass {
Ok(())
}
/// Produce an empty confuguration
///
/// # Examples
///
#[doc = "```ignore"]
#[doc = include_str!("../tests/config_Rosenpass_new.rs")]
#[doc = "```"]
pub fn empty() -> Self {
Self::new(None)
}
/// Produce configuration from the keypair
///
/// Shorthand for calling [Self::new] with Some([Keypair]::new(sk, pk)).
///
/// # Examples
///
#[doc = "```ignore"]
#[doc = include_str!("../tests/config_Rosenpass_new.rs")]
#[doc = "```"]
pub fn from_sk_pk<Sk: AsRef<Path>, Pk: AsRef<Path>>(sk: Sk, pk: Pk) -> Self {
Self::new(Some(Keypair::new(pk, sk)))
}
/// Creates a new configuration
/// Initialize a minimal configuration with the [Self::keypair] field supplied
/// as a parameter
///
/// # Examples
///
#[doc = "```ignore"]
#[doc = include_str!("../tests/config_Rosenpass_new.rs")]
#[doc = "```"]
pub fn new(keypair: Option<Keypair>) -> Self {
Self {
keypair,
@@ -321,6 +405,14 @@ impl Rosenpass {
}
/// Add IPv4 __and__ IPv6 IF_ANY address to the listen interfaces
///
/// I.e. listen on any interface.
///
/// # Examples
///
#[doc = "```ignore"]
#[doc = include_str!("../tests/config_Rosenpass_add_if_any.rs")]
#[doc = "```"]
pub fn add_if_any(&mut self, port: u16) {
let ipv4_any = SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::new(0, 0, 0, 0), port));
let ipv6_any = SocketAddr::V6(SocketAddrV6::new(
@@ -333,8 +425,20 @@ impl Rosenpass {
self.listen.push(ipv6_any);
}
/// from chaotic args
/// Quest: the grammar is undecideable, what do we do here?
/// Parser for the old, IP style grammar.
///
/// See out manual page rosenpass-exchange(1) on details about the grammar.
///
/// This function parses the grammar and turns it into an instance of the configuration
/// struct.
///
/// TODO: the grammar is undecidable, what do we do here?
///
/// # Examples
///
#[doc = "```ignore"]
#[doc = include_str!("../tests/config_Rosenpass_parse_args_simple.rs")]
#[doc = "```"]
pub fn parse_args(args: Vec<String>) -> anyhow::Result<Self> {
let mut config = Self::new(Some(Keypair::new("", "")));
@@ -525,11 +629,13 @@ impl Rosenpass {
}
impl Default for Verbosity {
/// Self::Quiet
fn default() -> Self {
Self::Quiet
}
}
/// Example configuration generated by the command `rosenpass gen-config <TOML-FILE>`.
pub static EXAMPLE_CONFIG: &str = r###"public_key = "/path/to/rp-public-key"
secret_key = "/path/to/rp-secret-key"
listen = []
@@ -553,7 +659,7 @@ key_out = "/path/to/rp-key-out.txt" # path to store the key
mod test {
use super::*;
use std::{borrow::Borrow, net::IpAddr};
use std::borrow::Borrow;
fn toml_des<S: Borrow<str>>(s: S) -> Result<toml::Table, toml::de::Error> {
toml::from_str(s.borrow())
@@ -664,37 +770,6 @@ mod test {
Ok(())
}
#[test]
fn test_simple_cli_parse() {
let args = split_str(
"public-key /my/public-key secret-key /my/secret-key verbose \
listen 0.0.0.0:9999 peer public-key /peer/public-key endpoint \
peer.test:9999 outfile /peer/rp-out",
);
let config = Rosenpass::parse_args(args).unwrap();
assert_eq!(
config.keypair,
Some(Keypair::new("/my/public-key", "/my/secret-key"))
);
assert_eq!(config.verbosity, Verbosity::Verbose);
assert_eq!(
&config.listen,
&vec![SocketAddr::new(IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)), 9999)]
);
assert_eq!(
config.peers,
vec![RosenpassPeer {
public_key: PathBuf::from("/peer/public-key"),
endpoint: Some("peer.test:9999".into()),
pre_shared_key: None,
key_out: Some(PathBuf::from("/peer/rp-out")),
..Default::default()
}]
)
}
#[test]
fn test_cli_parse_multiple_peers() {
let args = split_str(

View File

@@ -0,0 +1,10 @@
use rosenpass::config::Rosenpass;
#[test]
fn config_Rosenpass_add_if_any_example() {
let mut v = Rosenpass::empty();
v.add_if_any(4000);
assert!(v.listen.iter().any(|a| format!("{a:?}") == "0.0.0.0:4000"));
assert!(v.listen.iter().any(|a| format!("{a:?}") == "[::]:4000"));
}

View File

@@ -0,0 +1,18 @@
use rosenpass::config::{Keypair, Rosenpass};
#[test]
fn example_config_rosenpass_new() {
let (sk, pk) = ("./example.sk", "./example.pk");
assert_eq!(Rosenpass::empty(), Rosenpass::new(None));
assert_eq!(Rosenpass::empty(), Rosenpass::default());
assert_eq!(
Rosenpass::from_sk_pk(sk, pk),
Rosenpass::new(Some(Keypair::new(pk, sk)))
);
let mut v = Rosenpass::empty();
v.keypair = Some(Keypair::new(pk, sk));
assert_eq!(Rosenpass::from_sk_pk(sk, pk), v);
}

View File

@@ -0,0 +1,36 @@
use std::{
net::{IpAddr, Ipv4Addr, SocketAddr},
path::PathBuf,
};
use rosenpass::config::{Keypair, Rosenpass, RosenpassPeer, Verbosity};
#[test]
fn parse_simple() {
let argv = "public-key /my/public-key secret-key /my/secret-key verbose \
listen 0.0.0.0:9999 peer public-key /peer/public-key endpoint \
peer.test:9999 outfile /peer/rp-out";
let argv = argv.split(' ').map(|s| s.to_string()).collect();
let config = Rosenpass::parse_args(argv).unwrap();
assert_eq!(
config.keypair,
Some(Keypair::new("/my/public-key", "/my/secret-key"))
);
assert_eq!(config.verbosity, Verbosity::Verbose);
assert_eq!(
&config.listen,
&vec![SocketAddr::new(IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)), 9999)]
);
assert_eq!(
config.peers,
vec![RosenpassPeer {
public_key: PathBuf::from("/peer/public-key"),
endpoint: Some("peer.test:9999".into()),
pre_shared_key: None,
key_out: Some(PathBuf::from("/peer/rp-out")),
..Default::default()
}]
);
}

View File

@@ -0,0 +1,42 @@
use std::path::PathBuf;
use rosenpass::config::{Rosenpass, Verbosity};
#[test]
fn example_config_rosenpass_store() -> anyhow::Result<()> {
rosenpass_secret_memory::policy::secret_policy_use_only_malloc_secrets();
let tmpdir = tempfile::tempdir()?;
let sk = tmpdir.path().join("example.sk");
let pk = tmpdir.path().join("example.pk");
let cfg = tmpdir.path().join("config.toml");
let mut c = Rosenpass::from_sk_pk(&sk, &pk);
// Can not commit config, path not known
assert!(c.commit().is_err());
// We can store it to an explicit path though
c.store(&cfg)?;
// Storing does not set commitment path
assert!(c.commit().is_err());
// We can reload the config now and the configurations
// are equal if we adjust the commitment path
let mut c2 = Rosenpass::load(&cfg)?;
c.config_file_path = PathBuf::from(&cfg);
assert_eq!(c, c2);
// And this loaded config can now be committed
c2.verbosity = Verbosity::Verbose;
c2.commit()?;
// And the changes actually made it to disk
let c3 = Rosenpass::load(cfg)?;
assert_eq!(c2, c3);
assert_ne!(c, c3);
Ok(())
}

View File

@@ -0,0 +1,37 @@
use std::fs;
use rosenpass::{cli::generate_and_save_keypair, config::Rosenpass};
#[test]
fn example_config_rosenpass_validate() -> anyhow::Result<()> {
rosenpass_secret_memory::policy::secret_policy_use_only_malloc_secrets();
let tmpdir = tempfile::tempdir()?;
// Empty validates OK
assert!(Rosenpass::empty().validate().is_ok());
// Missing secret key does not pass usefulness
assert!(Rosenpass::empty().check_usefullness().is_err());
let sk = tmpdir.path().join("example.sk");
let pk = tmpdir.path().join("example.pk");
let cfg = Rosenpass::from_sk_pk(&sk, &pk);
// Missing secret key does not validate
assert!(cfg.validate().is_err());
// But passes usefulness (the configuration is useful but invalid)
assert!(cfg.check_usefullness().is_ok());
// Providing empty key files does not help
fs::write(&sk, b"")?;
fs::write(&pk, b"")?;
assert!(cfg.validate().is_err());
// But after providing proper key files, the configuration validates
generate_and_save_keypair(sk, pk)?;
assert!(cfg.validate().is_ok());
Ok(())
}