Skip to main content

walnut_dns/authority/dnssec/
mod.rs

1use std::collections::{BTreeSet, HashMap, HashSet};
2use std::fmt;
3use std::ops::{Deref, DerefMut};
4use std::sync::Arc;
5
6use chrono::Utc;
7use hickory_proto::ProtoError;
8use hickory_proto::dnssec::rdata::{DNSKEY, DNSSECRData, KEY, NSEC, NSEC3, NSEC3PARAM, RRSIG, SIG};
9use hickory_proto::dnssec::{DnsSecError, DnsSecResult, Nsec3HashAlgorithm, SigSigner, TBS};
10use hickory_proto::rr::{DNSClass, LowerName, RData, RecordType, RrKey};
11use hickory_server::authority::{AuthLookup, Authority, UpdateRequest as _};
12use hickory_server::authority::{LookupControlFlow, LookupError};
13use hickory_server::authority::{LookupObject, LookupOptions, LookupRecords};
14use hickory_server::authority::{MessageRequest, Nsec3QueryInfo, UpdateResult};
15use hickory_server::{dnssec::NxProofKind, server::RequestInfo};
16
17use super::{Lookup, ZoneAuthority, ZoneInfo};
18use crate::catalog::CatalogError;
19use crate::rr::{AsHickory as _, Mismatch, Name, Record, RecordSet, TimeToLive};
20
21mod authorize;
22mod prerequisites;
23mod update;
24
25/// Trait for journaling DNS zone operations
26///
27/// The Journal trait provides an interface for recording DNS operations
28/// such as record insertions and zone updates. This is useful for auditing,
29/// replication, and maintaining operation history.
30#[async_trait::async_trait]
31pub trait Journal<Z> {
32    /// Record the insertion of DNS records
33    ///
34    /// Logs the insertion of one or more DNS records into the specified zone.
35    /// This is typically called during DNS UPDATE operations.
36    ///
37    /// # Arguments
38    ///
39    /// * `zone` - The zone where records are being inserted
40    /// * `records` - The records being inserted
41    ///
42    /// # Returns
43    ///
44    /// Success or an error if the operation cannot be journaled
45    ///
46    /// # Errors
47    ///
48    /// Returns an error if the journal backend fails
49    async fn insert_records(&self, zone: &Z, records: &[Record]) -> Result<(), CatalogError>;
50
51    /// Record the update of a DNS zone
52    ///
53    /// Logs the complete state of a zone after an update operation.
54    /// This creates a checkpoint that can be used for recovery.
55    ///
56    /// # Arguments
57    ///
58    /// * `zone` - The zone being updated
59    ///
60    /// # Returns
61    ///
62    /// Success or an error if the operation cannot be journaled
63    ///
64    /// # Errors
65    ///
66    /// Returns an error if the journal backend fails
67    async fn upsert_zone(&self, zone: &Z) -> Result<(), CatalogError>;
68}
69
70/// Error type for DNSSEC zone operations
71///
72/// This error type unifies various errors that can occur during DNSSEC zone
73/// operations, including record mismatches and cryptographic errors.
74#[derive(Debug, thiserror::Error)]
75pub enum DnsSecZoneError {
76    /// Record mismatch error (e.g., name/type mismatch)
77    #[error(transparent)]
78    Mismatch(#[from] Mismatch),
79
80    /// DNSSEC cryptographic error
81    #[error(transparent)]
82    DnsSec(#[from] DnsSecError),
83}
84
85/// DNSSEC-enabled DNS zone authority
86///
87/// DNSSecZone wraps a regular zone authority and adds DNSSEC capabilities,
88/// including automatic zone signing, NSEC/NSEC3 generation, and DNS UPDATE
89/// authorization using cryptographic signatures.
90///
91/// The zone supports:
92/// - Automatic DNSSEC signing with configured keys
93/// - NSEC and NSEC3 non-existence proofs
94/// - SIG(0) authenticated DNS updates
95/// - Operation journaling for auditing and recovery
96/// - Configurable DNSSEC settings
97#[derive(Clone)]
98pub struct DNSSecZone<Z> {
99    zone: ZoneAuthority<Z>,
100    secure_keys: Vec<Arc<SigSigner>>,
101    nx_proof_kind: Option<NxProofKind>,
102    allow_update: bool,
103    is_dnssec_enabled: bool,
104    journal: Option<Arc<dyn Journal<Self> + Send + Sync + 'static>>,
105}
106
107impl<Z> From<Z> for DNSSecZone<Z>
108where
109    Z: ZoneInfo,
110{
111    fn from(value: Z) -> Self {
112        DNSSecZone::new(value)
113    }
114}
115
116impl<Z> Deref for DNSSecZone<Z> {
117    type Target = ZoneAuthority<Z>;
118
119    fn deref(&self) -> &Self::Target {
120        &self.zone
121    }
122}
123
124impl<Z> DerefMut for DNSSecZone<Z> {
125    fn deref_mut(&mut self) -> &mut Self::Target {
126        &mut self.zone
127    }
128}
129
130impl<Z> AsRef<Z> for DNSSecZone<Z> {
131    fn as_ref(&self) -> &Z {
132        &self.zone
133    }
134}
135
136impl<Z: ZoneInfo> fmt::Debug for DNSSecZone<Z> {
137    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
138        f.debug_struct("DNSSecZone")
139            .field("zone", &self.zone.origin())
140            .field("secure_keys", &self.secure_keys.len())
141            .field("nx_proof_kind", &self.nx_proof_kind)
142            .finish()
143    }
144}
145
146impl<Z> DNSSecZone<Z>
147where
148    Z: ZoneInfo,
149{
150    /// Create a new DNSSEC zone from a regular zone
151    ///
152    /// Wraps an existing zone with DNSSEC capabilities. The zone starts
153    /// with DNSSEC enabled but no signing keys configured.
154    ///
155    /// # Arguments
156    ///
157    /// * `zone` - The zone to wrap with DNSSEC capabilities
158    ///
159    /// # Returns
160    ///
161    /// A new DNSSEC-enabled zone
162    pub fn new(zone: Z) -> Self {
163        Self {
164            zone: ZoneAuthority::new(zone),
165            secure_keys: Vec::new(),
166            nx_proof_kind: None,
167            allow_update: false,
168            is_dnssec_enabled: true,
169            journal: None,
170        }
171    }
172
173    /// Get the non-existence proof type for this zone
174    ///
175    /// Returns the type of non-existence proof (NSEC or NSEC3) used by this zone,
176    /// or None if no non-existence proofs are configured.
177    ///
178    /// # Returns
179    ///
180    /// The non-existence proof configuration
181    pub fn nx_proof_kind(&self) -> Option<&NxProofKind> {
182        self.nx_proof_kind.as_ref()
183    }
184
185    /// Set the non-existence proof type for this zone
186    ///
187    /// Configures the type of non-existence proof to use. NSEC provides
188    /// simple non-existence proofs, while NSEC3 provides hashed proofs
189    /// that don't reveal zone contents.
190    ///
191    /// # Arguments
192    ///
193    /// * `kind` - The non-existence proof type to use
194    ///
195    /// # Returns
196    ///
197    /// A mutable reference to this zone for method chaining
198    pub fn set_nx_proof_kind(&mut self, kind: Option<NxProofKind>) -> &mut Self {
199        self.nx_proof_kind = kind;
200        self
201    }
202
203    /// Check if DNSSEC is enabled for this zone
204    ///
205    /// Returns whether DNSSEC signing and validation is enabled.
206    /// When disabled, the zone behaves like a regular DNS zone.
207    ///
208    /// # Returns
209    ///
210    /// `true` if DNSSEC is enabled
211    pub fn dnssec_enabled(&self) -> bool {
212        self.is_dnssec_enabled
213    }
214
215    /// Enable or disable DNSSEC for this zone
216    ///
217    /// Controls whether the zone performs DNSSEC signing and validation.
218    /// When disabled, the zone operates as a regular DNS zone.
219    ///
220    /// # Arguments
221    ///
222    /// * `enabled` - Whether to enable DNSSEC
223    ///
224    /// # Returns
225    ///
226    /// A mutable reference to this zone for method chaining
227    pub fn set_dnssec_enabled(&mut self, enabled: bool) -> &mut Self {
228        self.is_dnssec_enabled = enabled;
229        self
230    }
231
232    /// Check if DNS updates are allowed for this zone
233    ///
234    /// Returns whether this zone accepts DNS UPDATE requests.
235    /// Updates must be cryptographically authenticated when enabled.
236    ///
237    /// # Returns
238    ///
239    /// `true` if updates are allowed
240    pub fn allow_update(&self) -> bool {
241        self.allow_update
242    }
243
244    /// Enable or disable DNS updates for this zone
245    ///
246    /// Controls whether the zone accepts DNS UPDATE requests.
247    /// When enabled, updates must be cryptographically authenticated
248    /// using SIG(0) signatures.
249    ///
250    /// # Arguments
251    ///
252    /// * `allow_update` - Whether to allow updates
253    ///
254    /// # Returns
255    ///
256    /// A mutable reference to this zone for method chaining
257    pub fn set_allow_update(&mut self, allow_update: bool) -> &mut Self {
258        self.allow_update = allow_update;
259        self
260    }
261
262    /// Set a journal for recording DNS operations
263    ///
264    /// Configures a journal to record DNS operations for auditing
265    /// and recovery purposes. The journal will log all record insertions
266    /// and zone updates.
267    ///
268    /// # Arguments
269    ///
270    /// * `journal` - The journal implementation to use
271    ///
272    /// # Returns
273    ///
274    /// A mutable reference to this zone for method chaining
275    pub fn set_journal<J>(&mut self, journal: J) -> &mut Self
276    where
277        J: Journal<Self> + Send + Sync + 'static,
278    {
279        self.journal = Some(Arc::new(journal));
280        self
281    }
282
283    /// Get the signing keys configured for this zone
284    ///
285    /// Returns a slice of all DNSSEC signing keys that have been
286    /// added to this zone. These keys are used for zone signing.
287    ///
288    /// # Returns
289    ///
290    /// A slice of signing keys
291    pub fn secure_keys(&self) -> &[Arc<SigSigner>] {
292        &self.secure_keys
293    }
294
295    /// Persist the current zone state to the journal
296    ///
297    /// Records the complete current state of the zone to the journal
298    /// if one is configured. This creates a checkpoint for recovery.
299    ///
300    /// # Returns
301    ///
302    /// Success or an error if journaling fails
303    ///
304    /// # Errors
305    ///
306    /// Returns an error if the journal backend fails
307    pub async fn persist_to_journal(&self) -> Result<(), CatalogError> {
308        if let Some(journal) = self.journal.as_ref() {
309            journal.upsert_zone(self).await?;
310        }
311
312        Ok(())
313    }
314}
315
316/// DNSSEC helper functions
317impl<Z> DNSSecZone<Z>
318where
319    Z: ZoneInfo + Lookup,
320{
321    fn get_hashed_owner_name(
322        &self,
323        info: &Nsec3QueryInfo<'_>,
324        name: &LowerName,
325    ) -> Result<LowerName, ProtoError> {
326        let hash = info.algorithm.hash(info.salt, name, info.iterations)?;
327        let label = data_encoding::BASE32_DNSSEC.encode(hash.as_ref());
328        Ok(LowerName::new(&self.origin().prepend_label(label)?))
329    }
330
331    fn proof(&self, info: Nsec3QueryInfo<'_>) -> Result<Vec<RecordSet>, LookupError> {
332        let Nsec3QueryInfo {
333            qname,
334            qtype,
335            has_wildcard_match,
336            ..
337        } = info;
338
339        let rr_key = RrKey::new(
340            self.get_hashed_owner_name(&info, self.origin())?,
341            RecordType::NSEC3,
342        );
343        let qname_match = self.get(&rr_key);
344
345        if has_wildcard_match {
346            // - Wildcard answer response.
347            let closest_encloser_name = self.closest_encloser_proof(qname, &info)?;
348            let Some((closest_encloser_name, _)) = closest_encloser_name else {
349                return Ok(vec![]);
350            };
351
352            let cover = self.find_cover(&closest_encloser_name, &info)?;
353            return Ok(cover.map_or_else(Vec::new, |rr_set| vec![rr_set]));
354        }
355
356        if let Some(rr_set) = qname_match {
357            // - No data response if the QTYPE is not DS.
358            // - No data response if the QTYPE is DS and there is an NSEC3 record matching QNAME.
359            return Ok(vec![rr_set.clone()]);
360        }
361
362        // - Name error response.
363        // - No data response if QTYPE is DS and there is not an NSEC3 record matching QNAME.
364        // - Wildcard no data response.
365        let mut records = Vec::new();
366        let (next_closer_name, closest_encloser_match) =
367            self.closest_encloser_proof(qname, &info)?.unzip();
368        if let Some(cover) = closest_encloser_match {
369            records.push(cover);
370        }
371
372        let Some(next_closer_name) = next_closer_name else {
373            return Ok(records);
374        };
375
376        if let Some(cover) = self.find_cover(&next_closer_name, &info)? {
377            records.push(cover);
378        }
379
380        let wildcard_match = {
381            let wildcard = qname.clone().into_wildcard();
382            self.keys().any(|rr_key| rr_key.name == wildcard)
383        };
384
385        if wildcard_match {
386            let wildcard_at_closest_encloser = next_closer_name.into_wildcard();
387            let rr_key = RrKey::new(
388                self.get_hashed_owner_name(&info, &wildcard_at_closest_encloser)?,
389                RecordType::NSEC3,
390            );
391
392            if let Some(record) = self.get(&rr_key) {
393                records.push(record.clone());
394            }
395        } else if qtype != RecordType::DS {
396            let wildcard_at_closest_encloser = next_closer_name.into_wildcard();
397            if let Some(cover) = self.find_cover(&wildcard_at_closest_encloser, &info)? {
398                records.push(cover);
399            }
400        }
401
402        records.sort_by(|a, b| a.name().cmp(b.name()));
403        records.dedup_by(|a, b| a.name() == b.name());
404        Ok(records)
405    }
406
407    fn closest_encloser_proof(
408        &self,
409        name: &LowerName,
410        info: &Nsec3QueryInfo<'_>,
411    ) -> Result<Option<(LowerName, RecordSet)>, ProtoError> {
412        let mut next_closer_name = name.clone();
413        let mut closest_encloser = next_closer_name.base_name();
414
415        while !closest_encloser.is_root() {
416            let rr_key = RrKey::new(
417                self.get_hashed_owner_name(info, &closest_encloser)?,
418                RecordType::NSEC3,
419            );
420            if let Some(rrs) = self.get(&rr_key) {
421                return Ok(Some((next_closer_name, rrs.clone())));
422            }
423
424            next_closer_name = next_closer_name.base_name();
425            closest_encloser = closest_encloser.base_name();
426        }
427
428        Ok(None)
429    }
430
431    fn find_cover(
432        &self,
433        name: &LowerName,
434        info: &Nsec3QueryInfo<'_>,
435    ) -> Result<Option<RecordSet>, ProtoError> {
436        let owner_name = self.get_hashed_owner_name(info, name)?;
437        let records = self
438            .records()
439            .filter(|rr_set| rr_set.record_type() == RecordType::NSEC3);
440
441        // Find the record with the largest owner name such that its owner name is before the
442        // hashed QNAME. If this record exist, it already covers QNAME. Otherwise, the QNAME
443        // preceeds all the existing NSEC3 records' owner names, meaning that it is covered by
444        // the NSEC3 record with the largest owner name.
445        Ok(records
446            .filter(|rr_set| rr_set.record_type() == RecordType::NSEC3)
447            .filter(|rr_set| rr_set.name() < &*owner_name)
448            .max_by_key(|rr_set| rr_set.name())
449            .or_else(|| {
450                self.records()
451                    .filter(|rr_set| rr_set.record_type() == RecordType::NSEC3)
452                    .max_by_key(|rr_set| rr_set.name())
453            })
454            .cloned())
455    }
456
457    fn closest_nsec(&self, name: &LowerName) -> Option<RecordSet> {
458        for rr_set in self.records_reversed() {
459            if rr_set.record_type() != RecordType::NSEC {
460                continue;
461            }
462
463            if *name < rr_set.name().into() {
464                continue;
465            }
466
467            // there should only be one record
468            let Some(record) = rr_set.records().next() else {
469                continue;
470            };
471
472            let RData::DNSSEC(DNSSECRData::NSEC(nsec)) = record.rdata() else {
473                continue;
474            };
475
476            let next_domain_name = nsec.next_domain_name();
477            // the search name is less than the next NSEC record
478            if *name < next_domain_name.into() ||
479                // this is the last record, and wraps to the beginning of the zone
480                next_domain_name < rr_set.name()
481            {
482                return Some(rr_set.clone());
483            }
484        }
485
486        None
487    }
488
489    fn nsec_zone(&mut self) {
490        // only create nsec records for secure zones
491
492        use std::mem;
493        if self.secure_keys.is_empty() {
494            return;
495        }
496        tracing::debug!("generating nsec records: {}", self.origin());
497
498        // first remove all existing nsec records
499        let delete_keys: Vec<RrKey> = self
500            .zone
501            .keys()
502            .filter(|k| k.record_type == RecordType::NSEC)
503            .cloned()
504            .collect();
505
506        for key in delete_keys {
507            self.zone.remove(&key);
508        }
509
510        // now go through and generate the nsec records
511        let ttl = self.minimum_ttl();
512        let serial = self.serial();
513        let mut records: Vec<Record> = vec![];
514
515        {
516            let mut nsec_info: Option<(&hickory_proto::rr::Name, BTreeSet<RecordType>)> = None;
517            for key in self.zone.keys() {
518                match &mut nsec_info {
519                    None => nsec_info = Some((&key.name, BTreeSet::from([key.record_type]))),
520                    Some((name, vec)) if LowerName::new(name) == key.name => {
521                        vec.insert(key.record_type);
522                    }
523                    Some((name, vec)) => {
524                        // names aren't equal, create the NSEC record
525                        let rdata = NSEC::new_cover_self(key.name.clone().into(), mem::take(vec));
526                        let record = Record::from_rdata(name.clone(), ttl, rdata);
527                        records.push(record.into_record_rdata());
528
529                        // new record...
530                        nsec_info = Some((&key.name, BTreeSet::from([key.record_type])))
531                    }
532                }
533            }
534
535            // the last record
536            if let Some((name, vec)) = nsec_info {
537                // names aren't equal, create the NSEC record
538                let rdata = NSEC::new_cover_self(self.origin().clone().into(), vec);
539                let record = Record::from_rdata(name.clone(), ttl, rdata);
540                records.push(record.into_record_rdata());
541            }
542        }
543
544        // insert all the nsec records
545        for record in records {
546            let upserted = self.upsert(record, serial);
547            debug_assert!(upserted.is_ok());
548        }
549    }
550
551    fn nsec3_zone(
552        &mut self,
553        hash_alg: Nsec3HashAlgorithm,
554        salt: &[u8],
555        iterations: u16,
556        opt_out: bool,
557    ) -> DnsSecResult<()> {
558        // only create nsec records for secure zones
559        if self.secure_keys.is_empty() {
560            return Ok(());
561        }
562        tracing::debug!(
563            "generating nsec3 records: {origin}",
564            origin = self.zone.origin()
565        );
566
567        // first remove all existing nsec records
568        let delete_keys = self
569            .zone
570            .keys()
571            .filter(|k| k.record_type == RecordType::NSEC3)
572            .cloned()
573            .collect::<Vec<_>>();
574
575        for key in delete_keys {
576            self.zone.remove(&key);
577        }
578
579        // now go through and generate the nsec3 records
580        let ttl = self.minimum_ttl();
581        let serial = self.serial();
582
583        // Store the record types of each domain name so we can generate NSEC3 records for each
584        // domain name.
585        let mut record_types = HashMap::new();
586        record_types.insert(
587            self.zone.origin().clone(),
588            ([RecordType::NSEC3PARAM].into(), true),
589        );
590
591        let mut delegation_points = HashSet::<LowerName>::new();
592
593        for key in self.zone.keys() {
594            if !self.zone.origin().zone_of(&key.name) {
595                // Non-authoritative record outside of zone
596                continue;
597            }
598            if delegation_points
599                .iter()
600                .any(|name| name.zone_of(&key.name) && name != &key.name)
601            {
602                // Non-authoritative record below zone cut
603                continue;
604            }
605            if key.record_type == RecordType::NS && &key.name != self.zone.origin() {
606                delegation_points.insert(key.name.clone());
607            }
608
609            // Store the type of the current record under its domain name
610            match record_types.entry(key.name.clone()) {
611                std::collections::hash_map::Entry::Occupied(mut entry) => {
612                    let (rtypes, exists): &mut (HashSet<RecordType>, bool) = entry.get_mut();
613                    rtypes.insert(key.record_type);
614                    *exists = true;
615                }
616                std::collections::hash_map::Entry::Vacant(entry) => {
617                    entry.insert((HashSet::from([key.record_type]), true));
618                }
619            }
620        }
621
622        if opt_out {
623            // Delete owner names that have unsigned delegations.
624            let ns_only = HashSet::from([RecordType::NS]);
625            record_types.retain(|_name, (types, _exists)| types != &ns_only);
626        }
627
628        // For every domain name between the current name and the origin, add it to `record_types`
629        // without any record types. This covers all the empty non-terminals that must have an NSEC3
630        // record as well.
631        for name in record_types.keys().cloned().collect::<Vec<_>>() {
632            let mut parent = name.base_name();
633            while parent.num_labels() > self.zone.origin().num_labels() {
634                record_types
635                    .entry(parent.clone())
636                    .or_insert_with(|| (HashSet::new(), false));
637                parent = parent.base_name();
638            }
639        }
640
641        // Compute the hash of all the names.
642        let mut record_types = record_types
643            .into_iter()
644            .map(|(name, (type_bit_maps, exists))| {
645                let hashed_name = hash_alg.hash(salt, &name, iterations)?;
646                Ok((hashed_name, (type_bit_maps, exists)))
647            })
648            .collect::<Result<Vec<_>, ProtoError>>()?;
649        // Sort by hash.
650        record_types.sort_by(|(a, _), (b, _)| a.as_ref().cmp(b.as_ref()));
651
652        let mut records = vec![];
653
654        // Generate an NSEC3 record for every name
655        for (i, (hashed_name, (type_bit_maps, exists))) in record_types.iter().enumerate() {
656            // Get the next hashed name following the hash order.
657            let next_index = (i + 1) % record_types.len();
658            let next_hashed_name = record_types[next_index].0.as_ref().to_vec();
659
660            let rdata = NSEC3::new(
661                hash_alg,
662                opt_out,
663                iterations,
664                salt.to_vec(),
665                next_hashed_name,
666                type_bit_maps
667                    .iter()
668                    .copied()
669                    .chain(exists.then_some(RecordType::RRSIG)),
670            );
671
672            let name = self
673                .zone
674                .origin()
675                .prepend_label(data_encoding::BASE32_DNSSEC.encode(hashed_name.as_ref()))?;
676
677            let record = Record::from_rdata(name, ttl, rdata);
678            records.push(record.into_record_rdata());
679        }
680
681        // Include the NSEC3PARAM record.
682        let rdata = NSEC3PARAM::new(hash_alg, opt_out, iterations, salt.to_vec());
683        let record = Record::from_rdata(self.zone.origin().clone().into(), ttl, rdata);
684        records.push(record.into_record_rdata());
685
686        // insert all the NSEC3 records.
687        for record in records {
688            let upserted = self.zone.upsert(record, serial);
689            debug_assert!(upserted.is_ok());
690        }
691
692        Ok(())
693    }
694
695    fn sign_zone(&mut self) -> DnsSecResult<()> {
696        tracing::debug!("signing zone: {}", self.zone.origin());
697
698        let dns_class = self.zone.dns_class();
699        let minimum_ttl = self.minimum_ttl();
700        let secure_keys = &self.secure_keys;
701
702        // TODO: should this be an error?
703        if secure_keys.is_empty() {
704            tracing::warn!(
705                "attempt to sign_zone {} for dnssec, but no keys available!",
706                self.origin()
707            )
708        }
709
710        // sign all record_sets, as of 0.12.1 this includes DNSKEY
711        for rr_set in self.zone.records_mut() {
712            Self::sign_rrset(rr_set, secure_keys, minimum_ttl, dns_class)?;
713        }
714
715        Ok(())
716    }
717
718    pub(super) fn sign_rrset(
719        rr_set: &mut RecordSet,
720        secure_keys: &[Arc<SigSigner>],
721        zone_ttl: TimeToLive,
722        zone_class: DNSClass,
723    ) -> DnsSecResult<()> {
724        let inception = Utc::now();
725
726        rr_set.clear_rrsigs();
727
728        let rrsig_temp = Record::update0(rr_set.name().clone(), zone_ttl, RecordType::RRSIG);
729
730        for signer in secure_keys {
731            tracing::debug!(
732                "signing rr_set: {}, {} with: {}",
733                rr_set.name(),
734                rr_set.record_type(),
735                signer.key().algorithm(),
736            );
737
738            let expiration = inception + signer.sig_duration();
739            let records: Vec<_> = rr_set.records().map(|rr| rr.as_hickory()).collect();
740            let tbs = TBS::from_sig(
741                rr_set.name(),
742                zone_class,
743                &SIG::new(
744                    rr_set.record_type(),
745                    signer.key().algorithm(),
746                    rr_set.name().num_labels(),
747                    rr_set.ttl().into(),
748                    expiration.timestamp() as u32,
749                    inception.timestamp() as u32,
750                    signer.calculate_key_tag()?,
751                    signer.signer_name().clone(),
752                    Vec::new(), // Gets thrown away anyways.
753                ),
754                records.iter(),
755            );
756
757            // TODO, maybe chain these with some ETL operations instead?
758            let tbs = match tbs {
759                Ok(tbs) => tbs,
760                Err(err) => {
761                    tracing::error!("could not serialize rrset to sign: {}", err);
762                    continue;
763                }
764            };
765
766            let signature = signer.sign(&tbs);
767            let signature = match signature {
768                Ok(signature) => signature,
769                Err(err) => {
770                    tracing::error!("could not sign rrset: {}", err);
771                    continue;
772                }
773            };
774
775            let mut rrsig = rrsig_temp.clone();
776            rrsig.set_data(RData::DNSSEC(DNSSECRData::RRSIG(RRSIG::new(
777                // type_covered: RecordType,
778                rr_set.record_type(),
779                // algorithm: Algorithm,
780                signer.key().algorithm(),
781                // num_labels: u8,
782                rr_set.name().num_labels(),
783                // original_ttl: u32,
784                rr_set.ttl().into(),
785                // sig_expiration: u32,
786                expiration.timestamp() as u32,
787                inception.timestamp() as u32,
788                // key_tag: u16,
789                signer.calculate_key_tag()?,
790                // signer_name: Name,
791                signer.signer_name().clone(),
792                // sig: Vec<u8>
793                signature,
794            ))));
795
796            rr_set
797                .insert_rrsig(rrsig)
798                .map_err(|error| DnsSecError::from(error.to_string()))?;
799        }
800
801        Ok(())
802    }
803}
804
805impl<Z> DNSSecZone<Z>
806where
807    Z: ZoneInfo + Lookup,
808{
809    /// Add an authentication key for DNS updates
810    ///
811    /// Adds a DNSSEC KEY record that can be used to authenticate
812    /// DNS UPDATE requests using SIG(0) signatures.
813    ///
814    /// # Arguments
815    ///
816    /// * `name` - The name for the KEY record
817    /// * `key` - The DNSSEC KEY record data
818    /// * `ttl` - Time To Live for the KEY record
819    ///
820    /// # Returns
821    ///
822    /// `true` if the key was added successfully
823    ///
824    /// # Errors
825    ///
826    /// Returns an error if the key cannot be added due to name/type mismatch
827    pub fn add_update_auth_key(
828        &mut self,
829        name: Name,
830        key: KEY,
831        ttl: TimeToLive,
832    ) -> Result<bool, Mismatch> {
833        let rdata = RData::DNSSEC(DNSSECRData::KEY(key));
834        let record = Record::from_rdata(name, ttl, rdata);
835
836        let serial = self.zone.serial();
837        self.zone.upsert(record, serial)
838    }
839
840    /// Add a zone signing key for DNSSEC
841    ///
842    /// Adds a cryptographic key that will be used to sign all records
843    /// in this zone. The key is also added as a DNSKEY record in the zone.
844    /// Multiple keys can be added for key rollover scenarios.
845    ///
846    /// # Arguments
847    ///
848    /// * `signer` - The signing key to add
849    ///
850    /// # Returns
851    ///
852    /// Success or an error if the key cannot be processed
853    ///
854    /// # Errors
855    ///
856    /// Returns an error if the key is invalid or cannot be added to the zone
857    pub fn add_zone_signing_key(&mut self, signer: SigSigner) -> Result<(), DnsSecZoneError> {
858        let zone_ttl = self.minimum_ttl();
859        let dnskey = DNSKEY::from_key(&signer.key().to_public_key()?);
860        let dnskey = Record::from_rdata(
861            self.name().clone(),
862            zone_ttl,
863            RData::DNSSEC(DNSSECRData::DNSKEY(dnskey)),
864        );
865
866        // TODO: also generate the CDS and CDNSKEY
867        let serial = self.serial();
868        self.zone.upsert(dnskey, serial)?;
869        self.secure_keys.push(Arc::new(signer));
870        Ok(())
871    }
872
873    /// Secure the zone with DNSSEC
874    ///
875    /// Performs a complete DNSSEC operation on the zone:
876    /// 1. Generates NSEC or NSEC3 records for non-existence proofs
877    /// 2. Increments the SOA serial number
878    /// 3. Signs all records in the zone with configured keys
879    ///
880    /// This should be called after any zone modifications to maintain
881    /// DNSSEC validity.
882    ///
883    /// # Returns
884    ///
885    /// Success or a DNSSEC error if the operation fails
886    ///
887    /// # Errors
888    ///
889    /// Returns an error if signing fails or NSEC generation fails
890    #[tracing::instrument(skip_all, level = "trace")]
891    pub fn secure_zone(&mut self) -> DnsSecResult<()> {
892        match self.nx_proof_kind.as_ref() {
893            Some(NxProofKind::Nsec) => self.nsec_zone(),
894            Some(NxProofKind::Nsec3 {
895                algorithm,
896                salt,
897                iterations,
898                opt_out,
899            }) => self.nsec3_zone(*algorithm, &salt.clone(), *iterations, *opt_out)?,
900            None => (),
901        }
902
903        // need to resign any records at the current serial number and bump the number.
904        // first bump the serial number on the SOA, so that it is resigned with the new serial.
905        self.increment_soa_serial();
906
907        // TODO: should we auto sign here? or maybe up a level...
908        self.sign_zone()
909    }
910}
911
912#[async_trait::async_trait]
913impl<Z> Authority for DNSSecZone<Z>
914where
915    Z: Lookup + ZoneInfo + Clone + Send + Sync + 'static,
916{
917    type Lookup = AuthLookup;
918
919    /// What type is this zone
920    fn zone_type(&self) -> hickory_server::authority::ZoneType {
921        self.zone.zone_type()
922    }
923
924    /// Return true if AXFR is allowed
925    fn is_axfr_allowed(&self) -> bool {
926        self.zone.is_axfr_allowed()
927    }
928
929    /// Takes the UpdateMessage, extracts the Records, and applies the changes to the record set.
930    ///
931    /// [RFC 2136](https://tools.ietf.org/html/rfc2136), DNS Update, April 1997
932    ///
933    /// ```text
934    ///
935    /// 3.4 - Process Update Section
936    ///
937    ///   Next, the Update Section is processed as follows.
938    ///
939    /// 3.4.2 - Update
940    ///
941    ///   The Update Section is parsed into RRs and these RRs are processed in
942    ///   order.
943    ///
944    /// 3.4.2.1. If any system failure (such as an out of memory condition,
945    ///   or a hardware error in persistent storage) occurs during the
946    ///   processing of this section, signal SERVFAIL to the requestor and undo
947    ///   all updates applied to the zone during this transaction.
948    ///
949    /// 3.4.2.2. Any Update RR whose CLASS is the same as ZCLASS is added to
950    ///   the zone.  In case of duplicate RDATAs (which for SOA RRs is always
951    ///   the case, and for WKS RRs is the case if the ADDRESS and PROTOCOL
952    ///   fields both match), the Zone RR is replaced by Update RR.  If the
953    ///   TYPE is SOA and there is no Zone SOA RR, or the new SOA.SERIAL is
954    ///   lower (according to [RFC1982]) than or equal to the current Zone SOA
955    ///   RR's SOA.SERIAL, the Update RR is ignored.  In the case of a CNAME
956    ///   Update RR and a non-CNAME Zone RRset or vice versa, ignore the CNAME
957    ///   Update RR, otherwise replace the CNAME Zone RR with the CNAME Update
958    ///   RR.
959    ///
960    /// 3.4.2.3. For any Update RR whose CLASS is ANY and whose TYPE is ANY,
961    ///   all Zone RRs with the same NAME are deleted, unless the NAME is the
962    ///   same as ZNAME in which case only those RRs whose TYPE is other than
963    ///   SOA or NS are deleted.  For any Update RR whose CLASS is ANY and
964    ///   whose TYPE is not ANY all Zone RRs with the same NAME and TYPE are
965    ///   deleted, unless the NAME is the same as ZNAME in which case neither
966    ///   SOA or NS RRs will be deleted.
967    ///
968    /// 3.4.2.4. For any Update RR whose class is NONE, any Zone RR whose
969    ///   NAME, TYPE, RDATA and RDLENGTH are equal to the Update RR is deleted,
970    ///   unless the NAME is the same as ZNAME and either the TYPE is SOA or
971    ///   the TYPE is NS and the matching Zone RR is the only NS remaining in
972    ///   the RRset, in which case this Update RR is ignored.
973    ///
974    /// 3.4.2.5. Signal NOERROR to the requestor.
975    /// ```
976    ///
977    /// # Arguments
978    ///
979    /// * `update` - The `UpdateMessage` records will be extracted and used to perform the update
980    ///              actions as specified in the above RFC.
981    ///
982    /// # Return value
983    ///
984    /// true if any of additions, updates or deletes were made to the zone, false otherwise. Err is
985    ///  returned in the case of bad data, etc.
986    async fn update(&self, update: &MessageRequest) -> UpdateResult<bool> {
987        //let this = &mut self.in_memory.lock().await;
988        // the spec says to authorize after prereqs, seems better to auth first.
989        self.authorize(update).await?;
990        self.verify_prerequisites(update.prerequisites()).await?;
991        self.pre_scan(update.updates()).await?;
992
993        let mut updated_zone = self.clone();
994        updated_zone.update_records(update.updates(), true).await?;
995
996        Ok(true)
997    }
998
999    /// Get the origin of this zone, i.e. example.com is the origin for www.example.com
1000    fn origin(&self) -> &LowerName {
1001        self.zone.origin()
1002    }
1003
1004    /// Looks up all Resource Records matching the given `Name` and `RecordType`.
1005    ///
1006    /// # Arguments
1007    ///
1008    /// * `name` - The name to look up.
1009    /// * `rtype` - The `RecordType` to look up. `RecordType::ANY` will return all records matching
1010    ///             `name`. `RecordType::AXFR` will return all record types except `RecordType::SOA`
1011    ///             due to the requirements that on zone transfers the `RecordType::SOA` must both
1012    ///             precede and follow all other records.
1013    /// * `lookup_options` - Query-related lookup options (e.g., DNSSEC DO bit, supported hash
1014    ///                      algorithms, etc.)
1015    ///
1016    /// # Return value
1017    ///
1018    /// A LookupControlFlow containing the lookup that should be returned to the client.
1019    async fn lookup(
1020        &self,
1021        name: &LowerName,
1022        query_type: RecordType,
1023        lookup_options: LookupOptions,
1024    ) -> LookupControlFlow<Self::Lookup> {
1025        self.zone.lookup(name, query_type, lookup_options).await
1026    }
1027
1028    /// Consulting lookup for all Resource Records matching the given `Name` and `RecordType`.
1029    /// This will be called in a chained authority configuration after an authority in the chain
1030    /// has returned a lookup with a LookupControlFlow::Continue action. Every other authority in
1031    /// the chain will be called via this consult method, until one either returns a
1032    /// LookupControlFlow::Break action, or all authorities have been consulted.  The authority that
1033    /// generated the primary lookup (the one returned via 'lookup') will not be consulted.
1034    ///
1035    /// # Arguments
1036    ///
1037    /// * `name` - The name to look up.
1038    /// * `rtype` - The `RecordType` to look up. `RecordType::ANY` will return all records matching
1039    ///             `name`. `RecordType::AXFR` will return all record types except `RecordType::SOA`
1040    ///             due to the requirements that on zone transfers the `RecordType::SOA` must both
1041    ///             precede and follow all other records.
1042    /// * `lookup_options` - Query-related lookup options (e.g., DNSSEC DO bit, supported hash
1043    ///                      algorithms, etc.)
1044    /// * `last_result` - The lookup returned by a previous authority in a chained configuration.
1045    ///                   If a subsequent authority does not modify this lookup, it will be returned
1046    ///                   to the client after consulting all authorities in the chain.
1047    ///
1048    /// # Return value
1049    ///
1050    /// A LookupControlFlow containing the lookup that should be returned to the client.  This can
1051    /// be the same last_result that was passed in, or a new lookup, depending on the logic of the
1052    /// authority in question.
1053    async fn consult(
1054        &self,
1055        _name: &LowerName,
1056        _rtype: RecordType,
1057        _lookup_options: LookupOptions,
1058        last_result: LookupControlFlow<Box<dyn LookupObject>>,
1059    ) -> LookupControlFlow<Box<dyn LookupObject>> {
1060        last_result
1061    }
1062
1063    /// Using the specified query, perform a lookup against this zone.
1064    ///
1065    /// # Arguments
1066    ///
1067    /// * `request` - the query to perform the lookup with.
1068    /// * `lookup_options` - Query-related lookup options (e.g., DNSSEC DO bit, supported hash
1069    ///                      algorithms, etc.)
1070    ///
1071    /// # Return value
1072    ///
1073    /// A LookupControlFlow containing the lookup that should be returned to the client.
1074    async fn search(
1075        &self,
1076        request_info: RequestInfo<'_>,
1077        lookup_options: LookupOptions,
1078    ) -> LookupControlFlow<Self::Lookup> {
1079        self.zone.search(request_info, lookup_options).await
1080    }
1081
1082    /// Return the NSEC records based on the given name
1083    ///
1084    /// # Arguments
1085    ///
1086    /// * `name` - given this name (i.e. the lookup name), return the NSEC record that is less than
1087    ///            this
1088    /// * `lookup_options` - Query-related lookup options (e.g., DNSSEC DO bit, supported hash
1089    ///                      algorithms, etc.)
1090    #[allow(unused_variables)]
1091    async fn get_nsec_records(
1092        &self,
1093        name: &LowerName,
1094        lookup_options: LookupOptions,
1095    ) -> LookupControlFlow<Self::Lookup> {
1096        let rr_key = RrKey::new(name.clone(), RecordType::NSEC);
1097        let no_data = self
1098            .get(&rr_key)
1099            .map(|rr_set| LookupRecords::new(lookup_options, rr_set.as_hickory().into()));
1100
1101        if let Some(no_data) = no_data {
1102            return LookupControlFlow::Continue(Ok(no_data.into()));
1103        }
1104
1105        let closest_proof = self.closest_nsec(name);
1106
1107        // we need the wildcard proof, but make sure that it's still part of the zone.
1108        let wildcard = name.base_name();
1109        let origin = self.origin();
1110        let wildcard = if origin.zone_of(&wildcard) {
1111            wildcard
1112        } else {
1113            origin.clone()
1114        };
1115
1116        // don't duplicate the record...
1117        let wildcard_proof = if wildcard != *name {
1118            self.closest_nsec(&wildcard)
1119        } else {
1120            None
1121        };
1122
1123        let proofs = match (closest_proof, wildcard_proof) {
1124            (Some(closest_proof), Some(wildcard_proof)) => {
1125                // dedup with the wildcard proof
1126                if wildcard_proof != closest_proof {
1127                    vec![wildcard_proof, closest_proof]
1128                } else {
1129                    vec![closest_proof]
1130                }
1131            }
1132            (None, Some(proof)) | (Some(proof), None) => vec![proof],
1133            (None, None) => vec![],
1134        };
1135
1136        LookupControlFlow::Continue(Ok(LookupRecords::many(
1137            lookup_options,
1138            proofs
1139                .into_iter()
1140                .map(|rrset| rrset.as_hickory().into())
1141                .collect(),
1142        )
1143        .into()))
1144    }
1145
1146    /// Return the NSEC3 records based on the information available for a query.
1147    #[allow(unused_variables)]
1148    async fn get_nsec3_records(
1149        &self,
1150        info: Nsec3QueryInfo<'_>,
1151        lookup_options: LookupOptions,
1152    ) -> LookupControlFlow<Self::Lookup> {
1153        LookupControlFlow::Continue(self.proof(info).map(|proof| {
1154            LookupRecords::many(
1155                lookup_options,
1156                proof
1157                    .into_iter()
1158                    .map(|rrset| rrset.as_hickory().into())
1159                    .collect(),
1160            )
1161            .into()
1162        }))
1163    }
1164
1165    /// Returns the kind of non-existence proof used for this zone.
1166    fn nx_proof_kind(&self) -> Option<&NxProofKind> {
1167        self.nx_proof_kind.as_ref()
1168    }
1169}