diff --git a/src/sign/mod.rs b/src/sign/mod.rs index b365a78f..e5f94a84 100644 --- a/src/sign/mod.rs +++ b/src/sign/mod.rs @@ -23,6 +23,7 @@ pub use self::bytes::{RsaSecretKeyBytes, SecretKeyBytes}; pub mod common; pub mod openssl; +pub mod records; pub mod ring; //----------- SigningKey ----------------------------------------------------- diff --git a/src/sign/records.rs b/src/sign/records.rs index 61697f0f..44380347 100644 --- a/src/sign/records.rs +++ b/src/sign/records.rs @@ -2,13 +2,15 @@ use core::convert::From; use core::fmt::Display; +use std::collections::HashMap; use std::fmt::Debug; +use std::hash::Hash; use std::string::String; use std::vec::Vec; use std::{fmt, io, slice}; use octseq::builder::{EmptyBuilder, FromBuilder, OctetsBuilder, Truncate}; -use octseq::{FreezeBuilder, OctetsFrom}; +use octseq::{FreezeBuilder, OctetsFrom, OctetsInto}; use crate::base::cmp::CanonicalOrd; use crate::base::iana::{Class, Nsec3HashAlg, Rtype}; @@ -20,11 +22,11 @@ use crate::rdata::dnssec::{ ProtoRrsig, RtypeBitmap, RtypeBitmapBuilder, Timestamp, }; use crate::rdata::nsec3::{Nsec3Salt, OwnerHash}; -use crate::rdata::{Dnskey, Ds, Nsec, Nsec3, Nsec3param, Rrsig}; +use crate::rdata::{Nsec, Nsec3, Nsec3param, Rrsig}; use crate::utils::base32; +use crate::validate::{nsec3_hash, Nsec3HashError}; -use super::key::SigningKey; -use super::ring::{nsec3_hash, Nsec3HashError}; +use super::{SignRaw, SigningKey}; //------------ SortedRecords ------------------------------------------------- @@ -75,19 +77,18 @@ impl SortedRecords { } #[allow(clippy::type_complexity)] - pub fn sign( + pub fn sign( &self, - apex: &FamilyName, + apex: &FamilyName, expiration: Timestamp, inception: Timestamp, - key: Key, - ) -> Result>>, Key::Error> + key: SigningKey, + ) -> Result>>, ErrorTypeToBeDetermined> where N: ToName + Clone, D: RecordData + ComposeRecordData, - Key: SigningKey, - Octets: From + AsRef<[u8]>, - ApexName: ToName + Clone, + ConcreteSecretKey: SignRaw, + Octets: AsRef<[u8]> + OctetsFrom>, { let mut res = Vec::new(); let mut buf = Vec::new(); @@ -148,12 +149,12 @@ impl SortedRecords { buf.clear(); let rrsig = ProtoRrsig::new( rrset.rtype(), - key.algorithm()?, + key.algorithm(), name.owner().rrsig_label_count(), rrset.ttl(), expiration, inception, - key.key_tag()?, + key.public_key().key_tag(), apex.owner().clone(), ); rrsig.compose_canonical(&mut buf).unwrap(); @@ -162,31 +163,34 @@ impl SortedRecords { } // Create and push the RRSIG record. + let signature = key.raw_secret_key().sign_raw(&buf).unwrap(); + let signature = signature.as_ref().to_vec(); + let Ok(signature) = signature.try_octets_into() else { + return Err(ErrorTypeToBeDetermined); + }; + res.push(Record::new( name.owner().clone(), name.class(), rrset.ttl(), - rrsig - .into_rrsig(key.sign(&buf)?.into()) - .expect("long signature"), + rrsig.into_rrsig(signature).expect("long signature"), )); } } Ok(res) } - pub fn nsecs( + pub fn nsecs( &self, - apex: &FamilyName, + apex: &FamilyName, ttl: Ttl, ) -> Vec>> where - N: ToName + Clone, + N: ToName + Clone + PartialEq, D: RecordData, Octets: FromBuilder, Octets::Builder: EmptyBuilder + Truncate + AsRef<[u8]> + AsMut<[u8]>, - ::AppendError: fmt::Debug, - ApexName: ToName, + ::AppendError: Debug, { let mut res = Vec::new(); @@ -240,8 +244,13 @@ impl SortedRecords { } let mut bitmap = RtypeBitmap::::builder(); - // Assume there’s gonna be an RRSIG. + // Assume there's gonna be an RRSIG. bitmap.add(Rtype::RRSIG).unwrap(); + if family.owner() == &apex_owner { + // Assume there's gonna be a DNSKEY. + bitmap.add(Rtype::DNSKEY).unwrap(); + } + bitmap.add(Rtype::NSEC).unwrap(); for rrset in family.rrsets() { bitmap.add(rrset.rtype()).unwrap() } @@ -275,10 +284,11 @@ impl SortedRecords { apex: &FamilyName, ttl: Ttl, params: Nsec3param, - opt_out: bool, + opt_out: Nsec3OptOut, + capture_hash_to_owner_mappings: bool, ) -> Result, Nsec3HashError> where - N: ToName + Clone + From> + Display, + N: ToName + Clone + From> + Display + Ord + Hash, N: From::Octets>>, D: RecordData, Octets: FromBuilder + OctetsFrom> + Clone + Default, @@ -289,6 +299,7 @@ impl SortedRecords { + AsMut<[u8]> + EmptyBuilder + FreezeBuilder, + ::Octets: AsRef<[u8]>, { // TODO: // - Handle name collisions? (see RFC 5155 7.1 Zone Signing) @@ -296,10 +307,23 @@ impl SortedRecords { // Reject old algorithms? if not, map 3 to 6 and 5 to 7, or reject // use of 3 and 5? + // RFC 5155 7.1 step 2: + // "If Opt-Out is being used, set the Opt-Out bit to one." + let mut nsec3_flags = params.flags(); + if matches!( + opt_out, + Nsec3OptOut::OptOut | Nsec3OptOut::OptOutFlagsOnly + ) { + // Set the Opt-Out flag. + nsec3_flags |= 0b0000_0001; + } + // RFC 5155 7.1 step 5: _"Sort the set of NSEC3 RRs into hash order." // We store the NSEC3s as we create them in a self-sorting vec. let mut nsec3s = SortedRecords::new(); + let mut ents = Vec::::new(); + // The owner name of a zone cut if we currently are at or below one. let mut cut: Option> = None; @@ -313,6 +337,13 @@ impl SortedRecords { let apex_owner = families.first_owner().clone(); let apex_label_count = apex_owner.iter_labels().count(); + let mut last_nent_stack: Vec = vec![]; + let mut nsec3_hash_map = if capture_hash_to_owner_mappings { + Some(HashMap::::new()) + } else { + None + }; + for family in families { // If the owner is out of zone, we have moved out of our zone and // are done. @@ -343,7 +374,7 @@ impl SortedRecords { // "If Opt-Out is being used, owner names of unsigned // delegations MAY be excluded." let has_ds = family.records().any(|rec| rec.rtype() == Rtype::DS); - if cut.is_some() && !has_ds && opt_out { + if cut.is_some() && !has_ds && opt_out == Nsec3OptOut::OptOut { continue; } @@ -352,9 +383,20 @@ impl SortedRecords { // the original owner name is greater than 1, additional NSEC3 // RRs need to be added for every empty non-terminal between // the apex and the original owner name." + let mut last_nent_distance_to_apex = 0; + let mut last_nent = None; + while let Some(this_last_nent) = last_nent_stack.pop() { + if name.owner().ends_with(&this_last_nent) { + last_nent_distance_to_apex = + this_last_nent.iter_labels().count() + - apex_label_count; + last_nent = Some(this_last_nent); + break; + } + } let distance_to_root = name.owner().iter_labels().count(); let distance_to_apex = distance_to_root - apex_label_count; - if distance_to_apex > 1 { + if distance_to_apex > last_nent_distance_to_apex { // Are there any empty nodes between this node and the apex? // The zone file records are already sorted so if all of the // parent labels had records at them, i.e. they were non-empty @@ -375,7 +417,8 @@ impl SortedRecords { // It will NOT construct the last name as that will be dealt // with in the next outer loop iteration. // - a.b.c.mail.example.com - for n in (1..distance_to_apex - 1).rev() { + let distance = distance_to_apex - last_nent_distance_to_apex; + for n in (1..=distance - 1).rev() { let rev_label_it = name.owner().iter_labels().skip(n); // Create next longest ENT name. @@ -386,22 +429,9 @@ impl SortedRecords { let name = builder.append_origin(&apex_owner).unwrap().into(); - // Create the type bitmap, empty for an ENT NSEC3. - let bitmap = RtypeBitmap::::builder(); - - let rec = Self::mk_nsec3( - &name, - params.hash_algorithm(), - params.flags(), - params.iterations(), - params.salt(), - &apex_owner, - bitmap, - ttl, - )?; - - // Store the record by order of its owner name. - let _ = nsec3s.insert(rec); + if let Err(pos) = ents.binary_search(&name) { + ents.insert(pos, name); + } } } @@ -423,18 +453,42 @@ impl SortedRecords { if distance_to_apex == 0 { bitmap.add(Rtype::NSEC3PARAM).unwrap(); + bitmap.add(Rtype::DNSKEY).unwrap(); } - // RFC 5155 7.1 step 2: - // "If Opt-Out is being used, set the Opt-Out bit to one." - let mut nsec3_flags = params.flags(); - if opt_out { - // Set the Opt-Out flag. - nsec3_flags |= 0b0000_0001; + let rec = Self::mk_nsec3( + name.owner(), + params.hash_algorithm(), + nsec3_flags, + params.iterations(), + params.salt(), + &apex_owner, + bitmap, + ttl, + )?; + + if let Some(nsec3_hash_map) = &mut nsec3_hash_map { + nsec3_hash_map + .insert(rec.owner().clone(), name.owner().clone()); + } + + // Store the record by order of its owner name. + if nsec3s.insert(rec).is_err() { + return Err(Nsec3HashError::CollisionDetected); } + if let Some(last_nent) = last_nent { + last_nent_stack.push(last_nent); + } + last_nent_stack.push(name.owner().clone()); + } + + for name in ents { + // Create the type bitmap, empty for an ENT NSEC3. + let bitmap = RtypeBitmap::::builder(); + let rec = Self::mk_nsec3( - name.owner(), + &name, params.hash_algorithm(), nsec3_flags, params.iterations(), @@ -444,7 +498,14 @@ impl SortedRecords { ttl, )?; - let _ = nsec3s.insert(rec); + if let Some(nsec3_hash_map) = &mut nsec3_hash_map { + nsec3_hash_map.insert(rec.owner().clone(), name); + } + + // Store the record by order of its owner name. + if nsec3s.insert(rec).is_err() { + return Err(Nsec3HashError::CollisionDetected); + } } // RFC 5155 7.1 step 7: @@ -484,9 +545,15 @@ impl SortedRecords { // "If a hash collision is detected, then a new salt has to be // chosen, and the signing process restarted." // - // TODO + // Handled above. - Ok(Nsec3Records::new(nsec3s.records, nsec3param)) + let res = Nsec3Records::new(nsec3s.records, nsec3param); + + if let Some(nsec3_hash_map) = nsec3_hash_map { + Ok(res.with_hashes(nsec3_hash_map)) + } else { + Ok(res) + } } pub fn write(&self, target: &mut W) -> Result<(), io::Error> @@ -495,9 +562,49 @@ impl SortedRecords { D: RecordData + fmt::Display, W: io::Write, { - for record in &self.records { - writeln!(target, "{}", record)?; + for record in self.records.iter().filter(|r| r.rtype() == Rtype::SOA) + { + writeln!(target, "{record}")?; } + + for record in self.records.iter().filter(|r| r.rtype() != Rtype::SOA) + { + writeln!(target, "{record}")?; + } + + Ok(()) + } + + pub fn write_with_comments( + &self, + target: &mut W, + comment_cb: F, + ) -> Result<(), io::Error> + where + N: fmt::Display, + D: RecordData + fmt::Display, + W: io::Write, + C: fmt::Display, + F: Fn(&Record) -> Option, + { + for record in self.records.iter().filter(|r| r.rtype() == Rtype::SOA) + { + if let Some(comment) = comment_cb(record) { + writeln!(target, "{record} ;{}", comment)?; + } else { + writeln!(target, "{record}")?; + } + } + + for record in self.records.iter().filter(|r| r.rtype() != Rtype::SOA) + { + if let Some(comment) = comment_cb(record) { + writeln!(target, "{record} ;{}", comment)?; + } else { + writeln!(target, "{record}")?; + } + } + Ok(()) } } @@ -578,7 +685,7 @@ impl SortedRecords { } } -impl Default for SortedRecords { +impl Default for SortedRecords { fn default() -> Self { Self::new() } @@ -623,21 +730,34 @@ where //------------ Nsec3Records --------------------------------------------------- -/// The set of records created by [`SortedRecords::nsec3s()`]. pub struct Nsec3Records { /// The NSEC3 records. - pub nsec3s: Vec>>, + pub recs: Vec>>, /// The NSEC3PARAM record. - pub nsec3param: Record>, + pub param: Record>, + + /// A map of hashes to owner names. + /// + /// For diagnostic purposes. None if not generated. + pub hashes: Option>, } impl Nsec3Records { pub fn new( - nsec3s: Vec>>, - nsec3param: Record>, + recs: Vec>>, + param: Record>, ) -> Self { - Self { nsec3s, nsec3param } + Self { + recs, + param, + hashes: None, + } + } + + pub fn with_hashes(mut self, hashes: HashMap) -> Self { + self.hashes = Some(hashes); + self } } @@ -719,30 +839,6 @@ impl FamilyName { { Record::new(self.owner.clone(), self.class, ttl, data) } - - pub fn dnskey>( - &self, - ttl: Ttl, - key: K, - ) -> Result>, K::Error> - where - N: Clone, - { - key.dnskey() - .map(|dnskey| self.clone().into_record(ttl, dnskey.convert())) - } - - pub fn ds( - &self, - ttl: Ttl, - key: K, - ) -> Result>, K::Error> - where - N: ToName + Clone, - { - key.ds(&self.owner) - .map(|ds| self.clone().into_record(ttl, ds)) - } } impl<'a, N: Clone> FamilyName<&'a N> { @@ -947,3 +1043,54 @@ where Some(Rrset::new(res)) } } + +//------------ ErrorTypeToBeDetermined --------------------------------------- + +#[derive(Debug)] +pub struct ErrorTypeToBeDetermined; + +//------------ Nsec3OptOut --------------------------------------------------- + +/// The different types of NSEC3 opt-out. +#[derive(Copy, Clone, Debug, Default, Eq, PartialEq)] +pub enum Nsec3OptOut { + /// No opt-out. The opt-out flag of NSEC3 RRs will NOT be set and insecure + /// delegations will be included in the NSEC3 chain. + #[default] + NoOptOut, + + /// Opt-out. The opt-out flag of NSEC3 RRs will be set and insecure + /// delegations will NOT be included in the NSEC3 chain. + OptOut, + + /// Opt-out (flags only). The opt-out flag of NSEC3 RRs will be set and + /// insecure delegations will be included in the NSEC3 chain. + OptOutFlagsOnly, +} + +// TODO: Add tests for nsec3s() that validate the following from RFC 5155: +// +// https://www.rfc-editor.org/rfc/rfc5155.html#section-7.1 +// 7.1. Zone Signing +// "Zones using NSEC3 must satisfy the following properties: +// +// o Each owner name within the zone that owns authoritative RRSets +// MUST have a corresponding NSEC3 RR. Owner names that correspond +// to unsigned delegations MAY have a corresponding NSEC3 RR. +// However, if there is not a corresponding NSEC3 RR, there MUST be +// an Opt-Out NSEC3 RR that covers the "next closer" name to the +// delegation. Other non-authoritative RRs are not represented by +// NSEC3 RRs. +// +// o Each empty non-terminal MUST have a corresponding NSEC3 RR, unless +// the empty non-terminal is only derived from an insecure delegation +// covered by an Opt-Out NSEC3 RR. +// +// o The TTL value for any NSEC3 RR SHOULD be the same as the minimum +// TTL value field in the zone SOA RR. +// +// o The Type Bit Maps field of every NSEC3 RR in a signed zone MUST +// indicate the presence of all types present at the original owner +// name, except for the types solely contributed by an NSEC3 RR +// itself. Note that this means that the NSEC3 type itself will +// never be present in the Type Bit Maps." diff --git a/src/validate.rs b/src/validate.rs index c806a48f..61249323 100644 --- a/src/validate.rs +++ b/src/validate.rs @@ -1703,6 +1703,9 @@ pub enum Nsec3HashError { /// /// See: [OwnerHashError](crate::rdata::nsec3::OwnerHashError) OwnerHashError, + + /// The hashing process produced a hash that already exists. + CollisionDetected, } /// Compute an [RFC 5155] NSEC3 hash using default settings.