Add testing of doc links in test.sh
[dnssec-prover] / src / validation.rs
index 5a6fda3df71dba92a98dd7407e8bb13b7ef00774..ae0bb58f15de418ecc7305695374d4d8fcd89abd 100644 (file)
@@ -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<DS> {
        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<Vec<RR>, ()> {
-       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: Record>(rr: &RR, ttl: u32, out: &mut Vec<u8>) {
-       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<signature::RsaPublicKeyComponents<&'a [u8]>, ()> {
+       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<Item = &'a DnsKey> {
@@ -165,7 +162,7 @@ where T: IntoIterator<IntoIter = I>, I: Iterator<Item = &'a DS> + 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<IntoIter = I>, I: Iterator<Item = &'a DS> + 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<IntoIter = I>, I: Iterator<Item = &'a DS> + 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<VerifiedRRStream<'a>, ValidationError> {
        let mut zone = ".";
        let mut res = Vec::new();
@@ -292,7 +293,7 @@ pub fn verify_rr_stream<'a>(inp: &'a [RR]) -> Result<VerifiedRRStream<'a>, 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<VerifiedRRStream<'a>, 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<Txt>, 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::<Vec<_>>();
+               verified_txts.sort();
+               txts.sort();
+               assert_eq!(verified_txts, txts);
        }
 
        #[test]