Browse Source

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
pull/5/head
Rodolphe Breard 6 years ago
parent
commit
b3cbeaab4a
  1. 11
      CHANGELOG.md
  2. 1
      acmed/Cargo.toml
  3. 1
      acmed/acmed_example.toml
  4. 32
      acmed/src/acmed.rs
  5. 8
      acmed/src/config.rs
  6. 14
      acmed/src/encoding.rs
  7. 6
      acmed/src/errors.rs
  8. 1
      acmed/src/main.rs
  9. 4
      acmed/src/storage.rs

11
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.

1
acmed/Cargo.toml

@ -16,6 +16,7 @@ clap = "2.32"
env_logger = "0.6" env_logger = "0.6"
handlebars = "2.0.0-beta.1" handlebars = "2.0.0-beta.1"
log = "0.4" log = "0.4"
openssl = "0.10"
pem = "0.5" pem = "0.5"
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
time = "0.1" time = "0.1"

1
acmed/acmed_example.toml

@ -38,6 +38,7 @@ domains = [
"test.example.org" "test.example.org"
] ]
algorithm = "ecdsa_p384" algorithm = "ecdsa_p384"
kp_reuse = true
formats = ["pem"] formats = ["pem"]
challenge = "http-01" challenge = "http-01"
challenge_hooks = ["http-echo"] challenge_hooks = ["http-echo"]

32
acmed/src/acmed.rs

@ -4,6 +4,7 @@ use crate::errors::Error;
use crate::storage::Storage; use crate::storage::Storage;
use handlebars::Handlebars; use handlebars::Handlebars;
use log::{debug, info, warn}; use log::{debug, info, warn};
use openssl;
use serde::Serialize; use serde::Serialize;
use std::{fmt, thread}; use std::{fmt, thread};
use std::fs::File; use std::fs::File;
@ -148,6 +149,7 @@ impl HookData {
struct Certificate { struct Certificate {
domains: Vec<String>, domains: Vec<String>,
algo: Algorithm, algo: Algorithm,
kp_reuse: bool,
storage: Storage, storage: Storage,
email: String, email: String,
remote_url: String, remote_url: String,
@ -266,12 +268,29 @@ impl Certificate {
} }
ord_new.refresh()?; 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)?; let ord_cert = ord_csr.finalize_pkey(pkey_pri, pkey_pub, crate::DEFAULT_POOL_TIME)?;
ord_cert.download_and_save_cert()?; ord_cert.download_and_save_cert()?;
@ -292,6 +311,7 @@ impl Acmed {
let cert = Certificate { let cert = Certificate {
domains: crt.domains.to_owned(), domains: crt.domains.to_owned(),
algo: crt.get_algorithm()?, algo: crt.get_algorithm()?,
kp_reuse: crt.get_kp_reuse(),
storage: Storage { storage: Storage {
account_directory: cnf.get_account_dir(), account_directory: cnf.get_account_dir(),
account_name: crt.email.to_owned(), account_name: crt.email.to_owned(),

8
acmed/src/config.rs

@ -121,6 +121,7 @@ pub struct Certificate {
pub challenge_hooks: Vec<String>, pub challenge_hooks: Vec<String>,
pub post_operation_hook: Option<Vec<String>>, pub post_operation_hook: Option<Vec<String>>,
pub algorithm: Option<String>, pub algorithm: Option<String>,
pub kp_reuse: Option<bool>,
pub directory: Option<String>, pub directory: Option<String>,
pub name: Option<String>, pub name: Option<String>,
pub name_format: Option<String>, pub name_format: Option<String>,
@ -140,6 +141,13 @@ impl Certificate {
Challenge::from_str(&self.challenge) 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<Vec<Format>, Error> { pub fn get_formats(&self) -> Result<Vec<Format>, Error> {
let ret = match &self.formats { let ret = match &self.formats {
Some(fmts) => { Some(fmts) => {

14
acmed/src/encoding.rs

@ -161,12 +161,7 @@ zseNNFkN0oOc55UAd2ECe6gGOXB0r4MycFOM9ccR2t8ttwE=\r
#[test] #[test]
fn test_der_to_pem_pk() { 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()); assert!(res.is_ok());
let res = res.unwrap(); let res = res.unwrap();
assert_eq!(PK_PEM, res.as_slice()); assert_eq!(PK_PEM, res.as_slice());
@ -200,12 +195,7 @@ zseNNFkN0oOc55UAd2ECe6gGOXB0r4MycFOM9ccR2t8ttwE=\r
#[test] #[test]
fn test_pem_to_der_pk() { 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()); assert!(res.is_ok());
let res = res.unwrap(); let res = res.unwrap();
assert_eq!(PK_DER, res.as_slice()); assert_eq!(PK_DER, res.as_slice());

6
acmed/src/errors.rs

@ -50,6 +50,12 @@ impl From<handlebars::TemplateRenderError> for Error {
} }
} }
impl From<openssl::error::ErrorStack> for Error {
fn from(error: openssl::error::ErrorStack) -> Self {
Error::new(&format!("{}", error))
}
}
#[cfg(unix)] #[cfg(unix)]
impl From<nix::Error> for Error { impl From<nix::Error> for Error {
fn from(error: nix::Error) -> Self { fn from(error: nix::Error) -> Self {

1
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_POOL_TIME: u64 = 5000;
pub const DEFAULT_CERT_FILE_MODE: u32 = 0o644; pub const DEFAULT_CERT_FILE_MODE: u32 = 0o644;
pub const DEFAULT_PK_FILE_MODE: u32 = 0o600; pub const DEFAULT_PK_FILE_MODE: u32 = 0o600;
pub const DEFAULT_KP_REUSE: bool = false;
fn main() { fn main() {
let matches = App::new("acmed") let matches = App::new("acmed")

4
acmed/src/storage.rs

@ -112,6 +112,10 @@ impl Storage {
self.get_file(PersistKind::Certificate, fmt) self.get_file(PersistKind::Certificate, fmt)
} }
pub fn get_private_key(&self, fmt: &Format) -> Result<Option<Vec<u8>>, Error> {
self.get_file(PersistKind::PrivateKey, fmt)
}
pub fn get_file(&self, kind: PersistKind, fmt: &Format) -> Result<Option<Vec<u8>>, Error> { pub fn get_file(&self, kind: PersistKind, fmt: &Format) -> Result<Option<Vec<u8>>, Error> {
let src_fmt = if self.formats.contains(fmt) { let src_fmt = if self.formats.contains(fmt) {
fmt fmt

Loading…
Cancel
Save