X-Git-Url: http://git.bitcoin.ninja/index.cgi?a=blobdiff_plain;f=src%2Fvalidation.rs;h=ae0bb58f15de418ecc7305695374d4d8fcd89abd;hb=3d98f4ce7f1c8d824aa411bd5c08e2734bd98b91;hp=5a6fda3df71dba92a98dd7407e8bb13b7ef00774;hpb=54a0563ea610ef8a2c1d19347f6417cd61e43917;p=dnssec-prover diff --git a/src/validation.rs b/src/validation.rs index 5a6fda3..ae0bb58 100644 --- a/src/validation.rs +++ b/src/validation.rs @@ -7,7 +7,7 @@ use core::cmp; use ring::signature; use crate::rr::*; -use crate::ser::{bytes_to_rsa_pk, parse_rr, write_name}; +use crate::ser::write_name; /// Gets the trusted root anchors /// @@ -30,30 +30,6 @@ pub fn root_hints() -> Vec { res } -/// Parse a stream of [`RR`]s from the format described in [RFC 9102](https://www.rfc-editor.org/rfc/rfc9102.html). -/// -/// Note that this is only the series of `AuthenticationChain` records, and does not read the -/// `ExtSupportLifetime` field at the start of a `DnssecChainExtension`. -pub fn parse_rr_stream(mut inp: &[u8]) -> Result, ()> { - let mut res = Vec::with_capacity(32); - while !inp.is_empty() { - res.push(parse_rr(&mut inp)?); - } - Ok(res) -} - -/// Writes the given resource record in its wire encoding to the given `Vec`. -/// -/// An [RFC 9102](https://www.rfc-editor.org/rfc/rfc9102.html) `AuthenticationChain` is simply a -/// series of such records with no additional bytes in between. -pub fn write_rr(rr: &RR, ttl: u32, out: &mut Vec) { - write_name(out, rr.name()); - out.extend_from_slice(&rr.ty().to_be_bytes()); - out.extend_from_slice(&1u16.to_be_bytes()); // The INternet class - out.extend_from_slice(&ttl.to_be_bytes()); - rr.write_u16_len_prefixed_data(out); -} - #[derive(Debug, PartialEq)] /// An error when validating DNSSEC signatures or other data pub enum ValidationError { @@ -68,6 +44,27 @@ pub enum ValidationError { Invalid, } +pub(crate) fn bytes_to_rsa_pk<'a>(pubkey: &'a [u8]) +-> Result, ()> { + if pubkey.len() <= 3 { return Err(()); } + + let mut pos = 0; + let exponent_length; + if pubkey[0] == 0 { + exponent_length = ((pubkey[1] as usize) << 8) | (pubkey[2] as usize); + pos += 3; + } else { + exponent_length = pubkey[0] as usize; + pos += 1; + } + + if pubkey.len() <= pos + exponent_length { return Err(()); } + Ok(signature::RsaPublicKeyComponents { + n: &pubkey[pos + exponent_length..], + e: &pubkey[pos..pos + exponent_length] + }) +} + fn verify_rrsig<'a, RR: Record, Keys>(sig: &RRSig, dnskeys: Keys, mut records: Vec<&RR>) -> Result<(), ValidationError> where Keys: IntoIterator { @@ -165,7 +162,7 @@ where T: IntoIterator, I: Iterator + Clone { let mut had_ds = false; for ds in dses.clone() { had_ds = true; - if ds.digest_type == 2 || ds.digest_type == 4 { + if ds.digest_type == 1 || ds.digest_type == 2 || ds.digest_type == 4 { had_known_digest_type = true; break; } @@ -174,11 +171,13 @@ where T: IntoIterator, I: Iterator + Clone { if !had_known_digest_type { return Err(ValidationError::UnsupportedAlgorithm); } for dnskey in records.iter() { + // Only use SHA1 DS records if we don't have any SHA256/SHA384 DS RRs. + let trust_sha1 = dses.clone().all(|ds| ds.digest_type != 2 && ds.digest_type != 4); for ds in dses.clone() { - if ds.digest_type != 2 && ds.digest_type != 4 { continue; } if ds.alg != dnskey.alg { continue; } if dnskey.key_tag() == ds.key_tag { let alg = match ds.digest_type { + 1 if trust_sha1 => &ring::digest::SHA1_FOR_LEGACY_USE_ONLY, 2 => &ring::digest::SHA256, 4 => &ring::digest::SHA384, _ => continue, @@ -203,12 +202,14 @@ where T: IntoIterator, I: Iterator + Clone { /// Given a set of [`RR`]s, [`verify_rr_stream`] checks what it can and returns the set of /// non-[`RRSig`]/[`DnsKey`]/[`DS`] records which it was able to verify using this struct. /// -/// It also contains +/// It also contains signing and expiry times, which must be validated before considering the +/// contained records verified. +#[derive(Debug, Clone)] pub struct VerifiedRRStream<'a> { /// The set of verified [`RR`]s. /// /// These are not valid unless the current UNIX time is between [`Self::valid_from`] and - /// [`Self::expiration`]. + /// [`Self::expires`]. pub verified_rrs: Vec<&'a RR>, /// The latest [`RRSig::inception`] of all the [`RRSig`]s validated to verify /// [`Self::verified_rrs`]. @@ -252,8 +253,8 @@ fn resolve_time(time: u32) -> u64 { /// All records which could be validated are returned, though if an error is found validating any /// contained record, only `Err` will be returned. /// -/// You MUST check that the current UNIX time is between [`VerifiedRRStream::latest_inception`] and -/// [`VerifiedRRStream::earliest_expiry`]. +/// You MUST check that the current UNIX time is between [`VerifiedRRStream::valid_from`] and +/// [`VerifiedRRStream::expires`]. pub fn verify_rr_stream<'a>(inp: &'a [RR]) -> Result, ValidationError> { let mut zone = "."; let mut res = Vec::new(); @@ -292,7 +293,7 @@ pub fn verify_rr_stream<'a>(inp: &'a [RR]) -> Result, Valid min_ttl = cmp::min(min_ttl, rrsig.orig_ttl); for rrsig in inp.iter() .filter_map(|rr| if let RR::RRSig(sig) = rr { Some(sig) } else { None }) - .filter(move |rrsig| rrsig.key_name.as_str() == zone && rrsig.name.as_str() != zone) + .filter(move |rrsig| rrsig.key_name.as_str() == zone && rrsig.ty != DnsKey::TYPE) { if !rrsig.name.ends_with(zone) { return Err(ValidationError::Invalid); } let signed_records = inp.iter() @@ -349,6 +350,29 @@ pub fn verify_rr_stream<'a>(inp: &'a [RR]) -> Result, Valid } } +impl<'a> VerifiedRRStream<'a> { + /// Given a name, resolve any [`CName`] records and return any verified records which were + /// pointed to by the original name. + /// + /// Note that because of [`CName`]s, the [`RR::name`] in the returned records may or may not be + /// equal to `name`. + /// + /// You MUST still check that the current UNIX time is between + /// [`VerifiedRRStream::valid_from`] and [`VerifiedRRStream::expires`] before + /// using any records returned here. + pub fn resolve_name<'b>(&self, mut name: &'b Name) -> Vec<&'a RR> where 'a: 'b { + loop { + let mut cname_search = self.verified_rrs.iter() + .filter(|rr| rr.name() == name) + .filter_map(|rr| if let RR::CName(cn) = rr { Some(cn) } else { None }); + if let Some(cname) = cname_search.next() { + name = &cname.canonical_name; + } + return self.verified_rrs.iter().filter(|rr| rr.name() == name).map(|rr| *rr).collect(); + } + } +} + #[cfg(test)] mod tests { #![allow(deprecated)] @@ -357,6 +381,8 @@ mod tests { use alloc::borrow::ToOwned; + use crate::ser::{parse_rr_stream, write_rr}; + use hex_conservative::FromHex; use rand::seq::SliceRandom; @@ -549,6 +575,41 @@ mod tests { (cname_resp, cname_rrsig, txt_resp, txt_rrsig) } + fn matcorallo_txt_sort_edge_cases_records() -> (Vec, RRSig) { + let txts = vec![Txt { + name: "txt_sort_order.matcorallo.com.".try_into().unwrap(), + data: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaab".to_owned().into_bytes(), + }, Txt { + name: "txt_sort_order.matcorallo.com.".try_into().unwrap(), + data: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa".to_owned().into_bytes(), + }, Txt { + name: "txt_sort_order.matcorallo.com.".try_into().unwrap(), + data: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaa".to_owned().into_bytes(), + }, Txt { + name: "txt_sort_order.matcorallo.com.".try_into().unwrap(), + data: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaba".to_owned().into_bytes(), + }, Txt { + name: "txt_sort_order.matcorallo.com.".try_into().unwrap(), + data: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa".to_owned().into_bytes(), + }, Txt { + name: "txt_sort_order.matcorallo.com.".try_into().unwrap(), + data: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaab".to_owned().into_bytes(), + }, Txt { + name: "txt_sort_order.matcorallo.com.".try_into().unwrap(), + data: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa".to_owned().into_bytes(), + }, Txt { + name: "txt_sort_order.matcorallo.com.".try_into().unwrap(), + data: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaba".to_owned().into_bytes(), + }]; + let rrsig = RRSig { + name: "txt_sort_order.matcorallo.com.".try_into().unwrap(), + ty: Txt::TYPE, alg: 13, labels: 3, orig_ttl: 30, expiration: 1708449632, + inception: 1707234632, key_tag: 34530, key_name: "matcorallo.com.".try_into().unwrap(), + signature: base64::decode("elAhELwzkGpUMvzeiYZpg1+yRFPjmOeEd1ir1vYx2Dku9kzsXmAlejOYDPWdaJ6ekvHdMejCN/MtyI+iFAYqsw==").unwrap(), + }; + (txts, rrsig) + } + #[test] fn check_txt_record_a() { let dnskeys = mattcorallo_dnskey().0; @@ -626,6 +687,14 @@ mod tests { assert_eq!(cname.name.as_str(), "cname_test.matcorallo.com."); assert_eq!(cname.canonical_name.as_str(), "txt_test.matcorallo.com."); } else { panic!(); } + + let filtered_rrs = + verified_rrs.resolve_name(&"cname_test.matcorallo.com.".try_into().unwrap()); + assert_eq!(filtered_rrs.len(), 1); + if let RR::Txt(txt) = &filtered_rrs[0] { + assert_eq!(txt.name.as_str(), "txt_test.matcorallo.com."); + assert_eq!(txt.data, b"dnssec_prover_test"); + } else { panic!(); } } #[test] @@ -659,6 +728,35 @@ mod tests { assert_eq!(cname.name.as_str(), "test.cname_wildcard_test.matcorallo.com."); assert_eq!(cname.canonical_name.as_str(), "cname.wildcard_test.matcorallo.com."); } else { panic!(); } + + let filtered_rrs = + verified_rrs.resolve_name(&"test.cname_wildcard_test.matcorallo.com.".try_into().unwrap()); + assert_eq!(filtered_rrs.len(), 1); + if let RR::Txt(txt) = &filtered_rrs[0] { + assert_eq!(txt.name.as_str(), "cname.wildcard_test.matcorallo.com."); + assert_eq!(txt.data, b"wildcard_test"); + } else { panic!(); } + } + + #[test] + fn check_txt_sort_order() { + let mut rr_stream = Vec::new(); + for rr in root_dnskey().1 { write_rr(&rr, 1, &mut rr_stream); } + for rr in com_dnskey().1 { write_rr(&rr, 1, &mut rr_stream); } + for rr in matcorallo_dnskey().1 { write_rr(&rr, 1, &mut rr_stream); } + let (mut txts, rrsig) = matcorallo_txt_sort_edge_cases_records(); + write_rr(&rrsig, 1, &mut rr_stream); + for txt in txts.iter() { write_rr(txt, 1, &mut rr_stream); } + + let mut rrs = parse_rr_stream(&rr_stream).unwrap(); + rrs.shuffle(&mut rand::rngs::OsRng); + let verified_rrs = verify_rr_stream(&rrs).unwrap(); + let mut verified_txts = verified_rrs.verified_rrs + .iter().map(|rr| if let RR::Txt(txt) = rr { txt.clone() } else { panic!(); }) + .collect::>(); + verified_txts.sort(); + txts.sort(); + assert_eq!(verified_txts, txts); } #[test]