add a test for Secure records

This commit is contained in:
Benjamin Fry 2023-10-30 11:35:28 -07:00
parent 70d8e6fc0f
commit 14f4f0a4b6
4 changed files with 221 additions and 34 deletions

View File

@ -29,7 +29,7 @@ mod verifier;
pub use self::algorithm::Algorithm;
pub use self::digest_type::DigestType;
pub use self::nsec3::Nsec3HashAlgorithm;
pub use self::proof::Proof;
pub use self::proof::{Proof, ProofError, ProofErrorKind};
pub use self::public_key::PublicKey;
pub use self::public_key::PublicKeyBuf;
pub use self::public_key::PublicKeyEnum;

View File

@ -11,6 +11,16 @@ use std::fmt;
#[cfg(feature = "serde-config")]
use serde::{Deserialize, Serialize};
use thiserror::Error;
#[cfg(feature = "backtrace")]
use crate::ExtBacktrace;
use crate::{
error::{DnsSecError, ProtoError},
rr::Name,
};
use super::Algorithm;
/// Represents the status of a DNSSEC verified record.
///
@ -25,12 +35,13 @@ use serde::{Deserialize, Serialize};
/// ```
#[cfg_attr(feature = "serde-config", derive(Deserialize, Serialize))]
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
#[repr(u8)]
pub enum Proof {
/// An RRset for which the resolver is able to build a chain of
/// signed DNSKEY and DS RRs from a trusted security anchor to the
/// RRset. In this case, the RRset should be signed and is subject to
/// signature validation, as described above.
Secure,
Secure = 3,
/// An RRset for which the resolver knows that it has no chain
/// of signed DNSKEY and DS RRs from any trusted starting point to the
@ -38,7 +49,7 @@ pub enum Proof {
/// zone or in a descendent of an unsigned zone. In this case, the
/// RRset may or may not be signed, but the resolver will not be able
/// to verify the signature.
Insecure,
Insecure = 2,
/// An RRset for which the resolver believes that it ought to be
/// able to establish a chain of trust but for which it is unable to
@ -47,21 +58,42 @@ pub enum Proof {
/// indicate should be present. This case may indicate an attack but
/// may also indicate a configuration error or some form of data
/// corruption.
Bogus,
Bogus = 1,
/// An RRset for which the resolver is not able to
/// determine whether the RRset should be signed, as the resolver is
/// not able to obtain the necessary DNSSEC RRs. This can occur when
/// the security-aware resolver is not able to contact security-aware
/// name servers for the relevant zones.
Indeterminate,
Indeterminate = 0,
}
impl Proof {
/// Returns true if this Proof represents a validated DNSSEC record
#[inline]
pub fn is_secure(&self) -> bool {
*self == Self::Secure
}
/// Returns true if this Proof represents a validated to be insecure DNSSEC record,
/// meaning the zone is known to be not signed
#[inline]
pub fn is_insecure(&self) -> bool {
*self == Self::Insecure
}
/// Returns true if this Proof represents a DNSSEC record that failed validation,
/// meaning that the DNSSEC is bad, or other DNSSEC records are incorrect
#[inline]
pub fn is_bogus(&self) -> bool {
*self == Self::Bogus
}
/// Either the record has not been verified or
#[inline]
pub fn is_indeterminate(&self) -> bool {
*self == Self::Indeterminate
}
}
impl Default for Proof {
@ -84,3 +116,90 @@ impl fmt::Display for Proof {
f.write_str(s)
}
}
impl PartialOrd for Proof {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl Ord for Proof {
/// If self is great than other, it has a strong DNSSEC proof, i.e. Secure is the highest
/// Ordering from highest to lowest is: Secure, Insecure, Bogus, Indeterminate
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
let this = *self as u8;
let other = *other as u8;
this.cmp(&other)
}
}
#[test]
fn test_order() {
assert!(Proof::Secure > Proof::Insecure);
assert!(Proof::Insecure > Proof::Bogus);
assert!(Proof::Bogus > Proof::Indeterminate);
}
/// The error kind for dnssec errors that get returned in the crate
#[allow(unreachable_pub)]
#[derive(Debug, Error, Clone)]
#[non_exhaustive]
pub enum ProofErrorKind {
/// An error with an arbitrary message, referenced as &'static str
#[error("{0}")]
Message(&'static str),
/// An error with an arbitrary message, stored as String
#[error("{0}")]
Msg(String),
/// Algorithm mismatch between rrsig and dnskey
#[error("algorithm mismatch rrsig: {rrsig} dnskey: {dnskey}")]
AlgorithmMismatch { rrsig: Algorithm, dnskey: Algorithm },
/// A DNSSEC validation error, occured
#[error("ssl error: {0}")]
DnsSecError(#[from] DnsSecError),
/// A DnsKey verification of rrset and rrsig faile
#[error("dnskey and rrset failed to verify: {name} key_tag: {key_tag}")]
DnsKeyVerifyRrsig {
name: Name,
key_tag: u16,
error: ProtoError,
},
/// A DnsKey was revoked and could not be used for validation
#[error("dnskey revoked: {name} key_tag: {key_tag}")]
DnsKeyRevoked { name: Name, key_tag: u16 },
/// The DnsKey is not marked as a zone key
#[error("not a zone signing key: {name} key_tag: {key_tag}")]
NotZoneDnsKey { name: Name, key_tag: u16 },
}
/// The error type for dnssec errors that get returned in the crate
#[derive(Debug, Clone, Error)]
pub struct ProofError {
proof: Proof,
kind: ProofErrorKind,
}
impl ProofError {
/// Create an error with the given Proof and Associated Error
pub fn new(proof: Proof, kind: ProofErrorKind) -> Self {
Self { proof, kind }
}
/// Get the kind of the error
pub fn kind(&self) -> &ProofErrorKind {
&self.kind
}
}
impl fmt::Display for ProofError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}: {}", self.proof, self.kind)
}
}

View File

@ -16,12 +16,12 @@ use futures_util::{
use tracing::{debug, trace};
use crate::{
error::{ProtoError, ProtoErrorKind, ProtoResult},
error::{ProtoError, ProtoErrorKind},
op::{Edns, OpCode, Query},
rr::{
dnssec::{
rdata::{DNSSECRData, DNSKEY, DS, RRSIG},
Algorithm, Proof, SupportedAlgorithms, TrustAnchor,
Algorithm, Proof, ProofError, ProofErrorKind, SupportedAlgorithms, TrustAnchor,
},
rdata::opt::EdnsOption,
DNSClass, Name, RData, Record, RecordData, RecordType,
@ -325,6 +325,7 @@ where
// there was no data returned in that message
if rrset_types.is_empty() {
// TODO: stop cloning message here, take all the answers, etc, from above.
let mut message_result = message_result.into_message();
// there were no returned results, double check by dropping all the results
@ -461,18 +462,30 @@ where
.into_iter()
.chain(message_result.take_additionals().into_iter())
.filter(|record| verified_rrsets.contains(&(record.name().clone(), record.record_type())))
.map(|mut r| {
r.set_proof(Proof::Secure);
r
})
.collect::<Vec<Record>>();
let name_servers = message_result
.take_name_servers()
.into_iter()
.filter(|record| verified_rrsets.contains(&(record.name().clone(), record.record_type())))
.map(|mut r| {
r.set_proof(Proof::Secure);
r
})
.collect::<Vec<Record>>();
let additionals = message_result
.take_additionals()
.into_iter()
.filter(|record| verified_rrsets.contains(&(record.name().clone(), record.record_type())))
.map(|mut r| {
r.set_proof(Proof::Secure);
r
})
.collect::<Vec<Record>>();
// add the filtered records back to the message
@ -711,8 +724,28 @@ async fn verify_default_rrset<H>(
where
H: DnsHandle + Sync + Unpin,
{
let assoc_rrset_proof = |rrset: &mut Rrset, proof: Proof| {
rrset.records.iter_mut().for_each(|rr| {
rr.set_proof(proof);
});
};
// TODO: if there are no rrsigs, we are not necessarily failed first need to change this from an error return?
if rrsigs.is_empty() {
//let mut rrset = rrset;
//assoc_rrset_proof(&mut rrset, Proof::Indeterminate);
// instead of returning here, first deside if we're:
// 1) "indeterminate", i.e. no DNSSEC records are available back to the root
// 2) "insecure", the zone has a valid NSEC for the DS record in the parent zone
// 3) "bogus", the parent zone has a valid DS record, but the child zone didn't have the RRSIGs/DNSKEYs
return Err(ProtoError::from(ProtoErrorKind::RrsigsNotPresent {
name: rrset.name.clone(),
record_type: rrset.record_type,
}));
}
// the record set is going to be shared across a bunch of futures, Arc for that.
let rrset = Arc::new(rrset);
trace!(
"default validation {}, record_type: {:?}",
rrset.name,
@ -728,19 +761,20 @@ where
// then return rrset. Like the standard case below, the DNSKEY is validated
// after this function. This function is only responsible for validating the signature
// the DNSKey validation should come after, see verify_rrset().
return future::ready(
future::ready(
rrsigs
.iter()
.find_map(|rrsig| {
let rrset = Arc::clone(&rrset);
if rrset
.records
.iter()
.filter_map(|r| r.data().map(|d| (d, r.name())))
.filter_map(|(d, n)| DNSKEY::try_borrow(d).map(|d| (d, n)))
.any(|(dnskey, dnskey_name)| {
verify_rrset_with_dnskey(dnskey_name, dnskey, rrsig, &rrset).is_ok()
// If we had rrsigs to verify, then we want them to be secure, or the result is a Bogus proof
verify_rrset_with_dnskey(dnskey_name, dnskey, rrsig, &rrset)
.unwrap_or(Proof::Bogus)
.is_secure()
})
{
Some(())
@ -752,8 +786,12 @@ where
ProtoError::from(ProtoErrorKind::Message("self-signed dnskey is invalid"))
}),
)
.map_ok(move |_| Arc::try_unwrap(rrset).expect("unable to unwrap Arc"))
.await;
.await?;
// Getting here means the rrset (and records), have been verified
let mut rrset = rrset;
assoc_rrset_proof(&mut rrset, Proof::Secure);
return Ok(rrset);
}
// we can validate with any of the rrsigs...
@ -766,19 +804,18 @@ where
// dns over TLS will mitigate this.
// TODO: strip RRSIGS to accepted algorithms and make algorithms configurable.
let verifications = rrsigs.iter()
.map(|sig| {
let rrset = Arc::clone(&rrset);
.map(|rrsig| {
let handle = handle.clone_with_context();
// TODO: Should this sig.signer_name should be confirmed to be in the same zone as the rrsigs and rrset?
handle
.lookup(
Query::query(sig.signer_name().clone(), RecordType::DNSKEY),
Query::query(rrsig.signer_name().clone(), RecordType::DNSKEY),
options,
)
.first_answer()
.and_then(move |message|
// DNSKEYs are validated by the inner query
.and_then(|message|
// DNSKEYs were already validated by the inner query in the above lookup
future::ready(message
.answers()
.iter()
@ -787,7 +824,7 @@ where
.filter_map(|(dnskey_name, data)|
DNSKEY::try_borrow(data).map(|data| (dnskey_name, data)))
.find(|(dnskey_name, dnskey)|
verify_rrset_with_dnskey(dnskey_name, dnskey, &sig, &rrset).is_ok()
verify_rrset_with_dnskey(dnskey_name, dnskey, rrsig, &rrset).is_ok()
)
.map(|_| ())
.ok_or_else(|| ProtoError::from(ProtoErrorKind::Message("validation failed"))))
@ -806,6 +843,7 @@ where
// if there are no available verifications, then we are in a failed state.
if verifications.is_empty() {
// TODO: this is a bogus state, technically we can return the Rrset and make all the records Bogus?
return Err(ProtoError::from(ProtoErrorKind::RrsigsNotPresent {
name: rrset.name.clone(),
record_type: rrset.record_type,
@ -817,10 +855,14 @@ where
// getting here means at least one of the rrsigs succeeded...
.map_ok(move |((), rest)| {
drop(rest); // drop all others, should free up Arc
Arc::try_unwrap(rrset).expect("unable to unwrap Arc")
});
select.await
select.await?;
// getting here means we have secure and verified records.
let mut rrset = rrset;
assoc_rrset_proof(&mut rrset, Proof::Secure);
Ok(rrset)
}
/// Verifies the given SIG of the RRSET with the DNSKEY.
@ -828,36 +870,60 @@ where
fn verify_rrset_with_dnskey(
dnskey_name: &Name,
dnskey: &DNSKEY,
sig: &RRSIG,
rrsig: &RRSIG,
rrset: &Rrset,
) -> ProtoResult<()> {
) -> Result<Proof, ProofError> {
if dnskey.revoke() {
debug!("revoked");
return Err(ProtoErrorKind::Message("revoked").into());
return Err(ProofError::new(
Proof::Bogus,
ProofErrorKind::DnsKeyRevoked {
name: dnskey_name.clone(),
key_tag: rrsig.key_tag(),
},
));
} // TODO: does this need to be validated? RFC 5011
if !dnskey.zone_key() {
return Err(ProtoErrorKind::Message("is not a zone key").into());
return Err(ProofError::new(
Proof::Bogus,
ProofErrorKind::NotZoneDnsKey {
name: dnskey_name.clone(),
key_tag: rrsig.key_tag(),
},
));
}
if dnskey.algorithm() != sig.algorithm() {
return Err(ProtoErrorKind::Message("mismatched algorithm").into());
if dnskey.algorithm() != rrsig.algorithm() {
return Err(ProofError::new(
Proof::Bogus,
ProofErrorKind::AlgorithmMismatch {
rrsig: rrsig.algorithm(),
dnskey: dnskey.algorithm(),
},
));
}
dnskey
.verify_rrsig(&rrset.name, rrset.record_class, sig, &rrset.records)
.map(|r| {
.verify_rrsig(&rrset.name, rrset.record_class, rrsig, &rrset.records)
.map(|_| {
debug!(
"validated ({}, {:?}) with ({}, {})",
rrset.name, rrset.record_type, dnskey_name, dnskey
);
r
Proof::Secure
})
.map_err(Into::into)
.map_err(|e| {
debug!(
"failed validation of ({}, {:?}) with ({}, {})",
rrset.name, rrset.record_type, dnskey_name, dnskey
);
e
ProofError::new(
Proof::Bogus,
ProofErrorKind::DnsKeyVerifyRrsig {
name: dnskey_name.clone(),
key_tag: rrsig.key_tag(),
error: e,
},
)
})
}

View File

@ -14,7 +14,7 @@ use hickory_client::tcp::TcpClientStream;
use hickory_proto::iocompat::AsyncIoTokioAsStd;
use hickory_proto::op::ResponseCode;
use hickory_proto::rr::dnssec::TrustAnchor;
use hickory_proto::rr::dnssec::{Proof, TrustAnchor};
use hickory_proto::rr::rdata::A;
use hickory_proto::rr::Name;
use hickory_proto::rr::{DNSClass, RData, RecordType};
@ -62,6 +62,7 @@ where
assert_eq!(record.name(), &name);
assert_eq!(record.record_type(), RecordType::A);
assert_eq!(record.dns_class(), DNSClass::IN);
assert_eq!(record.proof(), Proof::Secure);
if let RData::A(ref address) = *record.data().unwrap() {
assert_eq!(address, &A::new(93, 184, 216, 34))
@ -288,6 +289,7 @@ where
join.join().unwrap();
}
// TODO: just make this a Tokio test?
fn with_tcp<F>(test: F)
where
F: Fn(DnssecDnsHandle<MemoizeClientHandle<AsyncClient>>, Runtime),