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}