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:
parent
52d88e615e
commit
bb3215adfe
3
NEWS
3
NEWS
@ -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
|
||||
|
@ -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",
|
||||
|
@ -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 =
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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
|
||||
|
138
tests/sq-key-subkey-expire.rs
Normal file
138
tests/sq-key-subkey-expire.rs
Normal 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(())
|
||||
}
|
Loading…
Reference in New Issue
Block a user