From 2bcad2a25c1027a90b179e7402a5f0afdb7c39aa Mon Sep 17 00:00:00 2001 From: Jorge Aparicio Date: Wed, 7 Feb 2024 14:59:15 +0100 Subject: [PATCH] parse RRSIG record & complete signed NS test --- src/client.rs | 1 + src/name_server.rs | 11 ++++- src/record.rs | 119 ++++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 128 insertions(+), 3 deletions(-) diff --git a/src/client.rs b/src/client.rs index 0a9fb1e2..c40eb33d 100644 --- a/src/client.rs +++ b/src/client.rs @@ -67,6 +67,7 @@ impl Recurse { } } +#[derive(Debug)] pub struct DigOutput { pub flags: DigFlags, pub status: DigStatus, diff --git a/src/name_server.rs b/src/name_server.rs index 61401158..c757cba5 100644 --- a/src/name_server.rs +++ b/src/name_server.rs @@ -359,7 +359,6 @@ mod tests { } #[test] - #[ignore = "FIXME need to parse RRSIG record in dig's output"] fn signed() -> Result<()> { let tld_ns = NameServer::new(FQDN::ROOT)?.sign()?; @@ -382,6 +381,15 @@ mod tests { assert!(output.status.is_noerror()); + let [soa, rrsig] = output + .answer + .try_into() + .expect("two records in answer section"); + + assert!(soa.is_soa()); + let rrsig = rrsig.try_into_rrsig().unwrap(); + assert_eq!(RecordType::SOA, rrsig.type_covered); + Ok(()) } @@ -392,6 +400,7 @@ mod tests { 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); diff --git a/src/record.rs b/src/record.rs index cc4edbfe..0f6885ac 100644 --- a/src/record.rs +++ b/src/record.rs @@ -8,6 +8,7 @@ use std::net::Ipv4Addr; use crate::{Error, Result, FQDN}; #[allow(clippy::upper_case_acronyms)] +#[derive(Debug, PartialEq)] pub enum RecordType { A, NS, @@ -24,10 +25,26 @@ impl RecordType { } } +impl FromStr for RecordType { + type Err = Error; + + fn from_str(input: &str) -> CoreResult { + let record_type = match input { + "A" => Self::A, + "SOA" => Self::SOA, + "NS" => Self::NS, + _ => return Err(format!("unknown record type: {input}").into()), + }; + + Ok(record_type) + } +} + #[derive(Debug)] #[allow(clippy::upper_case_acronyms)] pub enum Record { A(A), + RRSIG(RRSIG), SOA(SOA), } @@ -39,6 +56,18 @@ impl Record { Err(self) } } + + pub fn try_into_rrsig(self) -> CoreResult { + if let Self::RRSIG(v) = self { + Ok(v) + } else { + Err(self) + } + } + + pub fn is_soa(&self) -> bool { + matches!(self, Self::SOA(..)) + } } impl FromStr for Record { @@ -53,6 +82,7 @@ impl FromStr for Record { let record = match record_type { "A" => Record::A(input.parse()?), "NS" => todo!(), + "RRSIG" => Record::RRSIG(input.parse()?), "SOA" => Record::SOA(input.parse()?), _ => return Err(format!("unknown record type: {record_type}").into()), }; @@ -80,8 +110,11 @@ impl FromStr for A { return Err("expected 5 columns".into()); }; - if record_type != "A" { - return Err(format!("tried to parse `{record_type}` record as an A record").into()); + let expected = "A"; + if record_type != expected { + return Err( + format!("tried to parse `{record_type}` record as an {expected} record").into(), + ); } if class != "IN" { @@ -96,6 +129,67 @@ impl FromStr for A { } } +#[allow(clippy::upper_case_acronyms)] +#[derive(Debug)] +pub struct RRSIG { + pub fqdn: FQDN<'static>, + pub ttl: u32, + pub type_covered: RecordType, + pub algorithm: u32, + pub labels: u32, + pub original_ttl: u32, + pub signature_expiration: u64, + pub signature_inception: u64, + pub key_tag: u32, + pub signer_name: FQDN<'static>, + /// base64 encoded + pub signature: String, +} + +impl FromStr for RRSIG { + type Err = Error; + + fn from_str(input: &str) -> CoreResult { + let mut columns = input.split_whitespace(); + + let [Some(fqdn), Some(ttl), Some(class), Some(record_type), Some(type_covered), Some(algorithm), Some(labels), Some(original_ttl), Some(signature_expiration), Some(signature_inception), Some(key_tag), Some(signer_name)] = + array::from_fn(|_| columns.next()) + else { + return Err("expected at least 12 columns".into()); + }; + + let expected = "RRSIG"; + if record_type != expected { + return Err( + format!("tried to parse `{record_type}` record as a {expected} record").into(), + ); + } + + if class != "IN" { + return Err(format!("unknown class: {class}").into()); + } + + let mut signature = String::new(); + for column in columns { + signature.push_str(column); + } + + Ok(Self { + fqdn: fqdn.parse()?, + ttl: ttl.parse()?, + type_covered: type_covered.parse()?, + algorithm: algorithm.parse()?, + labels: labels.parse()?, + original_ttl: original_ttl.parse()?, + signature_expiration: signature_expiration.parse()?, + signature_inception: signature_inception.parse()?, + key_tag: key_tag.parse()?, + signer_name: signer_name.parse()?, + signature, + }) + } +} + #[allow(clippy::upper_case_acronyms)] #[derive(Debug)] pub struct SOA { @@ -178,4 +272,25 @@ mod tests { Ok(()) } + + #[test] + fn can_parse_rrsig_record() -> Result<()> { + let input = ". 1800 IN RRSIG SOA 7 0 1800 20240306132701 20240207132701 11264 . wXpRU4elJPGYm2kgVVsIwGf1IkYJcQ3UE4mwmItWdxj0XWSWY07MO4Ll DMJgsE0u64Q/345Ck7+aQ904uLebwCvpFnsmkyCxk82XIAfHN9FiwzSy qoR/zZEvBONaej3vrvsqPwh8q/pvypLft9647HcFdwY0juzZsbrAaDAX 8WY="; + + let rrsig: RRSIG = input.parse()?; + + assert_eq!(FQDN::ROOT, rrsig.fqdn); + assert_eq!(1800, rrsig.ttl); + assert_eq!(RecordType::SOA, rrsig.type_covered); + assert_eq!(7, rrsig.algorithm); + assert_eq!(0, rrsig.labels); + assert_eq!(20240306132701, rrsig.signature_expiration); + assert_eq!(20240207132701, rrsig.signature_inception); + assert_eq!(11264, rrsig.key_tag); + assert_eq!(FQDN::ROOT, rrsig.signer_name); + let expected = "wXpRU4elJPGYm2kgVVsIwGf1IkYJcQ3UE4mwmItWdxj0XWSWY07MO4LlDMJgsE0u64Q/345Ck7+aQ904uLebwCvpFnsmkyCxk82XIAfHN9FiwzSyqoR/zZEvBONaej3vrvsqPwh8q/pvypLft9647HcFdwY0juzZsbrAaDAX8WY="; + assert_eq!(expected, rrsig.signature); + + Ok(()) + } }