nsproxy-hickory-proto 0.25.4

Hickory DNS is a safe and secure DNS library. This is the foundational DNS protocol library for all Hickory DNS projects.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
//! Throughout this module several NSEC3 specific terms are used.
//!
//! "closest_encloser" - a name that is a longest parent / grandparent /
//!     great-grandparent, etc. of a `query_name`, that DOES exist.
//!
//! "next_closer" - a name that is one level deeper in the hierarchy from
//!     `closest_encloser`, that DOES NOT exist in the zone.
//!
//! "wildcard of closest encloser" - a name in a form of `*.closest_encloser`.
//!     If it exists then `query_name` would be serviced by the wildcard.
//!
//! "covering NSEC3 record" - NSEC3 record name has a hash (`hashed_owner_name`),
//!     and inside the record data there's `next_hashed_owner_name`.
//!     If the hash of `query_name` fits between the two hashes then the record
//!     "covers" `query_name`
//!
//! "matching NSEC3 record" - exists for *existing* names only.
//!     The `hashed_owner_name` would match one of the names *exactly*.
//!
//! In general:
//! * if a name exists we would expect to see its "matching" NSEC3 record,
//! * if the name doesn't exist we would expect to see a "covering" NSEC3 record.
//!
//! A various combinations of matching and covering records for `closest_encloser`,
//! `next_closer`, and `wildcard_encloser` can tell us whether `query_name`
//! exists, whether `wildcard_encloser` exists, and whether its own or
//! `wildcard_encloser`'s records have a record of a requested type.
//!
//! NOTE: A single NSEC3 record can in theory address multiple situations.
//! Thus, the number of records is not important as long as all
//! conditions are represented by them.
//!
//! NOTE: Normally, when there exist a wildcard that a given name fits into,
//! it is said that the wildcard "covers" the name. But since the wording
//! around NSEC3 records uses "cover" in a specific sense we use the word
//! "service" instead:
//! `*.w.soa.name` *services* `x.w.soa.name`
//!
//! In general, avoid using the word "cover" for anything not related to
//! "covering NSEC3 record"
//!
//! ## Examples
//!
//! To explain them let's use the following example zone:
//!
//! soa.name
//! *.w.soa.name
//! c.e.soa.name
//!
//! if we request:
//!     `query_name` = `x.y.z.nc.c.e.soa.name`
//! then:
//!     `closest_encloser` == `c.e.soa.name` (the first existing ancestor)
//!     `next_closer`      == `nc.c.e.soa.name` (doesn't exist in the zone)
//!     `wildcard_encloser` == `*.c.e.soa.name`
//!
//! if we request:
//!     `query_name` = `x.y.z.nc.w.soa.name`
//! then:
//!     `closest_encloser` == `w.soa.name`
//!     `next_closer`      == `nc.w.soa.name`
//!     `wildcard_encloser` == `*.w.soa.name`
//!
//! if we request:
//!     `query_name` = `x.soa.name`
//! then:
//!     `closest_encloser` == `soa.name`
//!     `next_closer`      == `x.soa.name`
//!     `wildcard_encloser` == `*.soa.name`
//!
//! if we request:
//!     `query_name` = `e.soa.name`
//! then:
//!     `closest_encloser` == `e.soa.name`
//!     `next_closer`      == doesn't exist
//!     `wildcard_encloser` == `*.soa.name`
//!

use alloc::vec::Vec;

use super::proof_log_yield;
use crate::{
    dnssec::{Nsec3HashAlgorithm, Proof, rdata::NSEC3},
    op::{Query, ResponseCode},
    rr::{Name, Record, RecordType, domain::Label},
};

