From 9ec48e7e03779c41056da0c7ad3ad4004e79013e Mon Sep 17 00:00:00 2001 From: Rodolphe Breard Date: Sat, 10 Oct 2020 18:57:36 +0200 Subject: [PATCH] Add the `root_certificates` parameter Being able to define root certificates in the command line is not enough for two reasons: 1. It is always global, you cannot define a root certificate for a specific endpoint. 2. Daemon scripts and unit files are not meant to be changed every time you need to add a root certificate. For those reasons, it is now to possible to define root certificates in the configuration. Those defined in the `global` section will be used on every endpoint, just like those added via the command line. Those defined in an endpoint will be used in this endpoint only. --- CHANGELOG.md | 6 +++++ acmed/src/account.rs | 22 ++++++------------ acmed/src/acme_proto.rs | 28 +++++++++++----------- acmed/src/acme_proto/account.rs | 25 ++++++-------------- acmed/src/acme_proto/http.rs | 41 ++++++++------------------------- acmed/src/config.rs | 34 +++++++++++++++++++++++---- acmed/src/endpoint.rs | 3 +++ acmed/src/http.rs | 19 +++++---------- acmed/src/main_event_loop.rs | 16 ++++--------- man/en/acmed.toml.5 | 4 ++++ 10 files changed, 90 insertions(+), 108 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 59e672c..1034511 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [Unreleased] + +### Added +- In the configuration, `root_certificates` has been added to the `global` and `endpoint` sections as an array of strings representing the path to root certificate files. + + ## [0.12.0] - 2020-09-26 ### Added diff --git a/acmed/src/account.rs b/acmed/src/account.rs index ddea50f..791a699 100644 --- a/acmed/src/account.rs +++ b/acmed/src/account.rs @@ -202,11 +202,7 @@ impl Account { .or_insert_with(AccountEndpoint::new); } - pub fn synchronize( - &mut self, - endpoint: &mut Endpoint, - root_certs: &[String], - ) -> Result<(), Error> { + pub fn synchronize(&mut self, endpoint: &mut Endpoint) -> Result<(), Error> { let acc_ep = self.get_endpoint(&endpoint.name)?; if !acc_ep.account_url.is_empty() { if let Some(ec) = &self.external_account { @@ -217,7 +213,7 @@ impl Account { &endpoint.name ); self.info(&msg); - register_account(endpoint, root_certs, self)?; + register_account(endpoint, self)?; return Ok(()); } } @@ -226,23 +222,19 @@ impl Account { let contacts_changed = ct_hash != acc_ep.contacts_hash; let key_changed = key_hash != acc_ep.key_hash; if contacts_changed { - update_account_contacts(endpoint, root_certs, self)?; + update_account_contacts(endpoint, self)?; } if key_changed { - update_account_key(endpoint, root_certs, self)?; + update_account_key(endpoint, self)?; } } else { - register_account(endpoint, root_certs, self)?; + register_account(endpoint, self)?; } Ok(()) } - pub fn register( - &mut self, - endpoint: &mut Endpoint, - root_certs: &[String], - ) -> Result<(), Error> { - register_account(endpoint, root_certs, self) + pub fn register(&mut self, endpoint: &mut Endpoint) -> Result<(), Error> { + register_account(endpoint, self) } pub fn save(&self) -> Result<(), Error> { diff --git a/acmed/src/acme_proto.rs b/acmed/src/acme_proto.rs index d77e07c..c3d3ce5 100644 --- a/acmed/src/acme_proto.rs +++ b/acmed/src/acme_proto.rs @@ -84,7 +84,6 @@ macro_rules! set_empty_data_builder { pub fn request_certificate( cert: &Certificate, - root_certs: &[String], endpoint: &mut Endpoint, account: &mut Account, ) -> Result<(), Error> { @@ -92,10 +91,10 @@ pub fn request_certificate( let endpoint_name = endpoint.name.clone(); // Refresh the directory - http::refresh_directory(endpoint, root_certs).map_err(HttpError::in_err)?; + http::refresh_directory(endpoint).map_err(HttpError::in_err)?; // Synchronize the account - account.synchronize(endpoint, root_certs)?; + account.synchronize(endpoint)?; // Create a new order let mut new_reg = false; @@ -103,7 +102,7 @@ pub fn request_certificate( let new_order = NewOrder::new(&cert.identifiers); let new_order = serde_json::to_string(&new_order)?; let data_builder = set_data_builder!(account, endpoint_name, new_order.as_bytes()); - match http::new_order(endpoint, root_certs, &data_builder) { + match http::new_order(endpoint, &data_builder) { Ok((order, order_url)) => { if let Some(e) = order.get_error() { cert.warn(&e.prefix("Error").message); @@ -112,7 +111,7 @@ pub fn request_certificate( } Err(e) => { if !new_reg && e.is_acme_err(AcmeError::AccountDoesNotExist) { - account.register(endpoint, root_certs)?; + account.register(endpoint)?; new_reg = true; } else { return Err(HttpError::in_err(e)); @@ -125,7 +124,7 @@ pub fn request_certificate( for auth_url in order.authorizations.iter() { // Fetch the authorization let data_builder = set_empty_data_builder!(account, endpoint_name); - let auth = http::get_authorization(endpoint, root_certs, &data_builder, &auth_url) + let auth = http::get_authorization(endpoint, &data_builder, &auth_url) .map_err(HttpError::in_err)?; if let Some(e) = auth.get_error() { cert.warn(&e.prefix("error").message); @@ -158,16 +157,15 @@ pub fn request_certificate( // Tell the server the challenge has been completed let chall_url = challenge.get_url(); let data_builder = set_data_builder!(account, endpoint_name, b"{}"); - let _ = - http::post_jose_no_response(endpoint, root_certs, &data_builder, &chall_url) - .map_err(HttpError::in_err)?; + let _ = http::post_jose_no_response(endpoint, &data_builder, &chall_url) + .map_err(HttpError::in_err)?; } } // Pool the authorization in order to see whether or not it is valid let data_builder = set_empty_data_builder!(account, endpoint_name); let break_fn = |a: &Authorization| a.status == AuthorizationStatus::Valid; - let _ = http::pool_authorization(endpoint, root_certs, &data_builder, &break_fn, &auth_url) + let _ = http::pool_authorization(endpoint, &data_builder, &break_fn, &auth_url) .map_err(HttpError::in_err)?; for (data, hook_type) in hook_datas.iter() { cert.call_challenge_hooks_clean(&data, (*hook_type).to_owned())?; @@ -179,7 +177,7 @@ pub fn request_certificate( // Pool the order in order to see whether or not it is ready let data_builder = set_empty_data_builder!(account, endpoint_name); let break_fn = |o: &Order| o.status == OrderStatus::Ready; - let order = http::pool_order(endpoint, root_certs, &data_builder, &break_fn, &order_url) + let order = http::pool_order(endpoint, &data_builder, &break_fn, &order_url) .map_err(HttpError::in_err)?; // Finalize the order by sending the CSR @@ -209,7 +207,7 @@ pub fn request_certificate( }); let csr = csr.to_string(); let data_builder = set_data_builder!(account, endpoint_name, csr.as_bytes()); - let order = http::finalize_order(endpoint, root_certs, &data_builder, &order.finalize) + let order = http::finalize_order(endpoint, &data_builder, &order.finalize) .map_err(HttpError::in_err)?; if let Some(e) = order.get_error() { cert.warn(&e.prefix("error").message); @@ -218,7 +216,7 @@ pub fn request_certificate( // Pool the order in order to see whether or not it is valid let data_builder = set_empty_data_builder!(account, endpoint_name); let break_fn = |o: &Order| o.status == OrderStatus::Valid; - let order = http::pool_order(endpoint, root_certs, &data_builder, &break_fn, &order_url) + let order = http::pool_order(endpoint, &data_builder, &break_fn, &order_url) .map_err(HttpError::in_err)?; // Download the certificate @@ -226,8 +224,8 @@ pub fn request_certificate( .certificate .ok_or_else(|| Error::from("no certificate available for download"))?; let data_builder = set_empty_data_builder!(account, endpoint_name); - let crt = http::get_certificate(endpoint, root_certs, &data_builder, &crt_url) - .map_err(HttpError::in_err)?; + let crt = + http::get_certificate(endpoint, &data_builder, &crt_url).map_err(HttpError::in_err)?; storage::write_certificate(&cert.file_manager, &crt.as_bytes())?; cert.info(&format!( diff --git a/acmed/src/acme_proto/account.rs b/acmed/src/acme_proto/account.rs index c08dedd..6c09cd8 100644 --- a/acmed/src/acme_proto/account.rs +++ b/acmed/src/acme_proto/account.rs @@ -9,7 +9,7 @@ use crate::set_data_builder; use acme_common::error::Error; macro_rules! create_account_if_does_not_exist { - ($e: expr, $endpoint: ident, $root_certs: ident, $account: ident) => { + ($e: expr, $endpoint: ident, $account: ident) => { match $e { Ok(r) => Ok(r), Err(he) => match he { @@ -20,7 +20,7 @@ macro_rules! create_account_if_does_not_exist { $endpoint.name ); $account.debug(&msg); - return register_account($endpoint, $root_certs, $account); + return register_account($endpoint, $account); } _ => Err(HttpError::in_err(he.to_owned())), }, @@ -30,11 +30,7 @@ macro_rules! create_account_if_does_not_exist { }; } -pub fn register_account( - endpoint: &mut Endpoint, - root_certs: &[String], - account: &mut BaseAccount, -) -> Result<(), Error> { +pub fn register_account(endpoint: &mut Endpoint, account: &mut BaseAccount) -> Result<(), Error> { account.debug(&format!( "creating account on endpoint \"{}\"...", &endpoint.name @@ -47,7 +43,7 @@ pub fn register_account( let data_builder = |n: &str, url: &str| encode_jwk(kp_ref, signature_algorithm, acc_ref.as_bytes(), url, n); let (acc_rep, account_url) = - http::new_account(endpoint, root_certs, &data_builder).map_err(HttpError::in_err)?; + http::new_account(endpoint, &data_builder).map_err(HttpError::in_err)?; account.set_account_url(&endpoint.name, &account_url)?; let orders_url = match acc_rep.orders { Some(url) => url, @@ -75,7 +71,6 @@ pub fn register_account( pub fn update_account_contacts( endpoint: &mut Endpoint, - root_certs: &[String], account: &mut BaseAccount, ) -> Result<(), Error> { let endpoint_name = endpoint.name.clone(); @@ -89,9 +84,8 @@ pub fn update_account_contacts( let data_builder = set_data_builder!(account, endpoint_name, acc_up_struct.as_bytes()); let url = account.get_endpoint(&endpoint_name)?.account_url.clone(); create_account_if_does_not_exist!( - http::post_jose_no_response(endpoint, root_certs, &data_builder, &url), + http::post_jose_no_response(endpoint, &data_builder, &url), endpoint, - root_certs, account )?; account.update_contacts_hash(&endpoint_name)?; @@ -103,11 +97,7 @@ pub fn update_account_contacts( Ok(()) } -pub fn update_account_key( - endpoint: &mut Endpoint, - root_certs: &[String], - account: &mut BaseAccount, -) -> Result<(), Error> { +pub fn update_account_key(endpoint: &mut Endpoint, account: &mut BaseAccount) -> Result<(), Error> { let endpoint_name = endpoint.name.clone(); account.debug(&format!( "updating account key on endpoint \"{}\"...", @@ -137,9 +127,8 @@ pub fn update_account_key( ) }; create_account_if_does_not_exist!( - http::post_jose_no_response(endpoint, root_certs, &data_builder, &url), + http::post_jose_no_response(endpoint, &data_builder, &url), endpoint, - root_certs, account )?; account.update_key_hash(&endpoint_name)?; diff --git a/acmed/src/acme_proto/http.rs b/acmed/src/acme_proto/http.rs index bee5aff..ca1f9f9 100644 --- a/acmed/src/acme_proto/http.rs +++ b/acmed/src/acme_proto/http.rs @@ -5,10 +5,10 @@ use acme_common::error::Error; use std::{thread, time}; macro_rules! pool_object { - ($obj_type: ty, $obj_name: expr, $endpoint: expr, $root_certs: expr, $url: expr, $data_builder: expr, $break: expr) => {{ + ($obj_type: ty, $obj_name: expr, $endpoint: expr, $url: expr, $data_builder: expr, $break: expr) => {{ for _ in 0..crate::DEFAULT_POOL_NB_TRIES { thread::sleep(time::Duration::from_secs(crate::DEFAULT_POOL_WAIT_SEC)); - let response = http::post_jose($endpoint, $root_certs, $url, $data_builder)?; + let response = http::post_jose($endpoint, $url, $data_builder)?; let obj = response.json::<$obj_type>()?; if $break(&obj) { return Ok(obj); @@ -19,39 +19,34 @@ macro_rules! pool_object { }}; } -pub fn refresh_directory( - endpoint: &mut Endpoint, - root_certs: &[String], -) -> Result<(), http::HttpError> { +pub fn refresh_directory(endpoint: &mut Endpoint) -> Result<(), http::HttpError> { let url = endpoint.url.clone(); - let response = http::get(endpoint, root_certs, &url)?; + let response = http::get(endpoint, &url)?; endpoint.dir = response.json::()?; Ok(()) } pub fn post_jose_no_response( endpoint: &mut Endpoint, - root_certs: &[String], data_builder: &F, url: &str, ) -> Result<(), http::HttpError> where F: Fn(&str, &str) -> Result, { - let _ = http::post_jose(endpoint, root_certs, &url, data_builder)?; + let _ = http::post_jose(endpoint, &url, data_builder)?; Ok(()) } pub fn new_account( endpoint: &mut Endpoint, - root_certs: &[String], data_builder: &F, ) -> Result<(AccountResponse, String), http::HttpError> where F: Fn(&str, &str) -> Result, { let url = endpoint.dir.new_account.clone(); - let response = http::post_jose(endpoint, root_certs, &url, data_builder)?; + let response = http::post_jose(endpoint, &url, data_builder)?; let acc_uri = response .get_header(http::HEADER_LOCATION) .ok_or_else(|| Error::from("no account location found"))?; @@ -61,14 +56,13 @@ where pub fn new_order( endpoint: &mut Endpoint, - root_certs: &[String], data_builder: &F, ) -> Result<(Order, String), http::HttpError> where F: Fn(&str, &str) -> Result, { let url = endpoint.dir.new_order.clone(); - let response = http::post_jose(endpoint, root_certs, &url, data_builder)?; + let response = http::post_jose(endpoint, &url, data_builder)?; let order_uri = response .get_header(http::HEADER_LOCATION) .ok_or_else(|| Error::from("no account location found"))?; @@ -78,21 +72,19 @@ where pub fn get_authorization( endpoint: &mut Endpoint, - root_certs: &[String], data_builder: &F, url: &str, ) -> Result where F: Fn(&str, &str) -> Result, { - let response = http::post_jose(endpoint, root_certs, &url, data_builder)?; + let response = http::post_jose(endpoint, &url, data_builder)?; let auth = response.json::()?; Ok(auth) } pub fn pool_authorization( endpoint: &mut Endpoint, - root_certs: &[String], data_builder: &F, break_fn: &S, url: &str, @@ -105,7 +97,6 @@ where Authorization, "authorization", endpoint, - root_certs, url, data_builder, break_fn @@ -114,7 +105,6 @@ where pub fn pool_order( endpoint: &mut Endpoint, - root_certs: &[String], data_builder: &F, break_fn: &S, url: &str, @@ -123,34 +113,24 @@ where F: Fn(&str, &str) -> Result, S: Fn(&Order) -> bool, { - pool_object!( - Order, - "order", - endpoint, - root_certs, - url, - data_builder, - break_fn - ) + pool_object!(Order, "order", endpoint, url, data_builder, break_fn) } pub fn finalize_order( endpoint: &mut Endpoint, - root_certs: &[String], data_builder: &F, url: &str, ) -> Result where F: Fn(&str, &str) -> Result, { - let response = http::post_jose(endpoint, root_certs, &url, data_builder)?; + let response = http::post_jose(endpoint, &url, data_builder)?; let order = response.json::()?; Ok(order) } pub fn get_certificate( endpoint: &mut Endpoint, - root_certs: &[String], data_builder: &F, url: &str, ) -> Result @@ -159,7 +139,6 @@ where { let response = http::post( endpoint, - root_certs, &url, data_builder, http::CONTENT_TYPE_JOSE, diff --git a/acmed/src/config.rs b/acmed/src/config.rs index 48281ae..c630e8d 100644 --- a/acmed/src/config.rs +++ b/acmed/src/config.rs @@ -186,6 +186,7 @@ pub struct GlobalOptions { #[serde(default)] pub env: HashMap, pub renew_delay: Option, + pub root_certificates: Option>, } impl GlobalOptions { @@ -206,6 +207,7 @@ pub struct Endpoint { #[serde(default)] pub rate_limits: Vec, pub renew_delay: Option, + pub root_certificates: Option>, } impl Endpoint { @@ -219,13 +221,33 @@ impl Endpoint { } } - fn to_generic(&self, cnf: &Config) -> Result { + fn to_generic( + &self, + cnf: &Config, + root_certs: &[&str], + ) -> Result { let mut limits = vec![]; for rl_name in self.rate_limits.iter() { let (nb, timeframe) = cnf.get_rate_limit(&rl_name)?; limits.push((nb, timeframe)); } - crate::endpoint::Endpoint::new(&self.name, &self.url, self.tos_agreed, &limits) + let mut root_lst: Vec = vec![]; + root_lst.extend(root_certs.iter().map(|v| v.to_string())); + if let Some(crt_lst) = &self.root_certificates { + root_lst.extend(crt_lst.iter().map(|v| v.to_owned())); + } + if let Some(glob) = &cnf.global { + if let Some(crt_lst) = &glob.root_certificates { + root_lst.extend(crt_lst.iter().map(|v| v.to_owned())); + } + } + crate::endpoint::Endpoint::new( + &self.name, + &self.url, + self.tos_agreed, + &limits, + root_lst.as_slice(), + ) } } @@ -477,9 +499,13 @@ impl Certificate { Err(format!("{}: unknown endpoint", self.endpoint).into()) } - pub fn get_endpoint(&self, cnf: &Config) -> Result { + pub fn get_endpoint( + &self, + cnf: &Config, + root_certs: &[&str], + ) -> Result { let endpoint = self.do_get_endpoint(cnf)?; - endpoint.to_generic(cnf) + endpoint.to_generic(cnf, root_certs) } pub fn get_hooks(&self, cnf: &Config) -> Result, Error> { diff --git a/acmed/src/endpoint.rs b/acmed/src/endpoint.rs index 74a462f..99c0442 100644 --- a/acmed/src/endpoint.rs +++ b/acmed/src/endpoint.rs @@ -13,6 +13,7 @@ pub struct Endpoint { pub nonce: Option, pub rl: RateLimit, pub dir: Directory, + pub root_certificates: Vec, } impl Endpoint { @@ -21,6 +22,7 @@ impl Endpoint { url: &str, tos_agreed: bool, limits: &[(usize, String)], + root_certs: &[String], ) -> Result { Ok(Self { name: name.to_string(), @@ -37,6 +39,7 @@ impl Endpoint { revoke_cert: String::new(), key_change: String::new(), }, + root_certificates: root_certs.to_vec(), }) } } diff --git a/acmed/src/http.rs b/acmed/src/http.rs index 55e323c..477cd98 100644 --- a/acmed/src/http.rs +++ b/acmed/src/http.rs @@ -106,10 +106,10 @@ fn is_nonce(data: &str) -> bool { .all(|c| c.is_ascii_alphanumeric() || c == b'-' || c == b'_') } -fn new_nonce(endpoint: &mut Endpoint, root_certs: &[String]) -> Result<(), HttpError> { +fn new_nonce(endpoint: &mut Endpoint) -> Result<(), HttpError> { rate_limit(endpoint); let url = endpoint.dir.new_nonce.clone(); - let _ = get(endpoint, root_certs, &url)?; + let _ = get(endpoint, &url)?; Ok(()) } @@ -170,12 +170,8 @@ fn get_session(root_certs: &[String]) -> Result { Ok(session) } -pub fn get( - endpoint: &mut Endpoint, - root_certs: &[String], - url: &str, -) -> Result { - let mut session = get_session(root_certs)?; +pub fn get(endpoint: &mut Endpoint, url: &str) -> Result { + let mut session = get_session(&endpoint.root_certificates)?; session.try_header(header::ACCEPT, CONTENT_TYPE_JSON)?; rate_limit(endpoint); let response = session.get(url).send()?; @@ -186,7 +182,6 @@ pub fn get( pub fn post( endpoint: &mut Endpoint, - root_certs: &[String], url: &str, data_builder: &F, content_type: &str, @@ -195,11 +190,11 @@ pub fn post( where F: Fn(&str, &str) -> Result, { - let mut session = get_session(root_certs)?; + let mut session = get_session(&endpoint.root_certificates)?; session.try_header(header::ACCEPT, accept)?; session.try_header(header::CONTENT_TYPE, content_type)?; if endpoint.nonce.is_none() { - let _ = new_nonce(endpoint, root_certs); + let _ = new_nonce(endpoint); } for _ in 0..crate::DEFAULT_HTTP_FAIL_NB_RETRY { let nonce = &endpoint.nonce.clone().unwrap_or_default(); @@ -228,7 +223,6 @@ where pub fn post_jose( endpoint: &mut Endpoint, - root_certs: &[String], url: &str, data_builder: &F, ) -> Result @@ -237,7 +231,6 @@ where { post( endpoint, - root_certs, url, data_builder, CONTENT_TYPE_JOSE, diff --git a/acmed/src/main_event_loop.rs b/acmed/src/main_event_loop.rs index 082acc5..e8a8eda 100644 --- a/acmed/src/main_event_loop.rs +++ b/acmed/src/main_event_loop.rs @@ -15,13 +15,8 @@ use std::time::Duration; type AccountSync = Arc>; type EndpointSync = Arc>; -fn renew_certificate( - crt: &Certificate, - root_certs: &[String], - endpoint: &mut Endpoint, - account: &mut Account, -) { - let (status, is_success) = match request_certificate(crt, root_certs, endpoint, account) { +fn renew_certificate(crt: &Certificate, endpoint: &mut Endpoint, account: &mut Account) { + let (status, is_success) = match request_certificate(crt, endpoint, account) { Ok(_) => ("success".to_string(), true), Err(e) => { let e = e.prefix("unable to renew the certificate"); @@ -40,7 +35,6 @@ fn renew_certificate( pub struct MainEventLoop { certs: Vec, - root_certs: Vec, accounts: HashMap, endpoints: HashMap, } @@ -98,7 +92,7 @@ impl MainEventLoop { let mut certs = Vec::new(); let mut endpoints = HashMap::new(); for (i, crt) in cnf.certificate.iter().enumerate() { - let endpoint = crt.get_endpoint(&cnf)?; + let endpoint = crt.get_endpoint(&cnf, root_certs)?; let endpoint_name = endpoint.name.clone(); let crt_name = crt.get_crt_name()?; let key_type = crt.get_key_type()?; @@ -155,7 +149,6 @@ impl MainEventLoop { Ok(MainEventLoop { certs, - root_certs: root_certs.iter().map(|v| (*v).to_string()).collect(), accounts: accounts .iter() .map(|(k, v)| (k.to_owned(), Arc::new(RwLock::new(v.to_owned())))) @@ -194,13 +187,12 @@ impl MainEventLoop { } let mut accounts_lock = self.accounts.clone(); let ep_lock = endpoint_lock.clone(); - let rc = self.root_certs.clone(); let handle = thread::spawn(move || { let mut endpoint = ep_lock.write().unwrap(); for crt in certs_to_renew { if let Some(acc_lock) = accounts_lock.get_mut(&crt.account_name) { let mut account = acc_lock.write().unwrap(); - renew_certificate(&crt, &rc, &mut endpoint, &mut account); + renew_certificate(&crt, &mut endpoint, &mut account); }; } }); diff --git a/man/en/acmed.toml.5 b/man/en/acmed.toml.5 index c4d9cf4..342d791 100644 --- a/man/en/acmed.toml.5 +++ b/man/en/acmed.toml.5 @@ -68,6 +68,8 @@ for more details. Period of time between the certificate renewal and its expiration date. The format is described in the .Sx TIME PERIODS section. Default is 3w. +.It Cm root_certificates Ar array +Array containing the path to root certificates that should be added to the trust store. .El .It Ic rate-limit Array of table where each element defines a HTTPS rate limit. @@ -99,6 +101,8 @@ The endpoint's directory URL. 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. +.It Cm root_certificates Ar array +Array containing the path to root certificates that should be added to the trust store. .El .It Ic hook Array of table where each element defines a command that will be launched at a defined point. See section