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}