parse RRSIG record & complete signed NS test

This commit is contained in:
Jorge Aparicio 2024-02-07 14:59:15 +01:00
parent 037cf4f698
commit 2bcad2a25c
3 changed files with 128 additions and 3 deletions

View File

@ -67,6 +67,7 @@ impl Recurse {
}
}
#[derive(Debug)]
pub struct DigOutput {
pub flags: DigFlags,
pub status: DigStatus,

View File

@ -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);

View File

@ -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<Self, Self::Err> {
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<RRSIG, Self> {
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<Self, Self::Err> {
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(())
}
}