pub(super) fn verify_nsec3(
    query: &Query,
    soa_name: &Name,
    response_code: ResponseCode,
    answers: &[Record],
    nsec3s: &[(&Name, &NSEC3)],
) -> Proof {
    debug_assert!(!nsec3s.is_empty());

    // For every NSEC3 record that in text form looks like:
    // <base32-hash>.soa.name NSEC3 <data>
    // we extract (<base32-hash>, <data>) pair from deeply nested structures
    let nsec3s: Option<Vec<Nsec3RecordPair<'_>>> = nsec3s
        .iter()
        .map(|(record_name, nsec3_data)| {
            split_first_label(record_name)
                .filter(|(_, base)| base == soa_name)
                .and_then(|(base32_hashed_name, _)| {
                    Some(Nsec3RecordPair {
                        base32_hashed_name: Label::from_raw_bytes(base32_hashed_name).ok()?,
                        nsec3_data,
                    })
                })
        })
        .collect();

    // Some of record names were NOT in a form of `<base32hash>.soa.name`
    let Some(nsec3s) = nsec3s else {
        return proof_log_yield(
            Proof::Bogus,
            query.name(),
            "nsec3",
            "record name format is invalid",
        );
    };

    debug_assert!(!nsec3s.is_empty());

    // RFC 5155 8.2 - all NSEC3 records share the same NSEC3 params
    let first = &nsec3s[0];
    let hash_algorithm = first.nsec3_data.hash_algorithm();
    let salt = first.nsec3_data.salt();
    let iterations = first.nsec3_data.iterations();
    if nsec3s.iter().any(|r| {
        r.nsec3_data.hash_algorithm() != hash_algorithm
            || r.nsec3_data.salt() != salt
            || r.nsec3_data.iterations() != iterations
    }) {
        return proof_log_yield(Proof::Bogus, query.name(), "nsec3", "parameter mismatch");
    }

    // Basic sanity checks are done.

    let query_name = query.name();

    // From here on 4 big situations are possible:
    // 1. No such name, and no servicing wildcard
    // 2. Name exists but there's no record of this type
    // 3. Name is serviced by wildcard that has a record of this type
    // 4. Name is serviced by wildcard that doesn't have a record of this type

    match response_code {
        // Case 1:
        ResponseCode::NXDomain => validate_nxdomain_response(query_name, soa_name, &nsec3s),

        // RFC 5155: NoData
        // Cases 2, 3, and 4:
        ResponseCode::NoError => {
            // Let's see if we received any answers.
            // This would signal that we have a wildcard servicing our `query_name`.
            // `num_labels` will show how many labels are there
            // in the wildcard that services the `query_name`
            let wildcard_num_labels = answers.iter().find_map(|record| {
                record
                    .data()
                    .as_dnssec()?
                    .as_rrsig()
                    .map(|data| data.num_labels())
            });
            validate_nodata_response(
                query_name,
                soa_name,
                query.query_type(),
                wildcard_num_labels,
                &nsec3s,
            )
        }
        _ => proof_log_yield(
            Proof::Bogus,
            query_name,
            "nsec3",
            &format!("unsupported response code ({response_code})")[..],
        ),
    }
}

struct Nsec3RecordPair<'a> {
    base32_hashed_name: Label,
    nsec3_data: &'a NSEC3,
}

fn split_first_label(name: &Name) -> Option<(&[u8], Name)> {
    let first_label = name.iter().next()?;
    let base = name.base_name();
    Some((first_label, base))
}

fn nsec3hash(name: &Name, salt: &[u8], iterations: u16) -> Vec<u8> {
    Nsec3HashAlgorithm::SHA1
        .hash(salt, name, iterations)
        // We only compute hashes of names between `query_name` and `soa_name`
        // and wildcards between `*.query_name.base_name()` and `*.soa_name`.
        // All of them are guaranteed to be valid names.
        .unwrap()
        .as_ref()
        .to_vec()
}

/// Hashes a name and returns both both the hash digest and the base32-encoded form.
fn hash_and_label(name: &Name, salt: &[u8], iterations: u16) -> (Vec<u8>, Label) {
    let hash = nsec3hash(name, salt, iterations);
    let base32_encoded = data_encoding::BASE32_DNSSEC.encode(&hash);
    // Unwrap safety: The length of the hashed name is valid because it is the output of the above
    // hash function. The input is all alphanumeric ASCII characters by construction.
    let label = Label::from_ascii(&base32_encoded).unwrap();
    (hash, label)
}

