diff --git a/README.md b/README.md index ef1b8c1..77d01c4 100644 --- a/README.md +++ b/README.md @@ -172,7 +172,7 @@ private_key_path = "priv_key.pem" ### 3. Certificates -For each account, several certificates can be ordered. Each certificate can cover multiple domains. On disk, a certificate is represented by two files: the full certificate chain, and the private key of the certificate (generated by agnos and different from the account private key). +For each account, several certificates can be ordered. Each certificate can cover multiple domains. On disk, a certificate is represented by two files: the full certificate chain, and the private key of the certificate (different from the account private key). This certificate private key is regenerated on each certificate renewal by default but if one is already present on disk, it can be reused by setting the `reuse_private_key` option to true In the configuration file, `accounts.certificates` is a TOML [array of tables](https://toml.io/en/v1.0.0#array-of-tables) meaning that several certificates can be attached to one account by writing them one after the other. ```toml @@ -181,7 +181,12 @@ In the configuration file, `accounts.certificates` is a TOML [array of tables](h domains = ["doma.in","*.doma.in"] fullchain_output_file = "fullchain_A.pem" key_output_file = "cert_key_A.pem" -renewal_days_advance = 30 # Renew certificate 30 days in advance of its expiration (this is the default value and can be omitted). +# Renew certificate 30 days in advance of its expiration +# (this is the default value and can be omitted). +renewal_days_advance = 30 +# Regenerate a private key for the certificate on each renewal +# (this is the default value and can be omitted). +reuse_private_key = false # A second certificate ordered for that account. [[accounts.certificates]] @@ -189,6 +194,9 @@ renewal_days_advance = 21 # Renew certificate 21 days in advance of its expirati domains = ["examp.le","another.examp.le","and.a.completely.different.one"] fullchain_output_file = "fullchain_B.pem" key_output_file = "cert_key_B.pem" +# Re-use the existing private key. +# If no key is present at `key_output_file`, a new one will be generated. +reuse_private_key = true ``` ## Configuration of your DNS provider diff --git a/config_example.toml b/config_example.toml index db3adca..d437408 100644 --- a/config_example.toml +++ b/config_example.toml @@ -24,6 +24,9 @@ fullchain_output_file = "fullchain_B.pem" key_output_file = "cert_key_B.pem" # Renew certificate 21 days in advance of its expiration (defaults to 30 if omitted). renewal_days_advance = 21 +# Re-use the existing private key. +# If no key is present at `key_output_file`, a new one will be generated. +reuse_private_key = true # A second account [[accounts]] diff --git a/integration-testing/agnos/config_test.toml b/integration-testing/agnos/config_test.toml index 02c7902..6c8489d 100644 --- a/integration-testing/agnos/config_test.toml +++ b/integration-testing/agnos/config_test.toml @@ -13,12 +13,14 @@ private_key_path = "priv_key_1.pem" domains = ["a.agnos.test"] fullchain_output_file = "fullchain_A.pem" key_output_file = "cert_key_A.pem" +reuse_private_key = false # A second certificate ordered for that account. [[accounts.certificates]] domains = ["b.agnos.test","*.b.agnos.test"] fullchain_output_file = "fullchain_B.pem" key_output_file = "cert_key_B.pem" +reuse_private_key = true # A second account [[accounts]] diff --git a/integration-testing/shell.nix b/integration-testing/shell.nix index 73a7cbb..c487d39 100644 --- a/integration-testing/shell.nix +++ b/integration-testing/shell.nix @@ -34,6 +34,8 @@ let $OLDWORKDIR/$CARGO_TARGET_DIR/release/agnos-generate-accounts-keys --key-size 2048 --no-confirm ${agnos_config} bash ${wait_for_it} -t 0 127.0.0.1:14000 $OLDWORKDIR/$CARGO_TARGET_DIR/release/agnos --debug --acme-url https://127.0.0.1:14000/dir --acme-serv-ca ${pebble_cert} ${agnos_config} + # Purposefully duplicated to test renewal + $OLDWORKDIR/$CARGO_TARGET_DIR/release/agnos --debug --acme-url https://127.0.0.1:14000/dir --acme-serv-ca ${pebble_cert} ${agnos_config} cd $OLDWORKDIR rm -rf $WORKDIR ''; diff --git a/src/config.rs b/src/config.rs index 744a57e..a42a47c 100644 --- a/src/config.rs +++ b/src/config.rs @@ -37,6 +37,8 @@ pub struct Certificate { pub domains: Vec, pub fullchain_output_file: PathBuf, pub key_output_file: PathBuf, + #[serde(default)] + pub reuse_private_key: bool, } const fn default_days() -> u32 { diff --git a/src/main_logic.rs b/src/main_logic.rs index 1dc4524..e93c9e8 100644 --- a/src/main_logic.rs +++ b/src/main_logic.rs @@ -3,6 +3,7 @@ use base64::Engine; use futures_util::future::join_all; use hickory_proto::rr::Name; +use openssl::pkey::PKey; use std::io; use std::path::Path; use std::{sync::Arc, time::Duration}; @@ -237,8 +238,32 @@ pub async fn process_config_certificate( order.status ) } - let pkey = acme2::gen_ec_p256_private_key()?; - let pkey_pem = pkey.private_key_to_pem_pkcs8()?; + + let (pkey, pkey_pem, loaded_pkey) = { + let existing_pkey_pem = if config_cert.reuse_private_key { + let loaded = try_load(&config_cert.key_output_file).await?; + if loaded.is_none() { + tracing::info!( + "Couldn't load certificate private key at {}, generating one.", + config_cert.key_output_file.display() + ) + } + loaded + } else { + None + }; + match existing_pkey_pem { + Some(pkey_pem) => { + let pkey = PKey::private_key_from_pem(&pkey_pem)?; + (pkey, pkey_pem, true) + } + None => { + let pkey = acme2::gen_ec_p256_private_key()?; + let pem = pkey.private_key_to_pem_pkcs8()?; + (pkey, pem, false) + } + } + }; let order = order.finalize(acme2::Csr::Automatic(pkey)).await?; tracing::info!("Waiting for certificate signature by the ACME server."); let order = order.wait_done(Duration::from_secs(5), 3).await?; @@ -267,14 +292,16 @@ pub async fn process_config_certificate( certificate_file.write_all(b"\n").await?; } } - tracing::info!( - "Writting certificate key to file {}.", - config_cert.key_output_file.display() - ); - { - let mut private_key_file: tokio::fs::File = - create_restricted_file(&config_cert.key_output_file)?; - private_key_file.write_all(&pkey_pem).await?; + if !loaded_pkey { + tracing::info!( + "Writting certificate key to file {}.", + config_cert.key_output_file.display() + ); + { + let mut private_key_file: tokio::fs::File = + create_restricted_file(&config_cert.key_output_file)?; + private_key_file.write_all(&pkey_pem).await?; + } } Ok(()) }