Skip to main content

email_auth/dkim/
verify.rs

1use std::time::{SystemTime, UNIX_EPOCH};
2
3use ring::signature as ring_sig;
4use subtle::ConstantTimeEq;
5
6use crate::common::dns::{DnsError, DnsResolver};
7
8use super::canon::{
9    apply_body_length_limit, canonicalize_body, canonicalize_header, normalize_line_endings,
10    select_headers, strip_b_tag_value,
11};
12use super::key::DkimPublicKey;
13use super::types::{
14    Algorithm, CanonicalizationMethod, DkimResult, DkimSignature, FailureKind, HashAlgorithm,
15    KeyType, PermFailKind,
16};
17
18/// DKIM signature verifier.
19pub struct DkimVerifier<R: DnsResolver> {
20    resolver: R,
21    clock_skew: u64, // default 300s
22}
23
24impl<R: DnsResolver> DkimVerifier<R> {
25    pub fn new(resolver: R) -> Self {
26        Self {
27            resolver,
28            clock_skew: 300,
29        }
30    }
31
32    pub fn clock_skew(mut self, seconds: u64) -> Self {
33        self.clock_skew = seconds;
34        self
35    }
36
37    /// Verify all DKIM-Signature headers in a message.
38    /// `headers`: message headers as (name, value) pairs, in order (first = top of message).
39    /// `body`: raw message body bytes.
40    /// Returns one DkimResult per DKIM-Signature, or `[DkimResult::None]` if none found.
41    pub async fn verify_message(
42        &self,
43        headers: &[(&str, &str)],
44        body: &[u8],
45    ) -> Vec<DkimResult> {
46        // Find all DKIM-Signature headers
47        let dkim_indices: Vec<usize> = headers
48            .iter()
49            .enumerate()
50            .filter(|(_, (name, _))| name.eq_ignore_ascii_case("dkim-signature"))
51            .map(|(i, _)| i)
52            .collect();
53
54        if dkim_indices.is_empty() {
55            return vec![DkimResult::None];
56        }
57
58        let mut results = Vec::new();
59        for idx in dkim_indices {
60            let (_, value) = headers[idx];
61            let result = self.verify_single(headers, body, value, idx).await;
62            results.push(result);
63        }
64        results
65    }
66
67    async fn verify_single(
68        &self,
69        headers: &[(&str, &str)],
70        body: &[u8],
71        sig_value: &str,
72        sig_idx: usize,
73    ) -> DkimResult {
74        // Parse signature
75        let sig = match DkimSignature::parse(sig_value) {
76            Ok(s) => s,
77            Err(e) => {
78                return DkimResult::PermFail {
79                    kind: e.kind,
80                    detail: e.detail,
81                }
82            }
83        };
84
85        // x= must be >= t= if both present (RFC 6376 §3.5)
86        if let (Some(t), Some(x)) = (sig.timestamp, sig.expiration) {
87            if x < t {
88                return DkimResult::PermFail {
89                    kind: PermFailKind::MalformedSignature,
90                    detail: format!("x= ({}) must be >= t= ({}) per RFC 6376 §3.5", x, t),
91                };
92            }
93        }
94
95        // Expiration check (BEFORE DNS lookup)
96        if let Some(expiration) = sig.expiration {
97            let now = current_timestamp();
98            if now > expiration + self.clock_skew {
99                return DkimResult::PermFail {
100                    kind: PermFailKind::ExpiredSignature,
101                    detail: format!("signature expired at {}, now {}", expiration, now),
102                };
103            }
104        }
105
106        // DNS key lookup
107        let query = format!("{}._domainkey.{}", sig.selector, sig.domain);
108        let key = match self.lookup_key(&query).await {
109            Ok(k) => k,
110            Err(result) => return result,
111        };
112
113        // Key constraint enforcement (ordered per spec §4.3)
114        if let Some(result) = enforce_key_constraints(&sig, &key) {
115            return result;
116        }
117
118        // Body hash verification
119        if let Some(result) =
120            verify_body_hash(&sig, body)
121        {
122            return result;
123        }
124
125        // Header hash computation + crypto verification
126        let header_data = compute_header_hash_input(&sig, headers, sig_idx);
127
128        // Crypto verification
129        match verify_signature(&sig.algorithm, &key, &header_data, &sig.signature) {
130            Ok(()) => DkimResult::Pass {
131                domain: sig.domain,
132                selector: sig.selector,
133                testing: key.is_testing(),
134            },
135            Err(detail) => DkimResult::Fail {
136                kind: FailureKind::SignatureVerificationFailed,
137                detail,
138            },
139        }
140    }
141
142    async fn lookup_key(&self, query: &str) -> Result<DkimPublicKey, DkimResult> {
143        let txt_records = match self.resolver.query_txt(query).await {
144            Ok(records) => records,
145            Err(DnsError::NxDomain) | Err(DnsError::NoRecords) => {
146                return Err(DkimResult::PermFail {
147                    kind: PermFailKind::KeyNotFound,
148                    detail: format!("no DNS key record at {}", query),
149                })
150            }
151            Err(DnsError::TempFail) => {
152                return Err(DkimResult::TempFail {
153                    reason: format!("DNS temp failure for {}", query),
154                })
155            }
156        };
157
158        // Concatenate multiple TXT strings
159        let concatenated = txt_records.join("");
160
161        DkimPublicKey::parse(&concatenated).map_err(|e| DkimResult::PermFail {
162            kind: e.kind,
163            detail: e.detail,
164        })
165    }
166}
167
168fn current_timestamp() -> u64 {
169    SystemTime::now()
170        .duration_since(UNIX_EPOCH)
171        .map(|d| d.as_secs())
172        .unwrap_or(0)
173}
174
175/// Enforce key constraints per spec §4.3 (ordered).
176pub(crate) fn enforce_key_constraints(sig: &DkimSignature, key: &DkimPublicKey) -> Option<DkimResult> {
177    // a. Empty p= → KeyRevoked
178    if key.revoked {
179        return Some(DkimResult::PermFail {
180            kind: PermFailKind::KeyRevoked,
181            detail: "key revoked (empty p= tag)".into(),
182        });
183    }
184
185    // b. Key h= tag: signature's hash must be in list
186    if let Some(ref hash_algs) = key.hash_algorithms {
187        let sig_hash = sig.algorithm.hash_algorithm();
188        if !hash_algs.contains(&sig_hash) {
189            return Some(DkimResult::PermFail {
190                kind: PermFailKind::HashNotPermitted,
191                detail: format!("key h= tag does not permit {:?}", sig_hash),
192            });
193        }
194    }
195
196    // c. Key s= tag: must include "email" or "*"
197    if let Some(ref service_types) = key.service_types {
198        if !service_types.iter().any(|s| s == "email" || s == "*") {
199            return Some(DkimResult::PermFail {
200                kind: PermFailKind::ServiceTypeMismatch,
201                detail: "key s= tag does not include 'email' or '*'".into(),
202            });
203        }
204    }
205
206    // d. Key t=s strict: i= domain must exactly equal d=
207    if key.is_strict() {
208        let i_domain = sig
209            .auid
210            .rfind('@')
211            .map(|pos| &sig.auid[pos + 1..])
212            .unwrap_or(&sig.auid);
213        if !i_domain.eq_ignore_ascii_case(&sig.domain) {
214            return Some(DkimResult::PermFail {
215                kind: PermFailKind::StrictModeViolation,
216                detail: format!(
217                    "key t=s strict mode: i= domain '{}' must exactly equal d= '{}'",
218                    i_domain, sig.domain
219                ),
220            });
221        }
222    }
223
224    // e. Key type must match algorithm
225    let expected_key_type = match sig.algorithm {
226        Algorithm::RsaSha1 | Algorithm::RsaSha256 => KeyType::Rsa,
227        Algorithm::Ed25519Sha256 => KeyType::Ed25519,
228    };
229    if key.key_type != expected_key_type {
230        return Some(DkimResult::PermFail {
231            kind: PermFailKind::AlgorithmMismatch,
232            detail: format!(
233                "key type {:?} incompatible with algorithm {:?}",
234                key.key_type, sig.algorithm
235            ),
236        });
237    }
238
239    None
240}
241
242/// Verify body hash: canonicalize → hash → constant-time compare with bh=.
243pub(crate) fn verify_body_hash(sig: &DkimSignature, body: &[u8]) -> Option<DkimResult> {
244    let normalized = normalize_line_endings(body);
245    let canonicalized = canonicalize_body(sig.body_canonicalization, &normalized);
246    let limited = apply_body_length_limit(&canonicalized, sig.body_length);
247
248    let computed_hash = compute_hash(sig.algorithm, limited);
249
250    // Constant-time comparison
251    if computed_hash.ct_eq(&sig.body_hash).into() {
252        None // match — continue verification
253    } else {
254        Some(DkimResult::Fail {
255            kind: FailureKind::BodyHashMismatch,
256            detail: "computed body hash does not match bh= value".into(),
257        })
258    }
259}
260
261/// Compute hash using the algorithm's hash function.
262pub(crate) fn compute_hash(algorithm: Algorithm, data: &[u8]) -> Vec<u8> {
263    match algorithm.hash_algorithm() {
264        HashAlgorithm::Sha256 => {
265            let digest = ring::digest::digest(&ring::digest::SHA256, data);
266            digest.as_ref().to_vec()
267        }
268        HashAlgorithm::Sha1 => {
269            let digest = ring::digest::digest(&ring::digest::SHA1_FOR_LEGACY_USE_ONLY, data);
270            digest.as_ref().to_vec()
271        }
272    }
273}
274
275/// Build the header hash input for DKIM verification.
276fn compute_header_hash_input(
277    sig: &DkimSignature,
278    headers: &[(&str, &str)],
279    sig_idx: usize,
280) -> Vec<u8> {
281    // Exclude the DKIM-Signature header being verified from the message headers
282    let filtered_headers: Vec<(&str, &str)> = headers
283        .iter()
284        .enumerate()
285        .filter(|(i, _)| *i != sig_idx)
286        .map(|(_, h)| *h)
287        .collect();
288
289    // Select and canonicalize headers per h= tag
290    let selected = select_headers(
291        sig.header_canonicalization,
292        &sig.signed_headers,
293        &filtered_headers,
294    );
295
296    let mut hash_input = Vec::new();
297    for header_line in &selected {
298        hash_input.extend_from_slice(header_line.as_bytes());
299    }
300
301    // Append DKIM-Signature header with b= stripped, canonicalized, NO trailing CRLF
302    let stripped = strip_b_tag_value(&sig.raw_header);
303    let canon_sig = if sig.header_canonicalization == CanonicalizationMethod::Simple {
304        // Simple: preserve original header name casing
305        format!("DKIM-Signature:{}", stripped)
306    } else {
307        canonicalize_header(
308            sig.header_canonicalization,
309            "dkim-signature",
310            &stripped,
311        )
312    };
313
314    hash_input.extend_from_slice(canon_sig.as_bytes());
315    // Note: NO trailing CRLF on the DKIM-Signature header
316
317    hash_input
318}
319
320/// Strip SPKI wrapper from RSA public key to get PKCS#1 format.
321/// DKIM p= stores SPKI DER. ring expects PKCS#1 for RSA.
322/// If already PKCS#1, returns as-is.
323pub(crate) fn strip_spki_wrapper(spki_der: &[u8]) -> &[u8] {
324    // SPKI structure: SEQUENCE { SEQUENCE { OID, NULL }, BIT STRING { RSAPublicKey } }
325    // Check for SPKI prefix: starts with 0x30 (SEQUENCE), contains RSA OID
326    // OID 1.2.840.113549.1.1.1 = 06 09 2a 86 48 86 f7 0d 01 01 01
327    let rsa_oid: &[u8] = &[0x06, 0x09, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x01, 0x01];
328
329    if spki_der.len() < 24 || spki_der[0] != 0x30 {
330        return spki_der; // Not SPKI, assume PKCS#1
331    }
332
333    // Search for the RSA OID
334    if let Some(oid_pos) = spki_der
335        .windows(rsa_oid.len())
336        .position(|w| w == rsa_oid)
337    {
338        // After OID + NULL, find the BIT STRING
339        let after_oid = oid_pos + rsa_oid.len();
340        // Skip NULL (05 00)
341        let mut pos = after_oid;
342        if pos + 1 < spki_der.len() && spki_der[pos] == 0x05 && spki_der[pos + 1] == 0x00 {
343            pos += 2;
344        }
345        // BIT STRING tag (03)
346        if pos < spki_der.len() && spki_der[pos] == 0x03 {
347            pos += 1;
348            // Parse length
349            let (len, len_bytes) = parse_asn1_length(&spki_der[pos..]);
350            pos += len_bytes;
351            if len > 0 && pos < spki_der.len() {
352                // Skip unused-bits byte (should be 0)
353                pos += 1;
354                // Remaining bytes are the PKCS#1 RSAPublicKey
355                if pos < spki_der.len() {
356                    return &spki_der[pos..];
357                }
358            }
359        }
360    }
361
362    spki_der // Fallback: return as-is
363}
364
365/// Parse ASN.1 DER length encoding. Returns (length, bytes_consumed).
366fn parse_asn1_length(data: &[u8]) -> (usize, usize) {
367    if data.is_empty() {
368        return (0, 0);
369    }
370    if data[0] < 0x80 {
371        (data[0] as usize, 1)
372    } else {
373        let num_bytes = (data[0] & 0x7f) as usize;
374        if num_bytes == 0 || num_bytes > 4 || data.len() < 1 + num_bytes {
375            return (0, 1);
376        }
377        let mut len: usize = 0;
378        for i in 0..num_bytes {
379            len = (len << 8) | (data[1 + i] as usize);
380        }
381        (len, 1 + num_bytes)
382    }
383}
384
385/// Verify the cryptographic signature using ring.
386pub(crate) fn verify_signature(
387    algorithm: &Algorithm,
388    key: &DkimPublicKey,
389    data: &[u8],
390    signature: &[u8],
391) -> Result<(), String> {
392    let ring_algorithm: &dyn ring_sig::VerificationAlgorithm = match algorithm {
393        Algorithm::RsaSha256 => {
394            if key.public_key.len() >= 256 {
395                &ring_sig::RSA_PKCS1_2048_8192_SHA256
396            } else {
397                &ring_sig::RSA_PKCS1_1024_8192_SHA256_FOR_LEGACY_USE_ONLY
398            }
399        }
400        Algorithm::RsaSha1 => {
401            if key.public_key.len() >= 256 {
402                &ring_sig::RSA_PKCS1_2048_8192_SHA1_FOR_LEGACY_USE_ONLY
403            } else {
404                &ring_sig::RSA_PKCS1_1024_8192_SHA1_FOR_LEGACY_USE_ONLY
405            }
406        }
407        Algorithm::Ed25519Sha256 => &ring_sig::ED25519,
408    };
409
410    // For RSA keys, strip SPKI wrapper to get PKCS#1
411    let key_bytes = match key.key_type {
412        KeyType::Rsa => strip_spki_wrapper(&key.public_key),
413        KeyType::Ed25519 => &key.public_key,
414    };
415
416    let public_key = ring_sig::UnparsedPublicKey::new(ring_algorithm, key_bytes);
417    public_key
418        .verify(data, signature)
419        .map_err(|_| "cryptographic signature verification failed".to_string())
420}
421
422#[cfg(test)]
423mod tests {
424    use super::*;
425    use crate::common::dns::mock::MockResolver;
426    use base64::Engine;
427    use ring::rand::SystemRandom;
428    use ring::signature::{Ed25519KeyPair, KeyPair};
429
430    /// Helper: generate Ed25519 key pair using ring, return (pkcs8, public_key_32_bytes).
431    fn gen_ed25519_keypair() -> (Vec<u8>, Vec<u8>) {
432        let rng = SystemRandom::new();
433        let pkcs8 = Ed25519KeyPair::generate_pkcs8(&rng).unwrap();
434        let key_pair = Ed25519KeyPair::from_pkcs8(pkcs8.as_ref()).unwrap();
435        let public_key = key_pair.public_key().as_ref().to_vec();
436        (pkcs8.as_ref().to_vec(), public_key)
437    }
438
439    /// Helper: sign data with Ed25519 key pair.
440    fn ed25519_sign(pkcs8: &[u8], data: &[u8]) -> Vec<u8> {
441        let key_pair = Ed25519KeyPair::from_pkcs8(pkcs8).unwrap();
442        key_pair.sign(data).as_ref().to_vec()
443    }
444
445    /// Helper: compute body hash for a given body, algorithm, and canonicalization.
446    fn compute_body_hash(
447        body: &[u8],
448        algorithm: Algorithm,
449        body_canon: CanonicalizationMethod,
450    ) -> String {
451        let normalized = normalize_line_endings(body);
452        let canonicalized = canonicalize_body(body_canon, &normalized);
453        let hash = compute_hash(algorithm, &canonicalized);
454        base64::engine::general_purpose::STANDARD.encode(&hash)
455    }
456
457    /// Helper: compute the header hash input manually (ground-truth).
458    fn compute_header_input_manual(
459        headers: &[(&str, &str)],
460        signed_header_names: &[&str],
461        sig_header_value_stripped: &str,
462        header_canon: CanonicalizationMethod,
463    ) -> Vec<u8> {
464        let signed: Vec<String> = signed_header_names.iter().map(|s| s.to_string()).collect();
465        let selected = select_headers(header_canon, &signed, headers);
466
467        let mut input = Vec::new();
468        for h in &selected {
469            input.extend_from_slice(h.as_bytes());
470        }
471
472        // Append DKIM-Signature header (canonicalized, no trailing CRLF)
473        let canon_sig =
474            canonicalize_header(header_canon, "dkim-signature", sig_header_value_stripped);
475        let canon_sig = if header_canon == CanonicalizationMethod::Simple {
476            format!("DKIM-Signature:{}", sig_header_value_stripped)
477        } else {
478            canon_sig
479        };
480        input.extend_from_slice(canon_sig.as_bytes());
481        input
482    }
483
484    fn setup_mock_key(resolver: &mut MockResolver, selector: &str, domain: &str, key_record: &str) {
485        let query = format!("{}._domainkey.{}", selector, domain);
486        resolver.add_txt(&query, vec![key_record.to_string()]);
487    }
488
489    // ─── CHK-367..CHK-370: Signature extraction ─────────────────────
490
491    // CHK-494: No DKIM-Signature → None
492    #[tokio::test]
493    async fn no_dkim_signature_returns_none() {
494        let resolver = MockResolver::new();
495        let verifier = DkimVerifier::new(resolver);
496        let headers: Vec<(&str, &str)> = vec![
497            ("From", " user@example.com"),
498            ("Subject", " test"),
499        ];
500        let results = verifier.verify_message(&headers, b"body").await;
501        assert_eq!(results.len(), 1);
502        assert_eq!(results[0], DkimResult::None);
503    }
504
505    // CHK-367: Find DKIM-Signature headers
506    // CHK-370: One result per signature
507    #[tokio::test]
508    async fn multiple_signatures_return_multiple_results() {
509        let resolver = MockResolver::new();
510        // Both signatures will fail DNS lookup
511        let headers: Vec<(&str, &str)> = vec![
512            ("From", " user@example.com"),
513            ("DKIM-Signature", " v=1; a=rsa-sha256; b=dGVzdA==; bh=dGVzdA==; d=a.com; h=from; s=s1"),
514            ("DKIM-Signature", " v=1; a=rsa-sha256; b=dGVzdA==; bh=dGVzdA==; d=b.com; h=from; s=s1"),
515        ];
516        // Leave DNS unconfigured → NxDomain → KeyNotFound
517        let verifier = DkimVerifier::new(resolver);
518        let results = verifier.verify_message(&headers, b"body").await;
519        assert_eq!(results.len(), 2);
520    }
521
522    // CHK-368, CHK-369: Parse each, malformed → PermFail
523    #[tokio::test]
524    async fn malformed_signature_returns_permfail() {
525        let resolver = MockResolver::new();
526        let verifier = DkimVerifier::new(resolver);
527        let headers: Vec<(&str, &str)> = vec![
528            ("From", " user@example.com"),
529            ("DKIM-Signature", " not-a-valid-signature"),
530        ];
531        let results = verifier.verify_message(&headers, b"body").await;
532        assert_eq!(results.len(), 1);
533        match &results[0] {
534            DkimResult::PermFail { kind, .. } => {
535                assert_eq!(*kind, PermFailKind::MalformedSignature);
536            }
537            _ => panic!("expected PermFail"),
538        }
539    }
540
541    // ─── CHK-371..CHK-376: DNS key lookup ───────────────────────────
542
543    // CHK-487: Key not found
544    #[tokio::test]
545    async fn key_not_found_nxdomain() {
546        let resolver = MockResolver::new();
547        let verifier = DkimVerifier::new(resolver);
548        let headers: Vec<(&str, &str)> = vec![
549            ("From", " user@example.com"),
550            ("DKIM-Signature", " v=1; a=rsa-sha256; b=dGVzdA==; bh=dGVzdA==; d=example.com; h=from; s=sel1"),
551        ];
552        let results = verifier.verify_message(&headers, b"body").await;
553        match &results[0] {
554            DkimResult::PermFail { kind, .. } => assert_eq!(*kind, PermFailKind::KeyNotFound),
555            _ => panic!("expected PermFail KeyNotFound"),
556        }
557    }
558
559    // CHK-493: DNS temp failure
560    #[tokio::test]
561    async fn dns_temp_failure() {
562        let mut resolver = MockResolver::new();
563        resolver.add_txt_err("sel1._domainkey.example.com", DnsError::TempFail);
564        let verifier = DkimVerifier::new(resolver);
565        let headers: Vec<(&str, &str)> = vec![
566            ("From", " user@example.com"),
567            ("DKIM-Signature", " v=1; a=rsa-sha256; b=dGVzdA==; bh=dGVzdA==; d=example.com; h=from; s=sel1"),
568        ];
569        let results = verifier.verify_message(&headers, b"body").await;
570        match &results[0] {
571            DkimResult::TempFail { .. } => {}
572            _ => panic!("expected TempFail"),
573        }
574    }
575
576    // CHK-488: Key revoked
577    #[tokio::test]
578    async fn key_revoked_empty_p() {
579        let mut resolver = MockResolver::new();
580        setup_mock_key(&mut resolver, "sel1", "example.com", "v=DKIM1; p=");
581        let verifier = DkimVerifier::new(resolver);
582        let headers: Vec<(&str, &str)> = vec![
583            ("From", " user@example.com"),
584            ("DKIM-Signature", " v=1; a=rsa-sha256; b=dGVzdA==; bh=dGVzdA==; d=example.com; h=from; s=sel1"),
585        ];
586        let results = verifier.verify_message(&headers, b"body").await;
587        match &results[0] {
588            DkimResult::PermFail { kind, .. } => assert_eq!(*kind, PermFailKind::KeyRevoked),
589            _ => panic!("expected PermFail KeyRevoked"),
590        }
591    }
592
593    // ─── CHK-377..CHK-381: Key constraints ──────────────────────────
594
595    // CHK-489: h= rejects algo
596    #[tokio::test]
597    async fn key_h_rejects_algorithm() {
598        let mut resolver = MockResolver::new();
599        let p = base64::engine::general_purpose::STANDARD.encode(vec![0x30u8; 162]);
600        setup_mock_key(
601            &mut resolver,
602            "sel1",
603            "example.com",
604            &format!("v=DKIM1; h=sha1; k=rsa; p={}", p),
605        );
606        let verifier = DkimVerifier::new(resolver);
607        // Signature uses rsa-sha256 but key only allows sha1
608        let headers: Vec<(&str, &str)> = vec![
609            ("From", " user@example.com"),
610            ("DKIM-Signature", " v=1; a=rsa-sha256; b=dGVzdA==; bh=dGVzdA==; d=example.com; h=from; s=sel1"),
611        ];
612        let results = verifier.verify_message(&headers, b"body").await;
613        match &results[0] {
614            DkimResult::PermFail { kind, .. } => assert_eq!(*kind, PermFailKind::HashNotPermitted),
615            _ => panic!("expected PermFail HashNotPermitted"),
616        }
617    }
618
619    // CHK-490: s= rejects email
620    #[tokio::test]
621    async fn key_s_rejects_email() {
622        let mut resolver = MockResolver::new();
623        let p = base64::engine::general_purpose::STANDARD.encode(vec![0x30u8; 162]);
624        setup_mock_key(
625            &mut resolver,
626            "sel1",
627            "example.com",
628            &format!("v=DKIM1; s=other; k=rsa; p={}", p),
629        );
630        let verifier = DkimVerifier::new(resolver);
631        let headers: Vec<(&str, &str)> = vec![
632            ("From", " user@example.com"),
633            ("DKIM-Signature", " v=1; a=rsa-sha256; b=dGVzdA==; bh=dGVzdA==; d=example.com; h=from; s=sel1"),
634        ];
635        let results = verifier.verify_message(&headers, b"body").await;
636        match &results[0] {
637            DkimResult::PermFail { kind, .. } => {
638                assert_eq!(*kind, PermFailKind::ServiceTypeMismatch)
639            }
640            _ => panic!("expected PermFail ServiceTypeMismatch"),
641        }
642    }
643
644    // CHK-491: t=s strict
645    #[tokio::test]
646    async fn key_strict_mode_violation() {
647        let mut resolver = MockResolver::new();
648        let p = base64::engine::general_purpose::STANDARD.encode(vec![0x30u8; 162]);
649        setup_mock_key(
650            &mut resolver,
651            "sel1",
652            "example.com",
653            &format!("v=DKIM1; t=s; k=rsa; p={}", p),
654        );
655        let verifier = DkimVerifier::new(resolver);
656        // i= defaults to @example.com, but let's use a subdomain
657        let headers: Vec<(&str, &str)> = vec![
658            ("From", " user@sub.example.com"),
659            ("DKIM-Signature", " v=1; a=rsa-sha256; b=dGVzdA==; bh=dGVzdA==; d=example.com; h=from; s=sel1; i=user@sub.example.com"),
660        ];
661        let results = verifier.verify_message(&headers, b"body").await;
662        match &results[0] {
663            DkimResult::PermFail { kind, .. } => {
664                assert_eq!(*kind, PermFailKind::StrictModeViolation)
665            }
666            _ => panic!("expected PermFail StrictModeViolation"),
667        }
668    }
669
670    // CHK-492: Algorithm/key mismatch
671    #[tokio::test]
672    async fn algorithm_key_type_mismatch() {
673        let mut resolver = MockResolver::new();
674        let p = base64::engine::general_purpose::STANDARD.encode(vec![0xABu8; 32]);
675        // Key is ed25519 but signature says rsa-sha256
676        setup_mock_key(
677            &mut resolver,
678            "sel1",
679            "example.com",
680            &format!("v=DKIM1; k=ed25519; p={}", p),
681        );
682        let verifier = DkimVerifier::new(resolver);
683        let headers: Vec<(&str, &str)> = vec![
684            ("From", " user@example.com"),
685            ("DKIM-Signature", " v=1; a=rsa-sha256; b=dGVzdA==; bh=dGVzdA==; d=example.com; h=from; s=sel1"),
686        ];
687        let results = verifier.verify_message(&headers, b"body").await;
688        match &results[0] {
689            DkimResult::PermFail { kind, .. } => {
690                assert_eq!(*kind, PermFailKind::AlgorithmMismatch)
691            }
692            _ => panic!("expected PermFail AlgorithmMismatch"),
693        }
694    }
695
696    // ─── CHK-382..CHK-384: Expiration ───────────────────────────────
697
698    // CHK-486: Expired signature
699    #[tokio::test]
700    async fn expired_signature() {
701        let resolver = MockResolver::new();
702        let verifier = DkimVerifier::new(resolver);
703        // x=1000000 is well in the past
704        let headers: Vec<(&str, &str)> = vec![
705            ("From", " user@example.com"),
706            ("DKIM-Signature", " v=1; a=rsa-sha256; b=dGVzdA==; bh=dGVzdA==; d=example.com; h=from; s=sel1; x=1000000"),
707        ];
708        let results = verifier.verify_message(&headers, b"body").await;
709        match &results[0] {
710            DkimResult::PermFail { kind, .. } => {
711                assert_eq!(*kind, PermFailKind::ExpiredSignature)
712            }
713            _ => panic!("expected PermFail ExpiredSignature"),
714        }
715    }
716
717    // CHK-XXX: x= expiration before t= timestamp → PermFail MalformedSignature
718    #[tokio::test]
719    async fn expiration_before_timestamp() {
720        let resolver = MockResolver::new();
721        let verifier = DkimVerifier::new(resolver);
722        // t=2000000, x=1000000: x < t → malformed per RFC 6376 §3.5
723        let headers: Vec<(&str, &str)> = vec![
724            ("From", " user@example.com"),
725            ("DKIM-Signature", " v=1; a=ed25519-sha256; b=dGVzdA==; bh=dGVzdA==; d=example.com; h=from; s=sel1; t=2000000; x=1000000"),
726        ];
727        let results = verifier.verify_message(&headers, b"body").await;
728        match &results[0] {
729            DkimResult::PermFail { kind, .. } => {
730                assert_eq!(*kind, PermFailKind::MalformedSignature)
731            }
732            other => panic!("expected PermFail MalformedSignature, got {:?}", other),
733        }
734    }
735
736    // ─── CHK-481: Ed25519 → Pass (ground truth) ─────────────────────
737
738    #[tokio::test]
739    async fn ed25519_pass_ground_truth() {
740        let (pkcs8, public_key) = gen_ed25519_keypair();
741        let body = b"Hello DKIM\r\n";
742        let domain = "example.com";
743        let selector = "ed";
744
745        // Compute body hash (relaxed/relaxed)
746        let bh = compute_body_hash(body, Algorithm::Ed25519Sha256, CanonicalizationMethod::Relaxed);
747
748        // Build signature header template (without real signature)
749        let sig_header_template = format!(
750            " v=1; a=ed25519-sha256; b=; bh={}; c=relaxed/relaxed; d={}; h=from; s={}",
751            bh, domain, selector
752        );
753
754        // Compute header hash input
755        let header_input = compute_header_input_manual(
756            &[("From", " user@example.com")],
757            &["from"],
758            &sig_header_template,
759            CanonicalizationMethod::Relaxed,
760        );
761
762        // Sign with Ed25519
763        let signature = ed25519_sign(&pkcs8, &header_input);
764        let sig_b64 = base64::engine::general_purpose::STANDARD.encode(&signature);
765
766        // Build final signature header
767        let final_sig_header = format!(
768            " v=1; a=ed25519-sha256; b={}; bh={}; c=relaxed/relaxed; d={}; h=from; s={}",
769            sig_b64, bh, domain, selector
770        );
771
772        // Setup DNS
773        let mut resolver = MockResolver::new();
774        let p = base64::engine::general_purpose::STANDARD.encode(&public_key);
775        setup_mock_key(&mut resolver, selector, domain, &format!("k=ed25519; p={}", p));
776
777        let verifier = DkimVerifier::new(resolver);
778        let headers: Vec<(&str, &str)> = vec![
779            ("From", " user@example.com"),
780            ("DKIM-Signature", &final_sig_header),
781        ];
782
783        let results = verifier.verify_message(&headers, body).await;
784        match &results[0] {
785            DkimResult::Pass { domain: d, selector: s, testing } => {
786                assert_eq!(d, domain);
787                assert_eq!(s, selector);
788                assert!(!testing);
789            }
790            other => panic!("expected Pass, got {:?}", other),
791        }
792    }
793
794    // ─── CHK-484: Tampered body ─────────────────────────────────────
795
796    #[tokio::test]
797    async fn ed25519_tampered_body() {
798        let (pkcs8, public_key) = gen_ed25519_keypair();
799        let body = b"Hello DKIM\r\n";
800        let domain = "example.com";
801        let selector = "ed";
802
803        let bh = compute_body_hash(body, Algorithm::Ed25519Sha256, CanonicalizationMethod::Relaxed);
804        let sig_header_template = format!(
805            " v=1; a=ed25519-sha256; b=; bh={}; c=relaxed/relaxed; d={}; h=from; s={}",
806            bh, domain, selector
807        );
808
809        let header_input = compute_header_input_manual(
810            &[("From", " user@example.com")],
811            &["from"],
812            &sig_header_template,
813            CanonicalizationMethod::Relaxed,
814        );
815
816        let signature = ed25519_sign(&pkcs8, &header_input);
817        let sig_b64 = base64::engine::general_purpose::STANDARD.encode(&signature);
818
819        let final_sig_header = format!(
820            " v=1; a=ed25519-sha256; b={}; bh={}; c=relaxed/relaxed; d={}; h=from; s={}",
821            sig_b64, bh, domain, selector
822        );
823
824        let mut resolver = MockResolver::new();
825        let p = base64::engine::general_purpose::STANDARD.encode(&public_key);
826        setup_mock_key(&mut resolver, selector, domain, &format!("k=ed25519; p={}", p));
827
828        let verifier = DkimVerifier::new(resolver);
829        let headers: Vec<(&str, &str)> = vec![
830            ("From", " user@example.com"),
831            ("DKIM-Signature", &final_sig_header),
832        ];
833
834        // Tampered body
835        let results = verifier.verify_message(&headers, b"TAMPERED BODY\r\n").await;
836        match &results[0] {
837            DkimResult::Fail { kind, .. } => assert_eq!(*kind, FailureKind::BodyHashMismatch),
838            other => panic!("expected Fail BodyHashMismatch, got {:?}", other),
839        }
840    }
841
842    // ─── CHK-485: Tampered header ───────────────────────────────────
843
844    #[tokio::test]
845    async fn ed25519_tampered_header() {
846        let (pkcs8, public_key) = gen_ed25519_keypair();
847        let body = b"Hello DKIM\r\n";
848        let domain = "example.com";
849        let selector = "ed";
850
851        let bh = compute_body_hash(body, Algorithm::Ed25519Sha256, CanonicalizationMethod::Relaxed);
852        let sig_header_template = format!(
853            " v=1; a=ed25519-sha256; b=; bh={}; c=relaxed/relaxed; d={}; h=from:subject; s={}",
854            bh, domain, selector
855        );
856
857        let header_input = compute_header_input_manual(
858            &[("From", " user@example.com"), ("Subject", " original")],
859            &["from", "subject"],
860            &sig_header_template,
861            CanonicalizationMethod::Relaxed,
862        );
863
864        let signature = ed25519_sign(&pkcs8, &header_input);
865        let sig_b64 = base64::engine::general_purpose::STANDARD.encode(&signature);
866
867        let final_sig_header = format!(
868            " v=1; a=ed25519-sha256; b={}; bh={}; c=relaxed/relaxed; d={}; h=from:subject; s={}",
869            sig_b64, bh, domain, selector
870        );
871
872        let mut resolver = MockResolver::new();
873        let p = base64::engine::general_purpose::STANDARD.encode(&public_key);
874        setup_mock_key(&mut resolver, selector, domain, &format!("k=ed25519; p={}", p));
875
876        let verifier = DkimVerifier::new(resolver);
877        // Subject is different from what was signed
878        let headers: Vec<(&str, &str)> = vec![
879            ("From", " user@example.com"),
880            ("Subject", " TAMPERED"),
881            ("DKIM-Signature", &final_sig_header),
882        ];
883
884        let results = verifier.verify_message(&headers, body).await;
885        match &results[0] {
886            DkimResult::Fail { kind, .. } => {
887                assert_eq!(*kind, FailureKind::SignatureVerificationFailed)
888            }
889            other => panic!("expected Fail SigVerificationFailed, got {:?}", other),
890        }
891    }
892
893    // ─── CHK-495: simple/simple e2e ─────────────────────────────────
894
895    #[tokio::test]
896    async fn ed25519_simple_simple_e2e() {
897        let (pkcs8, public_key) = gen_ed25519_keypair();
898        let body = b"Simple body content\r\n";
899        let domain = "example.com";
900        let selector = "ed";
901
902        let bh = compute_body_hash(body, Algorithm::Ed25519Sha256, CanonicalizationMethod::Simple);
903        let sig_header_template = format!(
904            " v=1; a=ed25519-sha256; b=; bh={}; c=simple/simple; d={}; h=from; s={}",
905            bh, domain, selector
906        );
907
908        let header_input = compute_header_input_manual(
909            &[("From", " user@example.com")],
910            &["from"],
911            &sig_header_template,
912            CanonicalizationMethod::Simple,
913        );
914
915        let signature = ed25519_sign(&pkcs8, &header_input);
916        let sig_b64 = base64::engine::general_purpose::STANDARD.encode(&signature);
917
918        let final_sig_header = format!(
919            " v=1; a=ed25519-sha256; b={}; bh={}; c=simple/simple; d={}; h=from; s={}",
920            sig_b64, bh, domain, selector
921        );
922
923        let mut resolver = MockResolver::new();
924        let p = base64::engine::general_purpose::STANDARD.encode(&public_key);
925        setup_mock_key(&mut resolver, selector, domain, &format!("k=ed25519; p={}", p));
926
927        let verifier = DkimVerifier::new(resolver);
928        let headers: Vec<(&str, &str)> = vec![
929            ("From", " user@example.com"),
930            ("DKIM-Signature", &final_sig_header),
931        ];
932
933        let results = verifier.verify_message(&headers, body).await;
934        match &results[0] {
935            DkimResult::Pass { .. } => {}
936            other => panic!("expected Pass, got {:?}", other),
937        }
938    }
939
940    // ─── CHK-496: relaxed/relaxed e2e ───────────────────────────────
941
942    #[tokio::test]
943    async fn ed25519_relaxed_relaxed_e2e() {
944        let (pkcs8, public_key) = gen_ed25519_keypair();
945        let body = b"Relaxed body  content  \r\n";
946        let domain = "example.com";
947        let selector = "ed";
948
949        let bh = compute_body_hash(body, Algorithm::Ed25519Sha256, CanonicalizationMethod::Relaxed);
950        let sig_header_template = format!(
951            " v=1; a=ed25519-sha256; b=; bh={}; c=relaxed/relaxed; d={}; h=from; s={}",
952            bh, domain, selector
953        );
954
955        let header_input = compute_header_input_manual(
956            &[("From", " user@example.com")],
957            &["from"],
958            &sig_header_template,
959            CanonicalizationMethod::Relaxed,
960        );
961
962        let signature = ed25519_sign(&pkcs8, &header_input);
963        let sig_b64 = base64::engine::general_purpose::STANDARD.encode(&signature);
964
965        let final_sig_header = format!(
966            " v=1; a=ed25519-sha256; b={}; bh={}; c=relaxed/relaxed; d={}; h=from; s={}",
967            sig_b64, bh, domain, selector
968        );
969
970        let mut resolver = MockResolver::new();
971        let p = base64::engine::general_purpose::STANDARD.encode(&public_key);
972        setup_mock_key(&mut resolver, selector, domain, &format!("k=ed25519; p={}", p));
973
974        let verifier = DkimVerifier::new(resolver);
975        let headers: Vec<(&str, &str)> = vec![
976            ("From", " user@example.com"),
977            ("DKIM-Signature", &final_sig_header),
978        ];
979
980        let results = verifier.verify_message(&headers, body).await;
981        match &results[0] {
982            DkimResult::Pass { .. } => {}
983            other => panic!("expected Pass, got {:?}", other),
984        }
985    }
986
987    // ─── CHK-497: Over-signed verify ────────────────────────────────
988
989    #[tokio::test]
990    async fn ed25519_over_signed_verify() {
991        let (pkcs8, public_key) = gen_ed25519_keypair();
992        let body = b"body\r\n";
993        let domain = "example.com";
994        let selector = "ed";
995
996        let bh = compute_body_hash(body, Algorithm::Ed25519Sha256, CanonicalizationMethod::Relaxed);
997        // h= lists "from" twice (over-sign)
998        let sig_header_template = format!(
999            " v=1; a=ed25519-sha256; b=; bh={}; c=relaxed/relaxed; d={}; h=from:from; s={}",
1000            bh, domain, selector
1001        );
1002
1003        let header_input = compute_header_input_manual(
1004            &[("From", " user@example.com")],
1005            &["from", "from"],
1006            &sig_header_template,
1007            CanonicalizationMethod::Relaxed,
1008        );
1009
1010        let signature = ed25519_sign(&pkcs8, &header_input);
1011        let sig_b64 = base64::engine::general_purpose::STANDARD.encode(&signature);
1012
1013        let final_sig_header = format!(
1014            " v=1; a=ed25519-sha256; b={}; bh={}; c=relaxed/relaxed; d={}; h=from:from; s={}",
1015            sig_b64, bh, domain, selector
1016        );
1017
1018        let mut resolver = MockResolver::new();
1019        let p = base64::engine::general_purpose::STANDARD.encode(&public_key);
1020        setup_mock_key(&mut resolver, selector, domain, &format!("k=ed25519; p={}", p));
1021
1022        let verifier = DkimVerifier::new(resolver);
1023        let headers: Vec<(&str, &str)> = vec![
1024            ("From", " user@example.com"),
1025            ("DKIM-Signature", &final_sig_header),
1026        ];
1027
1028        let results = verifier.verify_message(&headers, body).await;
1029        match &results[0] {
1030            DkimResult::Pass { .. } => {}
1031            other => panic!("expected Pass with over-signing, got {:?}", other),
1032        }
1033    }
1034
1035    // ─── CHK-498..CHK-501: Ground-truth tests ───────────────────────
1036
1037    // CHK-498: Manual ring primitives (ground-truth)
1038    // CHK-499: Full DkimVerifier pipeline
1039    // CHK-500: Catch self-consistent bugs
1040    #[tokio::test]
1041    async fn ground_truth_ed25519_manual_ring_primitives() {
1042        // This test constructs a DKIM signature MANUALLY using ring primitives,
1043        // bypassing any DkimSigner, then verifies through DkimVerifier.
1044        let (pkcs8, public_key) = gen_ed25519_keypair();
1045        let body = b"Ground truth test body\r\n";
1046        let domain = "gt.example.com";
1047        let selector = "gtsel";
1048
1049        // Step 1: Compute body hash manually
1050        let normalized_body = normalize_line_endings(body);
1051        let canon_body = canonicalize_body(CanonicalizationMethod::Relaxed, &normalized_body);
1052        let body_hash = ring::digest::digest(&ring::digest::SHA256, &canon_body);
1053        let bh_b64 = base64::engine::general_purpose::STANDARD.encode(body_hash.as_ref());
1054
1055        // Step 2: Build sig header template (b= empty)
1056        let sig_template = format!(
1057            " v=1; a=ed25519-sha256; b=; bh={}; c=relaxed/relaxed; d={}; h=from:to; s={}",
1058            bh_b64, domain, selector
1059        );
1060
1061        // Step 3: Compute header hash input manually
1062        let msg_headers = vec![
1063            ("From", " sender@gt.example.com"),
1064            ("To", " receiver@gt.example.com"),
1065        ];
1066        let header_input = compute_header_input_manual(
1067            &msg_headers,
1068            &["from", "to"],
1069            &sig_template,
1070            CanonicalizationMethod::Relaxed,
1071        );
1072
1073        // Step 4: Sign with ring Ed25519
1074        let key_pair = Ed25519KeyPair::from_pkcs8(&pkcs8).unwrap();
1075        let sig_bytes = key_pair.sign(&header_input);
1076        let sig_b64 = base64::engine::general_purpose::STANDARD.encode(sig_bytes.as_ref());
1077
1078        // Step 5: Build final header
1079        let final_sig = format!(
1080            " v=1; a=ed25519-sha256; b={}; bh={}; c=relaxed/relaxed; d={}; h=from:to; s={}",
1081            sig_b64, bh_b64, domain, selector
1082        );
1083
1084        // Step 6: Setup DNS and verify
1085        let mut resolver = MockResolver::new();
1086        let p_b64 = base64::engine::general_purpose::STANDARD.encode(&public_key);
1087        setup_mock_key(&mut resolver, selector, domain, &format!("k=ed25519; p={}", p_b64));
1088
1089        let verifier = DkimVerifier::new(resolver);
1090        let full_headers: Vec<(&str, &str)> = vec![
1091            ("From", " sender@gt.example.com"),
1092            ("To", " receiver@gt.example.com"),
1093            ("DKIM-Signature", &final_sig),
1094        ];
1095
1096        let results = verifier.verify_message(&full_headers, body).await;
1097        match &results[0] {
1098            DkimResult::Pass { domain: d, .. } => assert_eq!(d, domain),
1099            other => panic!("Ground-truth test failed: {:?}", other),
1100        }
1101    }
1102
1103    // CHK-501: Ed25519 ground-truth + tampered body
1104    #[tokio::test]
1105    async fn ground_truth_ed25519_tampered() {
1106        let (pkcs8, public_key) = gen_ed25519_keypair();
1107        let body = b"Original body\r\n";
1108        let domain = "gt.example.com";
1109        let selector = "gtsel";
1110
1111        let normalized_body = normalize_line_endings(body);
1112        let canon_body = canonicalize_body(CanonicalizationMethod::Relaxed, &normalized_body);
1113        let body_hash = ring::digest::digest(&ring::digest::SHA256, &canon_body);
1114        let bh_b64 = base64::engine::general_purpose::STANDARD.encode(body_hash.as_ref());
1115
1116        let sig_template = format!(
1117            " v=1; a=ed25519-sha256; b=; bh={}; c=relaxed/relaxed; d={}; h=from; s={}",
1118            bh_b64, domain, selector
1119        );
1120
1121        let header_input = compute_header_input_manual(
1122            &[("From", " sender@gt.example.com")],
1123            &["from"],
1124            &sig_template,
1125            CanonicalizationMethod::Relaxed,
1126        );
1127
1128        let key_pair = Ed25519KeyPair::from_pkcs8(&pkcs8).unwrap();
1129        let sig_bytes = key_pair.sign(&header_input);
1130        let sig_b64 = base64::engine::general_purpose::STANDARD.encode(sig_bytes.as_ref());
1131
1132        let final_sig = format!(
1133            " v=1; a=ed25519-sha256; b={}; bh={}; c=relaxed/relaxed; d={}; h=from; s={}",
1134            sig_b64, bh_b64, domain, selector
1135        );
1136
1137        let mut resolver = MockResolver::new();
1138        let p_b64 = base64::engine::general_purpose::STANDARD.encode(&public_key);
1139        setup_mock_key(&mut resolver, selector, domain, &format!("k=ed25519; p={}", p_b64));
1140
1141        let verifier = DkimVerifier::new(resolver);
1142        let full_headers: Vec<(&str, &str)> = vec![
1143            ("From", " sender@gt.example.com"),
1144            ("DKIM-Signature", &final_sig),
1145        ];
1146
1147        // Tampered body
1148        let results = verifier
1149            .verify_message(&full_headers, b"Tampered body\r\n")
1150            .await;
1151        match &results[0] {
1152            DkimResult::Fail { kind, .. } => assert_eq!(*kind, FailureKind::BodyHashMismatch),
1153            other => panic!("expected BodyHashMismatch, got {:?}", other),
1154        }
1155    }
1156
1157    // ─── CHK-531: Ground-truth complete ─────────────────────────────
1158    // (covered by the ground_truth_ed25519_manual_ring_primitives and _tampered tests above)
1159
1160    // ─── CHK-510..CHK-517: Security ─────────────────────────────────
1161    // (Most security properties are verified by the constraint/verification tests above)
1162
1163    // ─── CHK-482: RSA-SHA256 pre-computed fixture ─────────────────
1164    // Generated with OpenSSL RSA-2048 key. This is a TRUE pre-computed fixture:
1165    // signing was done externally (openssl dgst -sha256 -sign), not by this library.
1166    // The SPKI public key exercises strip_spki_wrapper with real ASN.1 data.
1167
1168    const RSA_FIXTURE_SPKI_B64: &str = "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0zzOtswuqB21FtQ+W5cgh7DiaJ+4TMQBmSm3V6gKYSq/WPbg0vaSdlru6PBbdocwBrn4di2bNpdy5Co1ujLtogyg6+f4A9K36CygLWqOhygt6A/Rl94daXwew1S/7oSksuAGnSg2+XMuU2An+IHSYnx/qAiGnnzkhGgsTnUnJxZ2mitvKemPjTDIB2dz1hJAmnJS0ffUADnSXgS55f8aXAdRRDQwYlTwBRLrdpcQzVRKU+L/hm4EzePVkvXUgeuBKqhIosNHl28fuN1nac3zuosWorJQ7Ox2MKKdVB5FkT85mZp/i7L0+/JMVXJeNHnlFe3OqFUEKmYpgL37oTGCdQIDAQAB";
1169
1170    #[tokio::test]
1171    async fn rsa_sha256_precomputed_fixture_pass() {
1172        let body = b"Test body for RSA verification\r\n";
1173        let sig_value = concat!(
1174            " v=1; a=rsa-sha256; b=zm5IJ/e9WakQhZ+pmKQafoSc2iZE2xGfYA7sbWF+O8vhES09D7HyUo",
1175            "sQVnG4fm6mHOc6pHLtTaQDe/4r0tOjjI7peVO8BCi3KSQtKZIORJ8wrs3PQLpZtZdK/zlfIZywW0",
1176            "n5DMxbHU+uqjkR4y191xYg/fWZaC14d/4V5RvzKb8ZV7qYzpi5EWDlXTCbJTryuJydjRVYIa1F+6",
1177            "cI3ROJn8U9GcyGcJJQo5HrrWYKAiPGhR3sXjKbBEOah7CH5XQv22j4Q3q2LhNjtTnXrS77rvw8lu",
1178            "b+H0e8vEB4Ps4Y9y81QPGqs9Xse2MakBVER44/1M4XvlpS+5bD4bUZfYG5cQ==; bh=A82pV6ef4",
1179            "eO/+6HFHShh58CZ7NYOh4gNm0JUpCe9AJU=; c=relaxed/relaxed; d=example.com; h=fro",
1180            "m:to:subject; s=rsa2048",
1181        );
1182
1183        let mut resolver = MockResolver::new();
1184        setup_mock_key(
1185            &mut resolver,
1186            "rsa2048",
1187            "example.com",
1188            &format!("v=DKIM1; k=rsa; p={}", RSA_FIXTURE_SPKI_B64),
1189        );
1190
1191        let verifier = DkimVerifier::new(resolver);
1192        let headers: Vec<(&str, &str)> = vec![
1193            ("From", " user@example.com"),
1194            ("To", " recipient@example.com"),
1195            ("Subject", " RSA test"),
1196            ("DKIM-Signature", sig_value),
1197        ];
1198
1199        let results = verifier.verify_message(&headers, body).await;
1200        match &results[0] {
1201            DkimResult::Pass { domain, selector, .. } => {
1202                assert_eq!(domain, "example.com");
1203                assert_eq!(selector, "rsa2048");
1204            }
1205            other => panic!("RSA-SHA256 fixture: expected Pass, got {:?}", other),
1206        }
1207    }
1208
1209    // ─── CHK-483: RSA-SHA1 pre-computed fixture ─────────────────────
1210    // ring 0.17 cannot sign SHA-1. This fixture was signed externally with openssl dgst -sha1 -sign.
1211
1212    #[tokio::test]
1213    async fn rsa_sha1_precomputed_fixture_pass() {
1214        let body = b"Test body for RSA verification\r\n";
1215        let sig_value = concat!(
1216            " v=1; a=rsa-sha1; b=Y9CjLLQ3d8kw7z7FnjDF7YDbD5jV8F4nmNN2IP7HcIIJFMmEdvE2+mMH",
1217            "OulTI26Kp7x+r0aubcmOAvOUh1eFX2t7359bnVL9n1MEKIcxdZO3fIU5LhXBAfrkILe/caA5hQgU",
1218            "94HdPiOyGUNIQdGIG4ECZ6zdcW1K4TVYQmGawJzwKyKo1m4MqT99bJot5MUEmK/7jX9aROrDtwok",
1219            "qtFAysXpmqWj3lOg+IJSmiKzD0DvbvU1G/LE4T95zjnot+rBtC0/jJ/ooq0ZBBOvC5KHQ0pwDxCC",
1220            "ENR18UkcyZG/6FRLFGGzReJPQViJ4XqBNpDOovEXj3v4q9tdBmNt5zNKzQ==; bh=wIO7ahU/Pub",
1221            "98XWknH1rIcruxRc=; c=relaxed/relaxed; d=example.com; h=from:to:subject; s=rs",
1222            "a2048",
1223        );
1224
1225        let mut resolver = MockResolver::new();
1226        setup_mock_key(
1227            &mut resolver,
1228            "rsa2048",
1229            "example.com",
1230            &format!("v=DKIM1; k=rsa; p={}", RSA_FIXTURE_SPKI_B64),
1231        );
1232
1233        let verifier = DkimVerifier::new(resolver);
1234        let headers: Vec<(&str, &str)> = vec![
1235            ("From", " user@example.com"),
1236            ("To", " recipient@example.com"),
1237            ("Subject", " RSA test"),
1238            ("DKIM-Signature", sig_value),
1239        ];
1240
1241        let results = verifier.verify_message(&headers, body).await;
1242        match &results[0] {
1243            DkimResult::Pass { domain, selector, .. } => {
1244                assert_eq!(domain, "example.com");
1245                assert_eq!(selector, "rsa2048");
1246            }
1247            other => panic!("RSA-SHA1 fixture: expected Pass, got {:?}", other),
1248        }
1249    }
1250
1251    // ─── CHK-529: All three algorithms verified ─────────────────────
1252    // Ed25519: ed25519_pass_ground_truth
1253    // RSA-SHA256: rsa_sha256_precomputed_fixture_pass
1254    // RSA-SHA1: rsa_sha1_precomputed_fixture_pass
1255
1256    // ─── SPKI stripping unit tests ──────────────────────────────────
1257
1258    #[test]
1259    fn strip_spki_passthrough_pkcs1() {
1260        // Non-SPKI data returned as-is
1261        let data = vec![0x30, 0x82, 0x00]; // looks like ASN.1 but no RSA OID
1262        assert_eq!(strip_spki_wrapper(&data), data.as_slice());
1263    }
1264
1265    #[test]
1266    fn strip_spki_too_short() {
1267        let data = vec![0x30; 10];
1268        assert_eq!(strip_spki_wrapper(&data), data.as_slice());
1269    }
1270
1271    #[test]
1272    fn strip_spki_real_rsa_2048_key() {
1273        // Test SPKI stripping with the real RSA-2048 SPKI key used in fixtures
1274        let spki_der = base64::engine::general_purpose::STANDARD
1275            .decode(RSA_FIXTURE_SPKI_B64)
1276            .unwrap();
1277        assert_eq!(spki_der.len(), 294); // 2048-bit RSA SPKI
1278
1279        let pkcs1 = strip_spki_wrapper(&spki_der);
1280        // PKCS#1 should be shorter (SPKI header stripped)
1281        assert!(pkcs1.len() < spki_der.len());
1282        // PKCS#1 RSAPublicKey starts with SEQUENCE tag 0x30
1283        assert_eq!(pkcs1[0], 0x30);
1284        // The stripped key should be usable by ring (verified by the RSA fixture tests above)
1285        assert!(pkcs1.len() > 250); // 2048-bit modulus is ~256 bytes
1286    }
1287}