Browse Source

Remove the `acme-lib` dependency

Although the `acme-lib` crate comes very handy when creating a simple
ACME client, a more advanced one may not want to use it. First of all,
`acme-lib` does not support all of the ACME specification, some very
interesting one are not implemented. Also, it does not store the account
public key, which does not allow to revoke certificates.
In addition to those two critical points, I would also add `acme-lib`
has some troubles with its own dependencies: it uses both `openssl` and
`ring`, which is redundant and contribute to inflate the size of the
binary. Some of the dependencies also makes an excessive use of logging:
even if logging is very useful, in some cases it really is too much and
debugging becomes a nightmare.

ref #1
pull/5/head
Rodolphe Breard 6 years ago
parent
commit
99f7ceabec
  1. 10
      CHANGELOG.md
  2. 13
      acmed/Cargo.toml
  3. 78
      acmed/build.rs
  4. 212
      acmed/src/acme_proto.rs
  5. 54
      acmed/src/acme_proto/account.rs
  6. 58
      acmed/src/acme_proto/certificate.rs
  7. 175
      acmed/src/acme_proto/http.rs
  8. 158
      acmed/src/acme_proto/jws.rs
  9. 181
      acmed/src/acme_proto/jws/algorithms.rs
  10. 43
      acmed/src/acme_proto/jws/jwk.rs
  11. 23
      acmed/src/acme_proto/structs.rs
  12. 154
      acmed/src/acme_proto/structs/account.rs
  13. 299
      acmed/src/acme_proto/structs/authorization.rs
  14. 147
      acmed/src/acme_proto/structs/directory.rs
  15. 135
      acmed/src/acme_proto/structs/order.rs
  16. 333
      acmed/src/acmed.rs
  17. 159
      acmed/src/certificate.rs
  18. 58
      acmed/src/config.rs
  19. 203
      acmed/src/encoding.rs
  20. 87
      acmed/src/error.rs
  21. 77
      acmed/src/errors.rs
  22. 61
      acmed/src/hooks.rs
  23. 45
      acmed/src/keygen.rs
  24. 10
      acmed/src/logs.rs
  25. 25
      acmed/src/main.rs
  26. 92
      acmed/src/main_event_loop.rs
  27. 413
      acmed/src/storage.rs

10
CHANGELOG.md

@ -5,6 +5,16 @@ 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]
### Changed
- The `token` challenge hook variable has been renamed `file_name`.
- The logs has been purged from many useless debug and trace entries.
### Removed
- The DER storage format has been removed.
## [0.2.1] - 2019-03-30
### Changed

13
acmed/Cargo.toml

@ -9,21 +9,26 @@ repository = "https://github.com/breard-r/acmed"
readme = "README.md"
license = "MIT OR Apache-2.0"
include = ["src/**/*", "Cargo.toml", "LICENSE-*.txt"]
build = "build.rs"
[dependencies]
acme-lib = "0.5"
base64 = "0.10"
clap = "2.32"
daemonize = "0.3"
daemonize = "0.4"
env_logger = "0.6"
handlebars = "2.0.0-beta.1"
http_req = "0.4"
log = "0.4"
openssl = "0.10"
pem = "0.5"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
syslog = "4.0"
time = "0.1"
toml = "0.5"
x509-parser = "0.4"
[build-dependencies]
serde = { version = "1.0", features = ["derive"] }
toml = "0.5"
[target.'cfg(unix)'.dependencies]
nix = "0.13"

78
acmed/build.rs

@ -0,0 +1,78 @@
extern crate serde;
extern crate toml;
use serde::Deserialize;
use std::env;
use std::fs::File;
use std::io::prelude::*;
use std::path::PathBuf;
macro_rules! set_rustc_env_var {
($name: expr, $value: expr) => {{
println!("cargo:rustc-env={}={}", $name, $value);
}};
}
#[derive(Deserialize)]
pub struct Lock {
package: Vec<Package>,
}
#[derive(Deserialize)]
struct Package {
name: String,
version: String,
}
struct Error;
impl From<std::io::Error> for Error {
fn from(_error: std::io::Error) -> Self {
Error {}
}
}
impl From<toml::de::Error> for Error {
fn from(_error: toml::de::Error) -> Self {
Error {}
}
}
fn get_lock() -> Result<Lock, Error> {
let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
path.pop();
path.push("Cargo.lock");
let mut file = File::open(path)?;
let mut contents = String::new();
file.read_to_string(&mut contents)?;
let ret: Lock = toml::from_str(&contents)?;
Ok(ret)
}
fn set_lock() {
let lock = match get_lock() {
Ok(l) => l,
Err(_) => {
return;
}
};
for p in lock.package.iter() {
if p.name == "http_req" {
let agent = format!("{}/{}", p.name, p.version);
set_rustc_env_var!("ACMED_HTTP_LIB_AGENT", agent);
return;
}
}
}
fn set_target() {
match env::var("TARGET") {
Ok(target) => set_rustc_env_var!("ACMED_TARGET", target),
Err(_) => {}
};
}
fn main() {
set_target();
set_lock();
}

212
acmed/src/acme_proto.rs

@ -0,0 +1,212 @@
use crate::acme_proto::account::AccountManager;
use crate::acme_proto::jws::encode_kid;
use crate::acme_proto::structs::{
Authorization, AuthorizationStatus, NewOrder, Order, OrderStatus,
};
use crate::certificate::Certificate;
use crate::error::Error;
use crate::storage;
use log::info;
use std::{fmt, thread, time};
mod account;
mod certificate;
mod http;
pub mod jws;
mod structs;
#[derive(Clone, Debug, PartialEq)]
pub enum Challenge {
Http01,
Dns01,
}
impl Challenge {
pub fn from_str(s: &str) -> Result<Self, Error> {
match s.to_lowercase().as_str() {
"http-01" => Ok(Challenge::Http01),
"dns-01" => Ok(Challenge::Dns01),
_ => Err(format!("{}: unknown challenge.", s).into()),
}
}
}
impl fmt::Display for Challenge {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let s = match self {
Challenge::Http01 => "http-01",
Challenge::Dns01 => "dns-01",
};
write!(f, "{}", s)
}
}
impl PartialEq<structs::Challenge> for Challenge {
fn eq(&self, other: &structs::Challenge) -> bool {
match (self, other) {
(Challenge::Http01, structs::Challenge::Http01(_)) => true,
(Challenge::Dns01, structs::Challenge::Dns01(_)) => true,
_ => false,
}
}
}
fn pool<T, F, G>(
account: &AccountManager,
url: &str,
nonce: &str,
get_fn: F,
break_fn: G,
) -> Result<(T, String), Error>
where
F: Fn(&str, &[u8]) -> Result<(T, String), Error>,
G: Fn(&T) -> bool,
{
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 data = encode_kid(&account.priv_key, &account.account_url, b"", url, &nonce)?;
let (obj, new_nonce) = get_fn(url, data.as_bytes())?;
if break_fn(&obj) {
return Ok((obj, new_nonce));
}
nonce = new_nonce;
}
let msg = format!("Pooling failed for {}", url);
Err(msg.into())
}
pub fn request_certificate(cert: &Certificate) -> Result<(), Error> {
// 1. Get the directory
let directory = http::get_directory(&cert.remote_url)?;
// 2. Get a first nonce
let nonce = http::get_nonce(&directory.new_nonce)?;
// 3. Get or create the account
let (account, nonce) = AccountManager::new(cert, &directory, &nonce)?;
// 4. Create a new order
let new_order = NewOrder::new(&cert.domains);
let new_order = serde_json::to_string(&new_order)?;
let new_order = encode_kid(
&account.priv_key,
&account.account_url,
new_order.as_bytes(),
&directory.new_order,
&nonce,
)?;
let (order, order_url, mut nonce) =
http::get_obj_loc::<Order>(&directory.new_order, new_order.as_bytes())?;
// 5. Get all the required authorizations
for auth_url in order.authorizations.iter() {
let auth_data = encode_kid(
&account.priv_key,
&account.account_url,
b"",
&auth_url,
&nonce,
)?;
let (auth, new_nonce) = http::get_obj::<Authorization>(&auth_url, auth_data.as_bytes())?;
nonce = new_nonce;
if auth.status == AuthorizationStatus::Valid {
continue;
}
if auth.status != AuthorizationStatus::Pending {
let msg = format!(
"{}: authorization status is {}",
auth.identifier, auth.status
);
return Err(msg.into());
}
// 6. For each authorization, fetch the associated challenges
for challenge in auth.challenges.iter() {
if cert.challenge == *challenge {
let proof = challenge.get_proof(&account.priv_key)?;
let file_name = challenge.get_file_name();
let domain = auth.identifier.value.to_owned();
// 7. Call the challenge hook in order to complete it
cert.call_challenge_hooks(&file_name, &proof, &domain)?;
// 8. Tell the server the challenge has been completed
let chall_url = challenge.get_url();
let chall_resp_data = encode_kid(
&account.priv_key,
&account.account_url,
b"{}",
&chall_url,
&nonce,
)?;
let new_nonce =
http::post_challenge_response(&chall_url, chall_resp_data.as_bytes())?;
nonce = new_nonce;
}
}
// 9. Pool the authorization in order to see whether or not it is valid
let (_, new_nonce) = pool(
&account,
&auth_url,
&nonce,
|u, d| http::get_obj::<Authorization>(u, d),
|a| a.status == AuthorizationStatus::Valid,
)?;
nonce = new_nonce;
}
// 10. Pool the order in order to see whether or not it is ready
let (order, nonce) = pool(
&account,
&order_url,
&nonce,
|u, d| http::get_obj::<Order>(u, d),
|a| a.status == OrderStatus::Ready,
)?;
// 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 csr_data = encode_kid(
&account.priv_key,
&account.account_url,
csr.as_bytes(),
&order.finalize,
&nonce,
)?;
let (_, nonce) = http::get_obj::<Order>(&order.finalize, &csr_data.as_bytes())?;
// 12. Pool the order in order to see whether or not it is valid
let (order, nonce) = pool(
&account,
&order_url,
&nonce,
|u, d| http::get_obj::<Order>(u, d),
|a| a.status == OrderStatus::Valid,
)?;
// 13. Download the certificate
// TODO: implement
let crt_url = order
.certificate
.ok_or_else(|| Error::from("No certificate available for download."))?;
let crt_data = encode_kid(
&account.priv_key,
&account.account_url,
b"",
&crt_url,
&nonce,
)?;
let (crt, _) = http::get_certificate(&crt_url, &crt_data.as_bytes())?;
storage::write_certificate(cert, &crt.as_bytes())?;
info!("Certificate renewed for {}", cert.domains.join(", "));
Ok(())
}
pub fn b64_encode<T: ?Sized + AsRef<[u8]>>(input: &T) -> String {
base64::encode_config(input, base64::URL_SAFE_NO_PAD)
}

54
acmed/src/acme_proto/account.rs

@ -0,0 +1,54 @@
use crate::acme_proto::http;
use crate::acme_proto::jws::algorithms::SignatureAlgorithm;
use crate::acme_proto::jws::encode_jwk;
use crate::acme_proto::structs::{Account, AccountResponse, Directory};
use crate::certificate::Certificate;
use crate::error::Error;
use crate::storage;
use openssl::pkey::{PKey, Private, Public};
use std::str::FromStr;
pub struct AccountManager {
pub priv_key: PKey<Private>,
pub pub_key: PKey<Public>,
pub account_url: String,
pub orders_url: String,
}
impl AccountManager {
pub fn new(
cert: &Certificate,
directory: &Directory,
nonce: &str,
) -> Result<(Self, String), Error> {
// TODO: store the key id (account url)
let (priv_key, pub_key) = if storage::account_files_exists(cert) {
// TODO: check if the keys are suitable for the specified signature algorithm
// and, if not, initiate a key rollover.
(
storage::get_account_priv_key(cert)?,
storage::get_account_pub_key(cert)?,
)
} else {
// TODO: allow to change the signature algo
let sign_alg = SignatureAlgorithm::from_str(crate::DEFAULT_JWS_SIGN_ALGO)?;
let (priv_key, pub_key) = sign_alg.gen_key_pair()?;
storage::set_account_priv_key(cert, &priv_key)?;
storage::set_account_pub_key(cert, &pub_key)?;
(priv_key, pub_key)
};
let account = Account::new(&[cert.email.to_owned()]);
let account = serde_json::to_string(&account)?;
let data = encode_jwk(&priv_key, account.as_bytes(), &directory.new_account, nonce)?;
let (acc_rep, account_url, nonce) =
http::get_obj_loc::<AccountResponse>(&directory.new_account, data.as_bytes())?;
let ac = AccountManager {
priv_key,
pub_key,
account_url,
orders_url: acc_rep.orders.unwrap_or_default(),
};
// TODO: check account data and, if different from config, update them
Ok((ac, nonce))
}
}

58
acmed/src/acme_proto/certificate.rs

@ -0,0 +1,58 @@
use crate::certificate::{Algorithm, Certificate};
use crate::error::Error;
use crate::{keygen, storage};
use openssl::hash::MessageDigest;
use openssl::pkey::{PKey, Private, Public};
use openssl::stack::Stack;
use openssl::x509::extension::SubjectAlternativeName;
use openssl::x509::X509ReqBuilder;
use serde_json::json;
fn gen_key_pair(cert: &Certificate) -> Result<(PKey<Private>, PKey<Public>), Error> {
let (priv_key, pub_key) = match cert.algo {
Algorithm::Rsa2048 => keygen::rsa2048(),
Algorithm::Rsa4096 => keygen::rsa4096(),
Algorithm::EcdsaP256 => keygen::p256(),
Algorithm::EcdsaP384 => keygen::p384(),
}?;
storage::set_priv_key(cert, &priv_key)?;
Ok((priv_key, pub_key))
}
fn read_key_pair(cert: &Certificate) -> Result<(PKey<Private>, PKey<Public>), Error> {
let priv_key = storage::get_priv_key(cert)?;
let pub_key = storage::get_pub_key(cert)?;
Ok((priv_key, pub_key))
}
pub fn get_key_pair(cert: &Certificate) -> Result<(PKey<Private>, PKey<Public>), Error> {
if cert.kp_reuse {
match read_key_pair(cert) {
Ok((priv_key, pub_key)) => Ok((priv_key, pub_key)),
Err(_) => gen_key_pair(cert),
}
} else {
gen_key_pair(cert)
}
}
pub fn generate_csr(
cert: &Certificate,
priv_key: &PKey<Private>,
pub_key: &PKey<Public>,
) -> Result<String, Error> {
let domains = cert.domains.join(", DNS:");
let mut builder = X509ReqBuilder::new()?;
builder.set_pubkey(pub_key)?;
let ctx = builder.x509v3_context(None);
let san = SubjectAlternativeName::new().dns(&domains).build(&ctx)?;
let mut ext_stack = Stack::new()?;
ext_stack.push(san)?;
builder.add_extensions(&ext_stack)?;
builder.sign(priv_key, MessageDigest::sha256())?;
let csr = builder.build();
let csr = csr.to_der()?;
let csr = super::b64_encode(&csr);
let csr = json!({ "csr": csr });
Ok(csr.to_string())
}

