ACME: reuseKey option to allow reusing private keys in renewals

This commit is contained in:
Maurus Decimus
2026-06-19 18:10:04 +02:00
parent ad47f03f48
commit cff46f26d4
11 changed files with 218 additions and 51 deletions

View File

@@ -21,7 +21,7 @@ use registry::{
TaskDomainManagement,
},
},
types::{datetime::UTCDateTime, map::Map},
types::{datetime::UTCDateTime, id::ObjectId, map::Map},
};
use serde_json::json;
use store::{registry::write::RegistryWrite, write::now};
@@ -280,10 +280,10 @@ pub async fn test(test: &TestServer) {
let not_valid_before = certificate.not_valid_before.timestamp();
let length = not_valid_after - not_valid_before;
assert_eq!(
not_valid_after - length / 2,
not_valid_before + length / 2,
task.due_timestamp() as i64,
"ACME renewal task has incorrect due timestamp, expected around {} but found {}",
not_valid_after - length / 2,
not_valid_before + length / 2,
task.due_timestamp() as i64
);
account.registry_destroy_all(ObjectType::Certificate).await;
@@ -537,6 +537,105 @@ pub async fn test(test: &TestServer) {
account.registry_destroy_all(ObjectType::Task).await;
account.registry_destroy_all(ObjectType::AcmeProvider).await;
// reuse_key: the keypair (and thus the SPKI published in DANE "3 1 1" records) must
// stay stable across renewals when enabled, and rotate when disabled.
for (reuse_key, expect_stable) in [(true, true), (false, false)] {
account.registry_destroy_all(ObjectType::Certificate).await;
account.registry_destroy_all(ObjectType::Task).await;
let reuse_acme_id = account
.registry_create_object(AcmeProvider {
directory: "https://localhost:14000/dir".to_string(),
contact: Map::new(vec!["mailto:hello@reuse.org".to_string()]),
challenge_type: AcmeChallengeType::TlsAlpn01,
reuse_key,
..Default::default()
})
.await;
let reuse_domain_id = account
.registry_create_object(Domain {
name: "reuse.org".to_string(),
certificate_management: CertificateManagement::Automatic(
CertificateManagementProperties {
acme_provider_id: reuse_acme_id,
subject_alternative_names: Default::default(),
},
),
dkim_management: DkimManagement::Manual,
dns_management: DnsManagement::Automatic(DnsManagementProperties {
dns_server_id: in_memory_dns_id,
..Default::default()
}),
..Default::default()
})
.await;
// Initial issuance
test.wait_for_tasks_skip_not_due().await;
let (first_id, first_cert) = account
.registry_get_all::<Certificate>()
.await
.into_iter()
.next()
.expect("a certificate to be issued");
let first_chain = first_cert.certificate.value().await.unwrap().into_owned();
let first_key = leaf_public_key(&first_chain);
// Backdate the stored certificate so a renewal is immediately due, then renew it.
// The reuse path must locate this certificate by its SANs and reuse its private key.
let reference = store::write::now() as i64;
let object_id = ObjectId::new(ObjectType::Certificate, first_id);
let old = test
.server
.registry()
.get(object_id)
.await
.unwrap()
.expect("stored certificate");
let mut backdated = Certificate::from(old.clone());
backdated.not_valid_before = UTCDateTime::from_timestamp(reference - 1_000_000);
backdated.not_valid_after = UTCDateTime::from_timestamp(reference - 10);
test.server
.registry()
.write(RegistryWrite::update(first_id, &backdated.into(), &old))
.await
.unwrap();
test.server
.acme_renew(reuse_domain_id)
.await
.ok()
.expect("certificate renewal to succeed");
let (_, renewed_cert) = account
.registry_get_all::<Certificate>()
.await
.into_iter()
.find(|(id, _)| *id != first_id)
.expect("a renewed certificate");
let renewed_chain = renewed_cert.certificate.value().await.unwrap().into_owned();
let second_key = leaf_public_key(&renewed_chain);
if expect_stable {
assert_eq!(
first_key, second_key,
"reuse_key=true must preserve the certificate public key across renewals"
);
} else {
assert_ne!(
first_key, second_key,
"reuse_key=false must rotate the certificate public key on renewal"
);
}
account
.registry_destroy(ObjectType::Domain, [reuse_domain_id])
.await
.assert_destroyed(&[reuse_domain_id]);
account.registry_destroy_all(ObjectType::Certificate).await;
account.registry_destroy_all(ObjectType::Task).await;
account.registry_destroy_all(ObjectType::AcmeProvider).await;
}
// Cleanup
account
.registry_update_object(
@@ -750,6 +849,15 @@ fn subject_common_name(pem: &str) -> Option<String> {
.map(str::to_string)
}
fn leaf_public_key(chain: &str) -> Vec<u8> {
let block = Pem::iter_from_buffer(chain.as_bytes())
.next()
.expect("certificate chain should contain a leaf")
.expect("valid PEM block");
let cert = block.parse_x509().expect("valid leaf certificate");
cert.public_key().raw.to_vec()
}
fn top_issuer_common_name(chain: &str) -> Option<String> {
let block = Pem::iter_from_buffer(chain.as_bytes())
.filter_map(Result::ok)

View File

@@ -34,7 +34,6 @@ use rustls_pki_types::CertificateDer;
use sha2::{Digest, Sha256};
use smtp::outbound::dane::{dnssec::TlsaLookup, verify::TlsaVerify};
use smtp::queue::{Error, ErrorDetails, Status};
use store::write::now;
use std::{
collections::BTreeSet,
fs::{self, File},
@@ -44,6 +43,7 @@ use std::{
sync::Arc,
time::{Duration, Instant},
};
use store::write::now;
#[tokio::test]
#[serial_test::serial]