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}