Browse Source

Refactor the account management

Until now, the account management was archaic and it was impossible to
improve it without this heavy refactoring. Accounts are now disjoint
from both certificates and endpoints. They have their own hooks and
their own environment variables. They are stored in a binary file
instead of the PEM exports of the private and public keys.

This refactoring will allow account management to evolve into something
more serious, with real key rollover, contact information update and so
on.
pull/39/head
Rodolphe Breard 4 years ago
parent
commit
51cfd49f08
  1. 11
      CHANGELOG.md
  2. 14
      acme_common/src/crypto/openssl_keys.rs
  3. 1
      acmed/Cargo.toml
  4. 252
      acmed/src/account.rs
  5. 110
      acmed/src/account/contact.rs
  6. 135
      acmed/src/account/storage.rs
  7. 38
      acmed/src/acme_proto.rs
  8. 91
      acmed/src/acme_proto/account.rs
  9. 5
      acmed/src/acme_proto/structs/account.rs
  10. 4
      acmed/src/acme_proto/structs/directory.rs
  11. 11
      acmed/src/certificate.rs
  12. 53
      acmed/src/config.rs
  13. 2
      acmed/src/endpoint.rs
  14. 77
      acmed/src/main_event_loop.rs
  15. 54
      acmed/src/storage.rs
  16. 14
      man/en/acmed.toml.5

11
CHANGELOG.md

@ -13,6 +13,17 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
### Added
- The `contacts` account configuration field has been added.
### Changed
- The `email` account configuration field has been removed. In replacement, use the `contacts` field.
- Accounts now have their own hooks and environment.
- Accounts are now stored in a single binary file.
## [0.10.0] - 2020-08-27
### Added

14
acme_common/src/crypto/openssl_keys.rs

