From d0783970c889a150db779c8eb314769438b094cf Mon Sep 17 00:00:00 2001 From: Rodolphe Breard Date: Tue, 7 May 2019 22:50:15 +0200 Subject: [PATCH] Renew a certificate that does not include all domains At some point, someone may add new domains to an existing certificate. In such case, this certificate should be renewed as soon as possible instead of upon expiration. --- CHANGELOG.md | 1 + acmed/src/certificate.rs | 70 ++++++++++++++++++++++++++++++++-------- 2 files changed, 57 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e57f85e..8f798ec 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - The project can now be built and installed using `make`. - The post-operation hooks now have access to the `is_success` template variable. - Challenge hooks now have the `is_clean_hook` template variable. +- An existing certificate will be renewed if more domains have been added in the configuration. ### Changed - Unknown configuration fields are no longer tolerated. diff --git a/acmed/src/certificate.rs b/acmed/src/certificate.rs index 047b05d..238f020 100644 --- a/acmed/src/certificate.rs +++ b/acmed/src/certificate.rs @@ -4,9 +4,22 @@ use crate::hooks::{self, ChallengeHookData, Hook, PostOperationHookData}; use crate::storage::{certificate_files_exists, get_certificate}; use acme_common::error::Error; use log::debug; +use openssl::x509::X509; +use std::collections::HashSet; use std::fmt; use time::{strptime, Duration}; +fn parse_openssl_time_string(time: &str) -> Result { + let formats = ["%b %d %T %Y", "%b %d %T %Y", "%b %d %T %Y"]; + for fmt in formats.iter() { + if let Ok(t) = strptime(time, fmt) { + return Ok(t); + } + } + let msg = format!("invalid time string: {}", time); + Err(msg.into()) +} + #[derive(Clone, Debug)] pub enum Algorithm { Rsa2048, @@ -103,26 +116,55 @@ impl Certificate { Err(msg.into()) } + fn is_expiring(&self, cert: &X509) -> Result { + let not_after = cert.not_after().to_string(); + let not_after = parse_openssl_time_string(¬_after)?; + debug!("not after: {}", not_after.asctime()); + // TODO: allow a custom duration (using time-parse ?) + let renewal_time = not_after - Duration::weeks(3); + debug!("renew on: {}", renewal_time.asctime()); + Ok(time::now_utc() > renewal_time) + } + + fn has_missing_domains(&self, cert: &X509) -> bool { + let cert_names = match cert.subject_alt_names() { + Some(s) => s + .iter() + .filter(|v| v.dnsname().is_some()) + .map(|v| v.dnsname().unwrap().to_string()) + .collect(), + None => HashSet::new(), + }; + let req_names = self + .domains + .iter() + .map(|v| v.0.to_owned()) + .collect::>(); + let has_miss = req_names.difference(&cert_names).count() != 0; + if has_miss { + let domains = req_names + .difference(&cert_names) + .map(std::borrow::ToOwned::to_owned) + .collect::>() + .join(", "); + debug!( + "The certificate does not include the following domains: {}", + domains + ); + } + has_miss + } + pub fn should_renew(&self) -> Result { if !certificate_files_exists(&self) { debug!("certificate does not exist: requesting one"); return Ok(true); } let cert = get_certificate(&self)?; - let not_after = cert.not_after().to_string(); - // TODO: check the time format and put it in a const - let not_after = match strptime(¬_after, "%b %d %T %Y") { - Ok(t) => t, - Err(_) => { - let msg = format!("invalid time string: {}", not_after); - return Err(msg.into()); - } - }; - debug!("not after: {}", not_after.asctime()); - // TODO: allow a custom duration (using time-parse ?) - let renewal_time = not_after - Duration::weeks(3); - debug!("renew on: {}", renewal_time.asctime()); - let renew = time::now_utc() > renewal_time; + + let renew = self.has_missing_domains(&cert); + let renew = renew || self.is_expiring(&cert)?; + if renew { debug!("The certificate will be renewed now."); } else {