Skip to content

Commit

Permalink
Merge branch 'main' into bin-imprv
Browse files Browse the repository at this point in the history
  • Loading branch information
sabify committed Jun 19, 2024
2 parents d6c0159 + 02a8668 commit 0259503
Show file tree
Hide file tree
Showing 8 changed files with 424 additions and 9 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ All notes should be prepended with the location of the change, e.g. `(proto)` or
- (all) Update mio to 0.8.11 to fix RUSTSEC-2024-0019 #2166 by marcus0x62
- (proto) Fix formatting issue in crates/proto/src/op/message.rs #2165 by marcus0x62
- (proto) fix internal representation of OPT #2151 by esensar
- (proto) ECH service parameter key corrected from "echconfig" to "ech" #2183 by cpu
- (proto) SVCB/HTTPS record parsing fixes (quoted values, arbitrary numeric keys, lists containing delim) #2183 by cpu

### Changed

Expand Down Expand Up @@ -45,6 +47,8 @@ All notes should be prepended with the location of the change, e.g. `(proto)` or
- (all) get(0) to first() and zerocopy package updates to fix clippy and cargo audit errors #2121 by marcus0x62
- (resolver) Add getters for resolver config and options #2093 by hoxxep
- (client) updated h2_client_connection and web-pki-roots config #2088 by marcbrevoort-cyberhive
- (proto) EchConfig renamed to EchConfigList to match content #2183 by cpu
- (proto) EchConfigList updated to wrap TLS presentation language encoding of content #2183 by cpu

### Added

Expand Down
1 change: 1 addition & 0 deletions conformance/packages/conformance-tests/src/name_server.rs
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
mod rfc4035;
mod rfc5155;
mod scenarios;
329 changes: 329 additions & 0 deletions conformance/packages/conformance-tests/src/name_server/rfc5155.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,329 @@
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};

const TLD_FQDN: &str = "alice.com.";
const NON_EXISTENT_FQDN: &str = "charlie.alice.com.";
const WILDCARD_FQDN: &str = "*.alice.com.";

// These hashes are computed with 1 iteration of SHA-1 without salt and must be recomputed if
// those parameters were to change.
const TLD_HASH: &str = "LLKH4L6I60VHAPP6VRM3DFR9RI8AK9I0"; /* h(alice.com.) */
const NON_EXISTENT_HASH: &str = "99P1CCPQ2N64LIRMT2838O4HK0QFA51B"; /* h(charlie.alice.com.) */
const WILDCARD_HASH: &str = "19GBV5V1BO0P51H34JQDH1C8CIAA5RAQ"; /* h(*.alice.com.) */

// This test checks that name servers produce a name error response compliant with section 7.2.2.
// of RFC5155.
#[test]
#[ignore]
fn name_error_response() -> Result<()> {
let alice_fqdn = FQDN(TLD_FQDN)?;
// The queried name
let qname = FQDN(NON_EXISTENT_FQDN)?;

let (nsec3_rrs, status, nsec3_rrs_response) = query_nameserver(
[Record::a(alice_fqdn, Ipv4Addr::new(1, 2, 3, 4))],
&qname,
RecordType::A,
)?;

assert!(status.is_nxdomain());

// Closest Encloser Proof
//
// The closest encloser of a name is its longest existing ancestor. In this scenario, the
// closest encloser of `charlie.alice.com.` is `alice.com.` as this is the longest ancestor with an
// existing RR.
//
// The next closer name of a name is the name one label longer than its closest encloser. In
// 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) = nsec3_rrs
.closest_encloser_proof(TLD_HASH, NON_EXISTENT_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
// QNAME.
//
// In this scenario, the closest encloser is `alice.com.`, so the wildcard at the closer
// encloser is `*.alice.com.`.
//
// 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 = 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(
&nsec3_rrs_response,
[
(
closest_encloser_rr,
"No RR in the response matches the closest encloser",
),
(
next_closer_name_rr,
"No RR in the response covers the next closer name",
),
(wildcard_rr, "No RR in the response covers the wildcard"),
],
);

Ok(())
}

// This test checks that name servers produce a no data response compliant with section 7.2.3.
// of RFC5155 when the query type is not DS.
#[test]
#[ignore]
fn no_data_response_not_ds() -> Result<()> {
let alice_fqdn = FQDN(TLD_FQDN)?;
// The queried name
let qname = alice_fqdn.clone();

let (nsec3_rrs, _status, nsec3_rrs_response) = query_nameserver(
[Record::a(alice_fqdn, Ipv4Addr::new(1, 2, 3, 4))],
&qname,
RecordType::MX,
)?;

// 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 = nsec3_rrs
.find_match(TLD_HASH)
.expect("No RR in the zonefile matches QNAME");

find_records(
&nsec3_rrs_response,
[(qname_rr, "No RR in the response matches QNAME")],
);

Ok(())
}

// This test checks that name servers produce a no data response compliant with section 7.2.4.
// of RFC5155 when the query type is DS and there is an NSEC3 RR that matches the queried name.
#[test]
#[ignore]
fn no_data_response_ds_match() -> Result<()> {
let alice_fqdn = FQDN(TLD_FQDN)?;
// The queried name
let qname = alice_fqdn.clone();

let (nsec3_rrs, _status, nsec3_rrs_response) = query_nameserver(
[Record::a(alice_fqdn, Ipv4Addr::new(1, 2, 3, 4))],
&qname,
RecordType::DS,
)?;

// 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 = nsec3_rrs
.find_match(TLD_HASH)
.expect("No RR in the zonefile matches QNAME");

find_records(
&nsec3_rrs_response,
[(qname_rr, "No RR in the response matches QNAME")],
);

Ok(())
}

