Match ldns-signzone NSEC/NSEC3 RR ordering and NSEC3 formatting (#109)

This commit is contained in:
Jannik Peters
2025-08-08 12:38:36 +02:00
committed by GitHub
parent 5a4c09763f
commit 3f84c1b26c
+123 -20
View File
@@ -498,8 +498,8 @@ impl LdnsCommand for SignZone {
hash_only: false,
use_yyyymmddhhmmss_rrsig_format: true,
preceed_zone_with_hash_list,
order_rrsigs_after_the_rtype_they_cover: extra_comments,
order_nsec3_rrs_by_unhashed_owner_name: extra_comments,
order_rrsigs_after_the_rtype_they_cover: true,
order_nsec3_rrs_by_unhashed_owner_name: true,
zonefile_path,
key_paths,
invoked_as_ldns: true,
@@ -970,7 +970,11 @@ impl SignZone {
let mut nsec3_hashes: Option<Nsec3HashMap> = None;
if self.use_nsec3 && (self.extra_comments || self.preceed_zone_with_hash_list) {
if self.use_nsec3
&& (self.extra_comments
|| self.preceed_zone_with_hash_list
|| self.order_nsec3_rrs_by_unhashed_owner_name)
{
// Create a collection of NSEC3 hashes that can later be used for
// debug output.
let mut hash_provider = Nsec3HashMap::new();
@@ -1153,9 +1157,6 @@ impl SignZone {
// compatibility with ldns-signzone, re-order them to be in canonical
// order by unhashed owner name and so that hashed names come after
// equivalent unhashed names.
//
// INCOMAPATIBILITY WARNING: Unlike ldns-signzone, we only apply this
// ordering if `-b` is specified.
let mut owner_rrs;
let owner_rrs_iter: AnyOwnerRrsIter =
if self.order_nsec3_rrs_by_unhashed_owner_name && nsec3_hashes.is_some() {
@@ -1248,12 +1249,22 @@ impl SignZone {
// we skip that, and we skip RRSIGs as they are output only after
// the RRset that they cover.
if self.order_rrsigs_after_the_rtype_they_cover {
for rrset in owner_rrs
.rrsets()
.filter(|rrset| !matches!(rrset.rtype(), Rtype::SOA | Rtype::RRSIG))
{
for rrset in owner_rrs.rrsets().filter(|rrset| {
!(matches!(rrset.rtype(), Rtype::SOA | Rtype::RRSIG)
// If run as ldns-signzone we want to list the NSEC RR
// at the end of the RRset of the apex. By default,
// the NSEC RR would preceed the DNSKEY RRset, so we
// need to filter it out here to manually reinsert it
// later. This is only necessary for the NSEC RR at
// the apex, as the ordering issue doesn't appear at
// other locations than the apex.
|| (self.invoked_as_ldns
&& rrset.rtype() == Rtype::NSEC
&& rrset.owner() == apex))
}) {
for rr in rrset.iter() {
self.write_rr(&mut writer, rr)?;
match rr.data() {
ZoneRecordData::Nsec3(nsec3) if self.extra_comments => {
nsec3.comment(&mut writer, rr, nsec3_cs)?
@@ -1276,13 +1287,52 @@ impl SignZone {
for covering_rrsigs in owner_rrs
.rrsets()
.filter(|this_rrset| this_rrset.rtype() == Rtype::RRSIG)
.map(|this_rrset| this_rrset.iter().filter(|rr| matches!(rr.data(), ZoneRecordData::Rrsig(rrsig) if rrsig.type_covered() == rrset.rtype())))
.map(|this_rrset| {
this_rrset.iter().filter(|rr| {
matches!(rr.data(), ZoneRecordData::Rrsig(rrsig)
if rrsig.type_covered() == rrset.rtype()
&& if self.invoked_as_ldns && rr.owner() == apex {
// Withhold an RRSIG that covers the NSEC of the apex
// as we' reinserting them at the end of the apex' RRsets
rrsig.type_covered() != Rtype::NSEC
} else { true }
)
})
})
{
for covering_rrsig_rr in covering_rrsigs {
self.writeln_rr(&mut writer, covering_rrsig_rr)?;
}
}
}
// If running as ldns-signzone, we've been withholding the NSEC and NSEC's RRSIG at
// the apex above to reinsert them after all other RRsets at the apex. By default,
// the DNSKEY RRset and it's RRSIG would take the rear of the RRsets at the apex.
// This doesn't apply, if we're using NSEC3. Additionally, the NSEC RRs at other
// places than the apex do not have the ordering issue.
if self.invoked_as_ldns && !self.use_nsec3 && owner_rrs.owner() == apex {
if let Some(nsec_rrset) = owner_rrs
.rrsets()
.find(|this_rrset| this_rrset.rtype() == Rtype::NSEC)
{
self.writeln_rr(&mut writer, nsec_rrset.first())?;
}
if let Some(rrsig_rrset) = owner_rrs
.rrsets()
.find(|this_rrset| this_rrset.rtype() == Rtype::RRSIG)
{
for rr in rrsig_rrset.iter() {
if matches!(rr.data(), ZoneRecordData::Rrsig(rrsig) if rrsig.type_covered() == Rtype::NSEC)
{
self.writeln_rr(&mut writer, rr)?;
break;
}
}
}
}
if self.extra_comments {
writer.write_str(";\n")?;
}
@@ -1327,6 +1377,13 @@ impl SignZone {
}
}
if self.invoked_as_ldns {
if let ZoneRecordData::Nsec3(nsec3) = rr.data() {
let rr = Record::new(rr.owner(), rr.class(), rr.ttl(), LdnsNsec3(nsec3));
return writer.write_fmt(format_args!("{}", rr.display_zonefile(DISPLAY_KIND)));
}
}
writer.write_fmt(format_args!("{}", rr.display_zonefile(DISPLAY_KIND)))
}
@@ -2026,6 +2083,52 @@ impl<O, N> RecordData for YyyyMmDdHhMMSsRrsig<'_, O, N> {
}
}
//------------ LdnsNsec3 -----------------------------------------------------
/// A wrapper around Nsec3 to print the Nsec3 data in the exact format used by
/// ldns-signzone with all its quirks.
struct LdnsNsec3<'a, O>(&'a Nsec3<O>);
impl<O: AsRef<[u8]>> ZonefileFmt for LdnsNsec3<'_, O> {
fn fmt(&self, p: &mut impl Formatter) -> zonefile_fmt::Result {
// This block of code was copied from the `domain` crate impl of
// `Zonefilefmt` for domain::rdata::nsec3::Nsec3 and adapted for
// ldns output format.
p.block(|p| {
p.write_show(self.0.hash_algorithm())?;
p.write_token(self.0.flags())?;
p.write_comment(format_args!(
"flags: {}",
if self.0.opt_out() {
"opt-out"
} else {
"<none>"
}
))?;
p.write_token(self.0.iterations())?;
p.write_comment("iterations")?;
p.write_show(self.0.salt())?;
p.write_token(format!(
" {}",
domain::utils::base32::encode_display_hex(&self.0.next_owner())
.to_string()
.to_lowercase()
))?;
p.write_show(self.0.types())?;
// ldns-signzone ends its NSEC3 rtype bitmap with a trailing
// space. Adding an empty token, because the formatter will add
// a space as a delimiter.
p.write_token("")
})
}
}
impl<O> RecordData for LdnsNsec3<'_, O> {
fn rtype(&self) -> Rtype {
Rtype::NSEC3
}
}
//-------------- Nsec3HashMap ------------------------------------------------
#[derive(Debug)]
@@ -2420,8 +2523,8 @@ mod test {
hash_only: false,
use_yyyymmddhhmmss_rrsig_format: true,
preceed_zone_with_hash_list: false,
order_rrsigs_after_the_rtype_they_cover: false,
order_nsec3_rrs_by_unhashed_owner_name: false,
order_rrsigs_after_the_rtype_they_cover: true,
order_nsec3_rrs_by_unhashed_owner_name: true,
zonefile_path: PathBuf::from("example.org.zone"),
key_paths: Vec::from([PathBuf::from("anykey")]),
invoked_as_ldns: true,
@@ -2960,14 +3063,14 @@ m.root-servers.net.\t3600000\tIN\tAAAA\t2001:dc3::35
// (dnst) ldns-signzone -np -f - -e 20241127162422 -i 20241127162422 nsec3_optout1_example.org.zone ksk1 | grep NSEC3
let ldns_dnst_output_stripped: &str = "\
example.org.\t3600\tIN\tRRSIG\tNSEC3PARAM 15 2 3600 20241127162422 20241127162422 38873 example.org. 0XdDm1l2Mm8dyhtzbyQb91CmyNONs8lc9d22FUGvpjfqo8T2h0xs04x5MIfP0DjmiVnNqIyPK6sipnDqf6tCDg==\n\
example.org.\t3600\tIN\tNSEC3PARAM\t1 1 1 -\n\
example.org.\t3600\tIN\tRRSIG\tNSEC3PARAM 15 2 3600 20241127162422 20241127162422 38873 example.org. 0XdDm1l2Mm8dyhtzbyQb91CmyNONs8lc9d22FUGvpjfqo8T2h0xs04x5MIfP0DjmiVnNqIyPK6sipnDqf6tCDg==\n\
93u63bg57ppj6649al2n31l92iedkjd6.example.org.\t240\tIN\tNSEC3\t1 1 1 - k71ku6aicr5jpdjoe9j7cdnlk6d5c3ue A NS SOA RRSIG DNSKEY NSEC3PARAM \n\
93u63bg57ppj6649al2n31l92iedkjd6.example.org.\t240\tIN\tRRSIG\tNSEC3 15 3 240 20241127162422 20241127162422 38873 example.org. z4ceUmbSZiSnluFj8CDJ7B9fukCR2flTWgca4GE2xrw48+fiieH/04xCKhJmDRJUJTVkKtIYpB4p0Q4m60M1Cg==\n\
93u63bg57ppj6649al2n31l92iedkjd6.example.org.\t240\tIN\tNSEC3\t1 1 1 - K71KU6AICR5JPDJOE9J7CDNLK6D5C3UE A NS SOA RRSIG DNSKEY NSEC3PARAM\n\
k71ku6aicr5jpdjoe9j7cdnlk6d5c3ue.example.org.\t240\tIN\tNSEC3\t1 1 1 - ojicmhri4vp8po7h2kvej99sklqnj5p2 NS \n\
k71ku6aicr5jpdjoe9j7cdnlk6d5c3ue.example.org.\t240\tIN\tRRSIG\tNSEC3 15 3 240 20241127162422 20241127162422 38873 example.org. HUrf7tOm3simXqpZj1oZeKX/P3eWoTTKc3fsyqfuLD6sGssXrBfpv1/LINBR9eEBjJ9rFbQXILgweS6huBL/Ag==\n\
k71ku6aicr5jpdjoe9j7cdnlk6d5c3ue.example.org.\t240\tIN\tNSEC3\t1 1 1 - OJICMHRI4VP8PO7H2KVEJ99SKLQNJ5P2 NS\n\
ojicmhri4vp8po7h2kvej99sklqnj5p2.example.org.\t240\tIN\tNSEC3\t1 1 1 - 93u63bg57ppj6649al2n31l92iedkjd6 NS DS RRSIG \n\
ojicmhri4vp8po7h2kvej99sklqnj5p2.example.org.\t240\tIN\tRRSIG\tNSEC3 15 3 240 20241127162422 20241127162422 38873 example.org. NG/8jk3UHht1ZYNEjUZ4swaEHea1amF4l3jZ893oARi95oxtPVLKoinVbBbfVuoanicOgeZxUPpKWHMBR12XDA==\n\
ojicmhri4vp8po7h2kvej99sklqnj5p2.example.org.\t240\tIN\tNSEC3\t1 1 1 - 93U63BG57PPJ6649AL2N31L92IEDKJD6 NS DS RRSIG\n\
";
let res = FakeCmd::new([
@@ -2996,14 +3099,14 @@ m.root-servers.net.\t3600000\tIN\tAAAA\t2001:dc3::35
fn ldns_signzone_disables_minus_b_when_output_is_to_stdout() {
let expected_output = r###"example.org.\t239\tIN\tSOA\texample.net. hostmaster.example.net. 1234567890 28800 7200 604800 238
example.org.\t239\tIN\tRRSIG\tSOA 8 2 239 20241127162422 20241127162422 51331 example.org. XD5+Exk0KLfvLYA7y+Qs6jhF+JeESFONqZAjkSvznXdjod80W6cv9C77XeHqqod+5glGHlw9bXmVhuJ/5n056BbnDcMWF+AV4taFc/RrDcZb5A0tS6LnRWbpO9puKeLVK10FeAChCygct6/+GNiE12DDLnzKJFuyMuu+nLa2p88=
example.org.\t238\tIN\tRRSIG\tNSEC 8 2 238 20241127162422 20241127162422 51331 example.org. AT4PDLEolpApcrYi7mcTXrqCQ6psXeZNdmFub08m6BJRs2jeW07fM11Amft53FXKgqbT23WILkEM7Raai8E8qPJoSdDCys6zYXW/NCU9Cf/oXIKdD4nxQXXWbnX4GCMN4XJy382dYnxTDssQK6lNIKKi4OvGYIxVUPthaLKJFU0=
example.org.\t239\tIN\tDNSKEY\t257 3 8 AwEAAckp/oMmocs+pv4KsCkCciazIl2+SohAZ2/bH2viAMg3tHAPjw5YfPNErUBqMGvN4c23iBCnt9TktT5bVoQdpXyCJ+ZwmWrFxlXvXIqG8rpkwHi1xFoXWVZLrG9XYCqLVMq2cB+FgMIaX504XMGk7WQydtV1LAqLgP3B8JA2Fc1j ;{id = 51331 (ksk), size = 1024b}
example.org.\t239\tIN\tRRSIG\tDNSKEY 8 2 239 20241127162422 20241127162422 51331 example.org. rLwqlu9fYkzAy0jM9crtw5du4rUaDVH9PI4m06lRwjSKhu1VQ1AHjRhlKy1OgUee/5LovXSRGcgNZi4wiTS5ZULTJw7UQTBRXaaNhVACENX/MoVw9SmYuDSTyvQboChmFmYSMch3Q/02VhgN+BT8F7+OdDVgsWqZUEKPVNixk/0=
example.org.\t238\tIN\tNSEC\tsome.example.org. SOA RRSIG NSEC DNSKEY
example.org.\t239\tIN\tDNSKEY\t257 3 8 AwEAAckp/oMmocs+pv4KsCkCciazIl2+SohAZ2/bH2viAMg3tHAPjw5YfPNErUBqMGvN4c23iBCnt9TktT5bVoQdpXyCJ+ZwmWrFxlXvXIqG8rpkwHi1xFoXWVZLrG9XYCqLVMq2cB+FgMIaX504XMGk7WQydtV1LAqLgP3B8JA2Fc1j ;{id = 51331 (ksk), size = 1024b}
example.org.\t238\tIN\tRRSIG\tNSEC 8 2 238 20241127162422 20241127162422 51331 example.org. AT4PDLEolpApcrYi7mcTXrqCQ6psXeZNdmFub08m6BJRs2jeW07fM11Amft53FXKgqbT23WILkEM7Raai8E8qPJoSdDCys6zYXW/NCU9Cf/oXIKdD4nxQXXWbnX4GCMN4XJy382dYnxTDssQK6lNIKKi4OvGYIxVUPthaLKJFU0=
some.example.org.\t240\tIN\tA\t1.2.3.4
some.example.org.\t240\tIN\tRRSIG\tA 8 3 240 20241127162422 20241127162422 51331 example.org. xdVbhbaMXEyMySCOKy2yYQgU2URAOnu+jLU5py+4R8R3yVVvdl6yMjzdUD3vyxprHitJ+xLrXU/wHSQvtjSwmxVL53ztu+9wrnrhQm6nqXGLW+iw58LepdLVRlppz2WlV0CJAlLIQPJ8rw4hND3NYLJojnO8OdrgpHL89ajD4II=
some.example.org.\t238\tIN\tRRSIG\tNSEC 8 3 238 20241127162422 20241127162422 51331 example.org. PP4tH4Y6JNymWSJebPd3zjvDrjyZXVBF8QTKxKAmbmtPacbWyIcRuI0L8+8Z1folAN2U5cUZmCaIbt5Ylaj6ab4UAYHiy0BrcF/zbNIeLRSTz4hOteencIooTDvqIqYuI9/xTVXcfJ+gVzzlIh2dJK2GW5O4+B1xR+CINLNJ/j8=
some.example.org.\t238\tIN\tNSEC\texample.org. A RRSIG NSEC
some.example.org.\t238\tIN\tRRSIG\tNSEC 8 3 238 20241127162422 20241127162422 51331 example.org. PP4tH4Y6JNymWSJebPd3zjvDrjyZXVBF8QTKxKAmbmtPacbWyIcRuI0L8+8Z1folAN2U5cUZmCaIbt5Ylaj6ab4UAYHiy0BrcF/zbNIeLRSTz4hOteencIooTDvqIqYuI9/xTVXcfJ+gVzzlIh2dJK2GW5O4+B1xR+CINLNJ/j8=
"###.replace("\\t", "\t");
let zone_file_path =