@ -43,12 +43,22 @@ macro_rules! get_key_type {
};
}
#[derive(Clone, Debug)]
pub struct KeyPair {
pub key_type: KeyType,
pub inner_key: PKey<Private>,
}
impl KeyPair {
pub fn from_der(der_data: &[u8]) -> Result<Self, Error> {
let inner_key = PKey::private_key_from_der(der_data)?;
let key_type = get_key_type!(inner_key);
Ok(KeyPair {
key_type,
inner_key,
})
}
pub fn from_pem(pem_data: &[u8]) -> Result<Self, Error> {
let inner_key = PKey::private_key_from_pem(pem_data)?;
let key_type = get_key_type!(inner_key);
@ -58,6 +68,10 @@ impl KeyPair {
})
}
pub fn private_key_to_der(&self) -> Result<Vec<u8>, Error> {
self.inner_key.private_key_to_der().map_err(Error::from)
}
pub fn private_key_to_pem(&self) -> Result<Vec<u8>, Error> {
self.inner_key
.private_key_to_pem_pkcs8()

1
acmed/Cargo.toml

@ -19,6 +19,7 @@ openssl_dyn = ["acme_common/openssl_dyn", "attohttpc/tls"]
[dependencies]
acme_common = { path = "../acme_common" }
attohttpc = { version = "0.15", default-features = false, features = ["charsets", "json"] }
bincode = "1.3"
clap = "2.32"
glob = "0.3"
handlebars = "3.0"

252
acmed/src/account.rs

@ -1,37 +1,259 @@
use crate::acme_proto::account::register_account;
use crate::endpoint::Endpoint;
use crate::logs::HasLogger;
use crate::storage::FileManager;
use acme_common::crypto::{JwsSignatureAlgorithm, KeyType};
use acme_common::crypto::{gen_keypair, HashFunction, JwsSignatureAlgorithm, KeyPair, KeyType};
use acme_common::error::Error;
use std::collections::HashMap;
use std::fmt;
use std::str::FromStr;
use std::time::SystemTime;
mod contact;
mod storage;
#[derive(Clone, Debug)]
pub enum AccountContactType {
Mailfrom,
}
impl FromStr for AccountContactType {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Error> {
match s.to_lowercase().as_str() {
"mailfrom" => Ok(AccountContactType::Mailfrom),
_ => Err(format!("{}: unknown contact type.", s).into()),
}
}
}
impl fmt::Display for AccountContactType {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let s = match self {
AccountContactType::Mailfrom => "mailfrom",
};
write!(f, "{}", s)
}
}
#[derive(Clone, Debug)]
pub struct AccountKey {
pub creation_date: SystemTime,
pub key: KeyPair,
pub signature_algorithm: JwsSignatureAlgorithm,
}
impl AccountKey {
fn new(key_type: KeyType, signature_algorithm: JwsSignatureAlgorithm) -> Result<Self, Error> {
Ok(AccountKey {
creation_date: SystemTime::now(),
key: gen_keypair(key_type)?,
signature_algorithm,
})
}
}
#[derive(Clone, Debug, Hash)]
pub struct AccountEndpoint {
pub creation_date: SystemTime,
pub account_url: String,
pub order_url: String,
pub key_hash: Vec<u8>,
pub contacts_hash: Vec<u8>,
}
impl AccountEndpoint {
pub fn new() -> Self {
AccountEndpoint {
creation_date: SystemTime::UNIX_EPOCH,
account_url: String::new(),
order_url: String::new(),
key_hash: Vec::new(),
contacts_hash: Vec::new(),
}
}
}
#[derive(Clone, Debug)]
pub struct Account {
pub name: String,
pub email: String,
pub key_type: KeyType,
pub signature_algorithm: JwsSignatureAlgorithm,
pub endpoints: HashMap<String, AccountEndpoint>,
pub contacts: Vec<contact::AccountContact>,
pub current_key: AccountKey,
pub past_keys: Vec<AccountKey>,
pub file_manager: FileManager,
}
impl HasLogger for Account {
fn warn(&self, msg: &str) {
log::warn!("account \"{}\": {}", &self.name, msg);
}
fn info(&self, msg: &str) {
log::info!("account \"{}\": {}", &self.name, msg);
}
fn debug(&self, msg: &str) {
log::debug!("account \"{}\": {}", &self.name, msg);
}
fn trace(&self, msg: &str) {
log::trace!("account \"{}\": {}", &self.name, msg);
}
}
impl Account {
pub fn new(
_file_manager: &FileManager,
pub fn get_endpoint_mut(&mut self, endpoint_name: &str) -> Result<&mut AccountEndpoint, Error> {
match self.endpoints.get_mut(endpoint_name) {
Some(ep) => Ok(ep),
None => {
let msg = format!(
"{}: unknown endpoint for account {}",
endpoint_name, self.name
);
Err(msg.into())
}
}
}
pub fn get_endpoint(&self, endpoint_name: &str) -> Result<&AccountEndpoint, Error> {
match self.endpoints.get(endpoint_name) {
Some(ep) => Ok(ep),
None => {
let msg = format!(
"{}: unknown endpoint for account {}",
endpoint_name, self.name
);
Err(msg.into())
}
}
}
pub fn load(
file_manager: &FileManager,
name: &str,
email: &str,
contacts: &[(String, String)],
key_type: &Option<String>,
signature_algorithm: &Option<String>,
) -> Result<Self, Error> {
let contacts = contacts
.iter()
.map(|(k, v)| contact::AccountContact::new(k, v))
.collect::<Result<Vec<contact::AccountContact>, Error>>()?;
let key_type = match key_type {
Some(kt) => KeyType::from_str(&kt)?,
Some(kt) => kt.parse()?,
None => crate::DEFAULT_ACCOUNT_KEY_TYPE,
};
let signature_algorithm = match signature_algorithm {
Some(sa) => JwsSignatureAlgorithm::from_str(&sa)?,
Some(sa) => sa.parse()?,
None => key_type.get_default_signature_alg(),
};
Ok(crate::account::Account {
name: name.to_string(),
email: email.to_string(),
key_type,
signature_algorithm,
})
key_type.check_alg_compatibility(&signature_algorithm)?;
let account = match storage::fetch(file_manager, name)? {
Some(mut a) => {
a.update_keys(key_type, signature_algorithm)?;
a.contacts = contacts;
a
}
None => {
let account = Account {
name: name.to_string(),
endpoints: HashMap::new(),
contacts,
current_key: AccountKey::new(key_type, signature_algorithm)?,
past_keys: Vec::new(),
file_manager: file_manager.clone(),
};
account.debug("initializing a new account");
account
}
};
Ok(account)
}
pub fn add_endpoint_name(&mut self, endpoint_name: &str) {
self.endpoints
.entry(endpoint_name.to_string())
.or_insert_with(AccountEndpoint::new);
}
pub fn synchronize(
&mut self,
endpoint: &mut Endpoint,
root_certs: &[String],
) -> Result<(), Error> {
register_account(endpoint, root_certs, self)?;
Ok(())
}
pub fn save(&self) -> Result<(), Error> {
storage::save(&self.file_manager, self)
}
pub fn set_account_url(&mut self, endpoint_name: &str, account_url: &str) -> Result<(), Error> {
let mut ep = self.get_endpoint_mut(endpoint_name)?;
ep.account_url = account_url.to_string();
Ok(())
}
pub fn set_order_url(&mut self, endpoint_name: &str, order_url: &str) -> Result<(), Error> {
let mut ep = self.get_endpoint_mut(endpoint_name)?;
ep.order_url = order_url.to_string();
Ok(())
}
pub fn update_key_hash(&mut self, endpoint_name: &str) -> Result<(), Error> {
let key = self.current_key.clone();
let mut ep = self.get_endpoint_mut(endpoint_name)?;
ep.key_hash = hash_key(&key)?;
Ok(())
}
pub fn update_contacts_hash(&mut self, endpoint_name: &str) -> Result<(), Error> {
let ct = self.contacts.clone();
let mut ep = self.get_endpoint_mut(endpoint_name)?;
ep.contacts_hash = hash_contacts(&ct);
Ok(())
}
fn update_keys(
&mut self,
key_type: KeyType,
signature_algorithm: JwsSignatureAlgorithm,
) -> Result<(), Error> {
if self.current_key.key.key_type != key_type
|| self.current_key.signature_algorithm != signature_algorithm
{
self.debug("account key has been changed in the configuration, creating a new one...");
self.past_keys.push(self.current_key.to_owned());
self.current_key = AccountKey::new(key_type, signature_algorithm)?;
self.save()?;
let msg = format!(
"new {} account key created, using {} as signing algorithm",
key_type, signature_algorithm
);
self.info(&msg);
} else {
self.trace("account key is up to date");
}
Ok(())
}
}
fn hash_contacts(contacts: &[contact::AccountContact]) -> Vec<u8> {
let msg = contacts
.iter()
.map(|v| v.to_string())
.collect::<Vec<String>>()
.join("")
.into_bytes();
HashFunction::Sha256.hash(&msg)
}
fn hash_key(key: &AccountKey) -> Result<Vec<u8>, Error> {
let mut msg = key.signature_algorithm.to_string().into_bytes();
let pem = key.key.public_key_to_pem()?;
msg.extend_from_slice(&pem);
Ok(HashFunction::Sha256.hash(&msg))
}

110
acmed/src/account/contact.rs

@ -0,0 +1,110 @@
use acme_common::error::Error;
use std::fmt;
use std::str::FromStr;
fn clean_mailto(value: &str) -> Result<String, Error> {
// TODO: implement a simple RFC 6068 parser
// - no "hfields"
// - max one "addr-spec" in the "to" component
Ok(value.to_string())
}
// TODO: implement other URI shemes
// https://www.iana.org/assignments/uri-schemes/uri-schemes.xhtml
// https://en.wikipedia.org/wiki/List_of_URI_schemes
// Exemples:
// - P1: tel, sms
// - P2: geo, maps
// - P3: irc, irc6, ircs, xmpp
// - P4: sip, sips
#[derive(Clone, Debug, PartialEq)]
pub enum ContactType {
Mailto,
}
impl ContactType {
pub fn clean_value(&self, value: &str) -> Result<String, Error> {
match self {
ContactType::Mailto => clean_mailto(value),
}
}
}
impl FromStr for ContactType {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Error> {
match s.to_lowercase().as_str() {
"mailto" => Ok(ContactType::Mailto),
_ => Err(format!("{}: unknown contact type.", s).into()),
}
}
}
impl fmt::Display for ContactType {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let s = match self {
ContactType::Mailto => "mailto",
};
write!(f, "{}", s)
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct AccountContact {
pub contact_type: ContactType,
pub value: String,
}
impl AccountContact {
pub fn new(contact_type: &str, value: &str) -> Result<Self, Error> {
let contact_type: ContactType = contact_type.parse()?;
let value = contact_type.clean_value(value)?;
Ok(AccountContact {
contact_type,
value,
})
}
}
impl fmt::Display for AccountContact {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}:{}", self.contact_type, self.value)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_account_contact_eq() {
let c1 = AccountContact::new("mailto", "derp.derpson@example.com").unwrap();
let c2 = AccountContact::new("mailto", "derp.derpson@example.com").unwrap();
let c3 = AccountContact::new("mailto", "derp@example.com").unwrap();
assert_eq!(c1, c2);
assert_eq!(c2, c1);
assert_ne!(c1, c3);
assert_ne!(c2, c3);
}
#[test]
fn test_account_contact_in_vec() {
let contacts = vec![
AccountContact::new("mailto", "derp.derpson@example.com").unwrap(),
AccountContact::new("mailto", "derp@example.com").unwrap(),
];
let c = AccountContact::new("mailto", "derp@example.com").unwrap();
assert!(contacts.contains(&c));
}
#[test]
fn test_account_contact_not_in_vec() {
let contacts = vec![
AccountContact::new("mailto", "derp.derpson@example.com").unwrap(),
AccountContact::new("mailto", "derp@example.com").unwrap(),
];
let c = AccountContact::new("mailto", "derpina@example.com").unwrap();
assert!(!contacts.contains(&c));
}
}

