1use std::collections::HashSet;
2
3use base64::Engine;
4
5use super::parser::{parse_tag_list, DkimParseError};
6use super::types::{HashAlgorithm, KeyFlag, KeyType, PermFailKind};
7
8#[derive(Debug, Clone, PartialEq, Eq)]
10pub struct DkimPublicKey {
11 pub key_type: KeyType,
12 pub public_key: Vec<u8>,
13 pub revoked: bool,
14 pub hash_algorithms: Option<Vec<HashAlgorithm>>,
15 pub service_types: Option<Vec<String>>,
16 pub flags: Vec<KeyFlag>,
17 pub notes: Option<String>,
18}
19
20fn key_error(detail: impl Into<String>) -> DkimParseError {
21 DkimParseError {
22 kind: PermFailKind::MalformedSignature,
23 detail: detail.into(),
24 }
25}
26
27impl DkimPublicKey {
28 pub fn parse(txt_record: &str) -> Result<Self, DkimParseError> {
31 let tags = parse_tag_list(txt_record);
32
33 let mut seen = HashSet::new();
35 for (name, _) in &tags {
36 if !seen.insert(name.as_str()) {
37 return Err(key_error(format!("duplicate tag: {}", name)));
38 }
39 }
40
41 let get = |name: &str| -> Option<&str> {
42 tags.iter()
43 .find(|(n, _)| n == name)
44 .map(|(_, v)| v.as_str())
45 };
46
47 if let Some(v) = get("v") {
49 if v != "DKIM1" {
50 return Err(key_error(format!("invalid version: {}", v)));
51 }
52 }
53
54 let key_type = if let Some(k) = get("k") {
56 KeyType::parse(k).ok_or_else(|| key_error(format!("unknown key type: {}", k)))?
57 } else {
58 KeyType::Rsa
59 };
60
61 let p_raw = get("p").ok_or_else(|| key_error("missing required tag: p"))?;
63 let (public_key, revoked) = if p_raw.is_empty() {
64 (Vec::new(), true)
65 } else {
66 let cleaned: String = p_raw.chars().filter(|c| !c.is_ascii_whitespace()).collect();
67 let decoded = base64::engine::general_purpose::STANDARD
68 .decode(&cleaned)
69 .map_err(|e| key_error(format!("invalid base64 in p=: {}", e)))?;
70 (decoded, false)
71 };
72
73 let hash_algorithms = if let Some(h) = get("h") {
75 let mut algs = Vec::new();
76 for part in h.split(':') {
77 let trimmed = part.trim();
78 if trimmed.is_empty() {
79 continue;
80 }
81 if let Some(alg) = HashAlgorithm::parse(trimmed) {
82 algs.push(alg);
83 }
84 }
86 if algs.is_empty() {
87 None
88 } else {
89 Some(algs)
90 }
91 } else {
92 None
93 };
94
95 let service_types = if let Some(s) = get("s") {
97 Some(
98 s.split(':')
99 .map(|p| p.trim().to_string())
100 .filter(|p| !p.is_empty())
101 .collect(),
102 )
103 } else {
104 None
105 };
106
107 let flags = if let Some(t) = get("t") {
109 let mut f = Vec::new();
110 for part in t.split(':') {
111 match part.trim() {
112 "y" => f.push(KeyFlag::Testing),
113 "s" => f.push(KeyFlag::Strict),
114 _ => {} }
116 }
117 f
118 } else {
119 Vec::new()
120 };
121
122 let notes = get("n").map(|s| s.to_string());
124
125 Ok(DkimPublicKey {
126 key_type,
127 public_key,
128 revoked,
129 hash_algorithms,
130 service_types,
131 flags,
132 notes,
133 })
134 }
135
136 pub fn is_testing(&self) -> bool {
138 self.flags.contains(&KeyFlag::Testing)
139 }
140
141 pub fn is_strict(&self) -> bool {
143 self.flags.contains(&KeyFlag::Strict)
144 }
145}
146
147#[cfg(test)]
148mod tests {
149 use super::*;
150
151 fn make_rsa_spki_stub() -> String {
152 let fake_key = vec![0x30u8; 162];
154 base64::engine::general_purpose::STANDARD.encode(&fake_key)
155 }
156
157 fn make_rsa_2048_spki_stub() -> String {
158 let fake_key = vec![0x30u8; 294];
160 base64::engine::general_purpose::STANDARD.encode(&fake_key)
161 }
162
163 fn make_ed25519_key() -> String {
164 let fake_key = vec![0xABu8; 32];
166 base64::engine::general_purpose::STANDARD.encode(&fake_key)
167 }
168
169 #[test]
171 fn parse_minimal_key() {
172 let p = make_rsa_spki_stub();
173 let input = format!("p={}", p);
174 let key = DkimPublicKey::parse(&input).unwrap();
175 assert_eq!(key.key_type, KeyType::Rsa); assert!(!key.revoked);
177 assert_eq!(key.public_key.len(), 162);
178 assert!(key.hash_algorithms.is_none());
179 assert!(key.service_types.is_none());
180 assert!(key.flags.is_empty());
181 assert!(key.notes.is_none());
182 }
183
184 #[test]
186 fn parse_full_key() {
187 let p = make_rsa_2048_spki_stub();
188 let input = format!(
189 "v=DKIM1; k=rsa; h=sha256; s=email; t=y:s; n=test key; p={}",
190 p
191 );
192 let key = DkimPublicKey::parse(&input).unwrap();
193 assert_eq!(key.key_type, KeyType::Rsa);
194 assert!(!key.revoked);
195 assert_eq!(key.public_key.len(), 294);
196 assert_eq!(
197 key.hash_algorithms,
198 Some(vec![HashAlgorithm::Sha256])
199 );
200 assert_eq!(
201 key.service_types,
202 Some(vec!["email".to_string()])
203 );
204 assert!(key.is_testing());
205 assert!(key.is_strict());
206 assert_eq!(key.notes, Some("test key".to_string()));
207 }
208
209 #[test]
211 fn parse_revoked_key() {
212 let input = "v=DKIM1; p=";
213 let key = DkimPublicKey::parse(input).unwrap();
214 assert!(key.revoked);
215 assert!(key.public_key.is_empty());
216 }
217
218 #[test]
220 fn parse_h_sha256_only() {
221 let p = make_rsa_spki_stub();
222 let input = format!("h=sha256; p={}", p);
223 let key = DkimPublicKey::parse(&input).unwrap();
224 assert_eq!(
225 key.hash_algorithms,
226 Some(vec![HashAlgorithm::Sha256])
227 );
228 }
229
230 #[test]
231 fn parse_h_sha1_and_sha256() {
232 let p = make_rsa_spki_stub();
233 let input = format!("h=sha1:sha256; p={}", p);
234 let key = DkimPublicKey::parse(&input).unwrap();
235 assert_eq!(
236 key.hash_algorithms,
237 Some(vec![HashAlgorithm::Sha1, HashAlgorithm::Sha256])
238 );
239 }
240
241 #[test]
243 fn parse_s_email() {
244 let p = make_rsa_spki_stub();
245 let input = format!("s=email; p={}", p);
246 let key = DkimPublicKey::parse(&input).unwrap();
247 assert_eq!(
248 key.service_types,
249 Some(vec!["email".to_string()])
250 );
251 }
252
253 #[test]
254 fn parse_s_wildcard() {
255 let p = make_rsa_spki_stub();
256 let input = format!("s=*; p={}", p);
257 let key = DkimPublicKey::parse(&input).unwrap();
258 assert_eq!(
259 key.service_types,
260 Some(vec!["*".to_string()])
261 );
262 }
263
264 #[test]
265 fn parse_s_other() {
266 let p = make_rsa_spki_stub();
267 let input = format!("s=other; p={}", p);
268 let key = DkimPublicKey::parse(&input).unwrap();
269 assert_eq!(
270 key.service_types,
271 Some(vec!["other".to_string()])
272 );
273 }
274
275 #[test]
277 fn parse_t_testing() {
278 let p = make_rsa_spki_stub();
279 let input = format!("t=y; p={}", p);
280 let key = DkimPublicKey::parse(&input).unwrap();
281 assert!(key.is_testing());
282 assert!(!key.is_strict());
283 }
284
285 #[test]
286 fn parse_t_strict() {
287 let p = make_rsa_spki_stub();
288 let input = format!("t=s; p={}", p);
289 let key = DkimPublicKey::parse(&input).unwrap();
290 assert!(!key.is_testing());
291 assert!(key.is_strict());
292 }
293
294 #[test]
295 fn parse_t_both() {
296 let p = make_rsa_spki_stub();
297 let input = format!("t=y:s; p={}", p);
298 let key = DkimPublicKey::parse(&input).unwrap();
299 assert!(key.is_testing());
300 assert!(key.is_strict());
301 }
302
303 #[test]
305 fn parse_unknown_key_type() {
306 let p = make_rsa_spki_stub();
307 let input = format!("k=dsa; p={}", p);
308 let err = DkimPublicKey::parse(&input).unwrap_err();
309 assert!(err.detail.contains("unknown key type"));
310 }
311
312 #[test]
314 fn parse_ed25519_key() {
315 let p = make_ed25519_key();
316 let input = format!("k=ed25519; p={}", p);
317 let key = DkimPublicKey::parse(&input).unwrap();
318 assert_eq!(key.key_type, KeyType::Ed25519);
319 assert_eq!(key.public_key.len(), 32);
320 }
321
322 #[test]
324 fn parse_rsa_1024_key() {
325 let p = make_rsa_spki_stub(); let input = format!("k=rsa; p={}", p);
327 let key = DkimPublicKey::parse(&input).unwrap();
328 assert_eq!(key.key_type, KeyType::Rsa);
329 assert!(key.public_key.len() < 256); }
331
332 #[test]
334 fn parse_rsa_2048_key() {
335 let p = make_rsa_2048_spki_stub(); let input = format!("k=rsa; p={}", p);
337 let key = DkimPublicKey::parse(&input).unwrap();
338 assert_eq!(key.key_type, KeyType::Rsa);
339 assert!(key.public_key.len() >= 256); }
341
342 #[test]
344 fn parse_v_dkim1() {
345 let p = make_rsa_spki_stub();
346 let input = format!("v=DKIM1; p={}", p);
347 let key = DkimPublicKey::parse(&input).unwrap();
348 assert!(!key.revoked);
349 }
350
351 #[test]
352 fn parse_v_wrong() {
353 let p = make_rsa_spki_stub();
354 let input = format!("v=DKIM2; p={}", p);
355 let err = DkimPublicKey::parse(&input).unwrap_err();
356 assert!(err.detail.contains("invalid version"));
357 }
358
359 #[test]
361 fn parse_unknown_tags_ignored() {
362 let p = make_rsa_spki_stub();
363 let input = format!("foo=bar; p={}; baz=qux", p);
364 let key = DkimPublicKey::parse(&input).unwrap();
365 assert!(!key.revoked);
366 }
367
368 #[test]
370 fn key_query_format() {
371 let selector = "sel1";
374 let domain = "example.com";
375 let query = format!("{}._domainkey.{}", selector, domain);
376 assert_eq!(query, "sel1._domainkey.example.com");
377 }
378
379 #[test]
381 fn different_selectors_different_keys() {
382 let p1 = make_rsa_spki_stub();
383 let p2 = make_ed25519_key();
384 let key1 = DkimPublicKey::parse(&format!("k=rsa; p={}", p1)).unwrap();
385 let key2 = DkimPublicKey::parse(&format!("k=ed25519; p={}", p2)).unwrap();
386 assert_eq!(key1.key_type, KeyType::Rsa);
387 assert_eq!(key2.key_type, KeyType::Ed25519);
388 }
389
390 #[test]
392 fn parse_concatenated_txt_strings() {
393 let p = make_rsa_spki_stub();
394 let part1 = "v=DKIM1; k=rsa; ";
396 let part2 = format!("p={}", p);
397 let concatenated = format!("{}{}", part1, part2);
398 let key = DkimPublicKey::parse(&concatenated).unwrap();
399 assert_eq!(key.key_type, KeyType::Rsa);
400 }
401
402 #[test]
404 fn parse_h_unknown_hash_ignored() {
405 let p = make_rsa_spki_stub();
406 let input = format!("h=sha256:sha512; p={}", p);
407 let key = DkimPublicKey::parse(&input).unwrap();
408 assert_eq!(
410 key.hash_algorithms,
411 Some(vec![HashAlgorithm::Sha256])
412 );
413 }
414
415 #[test]
417 fn parse_k_rsa_explicit() {
418 let p = make_rsa_spki_stub();
419 let input = format!("k=rsa; p={}", p);
420 let key = DkimPublicKey::parse(&input).unwrap();
421 assert_eq!(key.key_type, KeyType::Rsa);
422 }
423
424 #[test]
425 fn parse_k_ed25519() {
426 let p = make_ed25519_key();
427 let input = format!("k=ed25519; p={}", p);
428 let key = DkimPublicKey::parse(&input).unwrap();
429 assert_eq!(key.key_type, KeyType::Ed25519);
430 }
431
432 #[test]
434 fn parse_n_notes() {
435 let p = make_rsa_spki_stub();
436 let input = format!("n=This is a test key; p={}", p);
437 let key = DkimPublicKey::parse(&input).unwrap();
438 assert_eq!(key.notes, Some("This is a test key".to_string()));
439 }
440
441 #[test]
443 fn parse_missing_p() {
444 let err = DkimPublicKey::parse("v=DKIM1; k=rsa").unwrap_err();
445 assert!(err.detail.contains("missing required tag: p"));
446 }
447
448 #[test]
450 fn parse_s_multiple() {
451 let p = make_rsa_spki_stub();
452 let input = format!("s=email:*; p={}", p);
453 let key = DkimPublicKey::parse(&input).unwrap();
454 assert_eq!(
455 key.service_types,
456 Some(vec!["email".to_string(), "*".to_string()])
457 );
458 }
459
460 #[test]
462 fn parse_t_unknown_flag_ignored() {
463 let p = make_rsa_spki_stub();
464 let input = format!("t=y:x; p={}", p);
465 let key = DkimPublicKey::parse(&input).unwrap();
466 assert!(key.is_testing());
467 assert!(!key.is_strict());
468 assert_eq!(key.flags.len(), 1); }
470
471 #[test]
473 fn parse_rsa_spki_format() {
474 let p = make_rsa_spki_stub();
475 let key = DkimPublicKey::parse(&format!("p={}", p)).unwrap();
476 assert!(!key.public_key.is_empty());
478 assert_eq!(key.key_type, KeyType::Rsa);
479 }
480
481 #[test]
483 fn parse_ed25519_raw_32_bytes() {
484 let p = make_ed25519_key();
485 let key = DkimPublicKey::parse(&format!("k=ed25519; p={}", p)).unwrap();
486 assert_eq!(key.public_key.len(), 32);
487 }
488
489 #[test]
491 fn parse_malformed_base64() {
492 let err = DkimPublicKey::parse("p=!!!not-base64!!!").unwrap_err();
493 assert!(err.detail.contains("invalid base64"));
494 }
495
496 #[test]
498 fn key_parsing_complete() {
499 let p = make_rsa_2048_spki_stub();
501 let input = format!(
502 "v=DKIM1; k=rsa; h=sha256:sha1; s=email:*; t=y:s; n=full key test; p={}",
503 p
504 );
505 let key = DkimPublicKey::parse(&input).unwrap();
506 assert_eq!(key.key_type, KeyType::Rsa);
507 assert!(!key.revoked);
508 assert_eq!(key.public_key.len(), 294);
509 assert_eq!(
510 key.hash_algorithms,
511 Some(vec![HashAlgorithm::Sha256, HashAlgorithm::Sha1])
512 );
513 assert_eq!(
514 key.service_types,
515 Some(vec!["email".to_string(), "*".to_string()])
516 );
517 assert!(key.is_testing());
518 assert!(key.is_strict());
519 assert!(key.notes.is_some());
520 }
521
522 #[test]
524 fn parse_duplicate_tag_in_key() {
525 let p = make_rsa_spki_stub();
526 let input = format!("k=rsa; k=ed25519; p={}", p);
527 let err = DkimPublicKey::parse(&input).unwrap_err();
528 assert!(err.detail.contains("duplicate"));
529 }
530
531 #[test]
533 fn parse_default_service_type() {
534 let p = make_rsa_spki_stub();
535 let key = DkimPublicKey::parse(&format!("p={}", p)).unwrap();
536 assert!(key.service_types.is_none()); }
538
539 #[test]
541 fn parse_v_absent_valid() {
542 let p = make_rsa_spki_stub();
543 let key = DkimPublicKey::parse(&format!("p={}", p)).unwrap();
544 assert!(!key.revoked);
545 }
546}