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