1use crate::error::DaneError;
7use ans_types::{CertFingerprint, Fqdn};
8
9#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
11#[non_exhaustive]
12pub enum DanePolicy {
13 #[default]
15 Disabled,
16
17 ValidateIfPresent,
20
21 Required,
24}
25
26impl DanePolicy {
27 pub fn should_verify(&self) -> bool {
29 !matches!(self, Self::Disabled)
30 }
31
32 pub fn is_required(&self) -> bool {
34 matches!(self, Self::Required)
35 }
36}
37
38#[derive(Debug, Clone, Copy, PartialEq, Eq)]
40#[repr(u8)]
41#[non_exhaustive]
42pub enum TlsaUsage {
43 CaConstraint = 0,
45 ServiceCertificateConstraint = 1,
47 TrustAnchorAssertion = 2,
49 DomainIssuedCertificate = 3,
51}
52
53impl TryFrom<u8> for TlsaUsage {
54 type Error = DaneError;
55
56 fn try_from(value: u8) -> Result<Self, Self::Error> {
57 match value {
58 0 => Ok(Self::CaConstraint),
59 1 => Ok(Self::ServiceCertificateConstraint),
60 2 => Ok(Self::TrustAnchorAssertion),
61 3 => Ok(Self::DomainIssuedCertificate),
62 _ => Err(DaneError::InvalidRecord {
63 reason: format!("invalid TLSA usage: {value}"),
64 }),
65 }
66 }
67}
68
69#[derive(Debug, Clone, Copy, PartialEq, Eq)]
71#[repr(u8)]
72#[non_exhaustive]
73pub enum TlsaSelector {
74 FullCertificate = 0,
76 SubjectPublicKeyInfo = 1,
78}
79
80impl TryFrom<u8> for TlsaSelector {
81 type Error = DaneError;
82
83 fn try_from(value: u8) -> Result<Self, Self::Error> {
84 match value {
85 0 => Ok(Self::FullCertificate),
86 1 => Ok(Self::SubjectPublicKeyInfo),
87 _ => Err(DaneError::InvalidRecord {
88 reason: format!("invalid TLSA selector: {value}"),
89 }),
90 }
91 }
92}
93
94#[derive(Debug, Clone, Copy, PartialEq, Eq)]
96#[repr(u8)]
97#[non_exhaustive]
98pub enum TlsaMatchingType {
99 NoHash = 0,
101 Sha256 = 1,
103 Sha512 = 2,
105}
106
107impl TryFrom<u8> for TlsaMatchingType {
108 type Error = DaneError;
109
110 fn try_from(value: u8) -> Result<Self, Self::Error> {
111 match value {
112 0 => Ok(Self::NoHash),
113 1 => Ok(Self::Sha256),
114 2 => Ok(Self::Sha512),
115 _ => Err(DaneError::InvalidRecord {
116 reason: format!("invalid TLSA matching type: {value}"),
117 }),
118 }
119 }
120}
121
122#[derive(Debug, Clone, PartialEq, Eq)]
128#[non_exhaustive]
129pub struct TlsaRecord {
130 pub usage: TlsaUsage,
132 pub selector: TlsaSelector,
134 pub matching_type: TlsaMatchingType,
136 pub certificate_data: Vec<u8>,
138}
139
140impl TlsaRecord {
141 pub fn new(
143 usage: TlsaUsage,
144 selector: TlsaSelector,
145 matching_type: TlsaMatchingType,
146 certificate_data: Vec<u8>,
147 ) -> Self {
148 Self {
149 usage,
150 selector,
151 matching_type,
152 certificate_data,
153 }
154 }
155
156 pub fn from_rdata(rdata: &[u8]) -> Result<Self, DaneError> {
158 if rdata.len() < 4 {
159 return Err(DaneError::InvalidRecord {
160 reason: "TLSA record too short".to_string(),
161 });
162 }
163
164 let usage = TlsaUsage::try_from(rdata[0])?;
165 let selector = TlsaSelector::try_from(rdata[1])?;
166 let matching_type = TlsaMatchingType::try_from(rdata[2])?;
167 let certificate_data = rdata[3..].to_vec();
168
169 Ok(Self {
170 usage,
171 selector,
172 matching_type,
173 certificate_data,
174 })
175 }
176
177 pub fn is_verifiable(&self) -> bool {
182 self.usage == TlsaUsage::DomainIssuedCertificate
183 && self.selector == TlsaSelector::FullCertificate
184 && self.matching_type == TlsaMatchingType::Sha256
185 }
186
187 pub fn matches_fingerprint(&self, cert_fingerprint: &CertFingerprint) -> Option<bool> {
194 if self.usage != TlsaUsage::DomainIssuedCertificate {
196 tracing::debug!(
197 usage = ?self.usage,
198 "TLSA usage is not DANE-EE, cannot verify"
199 );
200 return None;
201 }
202
203 if self.selector != TlsaSelector::FullCertificate {
204 tracing::debug!(
205 selector = ?self.selector,
206 "TLSA selector is not full certificate (SPKI not yet supported), cannot verify"
207 );
208 return None;
209 }
210
211 if self.matching_type != TlsaMatchingType::Sha256 {
212 tracing::debug!(
213 matching_type = ?self.matching_type,
214 "TLSA matching type is not SHA-256, cannot verify"
215 );
216 return None;
217 }
218
219 let cert_bytes = cert_fingerprint.as_bytes();
223 let matches = if self.certificate_data.len() == cert_bytes.len() {
224 use subtle::ConstantTimeEq;
225 bool::from(self.certificate_data.ct_eq(cert_bytes.as_slice()))
226 } else {
227 false
228 };
229
230 tracing::debug!(
231 tlsa_fingerprint = %hex::encode(&self.certificate_data),
232 cert_fingerprint = %cert_fingerprint,
233 matches,
234 "TLSA fingerprint comparison"
235 );
236
237 Some(matches)
238 }
239}
240
241#[derive(Debug, Clone)]
243#[non_exhaustive]
244pub enum DaneVerificationResult {
245 Verified {
247 matched_record: TlsaRecord,
249 },
250 NoRecords,
252 Mismatch {
254 records_checked: usize,
256 },
257 DnssecFailed,
259 Skipped,
261}
262
263impl DaneVerificationResult {
264 pub fn is_acceptable(&self, policy: DanePolicy) -> bool {
266 match self {
267 Self::Verified { .. } | Self::Skipped => true,
268 Self::NoRecords => !policy.is_required(),
269 Self::Mismatch { .. } | Self::DnssecFailed => false,
270 }
271 }
272}
273
274pub fn verify_dane(
276 records: &[TlsaRecord],
277 cert_fingerprint: &CertFingerprint,
278 policy: DanePolicy,
279 fqdn: &Fqdn,
280 port: u16,
281) -> Result<DaneVerificationResult, DaneError> {
282 if !policy.should_verify() {
283 tracing::debug!("DANE verification disabled by policy");
284 return Ok(DaneVerificationResult::Skipped);
285 }
286
287 if records.is_empty() {
288 tracing::debug!(fqdn = %fqdn, port, "No TLSA records found");
289 if policy.is_required() {
290 return Err(DaneError::NoTlsaRecords {
291 fqdn: fqdn.to_string(),
292 port,
293 });
294 }
295 return Ok(DaneVerificationResult::NoRecords);
296 }
297
298 tracing::debug!(
299 fqdn = %fqdn,
300 port,
301 record_count = records.len(),
302 "Checking TLSA records"
303 );
304
305 let mut has_unsupported = false;
307
308 for record in records {
309 match record.matches_fingerprint(cert_fingerprint) {
310 Some(true) => {
311 tracing::info!(
312 fqdn = %fqdn,
313 port,
314 "DANE verification PASSED - certificate matches TLSA record"
315 );
316 return Ok(DaneVerificationResult::Verified {
317 matched_record: record.clone(),
318 });
319 }
320 Some(false) => {
321 tracing::debug!("TLSA record checked but did not match");
322 }
323 None => {
324 has_unsupported = true;
326 tracing::warn!(
327 usage = ?record.usage,
328 selector = ?record.selector,
329 matching_type = ?record.matching_type,
330 "TLSA record in unsupported format"
331 );
332 }
333 }
334 }
335
336 if has_unsupported {
340 tracing::error!(
341 fqdn = %fqdn,
342 port,
343 "DANE verification FAILED - TLSA records present but in unsupported format (only DANE-EE + FullCert + SHA256 supported)"
344 );
345 return Err(DaneError::InvalidRecord {
346 reason: "TLSA record format not supported (only usage=3, selector=0, matching_type=1)"
347 .to_string(),
348 });
349 }
350
351 tracing::warn!(
352 fqdn = %fqdn,
353 port,
354 records_checked = records.len(),
355 "DANE verification FAILED - no TLSA record matched certificate"
356 );
357
358 Err(DaneError::FingerprintMismatch)
359}
360
361#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
362#[cfg(test)]
363mod tests {
364 use super::*;
365
366 #[test]
367 fn test_dane_policy_defaults_to_disabled() {
368 assert_eq!(DanePolicy::default(), DanePolicy::Disabled);
369 }
370
371 #[test]
372 fn test_dane_policy_should_verify() {
373 assert!(!DanePolicy::Disabled.should_verify());
374 assert!(DanePolicy::ValidateIfPresent.should_verify());
375 assert!(DanePolicy::Required.should_verify());
376 }
377
378 #[test]
379 fn test_dane_policy_is_required() {
380 assert!(!DanePolicy::Disabled.is_required());
381 assert!(!DanePolicy::ValidateIfPresent.is_required());
382 assert!(DanePolicy::Required.is_required());
383 }
384
385 #[test]
386 fn test_tlsa_record_from_rdata() {
387 let mut rdata = vec![3, 0, 1];
389 let hash = hex::decode("e7b64d16f42055d6faf382a43dc35b98be76aba0db145a904b590a034b33b904")
390 .unwrap();
391 rdata.extend(&hash);
392
393 let record = TlsaRecord::from_rdata(&rdata).unwrap();
394 assert_eq!(record.usage, TlsaUsage::DomainIssuedCertificate);
395 assert_eq!(record.selector, TlsaSelector::FullCertificate);
396 assert_eq!(record.matching_type, TlsaMatchingType::Sha256);
397 assert_eq!(record.certificate_data, hash);
398 }
399
400 #[test]
401 fn test_tlsa_record_matches_fingerprint() {
402 let hash = hex::decode("e7b64d16f42055d6faf382a43dc35b98be76aba0db145a904b590a034b33b904")
403 .unwrap();
404
405 let record = TlsaRecord::new(
406 TlsaUsage::DomainIssuedCertificate,
407 TlsaSelector::FullCertificate,
408 TlsaMatchingType::Sha256,
409 hash,
410 );
411
412 let fingerprint = CertFingerprint::parse(
413 "SHA256:e7b64d16f42055d6faf382a43dc35b98be76aba0db145a904b590a034b33b904",
414 )
415 .unwrap();
416
417 assert_eq!(record.matches_fingerprint(&fingerprint), Some(true));
418 }
419
420 #[test]
421 fn test_tlsa_record_does_not_match_different_fingerprint() {
422 let hash = hex::decode("e7b64d16f42055d6faf382a43dc35b98be76aba0db145a904b590a034b33b904")
423 .unwrap();
424
425 let record = TlsaRecord::new(
426 TlsaUsage::DomainIssuedCertificate,
427 TlsaSelector::FullCertificate,
428 TlsaMatchingType::Sha256,
429 hash,
430 );
431
432 let fingerprint = CertFingerprint::parse(
433 "SHA256:0000000000000000000000000000000000000000000000000000000000000000",
434 )
435 .unwrap();
436
437 assert_eq!(record.matches_fingerprint(&fingerprint), Some(false));
438 }
439
440 #[test]
441 fn test_tlsa_record_unsupported_format_returns_none() {
442 let hash = hex::decode("e7b64d16f42055d6faf382a43dc35b98be76aba0db145a904b590a034b33b904")
443 .unwrap();
444
445 let record = TlsaRecord::new(
447 TlsaUsage::DomainIssuedCertificate,
448 TlsaSelector::SubjectPublicKeyInfo,
449 TlsaMatchingType::Sha256,
450 hash,
451 );
452
453 let fingerprint = CertFingerprint::parse(
454 "SHA256:e7b64d16f42055d6faf382a43dc35b98be76aba0db145a904b590a034b33b904",
455 )
456 .unwrap();
457
458 assert_eq!(record.matches_fingerprint(&fingerprint), None);
460 }
461
462 #[test]
463 fn test_verify_dane_disabled() {
464 let fqdn = Fqdn::new("test.example.com").unwrap();
465 let fingerprint = CertFingerprint::parse(
466 "SHA256:e7b64d16f42055d6faf382a43dc35b98be76aba0db145a904b590a034b33b904",
467 )
468 .unwrap();
469
470 let result = verify_dane(&[], &fingerprint, DanePolicy::Disabled, &fqdn, 443).unwrap();
471 assert!(matches!(result, DaneVerificationResult::Skipped));
472 }
473
474 #[test]
475 fn test_verify_dane_no_records_validate_if_present() {
476 let fqdn = Fqdn::new("test.example.com").unwrap();
477 let fingerprint = CertFingerprint::parse(
478 "SHA256:e7b64d16f42055d6faf382a43dc35b98be76aba0db145a904b590a034b33b904",
479 )
480 .unwrap();
481
482 let result =
483 verify_dane(&[], &fingerprint, DanePolicy::ValidateIfPresent, &fqdn, 443).unwrap();
484 assert!(matches!(result, DaneVerificationResult::NoRecords));
485 assert!(result.is_acceptable(DanePolicy::ValidateIfPresent));
486 }
487
488 #[test]
489 fn test_verify_dane_no_records_required() {
490 let fqdn = Fqdn::new("test.example.com").unwrap();
491 let fingerprint = CertFingerprint::parse(
492 "SHA256:e7b64d16f42055d6faf382a43dc35b98be76aba0db145a904b590a034b33b904",
493 )
494 .unwrap();
495
496 let result = verify_dane(&[], &fingerprint, DanePolicy::Required, &fqdn, 443);
497 assert!(matches!(result, Err(DaneError::NoTlsaRecords { .. })));
498 }
499
500 #[test]
501 fn test_verify_dane_match() {
502 let fqdn = Fqdn::new("test.example.com").unwrap();
503 let hash = hex::decode("e7b64d16f42055d6faf382a43dc35b98be76aba0db145a904b590a034b33b904")
504 .unwrap();
505
506 let record = TlsaRecord::new(
507 TlsaUsage::DomainIssuedCertificate,
508 TlsaSelector::FullCertificate,
509 TlsaMatchingType::Sha256,
510 hash,
511 );
512
513 let fingerprint = CertFingerprint::parse(
514 "SHA256:e7b64d16f42055d6faf382a43dc35b98be76aba0db145a904b590a034b33b904",
515 )
516 .unwrap();
517
518 let result =
519 verify_dane(&[record], &fingerprint, DanePolicy::Required, &fqdn, 443).unwrap();
520 assert!(matches!(result, DaneVerificationResult::Verified { .. }));
521 }
522
523 #[test]
524 fn test_verify_dane_mismatch() {
525 let fqdn = Fqdn::new("test.example.com").unwrap();
526 let hash = hex::decode("e7b64d16f42055d6faf382a43dc35b98be76aba0db145a904b590a034b33b904")
527 .unwrap();
528
529 let record = TlsaRecord::new(
530 TlsaUsage::DomainIssuedCertificate,
531 TlsaSelector::FullCertificate,
532 TlsaMatchingType::Sha256,
533 hash,
534 );
535
536 let fingerprint = CertFingerprint::parse(
537 "SHA256:0000000000000000000000000000000000000000000000000000000000000000",
538 )
539 .unwrap();
540
541 let result = verify_dane(&[record], &fingerprint, DanePolicy::Required, &fqdn, 443);
542 assert!(matches!(result, Err(DaneError::FingerprintMismatch)));
543 }
544
545 #[test]
546 fn test_verification_result_is_acceptable() {
547 let record = TlsaRecord::new(
548 TlsaUsage::DomainIssuedCertificate,
549 TlsaSelector::FullCertificate,
550 TlsaMatchingType::Sha256,
551 vec![0; 32],
552 );
553
554 let verified = DaneVerificationResult::Verified {
556 matched_record: record,
557 };
558 assert!(verified.is_acceptable(DanePolicy::Disabled));
559 assert!(verified.is_acceptable(DanePolicy::ValidateIfPresent));
560 assert!(verified.is_acceptable(DanePolicy::Required));
561
562 let no_records = DaneVerificationResult::NoRecords;
564 assert!(no_records.is_acceptable(DanePolicy::Disabled));
565 assert!(no_records.is_acceptable(DanePolicy::ValidateIfPresent));
566 assert!(!no_records.is_acceptable(DanePolicy::Required));
567
568 let skipped = DaneVerificationResult::Skipped;
570 assert!(skipped.is_acceptable(DanePolicy::Disabled));
571 assert!(skipped.is_acceptable(DanePolicy::ValidateIfPresent));
572 assert!(skipped.is_acceptable(DanePolicy::Required));
573
574 let mismatch = DaneVerificationResult::Mismatch { records_checked: 1 };
576 assert!(!mismatch.is_acceptable(DanePolicy::Disabled));
577 assert!(!mismatch.is_acceptable(DanePolicy::ValidateIfPresent));
578 assert!(!mismatch.is_acceptable(DanePolicy::Required));
579 }
580
581 #[test]
584 fn test_tlsa_from_rdata_too_short() {
585 let result = TlsaRecord::from_rdata(&[3, 0]);
586 assert!(result.is_err());
587 assert!(matches!(
588 result.unwrap_err(),
589 DaneError::InvalidRecord { .. }
590 ));
591 }
592
593 #[test]
594 fn test_tlsa_from_rdata_empty() {
595 let result = TlsaRecord::from_rdata(&[]);
596 assert!(result.is_err());
597 }
598
599 #[test]
602 fn test_tlsa_usage_invalid() {
603 let result = TlsaUsage::try_from(4_u8);
604 assert!(result.is_err());
605 }
606
607 #[test]
608 fn test_tlsa_selector_invalid() {
609 let result = TlsaSelector::try_from(2_u8);
610 assert!(result.is_err());
611 }
612
613 #[test]
614 fn test_tlsa_matching_type_invalid() {
615 let result = TlsaMatchingType::try_from(3_u8);
616 assert!(result.is_err());
617 }
618
619 #[test]
622 fn test_is_verifiable_true() {
623 let record = TlsaRecord::new(
624 TlsaUsage::DomainIssuedCertificate,
625 TlsaSelector::FullCertificate,
626 TlsaMatchingType::Sha256,
627 vec![0; 32],
628 );
629 assert!(record.is_verifiable());
630 }
631
632 #[test]
633 fn test_is_verifiable_wrong_usage() {
634 let record = TlsaRecord::new(
635 TlsaUsage::CaConstraint,
636 TlsaSelector::FullCertificate,
637 TlsaMatchingType::Sha256,
638 vec![0; 32],
639 );
640 assert!(!record.is_verifiable());
641 }
642
643 #[test]
644 fn test_is_verifiable_wrong_selector() {
645 let record = TlsaRecord::new(
646 TlsaUsage::DomainIssuedCertificate,
647 TlsaSelector::SubjectPublicKeyInfo,
648 TlsaMatchingType::Sha256,
649 vec![0; 32],
650 );
651 assert!(!record.is_verifiable());
652 }
653
654 #[test]
655 fn test_is_verifiable_wrong_matching_type() {
656 let record = TlsaRecord::new(
657 TlsaUsage::DomainIssuedCertificate,
658 TlsaSelector::FullCertificate,
659 TlsaMatchingType::Sha512,
660 vec![0; 64],
661 );
662 assert!(!record.is_verifiable());
663 }
664
665 #[test]
668 fn test_matches_fingerprint_non_dane_ee() {
669 let hash = vec![0u8; 32];
670 let record = TlsaRecord::new(
671 TlsaUsage::CaConstraint,
672 TlsaSelector::FullCertificate,
673 TlsaMatchingType::Sha256,
674 hash,
675 );
676 let fp = CertFingerprint::from_bytes([0u8; 32]);
677 assert_eq!(record.matches_fingerprint(&fp), None);
678 }
679
680 #[test]
681 fn test_matches_fingerprint_non_sha256() {
682 let hash = vec![0u8; 64];
683 let record = TlsaRecord::new(
684 TlsaUsage::DomainIssuedCertificate,
685 TlsaSelector::FullCertificate,
686 TlsaMatchingType::Sha512,
687 hash,
688 );
689 let fp = CertFingerprint::from_bytes([0u8; 32]);
690 assert_eq!(record.matches_fingerprint(&fp), None);
691 }
692
693 #[test]
696 fn test_dnssec_failed_is_not_acceptable() {
697 let result = DaneVerificationResult::DnssecFailed;
698 assert!(!result.is_acceptable(DanePolicy::Disabled));
699 assert!(!result.is_acceptable(DanePolicy::ValidateIfPresent));
700 assert!(!result.is_acceptable(DanePolicy::Required));
701 }
702}