feat(cli): Automatically generate man page

Instead of using a static one, generate it via clap_mangen. To generate
the manpage run `rosenpass --generate-manpage <folder>`.

Right now clap does not support flattening of generated manpages,
meaning that each subcommand is explained in its own file. To add extra
sections to the main file `rosenpass.1`, it's rewritten after the
initial creation.

Once clap support flattened Man pages, the `generate_to` call can be
removed and all subcommand are added to the `rosenpass.1` file.

This implementation allows downstream manpage generation to stay
unchanged even after switching from multiple manpages to a flattened
one.

Signed-off-by: Paul Spooren <mail@aparcar.org>
This commit is contained in:
Paul Spooren
2024-10-21 16:57:57 +02:00
parent f4ab2ac891
commit 3f9926e353
8 changed files with 107 additions and 191 deletions

View File

@@ -62,8 +62,6 @@ jobs:
- name: Install mandoc - name: Install mandoc
run: sudo apt-get install -y mandoc run: sudo apt-get install -y mandoc
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- name: Check rosenpass.1
run: doc/check.sh doc/rosenpass.1
- name: Check rp.1 - name: Check rp.1
run: doc/check.sh doc/rp.1 run: doc/check.sh doc/rp.1

27
Cargo.lock generated
View File

@@ -401,6 +401,15 @@ dependencies = [
"strsim 0.11.1", "strsim 0.11.1",
] ]
[[package]]
name = "clap_complete"
version = "4.5.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8937760c3f4c60871870b8c3ee5f9b30771f792a7045c48bcbba999d7d6b3b8e"
dependencies = [
"clap 4.5.20",
]
[[package]] [[package]]
name = "clap_derive" name = "clap_derive"
version = "4.5.18" version = "4.5.18"
@@ -428,6 +437,16 @@ version = "0.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1462739cb27611015575c0c11df5df7601141071f07518d56fcc1be504cbec97" checksum = "1462739cb27611015575c0c11df5df7601141071f07518d56fcc1be504cbec97"
[[package]]
name = "clap_mangen"
version = "0.2.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f17415fd4dfbea46e3274fcd8d368284519b358654772afb700dc2e8d2b24eeb"
dependencies = [
"clap 4.5.20",
"roff",
]
[[package]] [[package]]
name = "cmake" name = "cmake"
version = "0.1.51" version = "0.1.51"
@@ -1801,12 +1820,20 @@ version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
[[package]]
name = "roff"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "88f8660c1ff60292143c98d08fc6e2f654d722db50410e3f3797d40baaf9d8f3"
[[package]] [[package]]
name = "rosenpass" name = "rosenpass"
version = "0.3.0-dev" version = "0.3.0-dev"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"clap 4.5.20", "clap 4.5.20",
"clap_complete",
"clap_mangen",
"command-fds", "command-fds",
"criterion", "criterion",
"derive_builder 0.20.2", "derive_builder 0.20.2",

View File

@@ -48,6 +48,8 @@ rand = "0.8.5"
typenum = "1.17.0" typenum = "1.17.0"
log = { version = "0.4.22" } log = { version = "0.4.22" }
clap = { version = "4.5.20", features = ["derive"] } clap = { version = "4.5.20", features = ["derive"] }
clap_mangen = "0.2.23"
clap_complete = "4.5.29"
serde = { version = "1.0.210", features = ["derive"] } serde = { version = "1.0.210", features = ["derive"] }
arbitrary = { version = "1.3.2", features = ["derive"] } arbitrary = { version = "1.3.2", features = ["derive"] }
anyhow = { version = "1.0.89", features = ["backtrace", "std"] } anyhow = { version = "1.0.89", features = ["backtrace", "std"] }

View File

