Skip to main content

mail_auth/dkim/
record.rs

1/*
2 * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <hello@stalw.art>
3 *
4 * SPDX-License-Identifier: Apache-2.0 OR MIT
5 */
6
7//! DKIM1 DNS records: the `_domainkey` key record, the RFC 6651 report
8//! record and the RFC 6541 ATPS record.
9
10use super::Canonicalization;
11use crate::crypto::{Algorithm, VerifyingKey};
12
13/// A DKIM1 public key record (RFC 6376, Section 3.6.1).
14///
15/// Parsed from the TXT record at `<selector>._domainkey.<domain>` with
16/// [`TxtRecordParser::parse`](crate::parse::TxtRecordParser::parse); the
17/// verifier fetches it for every signature. Holds the public key (`p=`, of
18/// type `k=`) and the `t=` flags.
19pub struct DomainKey {
20    pub(crate) p: Box<dyn VerifyingKey + Send + Sync>,
21    pub(crate) f: u64,
22}
23
24/// An RFC 6651 DKIM failure reporting record (RFC 6651, Section 3.3).
25///
26/// Parsed from the TXT record at `_report._domainkey.<domain>`. The verifier
27/// fetches it when a signature with `r=y` does not pass, to decide whether
28/// and where to send a failure report (see
29/// [`DkimOutput::report_address`](super::DkimOutput::report_address)).
30#[derive(Debug, PartialEq, Eq, Clone)]
31pub struct DkimReportRecord {
32    pub(crate) ra: String,
33    pub(crate) rp: u8,
34    pub(crate) rr: u8,
35    pub(crate) rs: Option<String>,
36}
37
38/// An RFC 6541 Authorized Third-Party Signature record (RFC 6541,
39/// Section 4.3).
40///
41/// Parsed from the TXT record at `<hashed-or-plain-d>._atps.<atps-domain>`.
42/// Its existence (with `v=ATPS1`) authorizes the signing domain to sign on
43/// behalf of the `atps=` domain.
44#[derive(Debug, PartialEq, Eq, Clone)]
45pub struct AtpsRecord {
46    pub(crate) d: Option<String>,
47}
48
49pub(crate) const R_SVC_ALL: u64 = 0x04;
50pub(crate) const R_SVC_EMAIL: u64 = 0x08;
51pub(crate) const R_FLAG_TESTING: u64 = 0x10;
52pub(crate) const R_FLAG_MATCH_DOMAIN: u64 = 0x20;
53pub(crate) const R_MULTIPLE_RECORDS: u64 = 0x80;
54
55pub(crate) const RR_DNS: u8 = 0x01;
56pub(crate) const RR_OTHER: u8 = 0x02;
57pub(crate) const RR_POLICY: u8 = 0x04;
58pub(crate) const RR_SIGNATURE: u8 = 0x08;
59pub(crate) const RR_UNKNOWN_TAG: u8 = 0x10;
60pub(crate) const RR_VERIFICATION: u8 = 0x20;
61pub(crate) const RR_EXPIRATION: u8 = 0x40;
62
63#[derive(Debug, PartialEq, Eq, Clone)]
64#[repr(u64)]
65pub(crate) enum Service {
66    All = R_SVC_ALL,
67    Email = R_SVC_EMAIL,
68}
69
70#[derive(Debug, PartialEq, Eq, Clone)]
71#[repr(u64)]
72pub(crate) enum Flag {
73    Testing = R_FLAG_TESTING,
74    MatchDomain = R_FLAG_MATCH_DOMAIN,
75}
76
77impl From<Flag> for u64 {
78    fn from(v: Flag) -> Self {
79        v as u64
80    }
81}
82
83impl From<Service> for u64 {
84    fn from(v: Service) -> Self {
85        v as u64
86    }
87}
88
89impl DomainKey {
90    /// Returns `true` when the record has `t=y`: the domain is testing DKIM
91    /// and verifiers should not treat failures differently from unsigned
92    /// mail (RFC 6376, Section 3.6.1).
93    pub fn is_testing(&self) -> bool {
94        (self.f & R_FLAG_TESTING) != 0
95    }
96
97    /// Returns `true` when the record has `t=s`: the domain of the `i=` tag
98    /// must equal `d=` exactly, not a subdomain of it (RFC 6376,
99    /// Section 3.6.1).
100    pub fn requires_strict_identity(&self) -> bool {
101        (self.f & R_FLAG_MATCH_DOMAIN) != 0
102    }
103
104    /// Returns `true` when the name held more than one TXT record. DKIM uses
105    /// the first record that parses; DKIM2 treats this as an error
106    /// (draft-ietf-dkim-dkim2-spec Section 14.5).
107    pub fn has_multiple_records(&self) -> bool {
108        (self.f & R_MULTIPLE_RECORDS) != 0
109    }
110
111    pub(crate) fn verify<'a>(
112        &self,
113        headers: &mut dyn Iterator<Item = (&'a [u8], &'a [u8])>,
114        input: &impl VerifySignature,
115        canonicalization: Canonicalization,
116    ) -> crate::Result<()> {
117        self.p.verify(
118            headers,
119            input.signature(),
120            canonicalization,
121            input.algorithm(),
122        )
123    }
124}
125
126/// A signature that can be checked against a [`DomainKey`].
127///
128/// Implemented by DKIM1 [`Signature`](super::Signature) and by ARC seals and
129/// message signatures, which share the `_domainkey` key lookup.
130pub trait VerifySignature {
131    /// Returns the selector (`s=` tag).
132    fn selector(&self) -> &str;
133
134    /// Returns the signing domain (`d=` tag).
135    fn domain(&self) -> &str;
136
137    /// Returns the decoded signature data (`b=` tag).
138    fn signature(&self) -> &[u8];
139
140    /// Returns the signing algorithm (`a=` tag).
141    fn algorithm(&self) -> Algorithm;
142
143    /// Returns the fully qualified name of the key record,
144    /// `<selector>._domainkey.<domain>.`, with a trailing dot.
145    fn domain_key(&self) -> String {
146        let s = self.selector();
147        let d = self.domain();
148        let mut key = String::with_capacity(s.len() + d.len() + 13);
149        key.push_str(s);
150        key.push_str("._domainkey.");
151        key.push_str(d);
152        key.push('.');
153        key
154    }
155}