Files
Stalwart/crates/services/src/task_manager/dkim.rs
2026-04-10 20:18:52 +02:00

564 lines
20 KiB
Rust

/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <hello@stalw.art>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use crate::task_manager::TaskResult;
use common::{
Server,
cache::invalidate::CacheInvalidationBuilder,
ipc::CacheInvalidation,
network::dkim::{
generate_dkim_dns_record, generate_dkim_dns_record_name, generate_dkim_private_key,
generate_dkim_selector,
},
};
use registry::{
schema::{
enums::{DkimRotationStage, DkimSignatureType, DnsRecordType},
prelude::{Object, ObjectType, Property},
structs::{
Dkim1Signature, DkimManagement, DkimSignature, DnsManagement, Domain, SecretText,
SecretTextValue, Task, TaskDomainManagement, TaskStatus,
},
},
types::{datetime::UTCDateTime, id::ObjectId},
};
use std::fmt::Write;
use store::{
registry::{
RegistryObject, RegistryQuery,
write::{RegistryWrite, RegistryWriteResult},
},
write::now,
};
use trc::DkimEvent;
use types::id::Id;
pub(crate) trait DkimManagementTask: Sync + Send {
fn dkim_management(
&self,
task: &TaskDomainManagement,
) -> impl Future<Output = TaskResult> + Send;
}
impl DkimManagementTask for Server {
async fn dkim_management(&self, task: &TaskDomainManagement) -> TaskResult {
match dkim_management(self, task).await {
Ok(result) => result,
Err(err) => {
let result = TaskResult::temporary(err.to_string());
trc::error!(
err.caused_by(trc::location!())
.details("Failed to run DKIM management task")
);
result
}
}
}
}
async fn dkim_management(server: &Server, task: &TaskDomainManagement) -> trc::Result<TaskResult> {
let Some(domain) = server.registry().object::<Domain>(task.domain_id).await? else {
return Ok(TaskResult::permanent("Domain not found".to_string()));
};
let DkimManagement::Automatic(dkim) = domain.dkim_management else {
return Ok(TaskResult::permanent(
"Domain is not set to automatic DKIM management".to_string(),
));
};
let mut create_signatures = dkim.algorithms.into_inner();
if create_signatures.is_empty() {
return Ok(TaskResult::permanent(
"No DKIM algorithms configured for domain".to_string(),
));
}
let dns_updater = match domain.dns_management {
DnsManagement::Automatic(props) if props.publish_records.contains(&DnsRecordType::Dkim) => {
match server.build_dns_updater(props.dns_server_id).await? {
Ok(updater) => Some((updater, props.origin.unwrap_or_else(|| domain.name.clone()))),
Err(err) => {
return Ok(TaskResult::permanent(format!(
"Failed to build DNS updater: {}",
err
)));
}
}
}
_ => None,
};
// Fetch existing DKIM keys
let mut publish_signatures = Vec::new();
let mut retire_signatures = Vec::new();
let mut retiring_signatures = Vec::new();
let mut delete_signatures = Vec::new();
let mut next_transition = None;
let signature_ids = server
.registry()
.query::<Vec<Id>>(
RegistryQuery::new(ObjectType::DkimSignature)
.equal(Property::DomainId, task.domain_id.document_id()),
)
.await?;
for id in signature_ids {
let id = ObjectId::new(ObjectType::DkimSignature, id);
let Some(key) = server.registry().get(id).await? else {
continue;
};
let key: RegistryObject<DkimSignature> = RegistryObject {
id,
revision: key.revision,
object: key.into(),
};
let key_algo = key.object.object_type();
if let Some(current_stage) = key.object.rotation_due() {
match current_stage {
DkimRotationStage::Pending => {
create_signatures.retain(|algo| algo != &key_algo);
publish_signatures.push(key)
}
DkimRotationStage::Active => retiring_signatures.push(key),
DkimRotationStage::Retiring => retire_signatures.push(key),
DkimRotationStage::Retired => delete_signatures.push(key),
}
} else {
if key.object.is_active() {
create_signatures.retain(|algo| algo != &key_algo);
}
if let Some(transition) = key.object.next_transition()
&& next_transition.is_none_or(|next| transition < next)
{
next_transition = Some(transition);
}
}
}
let now = now();
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) => {
return Ok(TaskResult::permanent(err.to_string()));
}
};
let selector = match generate_dkim_selector(&dkim.selector_template, algorithm) {
Ok(selector) => selector,
Err(err) => {
return Ok(TaskResult::permanent(format!(
"Failed to generate DKIM selector: {}",
err
)));
}
};
// Build key
let signature = Dkim1Signature {
stage: DkimRotationStage::Active,
domain_id: task.domain_id,
member_tenant_id: domain.member_tenant_id,
selector: selector.clone(),
private_key: SecretText::Text(SecretTextValue { secret }),
..Default::default()
};
let mut signature = match algorithm {
DkimSignatureType::Dkim1Ed25519Sha256 => DkimSignature::Dkim1Ed25519Sha256(signature),
DkimSignatureType::Dkim1RsaSha256 => DkimSignature::Dkim1RsaSha256(signature),
};
// Publish key
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, 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 {
// Something went wrong, reschedule.
signature.set_stage(DkimRotationStage::Pending);
UTCDateTime::from_timestamp((now + 60) as i64) // Retry after 1 minute
};
if next_transition.is_none_or(|next| signature_transition < next) {
next_transition = Some(signature_transition);
}
signature.set_next_transition(signature_transition);
}
// Write key
match server
.registry()
.write(RegistryWrite::insert(&signature.into()))
.await?
{
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}"
)));
}
}
}
// Publish signatures
let mut temporary_errors = String::new();
for signature in publish_signatures {
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, true)
.await
{
Ok(true) => {
let signature_transition =
UTCDateTime::from_timestamp((now + dkim.rotate_after.as_secs()) as i64);
if next_transition.is_none_or(|next| signature_transition < next) {
next_transition = Some(signature_transition);
}
let mut new_signature = signature.object.clone();
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,
signature,
new_signature,
&record.name,
&mut temporary_errors,
)
.await?
{
return Ok(task_result);
}
do_refresh = true;
}
Ok(false) => {
if !temporary_errors.is_empty() {
temporary_errors.push_str("; ");
}
let _ = write!(
&mut temporary_errors,
"DKIM record {} did not propagate, will retry.",
record.name
);
}
Err(err) => {
if !temporary_errors.is_empty() {
temporary_errors.push_str("; ");
}
let _ = write!(
&mut temporary_errors,
"Failed to publish DKIM record {}: {err}.",
record.name
);
}
}
} else {
if !temporary_errors.is_empty() {
temporary_errors.push_str("; ");
}
let _ = write!(
&mut temporary_errors,
"No DNS server configured, cannot publish DKIM record {}.",
record.name
);
}
}
// Retiring signatures
for signature in retiring_signatures {
let record = generate_dkim_dns_record_name(&signature.object, &domain.name);
let signature_transition =
UTCDateTime::from_timestamp((now + dkim.retire_after.as_secs()) as i64);
if next_transition.is_none_or(|next| signature_transition < next) {
next_transition = Some(signature_transition);
}
let mut new_signature = signature.object.clone();
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,
signature,
new_signature,
&record,
&mut temporary_errors,
)
.await?
{
return Ok(task_result);
}
do_refresh = true;
}
// Retire signatures
for signature in retire_signatures {
let record = generate_dkim_dns_record_name(&signature.object, &domain.name);
if let Some((updater, origin)) = &dns_updater {
match updater
.delete(origin, &record, dns_update::DnsRecordType::TXT)
.await
{
Ok(_) => {
let signature_transition =
UTCDateTime::from_timestamp((now + dkim.delete_after.as_secs()) as i64);
if next_transition.is_none_or(|next| signature_transition < next) {
next_transition = Some(signature_transition);
}
let mut new_signature = signature.object.clone();
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,
signature,
new_signature,
&record,
&mut temporary_errors,
)
.await?
{
return Ok(task_result);
}
do_refresh = true;
}
Err(err) => {
if !temporary_errors.is_empty() {
temporary_errors.push_str("; ");
}
let _ = write!(
&mut temporary_errors,
"Failed to remove DKIM record {}: {err}.",
record
);
}
}
} else {
if !temporary_errors.is_empty() {
temporary_errors.push_str("; ");
}
let _ = write!(
&mut temporary_errors,
"No DNS server configured, cannot retire DKIM record {}.",
record
);
}
}
// 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(
signature.id,
&Object {
inner: signature.object.into(),
revision: signature.revision,
},
))
.await
{
Ok(RegistryWriteResult::Success(_)) => {}
Ok(err) => {
return Ok(TaskResult::permanent(format!(
"Failed to delete DKIM signature for record {record}: {err}"
)));
}
Err(err) => {
if err.is_assertion_failure() {
if !temporary_errors.is_empty() {
temporary_errors.push_str("; ");
}
let _ = write!(
temporary_errors,
"Failed to delete DKIM signature for record {record} due to concurrent modification, will retry.",
);
} else {
return Err(err);
}
}
}
}
if do_refresh
&& let Err(err) = server
.invalidate_caches(CacheInvalidationBuilder::default().with_invalidation(
CacheInvalidation::DkimSignature(task.domain_id.document_id()),
))
.await
{
trc::error!(
err.caused_by(trc::location!())
.details("Failed to invalidate caches after DKIM management task")
);
}
if !temporary_errors.is_empty() {
Ok(TaskResult::temporary(temporary_errors))
} else {
let tasks = if let Some(next_transition) = next_transition {
vec![Task::DkimManagement(TaskDomainManagement {
domain_id: task.domain_id,
status: TaskStatus::at(next_transition.timestamp()),
})]
} else {
vec![]
};
Ok(TaskResult::Success(tasks))
}
}
async fn update_signature(
server: &Server,
signature: RegistryObject<DkimSignature>,
new_signature: DkimSignature,
name: &str,
temporary_errors: &mut String,
) -> trc::Result<Option<TaskResult>> {
match server
.registry()
.write(RegistryWrite::update(
signature.id.id(),
&new_signature.into(),
&Object {
inner: signature.object.into(),
revision: signature.revision,
},
))
.await
{
Ok(RegistryWriteResult::Success(_)) => Ok(None),
Ok(err) => Ok(Some(TaskResult::permanent(format!(
"Failed to write DKIM signature for record {name}: {err}"
)))),
Err(err) => {
if err.is_assertion_failure() {
if !temporary_errors.is_empty() {
temporary_errors.push_str("; ");
}
let _ = write!(
temporary_errors,
"Failed to write DKIM signature for record {name} due to concurrent modification, will retry.",
);
Ok(None)
} else {
Err(err)
}
}
}
}
#[cfg(feature = "test_mode")]
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-----
"#;
#[cfg(feature = "test_mode")]
const TEST_ED25519_KEY: &str = r#"-----BEGIN PRIVATE KEY-----
MC4CAQAwBQYDK2VwBCIEIAO3hAf144lTAVjTkht3ZwBTK0CMCCd1bI0alggneN3B
-----END PRIVATE KEY-----
"#;