175
acmed/src/acme_proto/http.rs

@ -0,0 +1,175 @@
use crate::acme_proto::structs::Directory;
use crate::error::Error;
use http_req::request::{Method, Request};
use http_req::response::Response;
use http_req::uri::Uri;
use log::{debug, trace};
use std::str::FromStr;
const CONTENT_TYPE_JOSE: &str = "application/jose+json";
const CONTENT_TYPE_JSON: &str = "application/json";
fn new_request(uri: &Uri, method: Method) -> Request {
debug!("{}: {}", method, uri);
let useragent = format!(
"{}/{} ({}) {}",
crate::APP_NAME,
crate::APP_VERSION,
env!("ACMED_TARGET"),
env!("ACMED_HTTP_LIB_AGENT")
);
let mut rb = Request::new(uri);
rb.method(method);
rb.header("User-Agent", &useragent);
// TODO: allow to configure the language
rb.header("Accept-Language", "en-US,en;q=0.5");
rb
}
fn send_request(request: &Request) -> Result<(Response, String), Error> {
let mut buffer = Vec::new();
let res = request.send(&mut buffer)?;
let res_str = String::from_utf8(buffer)?;
if !res.status_code().is_success() {
debug!("Response: {}", res_str);
let msg = format!("HTTP error: {}: {}", res.status_code(), res.reason());
return Err(msg.into());
}
Ok((res, res_str))
}
fn check_response(_res: &Response) -> Result<(), Error> {
// TODO: implement
Ok(())
}
fn get_header(res: &Response, name: &str) -> Result<String, Error> {
match res.headers().get(name) {
Some(v) => Ok(v.to_string()),
None => Err(format!("{}: header not found.", name).into()),
}
}
fn is_nonce(data: &str) -> bool {
!data.is_empty()
&& data
.bytes()
.all(|c| c.is_ascii_alphanumeric() || c == b'-' || c == b'_')
}
fn nonce_from_response(res: &Response) -> Result<String, Error> {
let nonce = get_header(res, "Replay-Nonce")?;
if is_nonce(&nonce) {
trace!("New nonce: {}", nonce);
Ok(nonce.to_string())
} else {
let msg = format!("{}: invalid nonce.", nonce);
Err(msg.into())
}
}
fn post_jose_type(url: &str, data: &[u8], accept_type: &str) -> Result<(Response, String), Error> {
let uri = url.parse::<Uri>()?;
let mut request = new_request(&uri, Method::POST);
request.header("Content-Type", CONTENT_TYPE_JOSE);
request.header("Content-Length", &data.len().to_string());
request.header("Accept", accept_type);
request.body(data);
let rstr = String::from_utf8_lossy(data);
trace!("post_jose: request body: {}", rstr);
let (res, res_body) = send_request(&request)?;
trace!("post_jose: response body: {}", res_body);
check_response(&res)?;
Ok((res, res_body))
}
fn post_jose(url: &str, data: &[u8]) -> Result<(Response, String), Error> {
post_jose_type(url, data, CONTENT_TYPE_JSON)
}
pub fn get_directory(url: &str) -> Result<Directory, Error> {
let uri = url.parse::<Uri>()?;
let mut request = new_request(&uri, Method::GET);
request.header("Accept", CONTENT_TYPE_JSON);
let (r, s) = send_request(&request)?;
check_response(&r)?;
Directory::from_str(&s)
}
pub fn get_nonce(url: &str) -> Result<String, Error> {
let uri = url.parse::<Uri>()?;
let request = new_request(&uri, Method::HEAD);
let (res, _) = send_request(&request)?;
check_response(&res)?;
nonce_from_response(&res)
}
pub fn get_obj<T>(url: &str, data: &[u8]) -> Result<(T, String), Error>
where
T: std::str::FromStr<Err = Error>,
{
let (res, res_body) = post_jose(url, data)?;
let obj = T::from_str(&res_body)?;
let nonce = nonce_from_response(&res)?;
Ok((obj, nonce))
}
pub fn get_obj_loc<T>(url: &str, data: &[u8]) -> Result<(T, String, String), Error>
where
T: std::str::FromStr<Err = Error>,
{
let (res, res_body) = post_jose(url, data)?;
let obj = T::from_str(&res_body)?;
let location = get_header(&res, "Location")?;
let nonce = nonce_from_response(&res)?;
Ok((obj, location, nonce))
}
pub fn post_challenge_response(url: &str, data: &[u8]) -> Result<String, Error> {
let (res, _) = post_jose(url, data)?;
let nonce = nonce_from_response(&res)?;
Ok(nonce)
}
pub fn get_certificate(url: &str, data: &[u8]) -> Result<(String, String), Error> {
let (res, res_body) = post_jose_type(url, data, CONTENT_TYPE_JSON)?;
let nonce = nonce_from_response(&res)?;
Ok((res_body, nonce))
}
#[cfg(test)]
mod tests {
use super::is_nonce;
#[test]
fn test_nonce_valid() {
let lst = [
"XFHw3qcgFNZAdw",
"XFHw3qcg-NZAdw",
"XFHw3qcg_NZAdw",
"XFHw3qcg-_ZAdw",
"a",
"1",
"-",
"_",
];
for n in lst.iter() {
assert!(is_nonce(n));
}
}
#[test]
fn test_nonce_invalid() {
let lst = [
"",
"rdo9x8gS4K/mZg==",
"rdo9x8gS4K/mZg",
"rdo9x8gS4K+mZg",
"৬",
"京",
];
for n in lst.iter() {
assert!(!is_nonce(n));
}
}
}

158
acmed/src/acme_proto/jws.rs

@ -0,0 +1,158 @@
use crate::acme_proto::b64_encode;
use crate::acme_proto::jws::algorithms::{EdDsaVariant, SignatureAlgorithm};
use crate::error::Error;
use openssl::ecdsa::EcdsaSig;
use openssl::pkey::{PKey, Private};
use openssl::sha::sha256;
use serde::Serialize;
pub mod algorithms;
mod jwk;
#[derive(Serialize)]
struct JwsData {
protected: String,
payload: String,
signature: String,
}
#[derive(Serialize)]
struct JwsProtectedHeaderJwk {
alg: String,
jwk: jwk::Jwk,
nonce: String,
url: String,
}
#[derive(Serialize)]
struct JwsProtectedHeaderKid {
alg: String,
kid: String,
nonce: String,
url: String,
}
fn es256_sign(data: &[u8], private_key: &PKey<Private>) -> Result<String, Error> {
let signature = EcdsaSig::sign(data, private_key.ec_key()?.as_ref())?;
let r = signature.r().to_vec();
let mut s = signature.s().to_vec();
let mut signature = r;
signature.append(&mut s);
let signature = b64_encode(&signature);
Ok(signature)
}
fn eddsa_ed25519_sign(_data: &[u8], _private_key: &PKey<Private>) -> Result<String, Error> {
// TODO: implement
Err("EdDSA not implemented.".into())
}
fn get_data(
private_key: &PKey<Private>,
protected: &str,
payload: &[u8],
sign_alg: SignatureAlgorithm,
) -> Result<String, Error> {
let protected = b64_encode(protected);
let payload = b64_encode(payload);
let signing_input = format!("{}.{}", protected, payload);
let fingerprint = sha256(signing_input.as_bytes());
let signature = match sign_alg {
SignatureAlgorithm::Es256 => es256_sign(&fingerprint, private_key)?,
SignatureAlgorithm::EdDsa(variant) => match variant {
EdDsaVariant::Ed25519 => eddsa_ed25519_sign(&fingerprint, private_key)?,
},
};
let data = JwsData {
protected,
payload,
signature,
};
let str_data = serde_json::to_string(&data)?;
Ok(str_data)
}
pub fn encode_jwk(
private_key: &PKey<Private>,
payload: &[u8],
url: &str,
nonce: &str,
) -> Result<String, Error> {
let sign_alg = SignatureAlgorithm::from_pkey(private_key)?;
let protected = JwsProtectedHeaderJwk {
alg: sign_alg.to_string(),
jwk: sign_alg.get_jwk(private_key)?,
nonce: nonce.into(),
url: url.into(),
};
let protected = serde_json::to_string(&protected)?;
get_data(private_key, &protected, payload, sign_alg)
}
pub fn encode_kid(
private_key: &PKey<Private>,
key_id: &str,
payload: &[u8],
url: &str,
nonce: &str,
) -> Result<String, Error> {
let sign_alg = SignatureAlgorithm::from_pkey(private_key)?;
let protected = JwsProtectedHeaderKid {
alg: sign_alg.to_string(),
kid: key_id.to_string(),
nonce: nonce.into(),
url: url.into(),
};
let protected = serde_json::to_string(&protected)?;
get_data(private_key, &protected, payload, sign_alg)
}
#[cfg(test)]
mod tests {
use super::{encode_jwk, encode_kid};
#[test]
fn test_default_jwk() {
let (priv_key, _) = crate::keygen::p256().unwrap();
let payload = "Dummy payload 1";
let payload_b64 = "RHVtbXkgcGF5bG9hZCAx";
let s = encode_jwk(&priv_key, payload.as_bytes(), "", "");
assert!(s.is_ok());
let s = s.unwrap();
assert!(s.contains("\"protected\""));
assert!(s.contains("\"payload\""));
assert!(s.contains("\"signature\""));
assert!(s.contains(payload_b64));
}
#[test]
fn test_default_nopad_jwk() {
let (priv_key, _) = crate::keygen::p256().unwrap();
let payload = "Dummy payload";
let payload_b64 = "RHVtbXkgcGF5bG9hZA";
let payload_b64_pad = "RHVtbXkgcGF5bG9hZA==";
let s = encode_jwk(&priv_key, payload.as_bytes(), "", "");
assert!(s.is_ok());
let s = s.unwrap();
assert!(s.contains("\"protected\""));
assert!(s.contains("\"payload\""));
assert!(s.contains("\"signature\""));
assert!(s.contains(payload_b64));
assert!(!s.contains(payload_b64_pad));
}
#[test]
fn test_default_kid() {
let (priv_key, _) = crate::keygen::p256().unwrap();
let payload = "Dummy payload 1";
let payload_b64 = "RHVtbXkgcGF5bG9hZCAx";
let key_id = "0x2a";
let s = encode_kid(&priv_key, key_id, payload.as_bytes(), "", "");
assert!(s.is_ok());
let s = s.unwrap();
assert!(s.contains("\"protected\""));
assert!(s.contains("\"payload\""));
assert!(s.contains("\"signature\""));
assert!(s.contains(payload_b64));
}
}

181
acmed/src/acme_proto/jws/algorithms.rs

