Move subkey functionality from sq key expire into its own command.

- Split the subkey functionality out of `sq key expire` into its own
    command, `sq key subkey expire`.
This commit is contained in:
Neal H. Walfield 2024-05-30 10:07:53 +02:00
parent 52d88e615e
commit bb3215adfe
No known key found for this signature in database
GPG Key ID: 6863C9AD5B4D22D3
6 changed files with 250 additions and 34 deletions

3
NEWS
View File

@ -41,6 +41,9 @@
specified, the new subkey is saved to the key store.
- In `sq key expire`, change the certificate file parameter from a
positional parameter to a named parameter, `--cert-file`.
- Split the functionality to update a subkey's expiration time off
of `sq key expire` and into `sq key subkey expire`.
- Rename `sq key subkey expire`'s `--subkey` argument to `--key`.
* Changes in 0.36.0
- Missing
* Changes in 0.35.0

View File

@ -1077,6 +1077,7 @@ Add new subkeys to an existing key.
#[non_exhaustive]
pub enum SubkeyCommand {
Add(SubkeyAddCommand),
Expire(SubkeyExpireCommand),
Revoke(SubkeyRevokeCommand),
}
@ -1235,6 +1236,88 @@ certificate.",
pub with_password: bool,
}
const SQ_KEY_SUBKEY_EXPIRE_EXAMPLES: Actions = Actions {
actions: &[
Action::Example(Example {
comment: "Make Bob's authentication subkey expire in six months.",
command: &[
"sq", "key", "subkey", "expire", "6m",
"--cert-file", "bob-secret.pgp",
"--key", "6AEACDD24F896624",
],
}),
],
};
test_examples!(sq_key_subkey_expire, SQ_KEY_SUBKEY_EXPIRE_EXAMPLES);
#[derive(Debug, Args)]
#[clap(
name = "expire",
about = "Change expiration times",
long_about =
"Change expiration times
Change or clear a key's expiration time.
This subcommand changes a key's expiration time. To change the
expiration time of the certificate, use the `sq key expire`
subcommand.
Changing the expiration time of the primary key is equivalent to
changing the certificate's expiration time.
",
after_help = SQ_KEY_SUBKEY_EXPIRE_EXAMPLES,
)]
pub struct SubkeyExpireCommand {
#[clap(
help = FileOrStdout::HELP_OPTIONAL,
long,
short,
value_name = FileOrStdout::VALUE_NAME,
)]
pub output: Option<FileOrStdout>,
#[clap(
short = 'B',
long,
help = "Emit binary data",
)]
pub binary: bool,
#[clap(
long,
help = "Change expiration of this subkey",
required = true,
)]
pub key: Vec<KeyHandle>,
#[clap(
value_name = "EXPIRY",
help =
"Define EXPIRY for the key as ISO 8601 formatted string or \
custom duration.",
long_help =
"Define EXPIRY for the key as ISO 8601 formatted string or \
custom duration. \
If an ISO 8601 formatted string is provided, the validity period \
reaches from the reference time (may be set using `--time`) to \
the provided time. \
Custom durations starting from the reference time may be set using \
`N[ymwds]`, for N years, months, weeks, days, or seconds. \
The special keyword `never` sets an unlimited expiry.",
)]
pub expiry: Expiry,
#[clap(
long,
default_value_t = FileOrStdin::default(),
help = FileOrStdin::HELP_OPTIONAL,
value_name = FileOrStdin::VALUE_NAME,
)]
pub cert_file: FileOrStdin,
}
#[derive(Debug, Args)]
#[clap(
about = "Revoke a subkey",

View File

@ -2,9 +2,6 @@
use clap::Args;
use sequoia_openpgp as openpgp;
use openpgp::KeyHandle;
use crate::cli::types::ClapData;
use crate::cli::types::Expiry;
use crate::cli::types::FileOrStdin;
@ -29,15 +26,6 @@ const EXAMPLES: Actions = Actions {
"--cert-file", "alice-secret.pgp",
],
}),
Action::Example(Example {
comment: "Make Bob's authentication subkey expire in six months.",
command: &[
"sq", "key", "expire", "6m",
"--cert-file", "bob-secret.pgp",
"--subkey", "6AEACDD24F896624",
],
}),
],
};
@ -50,13 +38,11 @@ test_examples!(sq_key_expire, EXAMPLES);
long_about =
"Change expiration times
Keys and their individual subkeys can expire. This subcommand changes
or clears the expiration times.
Change or clear a certificate's expiration time.
By default, the expiration time of the entire key is changed. To
change the expiration of only some of the subkeys, use the `--subkey`
option.
",
This subcommand changes the certificate's expiration time. To change
the expiration time of an individual subkey, use the `sq key subkey
expire` subcommand.",
after_help = EXAMPLES,
)]
pub struct Command {
@ -75,12 +61,6 @@ pub struct Command {
)]
pub binary: bool,
#[clap(
long,
help = "Change expiration of this subkey, not the entire key",
)]
pub subkey: Vec<KeyHandle>,
#[clap(
value_name = "EXPIRY",
help =

View File

