use alloc::{borrow::ToOwned, boxed::Box, string::ToString, sync::Arc, vec::Vec};
use core::{clone::Clone, pin::Pin};
use std::{
collections::{HashMap, HashSet},
time::{SystemTime, UNIX_EPOCH},
};
use futures_util::{
future::{self, TryFutureExt},
stream::{self, Stream, TryStreamExt},
};
use tracing::{debug, error, trace, warn};
use crate::{
dnssec::{
Algorithm, Proof, ProofError, ProofErrorKind, TrustAnchors, Verifier,
rdata::{DNSKEY, DS, RRSIG},
},
error::{ProtoError, ProtoErrorKind},
op::{Edns, Message, OpCode, Query},
rr::{Name, Record, RecordData, RecordType, SerialNumber, resource::RecordRef},
xfer::{DnsRequest, DnsRequestOptions, DnsResponse, FirstAnswer, dns_handle::DnsHandle},
};
use self::rrset::Rrset;
mod nsec3_validation;
use nsec3_validation::verify_nsec3;
use super::rdata::NSEC;
#[derive(Clone)]
#[must_use = "queries can only be sent through a DnsHandle"]
pub struct DnssecDnsHandle<H>
where
H: DnsHandle + Unpin + 'static,
{
handle: H,
trust_anchor: Arc<TrustAnchors>,
request_depth: usize,
minimum_key_len: usize,
minimum_algorithm: Algorithm, }
impl<H> DnssecDnsHandle<H>
where
H: DnsHandle + Unpin + 'static,
{
pub fn new(handle: H) -> Self {
Self::with_trust_anchor(handle, Arc::new(TrustAnchors::default()))
}
pub fn with_trust_anchor(handle: H, trust_anchor: Arc<TrustAnchors>) -> Self {
Self {
handle,
trust_anchor,
request_depth: 0,
minimum_key_len: 0,
minimum_algorithm: Algorithm::RSASHA256,
}
}
fn clone_with_context(&self) -> Self {
Self {
handle: self.handle.clone(),
trust_anchor: Arc::clone(&self.trust_anchor),
request_depth: self.request_depth + 1,
minimum_key_len: self.minimum_key_len,
minimum_algorithm: self.minimum_algorithm,
}
}
}
#[cfg(any(feature = "std", feature = "no-std-rand"))]
impl<H> DnsHandle for DnssecDnsHandle<H>
where
H: DnsHandle + Sync + Unpin,
{
type Response = Pin<Box<dyn Stream<Item = Result<DnsResponse, ProtoError>> + Send>>;
fn is_verifying_dnssec(&self) -> bool {
true
}
fn send<R: Into<DnsRequest>>(&self, request: R) -> Self::Response {
let mut request = request.into();
if self.request_depth > request.options().max_request_depth {
error!("exceeded max validation depth");
return Box::pin(stream::once(future::err(ProtoError::from(
"exceeded max validation depth",
))));
}
match request.op_code() {
OpCode::Query => {}
_ => return Box::pin(self.handle.send(request)),
}
let query = if let Some(query) = request.queries().first().cloned() {
query
} else {
return Box::pin(stream::once(future::err(ProtoError::from(
"no query in request",
))));
};
let handle = self.clone_with_context();
request
.extensions_mut()
.get_or_insert_with(Edns::new)
.enable_dnssec();
request.set_authentic_data(true);
request.set_checking_disabled(false);
let options = *request.options();
Box::pin(
self.handle
.send(request)
.or_else(move |res| {
match res.kind() {
ProtoErrorKind::NoRecordsFound {
query,
authorities,
response_code,
..
} => {
let mut msg = Message::new();
debug!("translating NoRecordsFound to DnsResponse for {query}");
msg.add_query(*query.clone());
msg.set_response_code(*response_code);
if let Some(authorities) = authorities {
for ns in authorities.iter() {
msg.add_name_server(ns.clone());
}
}
match DnsResponse::from_message(msg) {
Ok(res) => future::ok(res),
Err(_e) => future::err(ProtoError::from(
"unable to construct DnsResponse: {_e:?}",
)),
}
}
_ => future::err(ProtoError::from(res.to_string())),
}
})
.and_then(move |message_response| {
verify_response(handle.clone(), message_response, options)
})
.and_then(move |verified_message| {
future::ready(check_nsec(verified_message, &query))
}),
)
}
}
fn check_nsec(verified_message: DnsResponse, query: &Query) -> Result<DnsResponse, ProtoError> {
if !verified_message.answers().is_empty() {
return Ok(verified_message);
}
if !verified_message.name_servers().is_empty()
&& verified_message
.name_servers()
.iter()
.all(|x| x.proof() == Proof::Insecure)
{
return Ok(verified_message);
}
let soa_name = if let Some(soa_name) = verified_message
.name_servers()
.iter()
.find(|rr| rr.record_type() == RecordType::SOA)
.map(Record::name)
{
soa_name
} else {
return Err(ProtoError::from(
"could not validate negative response missing SOA",
));
};
let nsec3s = verified_message
.name_servers()
.iter()
.filter_map(|rr| {
rr.data()
.as_dnssec()?
.as_nsec3()
.map(|data| (rr.name(), data))
})
.collect::<Vec<_>>();
let nsecs = verified_message
.name_servers()
.iter()
.filter_map(|rr| {
rr.data()
.as_dnssec()?
.as_nsec()
.map(|data| (rr.name(), data))
})
.collect::<Vec<_>>();
let nsec_proof = match (!nsec3s.is_empty(), !nsecs.is_empty()) {
(true, false) => verify_nsec3(
query,
soa_name,
verified_message.response_code(),
verified_message.answers(),
&nsec3s,
),
(false, true) => verify_nsec(query, soa_name, nsecs.as_slice()),
(true, true) => {
warn!(
"response contains both NSEC and NSEC3 records\nQuery:\n{query:?}\nResponse:\n{verified_message:?}"
);
Proof::Bogus
}
(false, false) => {
warn!(
"response does not contain NSEC or NSEC3 records. Query: {query:?} response: {verified_message:?}"
);
Proof::Bogus
}
};
if !nsec_proof.is_secure() {
debug!("returning Nsec error for {} {nsec_proof}", query.name());
return Err(ProtoError::from(ProtoErrorKind::Nsec {
query: Box::new(query.clone()),
proof: nsec_proof,
}));
}
Ok(verified_message)
}
async fn verify_response<H>(
handle: DnssecDnsHandle<H>,
mut message: DnsResponse,
options: DnsRequestOptions,
) -> Result<DnsResponse, ProtoError>
where
H: DnsHandle + Sync + Unpin,
{
debug!(
"validating message_response: {}, with {} trust_anchors",
message.id(),
handle.trust_anchor.len(),
);
let answers = message.take_answers();
let nameservers = message.take_name_servers();
let additionals = message.take_additionals();
let answers = verify_rrsets(&handle, answers, options).await;
let nameservers = verify_rrsets(&handle, nameservers, options).await;
let additionals = verify_rrsets(&handle, additionals, options).await;
message.insert_answers(answers);
message.insert_name_servers(nameservers);
message.insert_additionals(additionals);
Ok(message)
}
#[allow(clippy::type_complexity)]
async fn verify_rrsets<H>(
handle: &DnssecDnsHandle<H>,
records: Vec<Record>,
options: DnsRequestOptions,
) -> Vec<Record>
where
H: DnsHandle + Sync + Unpin,
{
let mut rrset_types: HashSet<(Name, RecordType)> = HashSet::new();
for rrset in records
.iter()
.filter(|rr| {
!is_dnssec(rr, RecordType::RRSIG) &&
(handle.request_depth <= 1 ||
is_dnssec(rr, RecordType::DNSKEY) ||
is_dnssec(rr, RecordType::DS))
})
.map(|rr| (rr.name().clone(), rr.record_type()))
{
rrset_types.insert(rrset);
}
if rrset_types.is_empty() {
return records;
}
let mut return_records = Vec::with_capacity(records.len());
let (mut rrsigs, mut records) = records
.into_iter()
.partition::<Vec<_>, _>(|r| r.record_type().is_rrsig());
for (name, record_type) in rrset_types {
let current_rrset;
(current_rrset, records) = records
.into_iter()
.partition::<Vec<_>, _>(|rr| rr.record_type() == record_type && rr.name() == &name);
let current_rrsigs;
(current_rrsigs, rrsigs) = rrsigs.into_iter().partition::<Vec<_>, _>(|rr| {
rr.try_borrow::<RRSIG>()
.map(|rr| rr.name() == &name && rr.data().type_covered() == record_type)
.unwrap_or_default()
});
let mut rrs_to_verify = current_rrset.iter();
let mut rrset = Rrset::new(rrs_to_verify.next().unwrap());
rrs_to_verify.for_each(|rr| rrset.add(rr));
let rrsigs: Vec<_> = current_rrsigs
.iter()
.filter_map(|rr| rr.try_borrow::<RRSIG>())
.filter(|rr| rr.name() == &name)
.filter(|rrsig| rrsig.data().type_covered() == record_type)
.collect();
debug!(
"verifying: {name} record_type: {record_type}, rrsigs: {rrsig_len}",
rrsig_len = rrsigs.len()
);
let proof = verify_rrset(handle.clone(), &rrset, rrsigs, options).await;
let proof = match proof {
Ok(proof) => {
debug!("verified: {name} record_type: {record_type}",);
proof
}
Err(ProofError { proof, kind }) => {
match kind {
ProofErrorKind::DsResponseNsec { .. } => {
debug!("verified insecure {name}/{record_type}")
}
_ => debug!("failed to verify: {name} record_type: {record_type}: {kind}"),
}
(proof, None, None)
}
};
let (proof, adjusted_ttl, rrsig_idx) = proof;
for mut record in current_rrset {
record.set_proof(proof);
if let (Proof::Secure, Some(ttl)) = (proof, adjusted_ttl) {
record.set_ttl(ttl);
}
return_records.push(record);
}
let mut current_rrsigs = current_rrsigs;
if let Some(rrsig_idx) = rrsig_idx {
if let Some(rrsig) = current_rrsigs.get_mut(rrsig_idx) {
rrsig.set_proof(proof);
if let (Proof::Secure, Some(ttl)) = (proof, adjusted_ttl) {
rrsig.set_ttl(ttl);
}
} else {
warn!(
"bad rrsig index {rrsig_idx} rrsigs.len = {}",
current_rrsigs.len()
);
}
}
return_records.extend(current_rrsigs);
}
return_records.extend(rrsigs);
return_records.extend(records);
return_records
}
fn is_dnssec<D: RecordData>(rr: &Record<D>, dnssec_type: RecordType) -> bool {
rr.record_type().is_dnssec() && dnssec_type.is_dnssec() && rr.record_type() == dnssec_type
}
async fn verify_rrset<H>(
handle: DnssecDnsHandle<H>,
rrset: &Rrset<'_>,
rrsigs: Vec<RecordRef<'_, RRSIG>>,
options: DnsRequestOptions,
) -> Result<(Proof, Option<u32>, Option<usize>), ProofError>
where
H: DnsHandle + Sync + Unpin,
{
let current_time = current_time();
if matches!(rrset.record_type(), RecordType::DNSKEY) {
let proof = verify_dnskey_rrset(handle, rrset, &rrsigs, current_time, options).await?;
return Ok(proof);
}
verify_default_rrset(&handle, rrset, &rrsigs, current_time, options).await
}
async fn verify_dnskey_rrset<H>(
handle: DnssecDnsHandle<H>,
rrset: &Rrset<'_>,
rrsigs: &Vec<RecordRef<'_, RRSIG>>,
current_time: u32,
options: DnsRequestOptions,
) -> Result<(Proof, Option<u32>, Option<usize>), ProofError>
where
H: DnsHandle + Sync + Unpin,
{
if RecordType::DNSKEY != rrset.record_type() {
panic!("All other RRSETs must use verify_default_rrset");
}
debug!(
"dnskey validation {}, record_type: {:?}",
rrset.name(),
rrset.record_type()
);
let mut dnskey_proofs =
Vec::<(Proof, Option<u32>, Option<usize>)>::with_capacity(rrset.records().len());
dnskey_proofs.resize(rrset.records().len(), (Proof::Bogus, None, None));
for (r, proof) in rrset.records().iter().zip(dnskey_proofs.iter_mut()) {
let Some(dnskey) = r.try_borrow::<DNSKEY>() else {
continue;
};
proof.0 = is_dnskey_in_root_store(&handle, &dnskey);
}
let ds_records = if !dnskey_proofs.iter().all(|p| p.0.is_secure()) && !rrset.name().is_root() {
fetch_ds_records(&handle, rrset.name().clone(), options).await?
} else {
debug!("ignoring DS lookup for root zone or registered keys");
Vec::default()
};
if ds_records
.iter()
.filter(|ds| ds.proof().is_secure() || ds.proof().is_insecure())
.all(|ds| !ds.data().algorithm().is_supported() || !ds.data().digest_type().is_supported())
&& !ds_records.is_empty()
{
debug!(
"all dnskeys use unsupported algorithms and there are no supported DS records in the parent zone"
);
return Err(ProofError::new(
Proof::Insecure,
ProofErrorKind::UnsupportedKeyAlgorithm,
));
}
for (r, proof) in rrset.records().iter().zip(dnskey_proofs.iter_mut()) {
let Some(dnskey) = r.try_borrow() else {
continue;
};
if proof.0.is_secure() {
continue;
}
match verify_dnskey(&dnskey, &ds_records) {
Ok(pf) => {
*proof = (pf, None, None);
}
Err(err) => {
*proof = (err.proof, None, None);
}
}
}
for (i, rrsig) in rrsigs.iter().enumerate() {
let signer_name = rrsig.data().signer_name();
let rrset_proof = rrset
.records()
.iter()
.zip(dnskey_proofs.iter())
.filter(|(_, (proof, ..))| proof.is_secure())
.filter(|(r, _)| r.name() == signer_name)
.filter_map(|(r, (proof, ..))| {
RecordRef::<'_, DNSKEY>::try_from(*r)
.ok()
.map(|r| (r, proof))
})
.find_map(|(dnskey, proof)| {
verify_rrset_with_dnskey(dnskey, *proof, rrsig, rrset, current_time).ok()
});
if let Some(rrset_proof) = rrset_proof {
return Ok((rrset_proof.0, rrset_proof.1, Some(i)));
}
}
if dnskey_proofs.iter().all(|(proof, ..)| proof.is_secure()) {
return Ok(dnskey_proofs.pop().unwrap());
}
if !ds_records.is_empty() {
trace!("bogus dnskey: {}", rrset.name());
return Err(ProofError::new(
Proof::Bogus,
ProofErrorKind::DsRecordsButNoDnskey {
name: rrset.name().clone(),
},
));
}
trace!("no dnskey found: {}", rrset.name());
Err(ProofError::new(
Proof::Bogus,
ProofErrorKind::DnskeyNotFound {
name: rrset.name().clone(),
},
))
}
fn is_dnskey_in_root_store<H>(handle: &DnssecDnsHandle<H>, rr: &RecordRef<'_, DNSKEY>) -> Proof
where
H: DnsHandle + Sync + Unpin,
{
let dns_key = rr.data();
let pub_key = dns_key.public_key();
if handle.trust_anchor.contains(pub_key) {
debug!(
"validated dnskey with trust_anchor: {}, {dns_key}",
rr.name(),
);
Proof::Secure
} else {
Proof::Bogus
}
}
#[allow(clippy::result_large_err)]
fn verify_dnskey(
rr: &RecordRef<'_, DNSKEY>,
ds_records: &[Record<DS>],
) -> Result<Proof, ProofError> {
let key_rdata = rr.data();
let key_tag = key_rdata.calculate_key_tag().map_err(|_| {
ProofError::new(
Proof::Insecure,
ProofErrorKind::ErrorComputingKeyTag {
name: rr.name().clone(),
},
)
})?;
let key_algorithm = key_rdata.algorithm();
if !key_algorithm.is_supported() {
return Err(ProofError::new(
Proof::Insecure,
ProofErrorKind::UnsupportedKeyAlgorithm,
));
}
let mut key_authentication_attempts = 0;
for r in ds_records.iter().filter(|ds| ds.proof().is_secure()) {
if r.data().algorithm() != key_algorithm {
trace!(
"skipping DS record due to algorithm mismatch, expected algorithm {}: ({}, {})",
key_algorithm,
r.name(),
r.data(),
);
continue;
}
if r.data().key_tag() != key_tag {
trace!(
"skipping DS record due to key tag mismatch, expected tag {key_tag}: ({}, {})",
r.name(),
r.data(),
);
continue;
}
key_authentication_attempts += 1;
if key_authentication_attempts > MAX_KEY_TAG_COLLISIONS {
warn!(
key_tag,
attempts = key_authentication_attempts,
"too many DS records with same key tag; skipping"
);
continue;
}
if !r.data().covers(rr.name(), key_rdata).unwrap_or(false) {
continue;
}
debug!(
"validated dnskey ({}, {key_rdata}) with {} {}",
rr.name(),
r.name(),
r.data(),
);
return Ok(Proof::Secure);
}
trace!("bogus dnskey: {}", rr.name());
Err(ProofError::new(
Proof::Bogus,
ProofErrorKind::DnsKeyHasNoDs {
name: rr.name().clone(),
},
))
}
async fn fetch_ds_records<H>(
handle: &DnssecDnsHandle<H>,
zone: Name,
options: DnsRequestOptions,
) -> Result<Vec<Record<DS>>, ProofError>
where
H: DnsHandle + Sync + Unpin,
{
let ds_message = handle
.lookup(Query::query(zone.clone(), RecordType::DS), options)
.first_answer()
.await;
let error = match ds_message {
Ok(mut ds_message)
if ds_message
.answers()
.iter()
.filter(|r| r.record_type() == RecordType::DS)
.any(|r| r.proof().is_secure()) =>
{
let all_records = ds_message
.take_answers()
.into_iter()
.filter_map(|r| Record::<DS>::try_from(r).ok());
let mut supported_records = vec![];
let mut all_unknown = None;
for record in all_records {
if (!record.data().algorithm().is_supported()
|| !record.data().digest_type().is_supported())
&& (record.proof().is_secure() || record.proof().is_insecure())
{
all_unknown.get_or_insert(true);
continue;
}
all_unknown = Some(false);
supported_records.push(record);
}
if all_unknown.unwrap_or(false) {
return Err(ProofError::new(
Proof::Insecure,
ProofErrorKind::UnknownKeyAlgorithm,
));
} else if !supported_records.is_empty() {
return Ok(supported_records);
} else {
ProtoError::from(ProtoErrorKind::NoError)
}
}
Ok(response) => {
let any_ds_rr = response
.answers()
.iter()
.any(|r| r.record_type() == RecordType::DS);
if any_ds_rr {
ProtoError::from(ProtoErrorKind::NoError)
} else {
debug!("marking {zone} as insecure based on secure NSEC/NSEC3 proof");
return Err(ProofError::new(
Proof::Insecure,
ProofErrorKind::DsResponseNsec { name: zone },
));
}
}
Err(error) => error,
};
if let Some((query, _proof)) = error
.kind()
.as_nsec()
.filter(|(_query, proof)| proof.is_insecure())
{
debug!(
"marking {} as insecure based on insecure NSEC/NSEC3 proof",
query.name()
);
return Err(ProofError::new(
Proof::Insecure,
ProofErrorKind::DsResponseNsec {
name: query.name().to_owned(),
},
));
}
Err(ProofError::ds_should_exist(zone))
}
async fn find_ds_records<H>(
handle: &DnssecDnsHandle<H>,
zone: Name,
options: DnsRequestOptions,
) -> Result<(), ProofError>
where
H: DnsHandle + Sync + Unpin,
{
match fetch_ds_records(handle, zone.clone(), options).await {
Ok(_) => return Ok(()),
Err(ProofError {
proof: _,
kind: ProofErrorKind::DsRecordShouldExist { .. },
}) => {}
Err(err) => return Err(err),
}
let mut parent = zone.base_name();
loop {
match fetch_ds_records(handle, parent.clone(), options).await {
Ok(_) => {
return Err(ProofError::ds_should_exist(zone));
}
Err(ProofError {
proof: _,
kind: ProofErrorKind::DsRecordShouldExist { .. },
}) => {}
Err(err) => return Err(err),
}
parent = match parent.is_root() {
true => return Err(ProofError::ds_should_exist(zone)),
false => parent.base_name(),
};
}
}
#[allow(clippy::blocks_in_conditions)]
async fn verify_default_rrset<H>(
handle: &DnssecDnsHandle<H>,
rrset: &Rrset<'_>,
rrsigs: &Vec<RecordRef<'_, RRSIG>>,
current_time: u32,
options: DnsRequestOptions,
) -> Result<(Proof, Option<u32>, Option<usize>), ProofError>
where
H: DnsHandle + Sync + Unpin,
{
if RecordType::DNSKEY == rrset.record_type() {
panic!("DNSKEYs must be validated with verify_dnskey_rrset");
}
if rrsigs.is_empty() {
find_ds_records(handle, rrset.name().clone(), options).await?;
return Err(ProofError::new(
Proof::Bogus,
ProofErrorKind::RrsigsNotPresent {
name: rrset.name().clone(),
record_type: rrset.record_type(),
},
));
}
trace!(
"default validation {}, record_type: {:?}",
rrset.name(),
rrset.record_type()
);
let verifications = rrsigs
.iter()
.enumerate()
.filter_map(|(i, rrsig)| {
let query = Query::query(rrsig.data().signer_name().clone(), RecordType::DNSKEY);
if i > MAX_RRSIGS_PER_RRSET {
warn!("too many ({i}) RRSIGs for rrset {rrset:?}; skipping");
return None;
}
Some(handle
.lookup(query.clone(), options)
.first_answer()
.map_err(|proto| {
ProofError::new(Proof::Bogus, ProofErrorKind::Proto { query, proto })
})
.map_ok(move |message| {
let mut tag_count = HashMap::<u16, usize>::new();
let dnskeys = message
.answers()
.iter()
.filter_map(|r| {
let dnskey = r.try_borrow::<DNSKEY>()?;
let tag = match dnskey.data().calculate_key_tag() {
Ok(tag) => tag,
Err(e) => {
warn!("unable to calculate key tag: {e:?}; skipping key");
return None;
}
};
match tag_count.get_mut(&tag) {
Some(n_keys) => {
*n_keys += 1;
if *n_keys > MAX_KEY_TAG_COLLISIONS {
warn!("too many ({n_keys}) DNSKEYs with key tag {tag}; skipping");
return None;
}
}
None => _ = tag_count.insert(tag, 1),
}
Some(dnskey)
});
let mut all_insecure = None;
for dnskey in dnskeys {
match dnskey.proof() {
Proof::Secure => {
all_insecure = Some(false);
if let Ok(proof) =
verify_rrset_with_dnskey(dnskey, dnskey.proof(), rrsig, rrset, current_time)
{
return Some((proof.0, proof.1, Some(i)));
}
}
Proof::Insecure => {
all_insecure.get_or_insert(true);
}
_ => all_insecure = Some(false),
}
}
if all_insecure.unwrap_or(false) {
Some((Proof::Insecure, None, None))
} else {
None
}
}))
})
.collect::<Vec<_>>();
if verifications.is_empty() {
return Err(ProofError::new(
Proof::Bogus,
ProofErrorKind::RrsigsNotPresent {
name: rrset.name().clone(),
record_type: rrset.record_type(),
},
));
}
let select = future::select_ok(verifications);
let (proof, rest) = select.await?;
drop(rest);
proof.ok_or_else(||
ProofError::new(Proof::Bogus, ProofErrorKind::RrsigsUnverified{name: rrset.name().clone(), record_type: rrset.record_type()})
)
}
#[allow(clippy::result_large_err)]
fn verify_rrset_with_dnskey(
dnskey: RecordRef<'_, DNSKEY>,
dnskey_proof: Proof,
rrsig: &RecordRef<'_, RRSIG>,
rrset: &Rrset<'_>,
current_time: u32,
) -> Result<(Proof, Option<u32>), ProofError> {
match dnskey_proof {
Proof::Secure => (),
proof => {
debug!("insecure dnskey {} {}", dnskey.name(), dnskey.data());
return Err(ProofError::new(
proof,
ProofErrorKind::InsecureDnsKey {
name: dnskey.name().clone(),
key_tag: rrsig.data().key_tag(),
},
));
}
}
if dnskey.data().revoke() {
debug!("revoked dnskey {} {}", dnskey.name(), dnskey.data());
return Err(ProofError::new(
Proof::Bogus,
ProofErrorKind::DnsKeyRevoked {
name: dnskey.name().clone(),
key_tag: rrsig.data().key_tag(),
},
));
} if !dnskey.data().zone_key() {
return Err(ProofError::new(
Proof::Bogus,
ProofErrorKind::NotZoneDnsKey {
name: dnskey.name().clone(),
key_tag: rrsig.data().key_tag(),
},
));
}
if dnskey.data().algorithm() != rrsig.data().algorithm() {
return Err(ProofError::new(
Proof::Bogus,
ProofErrorKind::AlgorithmMismatch {
rrsig: rrsig.data().algorithm(),
dnskey: dnskey.data().algorithm(),
},
));
}
let validity = check_rrsig_validity(*rrsig, rrset, dnskey, current_time);
if !matches!(validity, RrsigValidity::ValidRrsig) {
return Err(ProofError::new(
Proof::Bogus,
ProofErrorKind::Msg(format!("{:?}", validity)),
));
}
dnskey
.data()
.verify_rrsig(
rrset.name(),
rrset.record_class(),
rrsig.data(),
rrset.records().iter().copied(),
)
.map(|_| {
debug!(
"validated ({}, {:?}) with ({}, {})",
rrset.name(),
rrset.record_type(),
dnskey.name(),
dnskey.data()
);
(
Proof::Secure,
Some(rrsig.data().authenticated_ttl(rrset.record(), current_time)),
)
})
.map_err(|e| {
debug!(
"failed validation of ({}, {:?}) with ({}, {})",
rrset.name(),
rrset.record_type(),
dnskey.name(),
dnskey.data()
);
ProofError::new(
Proof::Bogus,
ProofErrorKind::DnsKeyVerifyRrsig {
name: dnskey.name().clone(),
key_tag: rrsig.data().key_tag(),
error: e,
},
)
})
}
fn check_rrsig_validity(
rrsig: RecordRef<'_, RRSIG>,
rrset: &Rrset<'_>,
dnskey: RecordRef<'_, DNSKEY>,
current_time: u32,
) -> RrsigValidity {
let current_time = SerialNumber(current_time);
let expiration = rrsig.data().sig_expiration();
let inception = rrsig.data().sig_inception();
let Ok(dnskey_key_tag) = dnskey.data().calculate_key_tag() else {
return RrsigValidity::WrongDnskey;
};
if !(
rrsig.name() == rrset.name() &&
rrsig.dns_class() == rrset.record_class() &&
rrsig.data().type_covered() == rrset.record_type() &&
rrset.name().num_labels() >= rrsig.data().num_labels()
) {
return RrsigValidity::WrongRrsig;
}
if !(
current_time <= expiration &&
current_time >= inception
) {
return RrsigValidity::ExpiredRrsig;
}
if !(
rrsig.data().signer_name() == dnskey.name() &&
rrsig.data().algorithm() == dnskey.data().algorithm() &&
rrsig.data().key_tag() == dnskey_key_tag &&
dnskey.data().zone_key()
) {
return RrsigValidity::WrongDnskey;
}
RrsigValidity::ValidRrsig
}
#[derive(Clone, Copy, Debug)]
enum RrsigValidity {
ExpiredRrsig,
ValidRrsig,
WrongDnskey,
WrongRrsig,
}
#[allow(clippy::blocks_in_conditions)]
#[doc(hidden)]
pub fn verify_nsec(query: &Query, soa_name: &Name, nsecs: &[(&Name, &NSEC)]) -> Proof {
if let Some((_, nsec_data)) = nsecs.iter().find(|(name, _)| query.name() == *name) {
if !nsec_data.type_set().contains(query.query_type()) {
return proof_log_yield(Proof::Secure, query.name(), "nsec1", "direct match");
} else {
return proof_log_yield(Proof::Bogus, query.name(), "nsec1", "direct match");
}
}
let verify_nsec_coverage = |query_name: &Name| -> bool {
nsecs.iter().any(|(nsec_name, nsec_data)| {
query_name >= nsec_name && {
query_name < nsec_data.next_domain_name()
|| nsec_data.next_domain_name() < nsec_name
}
})
};
if !verify_nsec_coverage(query.name()) {
return proof_log_yield(Proof::Bogus, query.name(), "nsec1", "no wildcard");
}
let wildcard = query.name().base_name();
let wildcard = if soa_name.zone_of(&wildcard) {
wildcard
} else {
soa_name.clone()
};
if wildcard == *query.name() {
proof_log_yield(
Proof::Secure,
query.name(),
"nsec1",
"direct wildcard match",
)
} else {
if verify_nsec_coverage(&wildcard) {
proof_log_yield(
Proof::Secure,
query.name(),
"nsec1",
"covering wildcard match",
)
} else {
proof_log_yield(
Proof::Bogus,
query.name(),
"nsec1",
"covering wildcard match",
)
}
}
}
fn current_time() -> u32 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_secs() as u32
}
fn proof_log_yield(proof: Proof, name: &Name, nsec_type: &str, msg: &str) -> Proof {
debug!("{nsec_type} proof for {name}, returning {proof}: {msg}");
proof
}
mod rrset {
use alloc::vec::Vec;
use crate::rr::{DNSClass, Name, Record, RecordType};
#[derive(Debug)]
pub(super) struct Rrset<'r> {
name: Name,
record_class: DNSClass,
record_type: RecordType,
records: Vec<&'r Record>,
}
impl<'r> Rrset<'r> {
pub(super) fn new(record: &'r Record) -> Self {
Self {
name: record.name().clone(),
record_class: record.dns_class(),
record_type: record.record_type(),
records: vec![record],
}
}
pub(super) fn add(&mut self, record: &'r Record) {
if self.name == *record.name()
&& self.record_type == record.record_type()
&& self.record_class == record.dns_class()
{
self.records.push(record);
}
}
pub(super) fn record(&self) -> &Record {
self.records[0]
}
pub(super) fn name(&self) -> &Name {
&self.name
}
pub(super) fn record_class(&self) -> DNSClass {
self.record_class
}
pub(super) fn record_type(&self) -> RecordType {
self.record_type
}
pub(super) fn records(&self) -> &[&Record] {
&self.records
}
}
}
const MAX_KEY_TAG_COLLISIONS: usize = 2;
const MAX_RRSIGS_PER_RRSET: usize = 8;