@ -0,0 +1,181 @@
use super::jwk::{EdDsaEd25519Jwk, Es256Jwk, Jwk};
use crate::acme_proto::b64_encode;
use crate::error::Error;
use crate::keygen;
use openssl::bn::{BigNum, BigNumContext};
use openssl::ec::EcGroup;
use openssl::nid::Nid;
use openssl::pkey::{Id, PKey, Private, Public};
use serde_json::json;
use std::fmt;
use std::str::FromStr;
#[derive(Debug, PartialEq, Eq)]
pub enum EdDsaVariant {
Ed25519,
}
impl fmt::Display for EdDsaVariant {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let s = match self {
EdDsaVariant::Ed25519 => "Ed25519",
};
write!(f, "{}", s)
}
}
#[derive(Debug, PartialEq, Eq)]
pub enum SignatureAlgorithm {
Es256,
EdDsa(EdDsaVariant),
}
impl fmt::Display for SignatureAlgorithm {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let s = match self {
SignatureAlgorithm::Es256 => "ES256",
SignatureAlgorithm::EdDsa(_) => "EdDSA",
};
write!(f, "{}", s)
}
}
impl FromStr for SignatureAlgorithm {
type Err = Error;
fn from_str(data: &str) -> Result<Self, Self::Err> {
match data.to_lowercase().as_str() {
"es256" => Ok(SignatureAlgorithm::Es256),
"eddsa-ed25519" => Ok(SignatureAlgorithm::EdDsa(EdDsaVariant::Ed25519)),
_ => Err(format!("{}: unknown signature algorithm", data).into()),
}
}
}
impl SignatureAlgorithm {
pub fn from_pkey(private_key: &PKey<Private>) -> Result<Self, Error> {
match private_key.id() {
Id::EC => match private_key.ec_key()?.group().curve_name() {
Some(nid) => {
if nid == Nid::X9_62_PRIME256V1 {
Ok(SignatureAlgorithm::Es256)
// TODO: add support for Ed25519 keys
} else {
Err(format!("{}: unsupported EC key type", nid.as_raw()).into())
}
}
None => Err("EC curve: name not found".into()),
},
_ => Err(format!("{}: unsupported key id", private_key.id().as_raw()).into()),
}
}
fn get_p256_coordinates(private_key: &PKey<Private>) -> Result<(String, String), Error> {
let group = EcGroup::from_curve_name(Nid::X9_62_PRIME256V1).unwrap();
let mut ctx = BigNumContext::new().unwrap();
let mut x = BigNum::new().unwrap();
let mut y = BigNum::new().unwrap();
private_key
.ec_key()
.unwrap()
.public_key()
.affine_coordinates_gfp(&group, &mut x, &mut y, &mut ctx)?;
let x = b64_encode(&x.to_vec());
let y = b64_encode(&y.to_vec());
Ok((x, y))
}
pub fn get_jwk_thumbprint(&self, private_key: &PKey<Private>) -> Result<String, Error> {
let jwk = match self {
SignatureAlgorithm::Es256 => {
let (x, y) = SignatureAlgorithm::get_p256_coordinates(private_key)?;
json!({
"crv": "P-256",
"kty": "EC",
"x": x,
"y": y,
})
}
SignatureAlgorithm::EdDsa(_crv) => json!({
// TODO: implement EdDsa
}),
};
Ok(jwk.to_string())
}
pub fn get_jwk(&self, private_key: &PKey<Private>) -> Result<Jwk, Error> {
let jwk = match self {
SignatureAlgorithm::Es256 => {
let (x, y) = SignatureAlgorithm::get_p256_coordinates(private_key)?;
Jwk::Es256(Es256Jwk::new(&x, &y))
}
// TODO: implement EdDsa
SignatureAlgorithm::EdDsa(_crv) => Jwk::EdDsaEd25519(EdDsaEd25519Jwk::new()),
};
Ok(jwk)
}
pub fn gen_key_pair(&self) -> Result<(PKey<Private>, PKey<Public>), Error> {
match self {
SignatureAlgorithm::Es256 => keygen::p256(),
SignatureAlgorithm::EdDsa(EdDsaVariant::Ed25519) => Err("Not implemented".into()),
}
}
}
#[cfg(test)]
mod tests {
use super::{EdDsaVariant, SignatureAlgorithm};
use openssl::ec::EcKey;
use openssl::pkey::PKey;
use std::str::FromStr;
#[test]
fn test_es256_from_str() {
let variants = ["ES256", "Es256", "es256"];
for v in variants.iter() {
let a = SignatureAlgorithm::from_str(v);
assert!(a.is_ok());
let a = a.unwrap();
assert_eq!(a, SignatureAlgorithm::Es256);
}
}
#[test]
fn test_es256_to_str() {
let a = SignatureAlgorithm::Es256;
assert_eq!(a.to_string().as_str(), "ES256");
}
#[test]
fn test_eddsa_ed25519_from_str() {
let variants = ["ES256", "Es256", "es256"];
for v in variants.iter() {
let a = SignatureAlgorithm::from_str(v);
assert!(a.is_ok());
let a = a.unwrap();
assert_eq!(a, SignatureAlgorithm::Es256);
}
}
#[test]
fn test_eddsa_ed25519_to_str() {
let a = SignatureAlgorithm::EdDsa(EdDsaVariant::Ed25519);
assert_eq!(a.to_string().as_str(), "EdDSA");
}
#[test]
fn test_from_p256() {
let pem = b"-----BEGIN PRIVATE KEY-----
MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQg6To1BW8qTehGhPca
0eMcW8iQU4yA02dvtKkuqfny4HChRANCAAQwxx+j3wYGzD5LSFNBTLlT7J+7rWrq
4BGdR8705iwpBeOQgMpLj+9vuFutlVtmoYpJSYa9+49Hxz8aCe1AQeWt
-----END PRIVATE KEY-----";
let ek = EcKey::private_key_from_pem(pem).unwrap();
let k = PKey::from_ec_key(ek).unwrap();
let s = SignatureAlgorithm::from_pkey(&k);
assert!(s.is_ok());
let s = s.unwrap();
assert_eq!(s, SignatureAlgorithm::Es256)
}
}

43
acmed/src/acme_proto/jws/jwk.rs

@ -0,0 +1,43 @@
use serde::Serialize;
#[derive(Serialize)]
#[serde(untagged)]
pub enum Jwk {
Es256(Es256Jwk),
EdDsaEd25519(EdDsaEd25519Jwk),
}
#[derive(Serialize)]
pub struct Es256Jwk {
kty: String,
#[serde(rename = "use")]
jwk_use: String,
crv: String,
alg: String,
x: String,
y: String,
}
impl Es256Jwk {
pub fn new(x: &str, y: &str) -> Self {
Es256Jwk {
kty: "EC".into(),
jwk_use: "sig".into(),
crv: "P-256".into(),
alg: "ES256".into(),
x: x.to_string(),
y: y.to_string(),
}
}
}
#[derive(Serialize)]
pub struct EdDsaEd25519Jwk {
// TODO: implement
}
impl EdDsaEd25519Jwk {
pub fn new() -> Self {
EdDsaEd25519Jwk {}
}
}

23
acmed/src/acme_proto/structs.rs

@ -0,0 +1,23 @@
#[macro_export]
macro_rules! deserialize_from_str {
($t: ty) => {
impl FromStr for $t {
type Err = Error;
fn from_str(data: &str) -> Result<Self, Self::Err> {
let res = serde_json::from_str(data)?;
Ok(res)
}
}
};
}
mod account;
mod authorization;
mod directory;
mod order;
pub use account::{Account, AccountDeactivation, AccountResponse, AccountUpdate};
pub use authorization::{Authorization, AuthorizationStatus, Challenge};
pub use directory::Directory;
pub use order::{Identifier, IdentifierType, NewOrder, Order, OrderStatus};

154
acmed/src/acme_proto/structs/account.rs

@ -0,0 +1,154 @@
use crate::error::Error;
use serde::{Deserialize, Serialize};
use std::str::FromStr;
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
pub struct Account {
pub contact: Vec<String>,
pub terms_of_service_agreed: bool,
pub only_return_existing: bool,
}
impl Account {
pub fn new(contact: &[String]) -> Self {
Account {
contact: contact.iter().map(|v| format!("mailto:{}", v)).collect(),
terms_of_service_agreed: true,
only_return_existing: false,
}
}
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct AccountResponse {
pub status: String,
pub contact: Option<Vec<String>>,
pub terms_of_service_agreed: Option<bool>,
pub external_account_binding: Option<String>,
pub orders: Option<String>,
}
deserialize_from_str!(AccountResponse);
// TODO: implement account update
#[allow(dead_code)]
#[derive(Serialize)]
pub struct AccountUpdate {
pub contact: Vec<String>,
}
impl AccountUpdate {
#[allow(dead_code)]
pub fn new(contact: &[String]) -> Self {
AccountUpdate {
contact: contact.into(),
}
}
}
// TODO: implement account deactivation
#[allow(dead_code)]
#[derive(Serialize)]
pub struct AccountDeactivation {
pub status: String,
}
impl AccountDeactivation {
#[allow(dead_code)]
pub fn new() -> Self {
AccountDeactivation {
status: "deactivated".into(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_account_new() {
let emails = vec![
"derp@example.com".to_string(),
"derp.derpson@example.com".to_string(),
];
let a = Account::new(&emails);
assert_eq!(a.contact.len(), 2);
assert_eq!(a.terms_of_service_agreed, true);
assert_eq!(a.only_return_existing, false);
let a_str = serde_json::to_string(&a);
assert!(a_str.is_ok());
let a_str = a_str.unwrap();
assert!(a_str.starts_with("{"));
assert!(a_str.ends_with("}"));
assert!(a_str.contains("\"contact\""));
assert!(a_str.contains("\"mailto:derp@example.com\""));
assert!(a_str.contains("\"mailto:derp.derpson@example.com\""));
assert!(a_str.contains("\"termsOfServiceAgreed\""));
assert!(a_str.contains("\"onlyReturnExisting\""));
assert!(a_str.contains("true"));
assert!(a_str.contains("false"));
}
#[test]
fn test_account_response() {
let data = "{
\"status\": \"valid\",
\"contact\": [
\"mailto:cert-admin@example.org\",
\"mailto:admin@example.org\"
],
\"termsOfServiceAgreed\": true,
\"orders\": \"https://example.com/acme/orders/rzGoeA\"
}";
let account_resp = AccountResponse::from_str(data);
assert!(account_resp.is_ok());
let account_resp = account_resp.unwrap();
assert_eq!(account_resp.status, "valid");
assert!(account_resp.contact.is_some());
let contacts = account_resp.contact.unwrap();
assert_eq!(contacts.len(), 2);
assert_eq!(contacts[0], "mailto:cert-admin@example.org");
assert_eq!(contacts[1], "mailto:admin@example.org");
assert!(account_resp.external_account_binding.is_none());
assert!(account_resp.terms_of_service_agreed.is_some());
assert!(account_resp.terms_of_service_agreed.unwrap());
assert_eq!(
account_resp.orders,
Some("https://example.com/acme/orders/rzGoeA".into())
);
}
#[test]
fn test_account_update() {
let emails = vec![
"mailto:derp@example.com".to_string(),
"mailto:derp.derpson@example.com".to_string(),
];
let au = AccountUpdate::new(&emails);
assert_eq!(au.contact.len(), 2);
let au_str = serde_json::to_string(&au);
assert!(au_str.is_ok());
let au_str = au_str.unwrap();
assert!(au_str.starts_with("{"));
assert!(au_str.ends_with("}"));
assert!(au_str.contains("\"contact\""));
assert!(au_str.contains("\"mailto:derp@example.com\""));
assert!(au_str.contains("\"mailto:derp.derpson@example.com\""));
}
#[test]
fn test_account_deactivation() {
let ad = AccountDeactivation::new();
assert_eq!(ad.status, "deactivated");
let ad_str = serde_json::to_string(&ad);
assert!(ad_str.is_ok());
let ad_str = ad_str.unwrap();
assert!(ad_str.starts_with("{"));
assert!(ad_str.ends_with("}"));
assert!(ad_str.contains("\"status\""));
assert!(ad_str.contains("\"deactivated\""));
}
}

299
acmed/src/acme_proto/structs/authorization.rs

@ -0,0 +1,299 @@
use crate::acme_proto::b64_encode;
use crate::acme_proto::jws::algorithms::SignatureAlgorithm;
use crate::acme_proto::structs::Identifier;
use crate::error::Error;
use openssl::pkey::{PKey, Private};
use openssl::sha::sha256;
use serde::Deserialize;
use std::fmt;
use std::str::FromStr;
#[derive(Deserialize)]
pub struct Authorization {
pub identifier: Identifier,
pub status: AuthorizationStatus,
pub expires: Option<String>,
pub challenges: Vec<Challenge>,
pub wildcard: Option<bool>,
}
impl FromStr for Authorization {
type Err = Error;
fn from_str(data: &str) -> Result<Self, Self::Err> {
let mut res: Self = serde_json::from_str(data)?;
res.challenges.retain(|c| *c != Challenge::Unknown);
Ok(res)
}
}
#[derive(Debug, PartialEq, Eq, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum AuthorizationStatus {
Pending,
Valid,
Invalid,
Deactivated,
Expired,
Revoked,
}
impl fmt::Display for AuthorizationStatus {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let s = match self {
AuthorizationStatus::Pending => "pending",
AuthorizationStatus::Valid => "valid",
AuthorizationStatus::Invalid => "invalid",
AuthorizationStatus::Deactivated => "deactivated",
AuthorizationStatus::Expired => "expired",
AuthorizationStatus::Revoked => "revoked",
};
write!(f, "{}", s)
}
}
#[derive(PartialEq, Deserialize)]
#[serde(tag = "type")]
pub enum Challenge {
#[serde(rename = "http-01")]
Http01(TokenChallenge),
#[serde(rename = "dns-01")]
Dns01(TokenChallenge),
// TODO: tls-alpn-01
#[serde(other)]
Unknown,
}
deserialize_from_str!(Challenge);
impl Challenge {
pub fn get_url(&self) -> String {
match self {
Challenge::Http01(tc) | Challenge::Dns01(tc) => tc.url.to_owned(),
Challenge::Unknown => String::new(),
}
}
pub fn get_proof(&self, private_key: &PKey<Private>) -> Result<String, Error> {
match self {
Challenge::Http01(tc) => tc.key_authorization(private_key),
Challenge::Dns01(tc) => {
let ka = tc.key_authorization(private_key)?;
let a = sha256(ka.as_bytes());
let a = b64_encode(&a);
Ok(a)
}
Challenge::Unknown => Ok(String::new()),
}
}
pub fn get_file_name(&self) -> String {
match self {
Challenge::Http01(tc) => tc.token.to_owned(),
Challenge::Dns01(_) => String::new(),
Challenge::Unknown => String::new(),
}
}
}
#[derive(PartialEq, Deserialize)]
pub struct TokenChallenge {
pub url: String,
pub status: Option<ChallengeStatus>,
pub validated: Option<String>,
pub error: Option<String>, // TODO: set the correct object
pub token: String,
}
impl TokenChallenge {
fn key_authorization(&self, private_key: &PKey<Private>) -> Result<String, Error> {
let sa = SignatureAlgorithm::from_pkey(private_key)?;
let thumbprint = sa.get_jwk_thumbprint(private_key)?;
let thumbprint = sha256(thumbprint.as_bytes());
let thumbprint = b64_encode(&thumbprint);
let auth = format!("{}.{}", self.token, thumbprint);
Ok(auth)
}
}
#[derive(Debug, PartialEq, Eq, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum ChallengeStatus {
Pending,
Processing,
Valid,
Invalid,
}
#[cfg(test)]
mod tests {
use super::{Authorization, AuthorizationStatus, Challenge, ChallengeStatus};
use crate::acme_proto::structs::IdentifierType;
use std::str::FromStr;
#[test]
fn test_authorization() {
let data = "{
\"status\": \"pending\",
\"identifier\": {
\"type\": \"dns\",
\"value\": \"example.com\"
},
\"challenges\": []
}";
let a = Authorization::from_str(data);
assert!(a.is_ok());
let a = a.unwrap();
assert_eq!(a.status, AuthorizationStatus::Pending);
assert!(a.challenges.is_empty());
let i = a.identifier;
assert_eq!(i.id_type, IdentifierType::Dns);
assert_eq!(i.value, "example.com".to_string());
}
#[test]
fn test_authorization_challenge() {
let data = "{
\"status\": \"pending\",
\"identifier\": {
\"type\": \"dns\",
\"value\": \"example.com\"
},
\"challenges\": [
{
\"type\": \"dns-01\",
\"status\": \"pending\",
\"url\": \"https://example.com/chall/jYWxob3N0OjE\",
\"token\": \"1y9UVMUvkqQVljCsnwlRLsbJcwN9nx-qDd6JHzXQQsw\"
}
]
}";
let a = Authorization::from_str(data);
assert!(a.is_ok());
let a = a.unwrap();
assert_eq!(a.status, AuthorizationStatus::Pending);
assert_eq!(a.challenges.len(), 1);
let i = a.identifier;
assert_eq!(i.id_type, IdentifierType::Dns);
assert_eq!(i.value, "example.com".to_string());
}
#[test]
fn test_authorization_unknown_challenge() {
let data = "{
\"status\": \"pending\",
\"identifier\": {
\"type\": \"dns\",
\"value\": \"example.com\"
},
\"challenges\": [
{
\"type\": \"invalid-challenge-01\",
\"status\": \"pending\",
\"url\": \"https://example.com/chall/jYWxob3N0OjE\",
\"token\": \"1y9UVMUvkqQVljCsnwlRLsbJcwN9nx-qDd6JHzXQQsw\"
}
]
}";
let a = Authorization::from_str(data);
assert!(a.is_ok());
let a = a.unwrap();
assert_eq!(a.status, AuthorizationStatus::Pending);
assert!(a.challenges.is_empty());
let i = a.identifier;
assert_eq!(i.id_type, IdentifierType::Dns);
assert_eq!(i.value, "example.com".to_string());
}
#[test]
fn test_invalid_authorization() {
let data = "{
\"status\": \"pending\",
\"identifier\": {
\"type\": \"foo\",
\"value\": \"bar\"
},
\"challenges\": []
}";
let a = Authorization::from_str(data);
assert!(a.is_err());
}
#[test]
fn test_http01_challenge() {
let data = "{
\"type\": \"http-01\",
\"url\": \"https://example.com/acme/chall/prV_B7yEyA4\",
\"status\": \"pending\",
\"token\": \"LoqXcYV8q5ONbJQxbmR7SCTNo3tiAXDfowyjxAjEuX0\"
}";
let challenge = Challenge::from_str(data);
assert!(challenge.is_ok());
let challenge = challenge.unwrap();
let c = match challenge {
Challenge::Http01(c) => c,
_ => {
assert!(false);
return;
}
};
assert_eq!(
c.url,
"https://example.com/acme/chall/prV_B7yEyA4".to_string()
);
assert_eq!(c.status, Some(ChallengeStatus::Pending));
assert_eq!(
c.token,
"LoqXcYV8q5ONbJQxbmR7SCTNo3tiAXDfowyjxAjEuX0".to_string()
);
assert!(c.validated.is_none());
assert!(c.error.is_none());
}
#[test]
fn test_dns01_challenge() {
let data = "{
\"type\": \"http-01\",
\"url\": \"https://example.com/acme/chall/prV_B7yEyA4\",
\"status\": \"valid\",
\"token\": \"LoqXcYV8q5ONbJQxbmR7SCTNo3tiAXDfowyjxAjEuX0\"
}";
let challenge = Challenge::from_str(data);
assert!(challenge.is_ok());
let challenge = challenge.unwrap();
let c = match challenge {
Challenge::Http01(c) => c,
_ => {
assert!(false);
return;
}
};
assert_eq!(
c.url,
"https://example.com/acme/chall/prV_B7yEyA4".to_string()
);
assert_eq!(c.status, Some(ChallengeStatus::Valid));
assert_eq!(
c.token,
"LoqXcYV8q5ONbJQxbmR7SCTNo3tiAXDfowyjxAjEuX0".to_string()
);
assert!(c.validated.is_none());
assert!(c.error.is_none());
}
#[test]
fn test_unknown_challenge_type() {
let data = "{
\"type\": \"invalid-01\",
\"url\": \"https://example.com/acme/chall/prV_B7yEyA4\",
\"status\": \"pending\",
\"token\": \"LoqXcYV8q5ONbJQxbmR7SCTNo3tiAXDfowyjxAjEuX0\"
}";
let challenge = Challenge::from_str(data);
assert!(challenge.is_ok());
match challenge.unwrap() {
Challenge::Unknown => assert!(true),
_ => assert!(false),
}
}
}

