Factor NSEC3 records logic to its own module
This commit is contained in:
parent
5d078ab765
commit
9f8c19cb71
|
@ -1,105 +1,11 @@
|
|||
use std::collections::BTreeMap;
|
||||
use std::net::Ipv4Addr;
|
||||
|
||||
use dns_test::client::{Client, DigSettings, DigStatus};
|
||||
use dns_test::name_server::NameServer;
|
||||
use dns_test::nsec3::NSEC3Records;
|
||||
use dns_test::record::{Record, RecordType, NSEC3};
|
||||
use dns_test::{Network, Result, FQDN};
|
||||
|
||||
/// An NSEC3 RR is said to "match" a name if the owner name of the NSEC3 RR is the same as the
|
||||
/// hashed owner name of that name.
|
||||
fn find_match<'a>(name_hash: &str, records: &'a BTreeMap<String, NSEC3>) -> Option<&'a NSEC3> {
|
||||
records.get(name_hash)
|
||||
}
|
||||
|
||||
/// An NSEC3 RR is said to cover a name if the hash of the name or "next closer" name falls between
|
||||
/// the owner name and the next hashed owner name of the NSEC3. In other words, if it proves the
|
||||
/// nonexistence of the name, either directly or by proving the nonexistence of an ancestor of the
|
||||
/// name.
|
||||
fn find_cover<'a>(name_hash: &str, records: &'a BTreeMap<String, NSEC3>) -> Option<&'a NSEC3> {
|
||||
let (hash, candidate) = records
|
||||
// Find the greater hash that is less or equal than the name's hash.
|
||||
.range(..=name_hash.to_owned())
|
||||
.last()
|
||||
// If no value is less or equal than the name's hash, it means that the name's hash is out
|
||||
// of range and the last record covers it.
|
||||
.or_else(|| records.last_key_value())?;
|
||||
|
||||
// If the found hash is exactly the name's hash, return None as it wouldn't be proving its
|
||||
// nonexistence. Otherwise return the RR with that hash.
|
||||
(hash != name_hash).then_some(candidate)
|
||||
}
|
||||
|
||||
/// This proof consists of (up to) two different NSEC3 RRs:
|
||||
/// - An NSEC3 RR that matches the closest (provable) encloser.
|
||||
/// - An NSEC3 RR that covers the "next closer" name to the closest encloser.
|
||||
fn closest_encloser_proof<'a>(
|
||||
closest_encloser_hash: &str,
|
||||
next_closer_name_hash: &str,
|
||||
records: &'a BTreeMap<String, NSEC3>,
|
||||
) -> Option<(&'a NSEC3, &'a NSEC3)> {
|
||||
Some((
|
||||
find_match(closest_encloser_hash, records)?,
|
||||
find_cover(next_closer_name_hash, records)?,
|
||||
))
|
||||
}
|
||||
|
||||
fn query_nameserver(
|
||||
records: impl IntoIterator<Item = Record>,
|
||||
qname: &FQDN,
|
||||
qtype: RecordType,
|
||||
) -> Result<(BTreeMap<String, NSEC3>, DigStatus, Vec<NSEC3>)> {
|
||||
let network = Network::new()?;
|
||||
let mut ns = NameServer::new(&dns_test::SUBJECT, FQDN::ROOT, &network)?;
|
||||
|
||||
for record in records {
|
||||
ns.add(record);
|
||||
}
|
||||
|
||||
let ns = ns.sign()?;
|
||||
// Extract the NSEC3 RRs from the signed zonefile and sort them by the hash embedded in the
|
||||
// last label of each record's owner.
|
||||
let nsec3_rrs = ns
|
||||
.signed_zone_file()
|
||||
.records
|
||||
.iter()
|
||||
.cloned()
|
||||
.filter_map(|rr| {
|
||||
let mut nsec3_rr = rr.try_into_nsec3().ok()?;
|
||||
nsec3_rr.next_hashed_owner_name = nsec3_rr.next_hashed_owner_name.to_uppercase();
|
||||
Some((nsec3_rr.fqdn.last_label().to_uppercase(), nsec3_rr))
|
||||
})
|
||||
.collect::<BTreeMap<_, _>>();
|
||||
|
||||
let ns = ns.start()?;
|
||||
|
||||
let client = Client::new(&network)?;
|
||||
let output = client.dig(
|
||||
*DigSettings::default().dnssec().authentic_data(),
|
||||
ns.ipv4_addr(),
|
||||
qtype,
|
||||
qname,
|
||||
)?;
|
||||
|
||||
let nsec3_rrs_response = output
|
||||
.authority
|
||||
.into_iter()
|
||||
.filter_map(|rr| rr.try_into_nsec3().ok())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
Ok((nsec3_rrs, output.status, nsec3_rrs_response))
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
fn find_records<'a>(
|
||||
records: &[NSEC3],
|
||||
records_and_err_msgs: impl IntoIterator<Item = (&'a NSEC3, &'a str)>,
|
||||
) {
|
||||
for (record, err_msg) in records_and_err_msgs {
|
||||
records.iter().find(|&rr| rr == record).expect(err_msg);
|
||||
}
|
||||
}
|
||||
|
||||
const ALICE_FQDN: &str = "alice.com.";
|
||||
const CHARLIE_FQDN: &str = "charlie.alice.com.";
|
||||
const WILDCARD_FQDN: &str = "*.alice.com.";
|
||||
|
@ -120,9 +26,7 @@ fn name_error_response() -> Result<()> {
|
|||
let qname = FQDN(CHARLIE_FQDN)?;
|
||||
|
||||
let (nsec3_rrs, status, nsec3_rrs_response) = query_nameserver(
|
||||
[
|
||||
Record::a(alice_fqdn, Ipv4Addr::new(1, 2, 3, 4)),
|
||||
],
|
||||
[Record::a(alice_fqdn, Ipv4Addr::new(1, 2, 3, 4))],
|
||||
&qname,
|
||||
RecordType::A,
|
||||
)?;
|
||||
|
@ -139,8 +43,8 @@ fn name_error_response() -> Result<()> {
|
|||
// this scenario, the closest encloser is `alice.com.` which means that the next closer name is `charlie.alice.com.`
|
||||
|
||||
// If this panics, it probably means that the precomputed hashes must be recomputed.
|
||||
let (closest_encloser_rr, next_closer_name_rr) =
|
||||
closest_encloser_proof(ALICE_HASH, CHARLIE_HASH, &nsec3_rrs)
|
||||
let (closest_encloser_rr, next_closer_name_rr) = nsec3_rrs
|
||||
.closest_encloser_proof(ALICE_HASH, CHARLIE_HASH)
|
||||
.expect("Cannot find a closest encloser proof in the zonefile");
|
||||
|
||||
// Wildcard at the closet encloser RR: Must cover the wildcard at the closest encloser of
|
||||
|
@ -152,8 +56,9 @@ fn name_error_response() -> Result<()> {
|
|||
// This NSEC3 RR must cover the hash of the wildcard at the closests encloser.
|
||||
|
||||
// if this panics, it probably means that the precomputed hashes must be recomputed.
|
||||
let wildcard_rr =
|
||||
find_cover(WILDCARD_HASH, &nsec3_rrs).expect("No RR in the zonefile covers the wildcard");
|
||||
let wildcard_rr = nsec3_rrs
|
||||
.find_cover(WILDCARD_HASH)
|
||||
.expect("No RR in the zonefile covers the wildcard");
|
||||
|
||||
// Now we check that the response has the three NSEC3 RRs.
|
||||
find_records(
|
||||
|
@ -192,7 +97,9 @@ fn no_data_response_not_ds() -> Result<()> {
|
|||
// The server MUST include the NSEC3 RR that matches QNAME.
|
||||
|
||||
// if this panics, it probably means that the precomputed hashes must be recomputed.
|
||||
let qname_rr = find_match(ALICE_HASH, &nsec3_rrs).expect("No RR in the zonefile matches QNAME");
|
||||
let qname_rr = nsec3_rrs
|
||||
.find_match(ALICE_HASH)
|
||||
.expect("No RR in the zonefile matches QNAME");
|
||||
|
||||
find_records(
|
||||
&nsec3_rrs_response,
|
||||
|
@ -220,7 +127,9 @@ fn no_data_response_ds_match() -> Result<()> {
|
|||
// If there is an NSEC3 RR that matches QNAME, the server MUST return it in the response.
|
||||
|
||||
// if this panics, it probably means that the precomputed hashes must be recomputed.
|
||||
let qname_rr = find_match(ALICE_HASH, &nsec3_rrs).expect("No RR in the zonefile matches QNAME");
|
||||
let qname_rr = nsec3_rrs
|
||||
.find_match(ALICE_HASH)
|
||||
.expect("No RR in the zonefile matches QNAME");
|
||||
|
||||
find_records(
|
||||
&nsec3_rrs_response,
|
||||
|
@ -258,8 +167,8 @@ fn no_data_response_ds_no_match() -> Result<()> {
|
|||
// this scenario, the closest encloser is `alice.com.` which means that the next closer name is `charlie.alice.com.`
|
||||
|
||||
// If this panics, it probably means that the precomputed hashes must be recomputed.
|
||||
let (closest_encloser_rr, next_closer_name_rr) =
|
||||
closest_encloser_proof(ALICE_HASH, CHARLIE_HASH, &nsec3_rrs)
|
||||
let (closest_encloser_rr, next_closer_name_rr) = nsec3_rrs
|
||||
.closest_encloser_proof(ALICE_HASH, CHARLIE_HASH)
|
||||
.expect("Cannot find a closest encloser proof in the zonefile");
|
||||
|
||||
find_records(
|
||||
|
@ -307,15 +216,16 @@ fn wildcard_no_data_response() -> Result<()> {
|
|||
// this scenario, the closest encloser is `alice.com.` which means that the next closer name is `charlie.alice.com.`
|
||||
|
||||
// If this panics, it probably means that the precomputed hashes must be recomputed.
|
||||
let (closest_encloser_rr, next_closer_name_rr) =
|
||||
closest_encloser_proof(ALICE_HASH, CHARLIE_HASH, &nsec3_rrs)
|
||||
let (closest_encloser_rr, next_closer_name_rr) = nsec3_rrs
|
||||
.closest_encloser_proof(ALICE_HASH, CHARLIE_HASH)
|
||||
.expect("Cannot find a closest encloser proof in the zonefile");
|
||||
|
||||
// Wildcard RR: This NSEC3 RR must match `*.alice.com`.
|
||||
|
||||
// If this panics, it probably means that the precomputed hashes must be recomputed.
|
||||
let wildcard_rr =
|
||||
find_match(WILDCARD_HASH, &nsec3_rrs).expect("No RR in the zonefile matches the wildcard");
|
||||
let wildcard_rr = nsec3_rrs
|
||||
.find_match(WILDCARD_HASH)
|
||||
.expect("No RR in the zonefile matches the wildcard");
|
||||
|
||||
find_records(
|
||||
&nsec3_rrs_response,
|
||||
|
@ -358,7 +268,8 @@ fn wildcard_answer_response() -> Result<()> {
|
|||
// this scenario, the closest encloser is `alice.com.` which means that the next closer name is `charlie.alice.com.`
|
||||
|
||||
// If this panics, it probably means that the precomputed hashes must be recomputed.
|
||||
let next_closer_name_rr = find_cover(CHARLIE_HASH, &nsec3_rrs)
|
||||
let next_closer_name_rr = nsec3_rrs
|
||||
.find_cover(CHARLIE_HASH)
|
||||
.expect("No RR in the zonefile covers the next closer name");
|
||||
|
||||
find_records(
|
||||
|
@ -371,3 +282,48 @@ fn wildcard_answer_response() -> Result<()> {
|
|||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn query_nameserver(
|
||||
records: impl IntoIterator<Item = Record>,
|
||||
qname: &FQDN,
|
||||
qtype: RecordType,
|
||||
) -> Result<(NSEC3Records, DigStatus, Vec<NSEC3>)> {
|
||||
let network = Network::new()?;
|
||||
let mut ns = NameServer::new(&dns_test::SUBJECT, FQDN::ROOT, &network)?;
|
||||
|
||||
for record in records {
|
||||
ns.add(record);
|
||||
}
|
||||
|
||||
let ns = ns.sign()?;
|
||||
|
||||
let nsec3_rrs = NSEC3Records::new(ns.signed_zone_file());
|
||||
|
||||
let ns = ns.start()?;
|
||||
|
||||
let client = Client::new(&network)?;
|
||||
let output = client.dig(
|
||||
*DigSettings::default().dnssec().authentic_data(),
|
||||
ns.ipv4_addr(),
|
||||
qtype,
|
||||
qname,
|
||||
)?;
|
||||
|
||||
let nsec3_rrs_response = output
|
||||
.authority
|
||||
.into_iter()
|
||||
.filter_map(|rr| rr.try_into_nsec3().ok())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
Ok((nsec3_rrs, output.status, nsec3_rrs_response))
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
fn find_records<'a>(
|
||||
records: &[NSEC3],
|
||||
records_and_err_msgs: impl IntoIterator<Item = (&'a NSEC3, &'a str)>,
|
||||
) {
|
||||
for (record, err_msg) in records_and_err_msgs {
|
||||
records.iter().find(|&rr| rr == record).expect(err_msg);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,6 +15,7 @@ mod container;
|
|||
mod fqdn;
|
||||
mod implementation;
|
||||
pub mod name_server;
|
||||
pub mod nsec3;
|
||||
pub mod record;
|
||||
mod resolver;
|
||||
mod trust_anchor;
|
||||
|
|
66
conformance/packages/dns-test/src/nsec3.rs
Normal file
66
conformance/packages/dns-test/src/nsec3.rs
Normal file
|
@ -0,0 +1,66 @@
|
|||
use std::collections::BTreeMap;
|
||||
|
||||
use crate::{record::NSEC3, zone_file::ZoneFile};
|
||||
|
||||
pub struct NSEC3Records {
|
||||
records: BTreeMap<String, NSEC3>,
|
||||
}
|
||||
|
||||
impl NSEC3Records {
|
||||
/// Extract the NSEC3 RRs from the signed zonefile and sort them by the hash embedded in the
|
||||
/// last label of each record's owner.
|
||||
pub fn new(signed_zf: &ZoneFile) -> Self {
|
||||
Self {
|
||||
records: signed_zf
|
||||
.records
|
||||
.iter()
|
||||
.cloned()
|
||||
.filter_map(|rr| {
|
||||
let mut nsec3_rr = rr.try_into_nsec3().ok()?;
|
||||
nsec3_rr.next_hashed_owner_name =
|
||||
nsec3_rr.next_hashed_owner_name.to_uppercase();
|
||||
Some((nsec3_rr.fqdn.last_label().to_uppercase(), nsec3_rr))
|
||||
})
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
|
||||
/// An NSEC3 RR is said to "match" a name if the owner name of the NSEC3 RR is the same as the
|
||||
/// hashed owner name of that name.
|
||||
pub fn find_match<'a>(&'a self, name_hash: &str) -> Option<&'a NSEC3> {
|
||||
self.records.get(name_hash)
|
||||
}
|
||||
|
||||
/// An NSEC3 RR is said to cover a name if the hash of the name or "next closer" name falls between
|
||||
/// the owner name and the next hashed owner name of the NSEC3. In other words, if it proves the
|
||||
/// nonexistence of the name, either directly or by proving the nonexistence of an ancestor of the
|
||||
/// name.
|
||||
pub fn find_cover<'a>(&'a self, name_hash: &str) -> Option<&'a NSEC3> {
|
||||
let (hash, candidate) = self
|
||||
.records
|
||||
// Find the greater hash that is less or equal than the name's hash.
|
||||
.range(..=name_hash.to_owned())
|
||||
.last()
|
||||
// If no value is less or equal than the name's hash, it means that the name's hash is out
|
||||
// of range and the last record covers it.
|
||||
.or_else(|| self.records.last_key_value())?;
|
||||
|
||||
// If the found hash is exactly the name's hash, return None as it wouldn't be proving its
|
||||
// nonexistence. Otherwise return the RR with that hash.
|
||||
(hash != name_hash).then_some(candidate)
|
||||
}
|
||||
|
||||
/// This proof consists of (up to) two different NSEC3 RRs:
|
||||
/// - An NSEC3 RR that matches the closest (provable) encloser.
|
||||
/// - An NSEC3 RR that covers the "next closer" name to the closest encloser.
|
||||
pub fn closest_encloser_proof<'a>(
|
||||
&'a self,
|
||||
closest_encloser_hash: &str,
|
||||
next_closer_name_hash: &str,
|
||||
) -> Option<(&'a NSEC3, &'a NSEC3)> {
|
||||
Some((
|
||||
self.find_match(closest_encloser_hash)?,
|
||||
self.find_cover(next_closer_name_hash)?,
|
||||
))
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user