From 447d1f848fdee18b174f704d7a83e03120af8614 Mon Sep 17 00:00:00 2001 From: Rodolphe Breard Date: Mon, 29 Apr 2019 19:52:05 +0200 Subject: [PATCH] Add an option to specify new trusted root certificates Sometimes, it is not possible to use certificates signed by a known certificate authority. Hence, in order to prevent a TLS error, it is required to explicitly add a new trusted root certificate. This is the case with Pebble, which provides the certificate. https://github.com/letsencrypt/pebble#avoiding-client-https-errors --- acmed/src/acme_proto.rs | 24 +++++++++---------- acmed/src/acme_proto/account.rs | 3 ++- acmed/src/acme_proto/http.rs | 42 ++++++++++++++++++++------------- acmed/src/main.rs | 15 +++++++++++- acmed/src/main_event_loop.rs | 10 +++++--- 5 files changed, 60 insertions(+), 34 deletions(-) diff --git a/acmed/src/acme_proto.rs b/acmed/src/acme_proto.rs index 3b1648d..dd23fb5 100644 --- a/acmed/src/acme_proto.rs +++ b/acmed/src/acme_proto.rs @@ -66,28 +66,28 @@ macro_rules! set_empty_data_builder { }; } -pub fn request_certificate(cert: &Certificate) -> Result<(), Error> { +pub fn request_certificate(cert: &Certificate, root_certs: &[String]) -> Result<(), Error> { // 1. Get the directory - let directory = http::get_directory(&cert.remote_url)?; + let directory = http::get_directory(root_certs, &cert.remote_url)?; // 2. Get a first nonce - let nonce = http::get_nonce(&directory.new_nonce)?; + let nonce = http::get_nonce(root_certs, &directory.new_nonce)?; // 3. Get or create the account - let (account, nonce) = AccountManager::new(cert, &directory, &nonce)?; + let (account, nonce) = AccountManager::new(cert, &directory, &nonce, root_certs)?; // 4. Create a new order let new_order = NewOrder::new(&cert.domains); let new_order = serde_json::to_string(&new_order)?; let data_builder = set_data_builder!(account, new_order.as_bytes(), directory.new_order); let (order, order_url, mut nonce): (Order, String, String) = - http::get_obj_loc(&directory.new_order, &data_builder, &nonce)?; + http::get_obj_loc(root_certs, &directory.new_order, &data_builder, &nonce)?; // 5. Get all the required authorizations for auth_url in order.authorizations.iter() { let data_builder = set_empty_data_builder!(account, auth_url); let (auth, new_nonce): (Authorization, String) = - http::get_obj(&auth_url, &data_builder, &nonce)?; + http::get_obj(root_certs, &auth_url, &data_builder, &nonce)?; nonce = new_nonce; if auth.status == AuthorizationStatus::Valid { @@ -114,7 +114,7 @@ pub fn request_certificate(cert: &Certificate) -> Result<(), Error> { // 8. Tell the server the challenge has been completed let chall_url = challenge.get_url(); let data_builder = set_data_builder!(account, b"{}", chall_url); - let new_nonce = http::post_challenge_response(&chall_url, &data_builder, &nonce)?; + let new_nonce = http::post_challenge_response(root_certs, &chall_url, &data_builder, &nonce)?; nonce = new_nonce; } } @@ -123,7 +123,7 @@ pub fn request_certificate(cert: &Certificate) -> Result<(), Error> { let data_builder = set_empty_data_builder!(account, auth_url); let break_fn = |a: &Authorization| a.status == AuthorizationStatus::Valid; let (_, new_nonce): (Authorization, String) = - http::pool_obj(&auth_url, &data_builder, &break_fn, &nonce)?; + http::pool_obj(root_certs, &auth_url, &data_builder, &break_fn, &nonce)?; nonce = new_nonce; } @@ -131,26 +131,26 @@ pub fn request_certificate(cert: &Certificate) -> Result<(), Error> { let data_builder = set_empty_data_builder!(account, order_url); let break_fn = |o: &Order| o.status == OrderStatus::Ready; let (order, nonce): (Order, String) = - http::pool_obj(&order_url, &data_builder, &break_fn, &nonce)?; + http::pool_obj(root_certs, &order_url, &data_builder, &break_fn, &nonce)?; // 11. Finalize the order by sending the CSR let (priv_key, pub_key) = certificate::get_key_pair(cert)?; let csr = certificate::generate_csr(cert, &priv_key, &pub_key)?; let data_builder = set_data_builder!(account, csr.as_bytes(), order.finalize); - let (_, nonce): (Order, String) = http::get_obj(&order.finalize, &data_builder, &nonce)?; + let (_, nonce): (Order, String) = http::get_obj(root_certs, &order.finalize, &data_builder, &nonce)?; // 12. Pool the order in order to see whether or not it is valid let data_builder = set_empty_data_builder!(account, order_url); let break_fn = |o: &Order| o.status == OrderStatus::Valid; let (order, nonce): (Order, String) = - http::pool_obj(&order_url, &data_builder, &break_fn, &nonce)?; + http::pool_obj(root_certs, &order_url, &data_builder, &break_fn, &nonce)?; // 13. Download the certificate let crt_url = order .certificate .ok_or_else(|| Error::from("No certificate available for download."))?; let data_builder = set_empty_data_builder!(account, crt_url); - let (crt, _) = http::get_certificate(&crt_url, &data_builder, &nonce)?; + let (crt, _) = http::get_certificate(root_certs, &crt_url, &data_builder, &nonce)?; storage::write_certificate(cert, &crt.as_bytes())?; info!("Certificate renewed for {}", cert.domains.join(", ")); diff --git a/acmed/src/acme_proto/account.rs b/acmed/src/acme_proto/account.rs index 3011f74..8ea5449 100644 --- a/acmed/src/acme_proto/account.rs +++ b/acmed/src/acme_proto/account.rs @@ -20,6 +20,7 @@ impl AccountManager { cert: &Certificate, directory: &Directory, nonce: &str, + root_certs: &[String], ) -> Result<(Self, String), Error> { // TODO: store the key id (account url) let (priv_key, pub_key) = if storage::account_files_exists(cert) { @@ -42,7 +43,7 @@ impl AccountManager { let data_builder = |n: &str| encode_jwk(&priv_key, account.as_bytes(), &directory.new_account, n); let (acc_rep, account_url, nonce): (AccountResponse, String, String) = - http::get_obj_loc(&directory.new_account, &data_builder, &nonce)?; + http::get_obj_loc(root_certs, &directory.new_account, &data_builder, &nonce)?; let ac = AccountManager { priv_key, pub_key, diff --git a/acmed/src/acme_proto/http.rs b/acmed/src/acme_proto/http.rs index d0ca4de..aa67fba 100644 --- a/acmed/src/acme_proto/http.rs +++ b/acmed/src/acme_proto/http.rs @@ -6,6 +6,7 @@ use http_req::uri::Uri; use log::{debug, trace, warn}; use std::str::FromStr; use std::{thread, time}; +use std::path::Path; const CONTENT_TYPE_JOSE: &str = "application/jose+json"; const CONTENT_TYPE_JSON: &str = "application/json"; @@ -24,7 +25,7 @@ impl FromStr for DummyString { } } -fn new_request(uri: &Uri, method: Method) -> Request { +fn new_request<'a>(root_certs: &'a [String], uri: &'a Uri, method: Method) -> Request<'a> { debug!("{}: {}", method, uri); let useragent = format!( "{}/{} ({}) {}", @@ -34,6 +35,9 @@ fn new_request(uri: &Uri, method: Method) -> Request { env!("ACMED_HTTP_LIB_AGENT") ); let mut rb = Request::new(uri); + for file_name in root_certs.iter() { + rb.root_cert_file_pem(&Path::new(file_name)); + } rb.method(method); rb.header("User-Agent", &useragent); // TODO: allow to configure the language @@ -93,9 +97,9 @@ fn nonce_from_response(res: &Response) -> Result { } } -fn post_jose_type(url: &str, data: &[u8], accept_type: &str) -> Result<(Response, String), Error> { +fn post_jose_type(root_certs: &[String], url: &str, data: &[u8], accept_type: &str) -> Result<(Response, String), Error> { let uri = url.parse::()?; - let mut request = new_request(&uri, Method::POST); + let mut request = new_request(root_certs, &uri, Method::POST); request.header("Content-Type", CONTENT_TYPE_JOSE); request.header("Content-Length", &data.len().to_string()); request.header("Accept", accept_type); @@ -116,6 +120,7 @@ fn check_response(res: &Response, body: &str) -> Result<(), AcmeError> { } fn fetch_obj_type( + root_certs: &[String], url: &str, data_builder: &G, nonce: &str, @@ -128,7 +133,7 @@ where let mut nonce = nonce.to_string(); for _ in 0..crate::DEFAULT_HTTP_FAIL_NB_RETRY { let data = data_builder(&nonce)?; - let (res, res_body) = post_jose_type(url, data.as_bytes(), accept_type)?; + let (res, res_body) = post_jose_type(root_certs, url, data.as_bytes(), accept_type)?; nonce = nonce_from_response(&res)?; match check_response(&res, &res_body) { @@ -150,15 +155,16 @@ where Err("Too much errors, will not retry".into()) } -fn fetch_obj(url: &str, data_builder: &G, nonce: &str) -> Result<(T, String, String), Error> +fn fetch_obj(root_certs: &[String], url: &str, data_builder: &G, nonce: &str) -> Result<(T, String, String), Error> where T: std::str::FromStr, G: Fn(&str) -> Result, { - fetch_obj_type(url, data_builder, nonce, CONTENT_TYPE_JSON) + fetch_obj_type(root_certs, url, data_builder, nonce, CONTENT_TYPE_JSON) } pub fn get_obj_loc( + root_certs: &[String], url: &str, data_builder: &G, nonce: &str, @@ -167,7 +173,7 @@ where T: std::str::FromStr, G: Fn(&str) -> Result, { - let (obj, location, nonce) = fetch_obj(url, data_builder, nonce)?; + let (obj, location, nonce) = fetch_obj(root_certs, url, data_builder, nonce)?; if location.is_empty() { Err("Location header not found.".into()) } else { @@ -175,16 +181,17 @@ where } } -pub fn get_obj(url: &str, data_builder: &G, nonce: &str) -> Result<(T, String), Error> +pub fn get_obj(root_certs: &[String], url: &str, data_builder: &G, nonce: &str) -> Result<(T, String), Error> where T: std::str::FromStr, G: Fn(&str) -> Result, { - let (obj, _, nonce) = fetch_obj(url, data_builder, nonce)?; + let (obj, _, nonce) = fetch_obj(root_certs, url, data_builder, nonce)?; Ok((obj, nonce)) } pub fn pool_obj( + root_certs: &[String], url: &str, data_builder: &G, break_fn: &S, @@ -198,7 +205,7 @@ where let mut nonce: String = nonce.to_string(); for _ in 0..crate::DEFAULT_POOL_NB_TRIES { thread::sleep(time::Duration::from_secs(crate::DEFAULT_POOL_WAIT_SEC)); - let (obj, _, new_nonce) = fetch_obj(url, data_builder, &nonce)?; + let (obj, _, new_nonce) = fetch_obj(root_certs, url, data_builder, &nonce)?; if break_fn(&obj) { return Ok((obj, new_nonce)); } @@ -208,15 +215,16 @@ where Err(msg.into()) } -pub fn post_challenge_response(url: &str, data_builder: &G, nonce: &str) -> Result +pub fn post_challenge_response(root_certs: &[String], url: &str, data_builder: &G, nonce: &str) -> Result where G: Fn(&str) -> Result, { - let (_, _, nonce): (DummyString, String, String) = fetch_obj(url, data_builder, nonce)?; + let (_, _, nonce): (DummyString, String, String) = fetch_obj(root_certs, url, data_builder, nonce)?; Ok(nonce) } pub fn get_certificate( + root_certs: &[String], url: &str, data_builder: &G, nonce: &str, @@ -224,22 +232,22 @@ pub fn get_certificate( where G: Fn(&str) -> Result, { - let (res_body, _, nonce): (DummyString, String, String) = fetch_obj(url, data_builder, nonce)?; + let (res_body, _, nonce): (DummyString, String, String) = fetch_obj(root_certs, url, data_builder, nonce)?; Ok((res_body.content, nonce)) } -pub fn get_directory(url: &str) -> Result { +pub fn get_directory(root_certs: &[String], url: &str) -> Result { let uri = url.parse::()?; - let mut request = new_request(&uri, Method::GET); + let mut request = new_request(root_certs, &uri, Method::GET); request.header("Accept", CONTENT_TYPE_JSON); let (r, s) = send_request_retry(&request)?; check_response(&r, &s)?; Directory::from_str(&s) } -pub fn get_nonce(url: &str) -> Result { +pub fn get_nonce(root_certs: &[String], url: &str) -> Result { let uri = url.parse::()?; - let request = new_request(&uri, Method::HEAD); + let request = new_request(root_certs, &uri, Method::HEAD); let (res, res_body) = send_request_retry(&request)?; check_response(&res, &res_body)?; nonce_from_response(&res) diff --git a/acmed/src/main.rs b/acmed/src/main.rs index 0a86163..e910f40 100644 --- a/acmed/src/main.rs +++ b/acmed/src/main.rs @@ -75,6 +75,14 @@ fn main() { .value_name("FILE") .conflicts_with("foregroung"), ) + .arg( + Arg::with_name("root-cert") + .long("root-cert") + .help("Add a root certificate to the trust store") + .takes_value(true) + .multiple(true) + .value_name("FILE") + ) .get_matches(); match acme_common::logs::set_log_system( @@ -89,13 +97,18 @@ fn main() { } }; + let root_certs = match matches.values_of("root-cert") { + Some(v) => v.collect(), + None => vec![], + }; + init_server( matches.is_present("foregroung"), matches.value_of("pid-file").unwrap_or(DEFAULT_PID_FILE), ); let config_file = matches.value_of("config").unwrap_or(DEFAULT_CONFIG_FILE); - let mut srv = match MainEventLoop::new(&config_file) { + let mut srv = match MainEventLoop::new(&config_file, &root_certs) { Ok(s) => s, Err(e) => { error!("{}", e); diff --git a/acmed/src/main_event_loop.rs b/acmed/src/main_event_loop.rs index 2cad4ff..5cc7182 100644 --- a/acmed/src/main_event_loop.rs +++ b/acmed/src/main_event_loop.rs @@ -8,10 +8,11 @@ use std::time::Duration; pub struct MainEventLoop { certs: Vec, + root_certs: Vec, } impl MainEventLoop { - pub fn new(config_file: &str) -> Result { + pub fn new(config_file: &str, root_certs: &[&str]) -> Result { let cnf = config::from_file(config_file)?; let mut certs = Vec::new(); @@ -44,7 +45,10 @@ impl MainEventLoop { certs.push(cert); } - Ok(MainEventLoop { certs }) + Ok(MainEventLoop { + certs, + root_certs: root_certs.iter().map(|v| v.to_string()).collect(), + }) } pub fn run(&mut self) { @@ -54,7 +58,7 @@ impl MainEventLoop { match crt.should_renew() { Ok(sr) => { if sr { - let status = match request_certificate(crt) { + let status = match request_certificate(crt, &self.root_certs) { Ok(_) => "Success.".to_string(), Err(e) => { let msg = format!(