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
+627
View File
@@ -0,0 +1,627 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use common::{
Server,
config::smtp::resolver::{Tlsa, TlsaEntry, TlsaMatching},
};
pub use mail_auth::DnssecStatus;
use mail_auth::{
MX, RecordSet,
common::resolver::ToFqdn,
hickory_resolver::{
net::{DnsError, NetError},
proto::{
dnssec::Proof,
op::ResponseCode,
rr::{
Name, RData, Record, RecordType,
rdata::tlsa::{CertUsage, Matching, Selector},
},
},
},
};
use std::{
future::Future,
net::{Ipv4Addr, Ipv6Addr},
sync::Arc,
time::{Duration, Instant},
};
pub trait TlsaLookup: Sync + Send {
fn mx_lookup(
&self,
key: impl ToFqdn + Sync + Send,
) -> impl Future<Output = mail_auth::Result<RecordSet<MX>>> + Send;
fn tlsa_lookup(
&self,
key: impl ToFqdn + Sync + Send,
) -> impl Future<Output = mail_auth::Result<TlsaResult>> + Send;
fn ipv4_lookup_dnssec(
&self,
key: impl ToFqdn + Sync + Send,
) -> impl Future<Output = mail_auth::Result<RecordSet<Ipv4Addr>>> + Send;
fn ipv6_lookup_dnssec(
&self,
key: impl ToFqdn + Sync + Send,
) -> impl Future<Output = mail_auth::Result<RecordSet<Ipv6Addr>>> + Send;
}
pub enum TlsaResult {
Secure(Arc<Tlsa>),
Bogus,
Missing,
}
impl TlsaLookup for Server {
async fn mx_lookup(&self, key: impl ToFqdn + Sync + Send) -> mail_auth::Result<RecordSet<MX>> {
if !self.core.smtp.resolvers.dnssec_available {
return self
.core
.smtp
.resolvers
.dns
.mx_lookup(key, Some(&self.inner.cache.dns_mx))
.await;
}
let key = key.to_fqdn().into_owned().into_boxed_str();
if let Some(value) = self.inner.cache.dns_mx.get::<str>(key.as_ref())
&& value.dnssec_status != DnssecStatus::Indeterminate
{
return Ok(value);
}
#[cfg(any(test, feature = "test_mode"))]
if true {
return mail_auth::common::resolver::mock_resolve(key.as_ref());
}
let mx_lookup = match self
.core
.smtp
.resolvers
.dnssec
.resolver
.mx_lookup(Name::from_str_relaxed::<&str>(key.as_ref())?)
.await
{
Ok(mx_lookup) => mx_lookup,
Err(err) => {
if let Some(denial) = NegativeAnswer::from_error(&err)
&& denial.response_code == ResponseCode::NoError
{
let records = RecordSet {
rrset: Arc::new([]),
dnssec_status: denial.dnssec_status,
};
if let Some(valid_until) = denial.valid_until {
self.inner.cache.dns_mx.insert_with_expiry(
key,
records.clone(),
valid_until,
);
}
return Ok(records);
}
return Err(err.into());
}
};
let mx_records = mx_lookup.answers();
let mut dnssec_status: Option<DnssecStatus> = None;
let mut records: Vec<(u16, Vec<Box<str>>)> = Vec::with_capacity(mx_records.len());
for mx_record in mx_records {
if let RData::MX(mx) = &mx_record.data {
dnssec_status = Some(match dnssec_status {
Some(status) => least_secure(status, proof_to_dnssec_status(mx_record.proof)),
None => proof_to_dnssec_status(mx_record.proof),
});
let preference = mx.preference;
let exchange = mx.exchange.to_lowercase().to_ascii().into_boxed_str();
if let Some(record) = records.iter_mut().find(|r| r.0 == preference) {
record.1.push(exchange);
} else {
records.push((preference, vec![exchange]));
}
}
}
records.sort_unstable_by_key(|a| a.0);
let rrset: Arc<[MX]> = records
.into_iter()
.map(|(preference, exchanges)| MX {
preference,
exchanges: exchanges.into_boxed_slice(),
})
.collect::<Arc<[MX]>>();
let records = RecordSet {
rrset,
dnssec_status: dnssec_status.unwrap_or(DnssecStatus::Indeterminate),
};
self.inner
.cache
.dns_mx
.insert_with_expiry(key, records.clone(), mx_lookup.valid_until());
Ok(records)
}
async fn tlsa_lookup(&self, key: impl ToFqdn + Sync + Send) -> mail_auth::Result<TlsaResult> {
let key = key.to_fqdn().into_owned().into_boxed_str();
if let Some(value) = self.inner.cache.dns_tlsa.get(key.as_ref()) {
return Ok(TlsaResult::Secure(value));
}
#[cfg(any(test, feature = "test_mode"))]
if true {
if key.as_ref().contains("_dnssec_bogus.") {
return Ok(TlsaResult::Bogus);
}
return mail_auth::common::resolver::mock_resolve(key.as_ref());
}
let tlsa_lookup = match self
.core
.smtp
.resolvers
.dnssec
.resolver
.tlsa_lookup(Name::from_str_relaxed(key.as_ref())?)
.await
{
Ok(tlsa_lookup) => tlsa_lookup,
Err(err) => {
if let Some(denial) = NegativeAnswer::from_error(&err) {
return Ok(if denial.dnssec_status == DnssecStatus::Bogus {
TlsaResult::Bogus
} else {
TlsaResult::Missing
});
}
return Err(err.into());
}
};
let mut entries = Vec::new();
let mut has_end_entities = false;
let mut has_intermediates = false;
let mut dnssec_status: Option<DnssecStatus> = None;
for record in tlsa_lookup.answers() {
if let RData::TLSA(tlsa) = &record.data {
dnssec_status = Some(match dnssec_status {
Some(status) => least_secure(status, proof_to_dnssec_status(record.proof)),
None => proof_to_dnssec_status(record.proof),
});
if !record.proof.is_secure() {
continue;
}
let is_end_entity = match tlsa.cert_usage {
CertUsage::DaneEe => true,
CertUsage::DaneTa => false,
_ => continue,
};
let matching = match tlsa.matching {
Matching::Raw => TlsaMatching::Full,
Matching::Sha256 => TlsaMatching::Sha256,
Matching::Sha512 => TlsaMatching::Sha512,
_ => continue,
};
let is_spki = match tlsa.selector {
Selector::Spki => true,
Selector::Full => false,
_ => continue,
};
if is_end_entity {
has_end_entities = true;
} else {
has_intermediates = true;
}
entries.push(TlsaEntry {
is_end_entity,
is_spki,
matching,
data: tlsa.cert_data.clone(),
});
}
}
match dnssec_status {
Some(DnssecStatus::Bogus) => Ok(TlsaResult::Bogus),
Some(DnssecStatus::Secure) => {
let tlsa = Arc::new(Tlsa {
entries,
has_end_entities,
has_intermediates,
});
self.inner.cache.dns_tlsa.insert_with_expiry(
key,
tlsa.clone(),
tlsa_lookup.valid_until(),
);
Ok(TlsaResult::Secure(tlsa))
}
_ => Ok(TlsaResult::Missing),
}
}
async fn ipv4_lookup_dnssec(
&self,
key: impl ToFqdn + Sync + Send,
) -> mail_auth::Result<RecordSet<Ipv4Addr>> {
if !self.core.smtp.resolvers.dnssec_available {
return self
.core
.smtp
.resolvers
.dns
.ipv4_lookup(key, Some(&self.inner.cache.dns_ipv4))
.await;
}
let key = key.to_fqdn().into_owned().into_boxed_str();
if let Some(value) = self.inner.cache.dns_ipv4.get::<str>(key.as_ref())
&& value.dnssec_status != DnssecStatus::Indeterminate
{
return Ok(value);
}
#[cfg(any(test, feature = "test_mode"))]
if true {
return mail_auth::common::resolver::mock_resolve(key.as_ref());
}
let name = Name::from_str_relaxed::<&str>(key.as_ref())?;
let lookup = match self
.core
.smtp
.resolvers
.dnssec
.resolver
.ipv4_lookup(name.clone())
.await
{
Ok(lookup) => lookup,
Err(err) => {
if let Some(denial) = NegativeAnswer::from_error(&err)
&& denial.response_code == ResponseCode::NoError
{
let records = RecordSet {
rrset: Arc::new([]),
dnssec_status: denial.dnssec_status,
};
if let Some(valid_until) = denial.valid_until {
self.inner.cache.dns_ipv4.insert_with_expiry(
key,
records.clone(),
valid_until,
);
}
return Ok(records);
}
return Err(err.into());
}
};
let answers = lookup.answers();
let records = RecordSet {
rrset: answers
.iter()
.filter_map(|record| match &record.data {
RData::A(addr) => Some(addr.0),
_ => None,
})
.collect::<Arc<[Ipv4Addr]>>(),
dnssec_status: tlsa_base_status(&name, answers, RecordType::A),
};
self.inner
.cache
.dns_ipv4
.insert_with_expiry(key, records.clone(), lookup.valid_until());
Ok(records)
}
async fn ipv6_lookup_dnssec(
&self,
key: impl ToFqdn + Sync + Send,
) -> mail_auth::Result<RecordSet<Ipv6Addr>> {
if !self.core.smtp.resolvers.dnssec_available {
return self
.core
.smtp
.resolvers
.dns
.ipv6_lookup(key, Some(&self.inner.cache.dns_ipv6))
.await;
}
let key = key.to_fqdn().into_owned().into_boxed_str();
if let Some(value) = self.inner.cache.dns_ipv6.get::<str>(key.as_ref())
&& value.dnssec_status != DnssecStatus::Indeterminate
{
return Ok(value);
}
#[cfg(any(test, feature = "test_mode"))]
if true {
return mail_auth::common::resolver::mock_resolve(key.as_ref());
}
let name = Name::from_str_relaxed::<&str>(key.as_ref())?;
let lookup = match self
.core
.smtp
.resolvers
.dnssec
.resolver
.ipv6_lookup(name.clone())
.await
{
Ok(lookup) => lookup,
Err(err) => {
if let Some(denial) = NegativeAnswer::from_error(&err)
&& denial.response_code == ResponseCode::NoError
{
let records = RecordSet {
rrset: Arc::new([]),
dnssec_status: denial.dnssec_status,
};
if let Some(valid_until) = denial.valid_until {
self.inner.cache.dns_ipv6.insert_with_expiry(
key,
records.clone(),
valid_until,
);
}
return Ok(records);
}
return Err(err.into());
}
};
let answers = lookup.answers();
let records = RecordSet {
rrset: answers
.iter()
.filter_map(|record| match &record.data {
RData::AAAA(addr) => Some(addr.0),
_ => None,
})
.collect::<Arc<[Ipv6Addr]>>(),
dnssec_status: tlsa_base_status(&name, answers, RecordType::AAAA),
};
self.inner
.cache
.dns_ipv6
.insert_with_expiry(key, records.clone(), lookup.valid_until());
Ok(records)
}
}
struct NegativeAnswer {
response_code: ResponseCode,
dnssec_status: DnssecStatus,
valid_until: Option<Instant>,
}
impl NegativeAnswer {
fn from_error(err: &NetError) -> Option<Self> {
let NetError::Dns(dns_error) = err else {
return None;
};
match dns_error {
DnsError::NoRecordsFound(no_records) => Some(NegativeAnswer {
response_code: no_records.response_code,
dnssec_status: no_records
.authorities
.as_deref()
.map(denial_dnssec_status)
.unwrap_or(DnssecStatus::Indeterminate),
valid_until: no_records
.negative_ttl
.map(|ttl| Instant::now() + Duration::from_secs(ttl as u64)),
}),
DnsError::Nsec {
response, proof, ..
} => Some(NegativeAnswer {
response_code: response.response_code,
dnssec_status: proof_to_dnssec_status(*proof),
valid_until: None,
}),
_ => None,
}
}
}
fn denial_dnssec_status(authorities: &[Record]) -> DnssecStatus {
authorities
.iter()
.filter(|record| matches!(record.record_type(), RecordType::NSEC | RecordType::NSEC3))
.map(|record| proof_to_dnssec_status(record.proof))
.reduce(least_secure)
.unwrap_or(DnssecStatus::Indeterminate)
}
fn proof_to_dnssec_status(proof: Proof) -> DnssecStatus {
match proof {
Proof::Secure => DnssecStatus::Secure,
Proof::Insecure => DnssecStatus::Insecure,
Proof::Bogus => DnssecStatus::Bogus,
Proof::Indeterminate => DnssecStatus::Indeterminate,
}
}
fn tlsa_base_status(query: &Name, answers: &[Record], address_type: RecordType) -> DnssecStatus {
let mut addresses: Option<DnssecStatus> = None;
let mut alias: Option<DnssecStatus> = None;
for record in answers {
let status = proof_to_dnssec_status(record.proof);
if record.record_type() == address_type {
addresses = Some(match addresses {
Some(current) => least_secure(current, status),
None => status,
});
} else if record.record_type() == RecordType::CNAME && &record.name == query {
alias = Some(match alias {
Some(current) => least_secure(current, status),
None => status,
});
}
}
match (addresses, alias) {
(Some(DnssecStatus::Insecure), Some(DnssecStatus::Secure)) => DnssecStatus::Secure,
(Some(status), _) => status,
(None, _) => DnssecStatus::Indeterminate,
}
}
pub(crate) fn least_secure(a: DnssecStatus, b: DnssecStatus) -> DnssecStatus {
fn rank(status: DnssecStatus) -> u8 {
match status {
DnssecStatus::Bogus => 0,
DnssecStatus::Indeterminate => 1,
DnssecStatus::Insecure => 2,
DnssecStatus::Secure => 3,
}
}
if rank(a) <= rank(b) { a } else { b }
}
#[cfg(test)]
mod tests {
use super::*;
use mail_auth::hickory_resolver::proto::rr::rdata::{A, CNAME};
use std::net::Ipv4Addr;
fn name(value: &str) -> Name {
Name::from_ascii(value).unwrap()
}
fn address(owner: &str, proof: Proof) -> Record {
let mut record =
Record::from_rdata(name(owner), 3600, RData::A(A(Ipv4Addr::new(192, 0, 2, 1))));
record.proof = proof;
record
}
fn alias(owner: &str, target: &str, proof: Proof) -> Record {
let mut record = Record::from_rdata(name(owner), 3600, RData::CNAME(CNAME(name(target))));
record.proof = proof;
record
}
#[test]
fn tlsa_base_status_follows_address_records() {
let query = name("mx.example.org.");
for (proof, expected) in [
(Proof::Secure, DnssecStatus::Secure),
(Proof::Insecure, DnssecStatus::Insecure),
(Proof::Bogus, DnssecStatus::Bogus),
(Proof::Indeterminate, DnssecStatus::Indeterminate),
] {
assert_eq!(
tlsa_base_status(&query, &[address("mx.example.org.", proof)], RecordType::A),
expected,
"proof {proof}"
);
}
}
#[test]
fn tlsa_base_status_is_indeterminate_without_addresses() {
assert_eq!(
tlsa_base_status(&name("mx.example.org."), &[], RecordType::A),
DnssecStatus::Indeterminate
);
}
#[test]
fn tlsa_base_status_takes_least_secure_address() {
let query = name("mx.example.org.");
assert_eq!(
tlsa_base_status(
&query,
&[
address("mx.example.org.", Proof::Secure),
address("mx.example.org.", Proof::Insecure),
],
RecordType::A
),
DnssecStatus::Insecure
);
}
#[test]
fn tlsa_base_status_keeps_secure_alias_to_insecure_zone() {
let query = name("mx.example.org.");
assert_eq!(
tlsa_base_status(
&query,
&[
alias("mx.example.org.", "mx.provider.net.", Proof::Secure),
address("mx.provider.net.", Proof::Insecure),
],
RecordType::A
),
DnssecStatus::Secure
);
}
#[test]
fn tlsa_base_status_skips_insecure_alias() {
let query = name("mx.example.org.");
assert_eq!(
tlsa_base_status(
&query,
&[
alias("mx.example.org.", "mx.provider.net.", Proof::Insecure),
address("mx.provider.net.", Proof::Insecure),
],
RecordType::A
),
DnssecStatus::Insecure
);
}
#[test]
fn tlsa_base_status_ignores_alias_below_query_name() {
let query = name("mx.example.org.");
assert_eq!(
tlsa_base_status(
&query,
&[
alias("mx.provider.net.", "mx.other.net.", Proof::Secure),
address("mx.other.net.", Proof::Insecure),
],
RecordType::A
),
DnssecStatus::Insecure
);
}
}
+8
View File
@@ -0,0 +1,8 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
pub mod dnssec;
pub mod verify;
+242
View File
@@ -0,0 +1,242 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use crate::queue::{Error, ErrorDetails, HostResponse, Status};
use common::config::smtp::resolver::{Tlsa, TlsaEntry, TlsaMatching};
use rustls_pki_types::{CertificateDer, Der, ServerName, TrustAnchor, UnixTime};
use sha2::{Digest, Sha256, Sha512};
use trc::DaneEvent;
use webpki::{ALL_VERIFICATION_ALGS, EndEntityCert, KeyUsage, anchor_from_trusted_cert};
use x509_parser::asn1_rs::Any;
use x509_parser::prelude::{FromDer, X509Certificate};
pub trait TlsaVerify {
fn verify(
&self,
session_id: u64,
hostname: &str,
reference_ids: &[&str],
certificates: Option<&[CertificateDer<'_>]>,
) -> Result<(), Status<HostResponse<Box<str>>, ErrorDetails>>;
}
impl TlsaVerify for Tlsa {
fn verify(
&self,
session_id: u64,
hostname: &str,
reference_ids: &[&str],
certificates: Option<&[CertificateDer<'_>]>,
) -> Result<(), Status<HostResponse<Box<str>>, ErrorDetails>> {
let certificates = match certificates {
Some(certificates) if !certificates.is_empty() => certificates,
_ => {
trc::event!(
Dane(DaneEvent::NoCertificatesFound),
SpanId = session_id,
Hostname = hostname.to_string(),
);
return Err(Status::TemporaryFailure(ErrorDetails {
entity: hostname.into(),
details: Error::DaneError("No certificates were provided by host".into()),
}));
}
};
let mut parsed = Vec::with_capacity(certificates.len());
for der_certificate in certificates {
match X509Certificate::from_der(der_certificate.as_ref()) {
Ok((_, cert)) => parsed.push(cert),
Err(err) => {
trc::event!(
Dane(DaneEvent::CertificateParseError),
SpanId = session_id,
Hostname = hostname.to_string(),
Reason = err.to_string(),
);
return Err(Status::TemporaryFailure(ErrorDetails {
entity: hostname.into(),
details: Error::DaneError("Failed to parse X.509 certificate".into()),
}));
}
}
}
if verify_end_entity(self, session_id, hostname, certificates, &parsed)
|| verify_trust_anchor(
self,
session_id,
hostname,
reference_ids,
certificates,
&parsed,
)
{
trc::event!(
Dane(DaneEvent::AuthenticationSuccess),
SpanId = session_id,
Hostname = hostname.to_string(),
);
Ok(())
} else {
trc::event!(
Dane(DaneEvent::AuthenticationFailure),
SpanId = session_id,
Hostname = hostname.to_string(),
);
Err(Status::TemporaryFailure(ErrorDetails {
entity: hostname.into(),
details: Error::DaneError("No matching certificates found in TLSA records".into()),
}))
}
}
}
fn verify_end_entity(
tlsa: &Tlsa,
session_id: u64,
hostname: &str,
certificates: &[CertificateDer<'_>],
parsed: &[X509Certificate<'_>],
) -> bool {
if tlsa.has_end_entities {
for record in tlsa.entries.iter().filter(|record| record.is_end_entity) {
if record_matches(record, &parsed[0], certificates[0].as_ref()) {
trc::event!(
Dane(DaneEvent::TlsaRecordMatch),
SpanId = session_id,
Hostname = hostname.to_string(),
Type = "end-entity",
);
return true;
}
}
}
false
}
fn verify_trust_anchor(
tlsa: &Tlsa,
session_id: u64,
hostname: &str,
reference_ids: &[&str],
certificates: &[CertificateDer<'_>],
parsed: &[X509Certificate<'_>],
) -> bool {
if !tlsa.has_intermediates {
return false;
}
let end_entity = match EndEntityCert::try_from(&certificates[0]) {
Ok(end_entity) => end_entity,
Err(_) => return false,
};
let mut anchors: Vec<TrustAnchor<'static>> = Vec::new();
for record in tlsa.entries.iter().filter(|record| !record.is_end_entity) {
match (record.is_spki, record.matching) {
(false, TlsaMatching::Full) => {
let der = CertificateDer::from(record.data.clone());
if let Ok(anchor) = anchor_from_trusted_cert(&der) {
anchors.push(anchor.to_owned());
}
}
(true, TlsaMatching::Full) => {
if let Some(depth) = (1..certificates.len())
.find(|&depth| parsed[depth].public_key().raw == record.data.as_slice())
{
if let Ok(anchor) = anchor_from_trusted_cert(&certificates[depth]) {
anchors.push(anchor.to_owned());
}
} else if let Some(spki) = der_value(&record.data) {
for depth in 1..certificates.len() {
if is_chain_top(parsed, depth)
&& let Some(subject) = der_value(parsed[depth].issuer().as_raw())
{
anchors.push(TrustAnchor {
subject: Der::from(subject.to_vec()),
subject_public_key_info: Der::from(spki.to_vec()),
name_constraints: None,
});
}
}
}
}
_ => {
for depth in 1..certificates.len() {
if record_matches(record, &parsed[depth], certificates[depth].as_ref())
&& let Ok(anchor) = anchor_from_trusted_cert(&certificates[depth])
{
anchors.push(anchor.to_owned());
}
}
}
}
}
if anchors.is_empty()
|| end_entity
.verify_for_usage(
ALL_VERIFICATION_ALGS,
&anchors,
&certificates[1..],
UnixTime::now(),
KeyUsage::server_auth(),
None,
None,
)
.is_err()
|| !reference_ids.iter().any(|reference| {
ServerName::try_from(*reference)
.map(|name| end_entity.verify_is_valid_for_subject_name(&name).is_ok())
.unwrap_or(false)
})
{
false
} else {
trc::event!(
Dane(DaneEvent::TlsaRecordMatch),
SpanId = session_id,
Hostname = hostname.to_string(),
Type = "trust-anchor",
);
true
}
}
fn is_chain_top(parsed: &[X509Certificate<'_>], depth: usize) -> bool {
let issuer = parsed[depth].issuer().as_raw();
!parsed
.iter()
.enumerate()
.any(|(other, cert)| other != depth && cert.subject().as_raw() == issuer)
}
fn record_matches(record: &TlsaEntry, cert: &X509Certificate<'_>, raw: &[u8]) -> bool {
let selected: &[u8] = if record.is_spki {
cert.public_key().raw
} else {
raw
};
match record.matching {
TlsaMatching::Full => selected == record.data.as_slice(),
TlsaMatching::Sha256 => Sha256::digest(selected).as_slice() == record.data.as_slice(),
TlsaMatching::Sha512 => Sha512::digest(selected).as_slice() == record.data.as_slice(),
}
}
#[inline(always)]
fn der_value(der: &[u8]) -> Option<&[u8]> {
Any::from_der(der).ok().map(|(_, any)| any.data)
}