// NSEC3 records use a base32 hashed name as a record name component.
// But within the record body the hash is stored as a binary blob.
// Thus we need both for comparisons.
struct HashedNameInfo {
    name: Name,
    hashed_name: Vec<u8>,
    base32_hashed_name: Label,
}

impl HashedNameInfo {
    /// Hash a query name and store all representations of it.
    fn new(name: Name, salt: &[u8], iterations: u16) -> Self {
        let (hashed_name, base32_hashed_name) = hash_and_label(&name, salt, iterations);
        Self {
            name,
            hashed_name,
            base32_hashed_name,
        }
    }
}

fn find_covering_record<'a>(
    nsec3s: &'a [Nsec3RecordPair<'a>],
    target_hashed_name: &[u8],
    // Strictly speaking we don't need this parameter, we can calculate
    // base32(target_hashed_name) inside the function.
    // However, we already have it available at call sites, may as well use
    // it and save on repeated base32 encodings.
    target_base32_hashed_name: &Label,
) -> Option<&'a Nsec3RecordPair<'a>> {
    nsec3s.iter().find(|record| {
        let Some(record_next_hashed_owner_name_base32) =
            record.nsec3_data.next_hashed_owner_name_base32()
        else {
            return false;
        };
        if record.base32_hashed_name < *record_next_hashed_owner_name_base32 {
            // Normal case: target must be between the hashed owner name and the next hashed owner
            // name.
            record.base32_hashed_name < *target_base32_hashed_name
                && target_hashed_name < record.nsec3_data.next_hashed_owner_name()
        } else {
            // Wraparound case: target must be less than the hashed owner name or greater than the
            // next hashed owner name.
            record.base32_hashed_name > *target_base32_hashed_name
                || target_hashed_name > record.nsec3_data.next_hashed_owner_name()
        }
    })
}

/// There is no such `query_name` in the zone and there's no wildcard that
/// can be expanded to service this `query_name`.
///
/// Expecting the following records:
/// * closest encloser - *matching* NSEC3 record
/// * next closer - *covering* NSEC3 record
/// * wildcard of closest encloser - *covering* NSEC3 record
fn validate_nxdomain_response(
    query_name: &Name,
    soa_name: &Name,
    nsec3s: &[Nsec3RecordPair<'_>],
) -> Proof {
    debug_assert!(!nsec3s.is_empty());
    let salt = nsec3s[0].nsec3_data.salt();
    let iterations = nsec3s[0].nsec3_data.iterations();

    let (_, base32_hashed_query_name) = hash_and_label(query_name, salt, iterations);

    // The response is NXDomain but there's a record for query_name
    if nsec3s
        .iter()
        .any(|r| r.base32_hashed_name == base32_hashed_query_name)
    {
        return proof_log_yield(
            Proof::Bogus,
            query_name,
            "nsec3",
            "NXDomain response with record for query name",
        );
    }

    let (closest_encloser_proof_info, early_proof) =
        closest_encloser_proof(query_name, soa_name, nsec3s);

    if let Some(proof) = early_proof {
        return proof_log_yield(proof, query_name, "nsec3", "returning early proof");
    }

    // Note that the three fields may hold references to the same NSEC3
    // record, because the interval of base32_hashed_name
    // and next_hashed_owner_name happen to match / cover all three components
    // of closest encloser proof.
    let ClosestEncloserProofInfo {
        closest_encloser,
        next_closer,
        closest_encloser_wildcard,
    } = closest_encloser_proof_info;

    match (closest_encloser, next_closer, closest_encloser_wildcard) {
        // Got all three components - we proved that there's no `query_name`
        // in the zone
        (Some(_), Some(_), Some(_)) => {
            proof_log_yield(Proof::Secure, query_name, "nsec3", "direct proof")
        }
        // `query_name`'s parent is the `soa_name` itself, so there's no need
        // to send `soa_name`'s NSEC3 record. Still we have to show that
        // both `query_name` doesn't exist and there's no wildcard to service it
        (None, Some(_), Some(_)) if &query_name.base_name() == soa_name => proof_log_yield(
            Proof::Secure,
            query_name,
            "nsec3",
            "no direct or wildcard proof, but parent name of query is SOA",
        ),
        _ => proof_log_yield(
            Proof::Bogus,
            query_name,
            "nsec3",
            "no proof of non-existence",
        ),
    }
}

struct ClosestEncloserProofInfo<'a> {
    closest_encloser: Option<(HashedNameInfo, &'a Nsec3RecordPair<'a>)>,
    next_closer: Option<(HashedNameInfo, &'a Nsec3RecordPair<'a>)>,
    closest_encloser_wildcard: Option<(HashedNameInfo, &'a Nsec3RecordPair<'a>)>,
}

