use std::{io::Cursor, sync::LazyLock};
use clap::{ArgAction, Parser, Subcommand};
use getset::{CopyGetters, Getters};
use vergen_pretty::{Pretty, vergen_pretty_env};
static LONG_VERSION: LazyLock<String> = LazyLock::new(|| {
let pretty = Pretty::builder().env(vergen_pretty_env!()).build();
let mut cursor = Cursor::new(vec![]);
let mut output = env!("CARGO_PKG_VERSION").to_string();
output.push_str("\n\n");
pretty
.display(&mut cursor)
.expect("writing to Vec never fails");
output += &String::from_utf8_lossy(cursor.get_ref());
output
});
#[derive(Clone, CopyGetters, Debug, Getters, Parser)]
#[command(author, version, about, long_version = LONG_VERSION.as_str(), long_about = None)]
pub(crate) struct Cli {
#[clap(
short,
long,
action = ArgAction::Count,
help = "Turn up logging verbosity (multiple will turn it up more)",
conflicts_with = "quiet",
)]
#[getset(get_copy = "pub(crate)")]
verbose: u8,
#[clap(
short,
long,
action = ArgAction::Count,
help = "Turn down logging verbosity (multiple will turn it down more)",
conflicts_with = "verbose",
)]
#[getset(get_copy = "pub(crate)")]
quiet: u8,
#[command(subcommand)]
#[getset(get = "pub(crate)")]
command: Commands,
}
#[derive(Clone, Debug, Subcommand)]
pub(crate) enum Commands {
#[clap(about = "Generate a new identity public/private key pair")]
Generate {
#[clap(
short = 'n',
long,
help = "Skip the passphrase prompt and create an unencrypted key",
default_value_t = false
)]
no_passphrase: bool,
#[clap(
short = 'o',
long,
help = "Write keys to this path (skips the interactive path prompt)"
)]
output_path: Option<String>,
#[clap(
short = 'f',
long,
help = "Overwrite existing key files without confirmation",
default_value_t = false
)]
force: bool,
#[clap(
short = 's',
long,
help = "Generate a server host key (allows unencrypted keys)",
default_value_t = false
)]
server: bool,
#[clap(
long,
help = "Read passphrase from stdin instead of prompting",
default_value_t = false,
conflicts_with = "no_passphrase"
)]
passphrase_stdin: bool,
#[clap(
short = 'k',
long,
value_name = "TYPE",
default_value = "x25519",
help = "Identity key algorithm: x25519 (default), p384, p256; with unstable: mldsa44, mldsa65, mldsa87"
)]
key_type: String,
},
#[clap(about = "Verify a public key fingerprint or randomart image")]
Verify {
#[clap(short, long, help = "Verify randomart", default_value_t = false)]
randomart: bool,
#[clap(help = "The signature or randomart to verify")]
signature: String,
},
#[clap(about = "Display the fingerprint of the given public key")]
Fingerprint {
#[clap(help = "The public key file path")]
public_key: String,
},
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn verify_cli() {
use clap::CommandFactory;
<Cli as CommandFactory>::command().debug_assert();
}
#[test]
fn verify_generate_command() -> anyhow::Result<()> {
let args = vec!["moshpit-keygen", "generate"];
let cli = Cli::try_parse_from(args)?;
assert!(matches!(
cli.command(),
Commands::Generate {
no_passphrase: false,
..
}
));
assert_eq!(cli.verbose(), 0);
assert_eq!(cli.quiet(), 0);
Ok(())
}
#[test]
fn verify_generate_no_passphrase_flag() -> anyhow::Result<()> {
let args = vec!["moshpit-keygen", "generate", "--no-passphrase"];
let cli = Cli::try_parse_from(args)?;
assert!(matches!(
cli.command(),
Commands::Generate {
no_passphrase: true,
..
}
));
Ok(())
}
#[test]
fn verify_generate_no_passphrase_short_flag() -> anyhow::Result<()> {
let args = vec!["moshpit-keygen", "generate", "-n"];
let cli = Cli::try_parse_from(args)?;
assert!(matches!(
cli.command(),
Commands::Generate {
no_passphrase: true,
..
}
));
Ok(())
}
#[test]
fn verify_generate_output_path_flag() -> anyhow::Result<()> {
let args = vec!["moshpit-keygen", "generate", "--output-path", "/tmp/key"];
let cli = Cli::try_parse_from(args)?;
match cli.command() {
Commands::Generate { output_path, .. } => {
assert_eq!(output_path.as_deref(), Some("/tmp/key"));
}
_ => panic!("Expected Generate command"),
}
Ok(())
}
#[test]
fn verify_generate_force_flag() -> anyhow::Result<()> {
let args = vec!["moshpit-keygen", "generate", "--force"];
let cli = Cli::try_parse_from(args)?;
match cli.command() {
Commands::Generate { force, .. } => {
assert!(force);
}
_ => panic!("Expected Generate command"),
}
Ok(())
}
#[test]
fn verify_generate_server_flag() -> anyhow::Result<()> {
let args = vec!["moshpit-keygen", "generate", "--server"];
let cli = Cli::try_parse_from(args)?;
match cli.command() {
Commands::Generate { server, .. } => {
assert!(server);
}
_ => panic!("Expected Generate command"),
}
Ok(())
}
#[test]
fn verify_generate_key_type_flag() -> anyhow::Result<()> {
let args = vec!["moshpit-keygen", "generate"];
let cli = Cli::try_parse_from(args)?;
match cli.command() {
Commands::Generate { key_type, .. } => {
assert_eq!(key_type, "x25519");
}
_ => panic!("Expected Generate command"),
}
let args = vec!["moshpit-keygen", "generate", "--key-type", "p384"];
let cli = Cli::try_parse_from(args)?;
match cli.command() {
Commands::Generate { key_type, .. } => {
assert_eq!(key_type, "p384");
}
_ => panic!("Expected Generate command"),
}
let args = vec!["moshpit-keygen", "generate", "-k", "p256"];
let cli = Cli::try_parse_from(args)?;
match cli.command() {
Commands::Generate { key_type, .. } => {
assert_eq!(key_type, "p256");
}
_ => panic!("Expected Generate command"),
}
Ok(())
}
#[test]
fn verify_verify_command() -> anyhow::Result<()> {
let args = vec!["moshpit-keygen", "verify", "--randomart", "dummy_sig"];
let cli = Cli::try_parse_from(args)?;
match cli.command() {
Commands::Verify {
randomart,
signature,
} => {
assert!(randomart);
assert_eq!(signature, "dummy_sig");
}
_ => panic!("Expected Verify command"),
}
Ok(())
}
#[test]
fn verify_fingerprint_command() -> anyhow::Result<()> {
let args = vec!["moshpit-keygen", "fingerprint", "dummy_path"];
let cli = Cli::try_parse_from(args)?;
match cli.command() {
Commands::Fingerprint { public_key } => {
assert_eq!(public_key, "dummy_path");
}
_ => panic!("Expected Fingerprint command"),
}
Ok(())
}
#[test]
fn verify_verbose_quiet_flags() -> anyhow::Result<()> {
let args = vec!["moshpit-keygen", "-vv", "generate"];
let cli = Cli::try_parse_from(args)?;
assert_eq!(cli.verbose(), 2);
assert_eq!(cli.quiet(), 0);
let args2 = vec!["moshpit-keygen", "-q", "generate"];
let cli2 = Cli::try_parse_from(args2)?;
assert_eq!(cli2.verbose(), 0);
assert_eq!(cli2.quiet(), 1);
Ok(())
}
}