// This test checks that name servers produce a no data response compliant with section 7.2.4.
// of RFC5155 when the query type is DS and no NSEC3 RR matches the queried name.
#[test]
#[ignore]
fn no_data_response_ds_no_match() -> Result<()> {
let alice_fqdn = FQDN(TLD_FQDN)?;
// The queried name
let qname = FQDN(NON_EXISTENT_FQDN)?;

let (nsec3_rrs, _status, nsec3_rrs_response) = query_nameserver(
[Record::a(alice_fqdn, Ipv4Addr::new(1, 2, 3, 4))],
&qname,
RecordType::DS,
)?;

// If no NSEC3 RR matches QNAME, the server MUST return a closest provable encloser proof for
// QNAME.

// Closest Encloser Proof
//
// The closest encloser of a name is its longest existing ancestor. In this scenario, the
// closest encloser of `charlie.alice.com.` is `alice.com.` as this is the longest ancestor with an
// existing RR.
//
// The next closer name of a name is the name one label longer than its closest encloser. In
// 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) = nsec3_rrs
.closest_encloser_proof(TLD_HASH, NON_EXISTENT_HASH)
.expect("Cannot find a closest encloser proof in the zonefile");

find_records(
&nsec3_rrs_response,
[
(
closest_encloser_rr,
"No RR in the response matches the closest encloser",
),
(
next_closer_name_rr,
"No RR in the response covers the next closer name",
),
],
);

Ok(())
}

// This test checks that name servers produce a wildcard no data response compliant with section 7.2.5.
#[test]
#[ignore]
fn wildcard_no_data_response() -> Result<()> {
let wildcard_fqdn = FQDN(WILDCARD_FQDN)?;
// The queried name
let qname = FQDN(NON_EXISTENT_FQDN)?;

let (nsec3_rrs, _status, nsec3_rrs_response) = query_nameserver(
[Record::a(wildcard_fqdn, Ipv4Addr::new(1, 2, 3, 4))],
&qname,
RecordType::MX,
)?;

// If there is a wildcard match for QNAME, but QTYPE is not present at that name, the response MUST
// include a closest encloser proof for QNAME and MUST include the NSEC3 RR that matches the
// wildcard.

// Closest Encloser Proof
//
// The closest encloser of a name is its longest existing ancestor. In this scenario, the
// closest encloser of `charlie.alice.com.` is `alice.com.` as this is the longest ancestor with an
// existing RR.
//
// The next closer name of a name is the name one label longer than its closest encloser. In
// 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) = nsec3_rrs
.closest_encloser_proof(TLD_HASH, NON_EXISTENT_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 = nsec3_rrs
.find_match(WILDCARD_HASH)
.expect("No RR in the zonefile matches the wildcard");

find_records(
&nsec3_rrs_response,
[
(
closest_encloser_rr,
"No RR in the response matches the closest encloser",
),
(
next_closer_name_rr,
"No RR in the response covers the next closer name",
),
(wildcard_rr, "No RR in the response covers the wildcard"),
],
);

Ok(())
}

// This test checks that name servers produce a wildcard answer response compliant with section 7.2.6.
#[test]
#[ignore]
fn wildcard_answer_response() -> Result<()> {
let wildcard_fqdn = FQDN(WILDCARD_FQDN)?;
// The queried name
let qname = FQDN(NON_EXISTENT_FQDN)?;

let (nsec3_rrs, _status, nsec3_rrs_response) = query_nameserver(
[Record::a(wildcard_fqdn, Ipv4Addr::new(1, 2, 3, 4))],
&qname,
RecordType::A,
)?;

// If there is a wildcard match for QNAME and QTYPE, then, in addition to the expanded wildcard
// RRSet returned in the answer section of the response, proof that the wildcard match was
// valid must be returned. ... To this end, the NSEC3 RR that covers the "next closer" name of the
// immediate ancestor of the wildcard MUST be returned.

// The next closer name of a name is the name one label longer than its closest encloser. In
// 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 = nsec3_rrs
.find_cover(NON_EXISTENT_HASH)
.expect("No RR in the zonefile covers the next closer name");

find_records(
&nsec3_rrs_response,
[(
next_closer_name_rr,
"No RR in the response covers the next closer name",
)],
);

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);
}
}
5 changes: 5 additions & 0 deletions conformance/packages/dns-test/src/fqdn.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ pub struct FQDN {
#[allow(non_snake_case)]
pub fn FQDN(input: impl Into<Cow<'static, str>>) -> Result<FQDN> {
let input = input.into();

if !input.ends_with('.') {
return Err("FQDN must end with a `.`".into());
}
Expand Down Expand Up @@ -77,6 +78,10 @@ impl FQDN {
.filter(|label| !label.is_empty())
.count()
}

pub fn last_label(&self) -> &str {
self.inner.split_once('.').map(|(label, _)| label).unwrap()
}
}

impl FromStr for FQDN {
Expand Down
1 change: 1 addition & 0 deletions conformance/packages/dns-test/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Loading

0 comments on commit 0259503

Please sign in to comment.