X-Git-Url: http://git.bitcoin.ninja/index.cgi?a=blobdiff_plain;f=src%2Fvalidation.rs;h=57ba096d53bd64169eb213fbaed47c7c31925da8;hb=1d725ca4a022415c85072bc763d50738df863d6d;hp=45320d460f0bf9d2e520ef4558002641dd90ab87;hpb=29454486a3636160e944bde211bf48bd10908180;p=dnssec-prover diff --git a/src/validation.rs b/src/validation.rs index 45320d4..57ba096 100644 --- a/src/validation.rs +++ b/src/validation.rs @@ -5,8 +5,6 @@ use alloc::vec::Vec; use alloc::vec; use core::cmp::{self, Ordering}; -use ring::signature; - use crate::base32; use crate::crypto; use crate::rr::*; @@ -47,28 +45,7 @@ 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>) +fn verify_rrsig<'a, RR: WriteableRecord, Keys>(sig: &RRSig, dnskeys: Keys, mut records: Vec<&RR>) -> Result<(), ValidationError> where Keys: IntoIterator { for record in records.iter() { @@ -82,15 +59,23 @@ where Keys: IntoIterator { if dnskey.flags & 0b1_0000_0000 == 0 { continue; } if dnskey.alg != sig.alg { continue; } - let mut signed_data = Vec::with_capacity(2048); - signed_data.extend_from_slice(&sig.ty.to_be_bytes()); - signed_data.extend_from_slice(&sig.alg.to_be_bytes()); - signed_data.extend_from_slice(&sig.labels.to_be_bytes()); - signed_data.extend_from_slice(&sig.orig_ttl.to_be_bytes()); - signed_data.extend_from_slice(&sig.expiration.to_be_bytes()); - signed_data.extend_from_slice(&sig.inception.to_be_bytes()); - signed_data.extend_from_slice(&sig.key_tag.to_be_bytes()); - write_name(&mut signed_data, &sig.key_name); + let mut hash_ctx = match sig.alg { + 8 => crypto::hash::Hasher::sha256(), + 10 => crypto::hash::Hasher::sha512(), + 13 => crypto::hash::Hasher::sha256(), + 14 => crypto::hash::Hasher::sha384(), + 15 => crypto::hash::Hasher::sha512(), + _ => return Err(ValidationError::UnsupportedAlgorithm), + }; + + hash_ctx.update(&sig.ty.to_be_bytes()); + hash_ctx.update(&sig.alg.to_be_bytes()); + hash_ctx.update(&sig.labels.to_be_bytes()); + hash_ctx.update(&sig.orig_ttl.to_be_bytes()); + hash_ctx.update(&sig.expiration.to_be_bytes()); + hash_ctx.update(&sig.inception.to_be_bytes()); + hash_ctx.update(&sig.key_tag.to_be_bytes()); + write_name(&mut hash_ctx, &sig.key_name); records.sort_unstable(); @@ -106,50 +91,27 @@ where Keys: IntoIterator { let signed_name = record.name().trailing_n_labels(sig.labels); debug_assert!(signed_name.is_some()); if let Some(name) = signed_name { - signed_data.extend_from_slice(b"\x01*"); - write_name(&mut signed_data, name); + hash_ctx.update(b"\x01*"); + write_name(&mut hash_ctx, name); } else { return Err(ValidationError::Invalid); } } else { - write_name(&mut signed_data, record.name()); + write_name(&mut hash_ctx, record.name()); } - signed_data.extend_from_slice(&record.ty().to_be_bytes()); - signed_data.extend_from_slice(&1u16.to_be_bytes()); // The INternet class - signed_data.extend_from_slice(&sig.orig_ttl.to_be_bytes()); - record.write_u16_len_prefixed_data(&mut signed_data); + hash_ctx.update(&record.ty().to_be_bytes()); + hash_ctx.update(&1u16.to_be_bytes()); // The INternet class + hash_ctx.update(&sig.orig_ttl.to_be_bytes()); + record.serialize_u16_len_prefixed(&mut hash_ctx); } + let hash = hash_ctx.finish(); let sig_validation = match sig.alg { - 8|10 => { - let alg = if sig.alg == 8 { - &signature::RSA_PKCS1_1024_8192_SHA256_FOR_LEGACY_USE_ONLY - } else { - &signature::RSA_PKCS1_1024_8192_SHA512_FOR_LEGACY_USE_ONLY - }; - bytes_to_rsa_pk(&dnskey.pubkey).map_err(|_| ValidationError::Invalid)? - .verify(alg, &signed_data, &sig.signature) - .map_err(|_| ValidationError::Invalid) - }, - 13|14 => { - let alg = if sig.alg == 13 { - &signature::ECDSA_P256_SHA256_FIXED - } else { - &signature::ECDSA_P384_SHA384_FIXED - }; - - // Add 0x4 identifier to the ECDSA pubkey as expected by ring. - let mut key = Vec::with_capacity(dnskey.pubkey.len() + 1); - key.push(0x4); - key.extend_from_slice(&dnskey.pubkey); - - signature::UnparsedPublicKey::new(alg, &key) - .verify(&signed_data, &sig.signature) - .map_err(|_| ValidationError::Invalid) - }, - 15 => { - signature::UnparsedPublicKey::new(&signature::ED25519, &dnskey.pubkey) - .verify(&signed_data, &sig.signature) - .map_err(|_| ValidationError::Invalid) - }, + 8|10 => crypto::rsa::validate_rsa(&dnskey.pubkey, &sig.signature, hash.as_ref()) + .map_err(|_| ValidationError::Invalid), + 13 => crypto::secp256r1::validate_ecdsa(&dnskey.pubkey, &sig.signature, hash.as_ref()) + .map_err(|_| ValidationError::Invalid), + 14 => crypto::secp384r1::validate_ecdsa(&dnskey.pubkey, &sig.signature, hash.as_ref()) + .map_err(|_| ValidationError::Invalid), + // TODO: 15 => ED25519 _ => return Err(ValidationError::UnsupportedAlgorithm), }; #[cfg(fuzzing)] { @@ -159,6 +121,12 @@ where Keys: IntoIterator { return Ok(()); } } + + // Note that technically there could be a key tag collision here, causing spurious + // verification failure. In most zones, there's only 2-4 DNSKEY entries, meaning a + // spurious collision shouldn't be much more often than one every billion zones. Much + // more likely in such a case, someone is just trying to do a KeyTrap attack, so we + // simply hard-fail and return an error immediately. sig_validation?; return Ok(()); @@ -167,9 +135,11 @@ where Keys: IntoIterator { Err(ValidationError::Invalid) } -fn verify_dnskey_rrsig<'a, T, I>(sig: &RRSig, dses: T, records: Vec<&DnsKey>) --> Result<(), ValidationError> -where T: IntoIterator, I: Iterator + Clone { +/// Verify [`RRSig`]s over [`DnsKey`], returning a reference to the [`RRSig`] that matched, if any. +fn verify_dnskeys<'r, 'd, RI, R, DI, D>(sigs: RI, dses: DI, records: Vec<&DnsKey>) +-> Result<&'r RRSig, ValidationError> +where RI: IntoIterator, R: Iterator, + DI: IntoIterator, D: Iterator + Clone { let mut validated_dnskeys = Vec::with_capacity(records.len()); let dses = dses.into_iter(); @@ -194,7 +164,7 @@ where T: IntoIterator, I: Iterator + Clone { let mut ctx = match ds.digest_type { 1 if trust_sha1 => crypto::hash::Hasher::sha1(), 2 => crypto::hash::Hasher::sha256(), - // TODO: 4 => crypto::hash::Hasher::sha384(), + 4 => crypto::hash::Hasher::sha384(), _ => continue, }; write_name(&mut ctx, &dnskey.name); @@ -210,7 +180,29 @@ where T: IntoIterator, I: Iterator + Clone { } } } - verify_rrsig(sig, validated_dnskeys.iter().map(|k| *k), records) + + let mut found_unsupported_alg = false; + for sig in sigs { + match verify_rrsig(sig, validated_dnskeys.iter().map(|k| *k), records.clone()) { + Ok(()) => return Ok(sig), + Err(ValidationError::UnsupportedAlgorithm) => { + // There may be redundant signatures by different keys, where one we don't + // supprt and another we do. Ignore ones we don't support, but if there are + // no more, return UnsupportedAlgorithm + found_unsupported_alg = true; + }, + Err(ValidationError::Invalid) => { + // If a signature is invalid, just immediately fail, avoiding KeyTrap issues. + return Err(ValidationError::Invalid); + }, + } + } + + if found_unsupported_alg { + Err(ValidationError::UnsupportedAlgorithm) + } else { + Err(ValidationError::Invalid) + } } /// Given a set of [`RR`]s, [`verify_rr_stream`] checks what it can and returns the set of @@ -326,7 +318,6 @@ pub fn verify_rr_stream<'a>(inp: &'a [RR]) -> Result, Valid let mut earliest_expiry = u64::MAX; let mut min_ttl = u32::MAX; 'next_zone: while zone == "." || !pending_ds_sets.is_empty() { - let mut found_unsupported_alg = false; let next_ds_set; if let Some((next_zone, ds_set)) = pending_ds_sets.pop() { next_ds_set = Some(ds_set); @@ -336,88 +327,78 @@ pub fn verify_rr_stream<'a>(inp: &'a [RR]) -> Result, Valid next_ds_set = None; } + let dnskey_rrsigs = inp.iter() + .filter_map(|rr| if let RR::RRSig(sig) = rr { Some(sig) } else { None }) + .filter(|rrsig| rrsig.name.as_str() == zone && rrsig.ty == DnsKey::TYPE); + let dnskeys = inp.iter() + .filter_map(|rr| if let RR::DnsKey(dnskey) = rr { Some(dnskey) } else { None }) + .filter(move |dnskey| dnskey.name.as_str() == zone); + let root_hints = root_hints(); + let verified_dnskey_rrsig = if zone == "." { + verify_dnskeys(dnskey_rrsigs, &root_hints, dnskeys.clone().collect())? + } else { + debug_assert!(next_ds_set.is_some()); + if next_ds_set.is_none() { break 'next_zone; } + verify_dnskeys(dnskey_rrsigs, next_ds_set.clone().unwrap(), dnskeys.clone().collect())? + }; + latest_inception = cmp::max(latest_inception, resolve_time(verified_dnskey_rrsig.inception)); + earliest_expiry = cmp::min(earliest_expiry, resolve_time(verified_dnskey_rrsig.expiration)); + min_ttl = cmp::min(min_ttl, verified_dnskey_rrsig.orig_ttl); for rrsig in inp.iter() .filter_map(|rr| if let RR::RRSig(sig) = rr { Some(sig) } else { None }) - .filter(|rrsig| rrsig.name.as_str() == zone && rrsig.ty == DnsKey::TYPE) + .filter(move |rrsig| rrsig.key_name.as_str() == zone && rrsig.ty != DnsKey::TYPE) { - let dnskeys = inp.iter() - .filter_map(|rr| if let RR::DnsKey(dnskey) = rr { Some(dnskey) } else { None }) - .filter(move |dnskey| dnskey.name.as_str() == zone); - let dnskeys_verified = if zone == "." { - verify_dnskey_rrsig(rrsig, &root_hints(), dnskeys.clone().collect()) - } else { - debug_assert!(next_ds_set.is_some()); - if next_ds_set.is_none() { break 'next_zone; } - verify_dnskey_rrsig(rrsig, next_ds_set.clone().unwrap(), dnskeys.clone().collect()) - }; - if dnskeys_verified.is_ok() { - latest_inception = cmp::max(latest_inception, resolve_time(rrsig.inception)); - earliest_expiry = cmp::min(earliest_expiry, resolve_time(rrsig.expiration)); - 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.ty != DnsKey::TYPE) - { - if !rrsig.name.ends_with(zone) { return Err(ValidationError::Invalid); } - let signed_records = inp.iter() - .filter(|rr| rr.name() == &rrsig.name && rr.ty() == rrsig.ty); - verify_rrsig(rrsig, dnskeys.clone(), signed_records.clone().collect())?; - latest_inception = cmp::max(latest_inception, resolve_time(rrsig.inception)); - earliest_expiry = cmp::min(earliest_expiry, resolve_time(rrsig.expiration)); - min_ttl = cmp::min(min_ttl, rrsig.orig_ttl); - match rrsig.ty { - // RRSigs shouldn't cover child `DnsKey`s or other `RRSig`s - RRSig::TYPE|DnsKey::TYPE => return Err(ValidationError::Invalid), - DS::TYPE => { - if !pending_ds_sets.iter().any(|(pending_zone, _)| pending_zone == &rrsig.name.as_str()) { - pending_ds_sets.push(( - &rrsig.name, - signed_records.filter_map(|rr| - if let RR::DS(ds) = rr { Some(ds) } - else { debug_assert!(false, "We already filtered by type"); None }) - )); - } - }, - _ => { - if rrsig.labels != rrsig.name.labels() && rrsig.ty != NSec::TYPE { - if rrsig.ty == NSec3::TYPE { - // NSEC3 records should never appear on wildcards, so treat the - // whole proof as invalid - return Err(ValidationError::Invalid); - } - // If the RR used a wildcard, we need an NSEC/NSEC3 proof, which we - // check for at the end. Note that the proof should be for the - // "next closest" name, i.e. if the name here is a.b.c and it was - // signed as *.c, we want a proof for nothing being in b.c. - // Alternatively, if it was signed as *.b.c, we'd want a proof for - // a.b.c. - let proof_name = rrsig.name.trailing_n_labels(rrsig.labels + 1) - .ok_or(ValidationError::Invalid)?; - rrs_needing_non_existence_proofs.push((proof_name, &rrsig.key_name, rrsig.ty)); - } - for record in signed_records { - if !res.contains(&record) { res.push(record); } - } - }, - } + if !rrsig.name.ends_with(zone) { return Err(ValidationError::Invalid); } + let signed_records = inp.iter() + .filter(|rr| rr.name() == &rrsig.name && rr.ty() == rrsig.ty); + match verify_rrsig(rrsig, dnskeys.clone(), signed_records.clone().collect()) { + Ok(()) => {}, + Err(ValidationError::UnsupportedAlgorithm) => continue, + Err(ValidationError::Invalid) => { + // If a signature is invalid, just immediately fail, avoiding KeyTrap issues. + return Err(ValidationError::Invalid); } - continue 'next_zone; - } else if dnskeys_verified == Err(ValidationError::UnsupportedAlgorithm) { - // There may be redundant signatures by different keys, where one we don't supprt - // and another we do. Ignore ones we don't support, but if there are no more, - // return UnsupportedAlgorithm - found_unsupported_alg = true; - } else { - // We don't explicitly handle invalid signatures here, instead we move on to the - // next RRSig (if there is one) and return `Invalid` if no `RRSig`s match. + } + latest_inception = cmp::max(latest_inception, resolve_time(rrsig.inception)); + earliest_expiry = cmp::min(earliest_expiry, resolve_time(rrsig.expiration)); + min_ttl = cmp::min(min_ttl, rrsig.orig_ttl); + match rrsig.ty { + // RRSigs shouldn't cover child `DnsKey`s or other `RRSig`s + RRSig::TYPE|DnsKey::TYPE => return Err(ValidationError::Invalid), + DS::TYPE => { + if !pending_ds_sets.iter().any(|(pending_zone, _)| pending_zone == &rrsig.name.as_str()) { + pending_ds_sets.push(( + &rrsig.name, + signed_records.filter_map(|rr| + if let RR::DS(ds) = rr { Some(ds) } + else { debug_assert!(false, "We already filtered by type"); None }) + )); + } + }, + _ => { + if rrsig.labels != rrsig.name.labels() && rrsig.ty != NSec::TYPE { + if rrsig.ty == NSec3::TYPE { + // NSEC3 records should never appear on wildcards, so treat the + // whole proof as invalid + return Err(ValidationError::Invalid); + } + // If the RR used a wildcard, we need an NSEC/NSEC3 proof, which we + // check for at the end. Note that the proof should be for the + // "next closest" name, i.e. if the name here is a.b.c and it was + // signed as *.c, we want a proof for nothing being in b.c. + // Alternatively, if it was signed as *.b.c, we'd want a proof for + // a.b.c. + let proof_name = rrsig.name.trailing_n_labels(rrsig.labels + 1) + .ok_or(ValidationError::Invalid)?; + rrs_needing_non_existence_proofs.push((proof_name, &rrsig.key_name, rrsig.ty)); + } + for record in signed_records { + if !res.contains(&record) { res.push(record); } + } + }, } } - // No RRSigs were able to verify our DnsKey set - if found_unsupported_alg { - return Err(ValidationError::UnsupportedAlgorithm); - } else { - return Err(ValidationError::Invalid); - } + continue 'next_zone; } if res.is_empty() { return Err(ValidationError::Invalid) } if latest_inception >= earliest_expiry { return Err(ValidationError::Invalid) } @@ -583,7 +564,7 @@ mod tests { signature: base64::decode("GIgwndRLXgt7GX/JNEqSvpYw5ij6EgeQivdC/hmNNuOd2MCQRSxZx2DdLZUoK0tmn2XmOd0vYP06DgkIMUpIXcBstw/Um55WQhvBkBTPIhuB3UvKYJstmq+8hFHWVJwKHTg9xu38JA43VgCV2AbzurbzNOLSgq+rDPelRXzpLr5aYE3y+EuvL+I5gusm4MMajnp5S+ioWOL+yWOnQE6XKoDmlrfcTrYfRSxRtJewPmGeCbNdwEUBOoLUVdkCjQG4uFykcKL40cY8EOhVmM3kXAyuPuNe2Xz1QrIcVad/U4FDns+hd8+W+sWnr8QAtIUFT5pBjXooGS02m6eMdSeU6g==").unwrap(), }; let root_hints = root_hints(); - verify_dnskey_rrsig(&dnskey_rrsig, &root_hints, dnskeys.iter().collect()).unwrap(); + verify_dnskeys([&dnskey_rrsig], &root_hints, dnskeys.iter().collect()).unwrap(); let rrs = vec![dnskeys[0].clone().into(), dnskeys[1].clone().into(), dnskey_rrsig.into()]; (dnskeys, rrs) } @@ -612,7 +593,7 @@ mod tests { expiration: 1710342155, inception: 1709045855, key_tag: 19718, key_name: "com.".try_into().unwrap(), signature: base64::decode("lF2B9nXZn0CgytrHH6xB0NTva4G/aWvg/ypnSxJ8+ZXlvR0C4974yB+nd2ZWzWMICs/oPYMKoQHqxVjnGyu8nA==").unwrap(), }; - verify_dnskey_rrsig(&dnskey_rrsig, &com_ds, dnskeys.iter().collect()).unwrap(); + verify_dnskeys([&dnskey_rrsig], &com_ds, dnskeys.iter().collect()).unwrap(); let rrs = vec![com_ds.pop().unwrap().into(), ds_rrsig.into(), dnskeys[0].clone().into(), dnskeys[1].clone().into(), dnskey_rrsig.into()]; (dnskeys, rrs) @@ -645,7 +626,7 @@ mod tests { expiration: 1710689605, inception: 1708871605, key_tag: 46082, key_name: "ninja.".try_into().unwrap(), signature: base64::decode("kYxV1z+9Ikxqbr13N+8HFWWnAUcvHkr/dmkdf21mliUhH4cxeYCXC6a95X+YzjYQEQi3fU+S346QBDJkbFYCca5q/TzUdE7ej1B/0uTzhgNrQznm0O6sg6DI3HuqDfZp2oaBQm2C/H4vjkcUW9zxgKP8ON0KKLrZUuYelGazeGSOscjDDlmuNMD7tHhFrmK9BiiX+8sp8Cl+IE5ArP+CPXsII+P+R2QTmTqw5ovJch2FLRMRqCliEzTR/IswBI3FfegZR8h9xJ0gfyD2rDqf6lwJhD1K0aS5wxia+bgzpRIKwiGfP87GDYzkygHr83QbmZS2YG1nxlnQ2rgkqTGgXA==").unwrap(), }; - verify_dnskey_rrsig(&dnskey_rrsig, &ninja_ds, dnskeys.iter().collect()).unwrap(); + verify_dnskeys([&dnskey_rrsig], &ninja_ds, dnskeys.iter().collect()).unwrap(); let rrs = vec![ninja_ds.pop().unwrap().into(), ds_rrsig.into(), dnskeys[0].clone().into(), dnskeys[1].clone().into(), dnskeys[2].clone().into(), dnskey_rrsig.into()]; @@ -679,7 +660,7 @@ mod tests { expiration:1710262250, inception: 1709047250, key_tag: 25630, key_name: "mattcorallo.com.".try_into().unwrap(), signature: base64::decode("dMLDvNU96m+tfgpDIQPxMBJy7T0xyZDj3Wws4b4E6+g3nt5iULdWJ8Eqrj+86KLerOVt7KH4h/YcHP18hHdMGA==").unwrap(), }; - verify_dnskey_rrsig(&dnskey_rrsig, &mattcorallo_ds, dnskeys.iter().collect()).unwrap(); + verify_dnskeys([&dnskey_rrsig], &mattcorallo_ds, dnskeys.iter().collect()).unwrap(); let rrs = vec![mattcorallo_ds.pop().unwrap().into(), ds_rrsig.into(), dnskeys[0].clone().into(), dnskeys[1].clone().into(), dnskeys[2].clone().into(), dnskey_rrsig.into()]; @@ -724,7 +705,7 @@ mod tests { expiration: 1709947337, inception: 1708732337, key_tag: 63175, key_name: "bitcoin.ninja.".try_into().unwrap(), signature: base64::decode("Y3To5FZoZuBDUMtIBZXqzRtufyRqOlDqbHVcoZQitXxerCgNQ1CsVdmoFVMmZqRV5n4itINX2x+9G/31j410og==").unwrap(), }; - verify_dnskey_rrsig(&dnskey_rrsig, &bitcoin_ninja_ds, dnskeys.iter().collect()).unwrap(); + verify_dnskeys([&dnskey_rrsig], &bitcoin_ninja_ds, dnskeys.iter().collect()).unwrap(); let rrs = vec![bitcoin_ninja_ds.pop().unwrap().into(), ds_rrsig.into(), dnskeys[0].clone().into(), dnskeys[1].clone().into(), dnskey_rrsig.into()]; (dnskeys, rrs) @@ -890,7 +871,7 @@ mod tests { key_tag: 8036, key_name: "nsec_tests.dnssec_proof_tests.bitcoin.ninja.".try_into().unwrap(), signature: base64::decode("nX+hkH14Kvjp26Z8x/pjYh5CQW3p9lZQQ+FVJcKHyfjAilEubpw6ihlPpb3Ddh9BbyxhCEFhXDMG2g4od9Y2ow==").unwrap(), }; - verify_dnskey_rrsig(&dnskey_rrsig, &bitcoin_ninja_ds, dnskeys.iter().collect()).unwrap(); + verify_dnskeys([&dnskey_rrsig], &bitcoin_ninja_ds, dnskeys.iter().collect()).unwrap(); let rrs = vec![bitcoin_ninja_ds.pop().unwrap().into(), ds_rrsig.into(), dnskeys[0].clone().into(), dnskeys[1].clone().into(), dnskey_rrsig.into()]; (dnskeys, rrs) @@ -1101,6 +1082,81 @@ mod tests { } else { panic!(); } } + #[test] + fn check_simple_nsec_zone_proof() { + let mut rr_stream = Vec::new(); + for rr in root_dnskey().1 { write_rr(&rr, 1, &mut rr_stream); } + for rr in ninja_dnskey().1 { write_rr(&rr, 1, &mut rr_stream); } + for rr in bitcoin_ninja_dnskey().1 { write_rr(&rr, 1, &mut rr_stream); } + for rr in bitcoin_ninja_nsec_dnskey().1 { write_rr(&rr, 1, &mut rr_stream); } + let (txt, txt_rrsig) = bitcoin_ninja_nsec_record(); + for rr in [RR::Txt(txt), RR::RRSig(txt_rrsig)] { write_rr(&rr, 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 filtered_rrs = + verified_rrs.resolve_name(&"a.nsec_tests.dnssec_proof_tests.bitcoin.ninja.".try_into().unwrap()); + assert_eq!(filtered_rrs.len(), 1); + if let RR::Txt(txt) = &filtered_rrs[0] { + assert_eq!(txt.name.as_str(), "a.nsec_tests.dnssec_proof_tests.bitcoin.ninja."); + assert_eq!(txt.data, b"txt_a"); + } else { panic!(); } + } + + #[test] + fn check_nsec_wildcard_proof() { + let check_proof = |pfx: &str, post_override: bool| -> Result<(), ()> { + let mut rr_stream = Vec::new(); + for rr in root_dnskey().1 { write_rr(&rr, 1, &mut rr_stream); } + for rr in ninja_dnskey().1 { write_rr(&rr, 1, &mut rr_stream); } + for rr in bitcoin_ninja_dnskey().1 { write_rr(&rr, 1, &mut rr_stream); } + for rr in bitcoin_ninja_nsec_dnskey().1 { write_rr(&rr, 1, &mut rr_stream); } + let (txt, txt_rrsig, nsec, nsec_rrsig) = if post_override { + bitcoin_ninja_nsec_post_override_wildcard_record(pfx) + } else { + bitcoin_ninja_nsec_wildcard_record(pfx) + }; + for rr in [RR::Txt(txt), RR::RRSig(txt_rrsig)] { write_rr(&rr, 1, &mut rr_stream); } + for rr in [RR::NSec(nsec), RR::RRSig(nsec_rrsig)] { write_rr(&rr, 1, &mut rr_stream); } + + let mut rrs = parse_rr_stream(&rr_stream).unwrap(); + rrs.shuffle(&mut rand::rngs::OsRng); + // If the post_override flag is wrong (or the pfx is override), this will fail. No + // other calls in this lambda should fail. + let verified_rrs = verify_rr_stream(&rrs).map_err(|_| ())?; + let name: Name = + (pfx.to_owned() + ".wildcard_test.nsec_tests.dnssec_proof_tests.bitcoin.ninja.").try_into().unwrap(); + let filtered_rrs = verified_rrs.resolve_name(&name); + assert_eq!(filtered_rrs.len(), 1); + if let RR::Txt(txt) = &filtered_rrs[0] { + assert_eq!(txt.name, name); + assert_eq!(txt.data, b"wildcard_test"); + } else { panic!(); } + Ok(()) + }; + // Records up to override will only work with the pre-override NSEC, and afterwards with + // the post-override NSEC. The literal override will always fail. + check_proof("a", false).unwrap(); + check_proof("a", true).unwrap_err(); + check_proof("a.b", false).unwrap(); + check_proof("a.b", true).unwrap_err(); + check_proof("o", false).unwrap(); + check_proof("o", true).unwrap_err(); + check_proof("a.o", false).unwrap(); + check_proof("a.o", true).unwrap_err(); + check_proof("override", false).unwrap_err(); + check_proof("override", true).unwrap_err(); + // Subdomains of override are also overridden by the override TXT entry and cannot use the + // wildcard record. + check_proof("b.override", false).unwrap_err(); + check_proof("b.override", true).unwrap_err(); + check_proof("z", false).unwrap_err(); + check_proof("z", true).unwrap_err(); + check_proof("a.z", false).unwrap_err(); + check_proof("a.z", true).unwrap_err(); + } + #[test] fn check_txt_sort_order() { let mut rr_stream = Vec::new();