From 8c116f0b55b4df8a80e41e98d9c7fd4ce7ec02cc Mon Sep 17 00:00:00 2001 From: Rodolphe Breard Date: Sat, 12 Sep 2020 19:13:02 +0200 Subject: [PATCH] Add external account binding --- CHANGELOG.md | 1 + README.md | 7 ++-- acmed/src/account.rs | 10 ++++++ acmed/src/account/storage.rs | 38 +++++++++++++++++++++- acmed/src/acme_proto/account.rs | 2 +- acmed/src/acme_proto/structs/account.rs | 31 +++++++++++++++--- acmed/src/config.rs | 43 ++++++++++++++++++++++++- acmed/src/main.rs | 5 ++- man/en/acmed.toml.5 | 21 ++++++++++++ 9 files changed, 147 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c466a08..d693ea3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - The `contacts` account configuration field has been added. +- External account binding. ### Changed - The `email` account configuration field has been removed. In replacement, use the `contacts` field. diff --git a/README.md b/README.md index 9f4be23..5278b59 100644 --- a/README.md +++ b/README.md @@ -28,10 +28,11 @@ The Automatic Certificate Management Environment (ACME), is an internet standard - A pre-built set of hooks that can be used in most circumstances - Run as a deamon: no need to set-up timers, crontab or other time-triggered process - Retry of HTTPS request rejected with a badNonce or other recoverable errors -- Customizable HTTPS requests rate limits. +- Customizable HTTPS requests rate limits +- External account binding - Optional key pair reuse (useful for [HPKP](https://en.wikipedia.org/wiki/HTTP_Public_Key_Pinning)) -- For a given certificate, each domain name may be validated using a different challenge. -- A standalone server dedicated to the tls-alpn-01 challenge validation (tacd). +- For a given certificate, each domain name may be validated using a different challenge +- A standalone server dedicated to the tls-alpn-01 challenge validation (tacd) ## Planned features diff --git a/acmed/src/account.rs b/acmed/src/account.rs index eaf71a7..3ab5438 100644 --- a/acmed/src/account.rs +++ b/acmed/src/account.rs @@ -14,6 +14,13 @@ use std::time::SystemTime; mod contact; mod storage; +#[derive(Clone, Debug)] +pub struct ExternalAccount { + pub identifier: String, + pub key: Vec, + pub signature_algorithm: JwsSignatureAlgorithm, +} + #[derive(Clone, Debug)] pub enum AccountContactType { Mailfrom, @@ -85,6 +92,7 @@ pub struct Account { pub current_key: AccountKey, pub past_keys: Vec, pub file_manager: FileManager, + pub external_account: Option, } impl HasLogger for Account { @@ -149,6 +157,7 @@ impl Account { contacts: &[(String, String)], key_type: &Option, signature_algorithm: &Option, + external_account: &Option, ) -> Result { let contacts = contacts .iter() @@ -177,6 +186,7 @@ impl Account { current_key: AccountKey::new(key_type, signature_algorithm)?, past_keys: Vec::new(), file_manager: file_manager.clone(), + external_account: external_account.to_owned(), }; account.debug("initializing a new account"); account diff --git a/acmed/src/account/storage.rs b/acmed/src/account/storage.rs index cafd59e..904ec60 100644 --- a/acmed/src/account/storage.rs +++ b/acmed/src/account/storage.rs @@ -1,5 +1,5 @@ use crate::account::contact::AccountContact; -use crate::account::{Account, AccountEndpoint, AccountKey}; +use crate::account::{Account, AccountEndpoint, AccountKey, ExternalAccount}; use crate::storage::{account_files_exists, get_account_data, set_account_data, FileManager}; use acme_common::crypto::KeyPair; use acme_common::error::Error; @@ -7,6 +7,31 @@ use serde::{Deserialize, Serialize}; use std::collections::HashMap; use std::time::SystemTime; +#[derive(Serialize, Deserialize, PartialEq, Debug)] +pub struct ExternalAccountStorage { + pub identifier: String, + pub key: Vec, + pub signature_algorithm: String, +} + +impl ExternalAccountStorage { + fn new(external_account: &ExternalAccount) -> Self { + ExternalAccountStorage { + identifier: external_account.identifier.to_owned(), + key: external_account.key.to_owned(), + signature_algorithm: external_account.signature_algorithm.to_string(), + } + } + + fn to_generic(&self) -> Result { + Ok(ExternalAccount { + identifier: self.identifier.to_owned(), + key: self.key.to_owned(), + signature_algorithm: self.signature_algorithm.parse()?, + }) + } +} + #[derive(Serialize, Deserialize, PartialEq, Debug)] struct AccountKeyStorage { creation_date: SystemTime, @@ -70,6 +95,7 @@ struct AccountStorage { contacts: Vec<(String, String)>, current_key: AccountKeyStorage, past_keys: Vec, + external_account: Option, } pub fn fetch(file_manager: &FileManager, name: &str) -> Result, Error> { @@ -93,6 +119,10 @@ pub fn fetch(file_manager: &FileManager, name: &str) -> Result, .iter() .map(|k| k.to_generic()) .collect::, Error>>()?; + let external_account = match obj.external_account { + Some(a) => Some(a.to_generic()?), + None => None, + }; Ok(Some(Account { name: obj.name, endpoints, @@ -100,6 +130,7 @@ pub fn fetch(file_manager: &FileManager, name: &str) -> Result, current_key, past_keys, file_manager: file_manager.clone(), + external_account, })) } else { Ok(None) @@ -122,12 +153,17 @@ pub fn save(file_manager: &FileManager, account: &Account) -> Result<(), Error> .iter() .map(|k| AccountKeyStorage::new(&k)) .collect::, Error>>()?; + let external_account = match &account.external_account { + Some(a) => Some(ExternalAccountStorage::new(&a)), + None => None, + }; let account_storage = AccountStorage { name: account.name.to_owned(), endpoints, contacts, current_key: AccountKeyStorage::new(&account.current_key)?, past_keys, + external_account, }; let encoded: Vec = bincode::serialize(&account_storage) .map_err(|e| Error::from(&e.to_string()).prefix(&account.name))?; diff --git a/acmed/src/acme_proto/account.rs b/acmed/src/acme_proto/account.rs index e5e9754..f0d32d5 100644 --- a/acmed/src/acme_proto/account.rs +++ b/acmed/src/acme_proto/account.rs @@ -39,7 +39,7 @@ pub fn register_account( "creating account on endpoint \"{}\"...", &endpoint.name )); - let account_struct = Account::new(account, endpoint); + 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; diff --git a/acmed/src/acme_proto/structs/account.rs b/acmed/src/acme_proto/structs/account.rs index fb8dd3e..9709296 100644 --- a/acmed/src/acme_proto/structs/account.rs +++ b/acmed/src/acme_proto/structs/account.rs @@ -1,4 +1,5 @@ use crate::endpoint::Endpoint; +use crate::jws::encode_kid_mac; use acme_common::crypto::KeyPair; use acme_common::error::Error; use serde::{Deserialize, Serialize}; @@ -11,15 +12,37 @@ pub struct Account { pub contact: Vec, pub terms_of_service_agreed: bool, pub only_return_existing: bool, + #[serde(skip_serializing_if = "Option::is_none")] + pub external_account_binding: Option, } impl Account { - pub fn new(account: &crate::account::Account, endpoint: &Endpoint) -> Self { - Account { + pub fn new(account: &crate::account::Account, endpoint: &Endpoint) -> Result { + let external_account_binding = match &account.external_account { + Some(a) => { + let k_ref = &a.key; + let signature_algorithm = &a.signature_algorithm; + let kid = &a.identifier; + let payload = account.current_key.key.jwk_public_key()?; + let payload = serde_json::to_string(&payload)?; + let data = encode_kid_mac( + k_ref, + signature_algorithm, + kid, + payload.as_bytes(), + &endpoint.dir.new_account, + )?; + let data: Value = serde_json::from_str(&data)?; + Some(data) + } + None => None, + }; + Ok(Account { contact: account.contacts.iter().map(|e| e.to_string()).collect(), terms_of_service_agreed: endpoint.tos_agreed, only_return_existing: false, - } + external_account_binding, + }) } } @@ -29,7 +52,7 @@ pub struct AccountResponse { pub status: String, pub contact: Option>, pub terms_of_service_agreed: Option, - pub external_account_binding: Option, + pub external_account_binding: Option, pub orders: Option, } diff --git a/acmed/src/config.rs b/acmed/src/config.rs index f7c26a7..75c228d 100644 --- a/acmed/src/config.rs +++ b/acmed/src/config.rs @@ -2,7 +2,8 @@ use crate::duration::parse_duration; use crate::hooks; use crate::identifier::IdentifierType; use crate::storage::FileManager; -use acme_common::crypto::{HashFunction, KeyType}; +use acme_common::b64_decode; +use acme_common::crypto::{HashFunction, JwsSignatureAlgorithm, KeyType}; use acme_common::error::Error; use glob::glob; use log::info; @@ -272,6 +273,40 @@ pub struct Group { pub hooks: Vec, } +#[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct ExternalAccount { + pub identifier: String, + pub key: String, + pub signature_algorithm: Option, +} + +impl ExternalAccount { + pub fn to_generic(&self) -> Result { + let signature_algorithm = match &self.signature_algorithm { + Some(a) => a.parse()?, + None => crate::DEFAULT_EXTERNAL_ACCOUNT_JWA, + }; + match signature_algorithm { + JwsSignatureAlgorithm::Hs256 + | JwsSignatureAlgorithm::Hs384 + | JwsSignatureAlgorithm::Hs512 => {} + _ => { + return Err(format!( + "{}: invalid signature algorithm for external account binding", + signature_algorithm + ) + .into()); + } + }; + Ok(crate::account::ExternalAccount { + identifier: self.identifier.to_owned(), + key: b64_decode(&self.key)?, + signature_algorithm, + }) + } +} + #[derive(Clone, Debug, Deserialize)] #[serde(deny_unknown_fields)] pub struct Account { @@ -280,6 +315,7 @@ pub struct Account { pub key_type: Option, pub signature_algorithm: Option, pub hooks: Option>, + pub external_account: Option, #[serde(default)] pub env: HashMap, } @@ -306,12 +342,17 @@ impl Account { .iter() .map(|e| (e.get_type(), e.get_value())) .collect(); + let external_account = match &self.external_account { + Some(a) => Some(a.to_generic()?), + None => None, + }; crate::account::Account::load( file_manager, &self.name, &contacts, &self.key_type, &self.signature_algorithm, + &external_account, ) } } diff --git a/acmed/src/main.rs b/acmed/src/main.rs index 5e7b81b..12407fc 100644 --- a/acmed/src/main.rs +++ b/acmed/src/main.rs @@ -1,5 +1,7 @@ use crate::main_event_loop::MainEventLoop; -use acme_common::crypto::{HashFunction, KeyType, TLS_LIB_NAME, TLS_LIB_VERSION}; +use acme_common::crypto::{ + HashFunction, JwsSignatureAlgorithm, KeyType, TLS_LIB_NAME, TLS_LIB_VERSION, +}; use acme_common::logs::{set_log_system, DEFAULT_LOG_LEVEL}; use acme_common::{clean_pid_file, init_server}; use clap::{App, Arg}; @@ -36,6 +38,7 @@ pub const DEFAULT_PK_FILE_MODE: u32 = 0o600; pub const DEFAULT_ACCOUNT_FILE_MODE: u32 = 0o600; pub const DEFAULT_KP_REUSE: bool = false; pub const DEFAULT_ACCOUNT_KEY_TYPE: KeyType = KeyType::EcdsaP256; +pub const DEFAULT_EXTERNAL_ACCOUNT_JWA: JwsSignatureAlgorithm = JwsSignatureAlgorithm::Hs256; pub const DEFAULT_POOL_NB_TRIES: usize = 20; pub const DEFAULT_POOL_WAIT_SEC: u64 = 5; pub const DEFAULT_HTTP_FAIL_NB_RETRY: usize = 10; diff --git a/man/en/acmed.toml.5 b/man/en/acmed.toml.5 index 3d9c20f..cadd97b 100644 --- a/man/en/acmed.toml.5 +++ b/man/en/acmed.toml.5 @@ -200,6 +200,27 @@ ES384 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. +.It Ic external_account Ar table +Table containing the information required to bind the account to an external one. Possible fields and values are: +.Bl -tag +.It Ic identifier Ar string +ASCII string identifying the key. +.It Ic key Ar string +Private key encoded in base64url without padding. +.It Ic signature_algorithm Ar string +Name of the signature algorithm used to sign the external account binding message sent to the endpoint as defined in +.Em RFC 7518 . +Possible values are: +.Bl -dash -compact +.It +HS256 +.Aq default +.It +HS384 +.It +HS512 +.El +.El .El .It Ic certificate Array of table representing a certificate that will be requested to a CA.