set up DS records and trust anchor to make DNSSEC work
This commit is contained in:
parent
2bcad2a25c
commit
306ce7a32b
|
@ -2,5 +2,3 @@ FROM ubuntu:22.04
|
|||
|
||||
RUN apt-get update && \
|
||||
apt-get install -y dnsutils unbound nsd iputils-ping tshark vim ldnsutils
|
||||
|
||||
COPY ./files/etc/unbound/unbound.conf /etc/unbound/unbound.conf
|
||||
|
|
|
@ -16,6 +16,22 @@ impl Client {
|
|||
})
|
||||
}
|
||||
|
||||
// FIXME this needs to use the same trust anchor as `RecursiveResolver` or validation will fail
|
||||
pub fn delv(
|
||||
&self,
|
||||
server: Ipv4Addr,
|
||||
record_type: RecordType,
|
||||
fqdn: &FQDN<'_>,
|
||||
) -> Result<String> {
|
||||
self.inner.stdout(&[
|
||||
"delv",
|
||||
"+mtrace",
|
||||
&format!("@{server}"),
|
||||
record_type.as_str(),
|
||||
fqdn.as_str(),
|
||||
])
|
||||
}
|
||||
|
||||
pub fn dig(
|
||||
&self,
|
||||
recurse: Recurse,
|
||||
|
@ -153,6 +169,7 @@ pub struct DigFlags {
|
|||
pub recursion_desired: bool,
|
||||
pub recursion_available: bool,
|
||||
pub authoritative_answer: bool,
|
||||
pub authenticated_data: bool,
|
||||
}
|
||||
|
||||
impl FromStr for DigFlags {
|
||||
|
@ -163,6 +180,7 @@ impl FromStr for DigFlags {
|
|||
let mut recursion_desired = false;
|
||||
let mut recursion_available = false;
|
||||
let mut authoritative_answer = false;
|
||||
let mut authenticated_data = false;
|
||||
|
||||
for flag in input.split_whitespace() {
|
||||
match flag {
|
||||
|
@ -170,6 +188,7 @@ impl FromStr for DigFlags {
|
|||
"rd" => recursion_desired = true,
|
||||
"ra" => recursion_available = true,
|
||||
"aa" => authoritative_answer = true,
|
||||
"ad" => authenticated_data = true,
|
||||
_ => return Err(format!("unknown flag: {flag}").into()),
|
||||
}
|
||||
}
|
||||
|
@ -179,6 +198,7 @@ impl FromStr for DigFlags {
|
|||
recursion_desired,
|
||||
recursion_available,
|
||||
authoritative_answer,
|
||||
authenticated_data,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,12 +1,10 @@
|
|||
use core::array;
|
||||
use core::str::FromStr;
|
||||
use core::sync::atomic::{self, AtomicUsize};
|
||||
use std::net::Ipv4Addr;
|
||||
use std::process::Child;
|
||||
|
||||
use crate::container::Container;
|
||||
use crate::zone_file::{self, SoaSettings, ZoneFile};
|
||||
use crate::{Error, Result, FQDN};
|
||||
use crate::zone_file::{self, SoaSettings, ZoneFile, DNSKEY, DS};
|
||||
use crate::{Result, FQDN};
|
||||
|
||||
pub struct NameServer<'a, State> {
|
||||
container: Container,
|
||||
|
@ -68,6 +66,12 @@ impl<'a> NameServer<'a, Stopped> {
|
|||
self
|
||||
}
|
||||
|
||||
/// Adds a DS record to the zone file
|
||||
pub fn ds(&mut self, ds: DS) -> &mut Self {
|
||||
self.zone_file.entry(ds);
|
||||
self
|
||||
}
|
||||
|
||||
/// Freezes and signs the name server's zone file
|
||||
pub fn sign(self) -> Result<NameServer<'a, Signed>> {
|
||||
// TODO do we want to make these settings configurable?
|
||||
|
@ -90,13 +94,13 @@ impl<'a> NameServer<'a, Stopped> {
|
|||
format!("cd {ZONES_DIR} && ldns-keygen -a {ALGORITHM} -b {ZSK_BITS} {zone}");
|
||||
let zsk_filename = container.stdout(&["sh", "-c", &zsk_keygen])?;
|
||||
let zsk_path = format!("{ZONES_DIR}/{zsk_filename}.key");
|
||||
let zsk: Key = container.stdout(&["cat", &zsk_path])?.parse()?;
|
||||
let zsk: DNSKEY = container.stdout(&["cat", &zsk_path])?.parse()?;
|
||||
|
||||
let ksk_keygen =
|
||||
format!("cd {ZONES_DIR} && ldns-keygen -k -a {ALGORITHM} -b {KSK_BITS} {zone}");
|
||||
let ksk_filename = container.stdout(&["sh", "-c", &ksk_keygen])?;
|
||||
let ksk_path = format!("{ZONES_DIR}/{ksk_filename}.key");
|
||||
let ksk: Key = container.stdout(&["cat", &ksk_path])?.parse()?;
|
||||
let ksk: DNSKEY = container.stdout(&["cat", &ksk_path])?.parse()?;
|
||||
|
||||
// -n = use NSEC3 instead of NSEC
|
||||
// -p = set the opt-out flag on all nsec3 rrs
|
||||
|
@ -105,6 +109,11 @@ impl<'a> NameServer<'a, Stopped> {
|
|||
);
|
||||
container.status_ok(&["sh", "-c", &signzone])?;
|
||||
|
||||
// TODO do we want to make the hashing algorithm configurable?
|
||||
// -2 = use SHA256 for the DS hash
|
||||
let key2ds = format!("cd {ZONES_DIR} && ldns-key2ds -n -2 {ZONE_FILENAME}.signed");
|
||||
let ds: DS = container.stdout(&["sh", "-c", &key2ds])?.parse()?;
|
||||
|
||||
// we have an in-memory representation of the zone file so we just delete the on-disk version
|
||||
let zone_file_path = zone_file_path();
|
||||
container.status_ok(&["mv", &format!("{zone_file_path}.signed"), &zone_file_path])?;
|
||||
|
@ -115,9 +124,10 @@ impl<'a> NameServer<'a, Stopped> {
|
|||
container,
|
||||
zone_file,
|
||||
state: Signed {
|
||||
zsk,
|
||||
ds,
|
||||
ksk,
|
||||
signed_zone_file,
|
||||
zsk,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
@ -148,62 +158,6 @@ impl<'a> NameServer<'a, Stopped> {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Key {
|
||||
pub bits: u16,
|
||||
pub encoded: String,
|
||||
pub id: u32,
|
||||
}
|
||||
|
||||
impl FromStr for Key {
|
||||
type Err = Error;
|
||||
|
||||
fn from_str(input: &str) -> Result<Self> {
|
||||
let (before, after) = input.split_once(';').ok_or("comment was not found")?;
|
||||
let mut columns = before.split_whitespace();
|
||||
|
||||
let [Some(_zone), Some(class), Some(record_type), Some(_flags), Some(_protocol), Some(_algorithm), Some(encoded), None] =
|
||||
array::from_fn(|_| columns.next())
|
||||
else {
|
||||
return Err("expected 7 columns".into());
|
||||
};
|
||||
|
||||
if record_type != "DNSKEY" {
|
||||
return Err(format!("tried to parse `{record_type}` record as a DNSKEY record").into());
|
||||
}
|
||||
|
||||
if class != "IN" {
|
||||
return Err(format!("unknown class: {class}").into());
|
||||
}
|
||||
|
||||
// {id = 24975 (zsk), size = 1024b}
|
||||
let error = "invalid comment syntax";
|
||||
let (id_expr, size_expr) = after.split_once(',').ok_or(error)?;
|
||||
|
||||
// {id = 24975 (zsk)
|
||||
let (id_lhs, id_rhs) = id_expr.split_once('=').ok_or(error)?;
|
||||
if id_lhs.trim() != "{id" {
|
||||
return Err(error.into());
|
||||
}
|
||||
|
||||
// 24975 (zsk)
|
||||
let (id, _key_type) = id_rhs.trim().split_once(' ').ok_or(error)?;
|
||||
|
||||
// size = 1024b}
|
||||
let (size_lhs, size_rhs) = size_expr.split_once('=').ok_or(error)?;
|
||||
if size_lhs.trim() != "size" {
|
||||
return Err(error.into());
|
||||
}
|
||||
let bits = size_rhs.trim().strip_suffix("b}").ok_or(error)?.parse()?;
|
||||
|
||||
Ok(Self {
|
||||
bits,
|
||||
encoded: encoded.to_string(),
|
||||
id: id.parse()?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const ZONES_DIR: &str = "/etc/nsd/zones";
|
||||
const ZONE_FILENAME: &str = "main.zone";
|
||||
|
||||
|
@ -239,17 +193,21 @@ impl<'a> NameServer<'a, Signed> {
|
|||
})
|
||||
}
|
||||
|
||||
pub fn key_signing_key(&self) -> &Key {
|
||||
pub fn key_signing_key(&self) -> &DNSKEY {
|
||||
&self.state.ksk
|
||||
}
|
||||
|
||||
pub fn zone_signing_key(&self) -> &Key {
|
||||
pub fn zone_signing_key(&self) -> &DNSKEY {
|
||||
&self.state.zsk
|
||||
}
|
||||
|
||||
pub fn signed_zone_file(&self) -> &str {
|
||||
&self.state.signed_zone_file
|
||||
}
|
||||
|
||||
pub fn ds(&self) -> &DS {
|
||||
&self.state.ds
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, S> NameServer<'a, S> {
|
||||
|
@ -257,6 +215,7 @@ impl<'a, S> NameServer<'a, S> {
|
|||
self.container.ipv4_addr()
|
||||
}
|
||||
|
||||
/// Zone file BEFORE signing
|
||||
pub fn zone_file(&self) -> &ZoneFile<'a> {
|
||||
&self.zone_file
|
||||
}
|
||||
|
@ -273,8 +232,9 @@ impl<'a, S> NameServer<'a, S> {
|
|||
pub struct Stopped;
|
||||
|
||||
pub struct Signed {
|
||||
zsk: Key,
|
||||
ksk: Key,
|
||||
ds: DS,
|
||||
zsk: DNSKEY,
|
||||
ksk: DNSKEY,
|
||||
signed_zone_file: String,
|
||||
}
|
||||
|
||||
|
@ -360,21 +320,21 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn signed() -> Result<()> {
|
||||
let tld_ns = NameServer::new(FQDN::ROOT)?.sign()?;
|
||||
let ns = NameServer::new(FQDN::ROOT)?.sign()?;
|
||||
|
||||
eprintln!("KSK: {:?}", tld_ns.key_signing_key());
|
||||
eprintln!("ZSK: {:?}", tld_ns.zone_signing_key());
|
||||
eprintln!("root.zone.signed:\n{}", tld_ns.signed_zone_file());
|
||||
eprintln!("KSK:\n{}", ns.key_signing_key());
|
||||
eprintln!("ZSK:\n{}", ns.zone_signing_key());
|
||||
eprintln!("root.zone.signed:\n{}", ns.signed_zone_file());
|
||||
|
||||
let tld_ns = tld_ns.start()?;
|
||||
let tld_ns = ns.start()?;
|
||||
|
||||
let ipv4_addr = tld_ns.ipv4_addr();
|
||||
let ns_addr = tld_ns.ipv4_addr();
|
||||
|
||||
let client = Client::new()?;
|
||||
let output = client.dig(
|
||||
Recurse::No,
|
||||
Dnssec::Yes,
|
||||
ipv4_addr,
|
||||
ns_addr,
|
||||
RecordType::SOA,
|
||||
&FQDN::ROOT,
|
||||
)?;
|
||||
|
@ -392,18 +352,4 @@ mod tests {
|
|||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn can_parse_ldns_keygen_output() -> Result<()> {
|
||||
let input = "example.com. IN DNSKEY 256 3 7 AwEAAdIpMlio4GJas7GbIZ9xRpzpB2pf4SxBJcsquN/0yNBPGNE2rzcFykqMAKmLwypk1/1q/EdHVa4tQ5RlK0w09CRhgSXfCaph+yLNJKpiPyuVcXKl2k0RnO4p835sgVEUIvx8qGTDo7c7DA9UBje+/3ViFKqVhOBaWyT6gHAmNVpb ;{id = 24975 (zsk), size = 1024b}";
|
||||
|
||||
let key: Key = input.parse()?;
|
||||
|
||||
assert_eq!(1024, key.bits);
|
||||
assert_eq!(24975, key.id);
|
||||
let expected = "AwEAAdIpMlio4GJas7GbIZ9xRpzpB2pf4SxBJcsquN/0yNBPGNE2rzcFykqMAKmLwypk1/1q/EdHVa4tQ5RlK0w09CRhgSXfCaph+yLNJKpiPyuVcXKl2k0RnO4p835sgVEUIvx8qGTDo7c7DA9UBje+/3ViFKqVhOBaWyT6gHAmNVpb";
|
||||
assert_eq!(expected, key.encoded);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,7 +3,7 @@ use std::net::Ipv4Addr;
|
|||
use std::process::Child;
|
||||
|
||||
use crate::container::Container;
|
||||
use crate::zone_file::Root;
|
||||
use crate::zone_file::{Root, DNSKEY};
|
||||
use crate::Result;
|
||||
|
||||
pub struct RecursiveResolver {
|
||||
|
@ -12,7 +12,9 @@ pub struct RecursiveResolver {
|
|||
}
|
||||
|
||||
impl RecursiveResolver {
|
||||
pub fn start(roots: &[Root]) -> Result<Self> {
|
||||
pub fn start(roots: &[Root], trust_anchors: &[DNSKEY]) -> Result<Self> {
|
||||
const TRUST_ANCHOR_FILE: &str = "/etc/trusted-key.key";
|
||||
|
||||
let container = Container::run()?;
|
||||
|
||||
let mut hints = String::new();
|
||||
|
@ -22,6 +24,18 @@ impl RecursiveResolver {
|
|||
|
||||
container.cp("/etc/unbound/root.hints", &hints)?;
|
||||
|
||||
let use_dnssec = !trust_anchors.is_empty();
|
||||
container.cp("/etc/unbound/unbound.conf", &unbound_conf(use_dnssec))?;
|
||||
|
||||
if use_dnssec {
|
||||
let trust_anchor = trust_anchors.iter().fold(String::new(), |mut buf, ds| {
|
||||
writeln!(buf, "{ds}").expect("infallible");
|
||||
buf
|
||||
});
|
||||
|
||||
container.cp(TRUST_ANCHOR_FILE, &trust_anchor)?;
|
||||
}
|
||||
|
||||
let child = container.spawn(&["unbound", "-d"])?;
|
||||
|
||||
Ok(Self { child, container })
|
||||
|
@ -38,8 +52,13 @@ impl Drop for RecursiveResolver {
|
|||
}
|
||||
}
|
||||
|
||||
fn unbound_conf(use_dnssec: bool) -> String {
|
||||
minijinja::render!(include_str!("templates/unbound.conf.jinja"), use_dnssec => use_dnssec)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
||||
use crate::{
|
||||
client::{Client, Dnssec, Recurse},
|
||||
name_server::NameServer,
|
||||
|
@ -81,7 +100,7 @@ mod tests {
|
|||
eprintln!("root.zone:\n{}", root_ns.zone_file());
|
||||
|
||||
let roots = &[Root::new(root_ns.fqdn().clone(), root_ns.ipv4_addr())];
|
||||
let resolver = RecursiveResolver::start(roots)?;
|
||||
let resolver = RecursiveResolver::start(roots, &[])?;
|
||||
let resolver_ip_addr = resolver.ipv4_addr();
|
||||
|
||||
let client = Client::new()?;
|
||||
|
@ -103,4 +122,115 @@ mod tests {
|
|||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// no DS records are involved; this is a single-link chain of trust
|
||||
#[test]
|
||||
fn can_validate_without_delegation() -> Result<()> {
|
||||
let mut ns = NameServer::new(FQDN::ROOT)?;
|
||||
ns.a(ns.fqdn().clone(), ns.ipv4_addr());
|
||||
let ns = ns.sign()?;
|
||||
|
||||
let root_ksk = ns.key_signing_key().clone();
|
||||
let root_zsk = ns.zone_signing_key().clone();
|
||||
|
||||
eprintln!("root.zone.signed:\n{}", ns.signed_zone_file());
|
||||
|
||||
let ns = ns.start()?;
|
||||
|
||||
eprintln!("root.zone:\n{}", ns.zone_file());
|
||||
|
||||
let roots = &[Root::new(ns.fqdn().clone(), ns.ipv4_addr())];
|
||||
|
||||
let trust_anchor = [root_ksk.clone(), root_zsk.clone()];
|
||||
let resolver = RecursiveResolver::start(roots, &trust_anchor)?;
|
||||
let resolver_addr = resolver.ipv4_addr();
|
||||
|
||||
let client = Client::new()?;
|
||||
let output = client.dig(
|
||||
Recurse::Yes,
|
||||
Dnssec::Yes,
|
||||
resolver_addr,
|
||||
RecordType::SOA,
|
||||
&FQDN::ROOT,
|
||||
)?;
|
||||
|
||||
assert!(output.status.is_noerror());
|
||||
assert!(output.flags.authenticated_data);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn can_validate_with_delegation() -> Result<()> {
|
||||
let expected_ipv4_addr = Ipv4Addr::new(1, 2, 3, 4);
|
||||
let needle = FQDN("example.nameservers.com.")?;
|
||||
|
||||
let mut root_ns = NameServer::new(FQDN::ROOT)?;
|
||||
let mut com_ns = NameServer::new(FQDN::COM)?;
|
||||
|
||||
let mut nameservers_ns = NameServer::new(FQDN("nameservers.com.")?)?;
|
||||
nameservers_ns
|
||||
.a(root_ns.fqdn().clone(), root_ns.ipv4_addr())
|
||||
.a(com_ns.fqdn().clone(), com_ns.ipv4_addr())
|
||||
.a(needle.clone(), expected_ipv4_addr);
|
||||
let nameservers_ns = nameservers_ns.sign()?;
|
||||
let nameservers_ds = nameservers_ns.ds().clone();
|
||||
let nameservers_ns = nameservers_ns.start()?;
|
||||
|
||||
eprintln!("nameservers.com.zone:\n{}", nameservers_ns.zone_file());
|
||||
|
||||
com_ns
|
||||
.referral(
|
||||
nameservers_ns.zone().clone(),
|
||||
nameservers_ns.fqdn().clone(),
|
||||
nameservers_ns.ipv4_addr(),
|
||||
)
|
||||
.ds(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())
|
||||
.ds(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 roots = &[Root::new(root_ns.fqdn().clone(), root_ns.ipv4_addr())];
|
||||
|
||||
let resolver = RecursiveResolver::start(roots, &[root_ksk.clone(), root_zsk.clone()])?;
|
||||
let resolver_ip_addr = resolver.ipv4_addr();
|
||||
|
||||
let client = Client::new()?;
|
||||
let output = client.dig(
|
||||
Recurse::Yes,
|
||||
Dnssec::Yes,
|
||||
resolver_ip_addr,
|
||||
RecordType::A,
|
||||
&needle,
|
||||
)?;
|
||||
|
||||
drop(resolver);
|
||||
|
||||
assert!(output.status.is_noerror());
|
||||
|
||||
assert!(output.flags.authenticated_data);
|
||||
|
||||
let [a, _rrsig] = output.answer.try_into().unwrap();
|
||||
let a = a.try_into_a().unwrap();
|
||||
|
||||
assert_eq!(needle, a.fqdn);
|
||||
assert_eq!(expected_ipv4_addr, a.ipv4_addr);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,6 +4,9 @@ server:
|
|||
interface: 0.0.0.0
|
||||
access-control: 172.17.0.0/16 allow
|
||||
root-hints: /etc/unbound/root.hints
|
||||
{% if use_dnssec %}
|
||||
trust-anchor-file: /etc/trusted-key.key
|
||||
{% endif %}
|
||||
|
||||
remote-control:
|
||||
control-enable: no
|
210
src/zone_file.rs
210
src/zone_file.rs
|
@ -4,10 +4,11 @@
|
|||
//! - the `@` syntax is not used to avoid relying on the order of the entries
|
||||
//! - relative domain names are not used; all domain names must be in fully-qualified form
|
||||
|
||||
use core::fmt;
|
||||
use core::{array, fmt};
|
||||
use std::net::Ipv4Addr;
|
||||
use std::str::FromStr;
|
||||
|
||||
use crate::FQDN;
|
||||
use crate::{Error, FQDN};
|
||||
|
||||
pub struct ZoneFile<'a> {
|
||||
pub origin: FQDN<'a>,
|
||||
|
@ -94,9 +95,17 @@ impl fmt::Display for Root<'_> {
|
|||
|
||||
pub enum Entry<'a> {
|
||||
A(A<'a>),
|
||||
DNSKEY(DNSKEY),
|
||||
DS(DS),
|
||||
NS(NS<'a>),
|
||||
}
|
||||
|
||||
impl<'a> From<DS> for Entry<'a> {
|
||||
fn from(v: DS) -> Self {
|
||||
Self::DS(v)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<A<'a>> for Entry<'a> {
|
||||
fn from(v: A<'a>) -> Self {
|
||||
Self::A(v)
|
||||
|
@ -113,6 +122,8 @@ impl fmt::Display for Entry<'_> {
|
|||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Entry::A(a) => a.fmt(f),
|
||||
Entry::DNSKEY(dnskey) => dnskey.fmt(f),
|
||||
Entry::DS(ds) => ds.fmt(f),
|
||||
Entry::NS(ns) => ns.fmt(f),
|
||||
}
|
||||
}
|
||||
|
@ -132,6 +143,166 @@ impl fmt::Display for A<'_> {
|
|||
}
|
||||
}
|
||||
|
||||
// integer types chosen based on bit sizes in section 2.1 of RFC4034
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct DNSKEY {
|
||||
zone: FQDN<'static>,
|
||||
flags: u16,
|
||||
protocol: u8,
|
||||
algorithm: u8,
|
||||
public_key: String,
|
||||
|
||||
// extra information in `+multiline` format and `ldns-keygen`'s output
|
||||
bits: u16,
|
||||
key_tag: u16,
|
||||
}
|
||||
|
||||
impl DNSKEY {
|
||||
pub fn bits(&self) -> u16 {
|
||||
self.bits
|
||||
}
|
||||
|
||||
pub fn key_tag(&self) -> u16 {
|
||||
self.key_tag
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for DNSKEY {
|
||||
type Err = Error;
|
||||
|
||||
fn from_str(input: &str) -> Result<Self, Self::Err> {
|
||||
let (before, after) = input.split_once(';').ok_or("comment was not found")?;
|
||||
let mut columns = before.split_whitespace();
|
||||
|
||||
let [Some(zone), Some(class), Some(record_type), Some(flags), Some(protocol), Some(algorithm), Some(public_key), None] =
|
||||
array::from_fn(|_| columns.next())
|
||||
else {
|
||||
return Err("expected 7 columns".into());
|
||||
};
|
||||
|
||||
if record_type != "DNSKEY" {
|
||||
return Err(format!("tried to parse `{record_type}` record as a DNSKEY record").into());
|
||||
}
|
||||
|
||||
if class != "IN" {
|
||||
return Err(format!("unknown class: {class}").into());
|
||||
}
|
||||
|
||||
// {id = 24975 (zsk), size = 1024b}
|
||||
let error = "invalid comment syntax";
|
||||
let (id_expr, size_expr) = after.split_once(',').ok_or(error)?;
|
||||
|
||||
// {id = 24975 (zsk)
|
||||
let (id_lhs, id_rhs) = id_expr.split_once('=').ok_or(error)?;
|
||||
if id_lhs.trim() != "{id" {
|
||||
return Err(error.into());
|
||||
}
|
||||
|
||||
// 24975 (zsk)
|
||||
let (key_tag, _key_type) = id_rhs.trim().split_once(' ').ok_or(error)?;
|
||||
|
||||
// size = 1024b}
|
||||
let (size_lhs, size_rhs) = size_expr.split_once('=').ok_or(error)?;
|
||||
if size_lhs.trim() != "size" {
|
||||
return Err(error.into());
|
||||
}
|
||||
let bits = size_rhs.trim().strip_suffix("b}").ok_or(error)?.parse()?;
|
||||
|
||||
Ok(Self {
|
||||
zone: zone.parse()?,
|
||||
flags: flags.parse()?,
|
||||
protocol: protocol.parse()?,
|
||||
algorithm: algorithm.parse()?,
|
||||
public_key: public_key.to_string(),
|
||||
|
||||
key_tag: key_tag.parse()?,
|
||||
bits,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for DNSKEY {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
let Self {
|
||||
zone,
|
||||
flags,
|
||||
protocol,
|
||||
algorithm,
|
||||
public_key,
|
||||
bits: _,
|
||||
key_tag: _,
|
||||
} = self;
|
||||
|
||||
write!(
|
||||
f,
|
||||
"{zone}\tIN\tDNSKEY\t{flags}\t{protocol}\t{algorithm}\t{public_key}"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct DS {
|
||||
zone: FQDN<'static>,
|
||||
_ttl: u32,
|
||||
key_tag: u16,
|
||||
algorithm: u8,
|
||||
digest_type: u8,
|
||||
digest: String,
|
||||
}
|
||||
|
||||
impl FromStr for DS {
|
||||
type Err = Error;
|
||||
|
||||
fn from_str(input: &str) -> Result<Self, Self::Err> {
|
||||
let mut columns = input.split_whitespace();
|
||||
|
||||
let [Some(zone), Some(ttl), Some(class), Some(record_type), Some(key_tag), Some(algorithm), Some(digest_type), Some(digest), None] =
|
||||
array::from_fn(|_| columns.next())
|
||||
else {
|
||||
return Err("expected 8 columns".into());
|
||||
};
|
||||
|
||||
let expected = "DS";
|
||||
if record_type != expected {
|
||||
return Err(
|
||||
format!("tried to parse `{record_type}` entry as a {expected} entry").into(),
|
||||
);
|
||||
}
|
||||
|
||||
if class != "IN" {
|
||||
return Err(format!("unknown class: {class}").into());
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
zone: zone.parse()?,
|
||||
_ttl: ttl.parse()?,
|
||||
key_tag: key_tag.parse()?,
|
||||
algorithm: algorithm.parse()?,
|
||||
digest_type: digest_type.parse()?,
|
||||
digest: digest.to_string(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// NOTE does NOT include the TTL field
|
||||
impl fmt::Display for DS {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
let Self {
|
||||
zone,
|
||||
_ttl,
|
||||
key_tag,
|
||||
algorithm,
|
||||
digest_type,
|
||||
digest,
|
||||
} = self;
|
||||
|
||||
write!(
|
||||
f,
|
||||
"{zone}\tIN\tDS\t{key_tag}\t{algorithm}\t{digest_type}\t{digest}"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct NS<'a> {
|
||||
pub zone: FQDN<'a>,
|
||||
pub nameserver: FQDN<'a>,
|
||||
|
@ -262,6 +433,41 @@ e.gtld-servers.net. IN A 192.12.94.30
|
|||
Ok(())
|
||||
}
|
||||
|
||||
// not quite roundtrip because we drop the TTL field when doing `to_string`
|
||||
#[test]
|
||||
fn ds_roundtrip() -> Result<()> {
|
||||
let input =
|
||||
". 1800 IN DS 31153 7 2 7846338aaacde9cc9518f1f450082adc015a207c45a1e69d6e660e6836f4ef3b";
|
||||
let ds: DS = input.parse()?;
|
||||
let output = ds.to_string();
|
||||
|
||||
let expected =
|
||||
". IN DS 31153 7 2 7846338aaacde9cc9518f1f450082adc015a207c45a1e69d6e660e6836f4ef3b";
|
||||
assert_eq!(expected, output);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dnskey_roundtrip() -> Result<()> {
|
||||
let input = "example.com. IN DNSKEY 256 3 7 AwEAAdIpMlio4GJas7GbIZ9xRpzpB2pf4SxBJcsquN/0yNBPGNE2rzcFykqMAKmLwypk1/1q/EdHVa4tQ5RlK0w09CRhgSXfCaph+yLNJKpiPyuVcXKl2k0RnO4p835sgVEUIvx8qGTDo7c7DA9UBje+/3ViFKqVhOBaWyT6gHAmNVpb ;{id = 24975 (zsk), size = 1024b}";
|
||||
|
||||
let dnskey: DNSKEY = input.parse()?;
|
||||
|
||||
assert_eq!(256, dnskey.flags);
|
||||
assert_eq!(3, dnskey.protocol);
|
||||
assert_eq!(7, dnskey.algorithm);
|
||||
let expected = "AwEAAdIpMlio4GJas7GbIZ9xRpzpB2pf4SxBJcsquN/0yNBPGNE2rzcFykqMAKmLwypk1/1q/EdHVa4tQ5RlK0w09CRhgSXfCaph+yLNJKpiPyuVcXKl2k0RnO4p835sgVEUIvx8qGTDo7c7DA9UBje+/3ViFKqVhOBaWyT6gHAmNVpb";
|
||||
assert_eq!(expected, dnskey.public_key);
|
||||
assert_eq!(1024, dnskey.bits());
|
||||
assert_eq!(24975, dnskey.key_tag());
|
||||
|
||||
let output = dnskey.to_string();
|
||||
assert!(input.starts_with(&output));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn example_a() -> Result<A<'static>> {
|
||||
Ok(A {
|
||||
fqdn: FQDN("e.gtld-servers.net.")?,
|
||||
|
|
Loading…
Reference in New Issue
Block a user