/// For each intermediary name from `query_name` to `soa_name` this function
/// constructs a triplet of (Name, HashedName, Base32EncodedHashedName)
///
/// For `a.b.c.soa.name` it will generate:
/// [
///     (a.b.c.soa.name, h(a.b.c.soa.name), b32h(a.b.c.soa.name)),
///     (b.c.soa.name, h(b.c.soa.name), b32h(b.c.soa.name)),
///     (c.soa.name, h(c.soa.name), b32h(c.soa.name)),
///     (soa.name, h(soa.name), b32h(soa.name)),
/// ]
///
/// The list *starts* with `query_name` and *ends* with `soa_name`. Other
/// code in this module exploits this invariant.
///
/// In simplest situations when `query_name` is `label.soa_name` it itself
/// will act as "next closer"
fn build_encloser_candidates_list(
    query_name: &Name,
    soa_name: &Name,
    salt: &[u8],
    iterations: u16,
) -> Vec<HashedNameInfo> {
    let mut candidates = Vec::with_capacity(query_name.num_labels() as usize);

    // `query_name` is our first candidate
    let mut name = query_name.clone();
    loop {
        candidates.push(HashedNameInfo::new(name.clone(), salt, iterations));
        if &name == soa_name {
            // `soa_name` is the final candidate, we already added it.
            return candidates;
        }
        name = name.base_name();
        // TODO: can `query_name` *not* be a sub-name of `soa_name`?
        debug_assert_ne!(name, Name::root());
    }
}

/// Expecting the following records:
/// * closest encloser - *matching* NSEC3 record
/// * next closer - *covering* NSEC3 record
/// * wildcard of closest encloser - *covering* NSEC3 record
fn closest_encloser_proof<'a>(
    query_name: &Name,
    soa_name: &Name,
    nsec3s: &'a [Nsec3RecordPair<'a>],
) -> (ClosestEncloserProofInfo<'a>, Option<Proof>) {
    debug_assert!(!nsec3s.is_empty());
    let salt = nsec3s[0].nsec3_data.salt();
    let iterations = nsec3s[0].nsec3_data.iterations();

    let mut closest_encloser_candidates =
        build_encloser_candidates_list(query_name, soa_name, salt, iterations);

    // Search for *matching* closing encloser record, i.e.
    // An NSEC3 record those name matches one of the names in
    // candidate list
    let closest_encloser_in_candidates = closest_encloser_candidates.iter().enumerate().find_map(
        |(candidate_index, candidate_name_info)| {
            let nsec3 = nsec3s
                .iter()
                .find(|r| r.base32_hashed_name == candidate_name_info.base32_hashed_name);
            nsec3.map(|record| (candidate_index, record))
        },
    );

    match closest_encloser_in_candidates {
        // General flow - there's a record for closest encloser
        Some((closest_encloser_index, closest_encloser_record)) if closest_encloser_index > 0 => {
            let closest_encloser_hash_info =
                closest_encloser_candidates.swap_remove(closest_encloser_index);
            let closest_encloser_wildcard_name = Name::new()
                .append_label("*")
                .unwrap()
                .append_name(&closest_encloser_hash_info.name)
                .expect("closest encloser name exists in the zone");
            let closest_encloser = Some((closest_encloser_hash_info, closest_encloser_record));

            let next_closer_hash_info =
                closest_encloser_candidates.swap_remove(closest_encloser_index - 1);
            let next_closer = find_covering_record(
                nsec3s,
                &next_closer_hash_info.hashed_name,
                &next_closer_hash_info.base32_hashed_name,
            )
            .map(|record| (next_closer_hash_info, record));

            let wildcard_name_info =
                HashedNameInfo::new(closest_encloser_wildcard_name, salt, iterations);
            let wildcard = find_covering_record(
                nsec3s,
                &wildcard_name_info.hashed_name,
                &wildcard_name_info.base32_hashed_name,
            )
            .map(|record| (wildcard_name_info, record));

            (
                ClosestEncloserProofInfo {
                    closest_encloser,
                    next_closer,
                    closest_encloser_wildcard: wildcard,
                },
                None,
            )
        }
        Some((0, _)) => {
            // Closest encloser at index 0 corresponds to an NSEC3 record
            // with the key = base32(hash(`query_name`)), which should not be
            // possible because that would mean `query_name` exists in the zone,
            // but the response code is NXDomain.
            (
                ClosestEncloserProofInfo {
                    closest_encloser: None,
                    next_closer: None,
                    closest_encloser_wildcard: None,
                },
                Some(Proof::Bogus),
            )
        }
        Some(_) => unreachable!(
            "the compiler is convinced the first two cases don't match all Some(_)s possible"
        ),
        None if &query_name.base_name() == soa_name => {
            // There's no record for closest encloser.
            // It may not be present since the encloser is `soa_name` which
            // is *known to exist*.
            //
            // Next closer *is* `query_name`, hence index 0
            let next_encloser_hash_info = closest_encloser_candidates.swap_remove(0);
            let next_closer = find_covering_record(
                nsec3s,
                &next_encloser_hash_info.hashed_name,
                &next_encloser_hash_info.base32_hashed_name,
            )
            .map(|record| (next_encloser_hash_info, record));

            // Additionally there should be an NSEC3 record *covering*
            // `*.soa_name` wildcard.
            // If the wildcard existed then the response code would be NoError
            // but we received `NXDomain`
            let closest_encloser_wildcard_name = Name::new()
                .append_label("*")
                .unwrap()
                .append_name(soa_name)
                .expect("`soa_name` is an existing domain with a valid name");
            let wildcard_name_info =
                HashedNameInfo::new(closest_encloser_wildcard_name, salt, iterations);
            let wildcard = find_covering_record(
                nsec3s,
                &wildcard_name_info.hashed_name,
                &wildcard_name_info.base32_hashed_name,
            )
            .map(|record| (wildcard_name_info, record));

            (
                ClosestEncloserProofInfo {
                    closest_encloser: None,
                    next_closer,
                    closest_encloser_wildcard: wildcard,
                },
                None,
            )
        }
        None => {
            // Problematic case: a.b.soa.name doesn't exist
            // but there's no NSEC3 record for any of the ancestors
            //
            // If `b.soa.name` existed we should have its *matching* NSEC3 record
            // and a record *covering* `a.b.soa.name` in `next_closer`
            //
            // If `b.soa.name` didn't exist we would get a record *covering* it
            // in `next_closer`.
            (
                ClosestEncloserProofInfo {
                    closest_encloser: None,
                    next_closer: None,
                    closest_encloser_wildcard: None,
                },
                Some(Proof::Bogus),
            )
        }
    }
}

