mirror of https://github.com/breard-r/acmed.git
Browse Source
Remove the `acme-lib` dependency
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 #1pull/5/head
Rodolphe Breard
6 years ago
27 changed files with 2439 additions and 864 deletions
-
10CHANGELOG.md
-
13acmed/Cargo.toml
-
78acmed/build.rs
-
212acmed/src/acme_proto.rs
-
54acmed/src/acme_proto/account.rs
-
58acmed/src/acme_proto/certificate.rs
-
175acmed/src/acme_proto/http.rs
-
158acmed/src/acme_proto/jws.rs
-
181acmed/src/acme_proto/jws/algorithms.rs
-
43acmed/src/acme_proto/jws/jwk.rs
-
23acmed/src/acme_proto/structs.rs
-
154acmed/src/acme_proto/structs/account.rs
-
299acmed/src/acme_proto/structs/authorization.rs
-
147acmed/src/acme_proto/structs/directory.rs
-
135acmed/src/acme_proto/structs/order.rs
-
333acmed/src/acmed.rs
-
159acmed/src/certificate.rs
-
58acmed/src/config.rs
-
203acmed/src/encoding.rs
-
87acmed/src/error.rs
-
77acmed/src/errors.rs
-
61acmed/src/hooks.rs
-
45acmed/src/keygen.rs
-
10acmed/src/logs.rs
-
25acmed/src/main.rs
-
92acmed/src/main_event_loop.rs
-
347acmed/src/storage.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();
|
||||
|
}
|
@ -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)
|
||||
|
}
|
@ -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))
|
||||
|
}
|
||||
|
}
|
@ -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())
|
||||
|
}
|
@ -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));
|
||||
|
}
|
||||
|
}
|
||||
|
}
|
@ -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));
|
||||
|
}
|
||||
|
}
|
@ -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)
|
||||
|
}
|
||||
|
}
|
@ -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 {}
|
||||
|
}
|
||||
|
}
|
@ -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};
|
@ -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\""));
|
||||
|
}
|
||||
|
}
|
@ -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),
|
||||
|
}
|
||||
|
}
|
||||
|
}
|
@ -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());
|
||||
|
}
|
||||
|
}
|
@ -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());
|
||||
|
}
|
||||
|
}
|
@ -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));
|
|
||||
}
|
|
||||
}
|
|
||||
}
|
|
@ -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(¬_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(())
|
||||
|
}
|
||||
|
}
|
@ -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());
|
|
||||
}
|
|
||||
}
|
|
@ -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()
|
||||
|
}
|
||||
|
}
|
@ -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))
|
|
||||
}
|
|
||||
}
|
|
@ -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)
|
||||
|
}
|
@ -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));
|
||||
|
}
|
||||
|
}
|
||||
|
}
|
Write
Preview
Loading…
Cancel
Save
Reference in new issue