diff --git a/CHANGELOG.md b/CHANGELOG.md index 3f37f4c..c466a08 100644 --- a/CHANGELOG.md +++ b/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 diff --git a/acme_common/src/crypto/openssl_keys.rs b/acme_common/src/crypto/openssl_keys.rs index e261a21..6c5b314 100644 --- a/acme_common/src/crypto/openssl_keys.rs +++ b/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, } impl KeyPair { + pub fn from_der(der_data: &[u8]) -> Result { + 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 { 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, Error> { + self.inner_key.private_key_to_der().map_err(Error::from) + } + pub fn private_key_to_pem(&self) -> Result, Error> { self.inner_key .private_key_to_pem_pkcs8() diff --git a/acmed/Cargo.toml b/acmed/Cargo.toml index 12df527..57e1dc0 100644 --- a/acmed/Cargo.toml +++ b/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" diff --git a/acmed/src/account.rs b/acmed/src/account.rs index 93ff8bd..3118506 100644 --- a/acmed/src/account.rs +++ b/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 { + 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 { + 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, + pub contacts_hash: Vec, +} + +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, + pub contacts: Vec, + pub current_key: AccountKey, + pub past_keys: Vec, + 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, signature_algorithm: &Option, ) -> Result { + let contacts = contacts + .iter() + .map(|(k, v)| contact::AccountContact::new(k, v)) + .collect::, 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 { + let msg = contacts + .iter() + .map(|v| v.to_string()) + .collect::>() + .join("") + .into_bytes(); + HashFunction::Sha256.hash(&msg) +} + +fn hash_key(key: &AccountKey) -> Result, 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)) } diff --git a/acmed/src/account/contact.rs b/acmed/src/account/contact.rs new file mode 100644 index 0000000..e72bdcb --- /dev/null +++ b/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 { + // 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 { + match self { + ContactType::Mailto => clean_mailto(value), + } + } +} + +impl FromStr for ContactType { + type Err = Error; + + fn from_str(s: &str) -> Result { + 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 { + 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)); + } +} diff --git a/acmed/src/account/storage.rs b/acmed/src/account/storage.rs new file mode 100644 index 0000000..cafd59e --- /dev/null +++ b/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, + signature_algorithm: String, +} + +impl AccountKeyStorage { + fn new(key: &AccountKey) -> Result { + 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 { + 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, + contacts_hash: Vec, +} + +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, + contacts: Vec<(String, String)>, + current_key: AccountKeyStorage, + past_keys: Vec, +} + +pub fn fetch(file_manager: &FileManager, name: &str) -> Result, 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::, Error>>()?; + let current_key = obj.current_key.to_generic()?; + let past_keys = obj + .past_keys + .iter() + .map(|k| k.to_generic()) + .collect::, 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 = 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::, Error>>()?; + let account_storage = AccountStorage { + name: account.name.to_owned(), + endpoints, + contacts, + current_key: AccountKeyStorage::new(&account.current_key)?, + past_keys, + }; + let encoded: Vec = bincode::serialize(&account_storage) + .map_err(|e| Error::from(&e.to_string()).prefix(&account.name))?; + set_account_data(file_manager, &encoded) +} diff --git a/acmed/src/acme_proto.rs b/acmed/src/acme_proto.rs index 2e9db53..a46ace8 100644 --- a/acmed/src/acme_proto.rs +++ b/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 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())?; diff --git a/acmed/src/acme_proto/account.rs b/acmed/src/acme_proto/account.rs index 5eddb27..dabda80 100644 --- a/acmed/src/acme_proto/account.rs +++ b/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 { - // 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(()) } diff --git a/acmed/src/acme_proto/structs/account.rs b/acmed/src/acme_proto/structs/account.rs index a869177..6fe7b54 100644 --- a/acmed/src/acme_proto/structs/account.rs +++ b/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, } diff --git a/acmed/src/acme_proto/structs/directory.rs b/acmed/src/acme_proto/structs/directory.rs index 42e5198..44b81be 100644 --- a/acmed/src/acme_proto/structs/directory.rs +++ b/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, @@ -11,7 +11,7 @@ pub struct DirectoryMeta { pub external_account_required: Option, } -#[derive(Debug, Deserialize)] +#[derive(Clone, Debug, Deserialize)] #[serde(rename_all = "camelCase")] pub struct Directory { pub meta: Option, diff --git a/acmed/src/certificate.rs b/acmed/src/certificate.rs index b41812a..c06297b 100644 --- a/acmed/src/certificate.rs +++ b/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, 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); } } diff --git a/acmed/src/config.rs b/acmed/src/config.rs index abaf4aa..05c6a32 100644 --- a/acmed/src/config.rs +++ b/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, pub key_type: Option, pub signature_algorithm: Option, + pub hooks: Vec, + #[serde(default)] + pub env: HashMap, } impl Account { + pub fn get_hooks(&self, cnf: &Config) -> Result, 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::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 { - 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 { match &self.key_type { Some(a) => a.parse(), diff --git a/acmed/src/endpoint.rs b/acmed/src/endpoint.rs index db7c239..74a462f 100644 --- a/acmed/src/endpoint.rs +++ b/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, diff --git a/acmed/src/main_event_loop.rs b/acmed/src/main_event_loop.rs index eab3e7a..895a4db 100644 --- a/acmed/src/main_event_loop.rs +++ b/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>; type EndpointSync = Arc>; -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, root_certs: Vec, + accounts: HashMap, endpoints: HashMap, } @@ -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); diff --git a/acmed/src/storage.rs b/acmed/src/storage.rs index 3d9512d..d52f162 100644 --- a/acmed/src/storage.rs +++ b/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 { - 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, 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 { @@ -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) } diff --git a/man/en/acmed.toml.5 b/man/en/acmed.toml.5 index d0daa52..3d9c20f 100644 --- a/man/en/acmed.toml.5 +++ b/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.