Skip to main content

walnut_dns/database/
dnssec.rs

1use std::fmt;
2use std::sync::Arc;
3
4use hickory_proto::dnssec::crypto::signing_key_from_der;
5use hickory_proto::dnssec::{Algorithm, DnsSecResult};
6use hickory_proto::dnssec::{SigSigner, rdata::DNSKEY};
7use hickory_proto::rr::{LowerName, Name};
8use rustls_pki_types::PrivateKeyDer;
9use zeroize::Zeroizing;
10
11use crate::authority::{DNSSecZone, DnsSecZoneError};
12use crate::catalog::{CatalogError, CatalogStore};
13use crate::database::journal::SqliteJournal;
14use crate::rr::{TimeToLive, Zone};
15use crate::{SqliteStore, ZoneInfo as _};
16
17/// DNSSEC cryptographic key for zone signing
18///
19/// DNSKey wraps cryptographic key material and provides the ability to create
20/// DNSSEC signers for zone signing operations. It securely stores private key
21/// data using zeroization to prevent key material from remaining in memory.
22#[derive(Clone)]
23pub struct DNSKey {
24    key_data: Zeroizing<Box<[u8]>>,
25    algorithm: Algorithm,
26    ttl: TimeToLive,
27}
28
29impl fmt::Debug for DNSKey {
30    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
31        f.debug_struct("KeyBuilder")
32            .field("algorithm", &self.algorithm)
33            .field("ttl", &self.ttl)
34            .finish()
35    }
36}
37
38impl DNSKey {
39    /// Create a new DNSSEC key
40    ///
41    /// Creates a new DNSSEC key from the provided key material, algorithm,
42    /// and TTL. The key data should be in DER format.
43    ///
44    /// # Arguments
45    ///
46    /// * `key_data` - The private key data in DER format
47    /// * `algorithm` - The DNSSEC algorithm to use
48    /// * `ttl` - TTL for DNSKEY records created from this key
49    ///
50    /// # Returns
51    ///
52    /// A new DNSKey instance
53    pub fn new(key_data: impl Into<Box<[u8]>>, algorithm: Algorithm, ttl: TimeToLive) -> Self {
54        Self {
55            key_data: Zeroizing::new(key_data.into()),
56            ttl,
57            algorithm,
58        }
59    }
60
61    fn build(&self, name: Name) -> DnsSecResult<SigSigner> {
62        let private = PrivateKeyDer::try_from(self.key_data.as_ref())?;
63        let key = signing_key_from_der(&private, self.algorithm)?;
64
65        Ok(SigSigner::dnssec(
66            DNSKEY::from_key(&key.to_public_key().unwrap()),
67            key,
68            name,
69            self.ttl.into(),
70        ))
71    }
72}
73
74/// DNSSEC-enabled DNS zone storage
75///
76/// DNSSecStore wraps a regular SqliteStore and adds DNSSEC capabilities,
77/// including key management and automatic zone signing. It provides the same
78/// storage interface as SqliteStore but returns DNSSEC-enabled zones.
79#[derive(Debug, Clone)]
80pub struct DNSSecStore {
81    catalog: SqliteStore,
82    keys: Vec<Arc<DNSKey>>,
83    allow_update: bool,
84    dnssec_enabled: bool,
85}
86
87impl DNSSecStore {
88    /// Create a new DNSSEC store from a regular SQLite store
89    ///
90    /// Wraps an existing SqliteStore to provide DNSSEC functionality.
91    /// The store starts with DNSSEC disabled and no keys configured.
92    ///
93    /// # Arguments
94    ///
95    /// * `catalog` - The underlying SQLite store
96    ///
97    /// # Returns
98    ///
99    /// A new DNSSecStore instance
100    pub fn new(catalog: SqliteStore) -> Self {
101        Self {
102            catalog,
103            keys: Vec::new(),
104            allow_update: false,
105            dnssec_enabled: false,
106        }
107    }
108
109    /// Add a zone signing key to this store
110    ///
111    /// Adds a DNSSEC key that will be used to sign all zones managed by
112    /// this store. Multiple keys can be added for key rollover scenarios.
113    ///
114    /// # Arguments
115    ///
116    /// * `key` - The DNSSEC key to add
117    ///
118    /// # Returns
119    ///
120    /// Success or an error if the key cannot be added
121    ///
122    /// # Errors
123    ///
124    /// Returns an error if the key is invalid or cannot be processed
125    pub fn add_zone_signing_key(
126        &mut self,
127        key: impl Into<Arc<DNSKey>>,
128    ) -> Result<(), DnsSecZoneError> {
129        self.keys.push(key.into());
130        Ok(())
131    }
132
133    /// Check if DNS updates are allowed
134    ///
135    /// Returns whether this store allows DNS UPDATE operations.
136    ///
137    /// # Returns
138    ///
139    /// `true` if updates are allowed
140    pub fn allow_update(&self) -> bool {
141        self.allow_update
142    }
143
144    /// Set whether to allow DNS updates
145    ///
146    /// Enables or disables DNS UPDATE operations for zones managed by this store.
147    ///
148    /// # Arguments
149    ///
150    /// * `allow_update` - Whether to allow updates
151    ///
152    /// # Returns
153    ///
154    /// A mutable reference to this store for method chaining
155    pub fn set_allow_update(&mut self, allow_update: bool) -> &mut Self {
156        self.allow_update = allow_update;
157        self
158    }
159
160    /// Check if DNSSEC is enabled
161    ///
162    /// Returns whether DNSSEC signing is enabled for zones managed by this store.
163    ///
164    /// # Returns
165    ///
166    /// `true` if DNSSEC is enabled
167    pub fn dnssec_enabled(&self) -> bool {
168        self.dnssec_enabled
169    }
170
171    /// Set whether to enable DNSSEC
172    ///
173    /// Enables or disables DNSSEC signing for zones managed by this store.
174    /// When enabled, zones will be automatically signed with configured keys.
175    ///
176    /// # Arguments
177    ///
178    /// * `dnssec_enabled` - Whether to enable DNSSEC
179    ///
180    /// # Returns
181    ///
182    /// A mutable reference to this store for method chaining
183    pub fn set_dnssec_enabled(&mut self, dnssec_enabled: bool) -> &mut Self {
184        self.dnssec_enabled = dnssec_enabled;
185        self
186    }
187
188    /// Get a journal for recording DNS operations
189    ///
190    /// Returns a journal that can be used to record DNS operations
191    /// for zones managed by this store.
192    ///
193    /// # Returns
194    ///
195    /// A SqliteJournal instance
196    pub fn journal(&self) -> SqliteJournal {
197        self.catalog.journal()
198    }
199
200    fn map_zone(&self, zone: Zone) -> Result<DNSSecZone<Zone>, DnsSecZoneError> {
201        let mut dnsseczone = DNSSecZone::new(zone);
202        dnsseczone
203            .set_allow_update(self.allow_update)
204            .set_dnssec_enabled(self.dnssec_enabled)
205            .set_journal(self.catalog.journal());
206        for key in &self.keys {
207            dnsseczone.add_zone_signing_key(key.build(dnsseczone.origin().clone().into())?)?;
208        }
209        Ok(dnsseczone)
210    }
211}
212
213#[async_trait::async_trait]
214impl CatalogStore<DNSSecZone<Zone>> for DNSSecStore {
215    #[tracing::instrument(skip_all, fields(%origin), level = "debug")]
216    async fn find(
217        &self,
218        origin: &LowerName,
219    ) -> Result<Option<Vec<DNSSecZone<Zone>>>, CatalogError> {
220        let zones = self.catalog.find(origin).await?;
221        if let Some(zones) = zones {
222            Ok(Some(
223                zones
224                    .into_iter()
225                    .map(|z| self.map_zone(z.into_inner()))
226                    .collect::<Result<Vec<_>, _>>()
227                    .map_err(CatalogError::new)?,
228            ))
229        } else {
230            Ok(None)
231        }
232    }
233
234    async fn upsert(
235        &self,
236        name: LowerName,
237        zones: &[DNSSecZone<Zone>],
238    ) -> Result<(), CatalogError> {
239        let mut conn = self.catalog.connection().await?;
240        crate::block_in_place(|| {
241            let tx = conn.transaction()?;
242            let zx = crate::database::ZonePersistence::new(&tx);
243
244            // First clear existing name
245            zx.clear(&name)?;
246            let mut n = 0;
247            for zone in zones {
248                n += zx.upsert(zone)?;
249            }
250            tx.commit()?;
251            tracing::debug!("upsert {n} zones");
252            Ok(())
253        })
254    }
255
256    async fn list(&self, name: &LowerName) -> Result<Vec<Name>, CatalogError> {
257        self.catalog.list(name).await
258    }
259
260    async fn remove(
261        &self,
262        name: &LowerName,
263    ) -> Result<Option<Vec<DNSSecZone<Zone>>>, CatalogError> {
264        self.catalog.remove(name).await.map(|dz| {
265            dz.map(|zs| {
266                zs.into_iter()
267                    .map(|z| DNSSecZone::new(z.into_inner()))
268                    .collect()
269            })
270        })
271    }
272}