135
acmed/src/account/storage.rs

@ -0,0 +1,135 @@
use crate::account::contact::AccountContact;
use crate::account::{Account, AccountEndpoint, AccountKey};
use crate::storage::{account_files_exists, get_account_data, set_account_data, FileManager};
use acme_common::crypto::KeyPair;
use acme_common::error::Error;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::time::SystemTime;
#[derive(Serialize, Deserialize, PartialEq, Debug)]
struct AccountKeyStorage {
creation_date: SystemTime,
key: Vec<u8>,
signature_algorithm: String,
}
impl AccountKeyStorage {
fn new(key: &AccountKey) -> Result<Self, Error> {
Ok(AccountKeyStorage {
creation_date: key.creation_date,
key: key.key.private_key_to_der()?,
signature_algorithm: key.signature_algorithm.to_string(),
})
}
fn to_generic(&self) -> Result<AccountKey, Error> {
Ok(AccountKey {
creation_date: self.creation_date,
key: KeyPair::from_der(&self.key)?,
signature_algorithm: self.signature_algorithm.parse()?,
})
}
}
#[derive(Serialize, Deserialize, PartialEq, Debug)]
struct AccountEndpointStorage {
creation_date: SystemTime,
account_url: String,
order_url: String,
key_hash: Vec<u8>,
contacts_hash: Vec<u8>,
}
impl AccountEndpointStorage {
fn new(account_endpoint: &AccountEndpoint) -> Self {
AccountEndpointStorage {
creation_date: account_endpoint.creation_date,
account_url: account_endpoint.account_url.clone(),
order_url: account_endpoint.order_url.clone(),
key_hash: account_endpoint.key_hash.clone(),
contacts_hash: account_endpoint.contacts_hash.clone(),
}
}
fn to_generic(&self) -> AccountEndpoint {
AccountEndpoint {
creation_date: self.creation_date,
account_url: self.account_url.clone(),
order_url: self.order_url.clone(),
key_hash: self.key_hash.clone(),
contacts_hash: self.contacts_hash.clone(),
}
}
}
#[derive(Serialize, Deserialize, PartialEq, Debug)]
struct AccountStorage {
name: String,
endpoints: HashMap<String, AccountEndpointStorage>,
contacts: Vec<(String, String)>,
current_key: AccountKeyStorage,
past_keys: Vec<AccountKeyStorage>,
}
pub fn fetch(file_manager: &FileManager, name: &str) -> Result<Option<Account>, Error> {
if account_files_exists(file_manager) {
let data = get_account_data(file_manager)?;
let obj: AccountStorage = bincode::deserialize(&data[..])
.map_err(|e| Error::from(&e.to_string()).prefix(name))?;
let endpoints = obj
.endpoints
.iter()
.map(|(k, v)| (k.clone(), v.to_generic()))
.collect();
let contacts = obj
.contacts
.iter()
.map(|(t, v)| AccountContact::new(t, v))
.collect::<Result<Vec<AccountContact>, Error>>()?;
let current_key = obj.current_key.to_generic()?;
let past_keys = obj
.past_keys
.iter()
.map(|k| k.to_generic())
.collect::<Result<Vec<AccountKey>, Error>>()?;
Ok(Some(Account {
name: obj.name,
endpoints,
contacts,
current_key,
past_keys,
file_manager: file_manager.clone(),
}))
} else {
Ok(None)
}
}
pub fn save(file_manager: &FileManager, account: &Account) -> Result<(), Error> {
let endpoints: HashMap<String, AccountEndpointStorage> = account
.endpoints
.iter()
.map(|(k, v)| (k.to_owned(), AccountEndpointStorage::new(v)))
.collect();
let contacts: Vec<(String, String)> = account
.contacts
.iter()
.map(|c| (c.contact_type.to_string(), c.value.to_owned()))
.collect();
let past_keys = account
.past_keys
.iter()
.map(|k| AccountKeyStorage::new(&k))
.collect::<Result<Vec<AccountKeyStorage>, Error>>()?;
let account_storage = AccountStorage {
name: account.name.to_owned(),
endpoints,
contacts,
current_key: AccountKeyStorage::new(&account.current_key)?,
past_keys,
};
let encoded: Vec<u8> = bincode::serialize(&account_storage)
.map_err(|e| Error::from(&e.to_string()).prefix(&account.name))?;
set_account_data(file_manager, &encoded)
}