@ -8,6 +8,6 @@ use crate::Result;
pub fn dispatch(sq: Sq, command: cli::key::expire::Command)
-> Result<()>
{
expire(sq, command.cert_file, &command.subkey, command.expiry,
expire(sq, command.cert_file, &[], command.expiry,
command.output, command.binary)
}

View File

@ -18,15 +18,36 @@ use openpgp::Result;
use crate::Sq;
use crate::cli::key::SubkeyAddCommand;
use crate::cli::key::SubkeyCommand;
use crate::cli::key::SubkeyExpireCommand;
use crate::cli::key::SubkeyRevokeCommand;
use crate::cli::types::EncryptPurpose;
use crate::cli::types::FileOrStdout;
use crate::common;
use crate::common::expire;
use crate::common::NULL_POLICY;
use crate::common::RevocationOutput;
use crate::common::get_secret_signer;
use crate::parse_notations;
pub fn dispatch(sq: Sq, command: SubkeyCommand) -> Result<()> {
match command {
SubkeyCommand::Add(c) => subkey_add(sq, c)?,
SubkeyCommand::Expire(c) => subkey_expire(sq, c)?,
SubkeyCommand::Revoke(c) => subkey_revoke(sq, c)?,
}
Ok(())
}
fn subkey_expire(sq: Sq, command: SubkeyExpireCommand)
-> Result<()>
{
assert!(! command.key.is_empty());
expire(sq, command.cert_file, &command.key[..], command.expiry,
command.output, command.binary)
}
/// Handle the revocation of a subkey
struct SubkeyRevocation {
cert: Cert,
@ -140,15 +161,6 @@ impl RevocationOutput for SubkeyRevocation {
}
}
pub fn dispatch(sq: Sq, command: SubkeyCommand) -> Result<()> {
match command {
SubkeyCommand::Add(c) => subkey_add(sq, c)?,
SubkeyCommand::Revoke(c) => subkey_revoke(sq, c)?,
}
Ok(())
}
/// Add a new Subkey for an existing primary key
///
/// Creates a subkey with features (e.g. `KeyFlags`, `CipherSuite`) based on

View File

@ -0,0 +1,138 @@
use std::time::Duration;
use openpgp::parse::Parse;
use openpgp::Cert;
use openpgp::Result;
use sequoia_openpgp as openpgp;
mod common;
use common::sq_key_generate;
use common::STANDARD_POLICY;
use common::Sq;
use common::power_set;
use common::time_as_string;
#[test]
fn sq_key_subkey_expire() -> Result<()> {
let (tmpdir, cert_path, time) = sq_key_generate(None)?;
let cert_path = cert_path.display().to_string();
let cert = Cert::from_file(&cert_path)?;
let updated_path = &tmpdir.path().join("updated.pgp");
let updated2_path = &tmpdir.path().join("updated2.pgp");
let keys = cert.keys().map(|k| k.fingerprint()).collect::<Vec<_>>();
for expiring in power_set(&keys) {
let mut sq = Sq::at(time.into());
// Two days go by.
sq.tick(2 * 24 * 60 * 60);
let cert_expiring = expiring.contains(&cert.fingerprint());
for (i, fpr) in keys.iter().enumerate() {
eprintln!(" {}. {}: {}expiring",
i, fpr,
if expiring.contains(&fpr) {
""
} else {
"NOT "
});
}
// Change the key to expire in one day.
let mut cmd = sq.command();
cmd.args([
"--force",
"key", "subkey", "expire", "1d",
"--cert-file", &cert_path,
"--output", &updated_path.to_string_lossy(),
]);
for k in expiring.iter() {
cmd.args(["--key", &k.to_string()]);
}
sq.run(cmd, true);
eprintln!("Updated keys at {} to expire in one day:\n{}",
sq.now_as_string(),
sq.inspect(&updated_path));
let updated = Cert::from_file(&updated_path).expect("valid cert");
// It should be alive now.
let vc = updated.with_policy(STANDARD_POLICY, sq.now()).expect("valid");
for k in vc.keys() {
assert!(k.alive().is_ok());
}
// It should be alive in 1 day minus 1 second.
let t = sq.now() + Duration::new(24 * 60 * 60 - 1, 0);
eprintln!("Checking expiration status at {}", time_as_string(t.into()));
let vc = updated.with_policy(STANDARD_POLICY, t).expect("valid");
for k in vc.keys() {
assert!(k.alive().is_ok());
}
// But in exactly one day, it should be expired.
let t = sq.now() + Duration::new(24 * 60 * 60, 0);
eprintln!("Checking expiration status at {}", time_as_string(t.into()));
let vc = updated.with_policy(STANDARD_POLICY, t).expect("valid");
for k in vc.keys() {
assert_eq!(
cert_expiring || expiring.contains(&k.fingerprint()),
k.alive().is_err(),
"{} is {}alive",
k.fingerprint(),
if k.alive().is_ok() { "" } else { "NOT "});
}
// 12 hours go by. Clear the expiration time.
sq.tick(12 * 60 * 60);
let mut cmd = sq.command();
cmd.args([
"--force",
"key", "subkey", "expire", "never",
"--cert-file", &updated_path.to_string_lossy(),
"--output", &updated2_path.to_string_lossy(),
]);
for k in expiring.iter() {
cmd.args(["--key", &k.to_string()]);
}
sq.run(cmd, true);
let updated = Cert::from_file(&updated2_path).expect("valid cert");
eprintln!("Updated keys at {} to never expire:\n{}",
sq.now_as_string(),
sq.inspect(&updated_path));
// It should be alive now.
let vc = updated.with_policy(STANDARD_POLICY, sq.now())
.expect("valid");
for k in vc.keys() {
assert!(k.alive().is_ok());
}
// It should be alive in 1 day minus 1 second.
let t = sq.now() + Duration::new(24 * 60 * 60 - 1, 0);
eprintln!("Checking expiration status at {}", time_as_string(t.into()));
let vc = updated.with_policy(STANDARD_POLICY, t).expect("valid");
for k in vc.keys() {
assert!(k.alive().is_ok());
}
// And in exactly one day...
let t = sq.now() + Duration::new(24 * 60 * 60, 0);
eprintln!("Checking expiration status at {}", time_as_string(t.into()));
let vc = updated.with_policy(STANDARD_POLICY, t).expect("valid");
for k in vc.keys() {
assert!(k.alive().is_ok());
}
}
tmpdir.close()?;
Ok(())
}