openpgp: Use absolute expiration time in cert builder.

  - The certificate builder is a mid-level interface, and should
    therefore use the more user-friendly way of specifying
    expiration.  Furthermore, with this interface we will be able to
    support setting a new expiration in cases where the keys have
    different creation times.

  - See #429.
parent e2f52c05
......@@ -100,7 +100,7 @@ impl CipherSuite {
#[derive(Clone, Debug)]
pub struct KeyBlueprint {
flags: KeyFlags,
expiration: Option<time::Duration>,
expiration: Option<time::SystemTime>,
}
/// Simplifies generation of Keys.
......@@ -157,7 +157,8 @@ impl CertBuilder {
.set_certification(true)
.set_signing(true),
expiration: Some(
time::Duration::new(3 * 52 * 7 * 24 * 60 * 60, 0)),
time::SystemTime::now()
+ time::Duration::new(3 * 52 * 7 * 24 * 60 * 60, 0)),
},
subkeys: vec![
KeyBlueprint {
......@@ -235,7 +236,7 @@ impl CertBuilder {
/// If `expiration` is `None`, the subkey uses the same expiration
/// time as the primary key.
pub fn add_subkey<T>(mut self, flags: KeyFlags, expiration: T) -> Self
where T: Into<Option<time::Duration>>
where T: Into<Option<time::SystemTime>>
{
self.subkeys.push(KeyBlueprint {
flags: flags,
......@@ -260,8 +261,8 @@ impl CertBuilder {
/// Sets the expiration time.
///
/// A value of None means never.
pub fn set_validity_period<T>(mut self, expiration: T) -> Self
where T: Into<Option<time::Duration>>
pub fn set_expiration_time<T>(mut self, expiration: T) -> Self
where T: Into<Option<time::SystemTime>>
{
self.primary.expiration = expiration.into();
self
......@@ -334,7 +335,8 @@ impl CertBuilder {
.set_hash_algo(HashAlgorithm::SHA512)
.set_features(&Features::sequoia())?
.set_key_flags(flags)?
.set_key_validity_period(
.set_key_expiration_time(
&subkey,
blueprint.expiration.or(self.primary.expiration))?;
if flags.for_transport_encryption() || flags.for_storage_encryption()
......@@ -400,7 +402,7 @@ impl CertBuilder {
.set_features(&Features::sequoia())?
.set_key_flags(&self.primary.flags)?
.set_signature_creation_time(creation_time)?
.set_key_validity_period(self.primary.expiration)?
.set_key_expiration_time(&key, self.primary.expiration)?
.set_issuer_fingerprint(key.fingerprint())?
.set_issuer(key.keyid())?
.set_preferred_hash_algorithms(vec![HashAlgorithm::SHA512])?;
......@@ -581,17 +583,18 @@ mod tests {
fn validity_periods() {
let p = &P::new();
let now = std::time::SystemTime::now();
let s = std::time::Duration::new(1, 0);
let (cert,_) = CertBuilder::new()
.set_validity_period(600 * s)
.set_creation_time(now)
.set_expiration_time(now + 600 * s)
.add_subkey(KeyFlags::default().set_signing(true),
300 * s)
now + 300 * s)
.add_subkey(KeyFlags::default().set_authentication(true),
None)
.generate().unwrap();
let now = cert.primary_key().creation_time()
+ 5 * s; // The subkeys may be created a tad later.
let key = cert.primary_key().key();
let sig = &cert.primary_key().bundle().self_signatures()[0];
assert!(sig.key_alive(key, now).is_ok());
......
......@@ -1961,12 +1961,14 @@ mod test {
}
#[test]
fn set_validity_period_uidless() {
use crate::types::{Duration, Timestamp};
let p = &P::new();
let (cert, _) = CertBuilder::new()
.set_validity_period(None) // Just to assert this works.
.set_validity_period(
Some(crate::types::Duration::weeks(52).unwrap().into()))
.set_expiration_time(None) // Just to assert this works.
.set_expiration_time(
Some(Timestamp::now().checked_add(
Duration::weeks(52).unwrap()).unwrap().into()))
.generate().unwrap();
assert_eq!(cert.clone().into_packet_pile().children().count(),
1 // primary key
......
......@@ -2,6 +2,7 @@ use failure;
use failure::Fail;
use clap::ArgMatches;
use itertools::Itertools;
use std::time::{SystemTime, Duration};
use crate::openpgp::Packet;
use crate::openpgp::cert::{CertBuilder, CipherSuite};
......@@ -11,6 +12,11 @@ use crate::openpgp::serialize::Serialize;
use crate::create_or_stdout;
const SECONDS_IN_DAY : u64 = 24 * 60 * 60;
const SECONDS_IN_YEAR : u64 =
// Average number of days in a year.
(365.2422222 * SECONDS_IN_DAY as f64) as u64;
pub fn generate(m: &ArgMatches, force: bool) -> failure::Fallible<()> {
let mut builder = CertBuilder::new();
......@@ -25,94 +31,27 @@ pub fn generate(m: &ArgMatches, force: bool) -> failure::Fallible<()> {
}
// Expiration.
const SECONDS_IN_DAY : u64 = 24 * 60 * 60;
const SECONDS_IN_YEAR : u64 =
// Average number of days in a year.
(365.2422222 * SECONDS_IN_DAY as f64) as u64;
let even_off = |s| {
if s < 7 * SECONDS_IN_DAY {
// Don't round down, too small.
s
} else {
s - (s % SECONDS_IN_DAY)
}
};
match m.value_of("expiry") {
Some(expiry) if expiry == "never" =>
builder = builder.set_validity_period(None),
Some(expiry) => {
let mut expiry = expiry.chars().peekable();
let _ = expiry.by_ref()
.peeking_take_while(|c| c.is_whitespace())
.for_each(|_| ());
let digits = expiry.by_ref()
.peeking_take_while(|c| {
*c == '+' || *c == '-' || c.is_digit(10)
}).collect::<String>();
let _ = expiry.by_ref()
.peeking_take_while(|c| c.is_whitespace())
.for_each(|_| ());
let suffix = expiry.next();
let _ = expiry.by_ref()
.peeking_take_while(|c| c.is_whitespace())
.for_each(|_| ());
let junk = expiry.collect::<String>();
if digits == "" {
return Err(format_err!(
"--expiry: missing count \
(try: '2y' for 2 years)"));
}
let count = match digits.parse::<i32>() {
Ok(count) if count < 0 =>
return Err(format_err!(
"--expiry: Expiration can't be in the past")),
Ok(count) => count as u64,
Err(err) =>
return Err(err.context(
"--expiry: count is out of range").into()),
};
let factor = match suffix {
Some('y') | Some('Y') => SECONDS_IN_YEAR,
Some('m') | Some('M') => SECONDS_IN_YEAR / 12,
Some('w') | Some('W') => 7 * SECONDS_IN_DAY,
Some('d') | Some('D') => SECONDS_IN_DAY,
None =>
return Err(format_err!(
"--expiry: missing suffix \
(try: '{}y', '{}m', '{}w' or '{}d' instead)",
digits, digits, digits, digits)),
Some(suffix) =>
return Err(format_err!(
"--expiry: invalid suffix '{}' \
(try: '{}y', '{}m', '{}w' or '{}d' instead)",
suffix, digits, digits, digits, digits)),
};
if junk != "" {
return Err(format_err!(
"--expiry: contains trailing junk ('{:?}') \
(try: '{}{}')",
junk, count, factor));
}
builder = builder.set_validity_period(
Some(std::time::Duration::new(even_off(count * factor), 0)));
}
// Not specified. Use the default.
None => {
builder = builder.set_validity_period(
Some(std::time::Duration::new(even_off(3 * SECONDS_IN_YEAR), 0))
);
}
};
match (m.value_of("expires"), m.value_of("expires-in")) {
(None, None) => // Default expiration.
builder = builder.set_expiration_time(
Some(SystemTime::now()
+ Duration::new(3 * SECONDS_IN_YEAR, 0))),
(Some(t), None) if t == "never" =>
builder = builder.set_expiration_time(None),
(Some(t), None) => {
let t =
crate::parse_iso8601(t, chrono::NaiveTime::from_hms(0, 0, 0))?;
builder = builder.set_expiration_time(Some(t.into()));
},
(None, Some(d)) if d == "never" =>
builder = builder.set_expiration_time(None),
(None, Some(d)) => {
let d = parse_duration(d)?;
builder = builder.set_expiration_time(
Some(SystemTime::now() + d));
},
(Some(_), Some(_)) => unreachable!("conflicting args"),
}
// Cipher Suite
match m.value_of("cipher-suite") {
......@@ -243,3 +182,74 @@ pub fn generate(m: &ArgMatches, force: bool) -> failure::Fallible<()> {
Ok(())
}
fn parse_duration(expiry: &str) -> failure::Fallible<Duration> {
let even_off = |s| {
if s < 7 * SECONDS_IN_DAY {
// Don't round down, too small.
s
} else {
s - (s % SECONDS_IN_DAY)
}
};
let mut expiry = expiry.chars().peekable();
let _ = expiry.by_ref()
.peeking_take_while(|c| c.is_whitespace())
.for_each(|_| ());
let digits = expiry.by_ref()
.peeking_take_while(|c| {
*c == '+' || *c == '-' || c.is_digit(10)
}).collect::<String>();
let _ = expiry.by_ref()
.peeking_take_while(|c| c.is_whitespace())
.for_each(|_| ());
let suffix = expiry.next();
let _ = expiry.by_ref()
.peeking_take_while(|c| c.is_whitespace())
.for_each(|_| ());
let junk = expiry.collect::<String>();
if digits == "" {
return Err(format_err!(
"--expiry: missing count \
(try: '2y' for 2 years)"));
}
let count = match digits.parse::<i32>() {
Ok(count) if count < 0 =>
return Err(format_err!(
"--expiry: Expiration can't be in the past")),
Ok(count) => count as u64,
Err(err) =>
return Err(err.context(
"--expiry: count is out of range").into()),
};
let factor = match suffix {
Some('y') | Some('Y') => SECONDS_IN_YEAR,
Some('m') | Some('M') => SECONDS_IN_YEAR / 12,
Some('w') | Some('W') => 7 * SECONDS_IN_DAY,
Some('d') | Some('D') => SECONDS_IN_DAY,
None =>
return Err(format_err!(
"--expiry: missing suffix \
(try: '{}y', '{}m', '{}w' or '{}d' instead)",
digits, digits, digits, digits)),
Some(suffix) =>
return Err(format_err!(
"--expiry: invalid suffix '{}' \
(try: '{}y', '{}m', '{}w' or '{}d' instead)",
suffix, digits, digits, digits, digits)),
};
if junk != "" {
return Err(format_err!(
"--expiry: contains trailing junk ('{:?}') \
(try: '{}{}')",
junk, count, factor));
}
Ok(Duration::new(even_off(count * factor), 0))
}
......@@ -494,8 +494,9 @@
//! transport, storage, universal]
//! -c, --cipher-suite <CIPHER-SUITE> Cryptographic algorithms used for the key. [default: cv25519] [possible
//! values: rsa3k, rsa4k, cv25519]
//! --expiry <EXPIRY> When the key should expire. Either 'N[ymwd]', for N years, months, weeks, or
//! days, or 'never'.
//! --expires <TIME> Absolute time When the key should expire, or 'never'.
//! --expires-in <DURATION> Relative time when the key should expire. Either 'N[ymwd]', for N years,
//! months, weeks, or days, or 'never'.
//! -e, --export <OUTFILE> Exports the key instead of saving it in the store
//! --rev-cert <FILE or -> Sets the output file for the revocation certificate. Default is <OUTFILE>.rev,
//! mandatory if OUTFILE is '-'.
......
......@@ -393,12 +393,21 @@ pub fn build() -> App<'static, 'static> {
.long("with-password")
.help("Prompt for a password to protect the \
generated key with."))
.arg(Arg::with_name("expiry")
.value_name("EXPIRY")
.long("expiry")
.group(ArgGroup::with_name("expiration-group")
.args(&["expires", "expires-in"]))
.arg(Arg::with_name("expires")
.value_name("TIME")
.long("expires")
.help("Absolute time When the key should expire, \
or 'never'."))
.arg(Arg::with_name("expires-in")
.value_name("DURATION")
.long("expires-in")
// Catch negative numbers.
.allow_hyphen_values(true)
.help("When the key should expire. \
.help("Relative time when the key should expire. \
Either 'N[ymwd]', for N years, months, \
weeks, or days, or 'never'."))
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment