use core::fmt::Display;
use super::{Proof, proof_log_yield};
use crate::proto::{
dnssec::{
Nsec3HashAlgorithm,
rdata::{DNSSECRData, NSEC3},
},
op::{Query, ResponseCode},
rr::{Label, Name, RData, Record, RecordType},
};
pub(super) fn verify_nsec3(
query: &Query,
soa: Option<&Name>,
response_code: ResponseCode,
answers: &[Record],
nsec3s: &[(&Name, &NSEC3)],
nsec3_soft_iteration_limit: u16,
nsec3_hard_iteration_limit: u16,
) -> Proof {
debug_assert!(!nsec3s.is_empty());
let mut pairs = Vec::with_capacity(nsec3s.len());
for (name, data) in nsec3s {
let Some((base32_hashed_name, base)) = split_first_label(name) else {
return nsec3_yield(Proof::Bogus, query, "record name format is invalid");
};
if soa.is_some_and(|soa| &base != soa) {
return nsec3_yield(Proof::Bogus, query, "record name is not in the zone");
}
let Ok(base32_hashed_name) = Label::from_raw_bytes(base32_hashed_name) else {
return nsec3_yield(Proof::Bogus, query, "base32-hashed name is invalid");
};
pairs.push(Nsec3RecordPair {
base32_hashed_name,
nsec3_data: data,
});
}
debug_assert!(!pairs.is_empty());
let first = &pairs[0];
let hash_algorithm = first.nsec3_data.hash_algorithm();
let salt = first.nsec3_data.salt();
let iterations = first.nsec3_data.iterations();
if pairs.iter().any(|r| {
r.nsec3_data.hash_algorithm() != hash_algorithm
|| r.nsec3_data.salt() != salt
|| r.nsec3_data.iterations() != iterations
}) {
return nsec3_yield(Proof::Bogus, query, "parameter mismatch");
}
if iterations > nsec3_hard_iteration_limit {
return nsec3_yield(
Proof::Bogus,
query,
format_args!("iteration count {iterations} is over {nsec3_hard_iteration_limit}"),
);
} else if iterations > nsec3_soft_iteration_limit {
return nsec3_yield(
Proof::Insecure,
query,
format_args!("iteration count {iterations} is over {nsec3_soft_iteration_limit}"),
);
}
let cx = Context {
query,
soa,
nsec3s: &pairs,
hash_algorithm,
salt,
iterations,
};
match response_code {
ResponseCode::NXDomain => validate_nxdomain_response(&cx),
ResponseCode::NoError => {
let wildcard_num_labels = answers.iter().find_map(|record| match &record.data {
RData::DNSSEC(DNSSECRData::RRSIG(data)) => Some(data.input().num_labels),
_ => None,
});
validate_nodata_response(query.query_type(), wildcard_num_labels, &cx)
}
_ => cx.proof(
Proof::Bogus,
format_args!("unsupported response code ({response_code})"),
),
}
}
fn validate_nxdomain_response(cx: &Context<'_>) -> Proof {
let (_, base32_hashed_query_name) = cx.hash_and_label(cx.query.name());
if cx
.nsec3s
.iter()
.any(|r| r.base32_hashed_name == base32_hashed_query_name)
{
return cx.proof(Proof::Bogus, "NXDomain response with record for query name");
}
let (
ClosestEncloserProofInfo {
closest_encloser,
next_closer,
},
closest_encloser_wildcard,
) = cx.closest_encloser_proof_with_wildcard(false);
if closest_encloser.is_none() && next_closer.is_none() {
return cx.proof(Proof::Bogus, "returning early proof");
}
match (closest_encloser, next_closer, closest_encloser_wildcard) {
(Some(_), Some(_), Some(_)) => cx.proof(Proof::Secure, "direct proof"),
(None, Some(_), Some(_)) if Some(&cx.query.name().base_name()) == cx.soa => cx.proof(
Proof::Secure,
"no direct or wildcard proof, but parent name of query is SOA",
),
_ => cx.proof(Proof::Bogus, "no proof of non-existence"),
}
}
fn validate_nodata_response(
query_type: RecordType,
wildcard_encloser_num_labels: Option<u8>,
cx: &Context<'_>,
) -> Proof {
let (hashed_query_name, base32_hashed_query_name) = cx.hash_and_label(cx.query.name());
let query_name_record = cx
.nsec3s
.iter()
.find(|record| record.base32_hashed_name == base32_hashed_query_name);
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 cx.proof(
Proof::Bogus,
format_args!("nsec3 type map covers {query_type} or CNAME"),
);
} else {
return cx.proof(
Proof::Secure,
format_args!("type map does not cover {query_type} or CNAME"),
);
}
}
if query_type == RecordType::DS
&& find_covering_record(cx.nsec3s, &hashed_query_name, &base32_hashed_query_name)
.is_some_and(|x| x.nsec3_data.opt_out())
{
return cx.proof(Proof::Secure, "DS query covered by opt-out proof");
}
let (proof, reason) = match wildcard_encloser_num_labels {
Some(wildcard_encloser_num_labels) => {
if cx.query.name().num_labels() <= wildcard_encloser_num_labels {
return cx.proof(
Proof::Bogus,
format_args!(
"query labels ({}) <= wildcard encloser labels ({})",
cx.query.name().num_labels(),
wildcard_encloser_num_labels,
),
);
}
let next_closer_labels = cx
.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, cx);
let next_closer_record = find_covering_record(
cx.nsec3s,
&next_closer_name_info.hashed_name,
&next_closer_name_info.base32_hashed_name,
);
match next_closer_record {
Some(_) => (Proof::Secure, "covering next closer record"),
None => (Proof::Bogus, "no covering next closer record"),
}
}
None => {
let (
ClosestEncloserProofInfo {
closest_encloser,
next_closer,
},
closest_encloser_wildcard,
) = cx.closest_encloser_proof_with_wildcard(true);
match (closest_encloser, next_closer, closest_encloser_wildcard) {
(Some(_), Some(_), Some((_, wildcard)))
if !wildcard.nsec3_data.type_set().contains(query_type)
&& !wildcard.nsec3_data.type_set().contains(RecordType::CNAME) =>
{
(
Proof::Secure,
"servicing wildcard with closest encloser proof",
)
}
(None, Some(_), Some(_)) if Some(&cx.query.name().base_name()) == cx.soa => (
Proof::Secure,
"servicing wildcard without closest encloser proof, but query parent name == SOA",
),
(None, None, None) if Some(cx.query.name()) == cx.soa => (
Proof::Secure,
"no servicing wildcard, but query name == SOA",
),
_ => (Proof::Bogus, "no valid servicing wildcard proof"),
}
}
};
cx.proof(proof, reason)
}
fn split_first_label(name: &Name) -> Option<(&[u8], Name)> {
let first_label = name.iter().next()?;
let base = name.base_name();
Some((first_label, base))
}
#[derive(Default)]
struct ClosestEncloserProofInfo<'a> {
closest_encloser: Option<(HashedNameInfo, &'a Nsec3RecordPair<'a>)>,
next_closer: Option<(HashedNameInfo, &'a Nsec3RecordPair<'a>)>,
}
struct Context<'a> {
query: &'a Query,
soa: Option<&'a Name>,
nsec3s: &'a [Nsec3RecordPair<'a>],
hash_algorithm: Nsec3HashAlgorithm,
salt: &'a [u8],
iterations: u16,
}
impl<'a> Context<'a> {
fn closest_encloser_proof_with_wildcard(
&'a self,
matching: bool,
) -> (
ClosestEncloserProofInfo<'a>,
Option<(HashedNameInfo, &'a Nsec3RecordPair<'a>)>,
) {
let closest_encloser_proof = self.closest_encloser_proof();
let closest_encloser_name = match closest_encloser_proof.closest_encloser.as_ref() {
Some((name_info, _)) => name_info.name.clone(),
None => return (closest_encloser_proof, None),
};
let Ok(wildcard_encloser_name) = closest_encloser_name.prepend_label("*") else {
return (closest_encloser_proof, None);
};
let wildcard_name_info = HashedNameInfo::new(wildcard_encloser_name, self);
let wildcard_record = if matching {
self.nsec3s
.iter()
.find(|record| record.base32_hashed_name == wildcard_name_info.base32_hashed_name)
} else {
find_covering_record(
self.nsec3s,
&wildcard_name_info.hashed_name,
&wildcard_name_info.base32_hashed_name,
)
};
(
closest_encloser_proof,
wildcard_record.map(|record| (wildcard_name_info, record)),
)
}
fn closest_encloser_proof(&'a self) -> ClosestEncloserProofInfo<'a> {
let mut closest_encloser_candidates = self
.encloser_candidates()
.map(|name| HashedNameInfo::new(name, self))
.collect::<Vec<_>>();
let Some(closest_encloser_matching_record) =
closest_encloser_candidates.iter().find_map(|candidate| {
self.nsec3s
.iter()
.find(|nsec| nsec.base32_hashed_name == candidate.base32_hashed_name)
})
else {
return ClosestEncloserProofInfo::default();
};
let Some(closest_encloser_index) = closest_encloser_candidates
.iter()
.enumerate()
.skip(1) .find(|(_, candidate)| {
candidate.base32_hashed_name == closest_encloser_matching_record.base32_hashed_name
})
.map(|(i, _)| i)
else {
return ClosestEncloserProofInfo::default();
};
let closest_encloser_name_info =
closest_encloser_candidates.swap_remove(closest_encloser_index);
let next_closer_name_info =
closest_encloser_candidates.swap_remove(closest_encloser_index - 1);
let next_closer_covering_record = find_covering_record(
self.nsec3s,
&next_closer_name_info.hashed_name,
&next_closer_name_info.base32_hashed_name,
);
ClosestEncloserProofInfo {
closest_encloser: Some((closest_encloser_name_info, closest_encloser_matching_record)),
next_closer: next_closer_covering_record.map(|record| (next_closer_name_info, record)),
}
}
fn hash_and_label(&self, name: &Name) -> (Vec<u8>, Label) {
let hash = self
.hash_algorithm
.hash(self.salt, name, self.iterations)
.unwrap()
.as_ref()
.to_vec();
let base32_encoded = data_encoding::BASE32_DNSSEC.encode(&hash);
let label = Label::from_ascii(&base32_encoded).unwrap();
(hash, label)
}
fn encloser_candidates(&self) -> EncloserCandidates<'a> {
EncloserCandidates {
cur: Some(self.query.name().clone()),
soa: self.soa,
}
}
fn proof(&self, proof: Proof, msg: impl Display) -> Proof {
nsec3_yield(proof, self.query, msg)
}
}
fn find_covering_record<'a>(
nsec3s: &'a [Nsec3RecordPair<'a>],
target_hashed_name: &[u8],
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 == *target_base32_hashed_name {
return false;
}
if record.base32_hashed_name < *record_next_hashed_owner_name_base32 {
record.base32_hashed_name < *target_base32_hashed_name
&& target_hashed_name < record.nsec3_data.next_hashed_owner_name()
} else {
record.base32_hashed_name > *target_base32_hashed_name
|| target_hashed_name > record.nsec3_data.next_hashed_owner_name()
}
})
}
#[derive(Clone, Debug)]
struct HashedNameInfo {
name: Name,
hashed_name: Vec<u8>,
base32_hashed_name: Label,
}
impl HashedNameInfo {
fn new(name: Name, cx: &Context<'_>) -> Self {
let (hashed_name, base32_hashed_name) = cx.hash_and_label(&name);
Self {
name,
hashed_name,
base32_hashed_name,
}
}
}
struct EncloserCandidates<'a> {
cur: Option<Name>,
soa: Option<&'a Name>,
}
impl Iterator for EncloserCandidates<'_> {
type Item = Name;
fn next(&mut self) -> Option<Self::Item> {
let cur = self.cur.take()?;
let soa = self.soa?;
if &cur != soa {
let next = cur.base_name();
debug_assert_ne!(next, Name::root());
self.cur = Some(next);
}
Some(cur)
}
}
fn nsec3_yield(proof: Proof, query: &Query, msg: impl Display) -> Proof {
proof_log_yield(proof, query, "nsec3", msg)
}
struct Nsec3RecordPair<'a> {
base32_hashed_name: Label,
nsec3_data: &'a NSEC3,
}
#[cfg(test)]
mod tests {
use core::str::FromStr;
use super::*;
use crate::proto::{
ProtoError,
dnssec::{
Algorithm,
rdata::{DNSSECRData, RRSIG as rdataRRSIG, SigInput},
},
rr::{
RData,
RecordType::{A, AAAA, DNSKEY, DS, MX, NS, NSEC3PARAM, RRSIG, SOA},
SerialNumber, rdata,
},
};
use test_support::subscribe;
#[test]
fn test_hash() {
let name = Name::from_str("www.example.com").unwrap();
let salt: Vec<u8> = vec![1, 2, 3, 4];
assert_eq!(
Nsec3HashAlgorithm::SHA1
.hash(&salt, &name, 0)
.unwrap()
.as_ref()
.len(),
20
);
assert_eq!(
Nsec3HashAlgorithm::SHA1
.hash(&salt, &name, 1)
.unwrap()
.as_ref()
.len(),
20
);
assert_eq!(
Nsec3HashAlgorithm::SHA1
.hash(&salt, &name, 3)
.unwrap()
.as_ref()
.len(),
20
);
}
#[test]
fn test_known_hashes() {
assert_eq!(
hash_with_base32("example"),
"0p9mhaveqvm6t7vbl5lop2u3t2rp3tom"
);
assert_eq!(
hash_with_base32("EXAMPLE"),
"0p9mhaveqvm6t7vbl5lop2u3t2rp3tom"
);
assert_eq!(
hash_with_base32("a.example"),
"35mthgpgcu1qg68fab165klnsnk3dpvl"
);
assert_eq!(
hash_with_base32("ai.example"),
"gjeqe526plbf1g8mklp59enfd789njgi"
);
assert_eq!(
hash_with_base32("ns1.example"),
"2t7b4g4vsa5smi47k61mv5bv1a22bojr"
);
assert_eq!(
hash_with_base32("ns2.example"),
"q04jkcevqvmu85r014c7dkba38o0ji5r"
);
assert_eq!(
hash_with_base32("w.example"),
"k8udemvp1j2f7eg6jebps17vp3n8i58h"
);
assert_eq!(
hash_with_base32("*.w.example"),
"r53bq7cc2uvmubfu5ocmm6pers9tk9en"
);
assert_eq!(
hash_with_base32("x.w.example"),
"b4um86eghhds6nea196smvmlo4ors995"
);
assert_eq!(
hash_with_base32("y.w.example"),
"ji6neoaepv8b5o6k4ev33abha8ht9fgc"
);
assert_eq!(
hash_with_base32("x.y.w.example"),
"2vptu5timamqttgl4luu9kg21e0aor3s"
);
assert_eq!(
hash_with_base32("xx.example"),
"t644ebqk9bibcna874givr6joj62mlhv"
);
}
#[test]
fn nsec3_name_error_tests() -> Result<(), ProtoError> {
subscribe();
assert_eq!(
verify_nsec3(
&Query::query(Name::from_ascii("a.c.x.w.example.")?, A),
Some(&Name::from_ascii("example.")?),
ResponseCode::NXDomain,
&[],
&[
Nsec3Pair::new(
Name::from_ascii("example.")?.prepend_label(hash_with_base32("example"))?,
hash("ns1.example."),
[MX, DNSKEY, NS, SOA, NSEC3PARAM, RRSIG],
)
.as_ref(),
Nsec3Pair::new(
Name::from_ascii("example.")?.prepend_label(hash_with_base32("x.w"))?,
hash("ai.example."),
[MX, RRSIG],
)
.as_ref(),
Nsec3Pair::new(
Name::from_ascii("example.")?.prepend_label(hash_with_base32("a"))?,
hash("x.w.example."),
[DS, NS, RRSIG],
)
.as_ref(),
],
200,
500,
),
Proof::Secure,
);
assert_eq!(
verify_nsec3(
&Query::query(Name::from_ascii("a.c.x.w.example.")?, A),
Some(&Name::from_ascii("example.")?),
ResponseCode::NXDomain,
&[],
&[
Nsec3Pair::new(
Name::from_ascii("example.")?.prepend_label(hash_with_base32("example"))?,
hash("ns1.example."),
[MX, DNSKEY, NS, SOA, NSEC3PARAM, RRSIG],
)
.as_ref(),
Nsec3Pair::new(
Name::from_ascii("example.")?.prepend_label(hash_with_base32("x.w"))?,
hash("ai.example."),
[MX, RRSIG],
)
.as_ref(),
],
200,
500,
),
Proof::Bogus,
);
assert_eq!(
verify_nsec3(
&Query::query(Name::from_ascii("a.c.x.w.example.")?, A),
Some(&Name::from_ascii("example.")?),
ResponseCode::NXDomain,
&[],
&[
Nsec3Pair::new(
Name::from_ascii("example.")?.prepend_label(hash_with_base32("x.w"))?,
hash("ai.example."),
[MX, RRSIG],
)
.as_ref(),
Nsec3Pair::new(
Name::from_ascii("example.")?.prepend_label(hash_with_base32("a"))?,
hash("x.w.example."),
[DS, NS, RRSIG],
)
.as_ref(),
],
200,
500,
),
Proof::Bogus,
);
assert_eq!(
verify_nsec3(
&Query::query(Name::from_ascii("a.c.x.w.example.")?, A),
Some(&Name::from_ascii("x.w.example.")?),
ResponseCode::NXDomain,
&[],
&[
Nsec3Pair::new(
Name::from_ascii("example.")?.prepend_label(hash_with_base32("example"))?,
hash("ns1.example."),
[MX, DNSKEY, NS, SOA, NSEC3PARAM, RRSIG],
)
.as_ref(),
Nsec3Pair::new(
Name::from_ascii("example.")?.prepend_label(hash_with_base32("x.w"))?,
hash("ai.example."),
[MX, RRSIG],
)
.as_ref(),
Nsec3Pair::new(
Name::from_ascii("example.")?.prepend_label(hash_with_base32("a"))?,
hash("x.w.example."),
[DS, NS, RRSIG],
)
.as_ref(),
],
200,
500,
),
Proof::Bogus,
);
Ok(())
}
#[test]
fn nsec3_no_data_error_tests() -> Result<(), ProtoError> {
subscribe();
assert_eq!(
verify_nsec3(
&Query::query(Name::from_ascii("ns1.example.")?, MX),
Some(&Name::from_ascii("example.")?),
ResponseCode::NoError,
&[],
&[
Nsec3Pair::new(
Name::from_ascii("example.")?
.prepend_label(hash_with_base32("ns1.example"))?,
hash("x.y.w.example."),
[A, RRSIG],
)
.as_ref(),
],
200,
500,
),
Proof::Secure,
);
assert_eq!(
verify_nsec3(
&Query::query(Name::from_ascii("y.w.example.")?, A),
Some(&Name::from_ascii("example.")?),
ResponseCode::NoError,
&[],
&[
Nsec3Pair::new(
Name::from_ascii("example.")?
.prepend_label(hash_with_base32("y.w.example"))?,
hash("w.example."),
[],
)
.as_ref(),
],
200,
500,
),
Proof::Secure,
);
assert_eq!(
verify_nsec3(
&Query::query(Name::from_ascii("ns1.example.")?, MX),
Some(&Name::from_ascii("example.")?),
ResponseCode::NoError,
&[],
&[
Nsec3Pair::new(
Name::from_ascii("example.")?
.prepend_label(hash_with_base32("ns1.example"))?,
hash("x.y.w.example."),
[A, RRSIG, MX],
)
.as_ref(),
],
200,
500,
),
Proof::Bogus,
);
assert_eq!(
verify_nsec3(
&Query::query(Name::from_ascii("ns1.example.")?, MX),
Some(&Name::from_ascii("example.")?),
ResponseCode::NoError,
&[],
&[Nsec3Pair::new(
Name::from_ascii("example.")?.prepend_label(hash_with_base32("ns2.example"))?,
hash("x.y.w.example."),
[A, RRSIG],
)
.as_ref(),],
200,
500,
),
Proof::Bogus,
);
assert_eq!(
verify_nsec3(
&Query::query(Name::from_ascii("ns1.example.")?, MX),
Some(&Name::from_ascii("example.")?),
ResponseCode::NoError,
&[],
&[Nsec3Pair::new(
Name::from_ascii("example.")?.prepend_label(hash_with_base32("example"))?,
hash("a.example."),
[A, SOA, DNSKEY, RRSIG],
)
.as_ref(),],
200,
500,
),
Proof::Bogus,
);
Ok(())
}
#[test]
fn nsec3_wildcard_expansion_tests() -> Result<(), ProtoError> {
subscribe();
let input = SigInput {
type_covered: MX,
algorithm: Algorithm::ED25519,
num_labels: 2,
original_ttl: 0,
sig_expiration: SerialNumber::new(0),
sig_inception: SerialNumber::new(0),
key_tag: 0,
signer_name: Name::root(),
};
let rrsig = rdataRRSIG::from_sig(input, vec![]);
let rrsig_record = Record::from_rdata(
Name::from_ascii("a.z.w.example.")?,
3600,
RData::DNSSEC(DNSSECRData::RRSIG(rrsig)),
);
let answers = [
Record::from_rdata(
Name::from_ascii("a.z.w.example.")?,
3600,
RData::MX(rdata::MX::new(10, Name::from_ascii("a.z.w.example.")?)),
),
rrsig_record,
];
assert_eq!(
verify_nsec3(
&Query::query(Name::from_ascii("a.z.w.example.")?, MX),
None,
ResponseCode::NoError,
&answers,
&[
Nsec3Pair::new(
Name::from_ascii("example.")?
.prepend_label(hash_with_base32("ns2.example"))?,
hash("*.w.example."),
[A, RRSIG],
)
.as_ref(),
],
200,
500,
),
Proof::Secure,
);
assert_eq!(
verify_nsec3(
&Query::query(Name::from_ascii("a.z.w.example.")?, MX),
None,
ResponseCode::NoError,
&answers,
&[
Nsec3Pair::new(
Name::from_ascii("example.")?.prepend_label(hash_with_base32("example"))?,
hash("a.example."),
[A, RRSIG],
)
.as_ref(),
],
200,
500,
),
Proof::Bogus,
);
assert_eq!(
verify_nsec3(
&Query::query(Name::from_ascii("a.z.w.example.")?, MX),
None,
ResponseCode::NoError,
&answers,
&[
Nsec3Pair::new(
Name::from_ascii("example.")?
.prepend_label(hash_with_base32("z.w.example"))?,
hash("a.example."),
[A, RRSIG],
)
.as_ref(),
],
200,
500,
),
Proof::Bogus,
);
Ok(())
}
#[test]
fn nsec3_wildcard_no_data_error_tests() -> Result<(), ProtoError> {
subscribe();
assert_eq!(
verify_nsec3(
&Query::query(Name::from_ascii("a.z.w.example.")?, AAAA),
Some(&Name::from_ascii("example.")?),
ResponseCode::NoError,
&[],
&[
Nsec3Pair::new(
Name::from_ascii("example.")?
.prepend_label(hash_with_base32("w.example"))?,
hash("2t7b4g4vsa5smi47k61mv5bv1a22bojr.example"),
[],
)
.as_ref(),
Nsec3Pair::new(
Name::from_ascii("example.")?
.prepend_label(hash_with_base32("ns2.example"))?,
hash("*.w.example"),
[A, RRSIG],
)
.as_ref(),
Nsec3Pair::new(
Name::from_ascii("example.")?
.prepend_label(hash_with_base32("*.w.example"))?,
hash("xx.example"),
[MX, RRSIG],
)
.as_ref(),
],
200,
500,
),
Proof::Secure,
);
assert_eq!(
verify_nsec3(
&Query::query(Name::from_ascii("a.z.w.example.")?, AAAA),
Some(&Name::from_ascii("example.")?),
ResponseCode::NoError,
&[],
&[
Nsec3Pair::new(
Name::from_ascii("example.")?
.prepend_label(hash_with_base32("ns2.example"))?,
hash("*.w.example"),
[A, RRSIG],
)
.as_ref(),
Nsec3Pair::new(
Name::from_ascii("example.")?
.prepend_label(hash_with_base32("*.w.example"))?,
hash("xx.example"),
[MX, RRSIG],
)
.as_ref(),
],
200,
500,
),
Proof::Bogus,
);
assert_eq!(
verify_nsec3(
&Query::query(Name::from_ascii("a.z.w.example.")?, AAAA),
Some(&Name::from_ascii("example.")?),
ResponseCode::NoError,
&[],
&[
Nsec3Pair::new(
Name::from_ascii("example.")?
.prepend_label(hash_with_base32("w.example"))?,
hash("2t7b4g4vsa5smi47k61mv5bv1a22bojr.example"),
[],
)
.as_ref(),
Nsec3Pair::new(
Name::from_ascii("example.")?
.prepend_label(hash_with_base32("*.w.example"))?,
hash("xx.example"),
[MX, RRSIG],
)
.as_ref(),
],
200,
500,
),
Proof::Bogus,
);
assert_eq!(
verify_nsec3(
&Query::query(Name::from_ascii("a.z.w.example.")?, AAAA),
Some(&Name::from_ascii("example.")?),
ResponseCode::NoError,
&[],
&[
Nsec3Pair::new(
Name::from_ascii("example.")?
.prepend_label(hash_with_base32("w.example"))?,
hash("2t7b4g4vsa5smi47k61mv5bv1a22bojr.example"),
[],
)
.as_ref(),
Nsec3Pair::new(
Name::from_ascii("example.")?
.prepend_label(hash_with_base32("ns2.example"))?,
hash("*.w.example"),
[A, RRSIG],
)
.as_ref(),
],
200,
500,
),
Proof::Bogus,
);
assert_eq!(
verify_nsec3(
&Query::query(Name::from_ascii("a.z.w.example.")?, AAAA),
None,
ResponseCode::NoError,
&[],
&[
Nsec3Pair::new(
Name::from_ascii("example.")?
.prepend_label(hash_with_base32("w.example"))?,
hash("2t7b4g4vsa5smi47k61mv5bv1a22bojr.example"),
[],
)
.as_ref(),
Nsec3Pair::new(
Name::from_ascii("example.")?
.prepend_label(hash_with_base32("ns2.example"))?,
hash("*.w.example"),
[A, RRSIG],
)
.as_ref(),
Nsec3Pair::new(
Name::from_ascii("example.")?
.prepend_label(hash_with_base32("*.w.example"))?,
hash("xx.example"),
[MX, RRSIG],
)
.as_ref(),
],
200,
500,
),
Proof::Bogus
);
Ok(())
}
#[derive(Debug)]
struct Nsec3Pair(Name, NSEC3);
impl Nsec3Pair {
fn new(
rr_name: Name,
next_name: Vec<u8>,
rrset: impl IntoIterator<Item = RecordType>,
) -> Self {
Self(
rr_name,
NSEC3::new(
Nsec3HashAlgorithm::SHA1,
false,
12,
KNOWN_SALT.to_vec(),
next_name,
rrset,
),
)
}
fn as_ref(&self) -> (&Name, &NSEC3) {
(&self.0, &self.1)
}
}
fn hash(name: &str) -> Vec<u8> {
let known_name = Name::from_ascii(name).unwrap();
Nsec3HashAlgorithm::SHA1
.hash(KNOWN_SALT, &known_name, 12)
.unwrap()
.as_ref()
.to_vec()
}
#[cfg(test)]
fn hash_with_base32(name: &str) -> String {
use data_encoding::BASE32_DNSSEC;
BASE32_DNSSEC.encode(&hash(name))
}
const KNOWN_SALT: &[u8] = &[0xAAu8, 0xBBu8, 0xCCu8, 0xDDu8];
}