38
acmed/src/acme_proto.rs

@ -1,4 +1,4 @@
use crate::acme_proto::account::AccountManager;
use crate::account::Account;
use crate::acme_proto::structs::{
ApiError, Authorization, AuthorizationStatus, NewOrder, Order, OrderStatus,
};
@ -59,12 +59,12 @@ impl PartialEq<structs::Challenge> for Challenge {
}
macro_rules! set_data_builder {
($account: ident, $data: expr) => {
($account: ident, $endpoint_name: ident, $data: expr) => {
|n: &str, url: &str| {
encode_kid(
&$account.key_pair,
&$account.signature_algorithm,
&$account.account_url,
&$account.current_key.key,
&$account.current_key.signature_algorithm,
&($account.get_endpoint(&$endpoint_name)?.account_url),
$data,
url,
n,
@ -73,8 +73,8 @@ macro_rules! set_data_builder {
};
}
macro_rules! set_empty_data_builder {
($account: ident) => {
set_data_builder!($account, b"")
($account: ident, $endpoint_name: ident) => {
set_data_builder!($account, $endpoint_name, b"")
};
}
@ -82,19 +82,21 @@ pub fn request_certificate(
cert: &Certificate,
root_certs: &[String],
endpoint: &mut Endpoint,
account: &mut Account,
) -> Result<(), Error> {
let mut hook_datas = vec![];
let endpoint_name = endpoint.name.clone();
// Refresh the directory
http::refresh_directory(endpoint, root_certs)?;
// Get or create the account
let account = AccountManager::new(endpoint, root_certs, cert)?;
// Synchronize the account
account.synchronize(endpoint, root_certs)?;
// Create a new order
let new_order = NewOrder::new(&cert.identifiers);
let new_order = serde_json::to_string(&new_order)?;
let data_builder = set_data_builder!(account, new_order.as_bytes());
let data_builder = set_data_builder!(account, endpoint_name, new_order.as_bytes());
let (order, order_url) = http::new_order(endpoint, root_certs, &data_builder)?;
if let Some(e) = order.get_error() {
cert.warn(&e.prefix("Error").message);
@ -103,7 +105,7 @@ pub fn request_certificate(
// Begin iter over authorizations
for auth_url in order.authorizations.iter() {
// Fetch the authorization
let data_builder = set_empty_data_builder!(account);
let data_builder = set_empty_data_builder!(account, endpoint_name);
let auth = http::get_authorization(endpoint, root_certs, &data_builder, &auth_url)?;
if let Some(e) = auth.get_error() {
cert.warn(&e.prefix("Error").message);
@ -124,7 +126,7 @@ pub fn request_certificate(
let current_challenge = current_identifier.challenge;
for challenge in auth.challenges.iter() {
if current_challenge == *challenge {
let proof = challenge.get_proof(&account.key_pair)?;
let proof = challenge.get_proof(&account.current_key.key)?;
let file_name = challenge.get_file_name();
let identifier = auth.identifier.value.to_owned();
@ -135,14 +137,14 @@ pub fn request_certificate(
// Tell the server the challenge has been completed
let chall_url = challenge.get_url();
let data_builder = set_data_builder!(account, b"{}");
let data_builder = set_data_builder!(account, endpoint_name, b"{}");
let _ =
http::post_challenge_response(endpoint, root_certs, &data_builder, &chall_url)?;
}
}
// Pool the authorization in order to see whether or not it is valid
let data_builder = set_empty_data_builder!(account);
let data_builder = set_empty_data_builder!(account, endpoint_name);
let break_fn = |a: &Authorization| a.status == AuthorizationStatus::Valid;
let _ =
http::pool_authorization(endpoint, root_certs, &data_builder, &break_fn, &auth_url)?;
@ -154,7 +156,7 @@ pub fn request_certificate(
// End iter over authorizations
// Pool the order in order to see whether or not it is ready
let data_builder = set_empty_data_builder!(account);
let data_builder = set_empty_data_builder!(account, endpoint_name);
let break_fn = |o: &Order| o.status == OrderStatus::Ready;
let order = http::pool_order(endpoint, root_certs, &data_builder, &break_fn, &order_url)?;
@ -183,14 +185,14 @@ pub fn request_certificate(
"csr": csr.to_der_base64()?,
});
let csr = csr.to_string();
let data_builder = set_data_builder!(account, csr.as_bytes());
let data_builder = set_data_builder!(account, endpoint_name, csr.as_bytes());
let order = http::finalize_order(endpoint, root_certs, &data_builder, &order.finalize)?;
if let Some(e) = order.get_error() {
cert.warn(&e.prefix("Error").message);
}
// Pool the order in order to see whether or not it is valid
let data_builder = set_empty_data_builder!(account);
let data_builder = set_empty_data_builder!(account, endpoint_name);
let break_fn = |o: &Order| o.status == OrderStatus::Valid;
let order = http::pool_order(endpoint, root_certs, &data_builder, &break_fn, &order_url)?;
@ -198,7 +200,7 @@ pub fn request_certificate(
let crt_url = order
.certificate
.ok_or_else(|| Error::from("No certificate available for download."))?;
let data_builder = set_empty_data_builder!(account);
let data_builder = set_empty_data_builder!(account, endpoint_name);
let crt = http::get_certificate(endpoint, root_certs, &data_builder, &crt_url)?;
storage::write_certificate(&cert.file_manager, &crt.as_bytes())?;

91
acmed/src/acme_proto/account.rs

@ -1,71 +1,38 @@
use crate::account::Account as BaseAccount;
use crate::acme_proto::http;
use crate::acme_proto::structs::Account;
use crate::certificate::Certificate;
use crate::endpoint::Endpoint;
use crate::jws::encode_jwk;
use crate::logs::HasLogger;
use crate::storage;
use acme_common::crypto::{gen_keypair, JwsSignatureAlgorithm, KeyPair};
use acme_common::error::Error;
pub struct AccountManager {
pub key_pair: KeyPair,
pub signature_algorithm: JwsSignatureAlgorithm,
pub account_url: String,
pub orders_url: String,
}
impl AccountManager {
pub fn new(
endpoint: &mut Endpoint,
root_certs: &[String],
cert: &Certificate,
) -> Result<Self, Error> {
// TODO: store the key id (account url)
let key_pair = storage::get_account_keypair(&cert.file_manager)?;
let signature_algorithm = cert.account.signature_algorithm;
let kp_ref = &key_pair;
let account = Account::new(cert, endpoint);
let account = serde_json::to_string(&account)?;
let acc_ref = &account;
let data_builder = |n: &str, url: &str| {
encode_jwk(kp_ref, &signature_algorithm, acc_ref.as_bytes(), url, n)
};
let (acc_rep, account_url) = http::new_account(endpoint, root_certs, &data_builder)?;
let ac = AccountManager {
key_pair,
signature_algorithm,
account_url,
orders_url: acc_rep.orders.unwrap_or_default(),
};
// TODO: check account data and, if different from config, update them
Ok(ac)
}
}
pub fn init_account(cert: &Certificate) -> Result<(), Error> {
if !storage::account_files_exists(&cert.file_manager) {
cert.info(&format!(
"Account {} does not exists. Creating it.",
&cert.account.name
));
let key_pair = gen_keypair(cert.account.key_type)?;
storage::set_account_keypair(&cert.file_manager, &key_pair)?;
cert.debug(&format!("Account {} created.", &cert.account.name));
} else {
let key_pair = storage::get_account_keypair(&cert.file_manager)?;
if key_pair.key_type != cert.account.key_type {
cert.info(&format!("Account {name} has a key pair of type {kt_has} while {kt_want} was expected. Creating a new {kt_want} key pair.", name=&cert.account.name, kt_has=key_pair.key_type, kt_want=cert.account.key_type));
// TODO: Do a propper key rollover
let key_pair = gen_keypair(cert.account.key_type)?;
storage::set_account_keypair(&cert.file_manager, &key_pair)?;
cert.debug(&format!(
"Account {} updated with a new {} key pair.",
&cert.account.name, cert.account.key_type
));
} else {
cert.debug(&format!("Account {} already exists.", &cert.account.name));
}
}
pub fn register_account(
endpoint: &mut Endpoint,
root_certs: &[String],
account: &mut BaseAccount,
) -> Result<(), Error> {
account.debug(&format!(
"creating account on endpoint {}...",
&endpoint.name
));
let account_struct = Account::new(account, endpoint);
let account_struct = serde_json::to_string(&account_struct)?;
let acc_ref = &account_struct;
let kp_ref = &account.current_key.key;
let signature_algorithm = &account.current_key.signature_algorithm;
let data_builder =
|n: &str, url: &str| encode_jwk(kp_ref, signature_algorithm, acc_ref.as_bytes(), url, n);
let (acc_rep, account_url) = http::new_account(endpoint, root_certs, &data_builder)?;
account.set_account_url(&endpoint.name, &account_url)?;
let msg = format!(
"endpoint {}: account {}: the server has not provided an order URL upon account creation",
&endpoint.name, &account.name
);
let order_url = acc_rep.orders.ok_or_else(|| Error::from(&msg))?;
account.set_order_url(&endpoint.name, &order_url)?;
account.update_key_hash(&endpoint.name)?;
account.update_contacts_hash(&endpoint.name)?;
account.save()?;
account.info(&format!("account created on endpoint {}", &endpoint.name));
Ok(())
}

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

@ -1,4 +1,3 @@
use crate::certificate::Certificate;
use crate::endpoint::Endpoint;
use acme_common::error::Error;
use serde::{Deserialize, Serialize};
@ -13,9 +12,9 @@ pub struct Account {
}
impl Account {
pub fn new(cert: &Certificate, endpoint: &Endpoint) -> Self {
pub fn new(account: &crate::account::Account, endpoint: &Endpoint) -> Self {
Account {
contact: vec![format!("mailto:{}", cert.account.email)],
contact: account.contacts.iter().map(|e| e.to_string()).collect(),
terms_of_service_agreed: endpoint.tos_agreed,
only_return_existing: false,
}

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

@ -2,7 +2,7 @@ use acme_common::error::Error;
use serde::Deserialize;
use std::str::FromStr;
#[derive(Debug, Deserialize)]
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct DirectoryMeta {
pub terms_of_service: Option<String>,
@ -11,7 +11,7 @@ pub struct DirectoryMeta {
pub external_account_required: Option<bool>,
}
#[derive(Debug, Deserialize)]
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Directory {
pub meta: Option<DirectoryMeta>,

11
acmed/src/certificate.rs

@ -1,4 +1,3 @@
use crate::account::Account;
use crate::acme_proto::Challenge;
use crate::hooks::{self, ChallengeHookData, Hook, HookEnvData, HookType, PostOperationHookData};
use crate::identifier::{Identifier, IdentifierType};
@ -13,7 +12,7 @@ use std::time::Duration;
#[derive(Clone, Debug)]
pub struct Certificate {
pub account: Account,
pub account_name: String,
pub identifiers: Vec<Identifier>,
pub key_type: KeyType,
pub csr_digest: HashFunction,
@ -36,19 +35,19 @@ impl fmt::Display for Certificate {
impl HasLogger for Certificate {
fn warn(&self, msg: &str) {
warn!("{}: {}", &self, msg);
warn!("certificate \"{}\": {}", &self, msg);
}
fn info(&self, msg: &str) {
info!("{}: {}", &self, msg);
info!("certificate \"{}\": {}", &self, msg);
}
fn debug(&self, msg: &str) {
debug!("{}: {}", &self, msg);
debug!("certificate \"{}\": {}", &self, msg);
}
fn trace(&self, msg: &str) {
trace!("{}: {}", &self, msg);
trace!("certificate \"{}\": {}", &self, msg);
}
}

53
acmed/src/config.rs

@ -276,23 +276,56 @@ pub struct Group {
#[serde(deny_unknown_fields)]
pub struct Account {
pub name: String,
pub email: String,
pub contacts: Vec<AccountContact>,
pub key_type: Option<String>,
pub signature_algorithm: Option<String>,
pub hooks: Vec<String>,
#[serde(default)]
pub env: HashMap<String, String>,
}
impl Account {
pub fn get_hooks(&self, cnf: &Config) -> Result<Vec<hooks::Hook>, Error> {
let mut res = vec![];
for name in self.hooks.iter() {
let mut h = cnf.get_hook(&name)?;
res.append(&mut h);
}
Ok(res)
}
pub fn to_generic(&self, file_manager: &FileManager) -> Result<crate::account::Account, Error> {
crate::account::Account::new(
let contacts: Vec<(String, String)> = self
.contacts
.iter()
.map(|e| (e.get_type(), e.get_value()))
.collect();
crate::account::Account::load(
file_manager,
&self.name,
&self.email,
&contacts,
&self.key_type,
&self.signature_algorithm,
)
}
}
#[derive(Clone, Debug, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct AccountContact {
pub mailto: String,
}
impl AccountContact {
pub fn get_type(&self) -> String {
"mailto".to_string()
}
pub fn get_value(&self) -> String {
self.mailto.clone()
}
}
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Certificate {
@ -313,20 +346,6 @@ pub struct Certificate {
}
impl Certificate {
pub fn get_account(
&self,
cnf: &Config,
file_manager: &FileManager,
) -> Result<crate::account::Account, Error> {
for account in cnf.account.iter() {
if account.name == self.account {
let acc = account.to_generic(file_manager)?;
return Ok(acc);
}
}
Err(format!("{}: account not found", self.account).into())
}
pub fn get_key_type(&self) -> Result<KeyType, Error> {
match &self.key_type {
Some(a) => a.parse(),

2
acmed/src/endpoint.rs

@ -5,7 +5,7 @@ use std::cmp;
use std::thread;
use std::time::{Duration, Instant};
#[derive(Debug)]
#[derive(Clone, Debug)]
pub struct Endpoint {
pub name: String,
pub url: String,

77
acmed/src/main_event_loop.rs

@ -1,4 +1,4 @@
use crate::acme_proto::account::init_account;
use crate::account::Account;
use crate::acme_proto::request_certificate;
use crate::certificate::Certificate;
use crate::config;
@ -12,10 +12,16 @@ use std::sync::{Arc, RwLock};
use std::thread;
use std::time::Duration;
type AccountSync = Arc<RwLock<Account>>;
type EndpointSync = Arc<RwLock<Endpoint>>;
fn renew_certificate(crt: &Certificate, root_certs: &[String], endpoint: &mut Endpoint) {
let (status, is_success) = match request_certificate(crt, root_certs, endpoint) {
fn renew_certificate(
crt: &Certificate,
root_certs: &[String],
endpoint: &mut Endpoint,
account: &mut Account,
) {
let (status, is_success) = match request_certificate(crt, root_certs, endpoint, account) {
Ok(_) => ("Success.".to_string(), true),
Err(e) => {
let e = e.prefix("Unable to renew the certificate");
@ -35,6 +41,7 @@ fn renew_certificate(crt: &Certificate, root_certs: &[String], endpoint: &mut En
pub struct MainEventLoop {
certs: Vec<Certificate>,
root_certs: Vec<String>,
accounts: HashMap<String, AccountSync>,
endpoints: HashMap<String, EndpointSync>,
}
@ -61,6 +68,33 @@ impl MainEventLoop {
.into_iter()
.collect();
let mut accounts = HashMap::new();
for acc in cnf.account.iter() {
let fm = FileManager {
account_directory: cnf.get_account_dir(),
account_name: acc.name.clone(),
crt_name: String::new(),
crt_name_format: String::new(),
crt_directory: String::new(),
crt_key_type: String::new(),
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(),
hooks: acc
.get_hooks(&cnf)?
.iter()
.filter(|h| !h.hook_type.is_disjoint(&file_hooks))
.map(|e| e.to_owned())
.collect(),
env: acc.env.clone(),
};
let account = acc.to_generic(&fm)?;
accounts.insert(acc.name.clone(), account);
}
let mut certs = Vec::new();
let mut endpoints = HashMap::new();
for (i, crt) in cnf.certificate.iter().enumerate() {
@ -71,7 +105,7 @@ impl MainEventLoop {
let hooks = crt.get_hooks(&cnf)?;
let fm = FileManager {
account_directory: cnf.get_account_dir(),
account_name: crt.account.to_owned(),
account_name: crt.account.clone(),
crt_name: crt_name.clone(),
crt_name_format: crt.get_crt_name_format(),
crt_directory: crt.get_crt_dir(&cnf),
@ -90,7 +124,7 @@ impl MainEventLoop {
env: crt.env.clone(),
};
let cert = Certificate {
account: crt.get_account(&cnf, &fm)?,
account_name: crt.account.clone(),
identifiers: crt.get_identifiers()?,
key_type,
csr_digest: crt.get_csr_digest()?,
@ -107,17 +141,30 @@ impl MainEventLoop {
renew_delay: crt.get_renew_delay(&cnf)?,
file_manager: fm,
};
endpoints
.entry(endpoint_name)
.or_insert_with(|| Arc::new(RwLock::new(endpoint)));
init_account(&cert)?;
match accounts.get_mut(&crt.account) {
Some(acc) => acc.add_endpoint_name(&endpoint_name),
None => {
let msg = format!("{}: account not found.", &crt.account);
return Err(msg.into());
}
};
endpoints.entry(endpoint_name).or_insert(endpoint);
certs.push(cert);
}
// TODO: call .synchronize() on every account
Ok(MainEventLoop {
certs,
root_certs: root_certs.iter().map(|v| (*v).to_string()).collect(),
endpoints,
accounts: accounts
.iter()
.map(|(k, v)| (k.to_owned(), Arc::new(RwLock::new(v.to_owned()))))
.collect(),
endpoints: endpoints
.iter()
.map(|(k, v)| (k.to_owned(), Arc::new(RwLock::new(v.to_owned()))))
.collect(),
})
}
@ -146,12 +193,16 @@ impl MainEventLoop {
}
}
}
let lock = endpoint_lock.clone();
let mut accounts_lock = self.accounts.clone();
let ep_lock = endpoint_lock.clone();
let rc = self.root_certs.clone();
let handle = thread::spawn(move || {
let mut endpoint = lock.write().unwrap();
let mut endpoint = ep_lock.write().unwrap();
for crt in certs_to_renew {
renew_certificate(&crt, &rc, &mut endpoint);
if let Some(acc_lock) = accounts_lock.get_mut(&crt.account_name) {
let mut account = acc_lock.write().unwrap();
renew_certificate(&crt, &rc, &mut endpoint, &mut account);
};
}
});
handles.push(handle);

54
acmed/src/storage.rs

@ -32,26 +32,36 @@ pub struct FileManager {
impl HasLogger for FileManager {
fn warn(&self, msg: &str) {
log::warn!("{}: {}", &self.crt_name, msg);
log::warn!("{}: {}", self, msg);
}
fn info(&self, msg: &str) {
log::info!("{}: {}", &self.crt_name, msg);
log::info!("{}: {}", self, msg);
}
fn debug(&self, msg: &str) {
log::debug!("{}: {}", &self.crt_name, msg);
log::debug!("{}: {}", self, msg);
}
fn trace(&self, msg: &str) {
log::trace!("{}: {}", &self.crt_name, msg);
log::trace!("{}: {}", self, msg);
}
}
impl fmt::Display for FileManager {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let s = if !self.crt_name.is_empty() {
format!("certificate \"{}\"", self.crt_name)
} else {
format!("account \"{}\"", self.account_name)
};
write!(f, "{}", s)
}
}
#[derive(Clone)]
enum FileType {
AccountPrivateKey,
AccountPublicKey,
Account,
PrivateKey,
Certificate,
}
@ -59,8 +69,7 @@ enum FileType {
impl fmt::Display for FileType {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let s = match self {
FileType::AccountPrivateKey => "priv-key",
FileType::AccountPublicKey => "pub-key",
FileType::Account => "account",
FileType::PrivateKey => "pk",
FileType::Certificate => "crt",
};
@ -73,16 +82,16 @@ fn get_file_full_path(
file_type: FileType,
) -> Result<(String, String, PathBuf), Error> {
let base_path = match file_type {
FileType::AccountPrivateKey | FileType::AccountPublicKey => &fm.account_directory,
FileType::Account => &fm.account_directory,
FileType::PrivateKey => &fm.crt_directory,
FileType::Certificate => &fm.crt_directory,
};
let file_name = match file_type {
FileType::AccountPrivateKey | FileType::AccountPublicKey => format!(
FileType::Account => format!(
"{account}.{file_type}.{ext}",
account = b64_encode(&fm.account_name),
file_type = file_type.to_string(),
ext = "pem"
ext = "bin"
),
FileType::PrivateKey | FileType::Certificate => {
// TODO: use fm.crt_name_format instead of a string literal
@ -118,7 +127,7 @@ fn set_owner(fm: &FileManager, path: &PathBuf, file_type: FileType) -> Result<()
let (uid, gid) = match file_type {
FileType::Certificate => (fm.cert_file_owner.to_owned(), fm.cert_file_group.to_owned()),
FileType::PrivateKey => (fm.pk_file_owner.to_owned(), fm.pk_file_group.to_owned()),
FileType::AccountPrivateKey | FileType::AccountPublicKey => {
FileType::Account => {
// The account file does not need to be accessible to users other different from the current one.
return Ok(());
}
@ -190,8 +199,7 @@ fn write_file(fm: &FileManager, file_type: FileType, data: &[u8]) -> Result<(),
options.mode(match &file_type {
FileType::Certificate => fm.cert_file_mode,
FileType::PrivateKey => fm.pk_file_mode,
FileType::AccountPublicKey => crate::DEFAULT_ACCOUNT_FILE_MODE,
FileType::AccountPrivateKey => crate::DEFAULT_ACCOUNT_FILE_MODE,
FileType::Account => crate::DEFAULT_ACCOUNT_FILE_MODE,
});
options.write(true).create(true).open(&path)?
} else {
@ -210,19 +218,13 @@ fn write_file(fm: &FileManager, file_type: FileType, data: &[u8]) -> Result<(),
Ok(())
}
pub fn get_account_keypair(fm: &FileManager) -> Result<KeyPair, Error> {
let path = get_file_path(fm, FileType::AccountPrivateKey)?;
let raw_key = read_file(fm, &path)?;
let key = KeyPair::from_pem(&raw_key)?;
Ok(key)
pub fn get_account_data(fm: &FileManager) -> Result<Vec<u8>, Error> {
let path = get_file_path(fm, FileType::Account)?;
read_file(fm, &path)
}
pub fn set_account_keypair(fm: &FileManager, key_pair: &KeyPair) -> Result<(), Error> {
let pem_pub_key = key_pair.private_key_to_pem()?;
let pem_priv_key = key_pair.public_key_to_pem()?;
write_file(fm, FileType::AccountPublicKey, &pem_priv_key)?;
write_file(fm, FileType::AccountPrivateKey, &pem_pub_key)?;
Ok(())
pub fn set_account_data(fm: &FileManager, data: &[u8]) -> Result<(), Error> {
write_file(fm, FileType::Account, data)
}
pub fn get_keypair(fm: &FileManager) -> Result<KeyPair, Error> {
@ -268,7 +270,7 @@ fn check_files(fm: &FileManager, file_types: &[FileType]) -> bool {
}
pub fn account_files_exists(fm: &FileManager) -> bool {
let file_types = vec![FileType::AccountPrivateKey, FileType::AccountPublicKey];
let file_types = vec![FileType::Account];
check_files(fm, &file_types)
}

14
man/en/acmed.toml.5

@ -163,8 +163,14 @@ Array of table representing an account on one or several endpoint.
.Bl -tag
.It Ic name Ar string
The name the account is registered under. Must be unique.
.It Ic email Ar string
The email address used to contact the account's holder.
.It Ic contacts Ar array
Array of tables describing describing the account holder's contact information. Each table must have one and only one key-value pair. Possible keys and their associated values are:
.Bl -tag
.It Ic mailto Ar string
A mailto URI as defined by
.Em RFC 6068 .
This URI cannot contains neither "hfields" nor more than one "addr-spec" in the "to" component.
.El
.It Cm key_type Ar string
Name of the asymmetric cryptography algorithm used to generate the key pair. Possible values are :
.Bl -dash -compact
@ -190,6 +196,10 @@ ES256
.It
ES384
.El
.It Ic env Ar table
Table of environment variables that will be accessible from hooks.
.It Ic hooks Ar array
Names of hooks that will be called during operations on the account storage file. The hooks are guaranteed to be called sequentially in the declaration order.
.El
.It Ic certificate
Array of table representing a certificate that will be requested to a CA.

Loading…
Cancel
Save