147
acmed/src/acme_proto/structs/directory.rs

@ -0,0 +1,147 @@
use crate::error::Error;
use serde::Deserialize;
use std::str::FromStr;
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct DirectoryMeta {
pub terms_of_service: Option<String>,
pub website: Option<String>,
pub caa_identities: Option<Vec<String>>,
pub external_account_required: Option<String>,
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Directory {
pub meta: Option<DirectoryMeta>,
pub new_nonce: String,
pub new_account: String,
pub new_order: String,
pub new_authz: Option<String>,
pub revoke_cert: String,
pub key_change: String,
}
deserialize_from_str!(Directory);
#[cfg(test)]
mod tests {
use super::Directory;
use std::str::FromStr;
#[test]
fn test_directory() {
let data = "{
\"newAccount\": \"https://example.org/acme/new-acct\",
\"newNonce\": \"https://example.org/acme/new-nonce\",
\"newOrder\": \"https://example.org/acme/new-order\",
\"revokeCert\": \"https://example.org/acme/revoke-cert\",
\"newAuthz\": \"https://example.org/acme/new-authz\",
\"keyChange\": \"https://example.org/acme/key-change\"
}";
let parsed_dir = Directory::from_str(data);
assert!(parsed_dir.is_ok());
let parsed_dir = parsed_dir.unwrap();
assert_eq!(parsed_dir.new_nonce, "https://example.org/acme/new-nonce");
assert_eq!(parsed_dir.new_account, "https://example.org/acme/new-acct");
assert_eq!(parsed_dir.new_order, "https://example.org/acme/new-order");
assert_eq!(
parsed_dir.new_authz,
Some("https://example.org/acme/new-authz".to_string())
);
assert_eq!(
parsed_dir.revoke_cert,
"https://example.org/acme/revoke-cert"
);
assert_eq!(parsed_dir.key_change, "https://example.org/acme/key-change");
assert!(parsed_dir.meta.is_none());
}
#[test]
fn test_directory_no_authz() {
let data = "{
\"newAccount\": \"https://example.org/acme/new-acct\",
\"newNonce\": \"https://example.org/acme/new-nonce\",
\"newOrder\": \"https://example.org/acme/new-order\",
\"revokeCert\": \"https://example.org/acme/revoke-cert\",
\"keyChange\": \"https://example.org/acme/key-change\"
}";
let parsed_dir = Directory::from_str(data);
assert!(parsed_dir.is_ok());
let parsed_dir = parsed_dir.unwrap();
assert_eq!(parsed_dir.new_nonce, "https://example.org/acme/new-nonce");
assert_eq!(parsed_dir.new_account, "https://example.org/acme/new-acct");
assert_eq!(parsed_dir.new_order, "https://example.org/acme/new-order");
assert!(parsed_dir.new_authz.is_none());
assert_eq!(
parsed_dir.revoke_cert,
"https://example.org/acme/revoke-cert"
);
assert_eq!(parsed_dir.key_change, "https://example.org/acme/key-change");
assert!(parsed_dir.meta.is_none());
}
#[test]
fn test_directory_meta() {
let data = "{
\"keyChange\": \"https://example.org/acme/key-change\",
\"meta\": {
\"caaIdentities\": [
\"example.org\"
],
\"termsOfService\": \"https://example.org/documents/tos.pdf\",
\"website\": \"https://example.org/\"
},
\"newAccount\": \"https://example.org/acme/new-acct\",
\"newNonce\": \"https://example.org/acme/new-nonce\",
\"newOrder\": \"https://example.org/acme/new-order\",
\"revokeCert\": \"https://example.org/acme/revoke-cert\"
}";
let parsed_dir = Directory::from_str(&data);
assert!(parsed_dir.is_ok());
let parsed_dir = parsed_dir.unwrap();
assert!(parsed_dir.meta.is_some());
let meta = parsed_dir.meta.unwrap();
assert_eq!(
meta.terms_of_service,
Some("https://example.org/documents/tos.pdf".to_string())
);
assert_eq!(meta.website, Some("https://example.org/".to_string()));
assert!(meta.caa_identities.is_some());
let caa_identities = meta.caa_identities.unwrap();
assert_eq!(caa_identities.len(), 1);
assert_eq!(caa_identities.first(), Some(&"example.org".to_string()));
assert!(meta.external_account_required.is_none());
}
#[test]
fn test_directory_extra_fields() {
let data = "{
\"foo\": \"bar\",
\"keyChange\": \"https://example.org/acme/key-change\",
\"newAccount\": \"https://example.org/acme/new-acct\",
\"baz\": \"quz\",
\"newNonce\": \"https://example.org/acme/new-nonce\",
\"newAuthz\": \"https://example.org/acme/new-authz\",
\"newOrder\": \"https://example.org/acme/new-order\",
\"revokeCert\": \"https://example.org/acme/revoke-cert\"
}";
let parsed_dir = Directory::from_str(&data);
assert!(parsed_dir.is_ok());
let parsed_dir = parsed_dir.unwrap();
assert_eq!(parsed_dir.new_nonce, "https://example.org/acme/new-nonce");
assert_eq!(parsed_dir.new_account, "https://example.org/acme/new-acct");
assert_eq!(parsed_dir.new_order, "https://example.org/acme/new-order");
assert_eq!(
parsed_dir.new_authz,
Some("https://example.org/acme/new-authz".to_string())
);
assert_eq!(
parsed_dir.revoke_cert,
"https://example.org/acme/revoke-cert"
);
assert_eq!(parsed_dir.key_change, "https://example.org/acme/key-change");
assert!(parsed_dir.meta.is_none());
}
}

135
acmed/src/acme_proto/structs/order.rs

@ -0,0 +1,135 @@
use crate::error::Error;
use serde::{Deserialize, Serialize};
use std::fmt;
use std::str::FromStr;
#[derive(Serialize)]
pub struct NewOrder {
pub identifiers: Vec<Identifier>,
pub not_before: Option<String>,
pub not_after: Option<String>,
}
impl NewOrder {
pub fn new(domains: &[String]) -> Self {
NewOrder {
identifiers: domains.iter().map(|n| Identifier::new_dns(n)).collect(),
not_before: None,
not_after: None,
}
}
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Order {
pub status: OrderStatus,
pub expires: Option<String>,
pub identifiers: Vec<Identifier>,
pub not_before: Option<String>,
pub not_after: Option<String>,
pub error: Option<String>, // TODO: set the correct structure
pub authorizations: Vec<String>,
pub finalize: String,
pub certificate: Option<String>,
}
deserialize_from_str!(Order);
#[derive(Debug, PartialEq, Eq, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum OrderStatus {
Pending,
Ready,
Processing,
Valid,
Invalid,
}
impl fmt::Display for OrderStatus {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let s = match self {
OrderStatus::Pending => "pending",
OrderStatus::Ready => "ready",
OrderStatus::Processing => "processing",
OrderStatus::Valid => "valid",
OrderStatus::Invalid => "invalid",
};
write!(f, "{}", s)
}
}
#[derive(Deserialize, Serialize)]
pub struct Identifier {
#[serde(rename = "type")]
pub id_type: IdentifierType,
pub value: String,
}
impl Identifier {
pub fn new_dns(value: &str) -> Self {
Identifier {
id_type: IdentifierType::Dns,
value: value.to_string(),
}
}
}
deserialize_from_str!(Identifier);
impl fmt::Display for Identifier {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}:{}", self.id_type, self.value)
}
}
#[derive(Debug, Deserialize, Serialize, Eq, PartialEq)]
pub enum IdentifierType {
#[serde(rename = "dns")]
Dns,
}
impl fmt::Display for IdentifierType {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let s = match self {
IdentifierType::Dns => "dns",
};
write!(f, "{}", s)
}
}
#[cfg(test)]
mod tests {
use super::{Identifier, IdentifierType};
use std::str::FromStr;
#[test]
fn id_serialize() {
let reference = "{\"type\":\"dns\",\"value\":\"test.example.org\"}";
let id = Identifier {
id_type: IdentifierType::Dns,
value: "test.example.org".to_string(),
};
let id_json = serde_json::to_string(&id);
assert!(id_json.is_ok());
let id_json = id_json.unwrap();
assert_eq!(id_json, reference.to_string());
}
#[test]
fn id_deserialize_valid() {
let id_str = "{\"type\":\"dns\",\"value\":\"test.example.org\"}";
let id = Identifier::from_str(id_str);
assert!(id.is_ok());
let id = id.unwrap();
assert_eq!(id.id_type, IdentifierType::Dns);
assert_eq!(id.value, "test.example.org".to_string());
}
#[test]
fn id_deserialize_invalid_type() {
let id_str = "{\"type\":\"trololo\",\"value\":\"test.example.org\"}";
let id = Identifier::from_str(id_str);
assert!(id.is_err());
}
}

333
acmed/src/acmed.rs

