add a test for Secure records
This commit is contained in:
parent
70d8e6fc0f
commit
14f4f0a4b6
@ -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;
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
},
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -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),
|
||||
|
Loading…
Reference in New Issue
Block a user