Move CLI definition of sq key generate to its own module.

This commit is contained in:
Justus Winter 2024-10-10 13:36:13 +02:00
parent 1a8ff4349a
commit 2bc425e080
No known key found for this signature in database
GPG Key ID: 686F55B4AB2B3386
3 changed files with 275 additions and 251 deletions

View File

@ -7,11 +7,8 @@ use clap::{ValueEnum, ArgGroup, Args, Parser, Subcommand};
use sequoia_openpgp as openpgp;
use openpgp::cert::CipherSuite as SqCipherSuite;
use openpgp::KeyHandle;
use openpgp::packet::UserID;
use openpgp::types::ReasonForRevocation;
use crate::cli::KEY_VALIDITY_DURATION;
use crate::cli::KEY_VALIDITY_IN_YEARS;
use crate::cli::types::ClapData;
use crate::cli::types::EncryptPurpose;
use crate::cli::types::Expiration;
@ -26,6 +23,7 @@ use examples::Setup;
pub mod approvals;
pub mod expire;
pub mod generate;
pub mod revoke;
pub mod subkey;
pub mod userid;
@ -91,7 +89,7 @@ pub struct Command {
#[derive(Debug, Subcommand)]
pub enum Subcommands {
List(ListCommand),
Generate(GenerateCommand),
Generate(generate::Command),
Import(ImportCommand),
Export(ExportCommand),
Delete(DeleteCommand),
@ -127,252 +125,6 @@ test_examples!(sq_key_list, LIST_EXAMPLES);
pub struct ListCommand {
}
const GENERATE_EXAMPLES: Actions = Actions {
actions: &[
Action::Example(Example {
comment: "\
Generate a key, and save it on the key store.",
command: &[
"sq", "key", "generate",
"--without-password",
"--name", "Alice",
"--email", "alice@example.org",
],
}),
Action::Example(Example {
comment: "\
Generate a key, and save it in a file instead of in the key store.",
command: &[
"sq", "key", "generate",
"--without-password",
"--name", "Alice",
"--email", "alice@example.org",
"--output", "alice-priv.pgp",
],
}),
Action::Example(Example {
comment: "\
Strip the secret key material from the new key.",
command: &[
"sq", "toolbox", "extract-cert",
"alice-priv.pgp",
"--output", "alice.pgp",
],
}),
]
};
test_examples!(sq_key_generate, GENERATE_EXAMPLES);
#[derive(Debug, Args)]
#[clap(
about = "Generate a new key",
long_about = format!(
"Generate a new key
Generating a key is the prerequisite to receiving encrypted messages \
and creating signatures. There are a few parameters to this process, \
but we provide reasonable defaults for most users.
When generating a key, we also generate an emergency revocation \
certificate. This can be used in case the key is lost or compromised. \
It is saved alongside the key. This can be changed using the \
`--rev-cert` argument.
By default a key expires after {} years. This can be changed using \
the `--expiration` argument.
`sq key generate` respects the reference time set by the top-level \
`--time` argument. It sets the creation time of the primary key, any \
subkeys, and the binding signatures to the reference time.
",
KEY_VALIDITY_IN_YEARS,
),
after_help = GENERATE_EXAMPLES,
)]
#[clap(group(ArgGroup::new("cap-sign").args(&["can_sign", "cannot_sign"])))]
#[clap(group(ArgGroup::new("cap-authenticate").args(&["can_authenticate", "cannot_authenticate"])))]
#[clap(group(ArgGroup::new("cap-encrypt").args(&["can_encrypt", "cannot_encrypt"])))]
#[clap(group(ArgGroup::new("cert-userid").args(&["names", "emails", "userid", "no_userids"]).required(true).multiple(true)))]
pub struct GenerateCommand {
#[clap(
long = "name",
value_name = "NAME",
help = "Add a name as user ID to the key"
)]
pub names: Vec<String>,
#[clap(
long = "email",
value_name = "ADDRESS",
help = "Add an email address as user ID to the key"
)]
pub emails: Vec<String>,
#[clap(
long = "userid",
value_name = "USERID",
help = "Add a user ID to the key",
long_help = "
Add a user ID to the key.
This user ID can combine name and email address, can optionally
contain a comment, or even be free-form if
`--allow-non-canonical-userids` is given. However, user IDs that
include different information such as name and email address are more
difficult to reason about, so using distinct user IDs for name and
email address is preferred nowadays.
In doubt, prefer `--name` and `--email`.
",
)]
pub userid: Vec<UserID>,
#[clap(
long = "allow-non-canonical-userids",
help = "Don't reject user IDs that are not in canonical form",
long_help = "\
Don't reject user IDs that are not in canonical form.
Canonical user IDs are of the form `Name (Comment) \
<localpart@example.org>`.",
)]
pub allow_non_canonical_userids: bool,
#[clap(
long = "no-userids",
help = "Create a key without any user IDs",
conflicts_with_all = ["names", "emails", "userid"],
)]
pub no_userids: bool,
#[clap(
long = "cipher-suite",
value_name = "CIPHER-SUITE",
default_value_t = Default::default(),
help = "Select the cryptographic algorithms for the key",
value_enum,
)]
pub cipher_suite: CipherSuite,
#[clap(
long = "new-password-file",
value_name = "PASSWORD_FILE",
help = "\
File containing password to encrypt the secret key material",
long_help = "\
File containing password to encrypt the secret key material.
Note that the entire key file will be used as the password including \
any surrounding whitespace like a trailing newline.",
conflicts_with = "without_password",
)]
pub new_password_file: Option<PathBuf>,
#[clap(
long = "without-password",
help = "Don't protect the secret key material with a password",
)]
pub without_password: bool,
#[clap(
long = "expiration",
value_name = "EXPIRATION",
default_value_t = Expiration::Duration(KEY_VALIDITY_DURATION),
help =
"Sets the certificate's expiration time",
long_help = "\
Sets the certificate's expiration time.
EXPIRATION is either an ISO 8601 formatted string or a custom duration, \
which takes the form `N[ymwds]`, where the letters stand for years, \
months, weeks, days, and seconds, respectively. Alternatively, the \
keyword `never` does not set an expiration time.
When using an ISO 8601 formatted string, the validity period is from \
the certificate's creation time to the specified time. When using a \
duration, the validity period is from the certificate's creation time \
for the specified duration.",
)]
pub expiration: Expiration,
#[clap(
long = "can-sign",
help ="Add a signing-capable subkey (default)",
)]
pub can_sign: bool,
#[clap(
long = "cannot-sign",
help = "Don't add a signing-capable subkey",
)]
pub cannot_sign: bool,
#[clap(
long = "can-authenticate",
help = "Add an authentication-capable subkey (default)",
)]
pub can_authenticate: bool,
#[clap(
long = "cannot-authenticate",
help = "Don't add an authentication-capable subkey",
)]
pub cannot_authenticate: bool,
#[clap(
long = "can-encrypt",
value_name = "PURPOSE",
help = "Add an encryption-capable subkey [default: universal]",
long_help = "\
Add an encryption-capable subkey.
Encryption-capable subkeys can be marked as suitable for transport \
encryption, storage encryption, or both, i.e., universal. [default: \
universal]",
value_enum,
)]
pub can_encrypt: Option<EncryptPurpose>,
#[clap(
long = "cannot-encrypt",
help = "Don't add an encryption-capable subkey",
)]
pub cannot_encrypt: bool,
#[clap(
long,
value_name = FileOrStdout::VALUE_NAME,
help = "Write the key to the specified file",
long_help = "\
Write the key to the specified file.
When not specified, the key is saved on the key store.",
)]
pub output: Option<FileOrStdout>,
#[clap(
long = "rev-cert",
value_name = "FILE",
help = "Write the emergency revocation certificate to FILE",
long_help = format!("\
Write the emergency revocation certificate to FILE.
When the key is stored on the key store, the revocation certificate is \
stored in {} by default.
When `--output` is specified, the revocation certificate is written to \
`FILE.rev` by default.
If `--output` is `-`, then this option must be provided.",
sequoia_directories::Home::default()
.map(|home| {
let p = home.data_dir(sequoia_directories::Component::Other(
"revocation-certificates".into()));
let p = p.display().to_string();
if let Some(home) = dirs::home_dir() {
let home = home.display().to_string();
if let Some(rest) = p.strip_prefix(&home) {
return format!("$HOME{}", rest);
}
}
p
})
.unwrap_or("<unknown>".to_string()))
)]
pub rev_cert: Option<PathBuf>
}
#[derive(ValueEnum, Clone, Debug, Default)]
pub enum CipherSuite {
Rsa2k,

272
src/cli/key/generate.rs Normal file
View File

@ -0,0 +1,272 @@
use std::path::PathBuf;
use clap::{ArgGroup, Args};
use sequoia_openpgp as openpgp;
use openpgp::packet::UserID;
use crate::cli::KEY_VALIDITY_DURATION;
use crate::cli::KEY_VALIDITY_IN_YEARS;
use crate::cli::types::ClapData;
use crate::cli::types::EncryptPurpose;
use crate::cli::types::Expiration;
use crate::cli::types::FileOrStdout;
use crate::cli::examples::*;
use crate::cli::key::CipherSuite;
#[derive(Debug, Args)]
#[clap(
about = "Generate a new key",
long_about = format!(
"Generate a new key
Generating a key is the prerequisite to receiving encrypted messages \
and creating signatures. There are a few parameters to this process, \
but we provide reasonable defaults for most users.
When generating a key, we also generate an emergency revocation \
certificate. This can be used in case the key is lost or compromised. \
It is saved alongside the key. This can be changed using the \
`--rev-cert` argument.
By default a key expires after {} years. This can be changed using \
the `--expiration` argument.
`sq key generate` respects the reference time set by the top-level \
`--time` argument. It sets the creation time of the primary key, any \
subkeys, and the binding signatures to the reference time.
",
KEY_VALIDITY_IN_YEARS,
),
after_help = GENERATE_EXAMPLES,
)]
#[clap(group(ArgGroup::new("cap-sign").args(&["can_sign", "cannot_sign"])))]
#[clap(group(ArgGroup::new("cap-authenticate").args(&["can_authenticate", "cannot_authenticate"])))]
#[clap(group(ArgGroup::new("cap-encrypt").args(&["can_encrypt", "cannot_encrypt"])))]
#[clap(group(ArgGroup::new("cert-userid").args(&["names", "emails", "userid", "no_userids"]).required(true).multiple(true)))]
pub struct Command {
#[clap(
long = "name",
value_name = "NAME",
help = "Add a name as user ID to the key"
)]
pub names: Vec<String>,
#[clap(
long = "email",
value_name = "ADDRESS",
help = "Add an email address as user ID to the key"
)]
pub emails: Vec<String>,
#[clap(
long = "userid",
value_name = "USERID",
help = "Add a user ID to the key",
long_help = "
Add a user ID to the key.
This user ID can combine name and email address, can optionally
contain a comment, or even be free-form if
`--allow-non-canonical-userids` is given. However, user IDs that
include different information such as name and email address are more
difficult to reason about, so using distinct user IDs for name and
email address is preferred nowadays.
In doubt, prefer `--name` and `--email`.
",
)]
pub userid: Vec<UserID>,
#[clap(
long = "allow-non-canonical-userids",
help = "Don't reject user IDs that are not in canonical form",
long_help = "\
Don't reject user IDs that are not in canonical form.
Canonical user IDs are of the form `Name (Comment) \
<localpart@example.org>`.",
)]
pub allow_non_canonical_userids: bool,
#[clap(
long = "no-userids",
help = "Create a key without any user IDs",
conflicts_with_all = ["names", "emails", "userid"],
)]
pub no_userids: bool,
#[clap(
long = "cipher-suite",
value_name = "CIPHER-SUITE",
default_value_t = Default::default(),
help = "Select the cryptographic algorithms for the key",
value_enum,
)]
pub cipher_suite: CipherSuite,
#[clap(
long = "new-password-file",
value_name = "PASSWORD_FILE",
help = "\
File containing password to encrypt the secret key material",
long_help = "\
File containing password to encrypt the secret key material.
Note that the entire key file will be used as the password including \
any surrounding whitespace like a trailing newline.",
conflicts_with = "without_password",
)]
pub new_password_file: Option<PathBuf>,
#[clap(
long = "without-password",
help = "Don't protect the secret key material with a password",
)]
pub without_password: bool,
#[clap(
long = "expiration",
value_name = "EXPIRATION",
default_value_t = Expiration::Duration(KEY_VALIDITY_DURATION),
help =
"Sets the certificate's expiration time",
long_help = "\
Sets the certificate's expiration time.
EXPIRATION is either an ISO 8601 formatted string or a custom duration, \
which takes the form `N[ymwds]`, where the letters stand for years, \
months, weeks, days, and seconds, respectively. Alternatively, the \
keyword `never` does not set an expiration time.
When using an ISO 8601 formatted string, the validity period is from \
the certificate's creation time to the specified time. When using a \
duration, the validity period is from the certificate's creation time \
for the specified duration.",
)]
pub expiration: Expiration,
#[clap(
long = "can-sign",
help ="Add a signing-capable subkey (default)",
)]
pub can_sign: bool,
#[clap(
long = "cannot-sign",
help = "Don't add a signing-capable subkey",
)]
pub cannot_sign: bool,
#[clap(
long = "can-authenticate",
help = "Add an authentication-capable subkey (default)",
)]
pub can_authenticate: bool,
#[clap(
long = "cannot-authenticate",
help = "Don't add an authentication-capable subkey",
)]
pub cannot_authenticate: bool,
#[clap(
long = "can-encrypt",
value_name = "PURPOSE",
help = "Add an encryption-capable subkey [default: universal]",
long_help = "\
Add an encryption-capable subkey.
Encryption-capable subkeys can be marked as suitable for transport \
encryption, storage encryption, or both, i.e., universal. [default: \
universal]",
value_enum,
)]
pub can_encrypt: Option<EncryptPurpose>,
#[clap(
long = "cannot-encrypt",
help = "Don't add an encryption-capable subkey",
)]
pub cannot_encrypt: bool,
#[clap(
long,
value_name = FileOrStdout::VALUE_NAME,
help = "Write the key to the specified file",
long_help = "\
Write the key to the specified file.
When not specified, the key is saved on the key store.",
)]
pub output: Option<FileOrStdout>,
#[clap(
long = "rev-cert",
value_name = "FILE",
help = "Write the emergency revocation certificate to FILE",
long_help = format!("\
Write the emergency revocation certificate to FILE.
When the key is stored on the key store, the revocation certificate is \
stored in {} by default.
When `--output` is specified, the revocation certificate is written to \
`FILE.rev` by default.
If `--output` is `-`, then this option must be provided.",
sequoia_directories::Home::default()
.map(|home| {
let p = home.data_dir(sequoia_directories::Component::Other(
"revocation-certificates".into()));
let p = p.display().to_string();
if let Some(home) = dirs::home_dir() {
let home = home.display().to_string();
if let Some(rest) = p.strip_prefix(&home) {
return format!("$HOME{}", rest);
}
}
p
})
.unwrap_or("<unknown>".to_string()))
)]
pub rev_cert: Option<PathBuf>
}
const GENERATE_EXAMPLES: Actions = Actions {
actions: &[
Action::Example(Example {
comment: "\
Generate a key, and save it on the key store.",
command: &[
"sq", "key", "generate",
"--without-password",
"--name", "Alice",
"--email", "alice@example.org",
],
}),
Action::Example(Example {
comment: "\
Generate a key, and save it in a file instead of in the key store.",
command: &[
"sq", "key", "generate",
"--without-password",
"--name", "Alice",
"--email", "alice@example.org",
"--output", "alice-priv.pgp",
],
}),
Action::Example(Example {
comment: "\
Strip the secret key material from the new key.",
command: &[
"sq", "toolbox", "extract-cert",
"alice-priv.pgp",
"--output", "alice.pgp",
],
}),
]
};
test_examples!(sq_key_generate, GENERATE_EXAMPLES);

View File

@ -23,7 +23,7 @@ use crate::commands::inspect::inspect;
pub fn generate(
mut sq: Sq,
mut command: cli::key::GenerateCommand,
mut command: cli::key::generate::Command,
) -> Result<()> {
let mut builder = CertBuilder::new();