1use crate::chain::ArcChain;
23use crate::error::ArcError;
24use crate::header::{ArcSealCv, MAX_INSTANCE};
25use crate::resolver::ArcResolver;
26
27#[derive(Debug, Clone, PartialEq, Eq)]
29pub enum ChainOutcome {
30 Pass,
34 Fail {
38 reason: String,
40 },
41 #[deprecated(
47 since = "1.1.0",
48 note = "verify_chain_with_crypto now performs cryptographic verification; \
49 this variant is no longer returned"
50 )]
51 CryptoUnimplemented,
52}
53
54pub fn verify_chain(chain: &ArcChain) -> ChainOutcome {
61 if chain.sets.is_empty() {
62 return ChainOutcome::Fail {
63 reason: "empty chain".into(),
64 };
65 }
66 if chain.sets.len() > MAX_INSTANCE as usize {
67 return ChainOutcome::Fail {
68 reason: format!("chain length {} exceeds RFC 8617 §4.2.1 max of 50", chain.sets.len()),
69 };
70 }
71 for (idx, set) in chain.sets.iter().enumerate() {
74 if idx == 0 {
75 if set.seal.cv != ArcSealCv::None {
76 return ChainOutcome::Fail {
77 reason: format!(
78 "first ARC-Seal must have cv=none, got cv={:?}",
79 set.seal.cv
80 ),
81 };
82 }
83 } else if set.seal.cv == ArcSealCv::None {
84 return ChainOutcome::Fail {
85 reason: format!("ARC-Seal i={} has cv=none but is not the first set", set.i),
86 };
87 }
88 }
89 for (idx, set) in chain.sets.iter().enumerate() {
93 let expected = (idx + 1) as u32;
94 if set.i != expected || set.aar.instance != expected || set.ams.instance != expected
95 || set.seal.instance != expected
96 {
97 return ChainOutcome::Fail {
98 reason: format!("ARC set {idx} has mismatched i= across its 3 headers"),
99 };
100 }
101 }
102 ChainOutcome::Pass
103}
104
105pub async fn verify_chain_with_crypto<R: ArcResolver + ?Sized>(
118 chain: &ArcChain,
119 resolver: &R,
120 raw_message: &[u8],
121) -> Result<ChainOutcome, ArcError> {
122 match verify_chain(chain) {
123 ChainOutcome::Pass => {}
124 other => return Ok(other),
125 }
126 for set in chain.sets.iter().rev() {
131 if let Err(e) = crate::crypto::verify_ams(set, raw_message, resolver).await {
132 return Ok(ChainOutcome::Fail {
133 reason: format!("ams i={}: {e}", set.i),
134 });
135 }
136 if let Err(e) = crate::crypto::verify_as(chain, set.i, resolver).await {
137 return Ok(ChainOutcome::Fail {
138 reason: format!("as i={}: {e}", set.i),
139 });
140 }
141 }
142 Ok(ChainOutcome::Pass)
143}
144
145#[cfg(test)]
146mod tests {
147 use super::*;
148 use crate::chain::ArcChain;
149
150 fn msg_with(headers: &[&str]) -> Vec<u8> {
151 let mut v = Vec::new();
152 for h in headers {
153 v.extend_from_slice(h.as_bytes());
154 }
155 v.extend_from_slice(b"From: a@b.c\r\nSubject: t\r\n\r\nbody");
156 v
157 }
158
159 const SET1_AAR: &str = "ARC-Authentication-Results: i=1; spf=pass\r\n";
160 const SET1_AMS: &str = "ARC-Message-Signature: i=1; a=rsa-sha256; d=example.com; s=mail; h=From; bh=BH1; b=SIG1\r\n";
161 const SET1_AS_NONE: &str = "ARC-Seal: i=1; a=rsa-sha256; cv=none; d=example.com; s=mail; b=SEAL1\r\n";
162 const SET1_AS_PASS: &str = "ARC-Seal: i=1; a=rsa-sha256; cv=pass; d=example.com; s=mail; b=SEAL1\r\n";
163
164 const SET2_AAR: &str = "ARC-Authentication-Results: i=2; dkim=pass\r\n";
165 const SET2_AMS: &str = "ARC-Message-Signature: i=2; a=rsa-sha256; d=forwarder.example; s=mail; h=From; bh=BH2; b=SIG2\r\n";
166 const SET2_AS_PASS: &str = "ARC-Seal: i=2; a=rsa-sha256; cv=pass; d=forwarder.example; s=mail; b=SEAL2\r\n";
167 const SET2_AS_NONE: &str = "ARC-Seal: i=2; a=rsa-sha256; cv=none; d=forwarder.example; s=mail; b=SEAL2\r\n";
168
169 #[test]
170 fn single_set_with_cv_none_passes_structural() {
171 let m = msg_with(&[SET1_AAR, SET1_AMS, SET1_AS_NONE]);
172 let chain = ArcChain::extract(&m).unwrap().unwrap();
173 assert_eq!(verify_chain(&chain), ChainOutcome::Pass);
174 }
175
176 #[test]
177 fn single_set_with_cv_pass_fails_structural() {
178 let m = msg_with(&[SET1_AAR, SET1_AMS, SET1_AS_PASS]);
180 let chain = ArcChain::extract(&m).unwrap().unwrap();
181 assert!(matches!(verify_chain(&chain), ChainOutcome::Fail { .. }));
182 }
183
184 #[test]
185 fn two_set_normal_chain_passes_structural() {
186 let m = msg_with(&[SET1_AAR, SET1_AMS, SET1_AS_NONE, SET2_AAR, SET2_AMS, SET2_AS_PASS]);
187 let chain = ArcChain::extract(&m).unwrap().unwrap();
188 assert_eq!(verify_chain(&chain), ChainOutcome::Pass);
189 }
190
191 #[test]
192 fn later_set_with_cv_none_fails_structural() {
193 let m = msg_with(&[SET1_AAR, SET1_AMS, SET1_AS_NONE, SET2_AAR, SET2_AMS, SET2_AS_NONE]);
195 let chain = ArcChain::extract(&m).unwrap().unwrap();
196 assert!(matches!(verify_chain(&chain), ChainOutcome::Fail { .. }));
197 }
198
199 struct DummyResolver;
200 #[async_trait::async_trait]
201 impl mailrs_dkim::DkimResolver for DummyResolver {
202 async fn lookup_txt(&self, _: &str) -> Result<Vec<String>, mailrs_dkim::DkimError> {
203 Ok(vec![])
204 }
205 }
206
207 #[tokio::test]
208 async fn crypto_verify_returns_fail_when_dns_empty_after_structural_pass() {
209 let m = msg_with(&[SET1_AAR, SET1_AMS, SET1_AS_NONE]);
214 let chain = ArcChain::extract(&m).unwrap().unwrap();
215 let r = verify_chain_with_crypto(&chain, &DummyResolver, &m)
216 .await
217 .unwrap();
218 match r {
219 ChainOutcome::Fail { reason } => assert!(reason.starts_with("ams i=1:"), "{reason}"),
220 other => panic!("expected Fail, got {other:?}"),
221 }
222 }
223
224 #[tokio::test]
225 async fn crypto_verify_returns_fail_when_structural_fails() {
226 let m = msg_with(&[SET1_AAR, SET1_AMS, SET1_AS_PASS]);
229 let chain = ArcChain::extract(&m).unwrap().unwrap();
230 let r = verify_chain_with_crypto(&chain, &DummyResolver, &m)
231 .await
232 .unwrap();
233 assert!(matches!(r, ChainOutcome::Fail { .. }));
234 }
235}