Browse Source

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.
pull/39/head
Rodolphe Breard 4 years ago
parent
commit
9ec48e7e03
  1. 6
      CHANGELOG.md
  2. 22
      acmed/src/account.rs
  3. 28
      acmed/src/acme_proto.rs
  4. 25
      acmed/src/acme_proto/account.rs
  5. 41
      acmed/src/acme_proto/http.rs
  6. 34
      acmed/src/config.rs
  7. 3
      acmed/src/endpoint.rs
  8. 19
      acmed/src/http.rs
  9. 16
      acmed/src/main_event_loop.rs
  10. 4
      man/en/acmed.toml.5

6
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

22
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> {

28
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!(

25
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)?;

41
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::<Directory>()?;
Ok(())
}
pub fn post_jose_no_response<F>(
endpoint: &mut Endpoint,
root_certs: &[String],
data_builder: &F,
url: &str,
) -> Result<(), http::HttpError>
where
F: Fn(&str, &str) -> Result<String, Error>,
{
let _ = http::post_jose(endpoint, root_certs, &url, data_builder)?;
let _ = http::post_jose(endpoint, &url, data_builder)?;
Ok(())
}
pub fn new_account<F>(
endpoint: &mut Endpoint,
root_certs: &[String],
data_builder: &F,
) -> Result<(AccountResponse, String), http::HttpError>
where
F: Fn(&str, &str) -> Result<String, Error>,
{
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<F>(
endpoint: &mut Endpoint,
root_certs: &[String],
data_builder: &F,
) -> Result<(Order, String), http::HttpError>
where
F: Fn(&str, &str) -> Result<String, Error>,
{
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<F>(
endpoint: &mut Endpoint,
root_certs: &[String],
data_builder: &F,
url: &str,
) -> Result<Authorization, http::HttpError>
where
F: Fn(&str, &str) -> Result<String, Error>,
{
let response = http::post_jose(endpoint, root_certs, &url, data_builder)?;
let response = http::post_jose(endpoint, &url, data_builder)?;
let auth = response.json::<Authorization>()?;
Ok(auth)
}
pub fn pool_authorization<F, S>(
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<F, S>(
endpoint: &mut Endpoint,
root_certs: &[String],
data_builder: &F,
break_fn: &S,
url: &str,
@ -123,34 +113,24 @@ where
F: Fn(&str, &str) -> Result<String, Error>,
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<F>(
endpoint: &mut Endpoint,
root_certs: &[String],
data_builder: &F,
url: &str,
) -> Result<Order, http::HttpError>
where
F: Fn(&str, &str) -> Result<String, Error>,
{
let response = http::post_jose(endpoint, root_certs, &url, data_builder)?;
let response = http::post_jose(endpoint, &url, data_builder)?;
let order = response.json::<Order>()?;
Ok(order)
}
pub fn get_certificate<F>(
endpoint: &mut Endpoint,
root_certs: &[String],
data_builder: &F,
url: &str,
) -> Result<String, http::HttpError>
@ -159,7 +139,6 @@ where
{
let response = http::post(
endpoint,
root_certs,
&url,
data_builder,
http::CONTENT_TYPE_JOSE,

34
acmed/src/config.rs

@ -186,6 +186,7 @@ pub struct GlobalOptions {
#[serde(default)]
pub env: HashMap<String, String>,
pub renew_delay: Option<String>,
pub root_certificates: Option<Vec<String>>,
}
impl GlobalOptions {
@ -206,6 +207,7 @@ pub struct Endpoint {
#[serde(default)]
pub rate_limits: Vec<String>,
pub renew_delay: Option<String>,
pub root_certificates: Option<Vec<String>>,
}
impl Endpoint {
@ -219,13 +221,33 @@ impl Endpoint {
}
}
fn to_generic(&self, cnf: &Config) -> Result<crate::endpoint::Endpoint, Error> {
fn to_generic(
&self,
cnf: &Config,
root_certs: &[&str],
) -> Result<crate::endpoint::Endpoint, Error> {
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<String> = 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<crate::endpoint::Endpoint, Error> {
pub fn get_endpoint(
&self,
cnf: &Config,
root_certs: &[&str],
) -> Result<crate::endpoint::Endpoint, Error> {
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<Vec<hooks::Hook>, Error> {

3
acmed/src/endpoint.rs

@ -13,6 +13,7 @@ pub struct Endpoint {
pub nonce: Option<String>,
pub rl: RateLimit,
pub dir: Directory,
pub root_certificates: Vec<String>,
}
impl Endpoint {
@ -21,6 +22,7 @@ impl Endpoint {
url: &str,
tos_agreed: bool,
limits: &[(usize, String)],
root_certs: &[String],
) -> Result<Self, Error> {
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(),
})
}
}

19
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<Session, Error> {
Ok(session)
}
pub fn get(
endpoint: &mut Endpoint,
root_certs: &[String],
url: &str,
) -> Result<ValidHttpResponse, HttpError> {
let mut session = get_session(root_certs)?;
pub fn get(endpoint: &mut Endpoint, url: &str) -> Result<ValidHttpResponse, HttpError> {
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<F>(
endpoint: &mut Endpoint,
root_certs: &[String],
url: &str,
data_builder: &F,
content_type: &str,
@ -195,11 +190,11 @@ pub fn post<F>(
where
F: Fn(&str, &str) -> Result<String, Error>,
{
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<F>(
endpoint: &mut Endpoint,
root_certs: &[String],
url: &str,
data_builder: &F,
) -> Result<ValidHttpResponse, HttpError>
@ -237,7 +231,6 @@ where
{
post(
endpoint,
root_certs,
url,
data_builder,
CONTENT_TYPE_JOSE,

16
acmed/src/main_event_loop.rs

@ -15,13 +15,8 @@ use std::time::Duration;
type AccountSync = Arc<RwLock<Account>>;
type EndpointSync = Arc<RwLock<Endpoint>>;
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<Certificate>,
root_certs: Vec<String>,
accounts: HashMap<String, AccountSync>,
endpoints: HashMap<String, EndpointSync>,
}
@ -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);
};
}
});

4
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

Loading…
Cancel
Save