@ -1,333 +0,0 @@
use crate::config::{self, Hook};
use crate::errors::Error;
use crate::hooks;
use crate::storage::Storage;
use acme_lib::{Directory, DirectoryUrl};
use log::{debug, info, warn};
use openssl;
use serde::Serialize;
use std::time::Duration;
use std::{fmt, thread};
use x509_parser::parse_x509_der;
#[derive(Clone, Debug, PartialEq, PartialOrd, Eq, Ord)]
pub enum Format {
Der,
Pem,
}
impl fmt::Display for Format {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let s = match self {
Format::Der => "der",
Format::Pem => "pem",
};
write!(f, "{}", s)
}
}
#[derive(Clone, Debug)]
pub enum Challenge {
Http01,
Dns01,
}
impl Challenge {
pub fn from_str(s: &str) -> Result<Self, Error> {
match s.to_lowercase().as_str() {
"http-01" => Ok(Challenge::Http01),
"dns-01" => Ok(Challenge::Dns01),
_ => Err(Error::new(&format!("{}: unknown challenge.", s))),
}
}
}
impl fmt::Display for Challenge {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let s = match self {
Challenge::Http01 => "http-01",
Challenge::Dns01 => "dns-01",
};
write!(f, "{}", s)
}
}
#[derive(Clone, Debug)]
pub enum Algorithm {
Rsa2048,
Rsa4096,
EcdsaP256,
EcdsaP384,
}
impl Algorithm {
pub fn from_str(s: &str) -> Result<Self, Error> {
match s.to_lowercase().as_str() {
"rsa2048" => Ok(Algorithm::Rsa2048),
"rsa4096" => Ok(Algorithm::Rsa4096),
"ecdsa_p256" => Ok(Algorithm::EcdsaP256),
"ecdsa_p384" => Ok(Algorithm::EcdsaP384),
_ => Err(Error::new(&format!("{}: unknown algorithm.", s))),
}
}
}
impl fmt::Display for Algorithm {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let s = match self {
Algorithm::Rsa2048 => "rsa2048",
Algorithm::Rsa4096 => "rsa4096",
Algorithm::EcdsaP256 => "ecdsa-p256",
Algorithm::EcdsaP384 => "ecdsa-p384",
};
write!(f, "{}", s)
}
}
#[derive(Serialize)]
struct HookData {
// Common
domains: Vec<String>,
algorithm: String,
challenge: String,
status: String,
// Challenge hooks
current_domain: String,
token: String,
proof: String,
}
#[derive(Debug)]
struct Certificate {
domains: Vec<String>,
algo: Algorithm,
kp_reuse: bool,
storage: Storage,
email: String,
remote_url: String,
challenge: Challenge,
challenge_hooks: Vec<Hook>,
post_operation_hooks: Vec<Hook>,
}
impl Certificate {
fn should_renew(&self) -> bool {
let domain = self.domains.first().unwrap();
let raw_cert = match self.storage.get_certificate(&Format::Der) {
Ok(c) => match c {
Some(d) => d,
None => {
debug!(
"{} certificate for {} is empty or does not exists",
self.algo, domain
);
return true;
}
},
Err(e) => {
warn!("{}", e);
return true;
}
};
match parse_x509_der(&raw_cert) {
Ok((_, cert)) => {
// TODO: allow a custom duration (using time-parse ?)
let renewal_time =
cert.tbs_certificate.validity.not_after - time::Duration::weeks(3);
debug!(
"{} certificate for {}: not after: {}",
self.algo,
domain,
cert.tbs_certificate.validity.not_after.asctime()
);
debug!(
"{} certificate for {}: renew on: {}",
self.algo,
domain,
renewal_time.asctime()
);
time::now_utc() > renewal_time
}
Err(_) => true,
}
}
fn call_challenge_hooks(&self, token: &str, proof: &str, domain: &str) -> Result<(), Error> {
let hook_data = HookData {
domains: self.domains.to_owned(),
algorithm: self.algo.to_string(),
challenge: self.challenge.to_string(),
status: format!("Validation pending for {}", domain),
current_domain: domain.to_string(),
token: token.to_string(),
proof: proof.to_string(),
};
hooks::call_multiple(&hook_data, &self.challenge_hooks)?;
Ok(())
}
fn call_post_operation_hooks(&self, status: &str) -> Result<(), Error> {
let hook_data = HookData {
domains: self.domains.to_owned(),
algorithm: self.algo.to_string(),
challenge: self.challenge.to_string(),
status: status.to_string(),
current_domain: "".to_string(),
token: "".to_string(),
proof: "".to_string(),
};
hooks::call_multiple(&hook_data, &self.post_operation_hooks)?;
Ok(())
}
fn renew(&mut self) -> Result<(), Error> {
// TODO: do it in a separated thread since it may take a while
let (name, alt_names_str) = self.domains.split_first().unwrap();
let mut alt_names = vec![];
for n in alt_names_str.iter() {
alt_names.push(n.as_str());
}
info!("Renewing the {} certificate for {}", self.algo, name);
let url = DirectoryUrl::Other(&self.remote_url);
let dir = Directory::from_url(self.storage.to_owned(), url)?;
let acc = dir.account(&self.email)?;
let mut ord_new = acc.new_order(name, &alt_names)?;
let ord_csr = loop {
if let Some(ord_csr) = ord_new.confirm_validations() {
break ord_csr;
}
let auths = ord_new.authorizations()?;
for auth in auths.iter() {
match self.challenge {
Challenge::Http01 => {
let chall = auth.http_challenge();
let token = chall.http_token();
let proof = chall.http_proof();
self.call_challenge_hooks(&token, &proof, auth.domain_name())?;
chall.validate(crate::DEFAULT_POOL_TIME)?;
}
Challenge::Dns01 => {
let chall = auth.dns_challenge();
let proof = chall.dns_proof();
self.call_challenge_hooks("", &proof, auth.domain_name())?;
chall.validate(crate::DEFAULT_POOL_TIME)?;
}
};
}
ord_new.refresh()?;
};
let mut raw_crt = vec![];
let mut raw_pk = vec![];
if self.kp_reuse {
raw_crt = self
.storage
.get_certificate(&Format::Der)?
.unwrap_or_else(|| vec![]);
raw_pk = self
.storage
.get_private_key(&Format::Der)?
.unwrap_or_else(|| vec![]);
};
let (pkey_pri, pkey_pub) = if !raw_crt.is_empty() && !raw_pk.is_empty() {
(
openssl::pkey::PKey::private_key_from_der(&raw_pk)?,
openssl::x509::X509::from_der(&raw_crt)?.public_key()?,
)
} else {
match self.algo {
Algorithm::Rsa2048 => acme_lib::create_rsa_key(2048),
Algorithm::Rsa4096 => acme_lib::create_rsa_key(4096),
Algorithm::EcdsaP256 => acme_lib::create_p256_key(),
Algorithm::EcdsaP384 => acme_lib::create_p384_key(),
}
};
let ord_cert = ord_csr.finalize_pkey(pkey_pri, pkey_pub, crate::DEFAULT_POOL_TIME)?;
ord_cert.download_and_save_cert()?;
Ok(())
}
}
pub struct Acmed {
certs: Vec<Certificate>,
}
impl Acmed {
pub fn new(config_file: &str) -> Result<Self, Error> {
let cnf = config::from_file(config_file)?;
let mut certs = Vec::new();
for crt in cnf.certificate.iter() {
let cert = Certificate {
domains: crt.domains.to_owned(),
algo: crt.get_algorithm()?,
kp_reuse: crt.get_kp_reuse(),
storage: Storage {
account_directory: cnf.get_account_dir(),
account_name: crt.email.to_owned(),
crt_directory: crt.get_crt_dir(&cnf),
crt_name: crt.get_crt_name(),
crt_name_format: crt.get_crt_name_format(),
formats: crt.get_formats()?,
algo: crt.get_algorithm()?,
cert_file_mode: cnf.get_cert_file_mode(),
cert_file_owner: cnf.get_cert_file_user(),
cert_file_group: cnf.get_cert_file_group(),
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)?,
challenge: crt.get_challenge()?,
challenge_hooks: crt.get_challenge_hooks(&cnf)?,
post_operation_hooks: crt.get_post_operation_hooks(&cnf)?,
};
certs.push(cert);
}
Ok(Acmed { certs })
}
pub fn run(&mut self) {
loop {
for crt in self.certs.iter_mut() {
debug!("{:?}", crt);
if crt.should_renew() {
// TODO: keep track of (not yet implemented) threads and wait for them to end.
let status = match crt.renew() {
Ok(_) => "Success.".to_string(),
Err(e) => {
let msg = format!(
"Unable to renew the {} certificate for {}: {}",
crt.algo,
crt.domains.first().unwrap(),
e
);
warn!("{}", msg);
format!("Failed: {}", msg)
}
};
match crt.call_post_operation_hooks(&status) {
Ok(_) => {}
Err(e) => {
let msg = format!(
"{} certificate for {}: post-operation hook error: {}",
crt.algo,
crt.domains.first().unwrap(),
e
);
warn!("{}", msg);
}
};
}
}
thread::sleep(Duration::from_secs(crate::DEFAULT_SLEEP_TIME));
}
}
}

159
acmed/src/certificate.rs

