Browse Source

Allow to define a custom delay for renewal

pull/39/head
Rodolphe Breard 5 years ago
parent
commit
62db048a46
  1. 1
      CHANGELOG.md
  2. 6
      acmed/src/certificate.rs
  3. 44
      acmed/src/config.rs
  4. 51
      acmed/src/duration.rs
  5. 50
      acmed/src/endpoint.rs
  6. 2
      acmed/src/main.rs
  7. 1
      acmed/src/main_event_loop.rs
  8. 12
      man/en/acmed.toml.5

1
CHANGELOG.md

@ -17,6 +17,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added
- The account key type and signature algorithm can now be specified in the configuration.
- The delay to renew a certificate before its expiration date can be specified in the configuration using the `renew_delay` parameter at either the certificate, endpoint and global level.
### Fixed
- The Makefile now works on FreeBSD. It should also work on other BSD although it has not been tested.

6
acmed/src/certificate.rs

@ -61,6 +61,7 @@ pub struct Certificate {
pub pk_file_group: Option<String>,
pub env: HashMap<String, String>,
pub id: usize,
pub renew_delay: Duration,
}
impl fmt::Display for Certificate {
@ -106,10 +107,7 @@ impl Certificate {
"Certificate expires in {} days",
expires_in.as_secs() / 86400
));
// TODO: allow a custom duration (using time-parse ?)
// 1814400 is 3 weeks (3 * 7 * 24 * 60 * 60)
let renewal_time = Duration::new(1_814_400, 0);
Ok(expires_in <= renewal_time)
Ok(expires_in <= self.renew_delay)
}
fn has_missing_domains(&self, cert: &X509Certificate) -> bool {

44
acmed/src/config.rs

@ -1,4 +1,5 @@
use crate::certificate::Algorithm;
use crate::duration::parse_duration;
use crate::hooks;
use acme_common::error::Error;
use acme_common::to_idna;
@ -9,6 +10,7 @@ use std::fmt;
use std::fs::{self, File};
use std::io::prelude::*;
use std::path::{Path, PathBuf};
use std::time::Duration;
macro_rules! set_cfg_attr {
($to: expr, $from: expr) => {
@ -171,6 +173,16 @@ pub struct GlobalOptions {
pub pk_file_group: Option<String>,
#[serde(default)]
pub env: HashMap<String, String>,
pub renew_delay: Option<String>,
}
impl GlobalOptions {
pub fn get_renew_delay(&self) -> Result<Duration, Error> {
match &self.renew_delay {
Some(d) => parse_duration(&d),
None => Ok(Duration::new(crate::DEFAULT_CERT_RENEW_DELAY, 0)),
}
}
}
#[derive(Clone, Deserialize)]
@ -183,9 +195,20 @@ pub struct Endpoint {
pub rate_limits: Vec<String>,
pub key_type: Option<String>,
pub signature_algorithm: Option<String>,
pub renew_delay: Option<String>,
}
impl Endpoint {
pub fn get_renew_delay(&self, cnf: &Config) -> Result<Duration, Error> {
match &self.renew_delay {
Some(d) => parse_duration(&d),
None => match &cnf.global {
Some(g) => g.get_renew_delay(),
None => Ok(Duration::new(crate::DEFAULT_CERT_RENEW_DELAY, 0)),
},
}
}
fn to_generic(&self, cnf: &Config) -> Result<crate::endpoint::Endpoint, Error> {
let mut limits = vec![];
for rl_name in self.rate_limits.iter() {
@ -277,6 +300,7 @@ pub struct Certificate {
pub hooks: Vec<String>,
#[serde(default)]
pub env: HashMap<String, String>,
pub renew_delay: Option<String>,
}
impl Certificate {
@ -343,16 +367,20 @@ impl Certificate {
crt_directory.to_string()
}
pub fn get_endpoint(&self, cnf: &Config) -> Result<crate::endpoint::Endpoint, Error> {
fn do_get_endpoint(&self, cnf: &Config) -> Result<Endpoint, Error> {
for endpoint in cnf.endpoint.iter() {
if endpoint.name == self.endpoint {
let ep = endpoint.to_generic(cnf)?;
return Ok(ep);
return Ok(endpoint.clone());
}
}
Err(format!("{}: unknown endpoint.", self.endpoint).into())
}
pub fn get_endpoint(&self, cnf: &Config) -> Result<crate::endpoint::Endpoint, Error> {
let endpoint = self.do_get_endpoint(cnf)?;
endpoint.to_generic(cnf)
}
pub fn get_hooks(&self, cnf: &Config) -> Result<Vec<hooks::Hook>, Error> {
let mut res = vec![];
for name in self.hooks.iter() {
@ -361,6 +389,16 @@ impl Certificate {
}
Ok(res)
}
pub fn get_renew_delay(&self, cnf: &Config) -> Result<Duration, Error> {
match &self.renew_delay {
Some(d) => parse_duration(&d),
None => {
let endpoint = self.do_get_endpoint(cnf)?;
endpoint.get_renew_delay(cnf)
}
}
}
}
#[derive(Clone, Debug, Deserialize)]

51
acmed/src/duration.rs

@ -0,0 +1,51 @@
use acme_common::error::Error;
use nom::bytes::complete::take_while_m_n;
use nom::character::complete::digit1;
use nom::combinator::map_res;
use nom::multi::fold_many1;
use nom::IResult;
use std::time::Duration;
fn is_duration_chr(c: char) -> bool {
c == 's' || c == 'm' || c == 'h' || c == 'd' || c == 'w'
}
fn get_multiplicator(input: &str) -> IResult<&str, u64> {
let (input, nb) = take_while_m_n(1, 1, is_duration_chr)(input)?;
let mult = match nb.chars().next() {
Some('s') => 1,
Some('m') => 60,
Some('h') => 3_600,
Some('d') => 86_400,
Some('w') => 604_800,
_ => 0,
};
Ok((input, mult))
}
fn get_duration_part(input: &str) -> IResult<&str, Duration> {
let (input, nb) = map_res(digit1, |s: &str| s.parse::<u64>())(input)?;
let (input, mult) = get_multiplicator(input)?;
Ok((input, Duration::from_secs(nb * mult)))
}
fn get_duration(input: &str) -> IResult<&str, Duration> {
fold_many1(
get_duration_part,
Duration::new(0, 0),
|mut acc: Duration, item| {
acc += item;
acc
},
)(input)
}
pub fn parse_duration(input: &str) -> Result<Duration, Error> {
match get_duration(input) {
Ok((r, d)) => match r.len() {
0 => Ok(d),
_ => Err(format!("{}: invalid duration", input).into()),
},
Err(_) => Err(format!("{}: invalid duration", input).into()),
}
}

50
acmed/src/endpoint.rs

@ -1,11 +1,7 @@
use crate::acme_proto::structs::Directory;
use crate::duration::parse_duration;
use acme_common::crypto::{JwsSignatureAlgorithm, KeyType};
use acme_common::error::Error;
use nom::bytes::complete::take_while_m_n;
use nom::character::complete::digit1;
use nom::combinator::map_res;
use nom::multi::fold_many1;
use nom::IResult;
use std::cmp;
use std::str::FromStr;
use std::thread;
@ -140,47 +136,3 @@ impl RateLimit {
}
}
}
fn is_duration_chr(c: char) -> bool {
c == 's' || c == 'm' || c == 'h' || c == 'd' || c == 'w'
}
fn get_multiplicator(input: &str) -> IResult<&str, u64> {
let (input, nb) = take_while_m_n(1, 1, is_duration_chr)(input)?;
let mult = match nb.chars().next() {
Some('s') => 1,
Some('m') => 60,
Some('h') => 3_600,
Some('d') => 86_400,
Some('w') => 604_800,
_ => 0,
};
Ok((input, mult))
}
fn get_duration_part(input: &str) -> IResult<&str, Duration> {
let (input, nb) = map_res(digit1, |s: &str| s.parse::<u64>())(input)?;
let (input, mult) = get_multiplicator(input)?;
Ok((input, Duration::from_secs(nb * mult)))
}
fn get_duration(input: &str) -> IResult<&str, Duration> {
fold_many1(
get_duration_part,
Duration::new(0, 0),
|mut acc: Duration, item| {
acc += item;
acc
},
)(input)
}
fn parse_duration(input: &str) -> Result<Duration, Error> {
match get_duration(input) {
Ok((r, d)) => match r.len() {
0 => Ok(d),
_ => Err(format!("{}: invalid duration", input).into()),
},
Err(_) => Err(format!("{}: invalid duration", input).into()),
}
}

2
acmed/src/main.rs

@ -6,6 +6,7 @@ use log::error;
mod acme_proto;
mod certificate;
mod config;
mod duration;
mod endpoint;
mod hooks;
mod http;
@ -23,6 +24,7 @@ pub const DEFAULT_CERT_FORMAT: &str = "{{name}}_{{algo}}.{{file_type}}.{{ext}}";
pub const DEFAULT_SLEEP_TIME: u64 = 3600;
pub const DEFAULT_POOL_TIME: u64 = 5000;
pub const DEFAULT_CERT_FILE_MODE: u32 = 0o644;
pub const DEFAULT_CERT_RENEW_DELAY: u64 = 1_814_400; // 1_814_400 is 3 weeks (3 * 7 * 24 * 60 * 60)
pub const DEFAULT_PK_FILE_MODE: u32 = 0o600;
pub const DEFAULT_ACCOUNT_FILE_MODE: u32 = 0o600;
pub const DEFAULT_KP_REUSE: bool = false;

1
acmed/src/main_event_loop.rs

@ -63,6 +63,7 @@ impl MainEventLoop {
pk_file_group: cnf.get_pk_file_group(),
env: crt.env.to_owned(),
id: i + 1,
renew_delay: crt.get_renew_delay(&cnf)?,
};
init_account(&cert, &endpoint)?;
endpoints

12
man/en/acmed.toml.5

@ -62,6 +62,10 @@ for more details.
Specify the group who will own newly-created private-key files. See
.Xr chown 2
for more details.
.It Cm renew_delay Ar string
Period of time between the certificate renewal and its expiration date. The format is described in the
.Sx TIME PERIODS
section. Default is 3w.
.El
.It Ic rate-limit
Array of table where each element defines a HTTPS rate limit.
@ -112,6 +116,10 @@ ES256
.It
ES384
.El
.It Cm renew_delay Ar string
Period of time between the certificate renewal and its expiration date. The format is described in the
.Sx TIME PERIODS
section. Default is the value defined in the global section.
.El
.It Ic hook
Array of table where each element defines a command that will be launched at a defined point. See section
@ -227,6 +235,10 @@ Set whether or not the private key should be reused when renewing the certificat
Path to the directory where certificates and their associated private keys are stored.
.It Ic hooks Ar array
Names of hooks that will be called when requesting a new certificate. The hooks are guaranteed to be called sequentially in the declaration order.
.It Cm renew_delay Ar string
Period of time between the certificate renewal and its expiration date. The format is described in the
.Sx TIME PERIODS
section. Default is the value defined in the associated endpoint.
.El
.Sh WRITING A HOOK
When requesting a certificate from a CA using ACME, there are three steps that are hard to automatize. The first one is solving challenges in order to prove the ownership of every domains to be included: It requires to interact with the configuration of other services, hence depends on how the infrastructure works. The second one is restarting all the services that use a given certificate, for the same reason. The last one is archiving: Although several default methods can be implemented, sometimes admins wants or are required to do it in a different way.

Loading…
Cancel
Save