@@ -1,114 +0,0 @@
.Dd $Mdocdate$
.Dt ROSENPASS 1
.Os
.Sh NAME
.Nm rosenpass
.Nd builds post-quantum-secure VPNs
.Sh SYNOPSIS
.Nm
.Op COMMAND
.Op Ar OPTIONS ...
.Op Ar ARGS ...
.Sh DESCRIPTION
.Nm
performs cryptographic key exchanges that are secure against quantum-computers
and then outputs the keys.
These keys can then be passed to various services, such as wireguard or other
vpn services, as pre-shared-keys to achieve security against attackers with
quantum computers.
.Pp
This is a research project and quantum computers are not thought to become
practical in fewer than ten years.
If you are not specifically tasked with developing post-quantum secure systems,
you probably do not need this tool.
.Ss COMMANDS
.Bl -tag -width Ds
.It Ar gen-keys --secret-key <file-path> --public-key <file-path>
Generate a keypair to use in the exchange command later.
Send the public-key file to your communication partner and keep the private-key
file secret!
.It Ar exchange private-key <file-path> public-key <file-path> [ OPTIONS ] PEERS
Start a process to exchange keys with the specified peers.
You should specify at least one peer.
.Pp
Its
.Ar OPTIONS
are as follows:
.Bl -tag -width Ds
.It Ar listen <ip>[:<port>]
Instructs
.Nm
to listen on the specified interface and port.
By default,
.Nm
will listen on all interfaces and select a random port.
.It Ar verbose
Extra logging.
.El
.El
.Ss PEER
Each
.Ar PEER
is defined as follows:
.Qq peer public-key <file-path> [endpoint <ip>[:<port>]] [preshared-key <file-path>] [outfile <file-path>] [wireguard <dev> <peer> <extra_params>]
.Pp
Providing a
.Ar PEER
instructs
.Nm
to exchange keys with the given peer and write the resulting PSK into the given
output file.
You must either specify the outfile or wireguard output option.
.Pp
The parameters of
.Ar PEER
are as follows:
.Bl -tag -width Ds
.It Ar endpoint <ip>[:<port>]
Specifies the address where the peer can be reached.
This will be automatically updated after the first successful key exchange with
the peer.
If this is unspecified, the peer must initiate the connection.
.It Ar preshared-key <file-path>
You may specify a pre-shared key which will be mixed into the final secret.
.It Ar outfile <file-path>
You may specify a file to write the exchanged keys to.
If this option is specified,
.Nm
will write a notification to standard out every time the key is updated.
.It Ar wireguard <dev> <peer> <extra_params>
This allows you to directly specify a wireguard peer to deploy the
pre-shared-key to.
You may specify extra parameters you would pass to
.Qq wg set
besides the preshared-key parameter which is used by
.Nm .
This makes it possible to add peers entirely from
.Nm .
.El
.Sh EXIT STATUS
.Ex -std
.Sh SEE ALSO
.Xr rp 1 ,
.Xr wg 1
.Rs
.%A Karolin Varner
.%A Benjamin Lipp
.%A Wanja Zaeske
.%A Lisa Schmidt
.%D 2023
.%T Rosenpass
.%U https://rosenpass.eu/whitepaper.pdf
.Re
.Sh STANDARDS
This tool is the reference implementation of the Rosenpass protocol, as
specified within the whitepaper referenced above.
.Sh AUTHORS
Rosenpass was created by Karolin Varner, Benjamin Lipp, Wanja Zaeske,
Marei Peischl, Stephan Ajuvo, and Lisa Schmidt.
.Pp
This manual page was written by
.An Clara Engler
.Sh BUGS
The bugs are tracked at
.Lk https://github.com/rosenpass/rosenpass/issues .

View File

@@ -47,6 +47,8 @@ env_logger = { workspace = true }
serde = { workspace = true } serde = { workspace = true }
toml = { workspace = true } toml = { workspace = true }
clap = { workspace = true } clap = { workspace = true }
clap_complete = { workspace = true }
clap_mangen = { workspace = true }
mio = { workspace = true } mio = { workspace = true }
rand = { workspace = true } rand = { workspace = true }
zerocopy = { workspace = true } zerocopy = { workspace = true }

View File

@@ -1,52 +0,0 @@
use anyhow::bail;
use anyhow::Result;
use std::env;
use std::fs::File;
use std::io::Write;
use std::path::PathBuf;
use std::process::Command;
/// Invokes a troff compiler to compile a manual page
fn render_man(compiler: &str, man: &str) -> Result<String> {
let out = Command::new(compiler).args(["-Tascii", man]).output()?;
if !out.status.success() {
bail!("{} returned an error", compiler);
}
Ok(String::from_utf8(out.stdout)?)
}
/// Generates the manual page
fn generate_man() -> String {
// This function is purposely stupid and redundant
let man = render_man("mandoc", "./doc/rosenpass.1");
if let Ok(man) = man {
return man;
}
let man = render_man("groff", "./doc/rosenpass.1");
if let Ok(man) = man {
return man;
}
"Cannot render manual page. Please visit https://rosenpass.eu/docs/manuals/\n".into()
}
fn man() {
let out_dir = PathBuf::from(env::var("OUT_DIR").unwrap());
let man = generate_man();
let path = out_dir.join("rosenpass.1.ascii");
let mut file = File::create(&path).unwrap();
file.write_all(man.as_bytes()).unwrap();
println!("cargo:rustc-env=ROSENPASS_MAN={}", path.display());
}
fn main() {
// For now, rerun the build script on every time, as the build script
// is not very expensive right now.
println!("cargo:rerun-if-changed=./");
man();
}

View File

@@ -41,7 +41,7 @@ pub enum BrokerInterface {
/// struct holding all CLI arguments for `clap` crate to parse /// struct holding all CLI arguments for `clap` crate to parse
#[derive(Parser, Debug)] #[derive(Parser, Debug)]
#[command(author, version, about, long_about)] #[command(author, version, about, long_about, arg_required_else_help = true)]
pub struct CliArgs { pub struct CliArgs {
/// lowest log level to show log messages at higher levels will be omitted /// lowest log level to show log messages at higher levels will be omitted
#[arg(long = "log-level", value_name = "LOG_LEVEL", group = "log-level")] #[arg(long = "log-level", value_name = "LOG_LEVEL", group = "log-level")]
@@ -80,7 +80,15 @@ pub struct CliArgs {
psk_broker_spawn: bool, psk_broker_spawn: bool,
#[command(subcommand)] #[command(subcommand)]
pub command: CliCommand, pub command: Option<CliCommand>,
/// Generate man page
#[clap(long, value_name = "out_dir")]
pub generate_manpage: Option<PathBuf>,
/// Generate completion file for a shell
#[clap(long, value_name = "shell")]
pub print_completions: Option<clap_complete::Shell>,
} }
impl CliArgs { impl CliArgs {
@@ -218,10 +226,6 @@ pub enum CliCommand {
/// Validate a configuration /// Validate a configuration
Validate { config_files: Vec<PathBuf> }, Validate { config_files: Vec<PathBuf> },
/// Show the rosenpass manpage
// TODO make this the default, but only after the manpage has been adjusted once the CLI stabilizes
Man,
} }
impl CliArgs { impl CliArgs {
@@ -236,16 +240,7 @@ impl CliArgs {
) -> anyhow::Result<()> { ) -> anyhow::Result<()> {
use CliCommand::*; use CliCommand::*;
match &self.command { match &self.command {
Man => { Some(GenConfig { config_file, force }) => {
let man_cmd = std::process::Command::new("man")
.args(["1", "rosenpass"])
.status();
if !(man_cmd.is_ok() && man_cmd.unwrap().success()) {
println!(include_str!(env!("ROSENPASS_MAN")));
}
}
GenConfig { config_file, force } => {
ensure!( ensure!(
*force || !config_file.exists(), *force || !config_file.exists(),
"config file {config_file:?} already exists" "config file {config_file:?} already exists"
@@ -255,7 +250,7 @@ impl CliArgs {
} }
// Deprecated - use gen-keys instead // Deprecated - use gen-keys instead
Keygen { args } => { Some(Keygen { args }) => {
log::warn!("The 'keygen' command is deprecated. Please use the 'gen-keys' command instead."); log::warn!("The 'keygen' command is deprecated. Please use the 'gen-keys' command instead.");
let mut public_key: Option<PathBuf> = None; let mut public_key: Option<PathBuf> = None;
@@ -288,12 +283,12 @@ impl CliArgs {
generate_and_save_keypair(secret_key.unwrap(), public_key.unwrap())?; generate_and_save_keypair(secret_key.unwrap(), public_key.unwrap())?;
} }
GenKeys { Some(GenKeys {
config_file, config_file,
public_key, public_key,
secret_key, secret_key,
force, force,
} => { }) => {
// figure out where the key file is specified, in the config file or directly as flag? // figure out where the key file is specified, in the config file or directly as flag?
let (pkf, skf) = match (config_file, public_key, secret_key) { let (pkf, skf) = match (config_file, public_key, secret_key) {
(Some(config_file), _, _) => { (Some(config_file), _, _) => {
@@ -337,7 +332,7 @@ impl CliArgs {
generate_and_save_keypair(skf, pkf)?; generate_and_save_keypair(skf, pkf)?;
} }
ExchangeConfig { config_file } => { Some(ExchangeConfig { config_file }) => {
ensure!( ensure!(
config_file.exists(), config_file.exists(),
"config file '{config_file:?}' does not exist" "config file '{config_file:?}' does not exist"
@@ -351,11 +346,11 @@ impl CliArgs {
Self::event_loop(config, broker_interface, test_helpers)?; Self::event_loop(config, broker_interface, test_helpers)?;
} }
Exchange { Some(Exchange {
first_arg, first_arg,
rest_of_args, rest_of_args,
config_file, config_file,
} => { }) => {
let mut rest_of_args = rest_of_args.clone(); let mut rest_of_args = rest_of_args.clone();
rest_of_args.insert(0, first_arg.clone()); rest_of_args.insert(0, first_arg.clone());
let args = rest_of_args; let args = rest_of_args;
@@ -372,7 +367,7 @@ impl CliArgs {
Self::event_loop(config, broker_interface, test_helpers)?; Self::event_loop(config, broker_interface, test_helpers)?;
} }
Validate { config_files } => { Some(Validate { config_files }) => {
for file in config_files { for file in config_files {
match config::Rosenpass::load(file) { match config::Rosenpass::load(file) {
Ok(config) => { Ok(config) => {
@@ -386,6 +381,8 @@ impl CliArgs {
} }
} }
} }
&None => {} // calp print help if no command is given
} }
Ok(()) Ok(())

View File

@@ -1,13 +1,51 @@
use clap::CommandFactory;
use clap::Parser; use clap::Parser;
use clap_mangen::roff::{roman, Roff};
use log::error; use log::error;
use rosenpass::cli::CliArgs; use rosenpass::cli::CliArgs;
use std::process::exit; use std::process::exit;
fn print_custom_man_section(section: &str, text: &str, file: &mut std::fs::File) {
let mut roff = Roff::default();
roff.control("SH", [section]);
roff.text([roman(text)]);
let _ = roff.to_writer(file);
}
/// Catches errors, prints them through the logger, then exits /// Catches errors, prints them through the logger, then exits
pub fn main() { pub fn main() {
// parse CLI arguments // parse CLI arguments
let args = CliArgs::parse(); let args = CliArgs::parse();
if let Some(shell) = args.print_completions {
let mut cmd = CliArgs::command();
clap_complete::generate(shell, &mut cmd, "rosenpass", &mut std::io::stdout());
return;
}
if let Some(out_dir) = args.generate_manpage {
std::fs::create_dir_all(&out_dir).expect("Failed to create man pages directory");
let cmd = CliArgs::command();
let man = clap_mangen::Man::new(cmd.clone());
let _ = clap_mangen::generate_to(cmd, &out_dir);
let file_path = out_dir.join("rosenpass.1");
let mut file = std::fs::File::create(file_path).expect("Failed to create man page file");
let _ = man.render_title(&mut file);
let _ = man.render_name_section(&mut file);
let _ = man.render_synopsis_section(&mut file);
let _ = man.render_subcommands_section(&mut file);
let _ = man.render_options_section(&mut file);
print_custom_man_section("EXIT STATUS", EXIT_STATUS_MAN, &mut file);
print_custom_man_section("SEE ALSO", SEE_ALSO_MAN, &mut file);
print_custom_man_section("STANDARDS", STANDARDS_MAN, &mut file);
print_custom_man_section("AUTHORS", AUTHORS_MAN, &mut file);
print_custom_man_section("BUGS", BUGS_MAN, &mut file);
return;
}
{ {
use rosenpass_secret_memory as SM; use rosenpass_secret_memory as SM;
#[cfg(feature = "experiment_memfd_secret")] #[cfg(feature = "experiment_memfd_secret")]
@@ -43,3 +81,21 @@ pub fn main() {
} }
} }
} }
static EXIT_STATUS_MAN: &str = r"
The rosenpass utility exits 0 on success, and >0 if an error occurs.";
static SEE_ALSO_MAN: &str = r"
rp(1), wg(1)
Karolin Varner, Benjamin Lipp, Wanja Zaeske, and Lisa Schmidt, Rosenpass, https://rosenpass.eu/whitepaper.pdf, 2023.";
static STANDARDS_MAN: &str = r"
This tool is the reference implementation of the Rosenpass protocol, as
specified within the whitepaper referenced above.";
static AUTHORS_MAN: &str = r"
Rosenpass was created by Karolin Varner, Benjamin Lipp, Wanja Zaeske, Marei
Peischl, Stephan Ajuvo, and Lisa Schmidt.";
static BUGS_MAN: &str = r"
The bugs are tracked at https://github.com/rosenpass/rosenpass/issues.";