From 166863bcc4a315442f1d49a2d61ecbec8de5d6cd Mon Sep 17 00:00:00 2001 From: Jorge Aparicio Date: Tue, 5 Mar 2024 14:10:20 +0100 Subject: [PATCH 1/7] allow enabling Extended DNS Errors (EDE) --- packages/dns-test/src/implementation.rs | 24 +++++++++++++++++-- packages/dns-test/src/resolver.rs | 10 ++++++++ .../dns-test/src/templates/unbound.conf.jinja | 1 + 3 files changed, 33 insertions(+), 2 deletions(-) diff --git a/packages/dns-test/src/implementation.rs b/packages/dns-test/src/implementation.rs index 256ad247..1ec81e15 100644 --- a/packages/dns-test/src/implementation.rs +++ b/packages/dns-test/src/implementation.rs @@ -8,8 +8,15 @@ use crate::FQDN; #[derive(Clone, Copy)] pub enum Config<'a> { - NameServer { origin: &'a FQDN }, - Resolver { use_dnssec: bool, netmask: &'a str }, + NameServer { + origin: &'a FQDN, + }, + Resolver { + use_dnssec: bool, + netmask: &'a str, + /// Extended DNS error (RFC8914) + ede: bool, + }, } impl Config<'_> { @@ -42,6 +49,14 @@ pub enum Implementation { } impl Implementation { + pub fn supports_ede(&self) -> bool { + match self { + Implementation::Bind => false, + Implementation::Hickory(_) => true, + Implementation::Unbound => true, + } + } + #[must_use] pub fn is_bind(&self) -> bool { matches!(self, Self::Bind) @@ -52,8 +67,11 @@ impl Implementation { Config::Resolver { use_dnssec, netmask, + ede, } => match self { Self::Bind => { + assert!(!ede, "the BIND resolver does not support EDE (RFC8914)"); + minijinja::render!( include_str!("templates/named.resolver.conf.jinja"), use_dnssec => use_dnssec, @@ -62,6 +80,7 @@ impl Implementation { } Self::Hickory(_) => { + // TODO enable EDE in Hickory when supported minijinja::render!( include_str!("templates/hickory.resolver.toml.jinja"), use_dnssec => use_dnssec, @@ -73,6 +92,7 @@ impl Implementation { include_str!("templates/unbound.conf.jinja"), use_dnssec => use_dnssec, netmask => netmask, + ede => ede, ) } }, diff --git a/packages/dns-test/src/resolver.rs b/packages/dns-test/src/resolver.rs index 93d17f3a..4a5ddf43 100644 --- a/packages/dns-test/src/resolver.rs +++ b/packages/dns-test/src/resolver.rs @@ -19,6 +19,7 @@ impl Resolver { #[allow(clippy::new_ret_no_self)] pub fn new(network: &Network, root: Root) -> ResolverSettings { ResolverSettings { + ede: false, network: network.clone(), roots: vec![root], trust_anchor: TrustAnchor::empty(), @@ -60,6 +61,8 @@ kill -TERM $(cat {pidfile})" } pub struct ResolverSettings { + /// Extended DNS Errors (RFC8914) + ede: bool, network: Network, roots: Vec, trust_anchor: TrustAnchor, @@ -84,6 +87,7 @@ impl ResolverSettings { let config = Config::Resolver { use_dnssec, netmask: self.network.netmask(), + ede: self.ede, }; container.cp( implementation.conf_file_path(config.role()), @@ -115,6 +119,12 @@ impl ResolverSettings { }) } + /// Enables the Extended DNS Errors (RFC8914) feature + pub fn extended_dns_errors(&mut self) -> &mut Self { + self.ede = true; + self + } + /// Adds a root hint pub fn root(&mut self, root: Root) -> &mut Self { self.roots.push(root); diff --git a/packages/dns-test/src/templates/unbound.conf.jinja b/packages/dns-test/src/templates/unbound.conf.jinja index 212078aa..13eeb758 100644 --- a/packages/dns-test/src/templates/unbound.conf.jinja +++ b/packages/dns-test/src/templates/unbound.conf.jinja @@ -5,6 +5,7 @@ server: access-control: {{ netmask }} allow root-hints: /etc/root.hints pidfile: /tmp/unbound.pid + ede: {% if ede %} yes {% else %} no {% endif %} {% if use_dnssec %} trust-anchor-file: /etc/trusted-key.key {% endif %} From dc197761075a50ad6953ec11473874e7df310ea6 Mon Sep 17 00:00:00 2001 From: Jorge Aparicio Date: Tue, 5 Mar 2024 18:47:50 +0100 Subject: [PATCH 2/7] parse EDE info from dig's output --- packages/dns-test/src/client.rs | 69 ++++++++++++++++++++++++++++++++- 1 file changed, 67 insertions(+), 2 deletions(-) diff --git a/packages/dns-test/src/client.rs b/packages/dns-test/src/client.rs index 0f9e4aed..3dd2a652 100644 --- a/packages/dns-test/src/client.rs +++ b/packages/dns-test/src/client.rs @@ -141,6 +141,7 @@ impl DigSettings { #[derive(Debug)] pub struct DigOutput { + pub ede: Option, pub flags: DigFlags, pub status: DigStatus, pub answer: Vec, @@ -154,6 +155,7 @@ impl FromStr for DigOutput { fn from_str(input: &str) -> Result { const FLAGS_PREFIX: &str = ";; flags: "; const STATUS_PREFIX: &str = ";; ->>HEADER<<- opcode: QUERY, status: "; + const EDE_PREFIX: &str = "; EDE: "; const ANSWER_HEADER: &str = ";; ANSWER SECTION:"; const AUTHORITY_HEADER: &str = ";; AUTHORITY SECTION:"; @@ -173,6 +175,7 @@ impl FromStr for DigOutput { let mut status = None; let mut answer = None; let mut authority = None; + let mut ede = None; let mut lines = input.lines(); while let Some(line) = lines.next() { @@ -196,6 +199,17 @@ impl FromStr for DigOutput { } status = Some(status_text.parse()?); + } else if let Some(unprefixed) = line.strip_prefix(EDE_PREFIX) { + let code = unprefixed + .split_once(' ') + .map(|(code, _rest)| code) + .unwrap_or(unprefixed); + + if ede.is_some() { + return Err(more_than_once(EDE_PREFIX).into()); + } + + ede = Some(code.parse()?); } else if line.starts_with(ANSWER_HEADER) { if answer.is_some() { return Err(more_than_once(ANSWER_HEADER).into()); @@ -230,14 +244,37 @@ impl FromStr for DigOutput { } Ok(Self { - flags: flags.ok_or_else(|| not_found(FLAGS_PREFIX))?, - status: status.ok_or_else(|| not_found(STATUS_PREFIX))?, answer: answer.unwrap_or_default(), authority: authority.unwrap_or_default(), + ede, + flags: flags.ok_or_else(|| not_found(FLAGS_PREFIX))?, + status: status.ok_or_else(|| not_found(STATUS_PREFIX))?, }) } } +#[derive(Debug, PartialEq)] +pub enum ExtendedDnsError { + DnssecBogus, + DnskeyMissing, +} + +impl FromStr for ExtendedDnsError { + type Err = Error; + + fn from_str(input: &str) -> std::prelude::v1::Result { + let code: u16 = input.parse()?; + + let code = match code { + 6 => Self::DnssecBogus, + 9 => Self::DnskeyMissing, + _ => todo!("EDE {code} has not yet been implemented"), + }; + + Ok(code) + } +} + #[derive(Debug, Default, PartialEq)] pub struct DigFlags { pub authenticated_data: bool, @@ -398,4 +435,32 @@ mod tests { Ok(()) } + + #[test] + fn ede() -> Result<()> { + let input = "; <<>> DiG 9.18.24-1-Debian <<>> +recurse +nodnssec +adflag +nocdflag @192.168.176.5 A example.nameservers.com. +; (1 server found) +;; global options: +cmd +;; Got answer: +;; ->>HEADER<<- opcode: QUERY, status: SERVFAIL, id: 49801 +;; flags: qr rd ra; QUERY: 1, ANSWER: 0, AUTHORITY: 0, ADDITIONAL: 1 + +;; OPT PSEUDOSECTION: +; EDNS: version: 0, flags:; udp: 1232 +; EDE: 9 (DNSKEY Missing) +;; QUESTION SECTION: +;example.nameservers.com. IN A + +;; Query time: 26 msec +;; SERVER: 192.168.176.5#53(192.168.176.5) (UDP) +;; WHEN: Tue Mar 05 17:45:29 UTC 2024 +;; MSG SIZE rcvd: 58 +"; + + let output: DigOutput = input.parse()?; + + assert_eq!(Some(ExtendedDnsError::DnskeyMissing), output.ede); + + Ok(()) + } } From 31048f5cd036708bbfdd9f4f80c38e4d8408ce16 Mon Sep 17 00:00:00 2001 From: Jorge Aparicio Date: Tue, 5 Mar 2024 18:48:32 +0100 Subject: [PATCH 3/7] extend DNSKEY API --- packages/dns-test/src/record.rs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/packages/dns-test/src/record.rs b/packages/dns-test/src/record.rs index 0abc7df8..aaaf446a 100644 --- a/packages/dns-test/src/record.rs +++ b/packages/dns-test/src/record.rs @@ -239,6 +239,8 @@ pub struct DNSKEY { } impl DNSKEY { + const KSK_BIT: u16 = 1; + /// formats the `DNSKEY` in the format `delv` expects pub(super) fn delv(&self) -> String { let Self { @@ -252,6 +254,19 @@ impl DNSKEY { format!("{zone} static-key {flags} {protocol} {algorithm} \"{public_key}\";\n") } + + pub fn clear_key_signing_key_bit(&mut self) { + self.flags &= !Self::KSK_BIT; + } + + pub fn is_key_signing_key(&self) -> bool { + let mask = Self::KSK_BIT; + self.flags & mask == mask + } + + pub fn is_zone_signing_key(&self) -> bool { + !self.is_key_signing_key() + } } impl FromStr for DNSKEY { From 95f94e2c7b5c9a7c73b6c3540549a2a4d7c4af98 Mon Sep 17 00:00:00 2001 From: Jorge Aparicio Date: Tue, 5 Mar 2024 18:48:48 +0100 Subject: [PATCH 4/7] add first EDE test --- .../src/resolver/dnssec/scenarios.rs | 1 + .../src/resolver/dnssec/scenarios/ede.rs | 101 ++++++++++++++++++ 2 files changed, 102 insertions(+) create mode 100644 packages/conformance-tests/src/resolver/dnssec/scenarios/ede.rs diff --git a/packages/conformance-tests/src/resolver/dnssec/scenarios.rs b/packages/conformance-tests/src/resolver/dnssec/scenarios.rs index 416fff70..6dd90eea 100644 --- a/packages/conformance-tests/src/resolver/dnssec/scenarios.rs +++ b/packages/conformance-tests/src/resolver/dnssec/scenarios.rs @@ -1,2 +1,3 @@ mod bogus; +mod ede; mod secure; diff --git a/packages/conformance-tests/src/resolver/dnssec/scenarios/ede.rs b/packages/conformance-tests/src/resolver/dnssec/scenarios/ede.rs new file mode 100644 index 00000000..95d1dcda --- /dev/null +++ b/packages/conformance-tests/src/resolver/dnssec/scenarios/ede.rs @@ -0,0 +1,101 @@ +use std::net::Ipv4Addr; + +use dns_test::client::{Client, DigSettings, ExtendedDnsError}; +use dns_test::name_server::NameServer; +use dns_test::record::{Record, RecordType}; +use dns_test::zone_file::Root; +use dns_test::{Network, Resolver, Result, FQDN}; + +#[ignore] +#[test] +fn dnskey_missing() -> Result<()> { + let subject = dns_test::subject(); + let supports_ede = subject.supports_ede(); + + let expected_ipv4_addr = Ipv4Addr::new(1, 2, 3, 4); + let needle_fqdn = FQDN("example.nameservers.com.")?; + + let network = Network::new()?; + let peer = dns_test::peer(); + let mut root_ns = NameServer::new(&peer, FQDN::ROOT, &network)?; + let mut com_ns = NameServer::new(&peer, FQDN::COM, &network)?; + + let mut nameservers_ns = NameServer::new(&peer, FQDN("nameservers.com.")?, &network)?; + nameservers_ns + .add(Record::a(root_ns.fqdn().clone(), root_ns.ipv4_addr())) + .add(Record::a(com_ns.fqdn().clone(), com_ns.ipv4_addr())) + .add(Record::a(needle_fqdn.clone(), expected_ipv4_addr)); + let mut nameservers_ns = nameservers_ns.sign()?; + + // remove the ZSK DNSKEY record + let records = &mut nameservers_ns.signed_zone_file_mut().records; + let mut remove_count = 0; + *records = records + .drain(..) + .filter(|record| { + let remove = if let Record::DNSKEY(dnskey) = record { + dnskey.is_zone_signing_key() + } else { + false + }; + + if remove { + remove_count += 1; + } + + !remove + }) + .collect(); + assert_eq!(1, remove_count); + + let nameservers_ds = nameservers_ns.ds().clone(); + let nameservers_ns = nameservers_ns.start()?; + + com_ns + .referral( + nameservers_ns.zone().clone(), + nameservers_ns.fqdn().clone(), + nameservers_ns.ipv4_addr(), + ) + .add(nameservers_ds); + let com_ns = com_ns.sign()?; + let com_ds = com_ns.ds().clone(); + let com_ns = com_ns.start()?; + + root_ns + .referral(FQDN::COM, com_ns.fqdn().clone(), com_ns.ipv4_addr()) + .add(com_ds); + let root_ns = root_ns.sign()?; + let root_ksk = root_ns.key_signing_key().clone(); + let root_zsk = root_ns.zone_signing_key().clone(); + + let root_ns = root_ns.start()?; + + let mut resolver = Resolver::new( + &network, + Root::new(root_ns.fqdn().clone(), root_ns.ipv4_addr()), + ); + + if supports_ede { + resolver.extended_dns_errors(); + } + + let resolver = resolver + .trust_anchor_key(root_ksk) + .trust_anchor_key(root_zsk) + .start(&subject)?; + let resolver_addr = resolver.ipv4_addr(); + + let client = Client::new(&network)?; + + let settings = *DigSettings::default().recurse().authentic_data(); + let output = client.dig(settings, resolver_addr, RecordType::A, &needle_fqdn)?; + + assert!(output.status.is_servfail()); + + if supports_ede { + assert_eq!(Some(ExtendedDnsError::DnskeyMissing), output.ede); + } + + Ok(()) +} From 7bb6b9439cb4cb2d6939c8783ca320206e1ce398 Mon Sep 17 00:00:00 2001 From: Jorge Aparicio Date: Wed, 6 Mar 2024 20:31:14 +0100 Subject: [PATCH 5/7] add name_server::Graph --- packages/dns-test/src/container.rs | 8 +- packages/dns-test/src/fqdn.rs | 60 ++++++++++++ packages/dns-test/src/name_server.rs | 131 ++++++++++++++++++++++++++- 3 files changed, 195 insertions(+), 4 deletions(-) diff --git a/packages/dns-test/src/container.rs b/packages/dns-test/src/container.rs index fb44c4ad..e19bd50a 100644 --- a/packages/dns-test/src/container.rs +++ b/packages/dns-test/src/container.rs @@ -157,7 +157,7 @@ impl Container { id, name, ipv4_addr, - _network: network.clone(), + network: network.clone(), }; Ok(Self { inner: Arc::new(inner), @@ -250,6 +250,10 @@ impl Container { pub fn id(&self) -> &str { &self.inner.id } + + pub(crate) fn network(&self) -> &Network { + &self.inner.network + } } fn verbose_docker_build() -> bool { @@ -282,7 +286,7 @@ struct Inner { id: String, // TODO probably also want the IPv6 address ipv4_addr: Ipv4Addr, - _network: Network, + network: Network, } /// NOTE unlike `std::process::Child`, the drop implementation of this type will `kill` the diff --git a/packages/dns-test/src/fqdn.rs b/packages/dns-test/src/fqdn.rs index 5c3d8678..b0ae757c 100644 --- a/packages/dns-test/src/fqdn.rs +++ b/packages/dns-test/src/fqdn.rs @@ -33,6 +33,10 @@ impl FQDN { inner: Cow::Borrowed("com."), }; + pub const NAMESERVERS: FQDN = FQDN { + inner: Cow::Borrowed("nameservers.com."), + }; + pub fn is_root(&self) -> bool { self.inner == "." } @@ -51,6 +55,28 @@ impl FQDN { inner: Cow::Owned(owned), } } + + pub fn parent(&self) -> Option { + let (fragment, parent) = self.inner.split_once('.').unwrap(); + + if fragment.is_empty() { + None + } else { + let parent = if parent.is_empty() { + FQDN::ROOT + } else { + FQDN(parent.to_string()).unwrap() + }; + Some(parent) + } + } + + pub fn num_labels(&self) -> usize { + self.inner + .split('.') + .filter(|label| !label.is_empty()) + .count() + } } impl FromStr for FQDN { @@ -72,3 +98,37 @@ impl fmt::Display for FQDN { f.write_str(&self.inner) } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn parent() -> Result<()> { + let mut fqdn = FQDN("example.nameservers.com.")?; + assert_eq!(3, fqdn.num_labels()); + + let parent = fqdn.parent(); + assert_eq!( + Some("nameservers.com."), + parent.as_ref().map(|fqdn| fqdn.as_str()) + ); + fqdn = parent.unwrap(); + assert_eq!(2, fqdn.num_labels()); + + let parent = fqdn.parent(); + assert_eq!(Some(FQDN::COM), parent); + fqdn = parent.unwrap(); + assert_eq!(1, fqdn.num_labels()); + + let parent = fqdn.parent(); + assert_eq!(Some(FQDN::ROOT), parent); + fqdn = parent.unwrap(); + assert_eq!(0, fqdn.num_labels()); + + let parent = fqdn.parent(); + assert!(parent.is_none()); + + Ok(()) + } +} diff --git a/packages/dns-test/src/name_server.rs b/packages/dns-test/src/name_server.rs index eea06b21..3bef3fc3 100644 --- a/packages/dns-test/src/name_server.rs +++ b/packages/dns-test/src/name_server.rs @@ -5,8 +5,135 @@ use crate::container::{Child, Container, Network}; use crate::implementation::{Config, Role}; use crate::record::{self, Record, SoaSettings, DS, SOA}; use crate::tshark::Tshark; -use crate::zone_file::{self, ZoneFile}; -use crate::{Implementation, Result, DEFAULT_TTL, FQDN}; +use crate::zone_file::{self, Root, ZoneFile}; +use crate::{Implementation, Result, TrustAnchor, DEFAULT_TTL, FQDN}; + +pub struct Graph { + pub nameservers: Vec>, + pub root: Root, + pub trust_anchor: Option, +} + +/// Whether to sign the zone files +pub enum Sign<'a> { + No, + Yes, + /// Signs the zone files and then modifies the records produced by the signing process + // XXX if captures are needed use `&dyn Fn(..)` instead of a function pointer + AndAmend(&'a dyn Fn(&FQDN, &mut Vec)), +} + +impl Graph { + /// Builds up a minimal DNS graph from `leaf` up to a root name server and returns all the + /// name servers in the graph + /// + /// All new name servers will share the `Implementation` of `leaf`. + /// + /// The returned name servers are sorted from leaf zone to root zone. + /// + /// both `Sign::Yes` and `Sign::AndAmend` will add a DS record with the hash of the child's + /// key to the parent's zone file + /// + /// a non-empty `TrustAnchor` is returned only when `Sign::Yes` or `Sign::AndAmend` is used + pub fn build(leaf: NameServer, sign: Sign) -> Result { + // TODO if `leaf` is not authoritative over `nameservers.com.`, we would need two "lines" to + // root. for example, if `leaf` is authoritative over `example.net.` we would need these two + // lines: + // - `nameservers.com.`, `com.`, `.` to cover the `primaryNNN.nameservers.com.` domains that + // `NameServer` implicitly uses + // - `example.net.`, `net.`, `.` to cover the requested `leaf` name server + assert_eq!(&FQDN::NAMESERVERS, leaf.zone(), "not yet implemented"); + + // first pass: create nameservers for parent zones + let mut zone = leaf.zone().clone(); + let mut nameservers = vec![leaf]; + while let Some(parent) = zone.parent() { + let leaf = &mut nameservers[0]; + let nameserver = NameServer::new( + &leaf.implementation, + parent.clone(), + leaf.container.network(), + )?; + + leaf.add(Record::a(nameserver.fqdn().clone(), nameserver.ipv4_addr())); + nameservers.push(nameserver); + + zone = parent; + } + + // XXX will not hold when `leaf` is not authoritative over `nameservers.com.` + assert_eq!(3, nameservers.len()); + + // second pass: add referrals from parent to child + // `windows_mut` is not a thing in `core::iter` so use indexing as a workaround + for index in 0..nameservers.len() - 1 { + let [child, parent] = &mut nameservers[index..][..2] else { + unreachable!() + }; + + parent.referral( + child.zone().clone(), + child.fqdn().clone(), + child.ipv4_addr(), + ); + } + + let root = nameservers.last().unwrap(); + let root = Root::new(root.fqdn().clone(), root.ipv4_addr()); + + // start name servers + let (nameservers, trust_anchor) = match sign { + Sign::No => ( + nameservers + .into_iter() + .map(|nameserver| nameserver.start()) + .collect::>()?, + None, + ), + + _ => { + let mut trust_anchor = TrustAnchor::empty(); + let maybe_mutate = match sign { + Sign::No => unreachable!(), + Sign::Yes => None, + Sign::AndAmend(f) => Some(f), + }; + + let mut running = vec![]; + let mut child_ds = None; + let len = nameservers.len(); + for (index, mut nameserver) in nameservers.into_iter().enumerate() { + if let Some(ds) = child_ds.take() { + nameserver.add(ds); + } + + let mut nameserver = nameserver.sign()?; + child_ds = Some(nameserver.ds().clone()); + if let Some(mutate) = maybe_mutate { + let zone = nameserver.zone().clone(); + mutate(&zone, &mut nameserver.signed_zone_file_mut().records); + } + + if index == len - 1 { + // the last nameserver covers `.` + trust_anchor.add(nameserver.key_signing_key().clone()); + trust_anchor.add(nameserver.zone_signing_key().clone()); + } + + running.push(nameserver.start()?); + } + + (running, Some(trust_anchor)) + } + }; + + Ok(Graph { + nameservers, + root, + trust_anchor, + }) + } +} pub struct NameServer { container: Container, From 4d31eca533c2b3d90dba7ff813ee51b7ee53aaf2 Mon Sep 17 00:00:00 2001 From: Jorge Aparicio Date: Wed, 6 Mar 2024 20:31:45 +0100 Subject: [PATCH 6/7] use Graph to simplify tests --- .../src/resolver/dns/scenarios.rs | 67 ++++--------- .../src/resolver/dnssec/scenarios/bogus.rs | 84 +++++++---------- .../src/resolver/dnssec/scenarios/ede.rs | 93 +++++++------------ .../src/resolver/dnssec/scenarios/secure.rs | 57 +++--------- 4 files changed, 92 insertions(+), 209 deletions(-) diff --git a/packages/conformance-tests/src/resolver/dns/scenarios.rs b/packages/conformance-tests/src/resolver/dns/scenarios.rs index 121936cd..2481bbd4 100644 --- a/packages/conformance-tests/src/resolver/dns/scenarios.rs +++ b/packages/conformance-tests/src/resolver/dns/scenarios.rs @@ -1,9 +1,8 @@ use std::net::Ipv4Addr; use dns_test::client::{Client, DigSettings}; -use dns_test::name_server::NameServer; +use dns_test::name_server::{Graph, NameServer, Sign}; use dns_test::record::{Record, RecordType}; -use dns_test::zone_file::Root; use dns_test::{Network, Resolver, Result, FQDN}; #[test] @@ -13,37 +12,17 @@ fn can_resolve() -> Result<()> { let network = Network::new()?; let peer = dns_test::peer(); - let mut root_ns = NameServer::new(&peer, FQDN::ROOT, &network)?; - let mut com_ns = NameServer::new(&peer, FQDN::COM, &network)?; - let mut nameservers_ns = NameServer::new(&peer, FQDN("nameservers.com.")?, &network)?; - nameservers_ns - .add(Record::a(root_ns.fqdn().clone(), root_ns.ipv4_addr())) - .add(Record::a(com_ns.fqdn().clone(), com_ns.ipv4_addr())) - .add(Record::a(needle_fqdn.clone(), expected_ipv4_addr)); - let nameservers_ns = nameservers_ns.start()?; + let mut leaf_ns = NameServer::new(&peer, FQDN::NAMESERVERS, &network)?; + leaf_ns.add(Record::a(needle_fqdn.clone(), expected_ipv4_addr)); - eprintln!("nameservers.com.zone:\n{}", nameservers_ns.zone_file()); + let Graph { + nameservers: _nameservers, + root, + .. + } = Graph::build(leaf_ns, Sign::No)?; - com_ns.referral( - nameservers_ns.zone().clone(), - nameservers_ns.fqdn().clone(), - nameservers_ns.ipv4_addr(), - ); - let com_ns = com_ns.start()?; - - eprintln!("com.zone:\n{}", com_ns.zone_file()); - - root_ns.referral(FQDN::COM, com_ns.fqdn().clone(), com_ns.ipv4_addr()); - let root_ns = root_ns.start()?; - - eprintln!("root.zone:\n{}", root_ns.zone_file()); - - let resolver = Resolver::new( - &network, - Root::new(root_ns.fqdn().clone(), root_ns.ipv4_addr()), - ) - .start(&dns_test::subject())?; + let resolver = Resolver::new(&network, root).start(&dns_test::subject())?; let resolver_ip_addr = resolver.ipv4_addr(); let client = Client::new(&network)?; @@ -69,30 +48,16 @@ fn nxdomain() -> Result<()> { let network = Network::new()?; let peer = dns_test::peer(); - let mut root_ns = NameServer::new(&peer, FQDN::ROOT, &network)?; - let mut com_ns = NameServer::new(&peer, FQDN::COM, &network)?; - let mut nameservers_ns = NameServer::new(&peer, FQDN("nameservers.com.")?, &network)?; - nameservers_ns - .add(Record::a(root_ns.fqdn().clone(), root_ns.ipv4_addr())) - .add(Record::a(com_ns.fqdn().clone(), com_ns.ipv4_addr())); - let nameservers_ns = nameservers_ns.start()?; + let leaf_ns = NameServer::new(&peer, FQDN::NAMESERVERS, &network)?; - com_ns.referral( - nameservers_ns.zone().clone(), - nameservers_ns.fqdn().clone(), - nameservers_ns.ipv4_addr(), - ); - let com_ns = com_ns.start()?; + let Graph { + nameservers: _nameservers, + root, + .. + } = Graph::build(leaf_ns, Sign::No)?; - root_ns.referral(FQDN::COM, com_ns.fqdn().clone(), com_ns.ipv4_addr()); - let root_ns = root_ns.start()?; - - let resolver = Resolver::new( - &network, - Root::new(root_ns.fqdn().clone(), root_ns.ipv4_addr()), - ) - .start(&dns_test::subject())?; + let resolver = Resolver::new(&network, root).start(&dns_test::subject())?; let resolver_ip_addr = resolver.ipv4_addr(); let client = Client::new(&network)?; diff --git a/packages/conformance-tests/src/resolver/dnssec/scenarios/bogus.rs b/packages/conformance-tests/src/resolver/dnssec/scenarios/bogus.rs index 5f51d7dc..3e33ed90 100644 --- a/packages/conformance-tests/src/resolver/dnssec/scenarios/bogus.rs +++ b/packages/conformance-tests/src/resolver/dnssec/scenarios/bogus.rs @@ -2,9 +2,8 @@ use std::net::Ipv4Addr; use base64::prelude::*; use dns_test::client::{Client, DigSettings}; -use dns_test::name_server::NameServer; +use dns_test::name_server::{Graph, NameServer, Sign}; use dns_test::record::{Record, RecordType}; -use dns_test::zone_file::Root; use dns_test::{Network, Resolver, Result, FQDN}; #[ignore] @@ -15,62 +14,41 @@ fn bad_signature_in_leaf_nameserver() -> Result<()> { let network = Network::new()?; let peer = dns_test::peer(); - let mut root_ns = NameServer::new(&peer, FQDN::ROOT, &network)?; - let mut com_ns = NameServer::new(&peer, FQDN::COM, &network)?; - let mut nameservers_ns = NameServer::new(&peer, FQDN("nameservers.com.")?, &network)?; - nameservers_ns - .add(Record::a(root_ns.fqdn().clone(), root_ns.ipv4_addr())) - .add(Record::a(com_ns.fqdn().clone(), com_ns.ipv4_addr())) - .add(Record::a(needle_fqdn.clone(), expected_ipv4_addr)); - let mut nameservers_ns = nameservers_ns.sign()?; + let mut leaf_ns = NameServer::new(&peer, FQDN::NAMESERVERS, &network)?; + leaf_ns.add(Record::a(needle_fqdn.clone(), expected_ipv4_addr)); - // fault injection: change the signature field of the RRSIG that covers the A record we'll query - let mut modified = 0; - for record in &mut nameservers_ns.signed_zone_file_mut().records { - if let Record::RRSIG(rrsig) = record { - if rrsig.fqdn == needle_fqdn { - let mut signature = BASE64_STANDARD.decode(&rrsig.signature)?; - let last = signature.last_mut().expect("empty signature"); - *last = !*last; + let Graph { + nameservers: _nameservers, + root, + trust_anchor, + } = Graph::build( + leaf_ns, + Sign::AndAmend(&|zone, records| { + if zone == &FQDN::NAMESERVERS { + let mut modified = 0; + for record in records { + if let Record::RRSIG(rrsig) = record { + if rrsig.fqdn == needle_fqdn { + let mut signature = BASE64_STANDARD.decode(&rrsig.signature).unwrap(); + let last = signature.last_mut().expect("empty signature"); + *last = !*last; - rrsig.signature = BASE64_STANDARD.encode(&signature); - modified += 1; + rrsig.signature = BASE64_STANDARD.encode(&signature); + modified += 1; + } + } + } + + assert_eq!(modified, 1, "sanity check"); } - } - } - assert_eq!(modified, 1, "sanity check"); + }), + )?; - let nameservers_ds = nameservers_ns.ds().clone(); - let nameservers_ns = nameservers_ns.start()?; - - com_ns - .referral( - nameservers_ns.zone().clone(), - nameservers_ns.fqdn().clone(), - nameservers_ns.ipv4_addr(), - ) - .add(nameservers_ds); - let com_ns = com_ns.sign()?; - let com_ds = com_ns.ds().clone(); - let com_ns = com_ns.start()?; - - root_ns - .referral(FQDN::COM, com_ns.fqdn().clone(), com_ns.ipv4_addr()) - .add(com_ds); - let root_ns = root_ns.sign()?; - let root_ksk = root_ns.key_signing_key().clone(); - let root_zsk = root_ns.zone_signing_key().clone(); - - let root_ns = root_ns.start()?; - - let resolver = Resolver::new( - &network, - Root::new(root_ns.fqdn().clone(), root_ns.ipv4_addr()), - ) - .trust_anchor_key(root_ksk) - .trust_anchor_key(root_zsk) - .start(&dns_test::subject())?; + let trust_anchor = &trust_anchor.unwrap(); + let resolver = Resolver::new(&network, root) + .trust_anchor(trust_anchor) + .start(&dns_test::subject())?; let resolver_addr = resolver.ipv4_addr(); let client = Client::new(&network)?; diff --git a/packages/conformance-tests/src/resolver/dnssec/scenarios/ede.rs b/packages/conformance-tests/src/resolver/dnssec/scenarios/ede.rs index 95d1dcda..1c30c7b3 100644 --- a/packages/conformance-tests/src/resolver/dnssec/scenarios/ede.rs +++ b/packages/conformance-tests/src/resolver/dnssec/scenarios/ede.rs @@ -1,9 +1,8 @@ use std::net::Ipv4Addr; use dns_test::client::{Client, DigSettings, ExtendedDnsError}; -use dns_test::name_server::NameServer; +use dns_test::name_server::{Graph, NameServer, Sign}; use dns_test::record::{Record, RecordType}; -use dns_test::zone_file::Root; use dns_test::{Network, Resolver, Result, FQDN}; #[ignore] @@ -16,74 +15,48 @@ fn dnskey_missing() -> Result<()> { let needle_fqdn = FQDN("example.nameservers.com.")?; let network = Network::new()?; - let peer = dns_test::peer(); - let mut root_ns = NameServer::new(&peer, FQDN::ROOT, &network)?; - let mut com_ns = NameServer::new(&peer, FQDN::COM, &network)?; + let mut leaf_ns = NameServer::new(&dns_test::peer(), FQDN::NAMESERVERS, &network)?; + leaf_ns.add(Record::a(needle_fqdn.clone(), expected_ipv4_addr)); - let mut nameservers_ns = NameServer::new(&peer, FQDN("nameservers.com.")?, &network)?; - nameservers_ns - .add(Record::a(root_ns.fqdn().clone(), root_ns.ipv4_addr())) - .add(Record::a(com_ns.fqdn().clone(), com_ns.ipv4_addr())) - .add(Record::a(needle_fqdn.clone(), expected_ipv4_addr)); - let mut nameservers_ns = nameservers_ns.sign()?; + let Graph { + nameservers: _nameservers, + root, + trust_anchor, + } = Graph::build( + leaf_ns, + Sign::AndAmend(&|zone, records| { + // remove the ZSK DNSKEY record + if zone == &FQDN::NAMESERVERS { + let mut remove_count = 0; + *records = records + .drain(..) + .filter(|record| { + let remove = if let Record::DNSKEY(dnskey) = record { + dnskey.is_zone_signing_key() + } else { + false + }; - // remove the ZSK DNSKEY record - let records = &mut nameservers_ns.signed_zone_file_mut().records; - let mut remove_count = 0; - *records = records - .drain(..) - .filter(|record| { - let remove = if let Record::DNSKEY(dnskey) = record { - dnskey.is_zone_signing_key() - } else { - false - }; + if remove { + remove_count += 1; + } - if remove { - remove_count += 1; + !remove + }) + .collect(); + assert_eq!(1, remove_count); } + }), + )?; - !remove - }) - .collect(); - assert_eq!(1, remove_count); - - let nameservers_ds = nameservers_ns.ds().clone(); - let nameservers_ns = nameservers_ns.start()?; - - com_ns - .referral( - nameservers_ns.zone().clone(), - nameservers_ns.fqdn().clone(), - nameservers_ns.ipv4_addr(), - ) - .add(nameservers_ds); - let com_ns = com_ns.sign()?; - let com_ds = com_ns.ds().clone(); - let com_ns = com_ns.start()?; - - root_ns - .referral(FQDN::COM, com_ns.fqdn().clone(), com_ns.ipv4_addr()) - .add(com_ds); - let root_ns = root_ns.sign()?; - let root_ksk = root_ns.key_signing_key().clone(); - let root_zsk = root_ns.zone_signing_key().clone(); - - let root_ns = root_ns.start()?; - - let mut resolver = Resolver::new( - &network, - Root::new(root_ns.fqdn().clone(), root_ns.ipv4_addr()), - ); + let mut resolver = Resolver::new(&network, root); if supports_ede { resolver.extended_dns_errors(); } - let resolver = resolver - .trust_anchor_key(root_ksk) - .trust_anchor_key(root_zsk) - .start(&subject)?; + let trust_anchor = &trust_anchor.unwrap(); + let resolver = resolver.trust_anchor(trust_anchor).start(&subject)?; let resolver_addr = resolver.ipv4_addr(); let client = Client::new(&network)?; diff --git a/packages/conformance-tests/src/resolver/dnssec/scenarios/secure.rs b/packages/conformance-tests/src/resolver/dnssec/scenarios/secure.rs index 3ed3439b..9a0b2515 100644 --- a/packages/conformance-tests/src/resolver/dnssec/scenarios/secure.rs +++ b/packages/conformance-tests/src/resolver/dnssec/scenarios/secure.rs @@ -1,7 +1,7 @@ use std::net::Ipv4Addr; use dns_test::client::{Client, DigSettings}; -use dns_test::name_server::NameServer; +use dns_test::name_server::{Graph, NameServer, Sign}; use dns_test::record::{Record, RecordType}; use dns_test::zone_file::Root; use dns_test::{Network, Resolver, Result, TrustAnchor, FQDN}; @@ -51,53 +51,20 @@ fn can_validate_with_delegation() -> Result<()> { let peer = dns_test::peer(); let network = Network::new()?; - let mut root_ns = NameServer::new(&peer, FQDN::ROOT, &network)?; - let mut com_ns = NameServer::new(&peer, FQDN::COM, &network)?; - let mut nameservers_ns = NameServer::new(&peer, FQDN("nameservers.com.")?, &network)?; - nameservers_ns - .add(Record::a(root_ns.fqdn().clone(), root_ns.ipv4_addr())) - .add(Record::a(com_ns.fqdn().clone(), com_ns.ipv4_addr())) - .add(Record::a(needle_fqdn.clone(), expected_ipv4_addr)); - let nameservers_ns = nameservers_ns.sign()?; - let nameservers_ds = nameservers_ns.ds().clone(); - let nameservers_ns = nameservers_ns.start()?; + let mut leaf_ns = NameServer::new(&peer, FQDN::NAMESERVERS, &network)?; + leaf_ns.add(Record::a(needle_fqdn.clone(), expected_ipv4_addr)); - eprintln!("nameservers.com.zone:\n{}", nameservers_ns.zone_file()); + let Graph { + nameservers: _nameservers, + root, + trust_anchor, + } = Graph::build(leaf_ns, Sign::Yes)?; - com_ns - .referral( - nameservers_ns.zone().clone(), - nameservers_ns.fqdn().clone(), - nameservers_ns.ipv4_addr(), - ) - .add(nameservers_ds); - let com_ns = com_ns.sign()?; - let com_ds = com_ns.ds().clone(); - let com_ns = com_ns.start()?; - - eprintln!("com.zone:\n{}", com_ns.zone_file()); - - root_ns - .referral(FQDN::COM, com_ns.fqdn().clone(), com_ns.ipv4_addr()) - .add(com_ds); - let root_ns = root_ns.sign()?; - let root_ksk = root_ns.key_signing_key().clone(); - let root_zsk = root_ns.zone_signing_key().clone(); - - eprintln!("root.zone.signed:\n{}", root_ns.signed_zone_file()); - - let root_ns = root_ns.start()?; - - eprintln!("root.zone:\n{}", root_ns.zone_file()); - - let trust_anchor = &TrustAnchor::from_iter([root_ksk, root_zsk]); - let resolver = Resolver::new( - &network, - Root::new(root_ns.fqdn().clone(), root_ns.ipv4_addr()), - ) - .trust_anchor(trust_anchor) - .start(&dns_test::subject())?; + let trust_anchor = &trust_anchor.unwrap(); + let resolver = Resolver::new(&network, root) + .trust_anchor(trust_anchor) + .start(&dns_test::subject())?; let resolver_addr = resolver.ipv4_addr(); let client = Client::new(&network)?; From b96aa89da9ceb2fe591f1609953e652889a14847 Mon Sep 17 00:00:00 2001 From: Jorge Aparicio Date: Thu, 7 Mar 2024 14:15:10 +0100 Subject: [PATCH 7/7] add more EDE tests --- .../src/resolver/dnssec/scenarios/ede.rs | 137 +++++++++++++++--- packages/dns-test/src/client.rs | 6 +- 2 files changed, 122 insertions(+), 21 deletions(-) diff --git a/packages/conformance-tests/src/resolver/dnssec/scenarios/ede.rs b/packages/conformance-tests/src/resolver/dnssec/scenarios/ede.rs index 1c30c7b3..ccae7178 100644 --- a/packages/conformance-tests/src/resolver/dnssec/scenarios/ede.rs +++ b/packages/conformance-tests/src/resolver/dnssec/scenarios/ede.rs @@ -8,25 +8,11 @@ use dns_test::{Network, Resolver, Result, FQDN}; #[ignore] #[test] fn dnskey_missing() -> Result<()> { - let subject = dns_test::subject(); - let supports_ede = subject.supports_ede(); - - let expected_ipv4_addr = Ipv4Addr::new(1, 2, 3, 4); - let needle_fqdn = FQDN("example.nameservers.com.")?; - - let network = Network::new()?; - let mut leaf_ns = NameServer::new(&dns_test::peer(), FQDN::NAMESERVERS, &network)?; - leaf_ns.add(Record::a(needle_fqdn.clone(), expected_ipv4_addr)); - - let Graph { - nameservers: _nameservers, - root, - trust_anchor, - } = Graph::build( - leaf_ns, - Sign::AndAmend(&|zone, records| { - // remove the ZSK DNSKEY record + fixture( + ExtendedDnsError::DnskeyMissing, + |_needle_fqdn, zone, records| { if zone == &FQDN::NAMESERVERS { + // remove the DNSKEY record that contains the ZSK let mut remove_count = 0; *records = records .drain(..) @@ -44,8 +30,119 @@ fn dnskey_missing() -> Result<()> { !remove }) .collect(); - assert_eq!(1, remove_count); + assert_eq!(1, remove_count, "sanity check"); } + }, + ) +} + +#[ignore] +#[test] +fn rrsigs_missing() -> Result<()> { + fixture( + ExtendedDnsError::RrsigsMissing, + |needle_fqdn, zone, records| { + if zone == &FQDN::NAMESERVERS { + // remove the RRSIG records that covers the needle record + let mut remove_count = 0; + *records = records + .drain(..) + .filter(|record| { + let remove = if let Record::RRSIG(rrsig) = record { + rrsig.type_covered == RecordType::A && rrsig.fqdn == *needle_fqdn + } else { + false + }; + + if remove { + remove_count += 1; + } + + !remove + }) + .collect(); + assert_eq!(1, remove_count, "sanity check"); + } + }, + ) +} + +#[ignore] +#[test] +fn unsupported_dnskey_algorithm() -> Result<()> { + fixture( + ExtendedDnsError::UnsupportedDnskeyAlgorithm, + |needle_fqdn, zone, records| { + if zone == &FQDN::NAMESERVERS { + // lie about the algorithm that was used to sign the needle record + let mut modified_count = 0; + for record in records { + if let Record::RRSIG(rrsig) = record { + if rrsig.type_covered == RecordType::A && rrsig.fqdn == *needle_fqdn { + assert_ne!(1, rrsig.algorithm, "modify the value below"); + rrsig.algorithm = 1; + modified_count += 1; + } + } + } + assert_eq!(1, modified_count, "sanity check"); + } + }, + ) +} + +#[ignore] +#[test] +fn dnssec_bogus() -> Result<()> { + fixture( + ExtendedDnsError::DnssecBogus, + |needle_fqdn, zone, records| { + if zone == &FQDN::NAMESERVERS { + // corrupt the RRSIG record that covers the needle record + let mut modified_count = 0; + for record in records { + if let Record::RRSIG(rrsig) = record { + if rrsig.type_covered == RecordType::A && rrsig.fqdn == *needle_fqdn { + rrsig.signature_expiration = rrsig.signature_inception - 1; + modified_count += 1; + } + } + } + assert_eq!(1, modified_count, "sanity check"); + } + }, + ) +} + +// Sets up a minimal, DNSSEC-enabled DNS graph where the leaf zone contains a "needle" A record +// that we'll search for +// +// `amend` can be used to modify zone files *after* they have been signed. it's used to introduce +// errors in the signed zone files +// +// the query for the needle record is expected to fail with the `expected` Extended DNS Error +fn fixture( + expected: ExtendedDnsError, + amend: fn(needle_fqdn: &FQDN, zone: &FQDN, records: &mut Vec), +) -> Result<()> { + let subject = dns_test::subject(); + let supports_ede = subject.supports_ede(); + + let expected_ipv4_addr = Ipv4Addr::new(1, 2, 3, 4); + let needle_fqdn = FQDN("example.nameservers.com.")?; + + let network = Network::new()?; + let mut leaf_ns = NameServer::new(&dns_test::peer(), FQDN::NAMESERVERS, &network)?; + leaf_ns.add(Record::a(needle_fqdn.clone(), expected_ipv4_addr)); + + let Graph { + nameservers: _nameservers, + root, + trust_anchor, + } = Graph::build( + leaf_ns, + Sign::AndAmend(&|zone, records| { + amend(&needle_fqdn, zone, records); }), )?; @@ -67,7 +164,7 @@ fn dnskey_missing() -> Result<()> { assert!(output.status.is_servfail()); if supports_ede { - assert_eq!(Some(ExtendedDnsError::DnskeyMissing), output.ede); + assert_eq!(Some(expected), output.ede); } Ok(()) diff --git a/packages/dns-test/src/client.rs b/packages/dns-test/src/client.rs index 3dd2a652..281eab82 100644 --- a/packages/dns-test/src/client.rs +++ b/packages/dns-test/src/client.rs @@ -255,8 +255,10 @@ impl FromStr for DigOutput { #[derive(Debug, PartialEq)] pub enum ExtendedDnsError { - DnssecBogus, DnskeyMissing, + DnssecBogus, + RrsigsMissing, + UnsupportedDnskeyAlgorithm, } impl FromStr for ExtendedDnsError { @@ -266,8 +268,10 @@ impl FromStr for ExtendedDnsError { let code: u16 = input.parse()?; let code = match code { + 1 => Self::UnsupportedDnskeyAlgorithm, 6 => Self::DnssecBogus, 9 => Self::DnskeyMissing, + 10 => Self::RrsigsMissing, _ => todo!("EDE {code} has not yet been implemented"), };