From b3cbeaab4af6e74f952e7cebc4fde9ba1b3c15ff Mon Sep 17 00:00:00 2001 From: Rodolphe Breard Date: Tue, 19 Mar 2019 21:37:25 +0100 Subject: [PATCH] Allow to reuse a key pair instead of creating a new one at each renewal The default behavior of most ACME clients is to generate a new key pair at each renewal. While this choice is respectable and perfectly justified in most configuration, it is also quite incompatible with the use of HTTP Public Key Pinning (HPKP). Although HPKP is not wildly supported and sometimes deprecated, users wishing to use it should not be blocked. https://tools.ietf.org/html/rfc7469 https://en.wikipedia.org/wiki/HTTP_Public_Key_Pinning --- CHANGELOG.md | 11 +++++++++++ acmed/Cargo.toml | 1 + acmed/acmed_example.toml | 1 + acmed/src/acmed.rs | 32 ++++++++++++++++++++++++++------ acmed/src/config.rs | 8 ++++++++ acmed/src/encoding.rs | 14 ++------------ acmed/src/errors.rs | 6 ++++++ acmed/src/main.rs | 1 + acmed/src/storage.rs | 4 ++++ 9 files changed, 60 insertions(+), 18 deletions(-) create mode 100644 CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..564551e --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,11 @@ +# Changelog +All notable changes to this project will be documented in this file. + +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 `kp_reuse` flag allow to reuse a key pair instead of creating a new one at each renewal. diff --git a/acmed/Cargo.toml b/acmed/Cargo.toml index 231cfd6..9b32003 100644 --- a/acmed/Cargo.toml +++ b/acmed/Cargo.toml @@ -16,6 +16,7 @@ clap = "2.32" env_logger = "0.6" handlebars = "2.0.0-beta.1" log = "0.4" +openssl = "0.10" pem = "0.5" serde = { version = "1.0", features = ["derive"] } time = "0.1" diff --git a/acmed/acmed_example.toml b/acmed/acmed_example.toml index 3571a4a..9bfa5ea 100644 --- a/acmed/acmed_example.toml +++ b/acmed/acmed_example.toml @@ -38,6 +38,7 @@ domains = [ "test.example.org" ] algorithm = "ecdsa_p384" +kp_reuse = true formats = ["pem"] challenge = "http-01" challenge_hooks = ["http-echo"] diff --git a/acmed/src/acmed.rs b/acmed/src/acmed.rs index 59abb5e..0ed45a6 100644 --- a/acmed/src/acmed.rs +++ b/acmed/src/acmed.rs @@ -4,6 +4,7 @@ use crate::errors::Error; use crate::storage::Storage; use handlebars::Handlebars; use log::{debug, info, warn}; +use openssl; use serde::Serialize; use std::{fmt, thread}; use std::fs::File; @@ -148,6 +149,7 @@ impl HookData { struct Certificate { domains: Vec, algo: Algorithm, + kp_reuse: bool, storage: Storage, email: String, remote_url: String, @@ -266,12 +268,29 @@ impl Certificate { } ord_new.refresh()?; }; - // TODO: allow PK reuse - let (pkey_pri, pkey_pub) = match self.algo { - Algorithm::Rsa2048 => acme_lib::create_rsa_key(2048), - Algorithm::Rsa4096 => acme_lib::create_rsa_key(4096), - Algorithm::EcdsaP256 => acme_lib::create_p256_key(), - Algorithm::EcdsaP384 => acme_lib::create_p384_key(), + + let mut raw_crt = vec![]; + let mut raw_pk = vec![]; + if self.kp_reuse { + raw_crt = self.storage + .get_certificate(&Format::Der)? + .unwrap_or_else(|| vec![]); + raw_pk = self.storage + .get_private_key(&Format::Der)? + .unwrap_or_else(|| vec![]); + }; + let (pkey_pri, pkey_pub) = if !raw_crt.is_empty() && !raw_pk.is_empty() { + ( + openssl::pkey::PKey::private_key_from_der(&raw_pk)?, + openssl::x509::X509::from_der(&raw_crt)?.public_key()?, + ) + } else { + match self.algo { + Algorithm::Rsa2048 => acme_lib::create_rsa_key(2048), + Algorithm::Rsa4096 => acme_lib::create_rsa_key(4096), + Algorithm::EcdsaP256 => acme_lib::create_p256_key(), + Algorithm::EcdsaP384 => acme_lib::create_p384_key(), + } }; let ord_cert = ord_csr.finalize_pkey(pkey_pri, pkey_pub, crate::DEFAULT_POOL_TIME)?; ord_cert.download_and_save_cert()?; @@ -292,6 +311,7 @@ impl Acmed { let cert = Certificate { domains: crt.domains.to_owned(), algo: crt.get_algorithm()?, + kp_reuse: crt.get_kp_reuse(), storage: Storage { account_directory: cnf.get_account_dir(), account_name: crt.email.to_owned(), diff --git a/acmed/src/config.rs b/acmed/src/config.rs index 002b0ef..e66a761 100644 --- a/acmed/src/config.rs +++ b/acmed/src/config.rs @@ -121,6 +121,7 @@ pub struct Certificate { pub challenge_hooks: Vec, pub post_operation_hook: Option>, pub algorithm: Option, + pub kp_reuse: Option, pub directory: Option, pub name: Option, pub name_format: Option, @@ -140,6 +141,13 @@ impl Certificate { Challenge::from_str(&self.challenge) } + pub fn get_kp_reuse(&self) -> bool { + match self.kp_reuse { + Some(b) => b, + None => crate::DEFAULT_KP_REUSE, + } + } + pub fn get_formats(&self) -> Result, Error> { let ret = match &self.formats { Some(fmts) => { diff --git a/acmed/src/encoding.rs b/acmed/src/encoding.rs index f1a6eae..2243c18 100644 --- a/acmed/src/encoding.rs +++ b/acmed/src/encoding.rs @@ -161,12 +161,7 @@ zseNNFkN0oOc55UAd2ECe6gGOXB0r4MycFOM9ccR2t8ttwE=\r #[test] fn test_der_to_pem_pk() { - let res = convert( - &PK_DER, - &Format::Der, - &Format::Pem, - PersistKind::PrivateKey, - ); + let res = convert(&PK_DER, &Format::Der, &Format::Pem, PersistKind::PrivateKey); assert!(res.is_ok()); let res = res.unwrap(); assert_eq!(PK_PEM, res.as_slice()); @@ -200,12 +195,7 @@ zseNNFkN0oOc55UAd2ECe6gGOXB0r4MycFOM9ccR2t8ttwE=\r #[test] fn test_pem_to_der_pk() { - let res = convert( - &PK_PEM, - &Format::Pem, - &Format::Der, - PersistKind::PrivateKey, - ); + let res = convert(&PK_PEM, &Format::Pem, &Format::Der, PersistKind::PrivateKey); assert!(res.is_ok()); let res = res.unwrap(); assert_eq!(PK_DER, res.as_slice()); diff --git a/acmed/src/errors.rs b/acmed/src/errors.rs index 2e61346..30042a8 100644 --- a/acmed/src/errors.rs +++ b/acmed/src/errors.rs @@ -50,6 +50,12 @@ impl From for Error { } } +impl From for Error { + fn from(error: openssl::error::ErrorStack) -> Self { + Error::new(&format!("{}", error)) + } +} + #[cfg(unix)] impl From for Error { fn from(error: nix::Error) -> Self { diff --git a/acmed/src/main.rs b/acmed/src/main.rs index 6f39753..39c12f5 100644 --- a/acmed/src/main.rs +++ b/acmed/src/main.rs @@ -18,6 +18,7 @@ pub const DEFAULT_SLEEP_TIME: u64 = 3600; pub const DEFAULT_POOL_TIME: u64 = 5000; pub const DEFAULT_CERT_FILE_MODE: u32 = 0o644; pub const DEFAULT_PK_FILE_MODE: u32 = 0o600; +pub const DEFAULT_KP_REUSE: bool = false; fn main() { let matches = App::new("acmed") diff --git a/acmed/src/storage.rs b/acmed/src/storage.rs index ca620cd..2cd3333 100644 --- a/acmed/src/storage.rs +++ b/acmed/src/storage.rs @@ -112,6 +112,10 @@ impl Storage { self.get_file(PersistKind::Certificate, fmt) } + pub fn get_private_key(&self, fmt: &Format) -> Result>, Error> { + self.get_file(PersistKind::PrivateKey, fmt) + } + pub fn get_file(&self, kind: PersistKind, fmt: &Format) -> Result>, Error> { let src_fmt = if self.formats.contains(fmt) { fmt