@ -0,0 +1,159 @@
use crate::acme_proto::Challenge;
use crate::error::Error;
use crate::hooks::{self, ChallengeHookData, Hook, PostOperationHookData};
use crate::storage::{certificate_files_exists, get_certificate};
use log::debug;
use std::fmt;
use time::{strptime, Duration};
#[derive(Clone, Debug)]
pub enum Algorithm {
Rsa2048,
Rsa4096,
EcdsaP256,
EcdsaP384,
}
impl Algorithm {
pub fn from_str(s: &str) -> Result<Self, Error> {
match s.to_lowercase().as_str() {
"rsa2048" => Ok(Algorithm::Rsa2048),
"rsa4096" => Ok(Algorithm::Rsa4096),
"ecdsa_p256" => Ok(Algorithm::EcdsaP256),
"ecdsa_p384" => Ok(Algorithm::EcdsaP384),
_ => Err(format!("{}: unknown algorithm.", s).into()),
}
}
}
impl fmt::Display for Algorithm {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let s = match self {
Algorithm::Rsa2048 => "rsa2048",
Algorithm::Rsa4096 => "rsa4096",
Algorithm::EcdsaP256 => "ecdsa-p256",
Algorithm::EcdsaP384 => "ecdsa-p384",
};
write!(f, "{}", s)
}
}
#[derive(Debug)]
pub struct Certificate {
pub domains: Vec<String>,
pub algo: Algorithm,
pub kp_reuse: bool,
pub email: String,
pub remote_url: String,
pub challenge: Challenge,
pub challenge_hooks: Vec<Hook>,
pub post_operation_hooks: Vec<Hook>,
pub account_directory: String,
pub crt_directory: String,
pub crt_name: String,
pub crt_name_format: String,
pub cert_file_mode: u32,
pub cert_file_owner: Option<String>,
pub cert_file_group: Option<String>,
pub pk_file_mode: u32,
pub pk_file_owner: Option<String>,
pub pk_file_group: Option<String>,
pub file_pre_create_hooks: Vec<Hook>,
pub file_post_create_hooks: Vec<Hook>,
pub file_pre_edit_hooks: Vec<Hook>,
pub file_post_edit_hooks: Vec<Hook>,
}
impl fmt::Display for Certificate {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let challenge_hooks = self
.challenge_hooks
.iter()
.map(std::string::ToString::to_string)
.collect::<Vec<String>>()
.join(", ");
let post_operation_hooks = self
.post_operation_hooks
.iter()
.map(std::string::ToString::to_string)
.collect::<Vec<String>>()
.join(", ");
write!(
f,
"Certificate information:
Domains: {domains}
Algorithm: {algo}
Contact: {email}
Private key reuse: {kp_reuse}
Challenge: {challenge}
Challenge hooks: {challenge_hooks}
Post operation hooks: {post_operation_hooks}",
domains = self.domains.join(", "),
algo = self.algo,
email = self.email,
kp_reuse = self.kp_reuse,
challenge = self.challenge,
challenge_hooks = challenge_hooks,
post_operation_hooks = post_operation_hooks,
)
}
}
impl Certificate {
pub fn should_renew(&self) -> Result<bool, Error> {
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(&not_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;
if renew {
debug!("The certificate will be renewed now.");
} else {
debug!("The certificate will not be renewed now.");
}
Ok(renew)
}
pub fn call_challenge_hooks(
&self,
file_name: &str,
proof: &str,
domain: &str,
) -> Result<(), Error> {
let hook_data = ChallengeHookData {
domains: self.domains.to_owned(),
algorithm: self.algo.to_string(),
challenge: self.challenge.to_string(),
current_domain: domain.to_string(),
file_name: file_name.to_string(),
proof: proof.to_string(),
};
hooks::call_multiple(&hook_data, &self.challenge_hooks)?;
Ok(())
}
pub fn call_post_operation_hooks(&self, status: &str) -> Result<(), Error> {
let hook_data = PostOperationHookData {
domains: self.domains.to_owned(),
algorithm: self.algo.to_string(),
challenge: self.challenge.to_string(),
status: status.to_string(),
};
hooks::call_multiple(&hook_data, &self.post_operation_hooks)?;
Ok(())
}
}

58
acmed/src/config.rs

@ -1,5 +1,7 @@
use crate::acmed::{Algorithm, Challenge, Format};
use crate::errors::Error;
use crate::acme_proto::Challenge;
use crate::certificate::Algorithm;
use crate::error::Error;
use crate::hooks;
use log::info;
use serde::Deserialize;
use std::fs::{self, File};
@ -27,10 +29,18 @@ impl Config {
account_dir.to_string()
}
pub fn get_hook(&self, name: &str) -> Result<Vec<Hook>, Error> {
pub fn get_hook(&self, name: &str) -> Result<Vec<hooks::Hook>, Error> {
for hook in self.hook.iter() {
if name == hook.name {
return Ok(vec![hook.clone()]);
let h = hooks::Hook {
name: hook.name.to_owned(),
cmd: hook.cmd.to_owned(),
args: hook.args.to_owned(),
stdin: hook.stdin.to_owned(),
stdout: hook.stdout.to_owned(),
stderr: hook.stderr.to_owned(),
};
return Ok(vec![h]);
}
}
for grp in self.group.iter() {
@ -43,7 +53,7 @@ impl Config {
return Ok(ret);
}
}
Err(Error::new(&format!("{}: hook not found", name)))
Err(format!("{}: hook not found", name).into())
}
pub fn get_cert_file_mode(&self) -> u32 {
@ -113,7 +123,7 @@ pub struct Endpoint {
pub url: String,
}
#[derive(Deserialize, Clone, Debug)]
#[derive(Deserialize)]
pub struct Hook {
pub name: String,
pub cmd: String,
@ -169,26 +179,6 @@ impl Certificate {
}
}
pub fn get_formats(&self) -> Result<Vec<Format>, Error> {
let ret = match &self.formats {
Some(fmts) => {
let mut lst = Vec::new();
for f in fmts.iter() {
lst.push(match f.as_str() {
"der" => Format::Der,
"pem" => Format::Pem,
_ => return Err(Error::new(&format!("{}: unknown format.", f))),
});
}
lst.sort();
lst.dedup();
lst
}
None => vec![crate::DEFAULT_FMT],
};
Ok(ret)
}
pub fn get_crt_name(&self) -> String {
match &self.name {
Some(n) => n.to_string(),
@ -223,42 +213,42 @@ impl Certificate {
return Ok(endpoint.url.to_owned());
}
}
Err(Error::new(&format!("{}: unknown endpoint.", self.endpoint)))
Err(format!("{}: unknown endpoint.", self.endpoint).into())
}
pub fn get_challenge_hooks(&self, cnf: &Config) -> Result<Vec<Hook>, Error> {
pub fn get_challenge_hooks(&self, cnf: &Config) -> Result<Vec<hooks::Hook>, Error> {
get_hooks(&self.challenge_hooks, cnf)
}
pub fn get_post_operation_hooks(&self, cnf: &Config) -> Result<Vec<Hook>, Error> {
pub fn get_post_operation_hooks(&self, cnf: &Config) -> Result<Vec<hooks::Hook>, Error> {
match &self.post_operation_hooks {
Some(hooks) => get_hooks(hooks, cnf),
None => Ok(vec![]),
}
}
pub fn get_file_pre_create_hooks(&self, cnf: &Config) -> Result<Vec<Hook>, Error> {
pub fn get_file_pre_create_hooks(&self, cnf: &Config) -> Result<Vec<hooks::Hook>, 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<Vec<Hook>, Error> {
pub fn get_file_post_create_hooks(&self, cnf: &Config) -> Result<Vec<hooks::Hook>, 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<Vec<Hook>, Error> {
pub fn get_file_pre_edit_hooks(&self, cnf: &Config) -> Result<Vec<hooks::Hook>, 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<Vec<Hook>, Error> {
pub fn get_file_post_edit_hooks(&self, cnf: &Config) -> Result<Vec<hooks::Hook>, Error> {
match &self.file_post_edit_hooks {
Some(hooks) => get_hooks(hooks, cnf),
None => Ok(vec![]),
@ -266,7 +256,7 @@ impl Certificate {
}
}
fn get_hooks(lst: &[String], cnf: &Config) -> Result<Vec<Hook>, Error> {
fn get_hooks(lst: &[String], cnf: &Config) -> Result<Vec<hooks::Hook>, Error> {
let mut res = vec![];
for name in lst.iter() {
let mut h = cnf.get_hook(&name)?;

203
acmed/src/encoding.rs

@ -1,203 +0,0 @@
use crate::acmed::Format;
use acme_lib::persist::PersistKind;
use acme_lib::Error;
use log::debug;
use pem::{encode, Pem};
enum ConversionType {
PemToDer,
DerToPem,
None,
}
impl ConversionType {
fn get(from: &Format, to: &Format) -> Self {
match from {
Format::Pem => match to {
Format::Pem => ConversionType::None,
Format::Der => ConversionType::PemToDer,
},
Format::Der => match to {
Format::Pem => ConversionType::DerToPem,
Format::Der => ConversionType::None,
},
}
}
}
fn pem_to_der(data: &[u8]) -> Result<Vec<u8>, Error> {
// We need to convert all CRLF into LF since x509_parser only supports the later.
let mut data = data.to_vec();
data.retain(|&c| c != 0x0d);
match x509_parser::pem::pem_to_der(&data) {
Ok((_, cert)) => Ok(cert.contents),
Err(_) => Err(Error::Other("invalid PEM certificate".to_string())),
}
}
fn der_to_pem(data: &[u8], kind: PersistKind) -> Result<Vec<u8>, Error> {
// TODO: allow the user to specify if we should use CRLF or LF (default is CRLF).
let tag_str = match kind {
PersistKind::AccountPrivateKey => "PRIVATE KEY",
PersistKind::PrivateKey => "PRIVATE KEY",
PersistKind::Certificate => "CERTIFICATE",
};
let pem = Pem {
tag: String::from(tag_str),
contents: data.to_vec(),
};
let res = encode(&pem);
Ok(res.into_bytes())
}
/// Convert a certificate encoded in a format into another format.
///
/// Warning: if the data contains multiple certificates (eg: a PEM
/// certificate chain), converting to DER will only include the first
/// certificate, the others will be lost.
pub fn convert(
data: &[u8],
from: &Format,
to: &Format,
kind: PersistKind,
) -> Result<Vec<u8>, Error> {
debug!("Converting a certificate from {} to {}", from, to);
match ConversionType::get(from, to) {
ConversionType::PemToDer => pem_to_der(data),
ConversionType::DerToPem => der_to_pem(data, kind),
ConversionType::None => Ok(data.to_vec()),
}
}
#[cfg(test)]
mod tests {
use super::convert;
use crate::acmed::Format;
use acme_lib::persist::PersistKind;
// Test data generated using:
//
// openssl req -x509 -nodes -newkey ED25519 -keyout key.pem -out cert.pem -days 365
// openssl pkey -inform PEM -outform DER -in key.pem -out key.der
// openssl x509 -inform PEM -outform DER -in cert.pem -out cert.der
pub const PK_PEM: &'static [u8] = b"-----BEGIN PRIVATE KEY-----\r
MC4CAQAwBQYDK2VwBCIEIJRKGvS3yKtxf+zjzvDTHx2dIcDXz0LKeBLnqE0H8ALb\r
-----END PRIVATE KEY-----\r\n";
pub const PK_DER: &'static [u8] = b"\x30\x2E\x02\x01\x00\x30\x05\x06\x03\
\x2B\x65\x70\x04\x22\x04\x20\x94\x4A\x1A\xF4\xB7\xC8\xAB\x71\x7F\xEC\xE3\xCE\
\xF0\xD3\x1F\x1D\x9D\x21\xC0\xD7\xCF\x42\xCA\x78\x12\xE7\xA8\x4D\x07\xF0\x02\
\xDB";
pub const CERT_PEM: &'static [u8] = b"-----BEGIN CERTIFICATE-----\r
MIICLzCCAeGgAwIBAgIUdlMenq7MVkx5b1lFrvaBwvjlIEQwBQYDK2VwMIGMMQsw\r
CQYDVQQGEwJGUjEZMBcGA1UECAwQw4PCjmxlLWRlLUZyYW5jZTEOMAwGA1UEBwwF\r
UGFyaXMxDjAMBgNVBAoMBUFDTUVkMRkwFwYDVQQDDBB0ZXN0LmV4YW1wbGUub3Jn\r
MScwJQYJKoZIhvcNAQkBFhhpbnZhbGlkQHRlc3QuZXhhbXBsZS5vcmcwHhcNMTkw\r
MzE0MTE0NDI1WhcNMjAwMzEzMTE0NDI1WjCBjDELMAkGA1UEBhMCRlIxGTAXBgNV\r
BAgMEMODwo5sZS1kZS1GcmFuY2UxDjAMBgNVBAcMBVBhcmlzMQ4wDAYDVQQKDAVB\r
Q01FZDEZMBcGA1UEAwwQdGVzdC5leGFtcGxlLm9yZzEnMCUGCSqGSIb3DQEJARYY\r
aW52YWxpZEB0ZXN0LmV4YW1wbGUub3JnMCowBQYDK2VwAyEAboP+S9yfoP3euk+C\r
FgMIZ9J/Q6KxLwteCAvJSkbWTwKjUzBRMB0GA1UdDgQWBBT49UVSayhFWUaRiyiB\r
oXkSoRgynTAfBgNVHSMEGDAWgBT49UVSayhFWUaRiyiBoXkSoRgynTAPBgNVHRMB\r
Af8EBTADAQH/MAUGAytlcANBAPITjbIYNioMcpMDMvbyzHf2IqPFiNW/Ce3KTS8T\r
zseNNFkN0oOc55UAd2ECe6gGOXB0r4MycFOM9ccR2t8ttwE=\r
-----END CERTIFICATE-----\r\n";
pub const CERT_DER: &'static [u8] = b"\x30\x82\x02\x2F\x30\x82\x01\xE1\xA0\
\x03\x02\x01\x02\x02\x14\x76\x53\x1E\x9E\xAE\xCC\x56\x4C\x79\x6F\x59\x45\xAE\
\xF6\x81\xC2\xF8\xE5\x20\x44\x30\x05\x06\x03\x2B\x65\x70\x30\x81\x8C\x31\x0B\
\x30\x09\x06\x03\x55\x04\x06\x13\x02\x46\x52\x31\x19\x30\x17\x06\x03\x55\x04\
\x08\x0C\x10\xC3\x83\xC2\x8E\x6C\x65\x2D\x64\x65\x2D\x46\x72\x61\x6E\x63\x65\
\x31\x0E\x30\x0C\x06\x03\x55\x04\x07\x0C\x05\x50\x61\x72\x69\x73\x31\x0E\x30\
\x0C\x06\x03\x55\x04\x0A\x0C\x05\x41\x43\x4D\x45\x64\x31\x19\x30\x17\x06\x03\
\x55\x04\x03\x0C\x10\x74\x65\x73\x74\x2E\x65\x78\x61\x6D\x70\x6C\x65\x2E\x6F\
\x72\x67\x31\x27\x30\x25\x06\x09\x2A\x86\x48\x86\xF7\x0D\x01\x09\x01\x16\x18\
\x69\x6E\x76\x61\x6C\x69\x64\x40\x74\x65\x73\x74\x2E\x65\x78\x61\x6D\x70\x6C\
\x65\x2E\x6F\x72\x67\x30\x1E\x17\x0D\x31\x39\x30\x33\x31\x34\x31\x31\x34\x34\
\x32\x35\x5A\x17\x0D\x32\x30\x30\x33\x31\x33\x31\x31\x34\x34\x32\x35\x5A\x30\
\x81\x8C\x31\x0B\x30\x09\x06\x03\x55\x04\x06\x13\x02\x46\x52\x31\x19\x30\x17\
\x06\x03\x55\x04\x08\x0C\x10\xC3\x83\xC2\x8E\x6C\x65\x2D\x64\x65\x2D\x46\x72\
\x61\x6E\x63\x65\x31\x0E\x30\x0C\x06\x03\x55\x04\x07\x0C\x05\x50\x61\x72\x69\
\x73\x31\x0E\x30\x0C\x06\x03\x55\x04\x0A\x0C\x05\x41\x43\x4D\x45\x64\x31\x19\
\x30\x17\x06\x03\x55\x04\x03\x0C\x10\x74\x65\x73\x74\x2E\x65\x78\x61\x6D\x70\
\x6C\x65\x2E\x6F\x72\x67\x31\x27\x30\x25\x06\x09\x2A\x86\x48\x86\xF7\x0D\x01\
\x09\x01\x16\x18\x69\x6E\x76\x61\x6C\x69\x64\x40\x74\x65\x73\x74\x2E\x65\x78\
\x61\x6D\x70\x6C\x65\x2E\x6F\x72\x67\x30\x2A\x30\x05\x06\x03\x2B\x65\x70\x03\
\x21\x00\x6E\x83\xFE\x4B\xDC\x9F\xA0\xFD\xDE\xBA\x4F\x82\x16\x03\x08\x67\xD2\
\x7F\x43\xA2\xB1\x2F\x0B\x5E\x08\x0B\xC9\x4A\x46\xD6\x4F\x02\xA3\x53\x30\x51\
\x30\x1D\x06\x03\x55\x1D\x0E\x04\x16\x04\x14\xF8\xF5\x45\x52\x6B\x28\x45\x59\
\x46\x91\x8B\x28\x81\xA1\x79\x12\xA1\x18\x32\x9D\x30\x1F\x06\x03\x55\x1D\x23\
\x04\x18\x30\x16\x80\x14\xF8\xF5\x45\x52\x6B\x28\x45\x59\x46\x91\x8B\x28\x81\
\xA1\x79\x12\xA1\x18\x32\x9D\x30\x0F\x06\x03\x55\x1D\x13\x01\x01\xFF\x04\x05\
\x30\x03\x01\x01\xFF\x30\x05\x06\x03\x2B\x65\x70\x03\x41\x00\xF2\x13\x8D\xB2\
\x18\x36\x2A\x0C\x72\x93\x03\x32\xF6\xF2\xCC\x77\xF6\x22\xA3\xC5\x88\xD5\xBF\
\x09\xED\xCA\x4D\x2F\x13\xCE\xC7\x8D\x34\x59\x0D\xD2\x83\x9C\xE7\x95\x00\x77\
\x61\x02\x7B\xA8\x06\x39\x70\x74\xAF\x83\x32\x70\x53\x8C\xF5\xC7\x11\xDA\xDF\
\x2D\xB7\x01";
#[test]
fn test_der_to_der() {
let res = convert(
&CERT_DER,
&Format::Der,
&Format::Der,
PersistKind::Certificate,
);
assert!(res.is_ok());
let res = res.unwrap();
assert_eq!(CERT_DER, res.as_slice());
}
#[test]
fn test_pem_to_pem() {
let res = convert(
&CERT_PEM,
&Format::Pem,
&Format::Pem,
PersistKind::Certificate,
);
assert!(res.is_ok());
let res = res.unwrap();
assert_eq!(CERT_PEM, res.as_slice());
}
#[test]
fn test_der_to_pem_pk() {
let res = convert(&PK_DER, &Format::Der, &Format::Pem, PersistKind::PrivateKey);
assert!(res.is_ok());
let res = res.unwrap();
assert_eq!(PK_PEM, res.as_slice());
}
#[test]
fn test_der_to_pem_crt() {
let res = convert(
&CERT_DER,
&Format::Der,
&Format::Pem,
PersistKind::Certificate,
);
assert!(res.is_ok());
let res = res.unwrap();
assert_eq!(CERT_PEM, res.as_slice());
}
#[test]
fn test_pem_to_der_crt() {
let res = convert(
&CERT_PEM,
&Format::Pem,
&Format::Der,
PersistKind::Certificate,
);
assert!(res.is_ok());
let res = res.unwrap();
assert_eq!(CERT_DER, res.as_slice());
}
#[test]
fn test_pem_to_der_pk() {
let res = convert(&PK_PEM, &Format::Pem, &Format::Der, PersistKind::PrivateKey);
assert!(res.is_ok());
let res = res.unwrap();
assert_eq!(PK_DER, res.as_slice());
}
}

87
acmed/src/error.rs

@ -0,0 +1,87 @@
use std::fmt;
#[derive(Debug)]
pub struct Error {
pub message: String,
}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}", self.message)
}
}
impl From<&str> for Error {
fn from(error: &str) -> Self {
Error {
message: error.to_string(),
}
}
}
impl From<String> for Error {
fn from(error: String) -> Self {
error.as_str().into()
}
}
impl From<&String> for Error {
fn from(error: &String) -> Self {
error.as_str().into()
}
}
impl From<std::io::Error> for Error {
fn from(error: std::io::Error) -> Self {
format!("IO error: {}", error).into()
}
}
impl From<std::string::FromUtf8Error> for Error {
fn from(error: std::string::FromUtf8Error) -> Self {
format!("UTF-8 error: {}", error).into()
}
}
impl From<syslog::Error> for Error {
fn from(error: syslog::Error) -> Self {
format!("syslog error: {}", error).into()
}
}
impl From<toml::de::Error> for Error {
fn from(error: toml::de::Error) -> Self {
format!("IO error: {}", error).into()
}
}
impl From<serde_json::error::Error> for Error {
fn from(error: serde_json::error::Error) -> Self {
format!("IO error: {}", error).into()
}
}
impl From<handlebars::TemplateRenderError> for Error {
fn from(error: handlebars::TemplateRenderError) -> Self {
format!("Template error: {}", error).into()
}
}
impl From<openssl::error::ErrorStack> for Error {
fn from(error: openssl::error::ErrorStack) -> Self {
format!("{}", error).into()
}
}
impl From<http_req::error::Error> for Error {
fn from(error: http_req::error::Error) -> Self {
format!("HTTP error: {}", error).into()
}
}
#[cfg(unix)]
impl From<nix::Error> for Error {
fn from(error: nix::Error) -> Self {
format!("{}", error).into()
}
}

77
acmed/src/errors.rs

@ -1,77 +0,0 @@
use std::fmt;
#[derive(Debug)]
pub struct Error {
pub message: String,
}
impl Error {
pub fn new(msg: &str) -> Self {
Error {
message: msg.to_string(),
}
}
}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}", self.message)
}
}
impl From<std::io::Error> for Error {
fn from(error: std::io::Error) -> Self {
Error::new(&format!("IO error: {}", error))
}
}
impl From<&str> for Error {
fn from(error: &str) -> Self {
Error::new(error)
}
}
impl From<syslog::Error> for Error {
fn from(error: syslog::Error) -> Self {
Error::new(&format!("syslog error: {}", error))
}
}
impl From<toml::de::Error> for Error {
fn from(error: toml::de::Error) -> Self {
Error::new(&format!("IO error: {}", error))
}
}
impl From<acme_lib::Error> for Error {
fn from(error: acme_lib::Error) -> Self {
let msg = match error {
acme_lib::Error::ApiProblem(e) => format!("An API call failed: {}", e),
acme_lib::Error::Call(e) => format!("An API call failed: {}", e),
acme_lib::Error::Base64Decode(e) => format!("base 64 decode error: {}", e),
acme_lib::Error::Json(e) => format!("JSON error: {}", e),
acme_lib::Error::Io(e) => format!("IO error: {}", e),
acme_lib::Error::Other(s) => s,
};
Error::new(&msg)
}
}
impl From<handlebars::TemplateRenderError> for Error {
fn from(error: handlebars::TemplateRenderError) -> Self {
Error::new(&format!("Template error: {}", error))
}
}
impl From<openssl::error::ErrorStack> for Error {
fn from(error: openssl::error::ErrorStack) -> Self {
Error::new(&format!("{}", error))
}
}
#[cfg(unix)]
impl From<nix::Error> for Error {
fn from(error: nix::Error) -> Self {
Error::new(&format!("{}", error))
}
}

61
acmed/src/hooks.rs

@ -1,12 +1,55 @@
use crate::config::Hook;
use crate::errors::Error;
use crate::error::Error;
use handlebars::Handlebars;
use log::debug;
use serde::Serialize;
use std::fmt;
use std::fs::File;
use std::io::prelude::*;
use std::path::PathBuf;
use std::process::{Command, Stdio};
#[derive(Serialize)]
pub struct PostOperationHookData {
pub domains: Vec<String>,
pub algorithm: String,
pub challenge: String,
pub status: String,
}
#[derive(Serialize)]
pub struct ChallengeHookData {
pub domains: Vec<String>,
pub algorithm: String,
pub challenge: String,
pub current_domain: String,
pub file_name: String,
pub proof: String,
}
#[derive(Serialize)]
pub struct FileStorageHookData {
// TODO: add the current operation (create/edit)
pub file_name: String,
pub file_directory: String,
pub file_path: PathBuf,
}
#[derive(Clone, Debug)]
pub struct Hook {
pub name: String,
pub cmd: String,
pub args: Option<Vec<String>>,
pub stdin: Option<String>,
pub stdout: Option<String>,
pub stderr: Option<String>,
}
impl fmt::Display for Hook {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}", self.name)
}
}
macro_rules! get_hook_output {
($out: expr, $reg: ident, $data: expr) => {{
match $out {
@ -20,13 +63,6 @@ macro_rules! get_hook_output {
}};
}
pub fn call_multiple<T: Serialize>(data: &T, hooks: &[Hook]) -> Result<(), Error> {
for hook in hooks.iter() {
call(data, &hook)?;
}
Ok(())
}
pub fn call<T: Serialize>(data: &T, hook: &Hook) -> Result<(), Error> {
debug!("Calling hook: {}", hook.name);
let reg = Handlebars::new();
@ -66,3 +102,10 @@ pub fn call<T: Serialize>(data: &T, hook: &Hook) -> Result<(), Error> {
};
Ok(())
}
pub fn call_multiple<T: Serialize>(data: &T, hooks: &[Hook]) -> Result<(), Error> {
for hook in hooks.iter() {
call(data, &hook)?;
}
Ok(())
}

45
acmed/src/keygen.rs

@ -0,0 +1,45 @@
use crate::error::Error;
use openssl::ec::{EcGroup, EcKey};
use openssl::nid::Nid;
use openssl::pkey::{PKey, Private, Public};
use openssl::rsa::Rsa;
fn gen_ec_pair(nid: Nid) -> Result<(PKey<Private>, PKey<Public>), Error> {
let group = EcGroup::from_curve_name(nid).unwrap();
let ec_priv_key = EcKey::generate(&group).unwrap();
let public_key_point = ec_priv_key.public_key();
let ec_pub_key = EcKey::from_public_key(&group, public_key_point).unwrap();
Ok((
PKey::from_ec_key(ec_priv_key).unwrap(),
PKey::from_ec_key(ec_pub_key).unwrap(),
))
}
pub fn p256() -> Result<(PKey<Private>, PKey<Public>), Error> {
gen_ec_pair(Nid::X9_62_PRIME256V1)
}
pub fn p384() -> Result<(PKey<Private>, PKey<Public>), Error> {
gen_ec_pair(Nid::SECP384R1)
}
fn gen_rsa_pair(nb_bits: u32) -> Result<(PKey<Private>, PKey<Public>), Error> {
let priv_key = Rsa::generate(nb_bits).unwrap();
let pub_key = Rsa::from_public_components(
priv_key.n().to_owned().unwrap(),
priv_key.e().to_owned().unwrap(),
)
.unwrap();
Ok((
PKey::from_rsa(priv_key).unwrap(),
PKey::from_rsa(pub_key).unwrap(),
))
}
pub fn rsa2048() -> Result<(PKey<Private>, PKey<Public>), Error> {
gen_rsa_pair(2048)
}
pub fn rsa4096() -> Result<(PKey<Private>, PKey<Public>), Error> {
gen_rsa_pair(4096)
}

10
acmed/src/logs.rs

@ -1,4 +1,4 @@
use crate::errors::Error;
use crate::error::Error;
use env_logger::Builder;
use log::LevelFilter;
use syslog::Facility;
@ -18,7 +18,7 @@ fn get_loglevel(log_level: Option<&str>) -> Result<LevelFilter, Error> {
"debug" => LevelFilter::Debug,
"trace" => LevelFilter::Trace,
_ => {
return Err(Error::new(&format!("{}: invalid log level", v)));
return Err(format!("{}: invalid log level", v).into());
}
},
None => crate::DEFAULT_LOG_LEVEL,
@ -27,7 +27,11 @@ fn get_loglevel(log_level: Option<&str>) -> Result<LevelFilter, Error> {
}
fn set_log_syslog(log_level: LevelFilter) -> Result<(), Error> {
syslog::init(Facility::LOG_DAEMON, log_level, Some(crate::APP_NAME))?;
syslog::init(
Facility::LOG_DAEMON,
log_level,
Some(env!("CARGO_PKG_NAME")),
)?;
Ok(())
}

25
acmed/src/main.rs

@ -1,34 +1,41 @@
use crate::main_event_loop::MainEventLoop;
use clap::{App, Arg};
use daemonize::Daemonize;
use log::{error, LevelFilter};
mod acmed;
mod acme_proto;
mod certificate;
mod config;
mod encoding;
mod errors;
mod error;
mod hooks;
mod keygen;
mod logs;
mod main_event_loop;
mod storage;
pub const APP_NAME: &str = "acmed";
pub const APP_NAME: &str = "ACMEd";
pub const APP_VERSION: &str = env!("CARGO_PKG_VERSION");
pub const DEFAULT_PID_FILE: &str = "/var/run/admed.pid";
pub const DEFAULT_CONFIG_FILE: &str = "/etc/acmed/acmed.toml";
pub const DEFAULT_ACCOUNTS_DIR: &str = "/etc/acmed/accounts";
pub const DEFAULT_CERT_DIR: &str = "/etc/acmed/certs";
pub const DEFAULT_CERT_FORMAT: &str = "{name}_{algo}.{kind}.{ext}";
pub const DEFAULT_CERT_FORMAT: &str = "{{name}}_{{algo}}.{{file_type}}.{{ext}}";
pub const DEFAULT_ALGO: &str = "rsa2048";
pub const DEFAULT_FMT: acmed::Format = acmed::Format::Pem;
pub const DEFAULT_SLEEP_TIME: u64 = 3600;
pub const DEFAULT_POOL_TIME: u64 = 5000;
pub const DEFAULT_CERT_FILE_MODE: u32 = 0o644;
pub const DEFAULT_PK_FILE_MODE: u32 = 0o600;
pub const DEFAULT_ACCOUNT_FILE_MODE: u32 = 0o600;
pub const DEFAULT_KP_REUSE: bool = false;
pub const DEFAULT_LOG_SYSTEM: logs::LogSystem = logs::LogSystem::SysLog;
pub const DEFAULT_LOG_LEVEL: LevelFilter = LevelFilter::Warn;
pub const DEFAULT_JWS_SIGN_ALGO: &str = "ES256";
pub const DEFAULT_POOL_NB_TRIES: usize = 10;
pub const DEFAULT_POOL_WAIT_SEC: u64 = 5;
fn main() {
let matches = App::new("acmed")
.version("0.2.1")
let matches = App::new(APP_NAME)
.version(APP_VERSION)
.arg(
Arg::with_name("config")
.short("c")
@ -98,7 +105,7 @@ fn main() {
}
let config_file = matches.value_of("config").unwrap_or(DEFAULT_CONFIG_FILE);
let mut srv = match acmed::Acmed::new(&config_file) {
let mut srv = match MainEventLoop::new(&config_file) {
Ok(s) => s,
Err(e) => {
error!("{}", e);

92
acmed/src/main_event_loop.rs

@ -0,0 +1,92 @@
use crate::acme_proto::request_certificate;
use crate::certificate::Certificate;
use crate::config;
use crate::error::Error;
use log::{debug, warn};
use std::thread;
use std::time::Duration;
pub struct MainEventLoop {
certs: Vec<Certificate>,
}
impl MainEventLoop {
pub fn new(config_file: &str) -> Result<Self, Error> {
let cnf = config::from_file(config_file)?;
let mut certs = Vec::new();
for crt in cnf.certificate.iter() {
let cert = Certificate {
domains: crt.domains.to_owned(),
algo: crt.get_algorithm()?,
kp_reuse: crt.get_kp_reuse(),
email: crt.email.to_owned(),
remote_url: crt.get_remote_url(&cnf)?,
challenge: crt.get_challenge()?,
challenge_hooks: crt.get_challenge_hooks(&cnf)?,
post_operation_hooks: crt.get_post_operation_hooks(&cnf)?,
account_directory: cnf.get_account_dir(),
crt_directory: crt.get_crt_dir(&cnf),
crt_name: crt.get_crt_name(),
crt_name_format: crt.get_crt_name_format(),
cert_file_mode: cnf.get_cert_file_mode(),
cert_file_owner: cnf.get_cert_file_user(),
cert_file_group: cnf.get_cert_file_group(),
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)?,
};
certs.push(cert);
}
Ok(MainEventLoop { certs })
}
pub fn run(&mut self) {
loop {
for crt in self.certs.iter_mut() {
debug!("{}", crt);
match crt.should_renew() {
Ok(sr) => {
if sr {
let status = match request_certificate(crt) {
Ok(_) => "Success.".to_string(),
Err(e) => {
let msg = format!(
"Unable to renew the {} certificate for {}: {}",
crt.algo,
crt.domains.first().unwrap(),
e
);
warn!("{}", msg);
format!("Failed: {}", msg)
}
};
match crt.call_post_operation_hooks(&status) {
Ok(_) => {}
Err(e) => {
let msg = format!(
"{} certificate for {}: post-operation hook error: {}",
crt.algo,
crt.domains.first().unwrap(),
e
);
warn!("{}", msg);
}
};
}
}
Err(e) => {
warn!("{}", e);
}
};
}
thread::sleep(Duration::from_secs(crate::DEFAULT_SLEEP_TIME));
}
}
}

413
acmed/src/storage.rs

@ -1,221 +1,250 @@
use crate::acmed::{Algorithm, Format};
use crate::config::Hook;
use crate::encoding::convert;
use crate::errors;
use crate::hooks;
use acme_lib::persist::{Persist, PersistKey, PersistKind};
use acme_lib::Error;
use log::debug;
use serde::Serialize;
use crate::certificate::Certificate;
use crate::error::Error;
use crate::hooks::{self, FileStorageHookData};
use log::trace;
use openssl::pkey::{PKey, Private, Public};
use openssl::x509::X509;
use std::fmt;
use std::fs::{File, OpenOptions};
use std::io::prelude::*;
use std::io::{Read, Write};
use std::path::PathBuf;
#[cfg(target_family = "unix")]
use std::os::unix::fs::OpenOptionsExt;
macro_rules! get_file_name {
($self: ident, $kind: ident, $fmt: ident) => {{
let kind = match $kind {
PersistKind::Certificate => "crt",
PersistKind::PrivateKey => "pk",
PersistKind::AccountPrivateKey => "pk",
#[derive(Clone)]
enum FileType {
AccountPrivateKey,
AccountPublicKey,
PrivateKey,
Certificate,
}
impl fmt::Display for FileType {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let s = match self {
FileType::AccountPrivateKey => "priv-key",
FileType::AccountPublicKey => "pub-key",
FileType::PrivateKey => "pk",
FileType::Certificate => "crt",
};
format!(
// TODO: use self.crt_name_format instead of a string literal
"{name}_{algo}.{kind}.{ext}",
name = $self.crt_name,
algo = $self.algo.to_string(),
kind = kind,
ext = $fmt.to_string()
)
}};
}
#[derive(Serialize)]
struct FileData {
file_name: String,
file_directory: String,
file_path: PathBuf,
}
#[derive(Clone, Debug)]
pub struct Storage {
pub account_directory: String,
pub account_name: String,
pub crt_directory: String,
pub crt_name: String,
pub crt_name_format: String,
pub formats: Vec<Format>,
pub algo: Algorithm,
pub cert_file_mode: u32,
pub cert_file_owner: Option<String>,
pub cert_file_group: Option<String>,
pub pk_file_mode: u32,
pub pk_file_owner: Option<String>,
pub pk_file_group: Option<String>,
pub file_pre_create_hooks: Vec<Hook>,
pub file_post_create_hooks: Vec<Hook>,
pub file_pre_edit_hooks: Vec<Hook>,
pub file_post_edit_hooks: Vec<Hook>,
}
impl Storage {
#[cfg(unix)]
fn get_file_mode(&self, kind: PersistKind) -> u32 {
match kind {
PersistKind::Certificate => self.cert_file_mode,
PersistKind::PrivateKey | PersistKind::AccountPrivateKey => self.pk_file_mode,
}
write!(f, "{}", s)
}
}
#[cfg(unix)]
fn set_owner(&self, path: &PathBuf, kind: PersistKind) -> Result<(), Error> {
let (uid, gid) = match kind {
PersistKind::Certificate => (&self.cert_file_owner, &self.cert_file_group),
PersistKind::PrivateKey | PersistKind::AccountPrivateKey => {
(&self.pk_file_owner, &self.pk_file_group)
}
};
let uid = match uid {
Some(u) => {
if u.bytes().all(|b| b.is_ascii_digit()) {
let raw_uid = u.parse::<u32>().unwrap();
let nix_uid = nix::unistd::Uid::from_raw(raw_uid);
Some(nix_uid)
} else {
// TODO: handle username
None
}
}
None => None,
};
let gid = match gid {
Some(g) => {
if g.bytes().all(|b| b.is_ascii_digit()) {
let raw_gid = g.parse::<u32>().unwrap();
let nix_gid = nix::unistd::Gid::from_raw(raw_gid);
Some(nix_gid)
} else {
// TODO: handle group name
None
}
}
None => None,
};
match nix::unistd::chown(path, uid, gid) {
Ok(_) => Ok(()),
Err(e) => Err(Error::Other(format!("{}", e))),
fn get_file_full_path(
cert: &Certificate,
file_type: FileType,
) -> Result<(String, String, PathBuf), Error> {
let base_path = match file_type {
FileType::AccountPrivateKey | FileType::AccountPublicKey => &cert.account_directory,
FileType::PrivateKey => &cert.crt_directory,
FileType::Certificate => &cert.crt_directory,
};
let file_name = match file_type {
FileType::AccountPrivateKey | FileType::AccountPublicKey => format!(
"{email}.{file_type}.{ext}",
email = cert.email,
file_type = file_type.to_string(),
ext = "pem"
),
FileType::PrivateKey | FileType::Certificate => {
// TODO: use cert.crt_name_format instead of a string literal
format!(
"{name}_{algo}.{file_type}.{ext}",
name = cert.crt_name,
algo = cert.algo.to_string(),
file_type = file_type.to_string(),
ext = "pem"
)
}
}
};
let mut path = PathBuf::from(&base_path);
path.push(&file_name);
Ok((base_path.to_string(), file_name, path))
}
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,
PersistKind::AccountPrivateKey => &self.account_directory,
};
let file_name = match kind {
PersistKind::Certificate => get_file_name!(self, kind, fmt),
PersistKind::PrivateKey => get_file_name!(self, kind, fmt),
PersistKind::AccountPrivateKey => {
format!("{}.{}", self.account_name.to_owned(), fmt.to_string())
fn get_file_path(cert: &Certificate, file_type: FileType) -> Result<PathBuf, Error> {
let (_, _, path) = get_file_full_path(cert, file_type)?;
Ok(path)
}
fn read_file(path: &PathBuf) -> Result<Vec<u8>, Error> {
trace!("Reading file {:?}", path);
let mut file = File::open(path)?;
let mut contents = vec![];
file.read_to_end(&mut contents)?;
Ok(contents)
}
#[cfg(unix)]
fn set_owner(cert: &Certificate, path: &PathBuf, file_type: FileType) -> Result<(), Error> {
let (uid, gid) = match file_type {
FileType::Certificate => (
cert.cert_file_owner.to_owned(),
cert.cert_file_group.to_owned(),
),
FileType::PrivateKey => (cert.pk_file_owner.to_owned(), cert.pk_file_group.to_owned()),
FileType::AccountPrivateKey | FileType::AccountPublicKey => {
// The account private and public keys does not need to be accessible to users other different from the current one.
return Ok(());
}
};
let uid = match uid {
Some(u) => {
if u.bytes().all(|b| b.is_ascii_digit()) {
let raw_uid = u.parse::<u32>().unwrap();
let nix_uid = nix::unistd::Uid::from_raw(raw_uid);
Some(nix_uid)
} else {
// TODO: handle username
None
}
}
None => None,
};
let gid = match gid {
Some(g) => {
if g.bytes().all(|b| b.is_ascii_digit()) {
let raw_gid = g.parse::<u32>().unwrap();
let nix_gid = nix::unistd::Gid::from_raw(raw_gid);
Some(nix_gid)
} else {
// TODO: handle group name
None
}
};
let mut path = PathBuf::from(base_path);
path.push(&file_name);
FileData {
file_directory: base_path.to_string(),
file_name,
file_path: path,
}
None => None,
};
match uid {
Some(u) => trace!("Setting the uid to {}", u.as_raw()),
None => trace!("Uid unchanged"),
};
match gid {
Some(g) => trace!("Setting the gid to {}", g.as_raw()),
None => trace!("Gid unchanged"),
};
match nix::unistd::chown(path, uid, gid) {
Ok(_) => Ok(()),
Err(e) => Err(format!("{}", e).into()),
}
}
fn write_file(cert: &Certificate, file_type: FileType, data: &[u8]) -> Result<(), Error> {
let (file_directory, file_name, path) = get_file_full_path(cert, file_type.clone())?;
let hook_data = FileStorageHookData {
file_name,
file_directory,
file_path: path.to_path_buf(),
};
let is_new = !path.is_file();
pub fn get_certificate(&self, fmt: &Format) -> Result<Option<Vec<u8>>, Error> {
self.get_file(PersistKind::Certificate, fmt)
if is_new {
hooks::call_multiple(&hook_data, &cert.file_pre_create_hooks)?;
} else {
hooks::call_multiple(&hook_data, &cert.file_pre_edit_hooks)?;
}
pub fn get_private_key(&self, fmt: &Format) -> Result<Option<Vec<u8>>, Error> {
self.get_file(PersistKind::PrivateKey, fmt)
trace!("Writing file {:?}", path);
let mut file = if cfg!(unix) {
let mut options = OpenOptions::new();
options.mode(match &file_type {
FileType::Certificate => cert.cert_file_mode,
FileType::PrivateKey => cert.pk_file_mode,
FileType::AccountPublicKey => crate::DEFAULT_ACCOUNT_FILE_MODE,
FileType::AccountPrivateKey => crate::DEFAULT_ACCOUNT_FILE_MODE,
});
options.write(true).create(true).open(&path)?
} else {
File::create(&path)?
};
file.write_all(data)?;
if cfg!(unix) {
set_owner(cert, &path, file_type)?;
}
pub fn get_file(&self, kind: PersistKind, fmt: &Format) -> Result<Option<Vec<u8>>, Error> {
let src_fmt = if self.formats.contains(fmt) {
fmt
} else {
self.formats.first().unwrap()
};
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(&file_data.file_path)?;
let mut contents = vec![];
file.read_to_end(&mut contents)?;
if contents.is_empty() {
return Ok(None);
}
if src_fmt == fmt {
Ok(Some(contents))
} else {
let ret = convert(&contents, src_fmt, fmt, kind)?;
Ok(Some(ret))
}
if is_new {
hooks::call_multiple(&hook_data, &cert.file_post_create_hooks)?;
} else {
hooks::call_multiple(&hook_data, &cert.file_post_edit_hooks)?;
}
Ok(())
}
impl Persist for Storage {
fn put(&self, key: &PersistKey, value: &[u8]) -> Result<(), Error> {
for fmt in self.formats.iter() {
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(&file_data.file_path)?
} else {
File::create(&file_data.file_path)?
};
match fmt {
Format::Der => {
let val = convert(value, &Format::Pem, &Format::Der, key.kind)?;
f.write_all(&val)?;
}
Format::Pem => f.write_all(value)?,
};
f.sync_all()?;
}
if cfg!(unix) {
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)?;
pub fn get_account_priv_key(cert: &Certificate) -> Result<PKey<Private>, Error> {
let path = get_file_path(cert, FileType::AccountPrivateKey)?;
let raw_key = read_file(&path)?;
let key = PKey::private_key_from_pem(&raw_key)?;
Ok(key)
}
pub fn set_account_priv_key(cert: &Certificate, key: &PKey<Private>) -> Result<(), Error> {
let data = key.private_key_to_pem_pkcs8()?;
write_file(cert, FileType::AccountPrivateKey, &data)
}
pub fn get_account_pub_key(cert: &Certificate) -> Result<PKey<Public>, Error> {
let path = get_file_path(cert, FileType::AccountPublicKey)?;
let raw_key = read_file(&path)?;
let key = PKey::public_key_from_pem(&raw_key)?;
Ok(key)
}
pub fn set_account_pub_key(cert: &Certificate, key: &PKey<Public>) -> Result<(), Error> {
let data = key.public_key_to_pem()?;
write_file(cert, FileType::AccountPublicKey, &data)
}
pub fn get_priv_key(cert: &Certificate) -> Result<PKey<Private>, Error> {
let path = get_file_path(cert, FileType::PrivateKey)?;
let raw_key = read_file(&path)?;
let key = PKey::private_key_from_pem(&raw_key)?;
Ok(key)
}
pub fn set_priv_key(cert: &Certificate, key: &PKey<Private>) -> Result<(), Error> {
let data = key.private_key_to_pem_pkcs8()?;
write_file(cert, FileType::PrivateKey, &data)
}
pub fn get_pub_key(cert: &Certificate) -> Result<PKey<Public>, Error> {
let pub_key = get_certificate(cert)?.public_key()?;
Ok(pub_key)
}
pub fn get_certificate(cert: &Certificate) -> Result<X509, Error> {
let path = get_file_path(cert, FileType::Certificate)?;
let raw_crt = read_file(&path)?;
let crt = X509::from_pem(&raw_crt)?;
Ok(crt)
}
pub fn write_certificate(cert: &Certificate, data: &[u8]) -> Result<(), Error> {
write_file(cert, FileType::Certificate, data)
}
fn check_files(cert: &Certificate, file_types: &[FileType]) -> bool {
for t in file_types.to_vec() {
let path = match get_file_path(cert, t) {
Ok(p) => p,
Err(_) => {
return false;
}
};
trace!("Testing file path: {}", path.to_str().unwrap());
if !path.is_file() {
return false;
}
Ok(())
}
true
}
fn get(&self, key: &PersistKey) -> Result<Option<Vec<u8>>, Error> {
self.get_file(key.kind, &Format::Pem)
}
pub fn account_files_exists(cert: &Certificate) -> bool {
let file_types = vec![FileType::AccountPrivateKey, FileType::AccountPublicKey];
check_files(cert, &file_types)
}
fn to_acme_err(e: errors::Error) -> Error {
Error::Other(e.message)
pub fn certificate_files_exists(cert: &Certificate) -> bool {
let file_types = vec![FileType::PrivateKey, FileType::Certificate];
check_files(cert, &file_types)
}
Loading…
Cancel
Save