/// This function addresses three situations:
///
/// Case 2. Name exists but there's no record of this type
/// Case 3. Opt-out proof for Name exists
/// Case 4. Name is serviced by wildcard that has a record of this type
/// Case 5. Name is serviced by wildcard that doesn't have a record of this type
fn validate_nodata_response(
    query_name: &Name,
    soa_name: &Name,
    query_type: RecordType,
    wildcard_encloser_num_labels: Option<u8>,
    nsec3s: &[Nsec3RecordPair<'_>],
) -> Proof {
    // 2. Name exists but there's no record of this type
    // 3. Opt-out proof for Name exists
    // 4. Name is serviced by wildcard that has a record of this type
    // 5. Name is serviced by wildcard that doesn't have a record of this type

    debug_assert!(!nsec3s.is_empty());
    let salt = nsec3s[0].nsec3_data.salt();
    let iterations = nsec3s[0].nsec3_data.iterations();

    let (hashed_query_name, base32_hashed_query_name) =
        hash_and_label(query_name, salt, iterations);

    let query_name_record = nsec3s
        .iter()
        .find(|record| record.base32_hashed_name == base32_hashed_query_name);

    // Case 2:
    // Name exists but there's no record of this type
    //
    // RFC 5155 § 8.5 et seq.
    //
    //   8.5.  Validating No Data Responses, QTYPE is not DS
    //
    //   The validator MUST verify that an NSEC3 RR that matches QNAME is
    //   present and that both the QTYPE and the CNAME type are not set in its
    //   Type Bit Maps field.
    //
    //   Note that this test also covers the case where the NSEC3 RR exists
    //   because it corresponds to an empty non-terminal, in which case the
    //   NSEC3 RR will have an empty Type Bit Maps field.
    //
    //   8.6.  Validating No Data Responses, QTYPE is DS
    //
    //   If there is an NSEC3 RR that matches QNAME present in the response,
    //   then that NSEC3 RR MUST NOT have the bits corresponding to DS and
    //   CNAME set in its Type Bit Maps field.
    //
    //   If there is no such NSEC3 RR, then the validator MUST verify that a
    //   closest provable encloser proof for QNAME is present in the response,
    //   and that the NSEC3 RR that covers the "next closer" name has the Opt-
    //   Out bit set.
    if let Some(query_record) = query_name_record {
        if query_record.nsec3_data.type_set().contains(query_type)
            || query_record
                .nsec3_data
                .type_set()
                .contains(RecordType::CNAME)
        {
            return proof_log_yield(
                Proof::Bogus,
                query_name,
                "nsec3",
                &format!("nsec3 type map covers {query_type} or CNAME")[..],
            );
        } else {
            return proof_log_yield(
                Proof::Secure,
                query_name,
                "nsec3",
                &format!("type map does not cover {query_type} or CNAME")[..],
            );
        }
    }

    // Case 3:
    // Query type is DS, records for name exist, but there are no DS records (opt-out proof)
    //
    // RFC 5155 § 6
    //
    //   In this specification, as in [RFC4033], [RFC4034] and [RFC4035], NS
    //   RRSets at delegation points are not signed and may be accompanied by
    //   a DS RRSet.  With the Opt-Out bit clear, the security status of the
    //   child zone is determined by the presence or absence of this DS RRSet,
    //   cryptographically proven by the signed NSEC3 RR at the hashed owner
    //   name of the delegation.  Setting the Opt-Out flag modifies this by
    //   allowing insecure delegations to exist within the signed zone without
    //   a corresponding NSEC3 RR at the hashed owner name of the delegation.
    //
    //   An Opt-Out NSEC3 RR is said to cover a delegation if the hash of the
    //   owner name or "next closer" name of the delegation is between the
    //   owner name of the NSEC3 RR and the next hashed owner name.
    //
    //   An Opt-Out NSEC3 RR does not assert the existence or non-existence of
    //   the insecure delegations that it may cover.  This allows for the
    //   addition or removal of these delegations without recalculating or re-
    //   signing RRs in the NSEC3 RR chain.  However, Opt-Out NSEC3 RRs do
    //   assert the (non)existence of other, authoritative RRSets.
    //
    //   An Opt-Out NSEC3 RR MAY have the same original owner name as an
    //   insecure delegation.  In this case, the delegation is proven insecure
    //   by the lack of a DS bit in the type map and the signed NSEC3 RR does
    //   assert the existence of the delegation.
    //
    //   Zones using Opt-Out MAY contain a mixture of Opt-Out NSEC3 RRs and
    //   non-Opt-Out NSEC3 RRs.  If an NSEC3 RR is not Opt-Out, there MUST NOT
    //   be any hashed owner names of insecure delegations (nor any other RRs)
    //   between it and the name indicated by the next hashed owner name in
    //   the NSEC3 RDATA.  If it is Opt-Out, it MUST only cover hashed owner
    //   names or hashed "next closer" names of insecure delegations.
    //
    //   The effects of the Opt-Out flag on signing, serving, and validating
    //   responses are covered in following sections.
    //
    // *Note*: the case of an opt-out NSEC3 record having the same original owner
    // name as the hashed query name and not having the DS bit set in the type flags
    // is covered here by case 2.
    if query_type == RecordType::DS
        && find_covering_record(nsec3s, &hashed_query_name, &base32_hashed_query_name)
            .is_some_and(|x| x.nsec3_data.opt_out())
    {
        return proof_log_yield(
            Proof::Secure,
            query_name,
            "nsec3",
            "DS query covered by opt-out proof",
        );
    }

    let (proof, reason) = match wildcard_encloser_num_labels {
        // Case 4:
        // Name is serviced by wildcard that has a record of this type
        Some(wildcard_encloser_num_labels) => {
            if query_name.num_labels() <= wildcard_encloser_num_labels {
                return proof_log_yield(
                    Proof::Bogus,
                    query_name,
                    "nsec3",
                    &format!(
                        "query labels ({}) <= wildcard encloser labels ({})",
                        query_name.num_labels(),
                        wildcard_encloser_num_labels,
                    )[..],
                );
            }
            // There should be an NSEC3 record *covering* `next_closer`
            let next_closer_labels = query_name
                .into_iter()
                .rev()
                .take(wildcard_encloser_num_labels as usize + 1)
                .rev()
                .collect::<Vec<_>>();
            let next_closer_name = Name::from_labels(next_closer_labels)
                .expect("next closer is `query_name` or its ancestor");
            let next_closer_name_info = HashedNameInfo::new(next_closer_name, salt, iterations);
            let next_closer_record = find_covering_record(
                nsec3s,
                &next_closer_name_info.hashed_name,
                &next_closer_name_info.base32_hashed_name,
            );
            match next_closer_record {
                Some(_) => (Proof::Secure, "matching next closer record"),
                None => (Proof::Bogus, "no matching next closer record"),
            }
        }

        // Case 5:
        // Name is serviced by wildcard that doesn't have a record of this type
        None => {
            let ClosestEncloserProofInfo {
                closest_encloser,
                next_closer,
                closest_encloser_wildcard,
            } = wildcard_based_encloser_proof(query_name, soa_name, nsec3s);
            match (closest_encloser, next_closer, closest_encloser_wildcard) {
                (Some(_), Some(_), Some(_)) => (
                    Proof::Secure,
                    "servicing wildcard with closest encloser proof",
                ),
                (None, Some(_), Some(_)) if &query_name.base_name() == soa_name => (
                    Proof::Secure,
                    "servicing wildcard without closest encloser proof, but query parent name == SOA",
                ),
                (None, None, None) if query_name == soa_name => (
                    Proof::Secure,
                    "no servicing wildcard, but query name == SOA",
                ),
                _ => (Proof::Bogus, "no valid servicing wildcard proof"),
            }
        }
    };

    proof_log_yield(proof, query_name, "nsec3", reason)
}

/// Expecting the following records:
/// * closest encloser - *matching* NSEC3 record
/// * next closer - *covering* NSEC3 record
/// * wildcard of closest encloser - *matching* NSEC3 record
///   NOTE: this is the difference between this and NXDomain case
///
/// Unlike non-wildcard version this cannot produce the early `Proof`
fn wildcard_based_encloser_proof<'a>(
    query_name: &Name,
    soa_name: &Name,
    nsec3s: &'a [Nsec3RecordPair<'a>],
) -> ClosestEncloserProofInfo<'a> {
    debug_assert!(!nsec3s.is_empty());
    let salt = nsec3s[0].nsec3_data.salt();
    let iterations = nsec3s[0].nsec3_data.iterations();

    let mut closest_encloser_candidates =
        build_encloser_candidates_list(query_name, soa_name, salt, iterations);

    // For `a.b.c.soa.name` the `closest_encloser_candidates` will have:
    // [
    //     (a.b.c.soa.name, h(a.b.c.soa.name), b32h(a.b.c.soa.name)),
    //     (b.c.soa.name, h(b.c.soa.name), b32h(b.c.soa.name)),
    //     (c.soa.name, h(c.soa.name), b32h(c.soa.name)),
    //     (soa.name, h(soa.name), b32h(soa.name)),
    // ]
    //
    // `wildcard_encloser_candidates` will have:
    // [
    //     (*.b.c.soa.name, h(*.b.c.soa.name), b32h(*.b.c.soa.name)),
    //     (*.c.soa.name, h(*.c.soa.name), b32h(*.c.soa.name)),
    //     (*.soa.name, h(*.soa.name), b32h(*.soa.name)),
    // ]
    //
    let mut wildcard_encloser_candidates = closest_encloser_candidates
        .iter()
        .filter(|HashedNameInfo { name, .. }| name != soa_name)
        .map(|info| {
            let wildcard = info.name.clone().into_wildcard();
            HashedNameInfo::new(wildcard, salt, iterations)
        })
        .collect::<Vec<_>>();

    let wildcard_encloser = wildcard_encloser_candidates
        .iter()
        .enumerate()
        .find_map(|(index, wildcard)| {
            let wildcard_nsec3 = nsec3s
                .iter()
                .find(|record| record.base32_hashed_name == wildcard.base32_hashed_name);
            wildcard_nsec3.map(|record| (index, record))
        })
        .map(|(index, record)| {
            let wildcard_name_info = wildcard_encloser_candidates.swap_remove(index);
            (wildcard_name_info, record)
        });

    let Some((wildcard_encloser_name_info, _)) = &wildcard_encloser else {
        return ClosestEncloserProofInfo {
            closest_encloser: None,
            next_closer: None,
            closest_encloser_wildcard: None,
        };
    };

    // Wildcard record exists. Within the wildcard there should be
    // a next_closer with a name <unknown>.<wildcard_base_name>.

    let closest_encloser_name = wildcard_encloser_name_info.name.base_name();

    // the shortest `wildcard_encloser` would be `*.soa.name`,
    // and its `.base_name()` would be `soa.name` itself, which is
    // guaranteed to be in the `closest_encloser_candidates`.
    // all other, longer, wildcards would have their base_names from
    // `query_name.base_name()` to `soa.name` and thus are guaranteed
    // to be in the `closest_encloser_candidates`, too.
    let closest_encloser_index = closest_encloser_candidates
        .iter()
        .position(|name_info| name_info.name == closest_encloser_name)
        .expect("cannot fail, always > 0");

    // `closest_encloser_candidates` starts with query_name itself, index 0.
    // The *closest* name a `closest_encloser` can be is
    // `query_name.base_name()`, because it's derived from a wildcard above.
    // Thus, `closest_encloser` would have an index 1 or bigger
    debug_assert!(closest_encloser_index >= 1);
    let closest_encloser_name_info =
        closest_encloser_candidates.swap_remove(closest_encloser_index);
    let closest_encloser_covering_record = find_covering_record(
        nsec3s,
        &closest_encloser_name_info.hashed_name,
        &closest_encloser_name_info.base32_hashed_name,
    );

    // Since `closest_encloser_index` is >= 1, this is always valid.
    let next_closer_index = closest_encloser_index - 1;
    let next_closer_name_info = closest_encloser_candidates.swap_remove(next_closer_index);
    let next_closer_covering_record = find_covering_record(
        nsec3s,
        &next_closer_name_info.hashed_name,
        &next_closer_name_info.base32_hashed_name,
    );

    ClosestEncloserProofInfo {
        closest_encloser: closest_encloser_covering_record
            .map(|record| (closest_encloser_name_info, record)),
        next_closer: next_closer_covering_record.map(|record| (next_closer_name_info, record)),
        closest_encloser_wildcard: wildcard_encloser,
    }
}