Import upstream v0.16.22, stripped

Upstream commit: 474dd0229cb20cf513036619781ed97bd8073c3f
Enterprise-only files removed or emptied: 63
Enterprise-only snippets removed: 117 in 50 files
Dangling module declarations removed: 5
Cargo edits turning enterprise off: 14
Verification: clean
Enterprise feature gates left for rebuilt features: 19 in 18 files

Produced by tools/fork/strip.py. The full report is in docs/fork/strip-reports/ on main.
This commit is contained in:
2026-09-18 10:21:56 -07:00
commit 7dae9b29fd
1650 changed files with 485521 additions and 0 deletions
+871
View File
@@ -0,0 +1,871 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
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;
use registry::{
schema::{
enums::{AcmeChallengeType, AcmeRenewBefore, DnsRecordType},
prelude::{ObjectType, Property},
structs::{
AcmeProvider, Action, Certificate, CertificateManagement,
CertificateManagementProperties, DkimManagement, DnsManagement,
DnsManagementProperties, DnsServer, DnsServerCloudflare, Domain, PublicText,
PublicTextValue, SecretKey, SecretKeyValue, SecretText, SecretTextValue, Task,
TaskDomainManagement,
},
},
types::{datetime::UTCDateTime, id::ObjectId, map::Map},
};
use serde_json::json;
use store::{registry::write::RegistryWrite, write::now};
use x509_parser::parse_x509_certificate;
use x509_parser::pem::Pem;
pub async fn test(test: &TestServer) {
println!("Running ACME tests...");
let account = test.account("[email protected]");
// 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::<Certificate>()
.await
.into_iter()
.map(|(_, c)| c.issuer)
.collect::<AHashSet<_>>(),
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::<Certificate>()
.await
.into_iter()
.map(|(_, c)| c.issuer)
.collect::<AHashSet<_>>(),
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 {
secret: SecretKey::Value(SecretKeyValue {
secret: "[email protected]".into(),
}),
description: "Pebble DNS server".to_string(),
..Default::default()
}))
.await;
let in_memory_dns_id = account
.registry_create_object(DnsServer::Cloudflare(DnsServerCloudflare {
secret: SecretKey::Value(SecretKeyValue {
secret: "[email protected]".into(),
}),
description: "In-memory DNS server".to_string(),
..Default::default()
}))
.await;
// ACME provider creation should fail without a contact email
account
.registry_create_object_expect_err(AcmeProvider {
directory: "https://localhost:14000/dir".to_string(),
..Default::default()
})
.await
.assert_type(SetErrorType::InvalidProperties)
.assert_description_contains("At least one contact email is required");
// Create an ACME provider using TLS-ALPN-01 challenge
let tls_acme_id = account
.registry_create_object(AcmeProvider {
directory: "https://localhost:14000/dir".to_string(),
contact: Map::new(vec!["mailto:[email protected]".to_string()]),
challenge_type: AcmeChallengeType::TlsAlpn01,
renew_before: AcmeRenewBefore::R12,
..Default::default()
})
.await;
let provider = account.registry_get::<AcmeProvider>(tls_acme_id).await;
assert_eq!(provider.directory, "https://localhost:14000/dir");
assert_eq!(
provider.contact,
Map::new(vec!["mailto:[email protected]".to_string()])
);
assert!(
provider.account_uri.starts_with("https://localhost:14000"),
"Provider {:?} has invalid account URI",
provider
);
// Create a domain and trigger TLS-ALPN-01 ACME renewal
let tls_domain_id = account
.registry_create_object(Domain {
name: "tls.org".to_string(),
certificate_management: CertificateManagement::Automatic(
CertificateManagementProperties {
acme_provider_id: tls_acme_id,
subject_alternative_names: Default::default(),
},
),
dkim_management: DkimManagement::Manual,
dns_management: DnsManagement::Automatic(DnsManagementProperties {
dns_server_id: in_memory_dns_id,
publish_records: Map::new(vec![
DnsRecordType::Tlsa,
DnsRecordType::AutoConfig,
DnsRecordType::AutoConfigLegacy,
DnsRecordType::AutoDiscover,
DnsRecordType::MtaSts,
]),
..Default::default()
}),
..Default::default()
})
.await;
test.wait_for_tasks_skip_not_due().await;
let certificate = account
.registry_get_all::<Certificate>()
.await
.into_iter()
.next()
.unwrap()
.1;
let mut sans = certificate.subject_alternative_names.into_inner();
sans.sort();
assert_eq!(
sans,
vec![
"autoconfig.tls.org".to_string(),
"autodiscover.tls.org".to_string(),
"mta-sts.tls.org".to_string(),
"ua-auto-config.tls.org".to_string()
]
);
// Make sure the TLSA records were added to the in-memory DNS server
let records = DNS_RECORDS.lock().unwrap().clone();
for record in [
"_443._tcp.mta-sts.tls.org.",
"_443._tcp.autoconfig.tls.org.",
"_443._tcp.ua-auto-config.tls.org.",
"_443._tcp.autodiscover.tls.org.",
] {
if records
.iter()
.find(|r| r.name == record && matches!(r.record, DnsRecord::TLSA(_)))
.is_none()
{
panic!(
"Expected TLSA record for {} not found in DNS records: {:?}",
record, records
);
}
}
// Make sure a task was created to renew the certificate before it expires
let tasks = account.registry_get_all::<Task>().await;
assert_eq!(
tasks.len(),
1,
"Expected 1 task, found {}: {:?}",
tasks.len(),
tasks
);
let task = tasks.into_iter().next().unwrap().1;
if let Task::AcmeRenewal(TaskDomainManagement {
domain_id: task_domain_id,
..
}) = task
{
assert_eq!(
task_domain_id, tls_domain_id,
"ACME renewal task has incorrect domain ID"
);
} else {
panic!("Expected ACME renewal task, found: {:?}", task);
}
let not_valid_after = certificate.not_valid_after.timestamp();
let not_valid_before = certificate.not_valid_before.timestamp();
let length = not_valid_after - not_valid_before;
assert_eq!(
not_valid_before + length / 2,
task.due_timestamp() as i64,
"ACME renewal task has incorrect due timestamp, expected around {} but found {}",
not_valid_before + length / 2,
task.due_timestamp() as i64
);
account.registry_destroy_all(ObjectType::Certificate).await;
account.registry_destroy_all(ObjectType::Task).await;
// Test ACME using HTTP-01 challenge and the server domain "mail.example.org"
let http_acme_id = account
.registry_create_object(AcmeProvider {
directory: "https://localhost:14000/dir".to_string(),
contact: Map::new(vec!["mailto:[email protected]".to_string()]),
challenge_type: AcmeChallengeType::Http01,
..Default::default()
})
.await;
let domain_id = account.find_or_create_domain("example.org").await;
account.registry_update_object(ObjectType::Domain, domain_id, json!({
Property::CertificateManagement: CertificateManagement::Automatic(CertificateManagementProperties {
acme_provider_id: http_acme_id,
subject_alternative_names: Default::default(),
}),
})).await;
test.wait_for_tasks_skip_not_due().await;
let certificate = account
.registry_get_all::<Certificate>()
.await
.into_iter()
.next()
.unwrap()
.1;
let mut sans = certificate.subject_alternative_names.into_inner();
sans.sort();
assert_eq!(
sans,
vec![
"autoconfig.example.org".to_string(),
"autodiscover.example.org".to_string(),
"imap.example.org".to_string(),
"mail.example.org".to_string(),
"mta-sts.example.org".to_string(),
"mx1.example.org".to_string(),
"mx2.example.org".to_string(),
"pop3.example.org".to_string(),
"smtp.example.org".to_string(),
"ua-auto-config.example.org".to_string()
]
);
account.registry_destroy_all(ObjectType::Certificate).await;
account.registry_destroy_all(ObjectType::Task).await;
// Test ACME using DNS-01 challenge
let dns_acme_id = account
.registry_create_object(AcmeProvider {
directory: "https://localhost:14000/dir".to_string(),
contact: Map::new(vec!["mailto:[email protected]".to_string()]),
challenge_type: AcmeChallengeType::Dns01,
..Default::default()
})
.await;
let dns_domain_id = account
.registry_create_object(Domain {
name: "dns.org".to_string(),
certificate_management: CertificateManagement::Automatic(
CertificateManagementProperties {
acme_provider_id: dns_acme_id,
subject_alternative_names: Default::default(),
},
),
dkim_management: DkimManagement::Manual,
dns_management: DnsManagement::Automatic(DnsManagementProperties {
dns_server_id: pebble_dns_id,
publish_records: Map::new(vec![DnsRecordType::Caa]),
..Default::default()
}),
..Default::default()
})
.await;
test.wait_for_tasks_skip_not_due().await;
let certificate = account
.registry_get_all::<Certificate>()
.await
.into_iter()
.next()
.unwrap()
.1;
let mut sans = certificate.subject_alternative_names.into_inner();
sans.sort();
assert_eq!(sans, vec!["*.dns.org".to_string(), "dns.org".to_string()]);
account.registry_destroy_all(ObjectType::Certificate).await;
account.registry_destroy_all(ObjectType::Task).await;
// Test ACME using DNS-01 challenge
let dns_acme_id = account
.registry_create_object(AcmeProvider {
directory: "https://localhost:14000/dir".to_string(),
contact: Map::new(vec!["mailto:[email protected]".to_string()]),
challenge_type: AcmeChallengeType::DnsPersist01,
..Default::default()
})
.await;
let persist_domain_id = account
.registry_create_object(Domain {
name: "persist.org".to_string(),
certificate_management: CertificateManagement::Automatic(
CertificateManagementProperties {
acme_provider_id: dns_acme_id,
subject_alternative_names: Default::default(),
},
),
dkim_management: DkimManagement::Manual,
dns_management: DnsManagement::Automatic(DnsManagementProperties {
dns_server_id: pebble_dns_id,
publish_records: Map::new(vec![DnsRecordType::Caa]),
..Default::default()
}),
..Default::default()
})
.await;
test.wait_for_tasks_skip_not_due().await;
let certificate = account
.registry_get_all::<Certificate>()
.await
.into_iter()
.next()
.unwrap()
.1;
let mut sans = certificate.subject_alternative_names.into_inner();
sans.sort();
assert_eq!(
sans,
vec!["*.persist.org".to_string(), "persist.org".to_string()]
);
// Test preferred chain selection against the alternate chains Pebble offers (RFC 8555 7.4.2)
let pebble_roots = pebble_root_common_names().await;
assert!(
pebble_roots.len() >= 2,
"Expected Pebble to offer multiple root chains, found: {:?}",
pebble_roots
);
// Renew without a preferred chain to discover the default root
let default_acme_id = account
.registry_create_object(AcmeProvider {
directory: "https://localhost:14000/dir".to_string(),
contact: Map::new(vec!["mailto:[email protected]".to_string()]),
challenge_type: AcmeChallengeType::TlsAlpn01,
..Default::default()
})
.await;
let default_domain_id = account
.registry_create_object(Domain {
name: "chain.org".to_string(),
certificate_management: CertificateManagement::Automatic(
CertificateManagementProperties {
acme_provider_id: default_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;
test.wait_for_tasks_skip_not_due().await;
let default_chain = account
.registry_get_all::<Certificate>()
.await
.into_iter()
.next()
.unwrap()
.1
.certificate
.value()
.await
.unwrap()
.into_owned();
let default_root = top_issuer_common_name(&default_chain)
.expect("default chain should expose a top issuer common name");
assert!(
pebble_roots.contains(&default_root),
"Default root {:?} not among Pebble roots {:?}",
default_root,
pebble_roots
);
account.registry_destroy_all(ObjectType::Certificate).await;
account.registry_destroy_all(ObjectType::Task).await;
// Renew with a preferred chain pointing at an alternate root and verify it is honored
let preferred_root = pebble_roots
.iter()
.find(|cn| **cn != default_root)
.cloned()
.expect("an alternate root distinct from the default");
let preferred_acme_id = account
.registry_create_object(AcmeProvider {
directory: "https://localhost:14000/dir".to_string(),
contact: Map::new(vec!["mailto:[email protected]".to_string()]),
challenge_type: AcmeChallengeType::TlsAlpn01,
preferred_chain: Some(preferred_root.clone()),
..Default::default()
})
.await;
let preferred_domain_id = account
.registry_create_object(Domain {
name: "chainalt.org".to_string(),
certificate_management: CertificateManagement::Automatic(
CertificateManagementProperties {
acme_provider_id: preferred_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;
test.wait_for_tasks_skip_not_due().await;
let preferred_chain = account
.registry_get_all::<Certificate>()
.await
.into_iter()
.next()
.unwrap()
.1
.certificate
.value()
.await
.unwrap()
.into_owned();
let selected_root = top_issuer_common_name(&preferred_chain)
.expect("preferred chain should expose a top issuer common name");
assert_eq!(
selected_root, preferred_root,
"ACME did not select the preferred certificate chain"
);
assert_ne!(
selected_root, default_root,
"Preferred chain matches the default; selection was not exercised"
);
account
.registry_destroy(ObjectType::Domain, [default_domain_id, preferred_domain_id])
.await
.assert_destroyed(&[default_domain_id, preferred_domain_id]);
account.registry_destroy_all(ObjectType::Certificate).await;
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:[email protected]".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(
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-----
"#;
async fn pebble_root_common_names() -> Vec<String> {
let client = reqwest::Client::builder()
.danger_accept_invalid_certs(true)
.build()
.expect("Failed to build HTTP client");
let mut common_names = Vec::new();
for index in 0.. {
let response = client
.get(format!("https://localhost:15000/roots/{index}"))
.send()
.await
.expect("Failed to query Pebble management API");
if !response.status().is_success() {
break;
}
let pem = response.text().await.expect("Failed to read Pebble root");
match subject_common_name(&pem) {
Some(common_name) => common_names.push(common_name),
None => break,
}
}
common_names
}
fn subject_common_name(pem: &str) -> Option<String> {
let block = Pem::iter_from_buffer(pem.as_bytes()).next()?.ok()?;
let cert = block.parse_x509().ok()?;
cert.subject()
.iter_common_name()
.filter_map(|cn| cn.as_str().ok())
.next()
.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)
.last()?;
let cert = block.parse_x509().ok()?;
cert.issuer()
.iter_common_name()
.filter_map(|cn| cn.as_str().ok())
.next()
.map(str::to_string)
}
+414
View File
@@ -0,0 +1,414 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use crate::utils::{account::Account, server::TestServer};
use ahash::AHashSet;
use common::{config::smtp::auth::Dkim1Signer, network::dns::update::DNS_RECORDS};
use dns_update::{DnsRecord, NamedDnsRecord};
use registry::{
schema::{
enums::{DkimRotationStage, DnsRecordType},
prelude::ObjectType,
structs::{
CertificateManagement, Dkim1Signature, DkimManagement, DkimManagementProperties,
DkimSignature, DnsManagement, DnsManagementProperties, DnsServer, DnsServerCloudflare,
Domain, SecretKey, SecretKeyValue,
},
},
types::duration::Duration,
};
use store::write::now;
use types::id::Id;
pub async fn test(test: &TestServer) {
println!("Running DKIM Management tests...");
let account = test.account("[email protected]");
DNS_RECORDS.lock().unwrap().clear();
// Create test In Memory DNS servers
let dns_server_id = account
.registry_create_object(DnsServer::Cloudflare(DnsServerCloudflare {
secret: SecretKey::Value(SecretKeyValue {
secret: "[email protected]".into(),
}),
description: "In-memory DNS server".to_string(),
..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 the DNS management task does not republish the retired keys
let (published, zone_file) = test.published_dkim_records(domain_id).await;
assert_key_has_no_dns_record(&published, &rot1_signatures.v1_rsa[0]);
assert_key_has_no_dns_record(&published, &rot1_signatures.v1_ed25519[0]);
assert_key_has_dns_record(&published, &rot3_signatures.v1_rsa[0]);
assert_key_has_dns_record(&published, &rot3_signatures.v1_ed25519[0]);
assert_zone_file_omits_key(&zone_file, &rot1_signatures.v1_rsa[0]);
assert_zone_file_omits_key(&zone_file, &rot1_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 the DNS management task does not republish the retired keys
let (published, zone_file) = test.published_dkim_records(domain_id).await;
assert_key_has_no_dns_record(&published, &rot2_signatures.v1_rsa[0]);
assert_key_has_no_dns_record(&published, &rot2_signatures.v1_ed25519[0]);
assert_key_has_dns_record(&published, &rot4_signatures.v1_rsa[0]);
assert_key_has_dns_record(&published, &rot4_signatures.v1_ed25519[0]);
assert_zone_file_omits_key(&zone_file, &rot2_signatures.v1_rsa[0]);
assert_zone_file_omits_key(&zone_file, &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<Dkim1Signature>,
v1_ed25519: Vec<Dkim1Signature>,
}
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::<DkimSignature>().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),
DkimSignature::Dkim2Ed25519Sha256(_) | DkimSignature::Dkim2RsaSha256(_) => todo!(),
}
}
// 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 published_dkim_records(&self, domain_id: Id) -> (Vec<NamedDnsRecord>, String) {
let domain = self
.server
.registry()
.object::<Domain>(domain_id)
.await
.unwrap()
.expect("Domain not found");
let records = self
.server
.build_dns_records(domain_id, &domain, &[DnsRecordType::Dkim])
.await
.unwrap();
let zone_file = self
.server
.build_bind_dns_records(domain_id, &domain)
.await
.unwrap();
(records, zone_file)
}
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))
.dkim1
.iter()
.map(|s| match s {
Dkim1Signer::RsaSha256(s) => s.template.s.as_str(),
Dkim1Signer::Ed25519Sha256(s) => s.template.s.as_str(),
})
.collect::<AHashSet<_>>(),
selectors.iter().copied().collect::<AHashSet<_>>()
);
}
}
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_zone_file_omits_key(zone_file: &str, key: &Dkim1Signature) {
assert!(
!zone_file.contains(&key.selector),
"Unexpected zone file entry for DKIM key with selector {}, zone file: {}",
key.selector,
zone_file
);
}
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
);
}
}
}
+218
View File
@@ -0,0 +1,218 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
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:[email protected]"
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:[email protected]"
_mta-sts.example.org. IN TXT "v=STSv1; id=12942536112359691423"
_smtp._tls.example.org. IN TXT "v=TLSRPTv1; rua=mailto:[email protected]"
_ua-auto-config.example.org. IN TXT "v=UAAC1; a=sha256; d=9X2mMgWAc10oSPuRKZSFBwPXEQpnxkS7SXPO8PC7euM="
_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("[email protected]");
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 {
secret: SecretKey::Value(SecretKeyValue {
secret: "[email protected]".into(),
}),
description: "In-memory DNS server".to_string(),
..Default::default()
}))
.await;
let acme_provider_id = account
.registry_create_object(AcmeProvider {
directory: "https://localhost:14000/dir".to_string(),
contact: Map::new(vec!["mailto:[email protected]".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::<Vec<_>>();
records.sort_unstable_by_key(|r| format!("{:?}-{}-{:?}", r.record.as_type(), r.name, r.record));
for record in &records {
let target = match &record.record {
dns_update::DnsRecord::CNAME(target) | dns_update::DnsRecord::NS(target) => {
Some(target.as_str())
}
dns_update::DnsRecord::MX(mx) => Some(mx.exchange.as_str()),
dns_update::DnsRecord::SRV(srv) => Some(srv.target.as_str()),
_ => None,
};
if let Some(target) = target {
assert!(
target.ends_with('.'),
"Published target {target:?} for {} is not fully qualified",
record.name
);
}
}
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;
}
+122
View File
@@ -0,0 +1,122 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
pub mod acme;
pub mod dkim;
pub mod dns;
pub mod rfc2136;
use registry::{
schema::{
enums::{NetworkListenerProtocol, ServiceProtocol},
prelude::Property,
structs::{MailExchanger, Service, SystemSettings},
},
types::list::List,
};
use utils::map::vec_map::VecMap;
use crate::utils::server::TestServerBuilder;
#[tokio::test(flavor = "multi_thread")]
async fn automation_tests() {
crate::utils::containers::ensure_acme().await;
let mut test = TestServerBuilder::new("automation_tests")
.await
.with_listener(NetworkListenerProtocol::Http, "http", 8898, false)
.await
.with_default_listeners()
.await
.build()
.await;
// Create admin account
let account = test.create_admin_account("[email protected]").await;
account
.registry_update_setting(
SystemSettings {
mail_exchangers: List::from_iter([
MailExchanger {
priority: 10u64,
hostname: "mx1.example.org".to_string().into(),
},
MailExchanger {
priority: 20u64,
hostname: "mx2.example.org".to_string().into(),
},
]),
services: VecMap::from_iter([
(
ServiceProtocol::Caldav,
Service {
cleartext: false,
..Default::default()
},
),
(
ServiceProtocol::Carddav,
Service {
cleartext: false,
..Default::default()
},
),
(
ServiceProtocol::Imap,
Service {
cleartext: false,
hostname: "imap.example.org".to_string().into(),
},
),
(
ServiceProtocol::Jmap,
Service {
cleartext: false,
..Default::default()
},
),
(
ServiceProtocol::Managesieve,
Service {
cleartext: false,
..Default::default()
},
),
(
ServiceProtocol::Pop3,
Service {
cleartext: false,
hostname: "pop3.example.org".to_string().into(),
},
),
(
ServiceProtocol::Smtp,
Service {
cleartext: false,
hostname: "smtp.example.org".to_string().into(),
},
),
(
ServiceProtocol::Webdav,
Service {
cleartext: false,
..Default::default()
},
),
]),
..Default::default()
},
&[Property::MailExchangers, Property::Services],
)
.await;
account.reload_settings().await;
test.insert_account(account);
acme::test(&test).await;
dkim::test(&test).await;
dns::test(&test).await;
rfc2136::test(&test).await;
}
+179
View File
@@ -0,0 +1,179 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use std::{net::Ipv4Addr, time::Duration as StdDuration};
use common::network::dns::update::DnsUpdater;
use dns_update::{DnsRecord, DnsRecordType, TLSARecord, TlsaCertUsage, TlsaMatching, TlsaSelector};
use registry::{
schema::{
enums::{IpProtocol, TsigAlgorithm},
structs::{DnsServer, DnsServerTsig, SecretKey, SecretKeyValue},
},
types::duration::Duration,
};
use crate::utils::server::TestServer;
const ZONE: &str = "stalwart.test";
const KEY_B64: &str = "c3RhbHdhcnQtdGVzdC10c2lnLXNlY3JldC1rZXkxMjM0NTY3ODkw";
pub async fn test(test: &TestServer) {
println!("Running RFC2136 (PowerDNS) tests...");
crate::utils::containers::ensure_powerdns().await;
let udp = DnsUpdater::build(dns_server(IpProtocol::Udp), test.server.core.clone())
.await
.expect("Failed to build UDP RFC2136 updater");
// Create, replace and delete an A record over UDP (TSIG signed)
let a_name = "rfc2136-a.stalwart.test";
udp.updater
.set_rrset(
a_name,
DnsRecordType::A,
60,
vec![DnsRecord::A(Ipv4Addr::new(10, 0, 0, 1))],
ZONE,
)
.await
.expect("set A");
assert_eq!(
list(&udp, a_name, DnsRecordType::A, 1).await,
vec![DnsRecord::A(Ipv4Addr::new(10, 0, 0, 1))]
);
udp.updater
.set_rrset(
a_name,
DnsRecordType::A,
60,
vec![DnsRecord::A(Ipv4Addr::new(10, 0, 0, 2))],
ZONE,
)
.await
.expect("replace A");
assert_eq!(
list(&udp, a_name, DnsRecordType::A, 1).await,
vec![DnsRecord::A(Ipv4Addr::new(10, 0, 0, 2))]
);
udp.updater
.set_rrset(a_name, DnsRecordType::A, 0, vec![], ZONE)
.await
.expect("delete A");
assert!(list(&udp, a_name, DnsRecordType::A, 0).await.is_empty());
// Publish two TLSA records at the same owner in a single set_rrset call
let tlsa_name = "_25._tcp.rfc2136-tlsa.stalwart.test";
let leaf: Vec<u8> = (0..32).collect();
let intermediate: Vec<u8> = (32..64).collect();
udp.updater
.set_rrset(
tlsa_name,
DnsRecordType::TLSA,
60,
vec![
DnsRecord::TLSA(TLSARecord {
cert_usage: TlsaCertUsage::DaneEe,
selector: TlsaSelector::Spki,
matching: TlsaMatching::Sha256,
cert_data: leaf.clone(),
}),
DnsRecord::TLSA(TLSARecord {
cert_usage: TlsaCertUsage::DaneTa,
selector: TlsaSelector::Spki,
matching: TlsaMatching::Sha256,
cert_data: intermediate.clone(),
}),
],
ZONE,
)
.await
.expect("set TLSA");
let tlsa = list(&udp, tlsa_name, DnsRecordType::TLSA, 2).await;
let cert_datas: Vec<Vec<u8>> = tlsa
.iter()
.filter_map(|r| match r {
DnsRecord::TLSA(t) => Some(t.cert_data.clone()),
_ => None,
})
.collect();
assert!(cert_datas.contains(&leaf), "leaf TLSA missing: {tlsa:?}");
assert!(
cert_datas.contains(&intermediate),
"intermediate TLSA missing: {tlsa:?}"
);
udp.updater
.set_rrset(tlsa_name, DnsRecordType::TLSA, 0, vec![], ZONE)
.await
.expect("cleanup TLSA");
// The TCP transport must also attach the TSIG signer
let tcp = DnsUpdater::build(dns_server(IpProtocol::Tcp), test.server.core.clone())
.await
.expect("Failed to build TCP RFC2136 updater");
let txt_name = "rfc2136-txt.stalwart.test";
tcp.updater
.set_rrset(
txt_name,
DnsRecordType::TXT,
60,
vec![DnsRecord::TXT("rfc2136-tcp-signed".to_string())],
ZONE,
)
.await
.expect("set TXT over TCP");
assert_eq!(
list(&tcp, txt_name, DnsRecordType::TXT, 1).await,
vec![DnsRecord::TXT("rfc2136-tcp-signed".to_string())]
);
tcp.updater
.set_rrset(txt_name, DnsRecordType::TXT, 0, vec![], ZONE)
.await
.expect("cleanup TXT");
}
async fn list(
updater: &DnsUpdater,
name: &str,
record_type: DnsRecordType,
expected_len: usize,
) -> Vec<DnsRecord> {
let mut latest = Vec::new();
for _ in 0..20 {
latest = updater
.updater
.list_rrset(name, record_type, ZONE)
.await
.unwrap_or_default();
if latest.len() == expected_len {
return latest;
}
tokio::time::sleep(StdDuration::from_millis(50)).await;
}
latest
}
fn dns_server(protocol: IpProtocol) -> DnsServer {
DnsServer::Tsig(DnsServerTsig {
host: "127.0.0.1".parse().unwrap(),
port: 5300,
key_name: "stalwart-update-key".to_string(),
key: SecretKey::Value(SecretKeyValue {
secret: KEY_B64.into(),
}),
protocol,
tsig_algorithm: TsigAlgorithm::HmacSha256,
description: "Test RFC2136 DNS server".to_string(),
member_tenant_id: None,
timeout: Duration::from_millis(10_000),
ttl: Duration::from_millis(60_000),
polling_interval: Duration::from_millis(100),
propagation_timeout: Duration::from_millis(10_000),
propagation_delay: None,
})
}