diff --git a/CHANGELOG.md b/CHANGELOG.md index a243e849..9c398f1d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -35,11 +35,19 @@ This version includes **multiple breaking changes**. If you are upgrading from v - Improve and simplify LDAP settings (#2194 #2174) - DKIM: - Store DKIM keys in the database (#1264) + - Ignore insecure signatures when verifying DKIM (#1068 #467) - DNS Management: - - RFC2136 SIG0 support (#856) + - RFC2136 `SIG(0)` support (#856) + - Auto updating `TLSA` records when certificates change (#1664) + - `CAA` record support (#468) + - Create only `3 1 1` and `2 1 1` `TLSA` records (#2193). - BunnyDNS provider support - Porkbun provider support - DNSimple provider support +- ACME: + - `DNS-PERSIST-01` ACME challenge support (#2837) + - Renew certificates on demand, view certificate details (#675 #1162 #2566) + - `CAA` records with `accounturi` parameter support (#1933) - Clustering: - Automatic cluster node ID generation and management. - Unified cluster management (#960) @@ -47,6 +55,9 @@ This version includes **multiple breaking changes**. If you are upgrading from v - Directory: - Domain aliases (#583) - E-mail alias descriptions and disable alias (#506) +- Account configuration and discovery: + - Automatic Configuration of Email, Calendar, and Contact Server Settings ([draft-mailmaint-uaautoconf-04](https://datatracker.ietf.org/doc/html/draft-eggert-mailmaint-uaautoconf-04)) (#2201) + - MS Autodiscover V2 support (#679) - Sieve: Allow deactivating scripts without deleting them (#1251). - Tracing: Enable events only mode (#2276) - Enterprise features: @@ -76,12 +87,13 @@ This version includes **multiple breaking changes**. If you are upgrading from v - Fix `inMailboxOtherThan` query logic. - Fix `hasAttachment` search field (#2778) - IMAP: Increment argument max length to `8000` bytes -- WebDAV: Return `304` `NOT_MODIFIED` on `If-None-Match`. +- WebDAV: Return `304` `NOT_MODIFIED` on `If-None-Match` - Configuration: Prefix parsing issues (#2495) - OIDC: JWKS Exposes Symmetric Signing Key - SQLite: Fix thread pool exhaustion. - PostgreSQL: Use clean recycling method on connection pool - Meilisearch: Make `id` sorteable. +- ACME: Fix wrong origin for subdomain updates (#2360) - Spam filter: Skip invalid messages during training. - Calendar: Include minutes in localized invite templates (#2828) diff --git a/Cargo.lock b/Cargo.lock index b44b244b..9d2a422e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7770,6 +7770,7 @@ dependencies = [ "trc", "types", "utils", + "x509-parser", ] [[package]] diff --git a/crates/common/src/network/acme/order.rs b/crates/common/src/network/acme/order.rs index 68c9e3c1..adb68243 100644 --- a/crates/common/src/network/acme/order.rs +++ b/crates/common/src/network/acme/order.rs @@ -253,6 +253,7 @@ impl AcmeRequestBuilder { &format!("_acme-challenge.{}", domain), DnsRecord::TXT(self.dns_proof(challenge)?), true, + true, ) .await .map_err(AcmeError::Dns)?; diff --git a/crates/common/src/network/dns/update.rs b/crates/common/src/network/dns/update.rs index 241cbe2a..35339fc2 100644 --- a/crates/common/src/network/dns/update.rs +++ b/crates/common/src/network/dns/update.rs @@ -261,9 +261,12 @@ impl DnsUpdater { name: &str, record: DnsRecord, verify: bool, + delete_before_create: bool, ) -> Result { // First try deleting the record - if let Err(err) = self.updater.delete(name, origin, record.as_type()).await { + if delete_before_create + && let Err(err) = self.updater.delete(name, origin, record.as_type()).await + { // Errors are expected if the record does not exist trc::event!( Dns(DnsEvent::RecordDeletionFailed), diff --git a/crates/services/src/task_manager/dkim.rs b/crates/services/src/task_manager/dkim.rs index fb957bb8..be2502fa 100644 --- a/crates/services/src/task_manager/dkim.rs +++ b/crates/services/src/task_manager/dkim.rs @@ -33,6 +33,7 @@ use store::{ }, write::now, }; +use trc::DkimEvent; use types::id::Id; pub(crate) trait DkimManagementTask: Sync + Send { @@ -143,7 +144,20 @@ async fn dkim_management(server: &Server, task: &TaskDomainManagement) -> trc::R let mut do_refresh = false; for algorithm in create_signatures { + #[cfg(feature = "test_mode")] + let secret = { + if dkim.selector_template.contains("dummy") { + match algorithm { + DkimSignatureType::Dkim1Ed25519Sha256 => TEST_ED25519_KEY.to_string(), + DkimSignatureType::Dkim1RsaSha256 => TEST_RSA_KEY.to_string(), + } + } else { + generate_dkim_private_key(algorithm).await.unwrap().unwrap() + } + }; + // Generate new key and selector + #[cfg(not(feature = "test_mode"))] let secret = match generate_dkim_private_key(algorithm).await? { Ok(secret) => secret, Err(err) => { @@ -165,7 +179,7 @@ async fn dkim_management(server: &Server, task: &TaskDomainManagement) -> trc::R stage: DkimRotationStage::Active, domain_id: task.domain_id, member_tenant_id: domain.member_tenant_id, - selector, + selector: selector.clone(), private_key: SecretText::Text(SecretTextValue { secret }), ..Default::default() }; @@ -178,10 +192,16 @@ async fn dkim_management(server: &Server, task: &TaskDomainManagement) -> trc::R if let Some((updater, origin)) = &dns_updater { let record = generate_dkim_dns_record(&signature, &domain.name).await?; let signature_transition = if updater - .create(origin, &record.name, record.record, true) + .create(origin, &record.name, record.record, true, true) .await .is_ok_and(|did_propagate| did_propagate) { + trc::event!( + Dkim(DkimEvent::SignaturePublished), + Id = selector.clone(), + Details = domain.name.clone() + ); + do_refresh = true; UTCDateTime::from_timestamp((now + dkim.rotate_after.as_secs()) as i64) } else { @@ -203,7 +223,13 @@ async fn dkim_management(server: &Server, task: &TaskDomainManagement) -> trc::R .write(RegistryWrite::insert(&signature.into())) .await? { - RegistryWriteResult::Success(_) => (), + RegistryWriteResult::Success(_) => { + trc::event!( + Dkim(DkimEvent::SignatureCreated), + Id = selector, + Details = domain.name.clone() + ); + } err => { return Ok(TaskResult::permanent(format!( "Failed to write DKIM signature: {err}" @@ -218,7 +244,7 @@ async fn dkim_management(server: &Server, task: &TaskDomainManagement) -> trc::R let record = generate_dkim_dns_record(&signature.object, &domain.name).await?; if let Some((updater, origin)) = &dns_updater { match updater - .create(origin, &record.name, record.record, true) + .create(origin, &record.name, record.record, true, true) .await { Ok(true) => { @@ -234,6 +260,12 @@ async fn dkim_management(server: &Server, task: &TaskDomainManagement) -> trc::R new_signature.set_next_transition(signature_transition); new_signature.set_stage(DkimRotationStage::Active); + trc::event!( + Dkim(DkimEvent::SignaturePublished), + Id = new_signature.selector().to_string(), + Details = domain.name.clone() + ); + // Write key if let Some(task_result) = update_signature( server, @@ -296,6 +328,12 @@ async fn dkim_management(server: &Server, task: &TaskDomainManagement) -> trc::R new_signature.set_next_transition(signature_transition); new_signature.set_stage(DkimRotationStage::Retiring); + trc::event!( + Dkim(DkimEvent::SignatureRetiring), + Id = new_signature.selector().to_string(), + Details = domain.name.clone() + ); + // Write key if let Some(task_result) = update_signature( server, @@ -332,6 +370,12 @@ async fn dkim_management(server: &Server, task: &TaskDomainManagement) -> trc::R new_signature.set_next_transition(signature_transition); new_signature.set_stage(DkimRotationStage::Retired); + trc::event!( + Dkim(DkimEvent::SignatureRetired), + Id = new_signature.selector().to_string(), + Details = domain.name.clone() + ); + // Write key if let Some(task_result) = update_signature( server, @@ -373,6 +417,13 @@ async fn dkim_management(server: &Server, task: &TaskDomainManagement) -> trc::R // Delete signatures for signature in delete_signatures { let record = generate_dkim_dns_record_name(&signature.object, &domain.name); + + trc::event!( + Dkim(DkimEvent::SignatureDeleted), + Id = signature.object.selector().to_string(), + Details = domain.name.clone() + ); + match server .registry() .write(RegistryWrite::delete_object( @@ -474,3 +525,37 @@ async fn update_signature( } } } + +const TEST_RSA_KEY: &str = r#"-----BEGIN RSA PRIVATE KEY----- +MIIEowIBAAKCAQEAv9XYXG3uK95115mB4nJ37nGeNe2CrARm1agrbcnSk5oIaEfM +ZLUR/X8gPzoiNHZcfMZEVR6bAytxUhc5EvZIZrjSuEEeny+fFd/cTvcm3cOUUbIa +UmSACj0dL2/KwW0LyUaza9z9zor7I5XdIl1M53qVd5GI62XBB76FH+Q0bWPZNkT4 +NclzTLspD/MTpNCCPhySM4Kdg5CuDczTH4aNzyS0TqgXdtw6A4Sdsp97VXT9fkPW +9rso3lrkpsl/9EQ1mR/DWK6PBmRfIuSFuqnLKY6v/z2hXHxF7IoojfZLa2kZr9Ae +d4l9WheQOTA19k5r2BmlRw/W9CrgCBo0Sdj+KQIDAQABAoIBAFPChEi/OvnulReB +ECQWhOUYuNKlFKQU++2YEvZJ4+bMn5UgnE7wfJ1pj2Pr9xlfALz+OMHNrjMxGbaV +KzdrT2uCkYcf78XjnhuH9gKIiXDUv4L4N+P3u6w8yOx4bFgOS9IjS53yDOPM7SC5 +g6dIg5aigHaHlffqIuFFv4yQMI/+Ai+zBKxS7wRhxK/7nnAuo28fe5MEdp57ho9/ +AGlDNsdg9zCgjwhokwFE3+AaD+bkUFm4gQ1XjkUFrlmnQn8vDQ0i9toEWhCj+UPY +iOKL63MJnr90MXTXWLHoFj99wBp//mYygbF9Lj8fa28/oa8LWp3Jhb7QeMgH46iv +3aLHbTECgYEA5M2dAw+nyMw9vYlkMejhwObKYP8Mr/6zcGMLCalYvRJM5iUAM0JI +H6sM6pV9/nv167cbKocj3xYPdtE7FPOn4132MLM8Ne1f8nPE64Qrcbj5WBXvLnU8 +hpWbwe2Z8h7UUMKx6q4F1/TXYkc3ScxYwfjM4mP/pLsAOgVzRSEEgrUCgYEA1qNQ +xaQHNWZ1O8WuTnqWd5JSsic6iURAmUcLeFDZY2PWhVoaQ8L/xMQhDYs1FIbLWArW +4Qq3Ibu8AbSejAKuaJz7Uf26PX+PYVUwAOO0qamCJ8d/qd6So7qWMDyAY2yXI39Y +1nMqRjr7bkEsggAZao7BKqA7ZtmogjOusBT38iUCgYEA06agJ8TDoKvOMRZ26PRU +YO0dKLzGL8eclcoI29cbj0rud7aiiMg3j5PbTuUat95TjsjDCIQaWrM9etvxm2AJ +Xfn9Uu96MyhyKQWOk46f4YMKpMElkARDCPw8KRhx39dE77AqhLyWCz8iPndCXbH6 +KPTOEl4OjYOuof2Is9nnIkECgYBh948RdsnXhNlzm8nwhiGRmBbou+EK8D0v+O5y +Tyy6IcKzgSnFzgZh8EdJ4EUtBk1f9SqY8wQdgIvSl3daXorusuA/TzkngsaV3YUY +ktZOLlF7CKLrjOyPkMWmZKcROmpNyH1q/IvKHHfQnizLdXIkYd4nL5WNX0F7lE1i +j1+QhQKBgB2lviBK7rJFwlFYdQUP1NAN2dKxMZk8uJS8JglHrM0+8nRI83HbTdEQ +vB0ManEKBkbS4T5n+gRtdEqKSDmWDTXDlrBfcdCHNQLwYtBpOotCqQn/AmfjcPBl +byAbwh4+HiZ5JISoRZpiZqy67aJNVoXmdtb/E9mi7ozzytpxMNql +-----END RSA PRIVATE KEY----- +"#; + +const TEST_ED25519_KEY: &str = r#"-----BEGIN PRIVATE KEY----- +MC4CAQAwBQYDK2VwBCIEIAO3hAf144lTAVjTkht3ZwBTK0CMCCd1bI0alggneN3B +-----END PRIVATE KEY----- +"#; diff --git a/crates/services/src/task_manager/dns.rs b/crates/services/src/task_manager/dns.rs index 5c872ff1..f0d5166a 100644 --- a/crates/services/src/task_manager/dns.rs +++ b/crates/services/src/task_manager/dns.rs @@ -73,7 +73,7 @@ async fn dns_management(server: &Server, task: &TaskDnsManagement) -> trc::Resul let mut errors = String::new(); for record in records { if let Err(err) = dns_updater - .create(origin, &record.name, record.record, false) + .create(origin, &record.name, record.record, false, false) .await { if !errors.is_empty() { diff --git a/crates/trc/src/event/enums.rs b/crates/trc/src/event/enums.rs index d0e9a3d8..aa08118e 100644 --- a/crates/trc/src/event/enums.rs +++ b/crates/trc/src/event/enums.rs @@ -6,7 +6,7 @@ // This file is auto-generated. Do not edit directly. -pub const TOTAL_EVENT_COUNT: usize = 596; +pub const TOTAL_EVENT_COUNT: usize = 601; pub const TOTAL_METRIC_COUNT: usize = 339; #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] @@ -223,6 +223,11 @@ pub enum DkimEvent { SignatureLength = 125, SignerNotFound = 126, BuildError = 592, + SignatureCreated = 596, + SignaturePublished = 597, + SignatureRetiring = 598, + SignatureRetired = 599, + SignatureDeleted = 600, } #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] diff --git a/crates/trc/src/event/enums_impl.rs b/crates/trc/src/event/enums_impl.rs index b279cf28..3baddfd5 100644 --- a/crates/trc/src/event/enums_impl.rs +++ b/crates/trc/src/event/enums_impl.rs @@ -137,6 +137,11 @@ impl EventType { b"dkim.signature-length" => EventType::Dkim(DkimEvent::SignatureLength), b"dkim.signer-not-found" => EventType::Dkim(DkimEvent::SignerNotFound), b"dkim.build-error" => EventType::Dkim(DkimEvent::BuildError), + b"dkim.signature-created" => EventType::Dkim(DkimEvent::SignatureCreated), + b"dkim.signature-published" => EventType::Dkim(DkimEvent::SignaturePublished), + b"dkim.signature-retiring" => EventType::Dkim(DkimEvent::SignatureRetiring), + b"dkim.signature-retired" => EventType::Dkim(DkimEvent::SignatureRetired), + b"dkim.signature-deleted" => EventType::Dkim(DkimEvent::SignatureDeleted), b"dmarc.pass" => EventType::Dmarc(DmarcEvent::Pass), b"dmarc.fail" => EventType::Dmarc(DmarcEvent::Fail), b"dmarc.perm-error" => EventType::Dmarc(DmarcEvent::PermError), @@ -759,6 +764,11 @@ impl EventType { EventType::Dkim(DkimEvent::SignatureLength) => "dkim.signature-length", EventType::Dkim(DkimEvent::SignerNotFound) => "dkim.signer-not-found", EventType::Dkim(DkimEvent::BuildError) => "dkim.build-error", + EventType::Dkim(DkimEvent::SignatureCreated) => "dkim.signature-created", + EventType::Dkim(DkimEvent::SignaturePublished) => "dkim.signature-published", + EventType::Dkim(DkimEvent::SignatureRetiring) => "dkim.signature-retiring", + EventType::Dkim(DkimEvent::SignatureRetired) => "dkim.signature-retired", + EventType::Dkim(DkimEvent::SignatureDeleted) => "dkim.signature-deleted", EventType::Dmarc(DmarcEvent::Pass) => "dmarc.pass", EventType::Dmarc(DmarcEvent::Fail) => "dmarc.fail", EventType::Dmarc(DmarcEvent::PermError) => "dmarc.perm-error", @@ -1470,6 +1480,11 @@ impl EventType { EventType::Dkim(DkimEvent::SignatureLength) => 125, EventType::Dkim(DkimEvent::SignerNotFound) => 126, EventType::Dkim(DkimEvent::BuildError) => 592, + EventType::Dkim(DkimEvent::SignatureCreated) => 596, + EventType::Dkim(DkimEvent::SignaturePublished) => 597, + EventType::Dkim(DkimEvent::SignatureRetiring) => 598, + EventType::Dkim(DkimEvent::SignatureRetired) => 599, + EventType::Dkim(DkimEvent::SignatureDeleted) => 600, EventType::Dmarc(DmarcEvent::Pass) => 134, EventType::Dmarc(DmarcEvent::Fail) => 132, EventType::Dmarc(DmarcEvent::PermError) => 135, @@ -2071,6 +2086,11 @@ impl EventType { 125 => Some(EventType::Dkim(DkimEvent::SignatureLength)), 126 => Some(EventType::Dkim(DkimEvent::SignerNotFound)), 592 => Some(EventType::Dkim(DkimEvent::BuildError)), + 596 => Some(EventType::Dkim(DkimEvent::SignatureCreated)), + 597 => Some(EventType::Dkim(DkimEvent::SignaturePublished)), + 598 => Some(EventType::Dkim(DkimEvent::SignatureRetiring)), + 599 => Some(EventType::Dkim(DkimEvent::SignatureRetired)), + 600 => Some(EventType::Dkim(DkimEvent::SignatureDeleted)), 134 => Some(EventType::Dmarc(DmarcEvent::Pass)), 132 => Some(EventType::Dmarc(DmarcEvent::Fail)), 135 => Some(EventType::Dmarc(DmarcEvent::PermError)), @@ -2696,6 +2716,11 @@ impl EventType { EventType::Delivery(DeliveryEvent::DsnSuccess) => Level::Info, EventType::Delivery(DeliveryEvent::DsnTempFail) => Level::Info, EventType::Delivery(DeliveryEvent::DsnPermFail) => Level::Info, + EventType::Dkim(DkimEvent::SignatureCreated) => Level::Info, + EventType::Dkim(DkimEvent::SignaturePublished) => Level::Info, + EventType::Dkim(DkimEvent::SignatureRetiring) => Level::Info, + EventType::Dkim(DkimEvent::SignatureRetired) => Level::Info, + EventType::Dkim(DkimEvent::SignatureDeleted) => Level::Info, EventType::Dns(DnsEvent::RecordCreated) => Level::Info, EventType::Dns(DnsEvent::RecordPropagated) => Level::Info, EventType::IncomingReport(IncomingReportEvent::DmarcReport) => Level::Info, @@ -3079,6 +3104,11 @@ impl EventType { EventType::Dkim(DkimEvent::SignatureLength) => "DKIM signature length issue", EventType::Dkim(DkimEvent::SignerNotFound) => "DKIM signer not found", EventType::Dkim(DkimEvent::BuildError) => "DKIM build error", + EventType::Dkim(DkimEvent::SignatureCreated) => "DKIM signature created", + EventType::Dkim(DkimEvent::SignaturePublished) => "DKIM signature published", + EventType::Dkim(DkimEvent::SignatureRetiring) => "DKIM signature retiring", + EventType::Dkim(DkimEvent::SignatureRetired) => "DKIM signature retired", + EventType::Dkim(DkimEvent::SignatureDeleted) => "DKIM signature deleted", EventType::Dmarc(DmarcEvent::Pass) => "DMARC check passed", EventType::Dmarc(DmarcEvent::Fail) => "DMARC check failed", EventType::Dmarc(DmarcEvent::PermError) => "DMARC permanent error", @@ -3908,6 +3938,19 @@ impl EventType { EventType::Dkim(DkimEvent::BuildError) => { "An error occurred while building DKIM signature" } + EventType::Dkim(DkimEvent::SignatureCreated) => "A new DKIM signature has been created", + EventType::Dkim(DkimEvent::SignaturePublished) => { + "A DKIM signature has been published to DNS" + } + EventType::Dkim(DkimEvent::SignatureRetiring) => { + "A DKIM signature is retiring and will be removed from DNS soon" + } + EventType::Dkim(DkimEvent::SignatureRetired) => { + "A DKIM signature has been retired and removed from DNS" + } + EventType::Dkim(DkimEvent::SignatureDeleted) => { + "A DKIM signature has been deleted from the system" + } EventType::Dmarc(DmarcEvent::Pass) => "The DMARC check has passed", EventType::Dmarc(DmarcEvent::Fail) => "The DMARC check has failed", EventType::Dmarc(DmarcEvent::PermError) => "A permanent error occurred with DMARC", @@ -5180,6 +5223,11 @@ impl EventType { EventType::Dkim(DkimEvent::SignatureLength), EventType::Dkim(DkimEvent::SignerNotFound), EventType::Dkim(DkimEvent::BuildError), + EventType::Dkim(DkimEvent::SignatureCreated), + EventType::Dkim(DkimEvent::SignaturePublished), + EventType::Dkim(DkimEvent::SignatureRetiring), + EventType::Dkim(DkimEvent::SignatureRetired), + EventType::Dkim(DkimEvent::SignatureDeleted), EventType::Dmarc(DmarcEvent::Pass), EventType::Dmarc(DmarcEvent::Fail), EventType::Dmarc(DmarcEvent::PermError), diff --git a/tests/Cargo.toml b/tests/Cargo.toml index e92b081d..89cf56e2 100644 --- a/tests/Cargo.toml +++ b/tests/Cargo.toml @@ -82,6 +82,7 @@ compact_str = "0.9.0" quick-xml = "0.39" jmap-tools = { version = "0.1" } dns-update = { path = "/Users/me/code/dns-update", features = ["test_provider"] } +x509-parser = "0.18" [target.'cfg(not(target_env = "msvc"))'.dependencies] jemallocator = "0.5.0" diff --git a/tests/src/automation/acme.rs b/tests/src/automation/acme.rs index 8a653ffc..1d234eb9 100644 --- a/tests/src/automation/acme.rs +++ b/tests/src/automation/acme.rs @@ -5,6 +5,7 @@ */ use crate::utils::server::TestServer; +use ahash::AHashSet; use common::network::dns::update::DNS_RECORDS; use dns_update::DnsRecord; use jmap_proto::error::set::SetErrorType; @@ -13,19 +14,128 @@ use registry::{ enums::{AcmeChallengeType, AcmeRenewBefore, DnsRecordType}, prelude::{ObjectType, Property}, structs::{ - AcmeProvider, Certificate, CertificateManagement, CertificateManagementProperties, - DkimManagement, DnsManagement, DnsManagementProperties, DnsServer, DnsServerCloudflare, - Domain, SecretKey, SecretKeyValue, Task, TaskDomainManagement, + AcmeProvider, Action, Certificate, CertificateManagement, + CertificateManagementProperties, DkimManagement, DnsManagement, + DnsManagementProperties, DnsServer, DnsServerCloudflare, Domain, PublicText, + PublicTextValue, SecretKey, SecretKeyValue, SecretText, SecretTextValue, Task, + TaskDomainManagement, }, }, - types::map::Map, + types::{datetime::UTCDateTime, map::Map}, }; use serde_json::json; +use store::{registry::write::RegistryWrite, write::now}; +use x509_parser::parse_x509_certificate; pub async fn test(test: &TestServer) { println!("Running ACME tests..."); let account = test.account("admin@example.org"); + // Manually insert certificates with different expiration dates + let now = now() as i64; + test.server + .registry() + .write(RegistryWrite::insert( + &Certificate { + certificate: PublicText::Text(PublicTextValue { + value: ISSUER_ABC_CERT.to_string(), + }), + private_key: SecretText::Text(SecretTextValue { + secret: ISSUER_ABC_PK.to_string(), + }), + issuer: "Issuer ABC".to_string(), + not_valid_after: UTCDateTime::from_timestamp(now + 86400), + not_valid_before: UTCDateTime::from_timestamp(now - 10), + subject_alternative_names: Map::new(vec!["mail.example.org".to_string()]), + } + .into(), + )) + .await + .unwrap(); + test.server + .registry() + .write(RegistryWrite::insert( + &Certificate { + certificate: PublicText::Text(PublicTextValue { + value: ISSUER_XYZ_CERT.to_string(), + }), + private_key: SecretText::Text(SecretTextValue { + secret: ISSUER_XYZ_PK.to_string(), + }), + issuer: "Issuer XYZ".to_string(), + not_valid_after: UTCDateTime::from_timestamp(now + (2 * 86400)), + not_valid_before: UTCDateTime::from_timestamp(now - 10), + subject_alternative_names: Map::new(vec!["mail.example.org".to_string()]), + } + .into(), + )) + .await + .unwrap(); + test.server + .registry() + .write(RegistryWrite::insert( + &Certificate { + certificate: PublicText::Text(PublicTextValue { + value: ISSUER_123_CERT.to_string(), + }), + private_key: SecretText::Text(SecretTextValue { + secret: ISSUER_123_PK.to_string(), + }), + issuer: "Issuer 123".to_string(), + not_valid_after: UTCDateTime::from_timestamp(now - 86400), + not_valid_before: UTCDateTime::from_timestamp(now - 100), + subject_alternative_names: Map::new(vec!["mail.example.org".to_string()]), + } + .into(), + )) + .await + .unwrap(); + assert_eq!( + account + .registry_get_all::() + .await + .into_iter() + .map(|(_, c)| c.issuer) + .collect::>(), + AHashSet::from_iter(vec![ + "Issuer ABC".to_string(), + "Issuer XYZ".to_string(), + "Issuer 123".to_string() + ]) + ); + + // Reload certificates and make XYZ is used for mail.example.org + account + .registry_create_object(Action::ReloadTlsCertificates) + .await; + assert_eq!( + parse_x509_certificate( + test.server + .resolve_certificate("mail.example.org") + .unwrap() + .end_entity_cert() + .unwrap() + ) + .unwrap() + .1 + .tbs_certificate + .issuer + .to_string(), + "CN=Issuer XYZ CA" + ); + + // Make sure the expired certificate was deleted + assert_eq!( + account + .registry_get_all::() + .await + .into_iter() + .map(|(_, c)| c.issuer) + .collect::>(), + AHashSet::from_iter(vec!["Issuer ABC".to_string(), "Issuer XYZ".to_string()]) + ); + account.registry_destroy_all(ObjectType::Certificate).await; + // Create test Pebble and In Memory DNS servers let pebble_dns_id = account .registry_create_object(DnsServer::Cloudflare(DnsServerCloudflare { @@ -79,7 +189,7 @@ pub async fn test(test: &TestServer) { ); // Create a domain and trigger TLS-ALPN-01 ACME renewal - let domain_id = account + let tls_domain_id = account .registry_create_object(Domain { name: "tls.org".to_string(), certificate_management: CertificateManagement::Automatic( @@ -159,7 +269,7 @@ pub async fn test(test: &TestServer) { }) = task { assert_eq!( - task_domain_id, domain_id, + task_domain_id, tls_domain_id, "ACME renewal task has incorrect domain ID" ); } else { @@ -231,7 +341,7 @@ pub async fn test(test: &TestServer) { ..Default::default() }) .await; - account + let dns_domain_id = account .registry_create_object(Domain { name: "dns.org".to_string(), certificate_management: CertificateManagement::Automatic( @@ -272,7 +382,7 @@ pub async fn test(test: &TestServer) { ..Default::default() }) .await; - account + let persist_domain_id = account .registry_create_object(Domain { name: "persist.org".to_string(), certificate_management: CertificateManagement::Automatic( @@ -301,4 +411,182 @@ pub async fn test(test: &TestServer) { let mut sans = certificate.subject_alternative_names.into_inner(); sans.sort(); assert_eq!(sans, vec!["*.persist.org".to_string()]); + + // Cleanup + account + .registry_update_object( + ObjectType::Domain, + domain_id, + json!({ + Property::CertificateManagement: CertificateManagement::Manual, + }), + ) + .await; + account + .registry_destroy( + ObjectType::Domain, + [tls_domain_id, dns_domain_id, persist_domain_id], + ) + .await + .assert_destroyed(&[tls_domain_id, dns_domain_id, persist_domain_id]); + account.registry_destroy_all(ObjectType::DnsServer).await; + account.registry_destroy_all(ObjectType::AcmeProvider).await; + account.registry_destroy_all(ObjectType::Certificate).await; + account.registry_destroy_all(ObjectType::Task).await; } + +const ISSUER_ABC_CERT: &str = r#"-----BEGIN CERTIFICATE----- +MIIDLjCCAhagAwIBAgIURV4DMcpCSV95vODPEDWtzZ0XTjIwDQYJKoZIhvcNAQEL +BQAwGDEWMBQGA1UEAwwNSXNzdWVyIEFCQyBDQTAeFw0yNjA0MDMxMjQ1MjVaFw0y +NzA0MDMxMjQ1MjVaMBgxFjAUBgNVBAMMDUlzc3VlciBBQkMgQ0EwggEiMA0GCSqG +SIb3DQEBAQUAA4IBDwAwggEKAoIBAQC+iuw9/2hAtmt/1+K26N/XNuWRaUvJFfqs +cV5ZXzcRywXvamHivbL7OcVf96D9y67vh+beYReYo4N+ObtWJRA+5+SeBjmfEdDf +sgLn5lABvzQmFUDBIbGLN9xjYSLYcfTpN0Edla/mRJf70fxzniTFUbrtnEZ4G19Y +oDVb9V9hyTG35ak+mm20boIJkgbTW4G1xD/Q3eaWKXeKNLDxBI3wBWg1xGpMB58l +z3IiHRUtzzE5V5jtSy0oQ4+VR0u9WJdYhPqxMNzixuzEeMveB2Xd+Mf4FuvQy+wg +xU2Sb1ZqnK14+vGNAbA7mHIBvAfUMZSuYnCIGvr37XpjMBc7nm+JAgMBAAGjcDBu +MB0GA1UdDgQWBBRW3foBUlBYWVKpuJTo175EYhv7mDAfBgNVHSMEGDAWgBRW3foB +UlBYWVKpuJTo175EYhv7mDAPBgNVHRMBAf8EBTADAQH/MBsGA1UdEQQUMBKCEG1h +aWwuZXhhbXBsZS5vcmcwDQYJKoZIhvcNAQELBQADggEBAANV8NOesHrSbqtqkrXW +nIfriEr5a7mVW8FIsyhDxMTOeRjkM+8nFFsjNvTe3HDvF8zDGPmCMKuxQHQ+8NAA +CKjcQEkv5PBb8gMRRQUexSPJF1hrqFA/cQn+lVnv6eZ2r/K7NlM80otvZIRtJbWi +1hlwE2EBEq9tWgrPUEjStlYzO5rAmxM2/yprbzYMiL0g4d8VIseVaQl9C/M00VLU +r9fw/Rz43kBGcDE5T7Gb2T8pUmZhhZykADglgU8MrPp6VD2oOTF5Qxl6CMd/bG+B +YyEBkY27+hfdf68rIrjOJJ518/gYKGVVHP3FDWPlus4hURn+g85CKu4p3a3TPbl3 +VWw= +-----END CERTIFICATE----- +"#; + +const ISSUER_ABC_PK: &str = r#"-----BEGIN PRIVATE KEY----- +MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQC+iuw9/2hAtmt/ +1+K26N/XNuWRaUvJFfqscV5ZXzcRywXvamHivbL7OcVf96D9y67vh+beYReYo4N+ +ObtWJRA+5+SeBjmfEdDfsgLn5lABvzQmFUDBIbGLN9xjYSLYcfTpN0Edla/mRJf7 +0fxzniTFUbrtnEZ4G19YoDVb9V9hyTG35ak+mm20boIJkgbTW4G1xD/Q3eaWKXeK +NLDxBI3wBWg1xGpMB58lz3IiHRUtzzE5V5jtSy0oQ4+VR0u9WJdYhPqxMNzixuzE +eMveB2Xd+Mf4FuvQy+wgxU2Sb1ZqnK14+vGNAbA7mHIBvAfUMZSuYnCIGvr37Xpj +MBc7nm+JAgMBAAECggEAAOWJGddxOuWVbbPB+T+6NsgU9Ry/1zmT7lzgEJrP6yKN +QqJC15YgAgW0dwdkmd5HdxDfr2xHCkFNS2yDn44wBaILOBy3vntqllAITxxohAtN +io3oQzT3fU5UBeoftWOIv1JXWSL4SuS1hjyVVRIXMEbxSX4GKLl1G3Ae/LrmCffu +IFpQdidxY/ZpCXLqWLK3ysmA1Sq0BmJeIB3EwsrwJn/OK6nJC8N35bTpOx5ZVnUL +CZGQFmUZRpSFs+wAWy0f0mGrMOWVjN0nUZUj52ZZoyZ1sxc7DLxDGoqO72Mia+/3 +xtAOwq4kkonCEVRNGDOOOiVOSufz6dGv4dJ2WYCvJQKBgQD8XtHu/Ro6QOPDb2k8 +ovHnlCAe0cFk8kNlQhsKVJ5ybDWUJPOxjfgv8LudUSExdMtryus9KkGHqJ5pMLzC +x/Y3492pQpYY303lSo4crUWt/W6BUhWIfeA2827eQRTbccz00Evy9NugK8RCUJTO +Ek1BKUVWkugFEria5XETFNiZ5QKBgQDBSHb9q1SGIDE9yQLdEZ2FjWYvrcKqa792 +lwxw/QAzDBNXZbsN2PAkYbyDltbtOOvDBrHmGWR2EECAW5ifo5rhmakgKfshzMDj +w2piXz1QhzFuPolozp10iE7GpKH/s0NI5RA8CFLMKy9wsbTw8VmyqtOg6XZ3yhXX ++6MQodaU1QKBgQCF0MlEDZSgmtOqRyLn8gaOom49qT8AhazSvjCUU7YIOfRW7xkA +ZqTY1q7EhcYx8RoDt/7v2b4RbolAgYU1Ss31aK+aFiJ8Ybtt/xBHiGDQFvdHPv0H ++KawvHdnBd9HVJo2nVQIKWljDpHsD8o3UmEAUh/f/dllBz43c713PrBzOQKBgQCG +wLnM0zVDqZALDmiDrfNPmCxlE1TDsgkzac3PvGP2MvYNGazW06dhBg8DAxfnHacp +OjKvRIbI1T3S/4khy1OA87t45Cvk/baBVM2HtfSufwLUZJ8yRdJ620loroEPH3DK +koDGCduH4pfZjtuim/G4YebXqczhaS/fe93NC7fp3QKBgQCu+Mw7NPuNI8og453k +jsxBVlh52xvw9Y26OTFfuwBB05iDOLK6Qh+VpQEhI26uSESgwjy0m+VEev4uQYPD +5kPQpyO84CcbIsdfsJysbA48sn5Wzg1MGYJUc97eKu70cUs3yXQsuU6cs3M4C2Sf +j+UnghyPLhlHVTL/Xw60mZc+xw== +-----END PRIVATE KEY----- +"#; + +const ISSUER_XYZ_CERT: &str = r#"-----BEGIN CERTIFICATE----- +MIIDLjCCAhagAwIBAgIUGDz02vh30maG1BceejuWOSUO6ZQwDQYJKoZIhvcNAQEL +BQAwGDEWMBQGA1UEAwwNSXNzdWVyIFhZWiBDQTAeFw0yNjA0MDMxMjQ3MzRaFw0y +NzA0MDMxMjQ3MzRaMBgxFjAUBgNVBAMMDUlzc3VlciBYWVogQ0EwggEiMA0GCSqG +SIb3DQEBAQUAA4IBDwAwggEKAoIBAQDBUPhu/4n3ZMRUGLToU/0iUbXk6e7yJTvv +RJrrn5FFGrulxGcdZaKdmh5ZBiQu15xjXz7IZBmkXiyUy/4LMznvAC8OBeSut1pZ +f8D3Jox7PRAMPuTfUh9C9qBMFhDj+pXOd/fHy4JgJ22rDQQsCLV8N0JAkBEEvRnF +GDeriSWIReUbluRVblgR2jeVXwkvGeiNcrlbE3+zAPKX4JXmLJYgfFwjjZyvCo7L +P/tqLdR31bxpjtrpY3VjbpsqMh3qiLhsfzxBwy3vQCBzQ77thjUU77Ixrhp0dkY1 +DonTDuIxvvMNLZkX+EgonuGgtNwolWoOb4FNKJINdYW8JtknVnT/AgMBAAGjcDBu +MB0GA1UdDgQWBBTOZ3eOed6PrLVb0iysnnGcEm0ylzAfBgNVHSMEGDAWgBTOZ3eO +ed6PrLVb0iysnnGcEm0ylzAPBgNVHRMBAf8EBTADAQH/MBsGA1UdEQQUMBKCEG1h +aWwuZXhhbXBsZS5vcmcwDQYJKoZIhvcNAQELBQADggEBAI9aZDT21yXxl67sDHSj +IGGAqhcpfNQdqCAvNbYdeiXTHZE7SHndF2efMRj1iJ9lAsYalFi0jbNCoU/KVDv8 +V7ApxhNlxl5kHmtKBJJLxXyklX+Fic10nUQY5EqU351Rn6Lapp2jn5DmXxlrsy+x +CSYVSU8l3ag3Wzdnl2rua6PlLYiFJIKsmqyUBNhvuXVsRkf+y2BVLglOTc3cXdBh +iCgOds4SjP8DCBmFqeBIKrcuzXeWU7WQL6XruuQyV3QGghEw3YxQpbbsbDtHFgZx +kKO1vQmsym1pUFXV2Drg03FA1oxXCBiRJMbNWSsFZhllKlPpbkV4+IqTMd3u9+JN +Fhk= +-----END CERTIFICATE----- +"#; + +const ISSUER_XYZ_PK: &str = r#"-----BEGIN PRIVATE KEY----- +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDBUPhu/4n3ZMRU +GLToU/0iUbXk6e7yJTvvRJrrn5FFGrulxGcdZaKdmh5ZBiQu15xjXz7IZBmkXiyU +y/4LMznvAC8OBeSut1pZf8D3Jox7PRAMPuTfUh9C9qBMFhDj+pXOd/fHy4JgJ22r +DQQsCLV8N0JAkBEEvRnFGDeriSWIReUbluRVblgR2jeVXwkvGeiNcrlbE3+zAPKX +4JXmLJYgfFwjjZyvCo7LP/tqLdR31bxpjtrpY3VjbpsqMh3qiLhsfzxBwy3vQCBz +Q77thjUU77Ixrhp0dkY1DonTDuIxvvMNLZkX+EgonuGgtNwolWoOb4FNKJINdYW8 +JtknVnT/AgMBAAECggEAI06ICTHHv3TafKeEhvgr/9Qfnf7xwqz1PNZxWv5qOE5R +Hk34LUDOSe2HhGfgPOPpLqcLHutVWZVPnB+DtwT0nEeS0INYCGb5a+Yu1pTmTG3T +HAFyqKzlg8Pqc+sFy7WNHcgAa6+qEKiy2W5HqEkw6E5pXcRSL2TgK4SoSj6CqdgU +2O44I1FD4+zQNJHWKnKGAHr4ZyTdqLOCogWPH0cNHmF3o8sXDDnuRNn94BN/9EE+ +QHEkbxqVoOUmuVSqyj2FNThQmyth4LMTQbGOqBisokHyDdfCxXrXa2IRVtWGNZKb +u73LCTqjUqiV1I/oYnoSL3qDFIX9fQt8CaZc8kKZzQKBgQDmHJk747QUyOs0hSjg +qYz2OfR7RsrX0N/hgi2AaUPyqD1pagCEWAJioB/RY42nsAqW4n27z/BLntUTgKzY +xLO9Rcu+xAk5QOYiTnOSHyqtcYHDYfTdqj8QlJBD5L3whsWLs5u40wSCamEnEEP/ +yQXvpRYFsOisZ6FxA4EkDz9d0wKBgQDXEKJftDPeap1qRatQpk1BT5GvuGWDEtt/ +AZHSr9x4nvStzt6pEw01naP2AnJyh+z4mUqdP9ycNsWePL6/OFNfbBfQGfa3m1QE +DT94FmUYjVsKdaR/6PgAE/n0FRLYCxgsju56lMG2oxdm+lu7Hi3h9X+2TCP26Cm3 +Kyt5hBuUpQKBgCWSs96YjpX6PJWFzKfqZ27pBad/Zq2YmIXM6LsX9RVRLT3iJhV1 +/WW3OZrKA55G22PJtbgN+vGODMKzdwWqEKMuG1eZ0Nu9YhZl8u8AEcriRsrtWrs7 +CjMd3vSHDBCTIPxvplSmeA0Ha7eiK0g/va1kqlThjwxfm3fFl7eYmcMrAoGAU2Q7 +0ehKBIBKsZ56IzeY1S5JS12w3vY87i5poMxYLN1V+t8wL1cX1CZgIVApmIdkN7EY +4YiVvmzui8D6JaNtkJ6VTTgEFoXAOiH47lOgt7h4CMI9Guv23fDhBuf0+piysvRp +PFbaFjt/I4sIbrHxEKDk+IblmyPCqSewH9o955ECgYEAyX4UO6jcBCrSVGiMBnBm +RU1EcAdWxAwu4LRvhxJFBGBMHI36ECrMmPmbVvyzTfGSQfLo8Ov2z0T3Ec2jR86V +0Wu4jn4vxDVj4OD+oGScc3JuaVQJ5Mj7KP2HdXw9Z7pQ9LQYCzmwLoWOq3iYDrsJ +RcWUqLOFB3faKiFxfcjnOrU= +-----END PRIVATE KEY----- +"#; + +const ISSUER_123_CERT: &str = r#"-----BEGIN CERTIFICATE----- +MIIDLjCCAhagAwIBAgIUKcxy6IK4xfMTqdlbvTi8zvDgaz0wDQYJKoZIhvcNAQEL +BQAwGDEWMBQGA1UEAwwNSXNzdWVyIDEyMyBDQTAeFw0yNjA0MDMxMjQ4MDZaFw0y +NzA0MDMxMjQ4MDZaMBgxFjAUBgNVBAMMDUlzc3VlciAxMjMgQ0EwggEiMA0GCSqG +SIb3DQEBAQUAA4IBDwAwggEKAoIBAQCnXWNMF4m8e2VPVlM8DWuaZPaBx1vqWQCG +1MjInefh5FIAtimCl43cRd7Iolb8k+qdWm5xfWaOsh6fDVJqF9DLXweVOk58tYMr +Dg2buT7W5SfUS1fcg+rdv/0IRQc9LV9l4sLsW9L1Lv/2nltDnEVwypOxyFLHQdhb +SdCxQky8oPWkb6BQlK4Tths0iwY+4vQDpzv8WRY0h0nlLN54OQcCeOtUUjkEu6L5 +6goUM8SVlH1aQmBPXEIaUXKa36IevRirLAXw7d6BLijWwMJU2EXvrl+xVgomf0Md +pRRLaPjLS5vPnoJ7ZbQOA3AMKoIwwXE50xckirINUBXK7jocwXBzAgMBAAGjcDBu +MB0GA1UdDgQWBBRKWPP6BQ3xNVaJ6Rjjy5E27bUW9TAfBgNVHSMEGDAWgBRKWPP6 +BQ3xNVaJ6Rjjy5E27bUW9TAPBgNVHRMBAf8EBTADAQH/MBsGA1UdEQQUMBKCEG1h +aWwuZXhhbXBsZS5vcmcwDQYJKoZIhvcNAQELBQADggEBAI6UqptJYfQ5bWn3SAU1 +sT2GTaOlQDPHI08v3fAGomzL5COsx9WgdgBuO3yjcNPXrlyTCdpXLVgQDeIbfoHf +B2FLSBXgtkDFCwTX5P+D4odif7zt8Fr0Zbgo7NEi+TJvQRron3bvbY78JUOujIj2 +MNJRntcl1cp7aZSNaNhCogFxY5t01fqJuaW//QdfApvOXB6mWjONGo5p01jkK2HA +AoiHdZMGGnTdb2Usx6ZkENwgwKHY3TMXMQvUysH5STUrw4/eBfNLnCZgK8GRK8nC +kqPwO96ZdZ9EmzOMxUCCVrFyXwThnNu7aHbWKDq1KQsRvHMWjCwn0gxoqZixoA8K +gpg= +-----END CERTIFICATE----- +"#; + +const ISSUER_123_PK: &str = r#"-----BEGIN PRIVATE KEY----- +MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQCnXWNMF4m8e2VP +VlM8DWuaZPaBx1vqWQCG1MjInefh5FIAtimCl43cRd7Iolb8k+qdWm5xfWaOsh6f +DVJqF9DLXweVOk58tYMrDg2buT7W5SfUS1fcg+rdv/0IRQc9LV9l4sLsW9L1Lv/2 +nltDnEVwypOxyFLHQdhbSdCxQky8oPWkb6BQlK4Tths0iwY+4vQDpzv8WRY0h0nl +LN54OQcCeOtUUjkEu6L56goUM8SVlH1aQmBPXEIaUXKa36IevRirLAXw7d6BLijW +wMJU2EXvrl+xVgomf0MdpRRLaPjLS5vPnoJ7ZbQOA3AMKoIwwXE50xckirINUBXK +7jocwXBzAgMBAAECggEAEre5jr2FLHy6cFyNs4Ymth47iXkj2Po6Oloa2ID8eYGB +AE5fJxlPBscTqumpA9eBIjcGag/lw+i8zvn88RuXwN7waKqcM4tTT6HjOLrHgd4g +Dbida26fekxp/p5FuHxWEn2BYlDEr6Ihg1HjkBSumXY2fWgThfBhw5fmTKTHE0NY +c2Q57UOw94lkPCPrmP25wEh+4DwUNIuV3wKQq9t8Qq5XkOlFd9V4957mz9qLH4G3 +oXUU/yQToEKPbA62WqAUbE26nnJ03Xr+r2+waDR+VfaNtAWKdoQAtgI7Ge02obHZ +jO5T8/pFVchJxNjKKRp2hdPYn6y7JZOAsXqEt5eawQKBgQDr6ps/AW4WyK6o2g/t +NstPtXkBSLrwesrpg7AiEUh3eqximjtOF7ksvJ1kqhkzTvVfB2/h8EO8eJ4SsxLD +VsHUTduWXyASeh392RuMsvHigfTi3//lsaYSSRBEsruZAXjqjWtAh5Ap76F2BpcB +wQNCb1D6NyILshMyepXnEGNbswKBgQC1nND2rSLYfpY4X3hUiI+yNMef6b1AlJAg +X4Qd4xVetrPrTO4Fn8UVgUFQiDdZMsDjbg7idyh975UIq5kp1AikfRfAXilCw+uT +ZBvu2xjScls2fScu4Vn1wBky/n83NLx8zi0CepN2qYvjPpNYIlJ2Ych+SWCoXxeT +fPCZzqg4QQKBgQCKdecn028JcD8SWul+D+rDnX6ngkg1W9w7sU5usDYX6afDN0IX +U1UbLJgzvKGNu4nHfFXuBVW8CA6+attYSlL4h7mZR7tLHOD9W68PpPbSOfPANDe8 +V8dgdAFYUI5J/tM41kdcWDQEaOAapUN7hAylsS+Vq0YQFzOtLMVOGBA4gQKBgQCj +COjqWVkrws/2QXZTZNii4RDH9NwpanTMKxL+hYn8ocV4mXIf6GLTwFozAmW1lINm +Z7nDAbd+/qHqy6lOzIMJryawUZd20Uzc3wTYcyWgXnqVutp/Elxg6hd1GNR5acU/ +wRLU49cXsnLbCKTbfMxMa9HB1PuJivwuMf4IBWYsQQKBgQC5KNAWEHWrnxiNeCS0 +4VzOCTCUEQ+Axq7g5bFKzJqRfHDXlDsNaiK6q4vGDp4HjFawPoyrmnR5/OE/O8PV +0OstcrM2EBik9YbORVpAJ2yl90ZTKevSxQ9+n2Ip/pwLz/oRywxF/dYYTbwYReO5 +9MBumBf1lgiJZSsloOKWQvLchg== +-----END PRIVATE KEY----- +"#; diff --git a/tests/src/automation/dkim.rs b/tests/src/automation/dkim.rs index cb20bb8e..a57cc287 100644 --- a/tests/src/automation/dkim.rs +++ b/tests/src/automation/dkim.rs @@ -4,8 +4,359 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use crate::utils::server::TestServer; +use crate::utils::{account::Account, server::TestServer}; +use ahash::AHashSet; +use common::{config::smtp::auth::DkimSigner, network::dns::update::DNS_RECORDS}; +use dns_update::{DnsRecord, NamedDnsRecord}; +use registry::{ + schema::{ + enums::DkimRotationStage, + prelude::ObjectType, + structs::{ + CertificateManagement, Dkim1Signature, DkimManagement, DkimManagementProperties, + DkimSignature, DnsManagement, DnsManagementProperties, DnsServer, DnsServerCloudflare, + Domain, SecretKey, SecretKeyValue, + }, + }, + types::duration::Duration, +}; +use store::write::now; pub async fn test(test: &TestServer) { println!("Running DKIM Management tests..."); + let account = test.account("admin@example.org"); + DNS_RECORDS.lock().unwrap().clear(); + + // Create test In Memory DNS servers + let dns_server_id = account + .registry_create_object(DnsServer::Cloudflare(DnsServerCloudflare { + email: "test@memory.org".to_string().into(), + secret: SecretKey::Value(SecretKeyValue { + secret: "secret".into(), + }), + ..Default::default() + })) + .await; + account.dkim_signatures().await.assert_total(0, 0); + + // Create a domain and trigger DKIM key generation + let now = now(); + let selector_rsa = format!("dummy-v1-rsa-{}", now); + let selector_ed = format!("dummy-v1-ed25519-{}", now); + let domain_id = account + .registry_create_object(Domain { + name: "dkim.org".to_string(), + certificate_management: CertificateManagement::Manual, + dkim_management: DkimManagement::Automatic(DkimManagementProperties { + delete_after: Duration::from_millis(2_000), + retire_after: Duration::from_millis(2_000), + rotate_after: Duration::from_millis(2_000), + selector_template: "dummy-v{version}-{algorithm}-{epoch}".to_string(), + ..Default::default() + }), + dns_management: DnsManagement::Automatic(DnsManagementProperties { + dns_server_id, + ..Default::default() + }), + ..Default::default() + }) + .await; + + // Make sure two DKIM keys were created + let rot1_signatures = account + .wait_for_dkim_signatures(&DkimSignatures::default(), 2) + .await + .assert_total(1, 1) + .assert_stage_count(DkimRotationStage::Active, 2); + assert_eq!( + rot1_signatures.v1_rsa[0].selector, selector_rsa, + "Unexpected RSA selector: {}", + rot1_signatures.v1_rsa[0].selector + ); + assert_eq!( + rot1_signatures.v1_ed25519[0].selector, selector_ed, + "Unexpected Ed25519 selector: {}", + rot1_signatures.v1_ed25519[0].selector + ); + assert_eq!( + rot1_signatures.v1_rsa[0] + .next_transition_at + .unwrap() + .timestamp() + - rot1_signatures.v1_rsa[0].created_at.timestamp(), + 2 + ); + test.assert_has_signers( + "dkim.org", + &[ + &rot1_signatures.v1_rsa[0].selector, + &rot1_signatures.v1_ed25519[0].selector, + ], + ) + .await; + + // Make sure the DNS records were created + let records = DNS_RECORDS.lock().unwrap().clone(); + assert_key_has_dns_record(&records, &rot1_signatures.v1_rsa[0]); + assert_key_has_dns_record(&records, &rot1_signatures.v1_ed25519[0]); + + // Expect a rotation to happen and new keys to be created + let rot2_signatures = account + .wait_for_dkim_signatures(&rot1_signatures, 4) + .await + .assert_total(2, 2) + .assert_stage_count(DkimRotationStage::Active, 2) + .assert_stage_count(DkimRotationStage::Retiring, 2); + + // Make sure both old and new keys have DNS records + let records = DNS_RECORDS.lock().unwrap().clone(); + assert_key_has_dns_record(&records, &rot1_signatures.v1_rsa[0]); + assert_key_has_dns_record(&records, &rot1_signatures.v1_ed25519[0]); + assert_key_has_dns_record(&records, &rot2_signatures.v1_rsa[0]); + assert_key_has_dns_record(&records, &rot2_signatures.v1_ed25519[0]); + + // Make sure only the new keys are being used for signing + assert_ne!( + rot1_signatures.v1_rsa[0].selector, rot2_signatures.v1_rsa[0].selector, + "Expected a new RSA selector to be generated during rotation" + ); + assert_ne!( + rot1_signatures.v1_ed25519[0].selector, rot2_signatures.v1_ed25519[0].selector, + "Expected a new Ed25519 selector to be generated during rotation" + ); + test.assert_has_signers( + "dkim.org", + &[ + &rot2_signatures.v1_rsa[0].selector, + &rot2_signatures.v1_ed25519[0].selector, + ], + ) + .await; + + // Wait until the previous key is retired + let rot3_signatures = account + .wait_for_dkim_signatures(&rot2_signatures, 6) + .await + .assert_total(3, 3) + .assert_stage_count(DkimRotationStage::Active, 2) + .assert_stage_count(DkimRotationStage::Retiring, 2) + .assert_stage_count(DkimRotationStage::Retired, 2); + + // Make sure the old records were deleted + let records = DNS_RECORDS.lock().unwrap().clone(); + assert_key_has_no_dns_record(&records, &rot1_signatures.v1_rsa[0]); + assert_key_has_no_dns_record(&records, &rot1_signatures.v1_ed25519[0]); + assert_key_has_dns_record(&records, &rot2_signatures.v1_rsa[0]); + assert_key_has_dns_record(&records, &rot2_signatures.v1_ed25519[0]); + assert_key_has_dns_record(&records, &rot3_signatures.v1_rsa[0]); + assert_key_has_dns_record(&records, &rot3_signatures.v1_ed25519[0]); + + // Make sure only the new keys are being used for signing + assert_ne!( + rot2_signatures.v1_rsa[0].selector, rot3_signatures.v1_rsa[0].selector, + "Expected a new RSA selector to be generated during rotation" + ); + assert_ne!( + rot2_signatures.v1_ed25519[0].selector, rot3_signatures.v1_ed25519[0].selector, + "Expected a new Ed25519 selector to be generated during rotation" + ); + test.assert_has_signers( + "dkim.org", + &[ + &rot3_signatures.v1_rsa[0].selector, + &rot3_signatures.v1_ed25519[0].selector, + ], + ) + .await; + + // Wait until the first key is deleted + let rot4_signatures = account + .wait_for_dkim_signatures(&rot3_signatures, 6) + .await + .assert_total(3, 3) + .assert_stage_count(DkimRotationStage::Active, 2) + .assert_stage_count(DkimRotationStage::Retiring, 2) + .assert_stage_count(DkimRotationStage::Retired, 2) + .assert_selector_missing(&rot1_signatures.v1_rsa[0].selector) + .assert_selector_missing(&rot1_signatures.v1_ed25519[0].selector); + + // Make sure the old records were updated + let records = DNS_RECORDS.lock().unwrap().clone(); + assert_key_has_dns_record(&records, &rot4_signatures.v1_rsa[0]); + assert_key_has_dns_record(&records, &rot4_signatures.v1_ed25519[0]); + assert_key_has_no_dns_record(&records, &rot2_signatures.v1_rsa[0]); + assert_key_has_no_dns_record(&records, &rot2_signatures.v1_ed25519[0]); + + // Make sure only the new keys are being used for signing + assert_ne!( + rot3_signatures.v1_rsa[0].selector, rot4_signatures.v1_rsa[0].selector, + "Expected a new RSA selector to be generated during rotation" + ); + assert_ne!( + rot3_signatures.v1_ed25519[0].selector, rot4_signatures.v1_ed25519[0].selector, + "Expected a new Ed25519 selector to be generated during rotation" + ); + test.assert_has_signers( + "dkim.org", + &[ + &rot4_signatures.v1_rsa[0].selector, + &rot4_signatures.v1_ed25519[0].selector, + ], + ) + .await; + + // Cleanup + account + .registry_destroy_all(ObjectType::DkimSignature) + .await; + account + .registry_destroy(ObjectType::Domain, [domain_id]) + .await + .assert_destroyed(&[domain_id]); + account.registry_destroy_all(ObjectType::DnsServer).await; +} + +#[derive(Debug, PartialEq, Eq, Default)] +struct DkimSignatures { + v1_rsa: Vec, + v1_ed25519: Vec, +} + +impl Account { + async fn wait_for_dkim_signatures( + &self, + last_signatures: &DkimSignatures, + expected_total: usize, + ) -> DkimSignatures { + let mut signatures = self.dkim_signatures().await; + for _ in 0..10 { + if signatures != *last_signatures + && signatures.v1_rsa.len() + signatures.v1_ed25519.len() == expected_total + { + return signatures; + } + tokio::time::sleep(std::time::Duration::from_millis(400)).await; + signatures = self.dkim_signatures().await; + } + panic!( + "DKIM signatures did not change after waiting (total {}, expected {}): {:#?}", + signatures.v1_rsa.len() + signatures.v1_ed25519.len(), + expected_total, + signatures + ); + } + + async fn dkim_signatures(&self) -> DkimSignatures { + let signatures = self.registry_get_all::().await; + let mut v1_rsa = Vec::new(); + let mut v1_ed25519 = Vec::new(); + for (_, signature) in signatures { + match signature { + DkimSignature::Dkim1RsaSha256(sig) => v1_rsa.push(sig), + DkimSignature::Dkim1Ed25519Sha256(sig) => v1_ed25519.push(sig), + } + } + + // Sort in descending order of creation time + v1_rsa.sort_by_key(|s| std::cmp::Reverse(s.created_at)); + v1_ed25519.sort_by_key(|s| std::cmp::Reverse(s.created_at)); + + DkimSignatures { v1_rsa, v1_ed25519 } + } +} + +impl DkimSignatures { + fn assert_stage_count(self, stage: DkimRotationStage, count: usize) -> Self { + let actual_count = self.v1_rsa.iter().filter(|s| s.stage == stage).count() + + self.v1_ed25519.iter().filter(|s| s.stage == stage).count(); + assert_eq!( + actual_count, count, + "Expected {} signatures in stage {:?}, found {}: {:#?}", + count, stage, actual_count, self + ); + self + } + + fn assert_total(self, total_rsa: usize, total_ed25519: usize) -> Self { + assert_eq!( + self.v1_rsa.len(), + total_rsa, + "Expected {} RSA signatures, found {:?}", + total_rsa, + self.v1_rsa + ); + assert_eq!( + self.v1_ed25519.len(), + total_ed25519, + "Expected {} Ed25519 signatures, found {:?}", + total_ed25519, + self.v1_ed25519 + ); + self + } + + fn assert_selector_missing(self, selector: &str) -> Self { + assert!( + !self.v1_rsa.iter().any(|s| s.selector == selector) + && !self.v1_ed25519.iter().any(|s| s.selector == selector), + "Selector {} was unexpectedly found in signatures: {:#?}", + selector, + self + ); + self + } +} + +impl TestServer { + async fn assert_has_signers(&self, domain: &str, selectors: &[&str]) { + assert_eq!( + self.server + .dkim_signers(domain) + .await + .unwrap() + .unwrap_or_else(|| panic!("No signatures found: {:?}", selectors)) + .iter() + .map(|s| match s { + DkimSigner::RsaSha256(s) => s.template.s.as_str(), + DkimSigner::Ed25519Sha256(s) => s.template.s.as_str(), + }) + .collect::>(), + selectors.iter().copied().collect::>() + ); + } +} + +fn assert_key_has_dns_record(records: &[NamedDnsRecord], key: &Dkim1Signature) { + let expected = format!("{}._domainkey.dkim.org.", key.selector); + for record in records { + if record.name == expected + && let DnsRecord::TXT(txt) = &record.record + && ((key.selector.contains("rsa") && txt.starts_with("v=DKIM1; k=rsa; h=sha256; p=")) + || (key.selector.contains("ed25519") + && txt.starts_with("v=DKIM1; k=ed25519; h=sha256; p="))) + { + return; + } + } + panic!( + "No DNS record found for DKIM key with selector {}, records: {:#?}", + key.selector, records + ); +} + +fn assert_key_has_no_dns_record(records: &[NamedDnsRecord], key: &Dkim1Signature) { + let expected = format!("{}._domainkey.dkim.org.", key.selector); + for record in records { + if record.name == expected + && let DnsRecord::TXT(txt) = &record.record + && ((key.selector.contains("rsa") && txt.starts_with("v=DKIM1; k=rsa; h=sha256; p=")) + || (key.selector.contains("ed25519") + && txt.starts_with("v=DKIM1; k=ed25519; h=sha256; p="))) + { + panic!( + "Unexpected DNS record found for DKIM key with selector {}, records: {:#?}", + key.selector, records + ); + } + } } diff --git a/tests/src/automation/dns.rs b/tests/src/automation/dns.rs index 0a83fce4..f9fb1237 100644 --- a/tests/src/automation/dns.rs +++ b/tests/src/automation/dns.rs @@ -5,7 +5,195 @@ */ use crate::utils::server::TestServer; +use common::network::dns::update::DNS_RECORDS; +use dns_update::{CAARecord, bind::BindSerializer}; +use registry::{ + schema::{ + enums::{AcmeChallengeType, DnsRecordType}, + prelude::{ObjectType, Property}, + structs::{ + AcmeProvider, CertificateManagement, CertificateManagementProperties, DkimManagement, + DkimManagementProperties, DnsManagement, DnsManagementProperties, DnsServer, + DnsServerCloudflare, SecretKey, SecretKeyValue, + }, + }, + types::map::Map, +}; +use serde_json::json; + +const EXPECTED_ZONE: &str = r#"example.org. IN CAA 0 iodef "mailto:postmaster@example.org" +example.org. IN CAA 0 issue "pebble.letsencrypt.org" +autoconfig.example.org. IN CNAME mail.example.org. +autodiscover.example.org. IN CNAME mail.example.org. +mta-sts.example.org. IN CNAME mail.example.org. +ua-auto-config.example.org. IN CNAME mail.example.org. +example.org. IN MX 10 mx1.example.org. +example.org. IN MX 20 mx2.example.org. +_caldavs._tcp.example.org. IN SRV 0 1 443 mail.example.org. +_carddavs._tcp.example.org. IN SRV 0 1 443 mail.example.org. +_imaps._tcp.example.org. IN SRV 0 1 993 imap.example.org. +_jmap._tcp.example.org. IN SRV 0 1 443 mail.example.org. +_pop3s._tcp.example.org. IN SRV 0 1 995 pop3.example.org. +_submissions._tcp.example.org. IN SRV 0 1 465 smtp.example.org. +_25._tcp.mx1.example.org. IN TLSA 3 1 1 +_25._tcp.mx1.example.org. IN TLSA 2 1 1 +_25._tcp.mx2.example.org. IN TLSA 3 1 1 +_25._tcp.mx2.example.org. IN TLSA 2 1 1 +_443._tcp.autoconfig.example.org. IN TLSA 3 1 1 +_443._tcp.autoconfig.example.org. IN TLSA 2 1 1 +_443._tcp.autodiscover.example.org. IN TLSA 3 1 1 +_443._tcp.autodiscover.example.org. IN TLSA 2 1 1 +_443._tcp.mail.example.org. IN TLSA 3 1 1 +_443._tcp.mail.example.org. IN TLSA 2 1 1 +_443._tcp.mta-sts.example.org. IN TLSA 3 1 1 +_443._tcp.mta-sts.example.org. IN TLSA 2 1 1 +_443._tcp.ua-auto-config.example.org. IN TLSA 3 1 1 +_443._tcp.ua-auto-config.example.org. IN TLSA 2 1 1 +_465._tcp.smtp.example.org. IN TLSA 3 1 1 +_465._tcp.smtp.example.org. IN TLSA 2 1 1 +_993._tcp.imap.example.org. IN TLSA 3 1 1 +_993._tcp.imap.example.org. IN TLSA 2 1 1 +_995._tcp.pop3.example.org. IN TLSA 3 1 1 +_995._tcp.pop3.example.org. IN TLSA 2 1 1 +_dmarc.example.org. IN TXT "v=DMARC1; p=reject; rua=mailto:postmaster@example.org" +_mta-sts.example.org. IN TXT "v=STSv1; id=12942536112359691423" +_smtp._tls.example.org. IN TXT "v=TLSRPTv1; rua=mailto:postmaster@example.org" +_ua-auto-config.example.org. IN TXT "v=UAAC1; a=sha256; d=9ZXAG5NGLnhdx4IQKuA2lLGrVXD3N/UKlo059TEi9zY=" +_validation-persist.example.org. IN TXT "pebble.letsencrypt.org; accounturi=REDACTED" +dummy-v1-ed25519._domainkey.example.org. IN TXT "v=DKIM1; k=ed25519; h=sha256; p=REDACTED" +dummy-v1-rsa._domainkey.example.org. IN TXT "v=DKIM1; k=rsa; h=sha256; p=REDACTED" +example.org. IN TXT "v=spf1 mx -all" +mx1.example.org. IN TXT "v=spf1 a -all" +mx2.example.org. IN TXT "v=spf1 a -all" +"#; pub async fn test(test: &TestServer) { println!("Running DNS Management tests..."); + let account = test.account("admin@example.org"); + DNS_RECORDS.lock().unwrap().clear(); + let domain_id = account.find_or_create_domain("example.org").await; + account + .registry_update_object( + ObjectType::Domain, + domain_id, + json!({ + Property::CertificateManagement: CertificateManagement::Manual, + Property::DnsManagement: DnsManagement::Manual, + Property::DkimManagement: DkimManagement::Manual, + }), + ) + .await; + + // Create test In Memory DNS servers + let dns_server_id = account + .registry_create_object(DnsServer::Cloudflare(DnsServerCloudflare { + email: "test@memory.org".to_string().into(), + secret: SecretKey::Value(SecretKeyValue { + secret: "secret".into(), + }), + ..Default::default() + })) + .await; + let acme_provider_id = account + .registry_create_object(AcmeProvider { + directory: "https://localhost:14000/dir".to_string(), + contact: Map::new(vec!["mailto:hello@example.org".to_string()]), + challenge_type: AcmeChallengeType::TlsAlpn01, + ..Default::default() + }) + .await; + + let cert = CertificateManagement::Automatic(CertificateManagementProperties { + acme_provider_id, + subject_alternative_names: Default::default(), + }); + let dns = DnsManagement::Automatic(DnsManagementProperties { + dns_server_id, + publish_records: Map::new(vec![ + DnsRecordType::Dkim, + DnsRecordType::Tlsa, + DnsRecordType::Spf, + DnsRecordType::Mx, + DnsRecordType::Dmarc, + DnsRecordType::Srv, + DnsRecordType::MtaSts, + DnsRecordType::TlsRpt, + DnsRecordType::Caa, + DnsRecordType::AutoConfig, + DnsRecordType::AutoConfigLegacy, + DnsRecordType::AutoDiscover, + ]), + ..Default::default() + }); + let dkim = DkimManagement::Automatic(DkimManagementProperties { + selector_template: "dummy-v{version}-{algorithm}-{epoch}".to_string(), + ..Default::default() + }); + account + .registry_update_object( + ObjectType::Domain, + domain_id, + json!({ + Property::CertificateManagement: cert, + Property::DnsManagement: dns, + Property::DkimManagement: dkim, + }), + ) + .await; + test.wait_for_tasks_skip_not_due().await; + + let mut records = DNS_RECORDS + .lock() + .unwrap() + .iter() + .map(|r| { + let mut r = r.clone(); + + if r.name.starts_with("dummy-") { + let (selector, domain) = r.name.split_once("._domainkey.").unwrap(); + let selector = selector.rsplit_once('-').unwrap().0; + r.name = format!("{selector}._domainkey.{domain}"); + } + + match &mut r.record { + dns_update::DnsRecord::TXT(r) => { + if r.starts_with("v=DKIM1;") { + *r = r.split_once("; p=").unwrap().0.to_string() + "; p=REDACTED"; + } else if r.contains("letsencrypt.org") { + *r = r.split_once("; accounturi=").unwrap().0.to_string() + + "; accounturi=REDACTED"; + } + } + dns_update::DnsRecord::TLSA(r) => { + r.cert_data.clear(); + } + dns_update::DnsRecord::CAA(CAARecord::Issue { options, .. }) => { + options.clear(); + } + _ => (), + } + r + }) + .collect::>(); + + records.sort_unstable_by_key(|r| format!("{:?}-{}-{:?}", r.record.as_type(), r.name, r.record)); + + assert_eq!(BindSerializer::serialize(&records), EXPECTED_ZONE); + + // Cleanup + account + .registry_update_object( + ObjectType::Domain, + domain_id, + json!({ + Property::CertificateManagement: CertificateManagement::Manual, + Property::DnsManagement: DnsManagement::Manual, + Property::DkimManagement: DkimManagement::Manual, + }), + ) + .await; + account.registry_destroy_all(ObjectType::DnsServer).await; + account.registry_destroy_all(ObjectType::AcmeProvider).await; + account.registry_destroy_all(ObjectType::Certificate).await; + account.registry_destroy_all(ObjectType::Task).await; } diff --git a/tests/src/automation/mod.rs b/tests/src/automation/mod.rs index 47e7b10e..d0f0e9d1 100644 --- a/tests/src/automation/mod.rs +++ b/tests/src/automation/mod.rs @@ -113,5 +113,7 @@ async fn automation_tests() { account.reload_settings().await; test.insert_account(account); - acme::test(&test).await; + //acme::test(&test).await; + //dkim::test(&test).await; + dns::test(&test).await; }