/src/hickory-dns/crates/proto/src/dnssec/handle.rs
Line | Count | Source |
1 | | // Copyright 2015-2023 Benjamin Fry <benjaminfry@me.com> |
2 | | // |
3 | | // Licensed under the Apache License, Version 2.0, <LICENSE-APACHE or |
4 | | // https://apache.org/licenses/LICENSE-2.0> or the MIT license <LICENSE-MIT or |
5 | | // https://opensource.org/licenses/MIT>, at your option. This file may not be |
6 | | // copied, modified, or distributed except according to those terms. |
7 | | |
8 | | //! The `DnssecDnsHandle` is used to validate all DNS responses for correct DNSSEC signatures. |
9 | | |
10 | | use alloc::{borrow::ToOwned, boxed::Box, sync::Arc, vec::Vec}; |
11 | | use core::{ |
12 | | clone::Clone, |
13 | | fmt::Display, |
14 | | hash::{Hash, Hasher}, |
15 | | ops::RangeInclusive, |
16 | | pin::Pin, |
17 | | time::Duration, |
18 | | }; |
19 | | use std::{ |
20 | | collections::{HashMap, HashSet, hash_map::DefaultHasher}, |
21 | | time::Instant, |
22 | | }; |
23 | | |
24 | | use futures_util::{ |
25 | | future::{self, FutureExt}, |
26 | | stream::{self, Stream, StreamExt}, |
27 | | }; |
28 | | use lru_cache::LruCache; |
29 | | use parking_lot::Mutex; |
30 | | use tracing::{debug, error, trace, warn}; |
31 | | |
32 | | use crate::{ |
33 | | dnssec::{ |
34 | | Proof, ProofError, ProofErrorKind, TrustAnchors, Verifier, |
35 | | nsec3::verify_nsec3, |
36 | | rdata::{DNSKEY, DNSSECRData, DS, NSEC, RRSIG}, |
37 | | }, |
38 | | error::{DnsError, NetError, NoRecords}, |
39 | | op::{DnsRequest, DnsRequestOptions, DnsResponse, Edns, Message, OpCode, Query, ResponseCode}, |
40 | | rr::{Name, RData, Record, RecordType, SerialNumber, resource::RecordRef}, |
41 | | runtime::{RuntimeProvider, Time}, |
42 | | xfer::{FirstAnswer, dns_handle::DnsHandle}, |
43 | | }; |
44 | | |
45 | | use self::rrset::Rrset; |
46 | | |
47 | | /// Performs DNSSEC validation of all DNS responses from the wrapped DnsHandle |
48 | | /// |
49 | | /// This wraps a DnsHandle, changing the implementation `send()` to validate all |
50 | | /// message responses for Query operations. Update operation responses are not validated by |
51 | | /// this process. |
52 | | #[derive(Clone)] |
53 | | #[must_use = "queries can only be sent through a DnsHandle"] |
54 | | pub struct DnssecDnsHandle<H> { |
55 | | handle: H, |
56 | | trust_anchor: Arc<TrustAnchors>, |
57 | | request_depth: usize, |
58 | | nsec3_soft_iteration_limit: u16, |
59 | | nsec3_hard_iteration_limit: u16, |
60 | | validation_cache: ValidationCache, |
61 | | } |
62 | | |
63 | | impl<H: DnsHandle> DnssecDnsHandle<H> { |
64 | | /// Create a new DnssecDnsHandle wrapping the specified handle. |
65 | | /// |
66 | | /// This uses the compiled in TrustAnchor default trusted keys. |
67 | | /// |
68 | | /// # Arguments |
69 | | /// * `handle` - handle to use for all connections to a remote server. |
70 | 0 | pub fn new(handle: H) -> Self { |
71 | 0 | Self::with_trust_anchor(handle, Arc::new(TrustAnchors::default())) |
72 | 0 | } |
73 | | |
74 | | /// Create a new DnssecDnsHandle wrapping the specified handle. |
75 | | /// |
76 | | /// This allows a custom TrustAnchor to be define. |
77 | | /// |
78 | | /// # Arguments |
79 | | /// * `handle` - handle to use for all connections to a remote server. |
80 | | /// * `trust_anchor` - custom DNSKEYs that will be trusted, can be used to pin trusted keys. |
81 | 0 | pub fn with_trust_anchor(handle: H, trust_anchor: Arc<TrustAnchors>) -> Self { |
82 | 0 | Self { |
83 | 0 | handle, |
84 | 0 | trust_anchor, |
85 | 0 | request_depth: 0, |
86 | 0 | // These default values are based on |
87 | 0 | // [RFC 9276 Appendix A](https://www.rfc-editor.org/rfc/rfc9276.html#appendix-A) |
88 | 0 | nsec3_soft_iteration_limit: 100, |
89 | 0 | nsec3_hard_iteration_limit: 500, |
90 | 0 | validation_cache: ValidationCache::new(DEFAULT_VALIDATION_CACHE_SIZE), |
91 | 0 | } |
92 | 0 | } |
93 | | |
94 | | /// Set custom NSEC3 iteration limits |
95 | | /// |
96 | | /// # Arguments |
97 | | /// * `soft_limit` - the soft limit for NSEC3 iterations. NSEC3 records with iteration counts |
98 | | /// above this limit, but below the hard limit will evaluate to Proof::Insecure. |
99 | | /// * `hard_limit` - the hard limit for NSEC3 iterations. NSEC3 records with iteration counts |
100 | | /// above this limit will evaluate to Proof::Bogus. |
101 | 0 | pub fn nsec3_iteration_limits( |
102 | 0 | mut self, |
103 | 0 | soft_limit: Option<u16>, |
104 | 0 | hard_limit: Option<u16>, |
105 | 0 | ) -> Self { |
106 | 0 | if let Some(soft) = soft_limit { |
107 | 0 | self.nsec3_soft_iteration_limit = soft; |
108 | 0 | } |
109 | | |
110 | 0 | if let Some(hard) = hard_limit { |
111 | 0 | self.nsec3_hard_iteration_limit = hard; |
112 | 0 | } |
113 | | |
114 | 0 | self |
115 | 0 | } |
116 | | |
117 | | /// Set a custom validation cache size |
118 | | /// |
119 | | /// # Arguments |
120 | | /// * `capacity` - the desired capacity of the DNSSEC validation cache. |
121 | 0 | pub fn validation_cache_size(mut self, capacity: usize) -> Self { |
122 | 0 | self.validation_cache = ValidationCache::new(capacity); |
123 | 0 | self |
124 | 0 | } |
125 | | |
126 | | /// Set custom negative response validation cache TTL range |
127 | | /// |
128 | | /// # Arguments |
129 | | /// * `ttl` - A range of permissible TTL values for negative responses. |
130 | | /// |
131 | | /// Validation cache TTLs are based on the Rrset TTL value, but will be clamped to |
132 | | /// this value, if specified, for negative responses. |
133 | 0 | pub fn negative_validation_ttl(mut self, ttl: RangeInclusive<Duration>) -> Self { |
134 | 0 | self.validation_cache.negative_ttl = Some(ttl); |
135 | 0 | self |
136 | 0 | } |
137 | | |
138 | | /// Set custom positive response validation cache TTL range |
139 | | /// |
140 | | /// # Arguments |
141 | | /// * `ttl` - A range of permissible TTL values for positive responses. |
142 | | /// |
143 | | /// Validation cache TTLs are based on the Rrset TTL value, but will be clamped to |
144 | | /// this value, if specified, for positive responses. |
145 | 0 | pub fn positive_validation_ttl(mut self, ttl: RangeInclusive<Duration>) -> Self { |
146 | 0 | self.validation_cache.positive_ttl = Some(ttl); |
147 | 0 | self |
148 | 0 | } |
149 | | |
150 | 0 | async fn verify_response( |
151 | 0 | self, |
152 | 0 | result: Result<DnsResponse, NetError>, |
153 | 0 | query: Query, |
154 | 0 | options: DnsRequestOptions, |
155 | 0 | ) -> Result<DnsResponse, NetError> { |
156 | 0 | let mut message = match result { |
157 | 0 | Ok(response) => response, |
158 | | // Translate NoRecordsFound errors into a DnsResponse message so the rest of the |
159 | | // DNSSEC handler chain can validate negative responses. |
160 | | Err(NetError::Dns(DnsError::NoRecordsFound(NoRecords { |
161 | 0 | query, |
162 | 0 | authorities, |
163 | 0 | response_code, |
164 | | .. |
165 | | }))) => { |
166 | 0 | debug!("translating NoRecordsFound to DnsResponse for {query}"); |
167 | 0 | let mut msg = Message::query(); |
168 | 0 | msg.add_query(*query); |
169 | 0 | msg.set_response_code(response_code); |
170 | | |
171 | 0 | if let Some(authorities) = authorities { |
172 | 0 | for record in authorities.iter() { |
173 | 0 | msg.add_authority(record.clone()); |
174 | 0 | } |
175 | 0 | } |
176 | | |
177 | 0 | match DnsResponse::from_message(msg) { |
178 | 0 | Ok(response) => response, |
179 | 0 | Err(err) => { |
180 | 0 | return Err(NetError::from(format!( |
181 | 0 | "unable to construct DnsResponse: {err:?}" |
182 | 0 | ))); |
183 | | } |
184 | | } |
185 | | } |
186 | 0 | Err(err) => return Err(err), |
187 | | }; |
188 | | |
189 | 0 | debug!( |
190 | 0 | "validating message_response: {}, with {} trust_anchors", |
191 | 0 | message.id(), |
192 | 0 | self.trust_anchor.len(), |
193 | | ); |
194 | | |
195 | | // use the same current time value for all rrsig + rrset pairs. |
196 | 0 | let current_time = <H::Runtime as RuntimeProvider>::Timer::current_time() as u32; |
197 | | |
198 | | // group the record sets by name and type |
199 | | // each rrset type needs to validated independently |
200 | 0 | let answers = message.take_answers(); |
201 | 0 | let authorities = message.take_authorities(); |
202 | 0 | let additionals = message.take_additionals(); |
203 | | |
204 | 0 | let answers = self |
205 | 0 | .verify_rrsets(&query, answers, options, current_time) |
206 | 0 | .await; |
207 | 0 | let authorities = self |
208 | 0 | .verify_rrsets(&query, authorities, options, current_time) |
209 | 0 | .await; |
210 | 0 | let additionals = self |
211 | 0 | .verify_rrsets(&query, additionals, options, current_time) |
212 | 0 | .await; |
213 | | |
214 | | // If we have any wildcard records, they must be validated with covering |
215 | | // NSEC/NSEC3 records. RFC 4035 5.3.4, 5.4, and RFC 5155 7.2.6. |
216 | 0 | let must_validate_nsec = answers.iter().any(|rr| match rr.data() { |
217 | 0 | RData::DNSSEC(DNSSECRData::RRSIG(rrsig)) => { |
218 | 0 | rrsig.input().num_labels < rr.name().num_labels() |
219 | | } |
220 | 0 | _ => false, |
221 | 0 | }); |
222 | | |
223 | 0 | message.insert_answers(answers); |
224 | 0 | message.insert_authorities(authorities); |
225 | 0 | message.insert_additionals(additionals); |
226 | | |
227 | 0 | if !message.authorities().is_empty() |
228 | 0 | && message |
229 | 0 | .authorities() |
230 | 0 | .iter() |
231 | 0 | .all(|x| x.proof() == Proof::Insecure) |
232 | | { |
233 | 0 | return Ok(message); |
234 | 0 | } |
235 | | |
236 | 0 | let nsec3s = message |
237 | 0 | .authorities() |
238 | 0 | .iter() |
239 | 0 | .filter_map(|rr| { |
240 | 0 | if message |
241 | 0 | .authorities() |
242 | 0 | .iter() |
243 | 0 | .any(|r| r.name() == rr.name() && r.proof() == Proof::Secure) |
244 | | { |
245 | 0 | match rr.data() { |
246 | 0 | RData::DNSSEC(DNSSECRData::NSEC3(nsec3)) => Some((rr.name(), nsec3)), |
247 | 0 | _ => None, |
248 | | } |
249 | | } else { |
250 | 0 | None |
251 | | } |
252 | 0 | }) |
253 | 0 | .collect::<Vec<_>>(); |
254 | | |
255 | 0 | let nsecs = message |
256 | 0 | .authorities() |
257 | 0 | .iter() |
258 | 0 | .filter_map(|rr| { |
259 | 0 | if message |
260 | 0 | .authorities() |
261 | 0 | .iter() |
262 | 0 | .any(|r| r.name() == rr.name() && r.proof() == Proof::Secure) |
263 | | { |
264 | 0 | match rr.data() { |
265 | 0 | RData::DNSSEC(DNSSECRData::NSEC(nsec)) => Some((rr.name(), nsec)), |
266 | 0 | _ => None, |
267 | | } |
268 | | } else { |
269 | 0 | None |
270 | | } |
271 | 0 | }) |
272 | 0 | .collect::<Vec<_>>(); |
273 | | |
274 | | // Both NSEC and NSEC3 records cannot coexist during |
275 | | // transition periods, as per RFC 5515 10.4.3 and |
276 | | // 10.5.2 |
277 | 0 | let nsec_proof = match (!nsec3s.is_empty(), !nsecs.is_empty(), must_validate_nsec) { |
278 | 0 | (true, false, _) => verify_nsec3( |
279 | 0 | &query, |
280 | 0 | find_soa_name(&message), |
281 | 0 | message.response_code(), |
282 | 0 | message.answers(), |
283 | 0 | &nsec3s, |
284 | 0 | self.nsec3_soft_iteration_limit, |
285 | 0 | self.nsec3_hard_iteration_limit, |
286 | | ), |
287 | 0 | (false, true, _) => verify_nsec( |
288 | 0 | &query, |
289 | 0 | find_soa_name(&message), |
290 | 0 | message.response_code(), |
291 | 0 | message.answers(), |
292 | 0 | nsecs.as_slice(), |
293 | | ), |
294 | | (true, true, _) => { |
295 | 0 | warn!( |
296 | 0 | "response contains both NSEC and NSEC3 records\nQuery:\n{query:?}\nResponse:\n{message:?}" |
297 | | ); |
298 | 0 | Proof::Bogus |
299 | | } |
300 | | (false, false, true) => { |
301 | 0 | warn!("response contains wildcard RRSIGs, but no NSEC/NSEC3s are present."); |
302 | 0 | Proof::Bogus |
303 | | } |
304 | | (false, false, false) => { |
305 | | // Return Ok if there were no NSEC/NSEC3 records and no wildcard RRSIGs. |
306 | 0 | if !message.answers().is_empty() { |
307 | 0 | return Ok(message); |
308 | 0 | } |
309 | | |
310 | | // Return Ok if the zone is insecure |
311 | 0 | if let Err(err) = self.find_ds_records(query.name().clone(), options).await { |
312 | 0 | if err.proof == Proof::Insecure { |
313 | 0 | return Ok(message); |
314 | 0 | } |
315 | 0 | } |
316 | | |
317 | | // If neither of the two conditions above are true, the response is Bogus - we should |
318 | | // have a covering NSEC/NSEC3 record for this scenario. |
319 | 0 | warn!( |
320 | 0 | "response does not contain NSEC or NSEC3 records. Query: {query:?} response: {message:?}" |
321 | | ); |
322 | 0 | Proof::Bogus |
323 | | } |
324 | | }; |
325 | | |
326 | 0 | if !nsec_proof.is_secure() { |
327 | 0 | debug!("returning Nsec error for {} {nsec_proof}", query.name()); |
328 | | // TODO change this to remove the NSECs, like we do for the others? |
329 | 0 | return Err(NetError::from(DnsError::Nsec { |
330 | 0 | query: Box::new(query.clone()), |
331 | 0 | response: Box::new(message), |
332 | 0 | proof: nsec_proof, |
333 | 0 | })); |
334 | 0 | } |
335 | | |
336 | 0 | Ok(message) |
337 | 0 | } |
338 | | |
339 | | /// This pulls all answers returned in a Message response and returns a future which will |
340 | | /// validate all of them. |
341 | 0 | async fn verify_rrsets( |
342 | 0 | &self, |
343 | 0 | query: &Query, |
344 | 0 | records: Vec<Record>, |
345 | 0 | options: DnsRequestOptions, |
346 | 0 | current_time: u32, |
347 | 0 | ) -> Vec<Record> { |
348 | 0 | let mut rrset_types: HashSet<(Name, RecordType)> = HashSet::new(); |
349 | | |
350 | 0 | for rrset in records |
351 | 0 | .iter() |
352 | 0 | .filter(|rr| { |
353 | 0 | rr.record_type() != RecordType::RRSIG && |
354 | | // if we are at a depth greater than 1, we are only interested in proving evaluation chains |
355 | | // this means that only DNSKEY, DS, NSEC, and NSEC3 are interesting at that point. |
356 | | // this protects against looping over things like NS records and DNSKEYs in responses. |
357 | | // TODO: is there a cleaner way to prevent cycles in the evaluations? |
358 | 0 | (self.request_depth <= 1 || matches!( |
359 | 0 | rr.record_type(), |
360 | | RecordType::DNSKEY | RecordType::DS | RecordType::NSEC | RecordType::NSEC3, |
361 | | )) |
362 | 0 | }) |
363 | 0 | .map(|rr| (rr.name().clone(), rr.record_type())) |
364 | 0 | { |
365 | 0 | rrset_types.insert(rrset); |
366 | 0 | } |
367 | | |
368 | | // there were no records to verify |
369 | 0 | if rrset_types.is_empty() { |
370 | 0 | return records; |
371 | 0 | } |
372 | | |
373 | | // Records for return, eventually, all records will be returned in here |
374 | 0 | let mut return_records = Vec::with_capacity(records.len()); |
375 | | |
376 | | // Removing the RRSIGs from the original records, the rest of the records will be mutable to remove those evaluated |
377 | | // and the remainder after all evalutions will be returned. |
378 | 0 | let (mut rrsigs, mut records) = records |
379 | 0 | .into_iter() |
380 | 0 | .partition::<Vec<_>, _>(|r| r.record_type().is_rrsig()); |
381 | | |
382 | 0 | for (name, record_type) in rrset_types { |
383 | | // collect all the rrsets to verify |
384 | | let current_rrset; |
385 | 0 | (current_rrset, records) = records |
386 | 0 | .into_iter() |
387 | 0 | .partition::<Vec<_>, _>(|rr| rr.record_type() == record_type && rr.name() == &name); |
388 | | |
389 | | let current_rrsigs; |
390 | 0 | (current_rrsigs, rrsigs) = rrsigs.into_iter().partition::<Vec<_>, _>(|rr| { |
391 | 0 | rr.try_borrow::<RRSIG>() |
392 | 0 | .map(|rr| rr.name() == &name && rr.data().input().type_covered == record_type) |
393 | 0 | .unwrap_or_default() |
394 | 0 | }); |
395 | | |
396 | | // TODO: we can do a better job here, no need for all the vec creation and clones in the Rrset. |
397 | 0 | let mut rrs_to_verify = current_rrset.iter(); |
398 | 0 | let mut rrset = Rrset::new(rrs_to_verify.next().unwrap()); |
399 | 0 | rrs_to_verify.for_each(|rr| rrset.add(rr)); |
400 | | |
401 | | // RRSIGS are never modified after this point |
402 | 0 | let rrsigs: Vec<_> = current_rrsigs |
403 | 0 | .iter() |
404 | 0 | .filter_map(|rr| rr.try_borrow::<RRSIG>()) |
405 | 0 | .filter(|rr| rr.name() == &name) |
406 | 0 | .filter(|rrsig| rrsig.data().input().type_covered == record_type) |
407 | 0 | .collect(); |
408 | | |
409 | | // if there is already an active validation going on, assume the other validation will |
410 | | // complete properly or error if it is invalid |
411 | | |
412 | | // TODO: support non-IN classes? |
413 | 0 | debug!( |
414 | 0 | "verifying: {name} record_type: {record_type}, rrsigs: {rrsig_len}", |
415 | 0 | rrsig_len = rrsigs.len() |
416 | | ); |
417 | | |
418 | | // verify this rrset |
419 | 0 | let proof = self |
420 | 0 | .verify_rrset(RrsetVerificationContext { |
421 | 0 | query, |
422 | 0 | rrset: &rrset, |
423 | 0 | rrsigs, |
424 | 0 | options, |
425 | 0 | current_time, |
426 | 0 | }) |
427 | 0 | .await; |
428 | | |
429 | 0 | let proof = match proof { |
430 | 0 | Ok(proof) => { |
431 | 0 | debug!("verified: {name} record_type: {record_type}",); |
432 | 0 | proof |
433 | | } |
434 | 0 | Err(err) => { |
435 | 0 | match err.kind() { |
436 | | ProofErrorKind::DsResponseNsec { .. } => { |
437 | 0 | debug!("verified insecure {name}/{record_type}") |
438 | | } |
439 | 0 | kind => { |
440 | 0 | debug!("failed to verify: {name} record_type: {record_type}: {kind}") |
441 | | } |
442 | | } |
443 | 0 | RrsetProof { |
444 | 0 | proof: err.proof, |
445 | 0 | adjusted_ttl: None, |
446 | 0 | rrsig_index: None, |
447 | 0 | } |
448 | | } |
449 | | }; |
450 | | |
451 | | let RrsetProof { |
452 | 0 | proof, |
453 | 0 | adjusted_ttl, |
454 | 0 | rrsig_index: rrsig_idx, |
455 | 0 | } = proof; |
456 | 0 | for mut record in current_rrset { |
457 | 0 | record.set_proof(proof); |
458 | 0 | if let (Proof::Secure, Some(ttl)) = (proof, adjusted_ttl) { |
459 | 0 | record.set_ttl(ttl); |
460 | 0 | } |
461 | | |
462 | 0 | return_records.push(record); |
463 | | } |
464 | | |
465 | | // only mark the RRSIG used for the proof |
466 | 0 | let mut current_rrsigs = current_rrsigs; |
467 | 0 | if let Some(rrsig_idx) = rrsig_idx { |
468 | 0 | if let Some(rrsig) = current_rrsigs.get_mut(rrsig_idx) { |
469 | 0 | rrsig.set_proof(proof); |
470 | 0 | if let (Proof::Secure, Some(ttl)) = (proof, adjusted_ttl) { |
471 | 0 | rrsig.set_ttl(ttl); |
472 | 0 | } |
473 | | } else { |
474 | 0 | warn!( |
475 | 0 | "bad rrsig index {rrsig_idx} rrsigs.len = {}", |
476 | 0 | current_rrsigs.len() |
477 | | ); |
478 | | } |
479 | 0 | } |
480 | | |
481 | | // push all the RRSIGs back to the return |
482 | 0 | return_records.extend(current_rrsigs); |
483 | | } |
484 | | |
485 | | // Add back all the RRSIGs and any records that were not verified |
486 | 0 | return_records.extend(rrsigs); |
487 | 0 | return_records.extend(records); |
488 | 0 | return_records |
489 | 0 | } |
490 | | |
491 | | /// Generic entrypoint to verify any RRSET against the provided signatures. |
492 | | /// |
493 | | /// Generally, the RRSET will be validated by `verify_default_rrset()`. There are additional |
494 | | /// checks that happen after the RRSET is successfully validated. In the case of DNSKEYs this |
495 | | /// triggers `verify_dnskey_rrset()`. If it's an NSEC record, then the NSEC record will be |
496 | | /// validated to prove it's correctness. There is a special case for DNSKEY, where if the RRSET |
497 | | /// is unsigned, `rrsigs` is empty, then an immediate `verify_dnskey_rrset()` is triggered. In |
498 | | /// this case, it's possible the DNSKEY is a trust_anchor and is not self-signed. |
499 | | /// |
500 | | /// # Returns |
501 | | /// |
502 | | /// If Ok, returns an RrsetProof containing the proof, adjusted TTL, and an index of the RRSIG used for |
503 | | /// validation of the rrset. |
504 | 0 | async fn verify_rrset( |
505 | 0 | &self, |
506 | 0 | context: RrsetVerificationContext<'_>, |
507 | 0 | ) -> Result<RrsetProof, ProofError> { |
508 | 0 | let key = context.key(); |
509 | | |
510 | 0 | if let Some(cached) = self.validation_cache.get(&key, &context) { |
511 | 0 | return cached; |
512 | 0 | } |
513 | | |
514 | | // DNSKEYS have different logic for their verification |
515 | 0 | let proof = match context.rrset.record_type { |
516 | 0 | RecordType::DNSKEY => self.verify_dnskey_rrset(&context).await, |
517 | 0 | _ => self.verify_default_rrset(&context).await, |
518 | | }; |
519 | | |
520 | 0 | match &proof { |
521 | | // These could be transient errors that should be retried. |
522 | 0 | Err(e) if matches!(e.kind(), ProofErrorKind::Net { .. }) => { |
523 | 0 | debug!("not caching DNSSEC validation with ProofErrorKind::Net") |
524 | | } |
525 | 0 | _ => { |
526 | 0 | self.validation_cache.insert(proof.clone(), key, &context); |
527 | 0 | } |
528 | | } |
529 | | |
530 | 0 | proof |
531 | 0 | } |
532 | | |
533 | | /// DNSKEY-specific verification |
534 | | /// |
535 | | /// A DNSKEY needs to be checked against a DS record provided by the parent zone. |
536 | | /// |
537 | | /// A DNSKEY that's part of the trust anchor does not need to have its DS record (which may |
538 | | /// not exist as it's the case of the root zone) nor its RRSIG validated. If an RRSIG is present |
539 | | /// it will be validated. |
540 | | /// |
541 | | /// # Return |
542 | | /// |
543 | | /// If Ok, returns an RrsetProof containing the proof, adjusted TTL, and an index of the RRSIG used for |
544 | | /// validation of the rrset. |
545 | | /// |
546 | | /// # Panics |
547 | | /// |
548 | | /// This method should only be called to validate DNSKEYs, see `verify_default_rrset` for other record types. |
549 | | /// if a non-DNSKEY RRSET is passed into this method it will always panic. |
550 | 0 | async fn verify_dnskey_rrset( |
551 | 0 | &self, |
552 | 0 | context: &RrsetVerificationContext<'_>, |
553 | 0 | ) -> Result<RrsetProof, ProofError> { |
554 | | let RrsetVerificationContext { |
555 | 0 | rrset, |
556 | 0 | rrsigs, |
557 | 0 | current_time, |
558 | 0 | options, |
559 | | .. |
560 | 0 | } = context; |
561 | | |
562 | | // Ensure that this method is not misused |
563 | 0 | if RecordType::DNSKEY != rrset.record_type { |
564 | 0 | panic!("All other RRSETs must use verify_default_rrset"); |
565 | 0 | } |
566 | | |
567 | 0 | debug!( |
568 | 0 | "dnskey validation {}, record_type: {:?}", |
569 | | rrset.name, rrset.record_type |
570 | | ); |
571 | | |
572 | 0 | let mut dnskey_proofs = |
573 | 0 | Vec::<(Proof, Option<u32>, Option<usize>)>::with_capacity(rrset.records.len()); |
574 | 0 | dnskey_proofs.resize(rrset.records.len(), (Proof::Bogus, None, None)); |
575 | | |
576 | | // check if the DNSKEYs are in the root store |
577 | 0 | for (r, proof) in rrset.records.iter().zip(dnskey_proofs.iter_mut()) { |
578 | 0 | let Some(dnskey) = r.try_borrow::<DNSKEY>() else { |
579 | 0 | continue; |
580 | | }; |
581 | | |
582 | 0 | proof.0 = self.is_dnskey_in_root_store(&dnskey); |
583 | | } |
584 | | |
585 | | // if not all of the DNSKEYs are in the root store, then we need to look for DS records to verify |
586 | 0 | let ds_records = if !dnskey_proofs.iter().all(|p| p.0.is_secure()) && !rrset.name.is_root() |
587 | | { |
588 | | // Need to get DS records for each DNSKEY. |
589 | | // Every DNSKEY other than the root zone's keys may have a corresponding DS record. |
590 | 0 | self.fetch_ds_records(rrset.name.clone(), *options).await? |
591 | | } else { |
592 | 0 | debug!("ignoring DS lookup for root zone or registered keys"); |
593 | 0 | Vec::default() |
594 | | }; |
595 | | |
596 | | // if the DS records are not empty and they also have no supported algorithms, then this is INSECURE |
597 | | // for secure DS records the BOGUS check happens after DNSKEYs are evaluated against the DS |
598 | 0 | if ds_records |
599 | 0 | .iter() |
600 | 0 | .filter(|ds| ds.proof().is_secure() || ds.proof().is_insecure()) |
601 | 0 | .all(|ds| { |
602 | 0 | !ds.data().algorithm().is_supported() || !ds.data().digest_type().is_supported() |
603 | 0 | }) |
604 | 0 | && !ds_records.is_empty() |
605 | | { |
606 | 0 | debug!( |
607 | 0 | "all dnskeys use unsupported algorithms and there are no supported DS records in the parent zone" |
608 | | ); |
609 | | // cannot validate; mark as insecure |
610 | 0 | return Err(ProofError::new( |
611 | 0 | Proof::Insecure, |
612 | 0 | ProofErrorKind::UnsupportedKeyAlgorithm, |
613 | 0 | )); |
614 | 0 | } |
615 | | |
616 | | // verify all dnskeys individually against the DS records |
617 | 0 | for (r, proof) in rrset.records.iter().zip(dnskey_proofs.iter_mut()) { |
618 | 0 | let Some(dnskey) = r.try_borrow() else { |
619 | 0 | continue; |
620 | | }; |
621 | | |
622 | 0 | if proof.0.is_secure() { |
623 | 0 | continue; |
624 | 0 | } |
625 | | |
626 | | // need to track each proof on each dnskey to ensure they are all validated |
627 | 0 | match verify_dnskey(&dnskey, &ds_records) { |
628 | 0 | Ok(pf) => *proof = (pf, None, None), |
629 | 0 | Err(err) => *proof = (err.proof, None, None), |
630 | | } |
631 | | } |
632 | | |
633 | | // There may have been a key-signing key for the zone, |
634 | | // we need to verify all the other DNSKEYS in the zone against it (i.e. the rrset) |
635 | 0 | for (i, rrsig) in rrsigs.iter().enumerate() { |
636 | | // These should all match, but double checking... |
637 | 0 | let signer_name = &rrsig.data().input().signer_name; |
638 | | |
639 | 0 | let rrset_proof = rrset |
640 | 0 | .records |
641 | 0 | .iter() |
642 | 0 | .zip(dnskey_proofs.iter()) |
643 | 0 | .filter(|(_, (proof, ..))| proof.is_secure()) |
644 | 0 | .filter(|(r, _)| r.name() == signer_name) |
645 | 0 | .filter_map(|(r, (proof, ..))| { |
646 | 0 | RecordRef::<'_, DNSKEY>::try_from(*r) |
647 | 0 | .ok() |
648 | 0 | .map(|r| (r, proof)) |
649 | 0 | }) |
650 | 0 | .find_map(|(dnskey, proof)| { |
651 | 0 | verify_rrset_with_dnskey(dnskey, *proof, rrsig, rrset, *current_time).ok() |
652 | 0 | }); |
653 | | |
654 | 0 | if let Some(rrset_proof) = rrset_proof { |
655 | 0 | return Ok(RrsetProof { |
656 | 0 | proof: rrset_proof.0, |
657 | 0 | adjusted_ttl: rrset_proof.1, |
658 | 0 | rrsig_index: Some(i), |
659 | 0 | }); |
660 | 0 | } |
661 | | } |
662 | | |
663 | | // if it was just the root DNSKEYS with no RRSIG, we'll accept the entire set, or none |
664 | 0 | if dnskey_proofs.iter().all(|(proof, ..)| proof.is_secure()) { |
665 | 0 | let proof = dnskey_proofs.pop().unwrap(/* This can not happen due to above test */); |
666 | 0 | return Ok(RrsetProof { |
667 | 0 | proof: proof.0, |
668 | 0 | adjusted_ttl: proof.1, |
669 | 0 | rrsig_index: proof.2, |
670 | 0 | }); |
671 | 0 | } |
672 | | |
673 | 0 | if !ds_records.is_empty() { |
674 | | // there were DS records, but no DNSKEYs, we're in a bogus state |
675 | 0 | trace!("bogus dnskey: {}", rrset.name); |
676 | 0 | return Err(ProofError::new( |
677 | 0 | Proof::Bogus, |
678 | 0 | ProofErrorKind::DsRecordsButNoDnskey { |
679 | 0 | name: rrset.name.clone(), |
680 | 0 | }, |
681 | 0 | )); |
682 | 0 | } |
683 | | |
684 | | // There were DS records or RRSIGs, but none of the signatures could be validated, so we're in a |
685 | | // bogus state. If there was no DS record, it should have gotten an NSEC upstream, and returned |
686 | | // early above. |
687 | 0 | trace!("no dnskey found: {}", rrset.name); |
688 | 0 | Err(ProofError::new( |
689 | 0 | Proof::Bogus, |
690 | 0 | ProofErrorKind::DnskeyNotFound { |
691 | 0 | name: rrset.name.clone(), |
692 | 0 | }, |
693 | 0 | )) |
694 | 0 | } |
695 | | |
696 | | /// Checks whether a DS RRset exists for the zone containing a name. |
697 | | /// |
698 | | /// Returns an error with an `Insecure` proof if the zone is proven to be insecure. Returns |
699 | | /// `Ok(())` if the zone is secure. |
700 | | /// |
701 | | /// This first finds the nearest zone cut at or above the given name, by making NS queries. |
702 | | /// Then, the DS RRset at the delegation point is requested. The DS response is validated to |
703 | | /// determine if any DS records exist or not, and thus whether the zone is secure, insecure, or |
704 | | /// bogus. See [RFC 6840 section 6.1](https://datatracker.ietf.org/doc/html/rfc6840#section-6.1) |
705 | | /// and [RFC 4035 section 4.2](https://datatracker.ietf.org/doc/html/rfc4035#section-4.2). |
706 | 0 | async fn find_ds_records( |
707 | 0 | &self, |
708 | 0 | name: Name, |
709 | 0 | options: DnsRequestOptions, |
710 | 0 | ) -> Result<(), ProofError> { |
711 | 0 | let mut ancestor = name.clone(); |
712 | 0 | let zone = loop { |
713 | 0 | if ancestor.is_root() { |
714 | 0 | return Err(ProofError::ds_should_exist(name)); |
715 | 0 | } |
716 | | |
717 | | // Make an un-verified request for the NS RRset at this ancestor name. |
718 | 0 | let query = Query::query(ancestor.clone(), RecordType::NS); |
719 | 0 | let result = self |
720 | 0 | .handle |
721 | 0 | .lookup(query.clone(), options) |
722 | 0 | .first_answer() |
723 | 0 | .await; |
724 | 0 | match result { |
725 | 0 | Ok(response) => { |
726 | 0 | if response.all_sections().any(|record| { |
727 | 0 | record.record_type() == RecordType::NS && record.name() == &ancestor |
728 | 0 | }) { |
729 | 0 | break ancestor; |
730 | 0 | } |
731 | | } |
732 | 0 | Err(e) if e.is_no_records_found() || e.is_nx_domain() => {} |
733 | 0 | Err(net) => { |
734 | 0 | return Err(ProofError::new( |
735 | 0 | Proof::Bogus, |
736 | 0 | ProofErrorKind::Net { query, net }, |
737 | 0 | )); |
738 | | } |
739 | | } |
740 | | |
741 | 0 | ancestor = ancestor.base_name(); |
742 | | }; |
743 | | |
744 | 0 | self.fetch_ds_records(zone, options).await?; |
745 | 0 | Ok(()) |
746 | 0 | } |
747 | | |
748 | | /// Retrieves DS records for the given zone. |
749 | 0 | async fn fetch_ds_records( |
750 | 0 | &self, |
751 | 0 | zone: Name, |
752 | 0 | options: DnsRequestOptions, |
753 | 0 | ) -> Result<Vec<Record<DS>>, ProofError> { |
754 | 0 | let ds_message = self |
755 | 0 | .lookup(Query::query(zone.clone(), RecordType::DS), options) |
756 | 0 | .first_answer() |
757 | 0 | .await; |
758 | | |
759 | 0 | let error_opt = match ds_message { |
760 | 0 | Ok(mut ds_message) |
761 | 0 | if ds_message |
762 | 0 | .answers() |
763 | 0 | .iter() |
764 | 0 | .filter(|r| r.record_type() == RecordType::DS) |
765 | 0 | .any(|r| r.proof().is_secure()) => |
766 | | { |
767 | | // This is a secure DS RRset. |
768 | | |
769 | 0 | let all_records = ds_message.take_answers().into_iter().filter_map(|r| { |
770 | 0 | r.map(|data| match data { |
771 | 0 | RData::DNSSEC(DNSSECRData::DS(ds)) => Some(ds), |
772 | 0 | _ => None, |
773 | 0 | }) |
774 | 0 | }); |
775 | | |
776 | 0 | let mut supported_records = vec![]; |
777 | 0 | let mut all_unknown = None; |
778 | 0 | for record in all_records { |
779 | | // A chain can be either SECURE or INSECURE, but we should not trust BOGUS or other |
780 | | // records. |
781 | 0 | if (!record.data().algorithm().is_supported() |
782 | 0 | || !record.data().digest_type().is_supported()) |
783 | 0 | && (record.proof().is_secure() || record.proof().is_insecure()) |
784 | | { |
785 | 0 | all_unknown.get_or_insert(true); |
786 | 0 | continue; |
787 | 0 | } |
788 | 0 | all_unknown = Some(false); |
789 | | |
790 | 0 | supported_records.push(record); |
791 | | } |
792 | | |
793 | 0 | if all_unknown.unwrap_or(false) { |
794 | 0 | return Err(ProofError::new( |
795 | 0 | Proof::Insecure, |
796 | 0 | ProofErrorKind::UnknownKeyAlgorithm, |
797 | 0 | )); |
798 | 0 | } else if !supported_records.is_empty() { |
799 | 0 | return Ok(supported_records); |
800 | | } else { |
801 | 0 | None |
802 | | } |
803 | | } |
804 | 0 | Ok(response) => { |
805 | 0 | let any_ds_rr = response |
806 | 0 | .answers() |
807 | 0 | .iter() |
808 | 0 | .any(|r| r.record_type() == RecordType::DS); |
809 | 0 | if any_ds_rr { |
810 | 0 | None |
811 | | } else { |
812 | | // If the response was an authenticated proof of nonexistence, then we have an |
813 | | // insecure zone. |
814 | 0 | debug!("marking {zone} as insecure based on secure NSEC/NSEC3 proof"); |
815 | 0 | return Err(ProofError::new( |
816 | 0 | Proof::Insecure, |
817 | 0 | ProofErrorKind::DsResponseNsec { name: zone }, |
818 | 0 | )); |
819 | | } |
820 | | } |
821 | 0 | Err(error) => Some(error), |
822 | | }; |
823 | | |
824 | | // If the response was an empty DS RRset that was itself insecure, then we have another insecure zone. |
825 | 0 | let dns_err = error_opt.as_ref().and_then(|e| match &e { |
826 | 0 | NetError::Dns(err) => Some(err), |
827 | 0 | _ => None, |
828 | 0 | }); |
829 | | |
830 | 0 | if let Some(DnsError::Nsec { query, proof, .. }) = dns_err { |
831 | 0 | if proof.is_insecure() { |
832 | 0 | debug!( |
833 | 0 | "marking {} as insecure based on insecure NSEC/NSEC3 proof", |
834 | 0 | query.name() |
835 | | ); |
836 | 0 | return Err(ProofError::new( |
837 | 0 | Proof::Insecure, |
838 | 0 | ProofErrorKind::DsResponseNsec { |
839 | 0 | name: query.name().to_owned(), |
840 | 0 | }, |
841 | 0 | )); |
842 | 0 | } |
843 | 0 | } |
844 | | |
845 | 0 | Err(ProofError::ds_should_exist(zone)) |
846 | 0 | } |
847 | | |
848 | | /// Verifies that the key is a trust anchor. |
849 | | /// |
850 | | /// # Returns |
851 | | /// |
852 | | /// Proof::Secure if registered in the root store, Proof::Bogus if not |
853 | 0 | fn is_dnskey_in_root_store(&self, rr: &RecordRef<'_, DNSKEY>) -> Proof { |
854 | 0 | let dns_key = rr.data(); |
855 | 0 | let pub_key = dns_key.public_key(); |
856 | | |
857 | | // Checks to see if the key is valid against the registered root certificates |
858 | 0 | if self.trust_anchor.contains(pub_key) { |
859 | 0 | debug!( |
860 | 0 | "validated dnskey with trust_anchor: {}, {dns_key}", |
861 | 0 | rr.name(), |
862 | | ); |
863 | | |
864 | 0 | Proof::Secure |
865 | | } else { |
866 | 0 | Proof::Bogus |
867 | | } |
868 | 0 | } |
869 | | |
870 | | /// Verifies that a given RRSET is validly signed by any of the specified RRSIGs. |
871 | | /// |
872 | | /// Invalid RRSIGs will be ignored. RRSIGs will only be validated against DNSKEYs which can |
873 | | /// be validated through a chain back to the `trust_anchor`. As long as one RRSIG is valid, |
874 | | /// then the RRSET will be valid. |
875 | | /// |
876 | | /// # Returns |
877 | | /// |
878 | | /// On Ok, the set of (Proof, AdjustedTTL, and IndexOfRRSIG) is returned, where the index is the one of the RRSIG that validated |
879 | | /// the Rrset |
880 | | /// |
881 | | /// # Panics |
882 | | /// |
883 | | /// This method should never be called to validate DNSKEYs, see `verify_dnskey_rrset` instead. |
884 | | /// if a DNSKEY RRSET is passed into this method it will always panic. |
885 | 0 | async fn verify_default_rrset( |
886 | 0 | &self, |
887 | 0 | context: &RrsetVerificationContext<'_>, |
888 | 0 | ) -> Result<RrsetProof, ProofError> { |
889 | | let RrsetVerificationContext { |
890 | 0 | query: original_query, |
891 | 0 | rrset, |
892 | 0 | rrsigs, |
893 | 0 | current_time, |
894 | 0 | options, |
895 | 0 | } = context; |
896 | | |
897 | | // Ensure that this method is not misused |
898 | 0 | if RecordType::DNSKEY == rrset.record_type { |
899 | 0 | panic!("DNSKEYs must be validated with verify_dnskey_rrset"); |
900 | 0 | } |
901 | | |
902 | 0 | if rrsigs.is_empty() { |
903 | | // Decide if we're: |
904 | | // 1) "insecure", the zone has a valid NSEC for the DS record in the parent zone |
905 | | // 2) "bogus", the parent zone has a valid DS record, but the child zone didn't have the RRSIGs/DNSKEYs |
906 | | // or the parent zone has a DS record without covering RRSIG records. |
907 | 0 | if rrset.record_type != RecordType::DS { |
908 | 0 | let mut search_name = rrset.name.clone(); |
909 | 0 | if rrset.record_type == RecordType::NSEC3 { |
910 | 0 | // No need to look for a zone cut at an NSEC3 owner name. Look at its parent |
911 | 0 | // instead, which ought to be a zone apex. |
912 | 0 | search_name = search_name.base_name(); |
913 | 0 | } |
914 | | |
915 | 0 | self.find_ds_records(search_name, *options).await?; // insecure will return early here |
916 | 0 | } |
917 | | |
918 | 0 | return Err(ProofError::new( |
919 | 0 | Proof::Bogus, |
920 | 0 | ProofErrorKind::RrsigsNotPresent { |
921 | 0 | name: rrset.name.clone(), |
922 | 0 | record_type: rrset.record_type, |
923 | 0 | }, |
924 | 0 | )); |
925 | 0 | } |
926 | | |
927 | | // the record set is going to be shared across a bunch of futures, Arc for that. |
928 | 0 | trace!( |
929 | 0 | "default validation {}, record_type: {:?}", |
930 | | rrset.name, rrset.record_type |
931 | | ); |
932 | | |
933 | | // we can validate with any of the rrsigs... |
934 | | // i.e. the first that validates is good enough |
935 | | // TODO: could there be a cert downgrade attack here with a MITM stripping stronger RRSIGs? |
936 | | // we could check for the strongest RRSIG and only use that... |
937 | | // though, since the entire package isn't signed any RRSIG could have been injected, |
938 | | // right? meaning if there is an attack on any of the acceptable algorithms, we'd be |
939 | | // susceptible until that algorithm is removed as an option. |
940 | | // dns over TLS will mitigate this. |
941 | | // TODO: strip RRSIGS to accepted algorithms and make algorithms configurable. |
942 | 0 | let verifications = rrsigs |
943 | 0 | .iter() |
944 | 0 | .enumerate() |
945 | 0 | .filter_map(|(i, rrsig)| { |
946 | 0 | let query = |
947 | 0 | Query::query(rrsig.data().input().signer_name.clone(), RecordType::DNSKEY); |
948 | | |
949 | 0 | if i > MAX_RRSIGS_PER_RRSET { |
950 | 0 | warn!("too many ({i}) RRSIGs for rrset {rrset:?}; skipping"); |
951 | 0 | return None; |
952 | 0 | } |
953 | | |
954 | | // TODO: Should this sig.signer_name should be confirmed to be in the same zone as the rrsigs and rrset? |
955 | | // Break verification cycle |
956 | 0 | if query.name() == original_query.name() |
957 | 0 | && query.query_type() == original_query.query_type() |
958 | | { |
959 | 0 | warn!( |
960 | 0 | query_name = %query.name(), |
961 | 0 | query_type = %query.query_type(), |
962 | 0 | original_query_name = %original_query.name(), |
963 | 0 | original_query_type = %original_query.query_type(), |
964 | 0 | "stopping verification cycle in verify_default_rrset", |
965 | | ); |
966 | 0 | return None; |
967 | 0 | } |
968 | | |
969 | | Some( |
970 | 0 | self.lookup(query.clone(), *options) |
971 | 0 | .first_answer() |
972 | 0 | .map(move |result| match result { |
973 | 0 | Ok(message) => { |
974 | 0 | Ok(verify_rrsig_with_keys(message, rrsig, rrset, *current_time) |
975 | 0 | .map(|(proof, adjusted_ttl)| RrsetProof { |
976 | 0 | proof, |
977 | 0 | adjusted_ttl, |
978 | 0 | rrsig_index: Some(i), |
979 | 0 | })) |
980 | | } |
981 | 0 | Err(net) => Err(ProofError::new( |
982 | 0 | Proof::Bogus, |
983 | 0 | ProofErrorKind::Net { query, net }, |
984 | 0 | )), |
985 | 0 | }), |
986 | | ) |
987 | 0 | }) |
988 | 0 | .collect::<Vec<_>>(); |
989 | | |
990 | | // if there are no available verifications, then we are in a failed state. |
991 | 0 | if verifications.is_empty() { |
992 | 0 | return Err(ProofError::new( |
993 | 0 | Proof::Bogus, |
994 | 0 | ProofErrorKind::RrsigsNotPresent { |
995 | 0 | name: rrset.name.clone(), |
996 | 0 | record_type: rrset.record_type, |
997 | 0 | }, |
998 | 0 | )); |
999 | 0 | } |
1000 | | |
1001 | | // as long as any of the verifications is good, then the RRSET is valid. |
1002 | 0 | let select = future::select_ok(verifications); |
1003 | | |
1004 | | // this will return either a good result or the errors |
1005 | 0 | let (proof, rest) = select.await?; |
1006 | 0 | drop(rest); |
1007 | | |
1008 | 0 | proof.ok_or_else(|| |
1009 | | // we are in a bogus state, DS records were available (see beginning of function), but RRSIGs couldn't be verified |
1010 | 0 | ProofError::new(Proof::Bogus, ProofErrorKind::RrsigsUnverified { |
1011 | 0 | name: rrset.name.clone(), |
1012 | 0 | record_type: rrset.record_type, |
1013 | 0 | } |
1014 | | )) |
1015 | 0 | } |
1016 | | |
1017 | | /// An internal function used to clone the handle, but maintain some information back to the |
1018 | | /// original handle, such as the request_depth such that infinite recursion does |
1019 | | /// not occur. |
1020 | 0 | fn clone_with_context(&self) -> Self { |
1021 | 0 | Self { |
1022 | 0 | handle: self.handle.clone(), |
1023 | 0 | trust_anchor: Arc::clone(&self.trust_anchor), |
1024 | 0 | request_depth: self.request_depth + 1, |
1025 | 0 | nsec3_soft_iteration_limit: self.nsec3_soft_iteration_limit, |
1026 | 0 | nsec3_hard_iteration_limit: self.nsec3_hard_iteration_limit, |
1027 | 0 | validation_cache: self.validation_cache.clone(), |
1028 | 0 | } |
1029 | 0 | } |
1030 | | |
1031 | | /// Get a reference to the underlying handle. |
1032 | 0 | pub fn inner(&self) -> &H { |
1033 | 0 | &self.handle |
1034 | 0 | } |
1035 | | } |
1036 | | |
1037 | | #[cfg(any(feature = "std", feature = "no-std-rand"))] |
1038 | | impl<H: DnsHandle> DnsHandle for DnssecDnsHandle<H> { |
1039 | | type Response = Pin<Box<dyn Stream<Item = Result<DnsResponse, NetError>> + Send>>; |
1040 | | type Runtime = H::Runtime; |
1041 | | |
1042 | 0 | fn is_verifying_dnssec(&self) -> bool { |
1043 | | // This handler is always verifying... |
1044 | 0 | true |
1045 | 0 | } |
1046 | | |
1047 | 0 | fn send(&self, mut request: DnsRequest) -> Self::Response { |
1048 | | // backstop |
1049 | 0 | if self.request_depth > request.options().max_request_depth { |
1050 | 0 | error!("exceeded max validation depth"); |
1051 | 0 | return Box::pin(stream::once(future::err(NetError::from( |
1052 | 0 | "exceeded max validation depth", |
1053 | 0 | )))); |
1054 | 0 | } |
1055 | | |
1056 | | // dnssec only matters on queries. |
1057 | 0 | match request.op_code() { |
1058 | 0 | OpCode::Query => {} |
1059 | 0 | _ => return Box::pin(self.handle.send(request)), |
1060 | | } |
1061 | | |
1062 | | // This will fail on no queries, that is a very odd type of request, isn't it? |
1063 | | // TODO: with mDNS there can be multiple queries |
1064 | 0 | let Some(query) = request.queries().first().cloned() else { |
1065 | 0 | return Box::pin(stream::once(future::err(NetError::from( |
1066 | 0 | "no query in request", |
1067 | 0 | )))); |
1068 | | }; |
1069 | | |
1070 | 0 | let handle = self.clone_with_context(); |
1071 | 0 | request |
1072 | 0 | .extensions_mut() |
1073 | 0 | .get_or_insert_with(Edns::new) |
1074 | 0 | .enable_dnssec(); |
1075 | | |
1076 | 0 | request.set_authentic_data(true); |
1077 | 0 | request.set_checking_disabled(false); |
1078 | 0 | let options = *request.options(); |
1079 | | |
1080 | 0 | Box::pin(self.handle.send(request).then(move |result| { |
1081 | 0 | handle |
1082 | 0 | .clone() |
1083 | 0 | .verify_response(result, query.clone(), options) |
1084 | 0 | })) |
1085 | 0 | } |
1086 | | } |
1087 | | |
1088 | 0 | fn verify_rrsig_with_keys( |
1089 | 0 | dnskey_message: DnsResponse, |
1090 | 0 | rrsig: &RecordRef<'_, RRSIG>, |
1091 | 0 | rrset: &Rrset<'_>, |
1092 | 0 | current_time: u32, |
1093 | 0 | ) -> Option<(Proof, Option<u32>)> { |
1094 | 0 | let mut tag_count = HashMap::<u16, usize>::new(); |
1095 | | |
1096 | 0 | if (rrset.record_type == RecordType::NSEC || rrset.record_type == RecordType::NSEC3) |
1097 | 0 | && rrset.name.num_labels() != rrsig.data().input().num_labels |
1098 | | { |
1099 | 0 | warn!( |
1100 | 0 | "{} record signature claims to be expanded from a wildcard", |
1101 | | rrset.record_type |
1102 | | ); |
1103 | 0 | return None; |
1104 | 0 | } |
1105 | | |
1106 | | // DNSKEYs were already validated by the inner query in the above lookup |
1107 | 0 | let dnskeys = dnskey_message.answers().iter().filter_map(|r| { |
1108 | 0 | let dnskey = r.try_borrow::<DNSKEY>()?; |
1109 | | |
1110 | 0 | let tag = match dnskey.data().calculate_key_tag() { |
1111 | 0 | Ok(tag) => tag, |
1112 | 0 | Err(e) => { |
1113 | 0 | warn!("unable to calculate key tag: {e:?}; skipping key"); |
1114 | 0 | return None; |
1115 | | } |
1116 | | }; |
1117 | | |
1118 | 0 | match tag_count.get_mut(&tag) { |
1119 | 0 | Some(n_keys) => { |
1120 | 0 | *n_keys += 1; |
1121 | 0 | if *n_keys > MAX_KEY_TAG_COLLISIONS { |
1122 | 0 | warn!("too many ({n_keys}) DNSKEYs with key tag {tag}; skipping"); |
1123 | 0 | return None; |
1124 | 0 | } |
1125 | | } |
1126 | 0 | None => _ = tag_count.insert(tag, 1), |
1127 | | } |
1128 | | |
1129 | 0 | Some(dnskey) |
1130 | 0 | }); |
1131 | | |
1132 | 0 | let mut all_insecure = None; |
1133 | 0 | for dnskey in dnskeys { |
1134 | 0 | match dnskey.proof() { |
1135 | | Proof::Secure => { |
1136 | 0 | all_insecure = Some(false); |
1137 | 0 | if let Ok(proof) = |
1138 | 0 | verify_rrset_with_dnskey(dnskey, dnskey.proof(), rrsig, rrset, current_time) |
1139 | | { |
1140 | 0 | return Some((proof.0, proof.1)); |
1141 | 0 | } |
1142 | | } |
1143 | 0 | Proof::Insecure => { |
1144 | 0 | all_insecure.get_or_insert(true); |
1145 | 0 | } |
1146 | 0 | _ => all_insecure = Some(false), |
1147 | | } |
1148 | | } |
1149 | | |
1150 | 0 | if all_insecure.unwrap_or(false) { |
1151 | | // inherit Insecure state |
1152 | 0 | Some((Proof::Insecure, None)) |
1153 | | } else { |
1154 | 0 | None |
1155 | | } |
1156 | 0 | } |
1157 | | |
1158 | | /// Find the SOA record, if present, in the response and return its name. |
1159 | | /// |
1160 | | /// Note that a SOA record may not be present in all responses that must be NSEC/NSEC3 validated. |
1161 | | /// See RFC 4035 B.4 - Referral to Signed Zone, B.5 Referral to Unsigned Zone, B.6 - Wildcard |
1162 | | /// Expansion, RFC 5155 B.3 - Referral to an Opt-Out Unsigned Zone, and B.4 - Wildcard Expansion. |
1163 | 0 | fn find_soa_name(verified_message: &DnsResponse) -> Option<&Name> { |
1164 | 0 | for record in verified_message.authorities() { |
1165 | 0 | if record.record_type() == RecordType::SOA { |
1166 | 0 | return Some(record.name()); |
1167 | 0 | } |
1168 | | } |
1169 | | |
1170 | 0 | None |
1171 | 0 | } |
1172 | | |
1173 | | /// This verifies a DNSKEY record against DS records from a secure delegation. |
1174 | 0 | fn verify_dnskey( |
1175 | 0 | rr: &RecordRef<'_, DNSKEY>, |
1176 | 0 | ds_records: &[Record<DS>], |
1177 | 0 | ) -> Result<Proof, ProofError> { |
1178 | 0 | let key_rdata = rr.data(); |
1179 | 0 | let key_tag = key_rdata.calculate_key_tag().map_err(|_| { |
1180 | 0 | ProofError::new( |
1181 | 0 | Proof::Insecure, |
1182 | 0 | ProofErrorKind::ErrorComputingKeyTag { |
1183 | 0 | name: rr.name().clone(), |
1184 | 0 | }, |
1185 | | ) |
1186 | 0 | })?; |
1187 | 0 | let key_algorithm = key_rdata.algorithm(); |
1188 | | |
1189 | 0 | if !key_algorithm.is_supported() { |
1190 | 0 | return Err(ProofError::new( |
1191 | 0 | Proof::Insecure, |
1192 | 0 | ProofErrorKind::UnsupportedKeyAlgorithm, |
1193 | 0 | )); |
1194 | 0 | } |
1195 | | |
1196 | | // DS check if covered by DS keys |
1197 | 0 | let mut key_authentication_attempts = 0; |
1198 | 0 | for r in ds_records.iter().filter(|ds| ds.proof().is_secure()) { |
1199 | 0 | if r.data().algorithm() != key_algorithm { |
1200 | 0 | trace!( |
1201 | 0 | "skipping DS record due to algorithm mismatch, expected algorithm {}: ({}, {})", |
1202 | | key_algorithm, |
1203 | 0 | r.name(), |
1204 | 0 | r.data(), |
1205 | | ); |
1206 | | |
1207 | 0 | continue; |
1208 | 0 | } |
1209 | | |
1210 | 0 | if r.data().key_tag() != key_tag { |
1211 | 0 | trace!( |
1212 | 0 | "skipping DS record due to key tag mismatch, expected tag {key_tag}: ({}, {})", |
1213 | 0 | r.name(), |
1214 | 0 | r.data(), |
1215 | | ); |
1216 | | |
1217 | 0 | continue; |
1218 | 0 | } |
1219 | | |
1220 | | // Count the number of DS records with the same algorithm and key tag as this DNSKEY. |
1221 | | // Ignore remaining DS records if there are too many key tag collisions. Doing so before |
1222 | | // checking hashes or signatures protects us from KeyTrap denial of service attacks. |
1223 | 0 | key_authentication_attempts += 1; |
1224 | 0 | if key_authentication_attempts > MAX_KEY_TAG_COLLISIONS { |
1225 | 0 | warn!( |
1226 | | key_tag, |
1227 | | attempts = key_authentication_attempts, |
1228 | 0 | "too many DS records with same key tag; skipping" |
1229 | | ); |
1230 | 0 | continue; |
1231 | 0 | } |
1232 | | |
1233 | 0 | if !r.data().covers(rr.name(), key_rdata).unwrap_or(false) { |
1234 | 0 | continue; |
1235 | 0 | } |
1236 | | |
1237 | 0 | debug!( |
1238 | 0 | "validated dnskey ({}, {key_rdata}) with {} {}", |
1239 | 0 | rr.name(), |
1240 | 0 | r.name(), |
1241 | 0 | r.data(), |
1242 | | ); |
1243 | | |
1244 | | // If this key is valid, then it is secure |
1245 | 0 | return Ok(Proof::Secure); |
1246 | | } |
1247 | | |
1248 | 0 | trace!("bogus dnskey: {}", rr.name()); |
1249 | 0 | Err(ProofError::new( |
1250 | 0 | Proof::Bogus, |
1251 | 0 | ProofErrorKind::DnsKeyHasNoDs { |
1252 | 0 | name: rr.name().clone(), |
1253 | 0 | }, |
1254 | 0 | )) |
1255 | 0 | } |
1256 | | |
1257 | | /// Verifies the given SIG of the RRSET with the DNSKEY. |
1258 | 0 | fn verify_rrset_with_dnskey( |
1259 | 0 | dnskey: RecordRef<'_, DNSKEY>, |
1260 | 0 | dnskey_proof: Proof, |
1261 | 0 | rrsig: &RecordRef<'_, RRSIG>, |
1262 | 0 | rrset: &Rrset<'_>, |
1263 | 0 | current_time: u32, |
1264 | 0 | ) -> Result<(Proof, Option<u32>), ProofError> { |
1265 | 0 | match dnskey_proof { |
1266 | 0 | Proof::Secure => (), |
1267 | 0 | proof => { |
1268 | 0 | debug!("insecure dnskey {} {}", dnskey.name(), dnskey.data()); |
1269 | 0 | return Err(ProofError::new( |
1270 | 0 | proof, |
1271 | 0 | ProofErrorKind::InsecureDnsKey { |
1272 | 0 | name: dnskey.name().clone(), |
1273 | 0 | key_tag: rrsig.data().input.key_tag, |
1274 | 0 | }, |
1275 | 0 | )); |
1276 | | } |
1277 | | } |
1278 | | |
1279 | 0 | if dnskey.data().revoke() { |
1280 | 0 | debug!("revoked dnskey {} {}", dnskey.name(), dnskey.data()); |
1281 | 0 | return Err(ProofError::new( |
1282 | 0 | Proof::Bogus, |
1283 | 0 | ProofErrorKind::DnsKeyRevoked { |
1284 | 0 | name: dnskey.name().clone(), |
1285 | 0 | key_tag: rrsig.data().input.key_tag, |
1286 | 0 | }, |
1287 | 0 | )); |
1288 | 0 | } // TODO: does this need to be validated? RFC 5011 |
1289 | 0 | if !dnskey.data().zone_key() { |
1290 | 0 | return Err(ProofError::new( |
1291 | 0 | Proof::Bogus, |
1292 | 0 | ProofErrorKind::NotZoneDnsKey { |
1293 | 0 | name: dnskey.name().clone(), |
1294 | 0 | key_tag: rrsig.data().input.key_tag, |
1295 | 0 | }, |
1296 | 0 | )); |
1297 | 0 | } |
1298 | 0 | if dnskey.data().algorithm() != rrsig.data().input.algorithm { |
1299 | 0 | return Err(ProofError::new( |
1300 | 0 | Proof::Bogus, |
1301 | 0 | ProofErrorKind::AlgorithmMismatch { |
1302 | 0 | rrsig: rrsig.data().input.algorithm, |
1303 | 0 | dnskey: dnskey.data().algorithm(), |
1304 | 0 | }, |
1305 | 0 | )); |
1306 | 0 | } |
1307 | | |
1308 | 0 | let validity = RrsigValidity::check(*rrsig, rrset, dnskey, current_time); |
1309 | 0 | if !matches!(validity, RrsigValidity::ValidRrsig) { |
1310 | | // TODO better error handling when the error payload is not immediately discarded by |
1311 | | // the caller |
1312 | 0 | return Err(ProofError::new( |
1313 | 0 | Proof::Bogus, |
1314 | 0 | ProofErrorKind::Msg(format!("{validity:?}")), |
1315 | 0 | )); |
1316 | 0 | } |
1317 | | |
1318 | 0 | dnskey |
1319 | 0 | .data() |
1320 | 0 | .verify_rrsig( |
1321 | 0 | &rrset.name, |
1322 | 0 | rrset.record_class, |
1323 | 0 | rrsig.data(), |
1324 | 0 | rrset.records.iter().copied(), |
1325 | | ) |
1326 | 0 | .map(|_| { |
1327 | 0 | debug!( |
1328 | 0 | "validated ({}, {:?}) with ({}, {})", |
1329 | | rrset.name, |
1330 | | rrset.record_type, |
1331 | 0 | dnskey.name(), |
1332 | 0 | dnskey.data() |
1333 | | ); |
1334 | 0 | ( |
1335 | 0 | Proof::Secure, |
1336 | 0 | Some(rrsig.data().authenticated_ttl(rrset.record(), current_time)), |
1337 | 0 | ) |
1338 | 0 | }) |
1339 | 0 | .map_err(|e| { |
1340 | 0 | debug!( |
1341 | 0 | "failed validation of ({}, {:?}) with ({}, {})", |
1342 | | rrset.name, |
1343 | | rrset.record_type, |
1344 | 0 | dnskey.name(), |
1345 | 0 | dnskey.data() |
1346 | | ); |
1347 | 0 | ProofError::new( |
1348 | 0 | Proof::Bogus, |
1349 | 0 | ProofErrorKind::DnsKeyVerifyRrsig { |
1350 | 0 | name: dnskey.name().clone(), |
1351 | 0 | key_tag: rrsig.data().input.key_tag, |
1352 | 0 | error: e, |
1353 | 0 | }, |
1354 | | ) |
1355 | 0 | }) |
1356 | 0 | } |
1357 | | |
1358 | | #[derive(Clone, Copy, Debug)] |
1359 | | enum RrsigValidity { |
1360 | | /// RRSIG has already expired |
1361 | | ExpiredRrsig, |
1362 | | /// RRSIG is valid |
1363 | | ValidRrsig, |
1364 | | /// DNSKEY does not match RRSIG |
1365 | | WrongDnskey, |
1366 | | /// RRSIG does not match RRset |
1367 | | WrongRrsig, |
1368 | | } |
1369 | | |
1370 | | impl RrsigValidity { |
1371 | | // see section 5.3.1 of RFC4035 "Checking the RRSIG RR Validity" |
1372 | 0 | fn check( |
1373 | 0 | rrsig: RecordRef<'_, RRSIG>, |
1374 | 0 | rrset: &Rrset<'_>, |
1375 | 0 | dnskey: RecordRef<'_, DNSKEY>, |
1376 | 0 | current_time: u32, |
1377 | 0 | ) -> Self { |
1378 | 0 | let Ok(dnskey_key_tag) = dnskey.data().calculate_key_tag() else { |
1379 | 0 | return Self::WrongDnskey; |
1380 | | }; |
1381 | | |
1382 | 0 | let current_time = SerialNumber(current_time); |
1383 | 0 | let sig_input = rrsig.data().input(); |
1384 | | if !( |
1385 | | // "The RRSIG RR and the RRset MUST have the same owner name and the same class" |
1386 | 0 | rrsig.name() == &rrset.name && |
1387 | 0 | rrsig.dns_class() == rrset.record_class && |
1388 | | |
1389 | | // "The RRSIG RR's Signer's Name field MUST be the name of the zone that contains the RRset" |
1390 | | // TODO(^) the zone name is in the SOA record, which is not accessible from here |
1391 | | |
1392 | | // "The RRSIG RR's Type Covered field MUST equal the RRset's type" |
1393 | 0 | sig_input.type_covered == rrset.record_type && |
1394 | | |
1395 | | // "The number of labels in the RRset owner name MUST be greater than or equal to the value |
1396 | | // in the RRSIG RR's Labels field" |
1397 | 0 | rrset.name.num_labels() >= sig_input.num_labels |
1398 | | ) { |
1399 | 0 | return Self::WrongRrsig; |
1400 | 0 | } |
1401 | | |
1402 | | // Section 3.1.5 of RFC4034 states that 'all comparisons involving these fields MUST use |
1403 | | // "Serial number arithmetic", as defined in RFC1982' |
1404 | | if !( |
1405 | | // "The validator's notion of the current time MUST be less than or equal to the time listed |
1406 | | // in the RRSIG RR's Expiration field" |
1407 | 0 | current_time <= sig_input.sig_expiration && |
1408 | | |
1409 | | // "The validator's notion of the current time MUST be greater than or equal to the time |
1410 | | // listed in the RRSIG RR's Inception field" |
1411 | 0 | current_time >= sig_input.sig_inception |
1412 | | ) { |
1413 | 0 | return Self::ExpiredRrsig; |
1414 | 0 | } |
1415 | | |
1416 | | if !( |
1417 | | // "The RRSIG RR's Signer's Name, Algorithm, and Key Tag fields MUST match the owner name, |
1418 | | // algorithm, and key tag for some DNSKEY RR in the zone's apex DNSKEY RRset" |
1419 | 0 | &sig_input.signer_name == dnskey.name() && |
1420 | 0 | sig_input.algorithm == dnskey.data().algorithm() && |
1421 | 0 | sig_input.key_tag == dnskey_key_tag && |
1422 | | // "The matching DNSKEY RR MUST be present in the zone's apex DNSKEY RRset, and MUST have the |
1423 | | // Zone Flag bit (DNSKEY RDATA Flag bit 7) set" |
1424 | 0 | dnskey.data().zone_key() |
1425 | | ) { |
1426 | 0 | return Self::WrongDnskey; |
1427 | 0 | } |
1428 | | |
1429 | 0 | Self::ValidRrsig |
1430 | 0 | } |
1431 | | } |
1432 | | |
1433 | | #[derive(Clone)] |
1434 | | struct RrsetProof { |
1435 | | proof: Proof, |
1436 | | adjusted_ttl: Option<u32>, |
1437 | | rrsig_index: Option<usize>, |
1438 | | } |
1439 | | |
1440 | | #[derive(Clone)] |
1441 | | #[allow(clippy::type_complexity)] |
1442 | | struct ValidationCache { |
1443 | | inner: Arc<Mutex<LruCache<ValidationCacheKey, (Instant, Result<RrsetProof, ProofError>)>>>, |
1444 | | negative_ttl: Option<RangeInclusive<Duration>>, |
1445 | | positive_ttl: Option<RangeInclusive<Duration>>, |
1446 | | } |
1447 | | |
1448 | | impl ValidationCache { |
1449 | 0 | fn new(capacity: usize) -> Self { |
1450 | 0 | Self { |
1451 | 0 | inner: Arc::new(Mutex::new(LruCache::new(capacity))), |
1452 | 0 | negative_ttl: None, |
1453 | 0 | positive_ttl: None, |
1454 | 0 | } |
1455 | 0 | } |
1456 | | |
1457 | 0 | fn get( |
1458 | 0 | &self, |
1459 | 0 | key: &ValidationCacheKey, |
1460 | 0 | context: &RrsetVerificationContext<'_>, |
1461 | 0 | ) -> Option<Result<RrsetProof, ProofError>> { |
1462 | 0 | let (ttl, cached) = self.inner.lock().get_mut(key)?.clone(); |
1463 | | |
1464 | 0 | if Instant::now() < ttl { |
1465 | 0 | debug!( |
1466 | | name = ?context.rrset.name, |
1467 | | record_type = ?context.rrset.record_type, |
1468 | 0 | "returning cached DNSSEC validation", |
1469 | | ); |
1470 | 0 | Some(cached) |
1471 | | } else { |
1472 | 0 | debug!( |
1473 | | name = ?context.rrset.name, |
1474 | | record_type = ?context.rrset.record_type, |
1475 | 0 | "cached DNSSEC validation expired" |
1476 | | ); |
1477 | 0 | None |
1478 | | } |
1479 | 0 | } |
1480 | | |
1481 | 0 | fn insert( |
1482 | 0 | &self, |
1483 | 0 | proof: Result<RrsetProof, ProofError>, |
1484 | 0 | key: ValidationCacheKey, |
1485 | 0 | cx: &RrsetVerificationContext<'_>, |
1486 | 0 | ) { |
1487 | 0 | debug!( |
1488 | | name = ?cx.rrset.name, |
1489 | | record_type = ?cx.rrset.record_type, |
1490 | 0 | "inserting DNSSEC validation cache entry", |
1491 | | ); |
1492 | | |
1493 | 0 | let (mut min, mut max) = (Duration::from_secs(0), Duration::from_secs(u64::MAX)); |
1494 | 0 | if proof.is_err() { |
1495 | 0 | if let Some(negative_bounds) = self.negative_ttl.clone() { |
1496 | 0 | (min, max) = negative_bounds.into_inner(); |
1497 | 0 | } |
1498 | 0 | } else if let Some(positive_bounds) = self.positive_ttl.clone() { |
1499 | 0 | (min, max) = positive_bounds.into_inner(); |
1500 | 0 | } |
1501 | | |
1502 | 0 | self.inner.lock().insert( |
1503 | 0 | key, |
1504 | 0 | ( |
1505 | 0 | Instant::now() |
1506 | 0 | + Duration::from_secs(cx.rrset.record().ttl().into()).clamp(min, max), |
1507 | 0 | proof.clone(), |
1508 | 0 | ), |
1509 | 0 | ); |
1510 | 0 | } |
1511 | | } |
1512 | | |
1513 | | struct RrsetVerificationContext<'a> { |
1514 | | query: &'a Query, |
1515 | | rrset: &'a Rrset<'a>, |
1516 | | rrsigs: Vec<RecordRef<'a, RRSIG>>, |
1517 | | options: DnsRequestOptions, |
1518 | | current_time: u32, |
1519 | | } |
1520 | | |
1521 | | impl<'a> RrsetVerificationContext<'a> { |
1522 | | // Build a cache lookup key based on the query, rrset, and rrsigs contents, minus the TTLs |
1523 | | // for each, since the recursor cache will return an adjusted TTL for each request and |
1524 | | // cause cache misses. |
1525 | 0 | fn key(&self) -> ValidationCacheKey { |
1526 | 0 | let mut hasher = DefaultHasher::new(); |
1527 | 0 | self.query.name().hash(&mut hasher); |
1528 | 0 | self.query.query_class().hash(&mut hasher); |
1529 | 0 | self.query.query_type().hash(&mut hasher); |
1530 | 0 | self.rrset.name.hash(&mut hasher); |
1531 | 0 | self.rrset.record_class.hash(&mut hasher); |
1532 | 0 | self.rrset.record_type.hash(&mut hasher); |
1533 | | |
1534 | 0 | for rec in &self.rrset.records { |
1535 | 0 | rec.name().hash(&mut hasher); |
1536 | 0 | rec.dns_class().hash(&mut hasher); |
1537 | 0 | rec.data().hash(&mut hasher); |
1538 | 0 | } |
1539 | | |
1540 | 0 | for rec in &self.rrsigs { |
1541 | 0 | rec.name().hash(&mut hasher); |
1542 | 0 | rec.dns_class().hash(&mut hasher); |
1543 | 0 | rec.data().hash(&mut hasher); |
1544 | 0 | } |
1545 | | |
1546 | 0 | ValidationCacheKey(hasher.finish()) |
1547 | 0 | } |
1548 | | } |
1549 | | |
1550 | | #[derive(Hash, Eq, PartialEq)] |
1551 | | struct ValidationCacheKey(u64); |
1552 | | |
1553 | | /// Verifies NSEC records |
1554 | | /// |
1555 | | /// ```text |
1556 | | /// RFC 4035 DNSSEC Protocol Modifications March 2005 |
1557 | | /// |
1558 | | /// 5.4. Authenticated Denial of Existence |
1559 | | /// |
1560 | | /// A resolver can use authenticated NSEC RRs to prove that an RRset is |
1561 | | /// not present in a signed zone. Security-aware name servers should |
1562 | | /// automatically include any necessary NSEC RRs for signed zones in |
1563 | | /// their responses to security-aware resolvers. |
1564 | | /// |
1565 | | /// Denial of existence is determined by the following rules: |
1566 | | /// |
1567 | | /// o If the requested RR name matches the owner name of an |
1568 | | /// authenticated NSEC RR, then the NSEC RR's type bit map field lists |
1569 | | /// all RR types present at that owner name, and a resolver can prove |
1570 | | /// that the requested RR type does not exist by checking for the RR |
1571 | | /// type in the bit map. If the number of labels in an authenticated |
1572 | | /// NSEC RR's owner name equals the Labels field of the covering RRSIG |
1573 | | /// RR, then the existence of the NSEC RR proves that wildcard |
1574 | | /// expansion could not have been used to match the request. |
1575 | | /// |
1576 | | /// o If the requested RR name would appear after an authenticated NSEC |
1577 | | /// RR's owner name and before the name listed in that NSEC RR's Next |
1578 | | /// Domain Name field according to the canonical DNS name order |
1579 | | /// defined in [RFC4034], then no RRsets with the requested name exist |
1580 | | /// in the zone. However, it is possible that a wildcard could be |
1581 | | /// used to match the requested RR owner name and type, so proving |
1582 | | /// that the requested RRset does not exist also requires proving that |
1583 | | /// no possible wildcard RRset exists that could have been used to |
1584 | | /// generate a positive response. |
1585 | | /// |
1586 | | /// In addition, security-aware resolvers MUST authenticate the NSEC |
1587 | | /// RRsets that comprise the non-existence proof as described in Section |
1588 | | /// 5.3. |
1589 | | /// |
1590 | | /// To prove the non-existence of an RRset, the resolver must be able to |
1591 | | /// verify both that the queried RRset does not exist and that no |
1592 | | /// relevant wildcard RRset exists. Proving this may require more than |
1593 | | /// one NSEC RRset from the zone. If the complete set of necessary NSEC |
1594 | | /// RRsets is not present in a response (perhaps due to message |
1595 | | /// truncation), then a security-aware resolver MUST resend the query in |
1596 | | /// order to attempt to obtain the full collection of NSEC RRs necessary |
1597 | | /// to verify the non-existence of the requested RRset. As with all DNS |
1598 | | /// operations, however, the resolver MUST bound the work it puts into |
1599 | | /// answering any particular query. |
1600 | | /// |
1601 | | /// Since a validated NSEC RR proves the existence of both itself and its |
1602 | | /// corresponding RRSIG RR, a validator MUST ignore the settings of the |
1603 | | /// NSEC and RRSIG bits in an NSEC RR. |
1604 | | /// ``` |
1605 | 0 | fn verify_nsec( |
1606 | 0 | query: &Query, |
1607 | 0 | soa_name: Option<&Name>, |
1608 | 0 | response_code: ResponseCode, |
1609 | 0 | answers: &[Record], |
1610 | 0 | nsecs: &[(&Name, &NSEC)], |
1611 | 0 | ) -> Proof { |
1612 | | // TODO: consider converting this to Result, and giving explicit reason for the failure |
1613 | | |
1614 | 0 | let nsec1_yield = |
1615 | 0 | |proof: Proof, msg: &str| -> Proof { proof_log_yield(proof, query, "nsec1", msg) }; |
1616 | | |
1617 | 0 | if response_code != ResponseCode::NXDomain && response_code != ResponseCode::NoError { |
1618 | 0 | return nsec1_yield(Proof::Bogus, "unsupported response code"); |
1619 | 0 | } |
1620 | | |
1621 | | // The SOA name, if present, must be an ancestor of the query name. If a SOA is present, |
1622 | | // we'll use that as the starting value for next_closest_encloser, otherwise, fall back to |
1623 | | // the parent of the query name. |
1624 | 0 | let mut next_closest_encloser = if let Some(soa_name) = soa_name { |
1625 | 0 | if !soa_name.zone_of(query.name()) { |
1626 | 0 | return nsec1_yield(Proof::Bogus, "SOA record is for the wrong zone"); |
1627 | 0 | } |
1628 | 0 | soa_name.clone() |
1629 | | } else { |
1630 | 0 | query.name().base_name() |
1631 | | }; |
1632 | | |
1633 | 0 | let have_answer = !answers.is_empty(); |
1634 | | |
1635 | | // For a no data response with a directly matching NSEC record, we just need to verify the NSEC |
1636 | | // type set does not contain the query type or CNAME. |
1637 | 0 | if let Some((_, nsec_data)) = nsecs.iter().find(|(name, _)| query.name() == *name) { |
1638 | 0 | return if nsec_data.type_set().contains(query.query_type()) |
1639 | 0 | || nsec_data.type_set().contains(RecordType::CNAME) |
1640 | | { |
1641 | 0 | nsec1_yield(Proof::Bogus, "direct match, record type should be present") |
1642 | 0 | } else if response_code == ResponseCode::NoError && !have_answer { |
1643 | 0 | nsec1_yield(Proof::Secure, "direct match") |
1644 | | } else { |
1645 | 0 | nsec1_yield( |
1646 | 0 | Proof::Bogus, |
1647 | 0 | "nxdomain response or answers present when direct match exists", |
1648 | 0 | ) |
1649 | | }; |
1650 | 0 | } |
1651 | | |
1652 | 0 | let Some((covering_nsec_name, covering_nsec_data)) = |
1653 | 0 | find_nsec_covering_record(soa_name, query.name(), nsecs) |
1654 | | else { |
1655 | 0 | return nsec1_yield( |
1656 | 0 | Proof::Bogus, |
1657 | 0 | "no NSEC record matches or covers the query name", |
1658 | 0 | ); |
1659 | | }; |
1660 | | |
1661 | | // Identify the names that exist (including names of empty non terminals) that are parents of |
1662 | | // the query name. Pick the longest such name, because wildcard synthesis would start looking |
1663 | | // for a wildcard record there. |
1664 | 0 | for seed_name in [covering_nsec_name, covering_nsec_data.next_domain_name()] { |
1665 | 0 | let mut candidate_name = seed_name.clone(); |
1666 | 0 | while candidate_name.num_labels() > next_closest_encloser.num_labels() { |
1667 | 0 | if candidate_name.zone_of(query.name()) { |
1668 | 0 | next_closest_encloser = candidate_name; |
1669 | 0 | break; |
1670 | 0 | } |
1671 | 0 | candidate_name = candidate_name.base_name(); |
1672 | | } |
1673 | | } |
1674 | | |
1675 | 0 | let Ok(wildcard_name) = next_closest_encloser.prepend_label("*") else { |
1676 | | // This fails if the prepended label is invalid or if the wildcard name would be too long. |
1677 | | // However, we already know that the query name is not too long. The next closest enclosing |
1678 | | // name must be strictly shorter than the query name, since we know that there is no NSEC |
1679 | | // record matching the query name. Thus the query name must be as long or longer than this |
1680 | | // wildcard name we are trying to construct, because we removed at least one label from the |
1681 | | // query name, and tried to add a single-byte label. This error condition should thus be |
1682 | | // unreachable. |
1683 | 0 | return nsec1_yield(Proof::Bogus, "unreachable error constructing wildcard name"); |
1684 | | }; |
1685 | | |
1686 | 0 | debug!(%wildcard_name, "looking for NSEC for wildcard"); |
1687 | | |
1688 | | // Identify the name of wildcard used to generate the response. This will be used to prove that no closer matches |
1689 | | // exist between the query name and the wildcard. |
1690 | 0 | let wildcard_base_name = if have_answer { |
1691 | | // For wildcard expansion responses, identify an RRSIG that: |
1692 | | // 1) Is a wildcard RRSIG (fewer rrsig labels than owner name labels) and is not longer than the query name. |
1693 | | // 2) Is a parent of the query name |
1694 | | // |
1695 | | // There should be only one of these, but if there are multiple, we'll pick the one with the fewest labels (the harder of the |
1696 | | // provided RRSIGs to validate, since more names have to be covered as a result.) |
1697 | 0 | answers |
1698 | 0 | .iter() |
1699 | 0 | .filter_map(|r| { |
1700 | 0 | if r.proof() != Proof::Secure { |
1701 | 0 | debug!(name = ?r.name(), "ignoring RRSIG with insecure proof for wildcard_base_name"); |
1702 | 0 | return None; |
1703 | 0 | } |
1704 | | |
1705 | 0 | let RData::DNSSEC(DNSSECRData::RRSIG(rrsig)) = r.data() else { |
1706 | 0 | return None; |
1707 | | }; |
1708 | | |
1709 | 0 | let rrsig_labels = rrsig.input.num_labels; |
1710 | 0 | if rrsig_labels >= r.name().num_labels() || rrsig_labels >= query.name().num_labels() { |
1711 | 0 | debug!(name = ?r.name(), labels = ?r.name().num_labels(), rrsig_labels, "ignoring RRSIG for wildcard base name rrsig_labels >= labels"); |
1712 | 0 | return None; |
1713 | 0 | } |
1714 | | |
1715 | 0 | let trimmed_name = r.name().trim_to(rrsig_labels as usize); |
1716 | 0 | if !trimmed_name.zone_of(query.name()) { |
1717 | 0 | debug!(name = ?r.name(), query_name = ?query.name(), "ignoring RRSIG for wildcard base name: RRSIG wildcard labels not a parent of query name"); |
1718 | 0 | return None; |
1719 | 0 | } |
1720 | | |
1721 | 0 | Some((rrsig_labels, trimmed_name.prepend_label("*").ok()?)) |
1722 | 0 | }).min_by_key(|(labels, _)| *labels) |
1723 | 0 | .map(|(_, name)| name) |
1724 | | } else { |
1725 | | // For no data responses, we have to recover the base name from a wildcard NSEC record as there are no answer RRSIGs present. |
1726 | 0 | nsecs |
1727 | 0 | .iter() |
1728 | 0 | .filter(|(name, _)| name.is_wildcard() && name.base_name().zone_of(query.name())) |
1729 | 0 | .min_by_key(|(name, _)| name.num_labels()) |
1730 | 0 | .map(|(name, _)| (*name).clone()) |
1731 | | }; |
1732 | | |
1733 | 0 | match find_nsec_covering_record(soa_name, &wildcard_name, nsecs) { |
1734 | | // For NXDomain responses, we've already proved the record does not exist. Now we just need to prove |
1735 | | // the wildcard name is covered. |
1736 | 0 | Some((_, _)) if response_code == ResponseCode::NXDomain && !have_answer => { |
1737 | 0 | nsec1_yield(Proof::Secure, "no direct match, no wildcard") |
1738 | | } |
1739 | | // For wildcard expansion responses, we need to prove there are no closer matches and no exact match. |
1740 | | // (RFC 4035 5.3.4 and B.6/C.6) |
1741 | | Some((_, _)) |
1742 | 0 | if response_code == ResponseCode::NoError |
1743 | 0 | && have_answer |
1744 | 0 | && no_closer_matches( |
1745 | 0 | query.name(), |
1746 | 0 | soa_name, |
1747 | 0 | nsecs, |
1748 | 0 | wildcard_base_name.as_ref(), |
1749 | | ) |
1750 | 0 | && find_nsec_covering_record(soa_name, query.name(), nsecs).is_some() => |
1751 | | { |
1752 | 0 | nsec1_yield( |
1753 | 0 | Proof::Secure, |
1754 | 0 | "no direct match, covering wildcard present for wildcard expansion response", |
1755 | 0 | ) |
1756 | | } |
1757 | | // For wildcard no data responses, we need to prove a wildcard matching wildcard_name does not contain |
1758 | | // the requested record type and that no closer match exists. (RFC 4035 3.1.3.4 and B.7/C.7) |
1759 | 0 | None if !have_answer |
1760 | 0 | && response_code == ResponseCode::NoError |
1761 | 0 | && nsecs.iter().any(|(name, nsec_data)| { |
1762 | 0 | name == &&wildcard_name |
1763 | 0 | && !nsec_data.type_set().contains(query.query_type()) |
1764 | 0 | && !nsec_data.type_set().contains(RecordType::CNAME) |
1765 | 0 | && no_closer_matches(query.name(), soa_name, nsecs, wildcard_base_name.as_ref()) |
1766 | 0 | }) => |
1767 | | { |
1768 | 0 | nsec1_yield(Proof::Secure, "no direct match, covering wildcard present") |
1769 | | } |
1770 | 0 | _ => nsec1_yield( |
1771 | 0 | Proof::Bogus, |
1772 | 0 | "no NSEC record matches or covers the wildcard name", |
1773 | 0 | ), |
1774 | | } |
1775 | 0 | } |
1776 | | |
1777 | | // Prove that no closer name exists between the query name and wildcard_base_name |
1778 | 0 | fn no_closer_matches( |
1779 | 0 | query_name: &Name, |
1780 | 0 | soa: Option<&Name>, |
1781 | 0 | nsecs: &[(&'_ Name, &'_ NSEC)], |
1782 | 0 | wildcard_base_name: Option<&Name>, |
1783 | 0 | ) -> bool { |
1784 | 0 | let Some(wildcard_base_name) = wildcard_base_name else { |
1785 | 0 | return false; |
1786 | | }; |
1787 | | |
1788 | | // If the SOA name is present, the query name and wildcard base name must be children of it. |
1789 | 0 | if let Some(soa) = soa { |
1790 | 0 | if !soa.zone_of(wildcard_base_name) { |
1791 | 0 | debug!(%wildcard_base_name, %soa, "wildcard_base_name is not a child of SOA"); |
1792 | 0 | return false; |
1793 | 0 | } |
1794 | | |
1795 | 0 | if !soa.zone_of(query_name) { |
1796 | 0 | debug!(%query_name, %soa, "query_name is not a child of SOA"); |
1797 | 0 | return false; |
1798 | 0 | } |
1799 | 0 | } |
1800 | | |
1801 | 0 | if wildcard_base_name.num_labels() > query_name.num_labels() { |
1802 | 0 | debug!(%wildcard_base_name, %query_name, "wildcard_base_name cannot have more labels than query_name"); |
1803 | 0 | return false; |
1804 | 0 | } |
1805 | | |
1806 | | // The query name must be a child of the wildcard (minus the *) |
1807 | 0 | if !wildcard_base_name.base_name().zone_of(query_name) { |
1808 | 0 | debug!(%wildcard_base_name, %query_name, "query_name is not a child of wildcard_name"); |
1809 | 0 | return false; |
1810 | 0 | } |
1811 | | |
1812 | | // Verify that an appropriate proof exists for each wildcard between query.name() and wildcard_base_name. |
1813 | 0 | let mut name = query_name.base_name(); |
1814 | 0 | while name.num_labels() > wildcard_base_name.num_labels() { |
1815 | 0 | let Ok(wildcard) = name.prepend_label("*") else { |
1816 | 0 | return false; |
1817 | | }; |
1818 | | |
1819 | 0 | if find_nsec_covering_record(soa, &wildcard, nsecs).is_none() { |
1820 | 0 | debug!(%wildcard, %name, ?nsecs, "covering record does not exist for name"); |
1821 | 0 | return false; |
1822 | 0 | } |
1823 | | |
1824 | 0 | name = name.base_name(); |
1825 | | } |
1826 | | |
1827 | 0 | true |
1828 | 0 | } |
1829 | | |
1830 | | /// Find the NSEC record covering `test_name`, if any. |
1831 | 0 | fn find_nsec_covering_record<'a>( |
1832 | 0 | soa_name: Option<&Name>, |
1833 | 0 | test_name: &Name, |
1834 | 0 | nsecs: &[(&'a Name, &'a NSEC)], |
1835 | 0 | ) -> Option<(&'a Name, &'a NSEC)> { |
1836 | 0 | nsecs.iter().copied().find(|(nsec_name, nsec_data)| { |
1837 | 0 | let next_domain_name = nsec_data.next_domain_name(); |
1838 | | |
1839 | 0 | test_name > nsec_name |
1840 | 0 | && (test_name < next_domain_name || Some(next_domain_name) == soa_name) |
1841 | 0 | }) |
1842 | 0 | } |
1843 | | |
1844 | | /// Logs a debug message and yields a Proof type for return |
1845 | 0 | pub(super) fn proof_log_yield( |
1846 | 0 | proof: Proof, |
1847 | 0 | query: &Query, |
1848 | 0 | nsec_type: &str, |
1849 | 0 | msg: impl Display, |
1850 | 0 | ) -> Proof { |
1851 | 0 | debug!( |
1852 | 0 | "{nsec_type} proof for {name}, returning {proof}: {msg}", |
1853 | 0 | name = query.name() |
1854 | | ); |
1855 | 0 | proof |
1856 | 0 | } Unexecuted instantiation: hickory_proto::dnssec::handle::proof_log_yield::<core::fmt::Arguments> Unexecuted instantiation: hickory_proto::dnssec::handle::proof_log_yield::<&str> |
1857 | | |
1858 | | mod rrset { |
1859 | | use alloc::vec::Vec; |
1860 | | |
1861 | | use crate::rr::{DNSClass, Name, Record, RecordType}; |
1862 | | |
1863 | | // TODO: combine this with crate::rr::RecordSet? |
1864 | | #[derive(Debug)] |
1865 | | pub(super) struct Rrset<'r> { |
1866 | | pub(super) name: Name, |
1867 | | pub(super) record_class: DNSClass, |
1868 | | pub(super) record_type: RecordType, |
1869 | | pub(super) records: Vec<&'r Record>, |
1870 | | } |
1871 | | |
1872 | | impl<'r> Rrset<'r> { |
1873 | 0 | pub(super) fn new(record: &'r Record) -> Self { |
1874 | 0 | Self { |
1875 | 0 | name: record.name().clone(), |
1876 | 0 | record_class: record.dns_class(), |
1877 | 0 | record_type: record.record_type(), |
1878 | 0 | records: vec![record], |
1879 | 0 | } |
1880 | 0 | } |
1881 | | |
1882 | | /// Adds `record` to this RRset IFF it belongs to it |
1883 | 0 | pub(super) fn add(&mut self, record: &'r Record) { |
1884 | 0 | if self.name == *record.name() |
1885 | 0 | && self.record_type == record.record_type() |
1886 | 0 | && self.record_class == record.dns_class() |
1887 | 0 | { |
1888 | 0 | self.records.push(record); |
1889 | 0 | } |
1890 | 0 | } |
1891 | | |
1892 | | /// Returns the first (main) record. |
1893 | 0 | pub(super) fn record(&self) -> &Record { |
1894 | 0 | self.records[0] |
1895 | 0 | } |
1896 | | } |
1897 | | } |
1898 | | |
1899 | | /// The maximum number of key tag collisions to accept when: |
1900 | | /// |
1901 | | /// 1) Retrieving DNSKEY records for a zone |
1902 | | /// 2) Retrieving DS records from a parent zone |
1903 | | /// |
1904 | | /// Any colliding records encountered beyond this limit will be discarded. |
1905 | | const MAX_KEY_TAG_COLLISIONS: usize = 2; |
1906 | | |
1907 | | /// The maximum number of RRSIGs to attempt to validate for each RRSET. |
1908 | | const MAX_RRSIGS_PER_RRSET: usize = 8; |
1909 | | |
1910 | | /// The default validation cache size. This is somewhat arbitrary, but set to the same size as the default |
1911 | | /// recursor response cache |
1912 | | const DEFAULT_VALIDATION_CACHE_SIZE: usize = 1_048_576; |
1913 | | |
1914 | | #[cfg(test)] |
1915 | | mod test { |
1916 | | use super::{no_closer_matches, verify_nsec}; |
1917 | | use crate::{ |
1918 | | dnssec::{ |
1919 | | Algorithm, Proof, |
1920 | | rdata::{DNSSECRData, NSEC as rdataNSEC, RRSIG as rdataRRSIG, SigInput}, |
1921 | | }, |
1922 | | error::ProtoError, |
1923 | | op::{Query, ResponseCode}, |
1924 | | rr::{ |
1925 | | Name, RData, Record, |
1926 | | RecordType::{A, AAAA, DNSKEY, MX, NS, NSEC, RRSIG, SOA, TXT}, |
1927 | | SerialNumber, rdata, |
1928 | | }, |
1929 | | }; |
1930 | | use test_support::subscribe; |
1931 | | |
1932 | | #[test] |
1933 | | fn test_no_closer_matches() -> Result<(), ProtoError> { |
1934 | | subscribe(); |
1935 | | |
1936 | | assert!(no_closer_matches( |
1937 | | &Name::from_ascii("a.a.a.z.w.example")?, |
1938 | | Some(&Name::from_ascii("example.")?), |
1939 | | &[ |
1940 | | // This NSEC encloses the query name and proves that no closer wildcard match |
1941 | | // exists in the zone. |
1942 | | ( |
1943 | | &Name::from_ascii("x.y.w.example.")?, |
1944 | | &rdataNSEC::new(Name::from_ascii("xx.example.")?, [MX, NSEC, RRSIG],), |
1945 | | ), |
1946 | | ], |
1947 | | Some(&Name::from_ascii("*.w.example.")?), |
1948 | | ),); |
1949 | | |
1950 | | assert!(!no_closer_matches( |
1951 | | &Name::from_ascii("a.a.a.z.w.example")?, |
1952 | | Some(&Name::from_ascii("example.")?), |
1953 | | &[ |
1954 | | // This doesn't prove the non-existence of the closer wildcard |
1955 | | ( |
1956 | | &Name::from_ascii("*.w.example.")?, |
1957 | | &rdataNSEC::new(Name::from_ascii("z.w.example.")?, [MX, NSEC, RRSIG],), |
1958 | | ), |
1959 | | ], |
1960 | | Some(&Name::from_ascii("*.w.example.")?), |
1961 | | ),); |
1962 | | |
1963 | | assert!(!no_closer_matches( |
1964 | | &Name::from_ascii("a.a.a.z.w.example")?, |
1965 | | Some(&Name::from_ascii("example.")?), |
1966 | | &[( |
1967 | | &Name::from_ascii("x.y.w.example.")?, |
1968 | | &rdataNSEC::new(Name::from_ascii("xx.example.")?, [MX, NSEC, RRSIG],), |
1969 | | ),], |
1970 | | // no_closer_matches requires a wildcard base name be present |
1971 | | None, |
1972 | | ),); |
1973 | | |
1974 | | // SOA mismatch |
1975 | | assert!(!no_closer_matches( |
1976 | | &Name::from_ascii("a.a.a.z.w.example")?, |
1977 | | Some(&Name::from_ascii("z.example.")?), |
1978 | | &[ |
1979 | | // This NSEC encloses the query name and proves that no closer wildcard match |
1980 | | // exists in the zone. |
1981 | | ( |
1982 | | &Name::from_ascii("x.y.w.example.")?, |
1983 | | &rdataNSEC::new(Name::from_ascii("xx.example.")?, [MX, NSEC, RRSIG],), |
1984 | | ), |
1985 | | // This NSEC proves the requested record type does not exist at the wildcard |
1986 | | ( |
1987 | | &Name::from_ascii("*.w.example.")?, |
1988 | | &rdataNSEC::new(Name::from_ascii("xw.example.")?, [MX, NSEC, RRSIG],), |
1989 | | ), |
1990 | | ], |
1991 | | Some(&Name::from_ascii("*.w.example.")?), |
1992 | | ),); |
1993 | | |
1994 | | // Irrelevant wildcard. |
1995 | | assert!(!no_closer_matches( |
1996 | | &Name::from_ascii("a.a.a.z.w.example")?, |
1997 | | Some(&Name::from_ascii("example.")?), |
1998 | | &[ |
1999 | | // This NSEC encloses the query name and proves that no closer wildcard match |
2000 | | // exists in the zone. |
2001 | | ( |
2002 | | &Name::from_ascii("x.y.w.example.")?, |
2003 | | &rdataNSEC::new(Name::from_ascii("xx.example.")?, [MX, NSEC, RRSIG],), |
2004 | | ), |
2005 | | // This NSEC proves the requested record type does not exist at the wildcard |
2006 | | ( |
2007 | | &Name::from_ascii("*.x.example.")?, |
2008 | | &rdataNSEC::new(Name::from_ascii("xw.example.")?, [MX, NSEC, RRSIG],), |
2009 | | ), |
2010 | | ], |
2011 | | Some(&Name::from_ascii("*.x.example.")?), |
2012 | | ),); |
2013 | | |
2014 | | Ok(()) |
2015 | | } |
2016 | | |
2017 | | // These test cases prove a name does not exist |
2018 | | #[test] |
2019 | | fn nsec_name_error() -> Result<(), ProtoError> { |
2020 | | subscribe(); |
2021 | | |
2022 | | // Based on RFC 4035 B.2 - Name Error |
2023 | | assert_eq!( |
2024 | | verify_nsec( |
2025 | | &Query::query(Name::from_ascii("ml.example.")?, A), |
2026 | | Some(&Name::from_ascii("example.")?), |
2027 | | ResponseCode::NXDomain, |
2028 | | &[], |
2029 | | &[ |
2030 | | // This NSEC encloses the query name and proves the record does not exist. |
2031 | | ( |
2032 | | &Name::from_ascii("b.example.")?, |
2033 | | &rdataNSEC::new(Name::from_ascii("ns1.example.")?, [NS, RRSIG, NSEC],), |
2034 | | ), |
2035 | | // This NSEC proves no covering wildcard record exists (i.e., it encloses |
2036 | | // *.example. and thus proves that record does not exist.) |
2037 | | ( |
2038 | | &Name::from_ascii("example.")?, |
2039 | | &rdataNSEC::new( |
2040 | | Name::from_ascii("a.example.")?, |
2041 | | [DNSKEY, MX, NS, NSEC, RRSIG, SOA], |
2042 | | ), |
2043 | | ) |
2044 | | ], |
2045 | | ), |
2046 | | Proof::Secure |
2047 | | ); |
2048 | | |
2049 | | // Single NSEC that proves the record does not exist, and no covering wildcard exists. |
2050 | | assert_eq!( |
2051 | | verify_nsec( |
2052 | | &Query::query(Name::from_ascii("a.example.")?, A), |
2053 | | Some(&Name::from_ascii("example.")?), |
2054 | | ResponseCode::NXDomain, |
2055 | | &[], |
2056 | | &[( |
2057 | | &Name::from_ascii("example.")?, |
2058 | | &rdataNSEC::new(Name::from_ascii("c.example.")?, [SOA, NS, RRSIG, NSEC],), |
2059 | | ),], |
2060 | | ), |
2061 | | Proof::Secure |
2062 | | ); |
2063 | | |
2064 | | Ok(()) |
2065 | | } |
2066 | | |
2067 | | /// Ensure invalid name error NSEC scenarios fail |
2068 | | #[test] |
2069 | | fn nsec_invalid_name_error() -> Result<(), ProtoError> { |
2070 | | subscribe(); |
2071 | | assert_eq!( |
2072 | | verify_nsec( |
2073 | | &Query::query(Name::from_ascii("ml.example.")?, A), |
2074 | | Some(&Name::from_ascii("example.")?), |
2075 | | ResponseCode::NXDomain, |
2076 | | &[], |
2077 | | &[ |
2078 | | // This NSEC does not enclose the query name and so should cause this |
2079 | | // verification to fail |
2080 | | ( |
2081 | | &Name::from_ascii("ml.example.")?, |
2082 | | &rdataNSEC::new(Name::from_ascii("ns1.example.")?, [NS, RRSIG, NSEC],), |
2083 | | ), |
2084 | | // This NSEC proves no covering wildcard record exists (i.e., it encloses |
2085 | | // *.example. and thus proves that record does not exist.) |
2086 | | ( |
2087 | | &Name::from_ascii("example.")?, |
2088 | | &rdataNSEC::new( |
2089 | | Name::from_ascii("a.example.")?, |
2090 | | [DNSKEY, MX, NS, NSEC, RRSIG, SOA], |
2091 | | ), |
2092 | | ) |
2093 | | ], |
2094 | | ), |
2095 | | Proof::Bogus |
2096 | | ); |
2097 | | |
2098 | | // Test without proving wildcard non-existence. |
2099 | | assert_eq!( |
2100 | | verify_nsec( |
2101 | | &Query::query(Name::from_ascii("ml.example.")?, A), |
2102 | | Some(&Name::from_ascii("example.")?), |
2103 | | ResponseCode::NXDomain, |
2104 | | &[], |
2105 | | &[ |
2106 | | // This NSEC encloses the query name and proves the record does not exist. |
2107 | | ( |
2108 | | &Name::from_ascii("ml.example.")?, |
2109 | | &rdataNSEC::new(Name::from_ascii("ns1.example.")?, [NS, RRSIG, NSEC],), |
2110 | | ), |
2111 | | ], |
2112 | | ), |
2113 | | Proof::Bogus |
2114 | | ); |
2115 | | |
2116 | | // Invalid SOA |
2117 | | assert_eq!( |
2118 | | verify_nsec( |
2119 | | &Query::query(Name::from_ascii("ml.example.")?, A), |
2120 | | Some(&Name::from_ascii("example2.")?), |
2121 | | ResponseCode::NXDomain, |
2122 | | &[], |
2123 | | &[ |
2124 | | // This NSEC encloses the query name and proves the record does not exist. |
2125 | | ( |
2126 | | &Name::from_ascii("b.example.")?, |
2127 | | &rdataNSEC::new(Name::from_ascii("ns1.example.")?, [NS, RRSIG, NSEC],), |
2128 | | ), |
2129 | | // This NSEC proves no covering wildcard record exists (i.e., it encloses |
2130 | | // *.example. and thus proves that record does not exist.) |
2131 | | ( |
2132 | | &Name::from_ascii("example.")?, |
2133 | | &rdataNSEC::new( |
2134 | | Name::from_ascii("a.example.")?, |
2135 | | [DNSKEY, MX, NS, NSEC, RRSIG, SOA], |
2136 | | ), |
2137 | | ) |
2138 | | ], |
2139 | | ), |
2140 | | Proof::Bogus |
2141 | | ); |
2142 | | |
2143 | | Ok(()) |
2144 | | } |
2145 | | |
2146 | | // These test cases prove that the requested record type does not exist at the query name |
2147 | | #[test] |
2148 | | fn nsec_no_data_error() -> Result<(), ProtoError> { |
2149 | | subscribe(); |
2150 | | |
2151 | | // Based on RFC 4035 B.3 - No Data Error |
2152 | | assert_eq!( |
2153 | | verify_nsec( |
2154 | | &Query::query(Name::from_ascii("ns1.example.")?, MX), |
2155 | | Some(&Name::from_ascii("example.")?), |
2156 | | ResponseCode::NoError, |
2157 | | &[], |
2158 | | &[ |
2159 | | // This NSEC encloses the query name and proves the record does exist, but |
2160 | | // the requested record type does not. |
2161 | | ( |
2162 | | &Name::from_ascii("ns1.example.")?, |
2163 | | &rdataNSEC::new(Name::from_ascii("ns2.example.")?, [A, NSEC, RRSIG],), |
2164 | | ), |
2165 | | ], |
2166 | | ), |
2167 | | Proof::Secure |
2168 | | ); |
2169 | | |
2170 | | // Record type at the SOA does not exist. |
2171 | | assert_eq!( |
2172 | | verify_nsec( |
2173 | | &Query::query(Name::from_ascii("example.")?, MX), |
2174 | | Some(&Name::from_ascii("example.")?), |
2175 | | ResponseCode::NoError, |
2176 | | &[], |
2177 | | &[ |
2178 | | // This NSEC encloses the query name and proves the record does exist, but |
2179 | | // the requested record type does not. |
2180 | | ( |
2181 | | &Name::from_ascii("example.")?, |
2182 | | &rdataNSEC::new(Name::from_ascii("a.example.")?, [A, NSEC, RRSIG, SOA],), |
2183 | | ), |
2184 | | ], |
2185 | | ), |
2186 | | Proof::Secure |
2187 | | ); |
2188 | | |
2189 | | Ok(()) |
2190 | | } |
2191 | | |
2192 | | // Ensure invalid no data NSEC scenarios fails |
2193 | | #[test] |
2194 | | fn nsec_invalid_no_data_error() -> Result<(), ProtoError> { |
2195 | | subscribe(); |
2196 | | |
2197 | | assert_eq!( |
2198 | | verify_nsec( |
2199 | | &Query::query(Name::from_ascii("ns1.example.")?, MX), |
2200 | | Some(&Name::from_ascii("example.")?), |
2201 | | ResponseCode::NoError, |
2202 | | &[], |
2203 | | &[ |
2204 | | // This NSEC claims the requested record type DOES exist at ns1.example. |
2205 | | ( |
2206 | | &Name::from_ascii("ns1.example.")?, |
2207 | | &rdataNSEC::new(Name::from_ascii("ns2.example.")?, [A, NSEC, RRSIG, MX],), |
2208 | | ), |
2209 | | ], |
2210 | | ), |
2211 | | Proof::Bogus |
2212 | | ); |
2213 | | |
2214 | | assert_eq!( |
2215 | | verify_nsec( |
2216 | | &Query::query(Name::from_ascii("ns1.example.")?, MX), |
2217 | | Some(&Name::from_ascii("example.")?), |
2218 | | ResponseCode::NoError, |
2219 | | &[], |
2220 | | &[ |
2221 | | // In this case, the response indicates *some* record exists at ns1.example., just not an |
2222 | | // MX record. This NSEC claims ns1.example. does not exist at all. |
2223 | | ( |
2224 | | &Name::from_ascii("ml.example.")?, |
2225 | | &rdataNSEC::new(Name::from_ascii("ns2.example.")?, [A, NSEC, RRSIG],), |
2226 | | ), |
2227 | | ], |
2228 | | ), |
2229 | | Proof::Bogus |
2230 | | ); |
2231 | | |
2232 | | assert_eq!( |
2233 | | verify_nsec( |
2234 | | &Query::query(Name::from_ascii("ns1.example.")?, MX), |
2235 | | Some(&Name::from_ascii("example.")?), |
2236 | | ResponseCode::NoError, |
2237 | | &[], |
2238 | | &[ |
2239 | | // This NSEC claims nothing exists from the SOA to ns2.example. |
2240 | | ( |
2241 | | &Name::from_ascii("example.")?, |
2242 | | &rdataNSEC::new(Name::from_ascii("ns2.example.")?, [A, NSEC, RRSIG],), |
2243 | | ), |
2244 | | ], |
2245 | | ), |
2246 | | Proof::Bogus |
2247 | | ); |
2248 | | |
2249 | | Ok(()) |
2250 | | } |
2251 | | |
2252 | | // Ensure that positive answers expanded from wildcards pass validation |
2253 | | #[test] |
2254 | | fn nsec_wildcard_expansion() -> Result<(), ProtoError> { |
2255 | | subscribe(); |
2256 | | |
2257 | | let input = SigInput { |
2258 | | type_covered: MX, |
2259 | | algorithm: Algorithm::ED25519, |
2260 | | num_labels: 2, |
2261 | | original_ttl: 3600, |
2262 | | sig_expiration: SerialNumber(0), |
2263 | | sig_inception: SerialNumber(0), |
2264 | | key_tag: 0, |
2265 | | signer_name: Name::root(), |
2266 | | }; |
2267 | | |
2268 | | let rrsig = rdataRRSIG::from_sig(input, vec![]); |
2269 | | let mut rrsig_record = Record::from_rdata( |
2270 | | Name::from_ascii("a.z.w.example.")?, |
2271 | | 3600, |
2272 | | RData::DNSSEC(DNSSECRData::RRSIG(rrsig)), |
2273 | | ); |
2274 | | rrsig_record.set_proof(Proof::Secure); |
2275 | | |
2276 | | let answers = [ |
2277 | | Record::from_rdata( |
2278 | | Name::from_ascii("a.z.w.example.")?, |
2279 | | 3600, |
2280 | | RData::MX(rdata::MX::new(10, Name::from_ascii("a.z.w.example.")?)), |
2281 | | ), |
2282 | | rrsig_record, |
2283 | | ]; |
2284 | | |
2285 | | // Based on RFC 4035 B.6 - Wildcard Expansion |
2286 | | assert_eq!( |
2287 | | verify_nsec( |
2288 | | &Query::query(Name::from_ascii("a.z.w.example.")?, MX), |
2289 | | None, |
2290 | | ResponseCode::NoError, |
2291 | | &answers, |
2292 | | &[ |
2293 | | // This NSEC encloses the query name and proves that no closer wildcard match |
2294 | | // exists in the zone. |
2295 | | ( |
2296 | | &Name::from_ascii("x.y.w.example.")?, |
2297 | | &rdataNSEC::new(Name::from_ascii("xx.example.")?, [MX, NSEC, RRSIG],), |
2298 | | ), |
2299 | | ], |
2300 | | ), |
2301 | | Proof::Secure |
2302 | | ); |
2303 | | |
2304 | | // This response could not have been synthesized from the query name (z.example can't be expanded from *.w.example |
2305 | | assert_eq!( |
2306 | | verify_nsec( |
2307 | | &Query::query(Name::from_ascii("z.example.")?, MX), |
2308 | | Some(&Name::from_ascii("example.")?), |
2309 | | ResponseCode::NoError, |
2310 | | &answers, |
2311 | | &[ |
2312 | | // This NSEC encloses the query name and proves that z.example. does not exist. |
2313 | | ( |
2314 | | &Name::from_ascii("y.example.")?, |
2315 | | &rdataNSEC::new(Name::from_ascii("example.")?, [A, NSEC, RRSIG],), |
2316 | | ), |
2317 | | // This NSEC proves *.example. exists and contains an MX record. |
2318 | | ( |
2319 | | &Name::from_ascii("example.")?, |
2320 | | &rdataNSEC::new( |
2321 | | Name::from_ascii("a.example.")?, |
2322 | | [MX, NS, NSEC, RRSIG, SOA], |
2323 | | ), |
2324 | | ), |
2325 | | ], |
2326 | | ), |
2327 | | Proof::Bogus |
2328 | | ); |
2329 | | |
2330 | | Ok(()) |
2331 | | } |
2332 | | |
2333 | | // Ensure that defective wildcard expansion positive answer scenarios fail validation |
2334 | | #[test] |
2335 | | fn nsec_invalid_wildcard_expansion() -> Result<(), ProtoError> { |
2336 | | subscribe(); |
2337 | | |
2338 | | let input = SigInput { |
2339 | | type_covered: MX, |
2340 | | algorithm: Algorithm::ED25519, |
2341 | | num_labels: 2, |
2342 | | original_ttl: 0, |
2343 | | sig_expiration: SerialNumber(0), |
2344 | | sig_inception: SerialNumber(0), |
2345 | | key_tag: 0, |
2346 | | signer_name: Name::root(), |
2347 | | }; |
2348 | | |
2349 | | let rrsig = rdataRRSIG::from_sig(input, vec![]); |
2350 | | let mut rrsig_record = Record::from_rdata( |
2351 | | Name::from_ascii("a.z.w.example.")?, |
2352 | | 3600, |
2353 | | RData::DNSSEC(DNSSECRData::RRSIG(rrsig)), |
2354 | | ); |
2355 | | rrsig_record.set_proof(Proof::Secure); |
2356 | | |
2357 | | let answers = [ |
2358 | | Record::from_rdata( |
2359 | | Name::from_ascii("a.z.w.example.")?, |
2360 | | 3600, |
2361 | | RData::MX(rdata::MX::new(10, Name::from_ascii("a.z.w.example.")?)), |
2362 | | ), |
2363 | | rrsig_record, |
2364 | | ]; |
2365 | | |
2366 | | assert_eq!( |
2367 | | verify_nsec( |
2368 | | &Query::query(Name::from_ascii("a.z.w.example.")?, MX), |
2369 | | None, |
2370 | | ResponseCode::NoError, |
2371 | | &answers, |
2372 | | &[ |
2373 | | // This NSEC does not prove the non-existence of *.z.w.example. |
2374 | | ( |
2375 | | &Name::from_ascii("x.y.w.example.")?, |
2376 | | &rdataNSEC::new(Name::from_ascii("z.w.example.")?, [MX, NSEC, RRSIG],), |
2377 | | ), |
2378 | | ], |
2379 | | ), |
2380 | | Proof::Bogus |
2381 | | ); |
2382 | | |
2383 | | assert_eq!( |
2384 | | verify_nsec( |
2385 | | &Query::query(Name::from_ascii("a.z.w.example.")?, MX), |
2386 | | None, |
2387 | | ResponseCode::NoError, |
2388 | | &answers, |
2389 | | &[], |
2390 | | ), |
2391 | | Proof::Bogus |
2392 | | ); |
2393 | | |
2394 | | Ok(()) |
2395 | | } |
2396 | | |
2397 | | #[test] |
2398 | | fn nsec_wildcard_no_data_error() -> Result<(), ProtoError> { |
2399 | | subscribe(); |
2400 | | |
2401 | | // Based on RFC 4035 B.7 - Wildcard No Data Error |
2402 | | assert_eq!( |
2403 | | verify_nsec( |
2404 | | &Query::query(Name::from_ascii("a.z.w.example.")?, AAAA), |
2405 | | Some(&Name::from_ascii("example.")?), |
2406 | | ResponseCode::NoError, |
2407 | | &[], |
2408 | | &[ |
2409 | | // This NSEC encloses the query name and proves that no closer wildcard match |
2410 | | // exists in the zone. |
2411 | | ( |
2412 | | &Name::from_ascii("x.y.w.example.")?, |
2413 | | &rdataNSEC::new(Name::from_ascii("xx.example.")?, [MX, NSEC, RRSIG],), |
2414 | | ), |
2415 | | // This NSEC proves the requested record type does not exist at the wildcard |
2416 | | ( |
2417 | | &Name::from_ascii("*.w.example.")?, |
2418 | | &rdataNSEC::new(Name::from_ascii("xw.example.")?, [MX, NSEC, RRSIG],), |
2419 | | ), |
2420 | | ], |
2421 | | ), |
2422 | | Proof::Secure |
2423 | | ); |
2424 | | |
2425 | | assert_eq!( |
2426 | | verify_nsec( |
2427 | | &Query::query(Name::from_ascii("zzzzzz.hickory-dns.testing.")?, TXT), |
2428 | | Some(&Name::from_ascii("hickory-dns.testing.")?), |
2429 | | ResponseCode::NoError, |
2430 | | &[], |
2431 | | &[ |
2432 | | // This NSEC proves zzzzzz.hickory-dns.testing. does not exist. |
2433 | | ( |
2434 | | &Name::from_ascii("record.hickory-dns.testing.")?, |
2435 | | &rdataNSEC::new( |
2436 | | Name::from_ascii("hickory-dns.testing.")?, |
2437 | | [A, NSEC, RRSIG], |
2438 | | ), |
2439 | | ), |
2440 | | // This NSEC proves a wildcard does exist at *.hickory-dns.testing. but does not contain the |
2441 | | // requested record type. |
2442 | | ( |
2443 | | &Name::from_ascii("*.hickory-dns.testing.")?, |
2444 | | &rdataNSEC::new( |
2445 | | Name::from_ascii("primary0.hickory-dns.testing.")?, |
2446 | | [A, NSEC, RRSIG], |
2447 | | ), |
2448 | | ), |
2449 | | ], |
2450 | | ), |
2451 | | Proof::Secure |
2452 | | ); |
2453 | | |
2454 | | Ok(()) |
2455 | | } |
2456 | | |
2457 | | #[test] |
2458 | | fn nsec_invalid_wildcard_no_data_error() -> Result<(), ProtoError> { |
2459 | | subscribe(); |
2460 | | |
2461 | | assert_eq!( |
2462 | | verify_nsec( |
2463 | | &Query::query(Name::from_ascii("a.z.w.example.")?, AAAA), |
2464 | | Some(&Name::from_ascii("example.")?), |
2465 | | ResponseCode::NoError, |
2466 | | &[], |
2467 | | &[ |
2468 | | // This NSEC doesn't prove the non-existence of the query name |
2469 | | ( |
2470 | | &Name::from_ascii("x.y.w.example.")?, |
2471 | | &rdataNSEC::new(Name::from_ascii("z.w.example.")?, [MX, NSEC, RRSIG],), |
2472 | | ), |
2473 | | // This NSEC proves the wildcard does not contain the requested record type |
2474 | | ( |
2475 | | &Name::from_ascii("*.w.example.")?, |
2476 | | &rdataNSEC::new(Name::from_ascii("x.y.w.example.")?, [MX, NSEC, RRSIG],), |
2477 | | ), |
2478 | | ], |
2479 | | ), |
2480 | | Proof::Bogus |
2481 | | ); |
2482 | | |
2483 | | assert_eq!( |
2484 | | verify_nsec( |
2485 | | &Query::query(Name::from_ascii("a.z.w.example.")?, AAAA), |
2486 | | Some(&Name::from_ascii("example.")?), |
2487 | | ResponseCode::NoError, |
2488 | | &[], |
2489 | | &[ |
2490 | | // This NSEC proves the query name does not exist |
2491 | | ( |
2492 | | &Name::from_ascii("x.y.w.example.")?, |
2493 | | &rdataNSEC::new(Name::from_ascii("xx.example.")?, [MX, NSEC, RRSIG],), |
2494 | | ), |
2495 | | // This NSEC proves the requested record type exists at the wildcard |
2496 | | ( |
2497 | | &Name::from_ascii("*.w.example.")?, |
2498 | | &rdataNSEC::new(Name::from_ascii("xw.example.")?, [AAAA, MX, NSEC, RRSIG],), |
2499 | | ), |
2500 | | ], |
2501 | | ), |
2502 | | Proof::Bogus |
2503 | | ); |
2504 | | |
2505 | | assert_eq!( |
2506 | | verify_nsec( |
2507 | | &Query::query(Name::from_ascii("r.hickory-dns.testing.")?, TXT), |
2508 | | Some(&Name::from_ascii("hickory-dns.testing.")?), |
2509 | | ResponseCode::NoError, |
2510 | | &[], |
2511 | | &[ |
2512 | | // There is no NSEC proving the non-existence of r.hickory-dns.testing. |
2513 | | |
2514 | | // This NSEC proves a wildcard does exist at *.hickory-dns.testing. but does not contain the |
2515 | | // requested record type. |
2516 | | ( |
2517 | | &Name::from_ascii("*.hickory-dns.testing.")?, |
2518 | | &rdataNSEC::new( |
2519 | | Name::from_ascii("primary0.hickory-dns.testing.")?, |
2520 | | [A, NSEC, RRSIG], |
2521 | | ), |
2522 | | ), |
2523 | | ], |
2524 | | ), |
2525 | | Proof::Bogus |
2526 | | ); |
2527 | | |
2528 | | Ok(()) |
2529 | | } |
2530 | | } |