From f4f339b8c25f15567634f4bbcf320f7e403ded82 Mon Sep 17 00:00:00 2001 From: Rodolphe Breard Date: Thu, 21 Mar 2019 23:36:06 +0100 Subject: [PATCH] Add hooks before and after a file is created or edited It is considered a good practice to archive old certificates and private keys instead of simply dropping them away. Because ACMEd should not impose a way of doing things to system administrators, hooks are the way to go. --- CHANGELOG.md | 1 + README.md | 3 ++- acmed/src/acmed.rs | 62 +++++-------------------------------------- acmed/src/config.rs | 63 +++++++++++++++++++++++++++++++++----------- acmed/src/hooks.rs | 57 +++++++++++++++++++++++++++++++++++++++ acmed/src/main.rs | 1 + acmed/src/storage.rs | 59 ++++++++++++++++++++++++++++++++--------- 7 files changed, 162 insertions(+), 84 deletions(-) create mode 100644 acmed/src/hooks.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 0787016..2725e4d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - The `kp_reuse` flag allow to reuse a key pair instead of creating a new one at each renewal. - It is now possible to define hook groups that can reference either hooks or other hook groups. +- Hooks can be defined when before and after a file is created or edited (`file_pre_create_hooks`, `file_post_create_hooks`, `file_pre_edit_hooks` and `file_post_edit_hooks`). ### Changed - `post_operation_hook` has been renamed `post_operation_hooks`. diff --git a/README.md b/README.md index d804274..df48dbc 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,8 @@ The Automatic Certificate Management Environment (ACME), is an internet standard - HTTP-01 and DNS-01 challenges - RSA 2048, RSA 4096, ECDSA P-256 and ECDSA P-384 certificates -- Fully customizable challenge validation action +- Fully customizable challenge validation action +- Fully customizable archiving method (yes, you can use git or anything else) - Run as a deamon: no need to set-up timers, crontab or other time-triggered process - Nice and simple configuration file diff --git a/acmed/src/acmed.rs b/acmed/src/acmed.rs index b31fb4d..fb3e7a1 100644 --- a/acmed/src/acmed.rs +++ b/acmed/src/acmed.rs @@ -1,15 +1,12 @@ use acme_lib::{Directory, DirectoryUrl}; use crate::config::{self, Hook}; use crate::errors::Error; +use crate::hooks; use crate::storage::Storage; -use handlebars::Handlebars; use log::{debug, info, warn}; use openssl; use serde::Serialize; use std::{fmt, thread}; -use std::fs::File; -use std::io::Write; -use std::process::{Command, Stdio}; use std::time::Duration; use x509_parser::parse_x509_der; @@ -100,51 +97,6 @@ struct HookData { proof: String, } -macro_rules! get_hook_output { - ($out: expr, $reg: ident, $data: expr) => {{ - match $out { - Some(path) => { - let path = $reg.render_template(path, $data)?; - let file = File::create(path)?; - Stdio::from(file) - } - None => Stdio::null(), - } - }}; -} - -impl HookData { - pub fn call(&self, hook: &Hook) -> Result<(), Error> { - let reg = Handlebars::new(); - let mut v = vec![]; - let args = match &hook.args { - Some(lst) => { - for fmt in lst.iter() { - let s = reg.render_template(fmt, &self)?; - v.push(s); - } - v.as_slice() - } - None => &[], - }; - let mut cmd = Command::new(&hook.cmd) - .args(args) - .stdout(get_hook_output!(&hook.stdout, reg, &self)) - .stderr(get_hook_output!(&hook.stderr, reg, &self)) - .stdin(match &hook.stdin { - Some(_) => Stdio::piped(), - None => Stdio::null(), - }) - .spawn()?; - if hook.stdin.is_some() { - let data_in = reg.render_template(&hook.stdin.to_owned().unwrap(), &self)?; - let stdin = cmd.stdin.as_mut().ok_or("stdin not found")?; - stdin.write_all(data_in.as_bytes())?; - } - Ok(()) - } -} - #[derive(Debug)] struct Certificate { domains: Vec, @@ -210,9 +162,7 @@ impl Certificate { token: token.to_string(), proof: proof.to_string(), }; - for hook in self.challenge_hooks.iter() { - hook_data.call(&hook)?; - } + hooks::call_multiple(&hook_data, &self.challenge_hooks)?; Ok(()) } @@ -226,9 +176,7 @@ impl Certificate { token: "".to_string(), proof: "".to_string(), }; - for hook in self.post_operation_hooks.iter() { - hook_data.call(&hook)?; - } + hooks::call_multiple(&hook_data, &self.post_operation_hooks)?; Ok(()) } @@ -326,6 +274,10 @@ impl Acmed { pk_file_mode: cnf.get_pk_file_mode(), pk_file_owner: cnf.get_pk_file_user(), pk_file_group: cnf.get_pk_file_group(), + file_pre_create_hooks: crt.get_file_pre_create_hooks(&cnf)?, + file_post_create_hooks: crt.get_file_post_create_hooks(&cnf)?, + file_pre_edit_hooks: crt.get_file_pre_edit_hooks(&cnf)?, + file_post_edit_hooks: crt.get_file_post_edit_hooks(&cnf)?, }, email: crt.email.to_owned(), remote_url: crt.get_remote_url(&cnf)?, diff --git a/acmed/src/config.rs b/acmed/src/config.rs index d92b9bb..b720562 100644 --- a/acmed/src/config.rs +++ b/acmed/src/config.rs @@ -135,14 +135,18 @@ pub struct Certificate { pub endpoint: String, pub domains: Vec, pub challenge: String, - pub challenge_hooks: Vec, - pub post_operation_hooks: Option>, pub algorithm: Option, pub kp_reuse: Option, pub directory: Option, pub name: Option, pub name_format: Option, pub formats: Option>, + pub challenge_hooks: Vec, + pub post_operation_hooks: Option>, + pub file_pre_create_hooks: Option>, + pub file_post_create_hooks: Option>, + pub file_pre_edit_hooks: Option>, + pub file_post_edit_hooks: Option>, } impl Certificate { @@ -223,25 +227,52 @@ impl Certificate { } pub fn get_challenge_hooks(&self, cnf: &Config) -> Result, Error> { - let mut res = vec![]; - for name in self.challenge_hooks.iter() { - let mut h = cnf.get_hook(&name)?; - res.append(&mut h); - } - Ok(res) + get_hooks(&self.challenge_hooks, cnf) } pub fn get_post_operation_hooks(&self, cnf: &Config) -> Result, Error> { - let mut res = vec![]; match &self.post_operation_hooks { - Some(po_hooks) => for name in po_hooks.iter() { - let mut h = cnf.get_hook(&name)?; - res.append(&mut h); - }, - None => {} - }; - Ok(res) + Some(hooks) => get_hooks(hooks, cnf), + None => Ok(vec![]), + } + } + + pub fn get_file_pre_create_hooks(&self, cnf: &Config) -> Result, Error> { + match &self.file_pre_create_hooks { + Some(hooks) => get_hooks(hooks, cnf), + None => Ok(vec![]), + } + } + + pub fn get_file_post_create_hooks(&self, cnf: &Config) -> Result, Error> { + match &self.file_post_create_hooks { + Some(hooks) => get_hooks(hooks, cnf), + None => Ok(vec![]), + } + } + + pub fn get_file_pre_edit_hooks(&self, cnf: &Config) -> Result, Error> { + match &self.file_pre_edit_hooks { + Some(hooks) => get_hooks(hooks, cnf), + None => Ok(vec![]), + } + } + + pub fn get_file_post_edit_hooks(&self, cnf: &Config) -> Result, Error> { + match &self.file_post_edit_hooks { + Some(hooks) => get_hooks(hooks, cnf), + None => Ok(vec![]), + } + } +} + +fn get_hooks(lst: &Vec, cnf: &Config) -> Result, Error> { + let mut res = vec![]; + for name in lst.iter() { + let mut h = cnf.get_hook(&name)?; + res.append(&mut h); } + Ok(res) } fn create_dir(path: &str) -> Result<(), Error> { diff --git a/acmed/src/hooks.rs b/acmed/src/hooks.rs new file mode 100644 index 0000000..206a6d2 --- /dev/null +++ b/acmed/src/hooks.rs @@ -0,0 +1,57 @@ +use crate::config::Hook; +use crate::errors::Error; +use handlebars::Handlebars; +use serde::Serialize; +use std::fs::File; +use std::io::prelude::*; +use std::process::{Command, Stdio}; + +macro_rules! get_hook_output { + ($out: expr, $reg: ident, $data: expr) => {{ + match $out { + Some(path) => { + let path = $reg.render_template(path, $data)?; + let file = File::create(path)?; + Stdio::from(file) + } + None => Stdio::null(), + } + }}; +} + +pub fn call_multiple(data: &T, hooks: &Vec) -> Result<(), Error> { + for hook in hooks.iter() { + call(data, &hook)?; + } + Ok(()) +} + +pub fn call(data: &T, hook: &Hook) -> Result<(), Error> { + let reg = Handlebars::new(); + let mut v = vec![]; + let args = match &hook.args { + Some(lst) => { + for fmt in lst.iter() { + let s = reg.render_template(fmt, data)?; + v.push(s); + } + v.as_slice() + } + None => &[], + }; + let mut cmd = Command::new(&hook.cmd) + .args(args) + .stdout(get_hook_output!(&hook.stdout, reg, data)) + .stderr(get_hook_output!(&hook.stderr, reg, data)) + .stdin(match &hook.stdin { + Some(_) => Stdio::piped(), + None => Stdio::null(), + }) + .spawn()?; + if hook.stdin.is_some() { + let data_in = reg.render_template(&hook.stdin.to_owned().unwrap(), data)?; + let stdin = cmd.stdin.as_mut().ok_or("stdin not found")?; + stdin.write_all(data_in.as_bytes())?; + } + Ok(()) +} diff --git a/acmed/src/main.rs b/acmed/src/main.rs index 39c12f5..e252df3 100644 --- a/acmed/src/main.rs +++ b/acmed/src/main.rs @@ -6,6 +6,7 @@ mod acmed; mod config; mod encoding; mod errors; +mod hooks; mod storage; pub const DEFAULT_CONFIG_FILE: &str = "/etc/acmed/acmed.toml"; diff --git a/acmed/src/storage.rs b/acmed/src/storage.rs index 2cd3333..7ba32f0 100644 --- a/acmed/src/storage.rs +++ b/acmed/src/storage.rs @@ -1,8 +1,12 @@ use acme_lib::Error; use acme_lib::persist::{Persist, PersistKey, PersistKind}; use crate::acmed::{Algorithm, Format}; +use crate::config::Hook; +use crate::errors; use crate::encoding::convert; +use crate::hooks; use log::debug; +use serde::Serialize; use std::fs::{File, OpenOptions}; use std::io::prelude::*; use std::path::PathBuf; @@ -28,6 +32,13 @@ macro_rules! get_file_name { }}; } +#[derive(Serialize)] +struct FileData { + file_name: String, + file_directory: String, + file_path: PathBuf, +} + #[derive(Clone, Debug)] pub struct Storage { pub account_directory: String, @@ -43,6 +54,10 @@ pub struct Storage { pub pk_file_mode: u32, pub pk_file_owner: Option, pub pk_file_group: Option, + pub file_pre_create_hooks: Vec, + pub file_post_create_hooks: Vec, + pub file_pre_edit_hooks: Vec, + pub file_post_edit_hooks: Vec, } impl Storage { @@ -90,7 +105,7 @@ impl Storage { } } - fn get_file_path(&self, kind: PersistKind, fmt: &Format) -> PathBuf { + fn get_file_path(&self, kind: PersistKind, fmt: &Format) -> FileData { let base_path = match kind { PersistKind::Certificate => &self.crt_directory, PersistKind::PrivateKey => &self.crt_directory, @@ -104,8 +119,12 @@ impl Storage { } }; let mut path = PathBuf::from(base_path); - path.push(file_name); - path + path.push(&file_name); + FileData { + file_directory: base_path.to_string(), + file_name: file_name, + file_path: path, + } } pub fn get_certificate(&self, fmt: &Format) -> Result>, Error> { @@ -122,12 +141,12 @@ impl Storage { } else { self.formats.first().unwrap() }; - let path = self.get_file_path(kind, src_fmt); - debug!("Reading file {:?}", path); - if !path.exists() { + let file_data = self.get_file_path(kind, src_fmt); + debug!("Reading file {:?}", file_data.file_path); + if !file_data.file_path.exists() { return Ok(None); } - let mut file = File::open(&path)?; + let mut file = File::open(&file_data.file_path)?; let mut contents = vec![]; file.read_to_end(&mut contents)?; if contents.is_empty() { @@ -145,15 +164,21 @@ impl Storage { impl Persist for Storage { fn put(&self, key: &PersistKey, value: &[u8]) -> Result<(), Error> { for fmt in self.formats.iter() { - let path = self.get_file_path(key.kind, &fmt); - debug!("Writing file {:?}", path); + let file_data = self.get_file_path(key.kind, &fmt); + debug!("Writing file {:?}", file_data.file_path); + let file_exists = file_data.file_path.exists(); + if file_exists { + hooks::call_multiple(&file_data, &self.file_pre_edit_hooks).map_err(to_acme_err)?; + } else { + hooks::call_multiple(&file_data, &self.file_pre_create_hooks).map_err(to_acme_err)?; + } { let mut f = if cfg!(unix) { let mut options = OpenOptions::new(); options.mode(self.get_file_mode(key.kind)); - options.write(true).create(true).open(&path)? + options.write(true).create(true).open(&file_data.file_path)? } else { - File::create(&path)? + File::create(&file_data.file_path)? }; match fmt { Format::Der => { @@ -165,7 +190,13 @@ impl Persist for Storage { f.sync_all()?; } if cfg!(unix) { - self.set_owner(&path, key.kind)?; + self.set_owner(&file_data.file_path, key.kind)?; + } + if file_exists { + hooks::call_multiple(&file_data, &self.file_post_edit_hooks).map_err(to_acme_err)?; + } else { + hooks::call_multiple(&file_data, &self.file_post_create_hooks) + .map_err(to_acme_err)?; } } Ok(()) @@ -175,3 +206,7 @@ impl Persist for Storage { self.get_file(key.kind, &Format::Pem) } } + +fn to_acme_err(e: errors::Error) -> Error { + Error::Other(e.message) +}