diff --git a/CHANGELOG.md b/CHANGELOG.md index 740c40a..2a20f25 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added +- The account key type and signature algorithm can now be specified in the configuration. + ### Fixed - The Makefile now works on FreeBSD. It should also work on other BSD although it has not been tested. diff --git a/README.md b/README.md index 0ad9bef..04e5f85 100644 --- a/README.md +++ b/README.md @@ -117,7 +117,7 @@ An example service file is provided (see `contrib/acmed.service.example`). The f It depends on your definition of a beginner. This software is intended to be used by system administrator with a certain knowledge of their environment. Furthermore, it is also expected to know the bases of the ACME protocol. Let's Encrypt wrote a nice article about [how it works](https://letsencrypt.org/how-it-works/). -### Why is RSA 2048 the default? +### Why is RSA 2048 the default certificate key type? Short answer: it is sufficiently secured, has good performances and is wildly supported. @@ -128,3 +128,7 @@ Before choosing a different algorithm for your certificate's signature, you migh * Nowadays, every client support ECDSA support. So, unless you have very specific requirements, you can safely use it. At time of writing, EdDSA certificates are not yet supported, but it might become a thing in the future. Currently, security and client support aren't the main concerns since every possible type of certificates is good enough on those two points. The performances clearly favors ECDSA P-256, Ed25519 and RSA 2048. The later has been chosen as the default because it's the most wildly used as Certification Authorities root and intermediate certificates. This choice could change in favor of ECDSA once Let's Encrypt issues [a full ECDSA certificates chain](https://community.letsencrypt.org/t/lets-encrypt-new-hierarchy-plans/125517). + +### Why is ECDSA P-256 the default account key type? + +RFC 8555 section 6.2 defines ECDSA P-256 as the only account key type that any ACME servers must implement. It is therefore the best choice for the default value. diff --git a/acme_common/src/crypto/key_type.rs b/acme_common/src/crypto/key_type.rs index 84696ec..6e3a987 100644 --- a/acme_common/src/crypto/key_type.rs +++ b/acme_common/src/crypto/key_type.rs @@ -3,7 +3,7 @@ use crate::error::Error; use std::fmt; use std::str::FromStr; -#[derive(Clone, Copy, Debug)] +#[derive(Clone, Copy, Debug, PartialEq)] pub enum KeyType { Rsa2048, Rsa4096, diff --git a/acmed/src/acme_proto.rs b/acmed/src/acme_proto.rs index cd680af..349aff9 100644 --- a/acmed/src/acme_proto.rs +++ b/acmed/src/acme_proto.rs @@ -58,7 +58,16 @@ impl PartialEq for Challenge { macro_rules! set_data_builder { ($account: ident, $data: expr) => { - |n: &str, url: &str| encode_kid(&$account.key_pair, &$account.account_url, $data, url, n) + |n: &str, url: &str| { + encode_kid( + &$account.key_pair, + &$account.signature_algorithm, + &$account.account_url, + $data, + url, + n, + ) + } }; } macro_rules! set_empty_data_builder { diff --git a/acmed/src/acme_proto/account.rs b/acmed/src/acme_proto/account.rs index d68d23e..2e955fd 100644 --- a/acmed/src/acme_proto/account.rs +++ b/acmed/src/acme_proto/account.rs @@ -4,11 +4,12 @@ use crate::certificate::Certificate; use crate::endpoint::Endpoint; use crate::jws::encode_jwk; use crate::storage; -use acme_common::crypto::{gen_keypair, KeyPair}; +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, } @@ -21,14 +22,18 @@ impl AccountManager { ) -> Result { // TODO: store the key id (account url) let key_pair = storage::get_account_keypair(cert)?; + let signature_algorithm = endpoint.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, acc_ref.as_bytes(), url, n); + 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(), }; @@ -37,16 +42,21 @@ impl AccountManager { } } -pub fn init_account(cert: &Certificate) -> Result<(), Error> { +pub fn init_account(cert: &Certificate, endpoint: &Endpoint) -> Result<(), Error> { if !storage::account_files_exists(cert) { - // TODO: allow to change the account key type - let key_pair = gen_keypair(crate::DEFAULT_ACCOUNT_KEY_TYPE)?; + let key_pair = gen_keypair(endpoint.key_type)?; storage::set_account_keypair(cert, &key_pair)?; cert.info(&format!("Account {} created", &cert.account.name)); } else { - // TODO: check if the keys are suitable for the specified signature algorithm - // and, if not, initiate a key rollover. - cert.debug(&format!("Account {} already exists", &cert.account.name)); + let key_pair = storage::get_account_keypair(cert)?; + if key_pair.key_type != endpoint.key_type { + cert.debug(&format!("Account {} has a key pair of type {} while {} was expected. Creating a new {} key pair.", &cert.account.name, key_pair.key_type, endpoint.key_type, endpoint.key_type)); + // TODO: Do a propper key rollover? + let key_pair = gen_keypair(endpoint.key_type)?; + storage::set_account_keypair(cert, &key_pair)?; + } else { + cert.debug(&format!("Account {} already exists", &cert.account.name)); + } } Ok(()) } diff --git a/acmed/src/config.rs b/acmed/src/config.rs index fa6bd4e..245cec9 100644 --- a/acmed/src/config.rs +++ b/acmed/src/config.rs @@ -181,6 +181,8 @@ pub struct Endpoint { pub tos_agreed: bool, #[serde(default)] pub rate_limits: Vec, + pub key_type: Option, + pub signature_algorithm: Option, } impl Endpoint { @@ -190,7 +192,14 @@ impl Endpoint { let (nb, timeframe) = cnf.get_rate_limit(&rl_name)?; limits.push((nb, timeframe)); } - crate::endpoint::Endpoint::new(&self.name, &self.url, self.tos_agreed, &limits) + crate::endpoint::Endpoint::new( + &self.name, + &self.url, + self.tos_agreed, + &limits, + &self.key_type, + &self.signature_algorithm, + ) } } diff --git a/acmed/src/endpoint.rs b/acmed/src/endpoint.rs index 89e5ea6..dc8fe9c 100644 --- a/acmed/src/endpoint.rs +++ b/acmed/src/endpoint.rs @@ -1,4 +1,5 @@ use crate::acme_proto::structs::Directory; +use acme_common::crypto::{JwsSignatureAlgorithm, KeyType}; use acme_common::error::Error; use nom::bytes::complete::take_while_m_n; use nom::character::complete::digit1; @@ -6,6 +7,7 @@ use nom::combinator::map_res; use nom::multi::fold_many1; use nom::IResult; use std::cmp; +use std::str::FromStr; use std::thread; use std::time::{Duration, Instant}; @@ -17,6 +19,8 @@ pub struct Endpoint { pub nonce: Option, pub rl: RateLimit, pub dir: Directory, + pub key_type: KeyType, + pub signature_algorithm: JwsSignatureAlgorithm, } impl Endpoint { @@ -25,8 +29,19 @@ impl Endpoint { url: &str, tos_agreed: bool, limits: &[(usize, String)], + key_type: &Option, + signature_algorithm: &Option, ) -> Result { let rl = RateLimit::new(limits)?; + let key_type = match key_type { + Some(kt) => KeyType::from_str(&kt)?, + None => crate::DEFAULT_ACCOUNT_KEY_TYPE, + }; + let signature_algorithm = match signature_algorithm { + Some(sa) => JwsSignatureAlgorithm::from_str(&sa)?, + None => key_type.get_default_signature_alg(), + }; + let _ = key_type.check_alg_compatibility(&signature_algorithm)?; Ok(Self { name: name.to_string(), url: url.to_string(), @@ -42,6 +57,8 @@ impl Endpoint { revoke_cert: String::new(), key_change: String::new(), }, + key_type, + signature_algorithm, }) } } diff --git a/acmed/src/jws.rs b/acmed/src/jws.rs index 0221126..1f1b376 100644 --- a/acmed/src/jws.rs +++ b/acmed/src/jws.rs @@ -49,12 +49,11 @@ fn get_data( pub fn encode_jwk( key_pair: &KeyPair, + sign_alg: &JwsSignatureAlgorithm, payload: &[u8], url: &str, nonce: &str, ) -> Result { - // TODO: allow to change the signature algo - let sign_alg = key_pair.key_type.get_default_signature_alg(); let protected = JwsProtectedHeaderJwk { alg: sign_alg.to_string(), jwk: key_pair.jwk_public_key()?, @@ -62,18 +61,17 @@ pub fn encode_jwk( url: url.into(), }; let protected = serde_json::to_string(&protected)?; - get_data(key_pair, &sign_alg, &protected, payload) + get_data(key_pair, sign_alg, &protected, payload) } pub fn encode_kid( key_pair: &KeyPair, + sign_alg: &JwsSignatureAlgorithm, key_id: &str, payload: &[u8], url: &str, nonce: &str, ) -> Result { - // TODO: allow to change the signature algo - let sign_alg = key_pair.key_type.get_default_signature_alg(); let protected = JwsProtectedHeaderKid { alg: sign_alg.to_string(), kid: key_id.to_string(), @@ -81,7 +79,7 @@ pub fn encode_kid( url: url.into(), }; let protected = serde_json::to_string(&protected)?; - get_data(key_pair, &sign_alg, &protected, payload) + get_data(key_pair, sign_alg, &protected, payload) } #[cfg(test)] diff --git a/acmed/src/main_event_loop.rs b/acmed/src/main_event_loop.rs index b4ddb00..2f9e794 100644 --- a/acmed/src/main_event_loop.rs +++ b/acmed/src/main_event_loop.rs @@ -64,10 +64,10 @@ impl MainEventLoop { env: crt.env.to_owned(), id: i + 1, }; + init_account(&cert, &endpoint)?; endpoints .entry(endpoint_name) .or_insert_with(|| Arc::new(RwLock::new(endpoint))); - init_account(&cert)?; certs.push(cert); } diff --git a/man/en/acmed.toml.5 b/man/en/acmed.toml.5 index 0955f8b..d506af8 100644 --- a/man/en/acmed.toml.5 +++ b/man/en/acmed.toml.5 @@ -89,6 +89,29 @@ Set whether or not the user agrees to the Terms Of Service .Pq TOS . .It Cm url Ar string The endpoint's directory URL. +.It Cm key_type Ar string +Name of the asymmetric cryptography algorithm used to generate the client account's key pair. Possible values are : +.Bl -dash -compact +.It +rsa2048 +.It +rsa4096 +.It +ecdsa_p256 +.Aq default +.It +ecdsa_p384 +.El +.It Cm signature_algorithm Ar string +Name of the signature algorithm used to sign the messages sent to the endpoint. The default value is derived from the key type. Possible values are: +.Bl -dash -compact +.It +RS256 +.It +ES256 +.It +ES384 +.El .El .It Ic hook Array of table where each element defines a command that will be launched at a defined point. See section @@ -186,7 +209,7 @@ The domain name. Table of environment variables that will be accessible from hooks. .El .It Ic algorithm Ar string -Name of the asymetric cryptography algorithm used to generate the certificate's key pair. Possible values are : +Name of the asymmetric cryptography algorithm used to generate the certificate's key pair. Possible values are : .Bl -dash -compact .It rsa2048 @@ -628,6 +651,13 @@ status: {{status}}""" .%T Handlebars .%U https://handlebarsjs.com/ .Re +.It +.Rs +.%A M. Jones +.%D May 2015 +.%R RFC 7518 +.%T JSON Web Algorithms (JWA) +.Re .El .Sh AUTHORS .An Rodolphe Bréard