Skip to main content

dnssec_prover/
validation.rs

1//! Utilities to deserialize and validate RFC 9102 proofs
2
3use alloc::borrow::ToOwned;
4use alloc::vec::Vec;
5use alloc::vec;
6use core::cmp::{self, Ordering};
7
8use crate::base32;
9use crate::crypto;
10use crate::rr::*;
11use crate::ser::write_name;
12use crate::unhex::unhex;
13use crate::MAX_PROOF_STEPS;
14
15/// Gets the trusted root anchors
16///
17/// These are available at <https://data.iana.org/root-anchors/root-anchors.xml>
18pub fn root_hints() -> Vec<DS> {
19	#[allow(unused_mut)]
20	let mut res = vec![
21	// The 2010 key was only valid until 2019, predating this software substantially. We don't
22	// bother to implement checking that it is only used on old proofs so simply do not use it.
23	/*DS {
24		name: ".".try_into().unwrap(), key_tag: 19036, alg: 8, digest_type: 2,
25		digest: unhex:<32>("49AAC11D7B6F6446702E54A1607371607A1A41855200FD2CE1CDDE32F24E8FB5").to_vec(),
26	},*/
27	DS {
28		name: ".".try_into().unwrap(), key_tag: 20326, alg: 8, digest_type: 2,
29		digest: unhex::<32>("E06D44B80B8F1D39A95C0B0D7C65D08458E880409BBC683457104237C7F8EC8D").to_vec(),
30	}, DS {
31		name: ".".try_into().unwrap(), key_tag: 38696, alg: 8, digest_type: 2,
32		digest: unhex::<32>("683D2D0ACB8C9B712A1948B27F741219298D0A450D612C483AF444A4C0FB2B16").to_vec(),
33	}];
34	// In tests, add the trust anchor from RFC 9102
35	#[cfg(test)]
36	res.push(DS {
37		name: ".".try_into().unwrap(), key_tag: 47005, alg: 13, digest_type: 2,
38		digest: unhex::<32>("2eb6e9f2480126691594d649a5a613de3052e37861634641bb568746f2ffc4d4").to_vec(),
39	});
40	res
41}
42
43#[derive(Debug, PartialEq)]
44/// An error when validating DNSSEC signatures or other data
45pub enum ValidationError {
46	/// An algorithm used in signing was not supported.
47	///
48	/// While data was provided, it couldn't be authenticated and may be forged.
49	///
50	/// In cases where signing is mandatory, this can be treated as an error.
51	UnsupportedAlgorithm,
52	/// The provided data was invalid or signatures did not validate.
53	Invalid,
54	/// We would need to validate more than [`MAX_PROOF_STEPS`] sets of [`RRSig`]s to validate the
55	/// proof we were given.
56	ValidationCountLimited,
57}
58
59fn verify_rrsig<'a, RR: WriteableRecord, Keys>(sig: &RRSig, dnskeys: Keys, mut records: Vec<&RR>)
60-> Result<(), ValidationError>
61where Keys: IntoIterator<Item = &'a DnsKey> {
62	for record in records.iter() {
63		if sig.ty != record.ty() { return Err(ValidationError::Invalid); }
64	}
65	for dnskey in dnskeys.into_iter() {
66		if dnskey.key_tag() == sig.key_tag {
67			// Protocol must be 3, otherwise its not DNSSEC
68			if dnskey.protocol != 3 { continue; }
69			// The ZONE flag must be set if we're going to validate RRs with this key.
70			if dnskey.flags & 0b1_0000_0000 == 0 { continue; }
71			// the REVOKE flag must not be set.
72			if dnskey.flags & 0b0_1000_0000 != 0 { continue; }
73			if dnskey.alg != sig.alg { continue; }
74
75			let mut hash_ctx = match sig.alg {
76				8 => crypto::hash::Hasher::sha256(),
77				10 => crypto::hash::Hasher::sha512(),
78				13 => crypto::hash::Hasher::sha256(),
79				14 => crypto::hash::Hasher::sha384(),
80				15 => crypto::hash::Hasher::sha512(),
81				_ => return Err(ValidationError::UnsupportedAlgorithm),
82			};
83
84			hash_ctx.update(&sig.ty.to_be_bytes());
85			hash_ctx.update(&sig.alg.to_be_bytes());
86			hash_ctx.update(&sig.labels.to_be_bytes());
87			hash_ctx.update(&sig.orig_ttl.to_be_bytes());
88			hash_ctx.update(&sig.expiration.to_be_bytes());
89			hash_ctx.update(&sig.inception.to_be_bytes());
90			hash_ctx.update(&sig.key_tag.to_be_bytes());
91			write_name(&mut hash_ctx, &sig.key_name);
92
93			records.sort_unstable();
94
95			// Some recursive resolvers (at least 9.9.9.9) give us a few too many records, and the
96			// proof builder is too naive to filter them out. Instead, we filter them out here, as
97			// there's no security harm to just removing identical records here.
98			records.dedup();
99
100			for record in records.iter() {
101				let record_labels = record.name().labels() as usize;
102				let labels = sig.labels.into();
103				// For NSec types, the name should already match the wildcard, so we don't do any
104				// filtering here. This is relied upon in `verify_rr_stream` to check whether an
105				// NSec record is matching via wildcard (as otherwise we'd allow a resolver to
106				// change the name out from under us and change the wildcard to something else).
107				if record.ty() != NSec::TYPE && record_labels != labels {
108					if record_labels < labels { return Err(ValidationError::Invalid); }
109					let signed_name = record.name().trailing_n_labels(sig.labels);
110					debug_assert!(signed_name.is_some());
111					if let Some(name) = signed_name {
112						hash_ctx.update(b"\x01*");
113						write_name(&mut hash_ctx, name);
114					} else { return Err(ValidationError::Invalid); }
115				} else {
116					write_name(&mut hash_ctx, record.name());
117				}
118				hash_ctx.update(&record.ty().to_be_bytes());
119				hash_ctx.update(&1u16.to_be_bytes()); // The INternet class
120				hash_ctx.update(&sig.orig_ttl.to_be_bytes());
121				record.serialize_u16_len_prefixed(&mut hash_ctx);
122			}
123
124			let hash = hash_ctx.finish();
125			let sig_validation = match sig.alg {
126				8|10 => crypto::rsa::validate_rsa(&dnskey.pubkey, &sig.signature, hash.as_ref())
127					.map_err(|_| ValidationError::Invalid),
128				13 => crypto::secp256r1::validate_ecdsa(&dnskey.pubkey, &sig.signature, hash.as_ref())
129					.map_err(|_| ValidationError::Invalid),
130				14 => crypto::secp384r1::validate_ecdsa(&dnskey.pubkey, &sig.signature, hash.as_ref())
131					.map_err(|_| ValidationError::Invalid),
132				// TODO: 15 => ED25519
133				_ => return Err(ValidationError::UnsupportedAlgorithm),
134			};
135			#[cfg(fuzzing)] {
136				// When fuzzing, treat any signature starting with a 1 as valid, but only after
137				// parsing and checking signatures to give that code a chance to panic.
138				if sig.signature.get(0) == Some(&1) {
139					return Ok(());
140				}
141			}
142
143			// Note that technically there could be a key tag collision here, causing spurious
144			// verification failure. In most zones, there's only 2-4 DNSKEY entries, meaning a
145			// spurious collision shouldn't be much more often than one every billion zones. Much
146			// more likely in such a case, someone is just trying to do a KeyTrap attack, so we
147			// simply hard-fail and return an error immediately.
148			sig_validation?;
149
150			return Ok(());
151		}
152	}
153	Err(ValidationError::Invalid)
154}
155
156/// Verify [`RRSig`]s over [`DnsKey`], returning a reference to the [`RRSig`] that matched, if any.
157fn verify_dnskeys<'r, 'd, RI, R, DI, D>(sigs: RI, dses: DI, records: Vec<&DnsKey>)
158-> Result<&'r RRSig, ValidationError>
159where RI: IntoIterator<IntoIter = R>, R: Iterator<Item = &'r RRSig>,
160      DI: IntoIterator<IntoIter = D>, D: Iterator<Item = &'d DS> + Clone {
161	let mut validated_dnskeys = Vec::with_capacity(records.len());
162	let dses = dses.into_iter();
163
164	let mut had_known_digest_type = false;
165	let mut had_ds = false;
166	for ds in dses.clone() {
167		had_ds = true;
168		if ds.digest_type == 1 || ds.digest_type == 2 || ds.digest_type == 4 {
169			had_known_digest_type = true;
170			break;
171		}
172	}
173	if !had_ds { return Err(ValidationError::Invalid); }
174	if !had_known_digest_type { return Err(ValidationError::UnsupportedAlgorithm); }
175
176	for dnskey in records.iter() {
177		// Only use SHA1 DS records if we don't have any SHA256/SHA384 DS RRs.
178		let trust_sha1 = dses.clone().all(|ds| ds.digest_type != 2 && ds.digest_type != 4);
179		for ds in dses.clone() {
180			if ds.alg != dnskey.alg { continue; }
181			if dnskey.key_tag() == ds.key_tag {
182				let mut ctx = match ds.digest_type {
183					1 if trust_sha1 => crypto::hash::Hasher::sha1(),
184					2 => crypto::hash::Hasher::sha256(),
185					4 => crypto::hash::Hasher::sha384(),
186					_ => continue,
187				};
188				write_name(&mut ctx, &dnskey.name);
189				ctx.update(&dnskey.flags.to_be_bytes());
190				ctx.update(&dnskey.protocol.to_be_bytes());
191				ctx.update(&dnskey.alg.to_be_bytes());
192				ctx.update(&dnskey.pubkey);
193				let hash = ctx.finish();
194				if hash.as_ref() == ds.digest {
195					validated_dnskeys.push(*dnskey);
196					break;
197				}
198			}
199		}
200	}
201
202	let mut found_unsupported_alg = false;
203	for sig in sigs {
204		if !validated_dnskeys.iter().any(|key| key.key_tag() == sig.key_tag) {
205			// Some DNS servers include spurious RRSig records signed by the ZSK covering the
206			// DNSKEY set (looking at you OVH). This is harmless (but wasteful) and we should
207			// ignore such signatures rather than immediately failing.
208			continue;
209		}
210		match verify_rrsig(sig, validated_dnskeys.iter().copied(), records.clone()) {
211			Ok(()) => return Ok(sig),
212			Err(ValidationError::UnsupportedAlgorithm) => {
213				// There may be redundant signatures by different keys, where one we don't
214				// supprt and another we do. Ignore ones we don't support, but if there are
215				// no more, return UnsupportedAlgorithm
216				found_unsupported_alg = true;
217			},
218			Err(ValidationError::ValidationCountLimited) => {
219				debug_assert!(false, "verify_rrsig doesn't internally limit");
220				return Err(ValidationError::ValidationCountLimited);
221			},
222			Err(ValidationError::Invalid) => {
223				// If a signature is invalid, just immediately fail, avoiding KeyTrap issues.
224				return Err(ValidationError::Invalid);
225			},
226		}
227	}
228
229	if found_unsupported_alg {
230		Err(ValidationError::UnsupportedAlgorithm)
231	} else {
232		Err(ValidationError::Invalid)
233	}
234}
235
236/// Given a set of [`RR`]s, [`verify_rr_stream`] checks what it can and returns the set of
237/// non-[`RRSig`]/[`DnsKey`]/[`DS`] records which it was able to verify using this struct.
238///
239/// It also contains signing and expiry times, which must be validated before considering the
240/// contained records verified.
241#[derive(Debug, Clone)]
242pub struct VerifiedRRStream<'a> {
243	/// The set of verified [`RR`]s, not including [`DnsKey`], [`RRSig`], [`NSec`], and [`NSec3`]
244	/// records.
245	///
246	/// These are not valid unless the current UNIX time is between [`Self::valid_from`] and
247	/// [`Self::expires`].
248	pub verified_rrs: Vec<&'a RR>,
249	/// The latest [`RRSig::inception`] of all the [`RRSig`]s validated to verify
250	/// [`Self::verified_rrs`].
251	///
252	/// Any records in [`Self::verified_rrs`] should not be considered valid unless this is before
253	/// the current UNIX time.
254	///
255	/// While the field here is a u64, the algorithm used to identify rollovers will fail in 2133.
256	pub valid_from: u64,
257	/// The earliest [`RRSig::expiration`] of all the [`RRSig`]s validated to verify
258	/// [`Self::verified_rrs`].
259	///
260	/// Any records in [`Self::verified_rrs`] should not be considered valid unless this is after
261	/// the current UNIX time.
262	///
263	/// While the field here is a u64, the algorithm used to identify rollovers will fail in 2133.
264	pub expires: u64,
265	/// The minimum [`RRSig::orig_ttl`] of all the [`RRSig`]s validated to verify
266	/// [`Self::verified_rrs`].
267	///
268	/// Any caching of [`Self::verified_rrs`] must not last longer than this value, in seconds.
269	pub max_cache_ttl: u32,
270}
271
272fn resolve_time(time: u32) -> u64 {
273	// RFC 2065 was published in January 1997, so we arbitrarily use that as a cutoff and assume
274	// any timestamps before then are actually past 2106 instead.
275	// We ignore leap years for simplicity.
276	if time < 60*60*24*365*27 {
277		(time as u64) + (u32::MAX as u64)
278	} else {
279		time.into()
280	}
281}
282
283fn nsec_ord(a: &[u8], b: &[u8]) -> Ordering {
284	let mut a_label_iter = a.rsplit(|c| *c == b'.');
285	let mut b_label_iter = b.rsplit(|c| *c == b'.');
286	loop {
287		match (a_label_iter.next(), b_label_iter.next()) {
288			(Some(_), None) => return Ordering::Greater,
289			(None, Some(_)) => return Ordering::Less,
290			(Some(a_label), Some(b_label)) => {
291				let mut a_bytes = a_label.iter().copied();
292				let mut b_bytes = b_label.iter().copied();
293				loop {
294					match (a_bytes.next(), b_bytes.next()) {
295						(Some(_), None) => return Ordering::Greater,
296						(None, Some(_)) => return Ordering::Less,
297						(Some(mut a), Some(mut b)) => {
298							if a.is_ascii_uppercase() {
299								a += b'a' - b'A';
300							}
301							if b.is_ascii_uppercase() {
302								b += b'a' - b'A';
303							}
304							if a != b { return a.cmp(&b); }
305						},
306						(None, None) => break,
307					}
308				}
309			},
310			(None, None) => return Ordering::Equal,
311		}
312	}
313}
314fn nsec_ord_extra<T>(a: &(&str, T), b: &(&str, T)) -> Ordering {
315	nsec_ord(a.0.as_bytes(), b.0.as_bytes())
316}
317
318#[cfg(test)]
319#[test]
320fn rfc4034_sort_test() {
321	// Test nsec_ord based on RFC 4034 section 6.1's example
322	let v = vec![&b"example."[..], &b"a.example."[..], &b"yljkjljk.a.example."[..],
323		&b"Z.a.example."[..], &b"zABC.a.EXAMPLE."[..], &b"z.example."[..],
324		&b"\001.z.example."[..], &b"*.z.example."[..], &b"\xc8.z.example."[..]];
325	let mut sorted = v.clone();
326	sorted.sort_unstable_by(|a, b| nsec_ord(a, b));
327	assert_eq!(sorted, v);
328}
329
330/// Verifies the given set of resource records.
331///
332/// Given a set of arbitrary records, this attempts to validate DNSSEC data from the [`root_hints`]
333/// through to any supported non-DNSSEC record types.
334///
335/// All records which could be validated are returned, though if an error is found validating any
336/// contained record, only `Err` will be returned.
337///
338/// You MUST check that the current UNIX time is between [`VerifiedRRStream::valid_from`] and
339/// [`VerifiedRRStream::expires`].
340pub fn verify_rr_stream<'a>(inp: &'a [RR]) -> Result<VerifiedRRStream<'a>, ValidationError> {
341	let mut zone = ".";
342	let mut res = Vec::new();
343	let mut rrs_needing_non_existence_proofs = Vec::new();
344	let mut nsec_records = Vec::new();
345	let mut pending_ds_sets = Vec::with_capacity(1);
346	let mut latest_inception = 0;
347	let mut earliest_expiry = u64::MAX;
348	let mut min_ttl = u32::MAX;
349	let mut rrsig_sets_validated = 0;
350	'next_zone: while zone == "." || !pending_ds_sets.is_empty() {
351		let next_ds_set;
352		if let Some((next_zone, ds_set)) = pending_ds_sets.pop() {
353			next_ds_set = Some(ds_set);
354			zone = next_zone;
355		} else {
356			debug_assert_eq!(zone, ".");
357			next_ds_set = None;
358		}
359
360		rrsig_sets_validated += 1;
361		if rrsig_sets_validated > MAX_PROOF_STEPS {
362			return Err(ValidationError::ValidationCountLimited);
363		}
364
365		let dnskey_rrsigs = inp.iter()
366			.filter_map(|rr| if let RR::RRSig(sig) = rr { Some(sig) } else { None })
367			.filter(|rrsig| rrsig.name.as_str() == zone && rrsig.ty == DnsKey::TYPE);
368		let dnskeys = inp.iter()
369			.filter_map(|rr| if let RR::DnsKey(dnskey) = rr { Some(dnskey) } else { None })
370			.filter(move |dnskey| dnskey.name.as_str() == zone);
371		let root_hints = root_hints();
372		let verified_dnskey_rrsig = if zone == "." {
373			verify_dnskeys(dnskey_rrsigs, &root_hints, dnskeys.clone().collect())?
374		} else {
375			debug_assert!(next_ds_set.is_some());
376			if next_ds_set.is_none() { break 'next_zone; }
377			verify_dnskeys(dnskey_rrsigs, next_ds_set.clone().unwrap(), dnskeys.clone().collect())?
378		};
379		latest_inception = cmp::max(latest_inception, resolve_time(verified_dnskey_rrsig.inception));
380		earliest_expiry = cmp::min(earliest_expiry, resolve_time(verified_dnskey_rrsig.expiration));
381		min_ttl = cmp::min(min_ttl, verified_dnskey_rrsig.orig_ttl);
382
383		for rrsig in inp.iter()
384			.filter_map(|rr| if let RR::RRSig(sig) = rr { Some(sig) } else { None })
385			.filter(move |rrsig| rrsig.key_name.as_str() == zone && rrsig.ty != DnsKey::TYPE)
386		{
387			rrsig_sets_validated += 1;
388			if rrsig_sets_validated > MAX_PROOF_STEPS {
389				return Err(ValidationError::ValidationCountLimited);
390			}
391
392			if !rrsig.name.ends_with_labels(zone) { return Err(ValidationError::Invalid); }
393			let signed_records = inp.iter()
394				.filter(|rr| rr.name() == &rrsig.name && rr.ty() == rrsig.ty);
395			match verify_rrsig(rrsig, dnskeys.clone(), signed_records.clone().collect()) {
396				Ok(()) => {},
397				Err(ValidationError::UnsupportedAlgorithm) => continue,
398				Err(ValidationError::ValidationCountLimited) => {
399					debug_assert!(false, "verify_rrsig doesn't internally limit");
400					return Err(ValidationError::ValidationCountLimited);
401				},
402				Err(ValidationError::Invalid) => {
403					// If a signature is invalid, just immediately fail, avoiding KeyTrap issues.
404					return Err(ValidationError::Invalid);
405				}
406			}
407			latest_inception = cmp::max(latest_inception, resolve_time(rrsig.inception));
408			earliest_expiry = cmp::min(earliest_expiry, resolve_time(rrsig.expiration));
409			min_ttl = cmp::min(min_ttl, rrsig.orig_ttl);
410			match rrsig.ty {
411				// RRSigs shouldn't cover child `DnsKey`s or other `RRSig`s
412				RRSig::TYPE|DnsKey::TYPE => return Err(ValidationError::Invalid),
413				DS::TYPE => {
414					// Ignore wildcard `DS` records as it would be impossible to include the
415					// nearest-neighbor non-existence NSEC/NSEC3 required after the zone cut.
416					if rrsig.labels != rrsig.name.labels() { continue; }
417					if !pending_ds_sets.iter().any(|(pending_zone, _)| pending_zone == &rrsig.name.as_str()) {
418						pending_ds_sets.push((
419							&rrsig.name,
420							signed_records.filter_map(|rr|
421								if let RR::DS(ds) = rr { Some(ds) }
422								else { debug_assert!(false, "We already filtered by type"); None })
423						));
424					}
425				},
426				_ => {
427					if rrsig.labels != rrsig.name.labels() && rrsig.ty != NSec::TYPE {
428						if rrsig.ty == NSec3::TYPE {
429							// NSEC3 records should never appear on wildcards, so treat the
430							// whole proof as invalid
431							return Err(ValidationError::Invalid);
432						}
433						// If the RR used a wildcard, we need an NSEC/NSEC3 proof, which we
434						// check for at the end. Note that the proof should be for the
435						// "next closest" name, i.e. if the name here is a.b.c and it was
436						// signed as *.c, we want a proof for nothing being in b.c.
437						// Alternatively, if it was signed as *.b.c, we'd want a proof for
438						// a.b.c.
439						if rrsig.labels == u8::MAX { return Err(ValidationError::Invalid); }
440						let proof_name = rrsig.name.trailing_n_labels(rrsig.labels + 1)
441							.ok_or(ValidationError::Invalid)?;
442						rrs_needing_non_existence_proofs.push((proof_name, &rrsig.key_name));
443					}
444					for record in signed_records {
445						if !res.contains(&record) {
446							if record.ty() == NSec::TYPE || record.ty() == NSec3::TYPE {
447								nsec_records.push((record, &rrsig.key_name));
448							}
449							res.push(record);
450						}
451					}
452				},
453			}
454		}
455		continue 'next_zone;
456	}
457	if res.is_empty() { return Err(ValidationError::Invalid) }
458	if latest_inception >= earliest_expiry { return Err(ValidationError::Invalid) }
459
460	// First sort the proofs we're looking for so that the retains below avoid shifting.
461	rrs_needing_non_existence_proofs.sort_unstable_by(nsec_ord_extra);
462	'proof_search_loop: while let Some((name, zone)) = rrs_needing_non_existence_proofs.pop() {
463		let local_zone_nsecs =
464			nsec_records.iter().filter(|(_, nsec_zone)| *nsec_zone == zone).map(|(rr, _)| rr);
465		let nsec_search = local_zone_nsecs.clone()
466			.filter_map(|rr| if let RR::NSec(nsec) = rr { Some(nsec) } else { None });
467		for nsec in nsec_search {
468			// If the NSEC next_name ends with the next closest name we're looking for we have an
469			// overlap between a real subdomain (or a subdomain of one) and the name we were told
470			// resolved to a wildcard. This is forbidden - if a.b.c.d.e exists, *.e cannot be used
471			// for any of a.b.c.d.e, *.b.c.d.e, *.c.d.e or *.d.e.
472			if name_ends_with_labels(&nsec.next_name, name) { continue; }
473			// Note that the last NSEC in a zone's chain wraps around.
474			let after_start = nsec_ord(nsec.name.as_bytes(), name.as_bytes()) == Ordering::Less;
475			let before_end = nsec_ord(&nsec.next_name, name.as_bytes()) == Ordering::Greater;
476			let name_contained = if nsec_ord(nsec.name.as_bytes(), &nsec.next_name) == Ordering::Less {
477				after_start && before_end
478			} else {
479				after_start || before_end
480			};
481			if name_contained {
482				rrs_needing_non_existence_proofs.retain(|(n, z)| *n != name || *z != zone);
483				continue 'proof_search_loop;
484			}
485		}
486
487		let nsec3_search = local_zone_nsecs.clone()
488			.filter_map(|rr| if let RR::NSec3(nsec3) = rr { Some(nsec3) } else { None });
489		// Because we will only ever have two entries, a Vec is simpler than a map here.
490		let mut nsec3params_to_name_hash = Vec::new();
491		for nsec3 in nsec3_search.clone() {
492			if nsec3.hash_iterations > 2500 {
493				// RFC 5115 places different limits on the iterations based on the signature key
494				// length, but we just use 2500 for all key types
495				continue;
496			}
497			if nsec3.hash_algo != 1 { continue; }
498			if nsec3params_to_name_hash.iter()
499				.any(|(iterations, salt, _)| *iterations == nsec3.hash_iterations && *salt == &nsec3.salt)
500			{ continue; }
501
502			let mut hasher = crypto::hash::Hasher::sha1();
503			write_name(&mut hasher, name);
504			hasher.update(&nsec3.salt);
505			for _ in 0..nsec3.hash_iterations {
506				let res = hasher.finish();
507				hasher = crypto::hash::Hasher::sha1();
508				hasher.update(res.as_ref());
509				hasher.update(&nsec3.salt);
510			}
511			nsec3params_to_name_hash.push((nsec3.hash_iterations, &nsec3.salt, hasher.finish()));
512
513			if nsec3params_to_name_hash.len() >= 2 {
514				// We only allow for up to two sets of hash_iterations/salt per zone. Beyond that
515				// we assume this is a malicious DoSing proof and give up.
516				break;
517			}
518		}
519		for nsec3 in nsec3_search {
520			if nsec3.flags != 0 {
521				// This is an opt-out NSEC3 (or has unknown flags set). Thus, we shouldn't rely on
522				// it as proof that some record doesn't exist.
523				continue;
524			}
525			if nsec3.hash_algo != 1 { continue; }
526			let name_hash = if let Some((_, _, hash)) =
527				nsec3params_to_name_hash.iter()
528				.find(|(iterations, salt, _)| *iterations == nsec3.hash_iterations && *salt == &nsec3.salt)
529			{
530				hash
531			} else { continue };
532
533			let (start_hash_base32, _) = nsec3.name.split_once('.')
534				.unwrap_or_else(|| { debug_assert!(false); ("", "")});
535			let start_hash = if let Ok(start_hash) = base32::decode(start_hash_base32) {
536				start_hash
537			} else { continue };
538			if start_hash.len() != 20 || nsec3.next_name_hash.len() != 20 { continue; }
539
540			// Note that the last NSEC3 in a zone's chain wraps around.
541			let after_start = &start_hash[..] < name_hash.as_ref();
542			let before_end = &nsec3.next_name_hash[..] > name_hash.as_ref();
543			let hash_contained = if start_hash[..] < nsec3.next_name_hash[..] {
544				after_start && before_end
545			} else {
546				after_start || before_end
547			};
548			if hash_contained {
549				rrs_needing_non_existence_proofs.retain(|(n, z)| *n != name || *z != zone);
550				continue 'proof_search_loop;
551			}
552		}
553		return Err(ValidationError::Invalid);
554	}
555
556	res.retain(|rr| rr.ty() != NSec::TYPE && rr.ty() != NSec3::TYPE);
557
558	Ok(VerifiedRRStream {
559		verified_rrs: res, valid_from: latest_inception, expires: earliest_expiry,
560		max_cache_ttl: min_ttl,
561	})
562}
563
564impl<'a> VerifiedRRStream<'a> {
565	/// Given a name, resolve any [`CName`] records and return any verified records which were
566	/// pointed to by the original name.
567	///
568	/// Note that because of [`CName`]s, the [`RR::name`] in the returned records may or may not be
569	/// equal to `name`.
570	///
571	/// You MUST still check that the current UNIX time is between
572	/// [`VerifiedRRStream::valid_from`] and [`VerifiedRRStream::expires`] before
573	/// using any records returned here.
574	pub fn resolve_name<'b>(&self, name_param: &'b Name) -> Vec<&'a RR> where 'a: 'b {
575		let mut dname_name;
576		let mut name = name_param;
577		for _ in 0..MAX_PROOF_STEPS {
578			let mut cname_search = self.verified_rrs.iter()
579				.filter(|rr| rr.name() == name)
580				.filter_map(|rr| if let RR::CName(cn) = rr { Some(cn) } else { None });
581			if let Some(cname) = cname_search.next() {
582				name = &cname.canonical_name;
583				continue;
584			}
585
586			let mut dname_search = self.verified_rrs.iter()
587				.filter(|rr| name.len() > rr.name().len() && name.ends_with_labels(&**rr.name()))
588				.filter_map(|rr| if let RR::DName(dn) = rr { Some(dn) } else { None });
589			if let Some(dname) = dname_search.next() {
590				let prefix = name.strip_suffix(&*dname.name).expect("We just filtered for this");
591				let resolved_name = if dname.delegation_name.as_str() == "." {
592					prefix.to_owned()
593				} else {
594					prefix.to_owned() + &dname.delegation_name
595				};
596				dname_name = if let Ok(name) = resolved_name.try_into() {
597					name
598				} else {
599					// This should only happen if the combined name ended up being too long
600					return Vec::new();
601				};
602				name = &dname_name;
603				continue;
604			}
605
606			return self.verified_rrs.iter().filter(|rr| rr.name() == name).copied().collect();
607		}
608		Vec::new()
609	}
610}
611
612#[cfg(test)]
613mod tests {
614	#![allow(deprecated)]
615
616	use super::*;
617
618	use alloc::borrow::ToOwned;
619
620	use crate::ser::{parse_rr_stream, write_rr};
621
622	use hex_conservative::FromHex;
623	use rand::seq::SliceRandom;
624
625	fn root_dnskey() -> (Vec<DnsKey>, Vec<RR>) {
626		let dnskeys = vec![DnsKey {
627			name: ".".try_into().unwrap(), flags: 256, protocol: 3, alg: 8,
628			pubkey: base64::decode("AwEAAeCYD6Z7WWKVLeuWgowKP+3g+Gs1cnLKq7a3CaQxQpv8bfuFVI0WnG33qaSH/Mw9IBgifrdzf4XY/DQLnyBJ9MfaOyAWuEaEmYJ+GQPiwVVfstGwSA1McfFJUttTgq2Huu74KARhtA8wPo/N3XcyYQtNhz+qCM5NBb3ecx/naw6sYab9LxS6f2cU0q03++BP5Ks0Uef8WJCa/1izCYE+vMkwoltV+tENa3hpXiZ7jle/xdgaZrPi5ZGmyLVI34g1XVYrNlsCCTmNvFQIfzW5STFQFsQpizczyFn9r3LzSxxPCNwdlCG84bER0BmdwqbF6Tanv+FxMOavrahkj4wIy5k=").unwrap(),
629		}, DnsKey {
630			name: ".".try_into().unwrap(), flags: 257, protocol: 3, alg: 8,
631			pubkey: base64::decode("AwEAAaz/tAm8yTn4Mfeh5eyI96WSVexTBAvkMgJzkKTOiW1vkIbzxeF3+/4RgWOq7HrxRixHlFlExOLAJr5emLvN7SWXgnLh4+B5xQlNVz8Og8kvArMtNROxVQuCaSnIDdD5LKyWbRd2n9WGe2R8PzgCmr3EgVLrjyBxWezF0jLHwVN8efS3rCj/EWgvIWgb9tarpVUDK/b58Da+sqqls3eNbuv7pr+eoZG+SrDK6nWeL3c6H5Apxz7LjVc1uTIdsIXxuOLYA4/ilBmSVIzuDWfdRUfhHdY6+cn8HFRm+2hM8AnXGXws9555KrUB5qihylGa8subX2Nn6UwNR1AkUTV74bU=").unwrap(),
632		}, DnsKey {
633			name: ".".try_into().unwrap(), flags: 257, protocol: 3, alg: 8,
634			pubkey: base64::decode("AwEAAa96jeuknZlaeSrvyAJj6ZHv28hhOKkx3rLGXVaC6rXTsDc449/cidltpkyGwCJNnOAlFNKF2jBosZBU5eeHspaQWOmOElZsjICMQMC3aeHbGiShvZsx4wMYSjH8e7Vrhbu6irwCzVBApESjbUdpWWmEnhathWu1jo+siFUiRAAxm9qyJNg/wOZqqzL/dL/q8PkcRU5oUKEpUge71M3ej2/7CPqpdVwuMoTvoB+ZOT4YeGyxMvHmbrxlFzGOHOijtzN+u1TQNatX2XBuzZNQ1K+s2CXkPIZo7s6JgZyvaBevYtxPvYLw4z9mR7K2vaF18UYH9Z9GNUUeayffKC73PYc=").unwrap(),
635		}];
636		let dnskey_rrsig = RRSig {
637			name: ".".try_into().unwrap(), ty: DnsKey::TYPE, alg: 8, labels: 0, orig_ttl: 172800,
638			expiration: 1787270400, inception: 1785456000, key_tag: 20326, key_name: ".".try_into().unwrap(),
639			signature: base64::decode("kKpaPEyKDhNNs9rizpyOEcK2Nwg04lOQEeUgL175fR1W7zTzCo96Lpf5xuMeN9vZKHq362Yi6pX11BeCL5JejRJ0q4OflNu2jkjZpqW4JWNeim0gaJxd2fg8MMh7qg9gZnMfFLzK8y2Hudve+JF3LD/MjIPpdPd/GEISqynmOLYxNmol2eKMdiDJ3RNxVTfzmEo8QqXGGDEozWf5pM9XvTa22LLJ37NHblwT0eeHnhxKwt5q4RH7YabmC2NlUge5R/GoCQD0WJdmi139O7DyyRDa1YEHSPigdLDRjXuAmw1acBrLbmvS7F9j1Zbc2Mm1MO924z6z9L3GXPlaSV1NJw==").unwrap(),
640		};
641		let root_hints = root_hints();
642		verify_dnskeys([&dnskey_rrsig], &root_hints, dnskeys.iter().collect()).unwrap();
643		let rrs = vec![dnskeys[0].clone().into(), dnskeys[1].clone().into(), dnskeys[2].clone().into(), dnskey_rrsig.into()];
644		(dnskeys, rrs)
645	}
646
647	fn com_dnskey() -> (Vec<DnsKey>, Vec<RR>) {
648		let root_dnskeys = root_dnskey().0;
649		let mut com_ds = vec![DS {
650			name: "com.".try_into().unwrap(), key_tag: 19718, alg: 13, digest_type: 2,
651			digest: Vec::from_hex("8ACBB0CD28F41250A80A491389424D341522D946B0DA0C0291F2D3D771D7805A").unwrap(),
652		}];
653		let ds_rrsig = RRSig {
654			name: "com.".try_into().unwrap(), ty: DS::TYPE, alg: 8, labels: 1, orig_ttl: 86400,
655			expiration: 1787115600, inception: 1785988800, key_tag: 57780, key_name: ".".try_into().unwrap(),
656			signature: base64::decode("n8/OgzlB4FN+3Qmdv+U7tVrHXIRumf2fqRXun1TQW4GUwQr7RMFLiYuymzog8chIA+7RsIkTMTNmV2FezFqGWtl2/Kp1PSTtdEKgziLDtXz2em0KZmvoOdm/uH+37wRtgK91HdVXpKKhESp4zGYZKBQP1oMZQ4hsCHndZrIh7sHgmp7b7lypbqrPFSlDm71VZsYpEXKF+3nD3CUD3S0spCHkS5StcXNfxZGDyr/UBX3/gYWPyX9VU3zcHfShOckX+QLIKqtbbXzM8/LjMwZHnZdehv156JQzOJRoGGrhDlKZ6L5C5PPXVRi6JthP+bwGRFWeJ9/yyWbH6o/RtUJdLg==").unwrap(),
657		};
658		verify_rrsig(&ds_rrsig, &root_dnskeys, com_ds.iter().collect()).unwrap();
659		let dnskeys = vec![DnsKey {
660			name: "com.".try_into().unwrap(), flags: 256, protocol: 3, alg: 13,
661			pubkey: base64::decode("o6onp+t66olg3PFhuIXaaatogT3OrqHrqt48IkYGuxwW8tSnQVMGyO+TSmp8sTRtkD9km+N/VQvvtqVM8ra9/Q==").unwrap(),
662		}, DnsKey {
663			name: "com.".try_into().unwrap(), flags: 257, protocol: 3, alg: 13,
664			pubkey: base64::decode("tx8EZRAd2+K/DJRV0S+hbBzaRPS/G6JVNBitHzqpsGlz8huE61Ms9ANe6NSDLKJtiTBqfTJWDAywEp1FCsEINQ==").unwrap(),
665		}];
666		let dnskey_rrsig = RRSig {
667			name: "com.".try_into().unwrap(), ty: DnsKey::TYPE, alg: 13, labels: 1, orig_ttl: 86400,
668			expiration: 1787234555, inception: 1785938255, key_tag: 19718, key_name: "com.".try_into().unwrap(),
669			signature: base64::decode("+mh5ldazL68oULmaud6VQrHXYpcjbUZ8sJLBlc8HWf4jaB7TXCSsnUCQhxpZ9wtemVxj7TptSGsAhHXEvrgpvw==").unwrap(),
670		};
671		verify_dnskeys([&dnskey_rrsig], &com_ds, dnskeys.iter().collect()).unwrap();
672		let rrs = vec![com_ds.pop().unwrap().into(), ds_rrsig.into(),
673			dnskeys[0].clone().into(), dnskeys[1].clone().into(), dnskey_rrsig.into()];
674		(dnskeys, rrs)
675	}
676
677	fn ninja_dnskey() -> (Vec<DnsKey>, Vec<RR>) {
678		let root_dnskeys = root_dnskey().0;
679		let mut ninja_ds = vec![DS {
680			name: "ninja.".try_into().unwrap(), key_tag: 46082, alg: 8, digest_type: 2,
681			digest: Vec::from_hex("C8F816A7A575BDB2F997F682AAB2653BA2CB5EDDB69B036A30742A33BEFAF141").unwrap(),
682		}];
683		let ds_rrsig = RRSig {
684			name: "ninja.".try_into().unwrap(), ty: DS::TYPE, alg: 8, labels: 1, orig_ttl: 86400,
685			expiration: 1787115600, inception: 1785988800, key_tag: 57780, key_name: ".".try_into().unwrap(),
686			signature: base64::decode("tCn+vWmRGtTe0aAIajh5+u0FDgs4u7+RLIkXyzjoE5RQ1wm9yO2pQDaGAvXZFI/gpu1AI0JRDHg8Wt5Pwcu6ng7UrhhrPEvATfTbPqRXVBix7GGbm7r3RjgNZriSBSJpP9ehiI56Ay/32TN1ejS5LfcLq0kCZX1MTh4vEhjy0vSzaf7f3QiyhUcTeg+1x+o2kVCKbvtsrTWhJXpt0Y5KGN/VaUUlMw80XTK42heYBz0U4OBZVhiYDem9uBozH2q0/8FxbAMqhLUuMcZNq1cSQ23LeuzbpvpjIul4dI3R0p2kCCIZx/weNGjVbsPNBt0FwVqeBCKaD7lUaMAiKBGqpQ==").unwrap(),
687		};
688		verify_rrsig(&ds_rrsig, &root_dnskeys, ninja_ds.iter().collect()).unwrap();
689		let dnskeys = vec![DnsKey {
690			name: "ninja.".try_into().unwrap(), flags: 256, protocol: 3, alg: 8,
691			pubkey: base64::decode("AwEAAcEhG/hFkNTJK5wLP7a1xXzT076x5qG24PPISw5r+JSD9mS0mcaZX+87tmOmuiKPvxAZj14nnMjcHA4J/n2RTJkofPZ+SLASOxg/mngB2trNdFEUhmEbisqE7UOuPQD55C6jee4tJ9Xs0thHEXlN3gO6MJj0+jk17skLJuH4P+td").unwrap(),
692		}, DnsKey {
693			name: "ninja.".try_into().unwrap(), flags: 256, protocol: 3, alg: 8,
694			pubkey: base64::decode("AwEAAc2FxKHX6CerPDnSAS8XKiERe1LU4K6Du1QW8WpR3c7D/6LWLnNQpNbRTZl/NIoAcoQ9sOLl2zvOMUR7KCBpC7DYvmG+Nh8dzwYWlI+rIZj9UUyy2f+9QB6gVMPeqF49vTzibNxeZf7ftiTHKi3ubcMYZdTA4JOdlX6hUL3w7+69").unwrap(),
695		}, DnsKey {
696			name: "ninja.".try_into().unwrap(), flags: 257, protocol: 3, alg: 8,
697			pubkey: base64::decode("AwEAAcceTJ3Ekkmiez70L8uNVrTDrHZxXHrQHEHQ1DJZDRXDxizuSy0prDXy1yybMqcKAkPL0IruvJ9vHg5j2eHN/hM8RVqCQ1wHgLdQASyUL37VtmLuyNmuiFpYmT+njXVh/tzRHZ4cFxrLAtACWDe6YaPApnVkJ0FEcMnKCQaymBaLX02WQOYuG3XdBr5mQQTtMs/kR/oh83QBcSxyCg3KS7G8IPP6MQPK0za94gsW9zlI5rgN2gpSjbU2qViGjDhw7N3PsC37PLTSLirUmkufeMkP9sfhDjAbP7Nv6FmpTDAIRmBmV0HBT/YNBTUBP89DmEDsrYL8knjkrOaLqV5wgkk=").unwrap(),
698		}];
699		let dnskey_rrsig = RRSig {
700			name: "ninja.".try_into().unwrap(), ty: DnsKey::TYPE, alg: 8, labels: 1, orig_ttl: 3600,
701			expiration: 1787414103, inception: 1785596103, key_tag: 46082, key_name: "ninja.".try_into().unwrap(),
702			signature: base64::decode("gx365BoyGgZpy1zqP9Ih8MAx0/SG0e5v1OR7Z7ecXONuufBne0ksny8IoMY7fxEj1Fug/7CJwJMqN1AICPVgK45u9wAPGMlIp552cTaQEw+YIoeOpYaIt5XbQ7xkbVTjoXNkMJQUam1cQrodQzvd5U6AlfoTAPAgWYzVXnbGFOeOjTNqmetf3hz13qDX5zNhG4TJOdOvdLplYXEpGZR+2CMRZTBguuwXEVTWVo9+Cyet+aKNQN0vx9aRanY6PMqbXgNxDFtFlBkqAdaESI2/6gz4QkDDZbG2R357wNqHM82FQdAu2FBSqH4NkhP0OBV6xTJ2EQrmwVsAz7R7rJOk3A==").unwrap(),
703		};
704		verify_dnskeys([&dnskey_rrsig], &ninja_ds, dnskeys.iter().collect()).unwrap();
705		let rrs = vec![ninja_ds.pop().unwrap().into(), ds_rrsig.into(),
706			dnskeys[0].clone().into(), dnskeys[1].clone().into(), dnskeys[2].clone().into(),
707			dnskey_rrsig.into()];
708		(dnskeys, rrs)
709	}
710
711	fn mattcorallo_dnskey() -> (Vec<DnsKey>, Vec<RR>) {
712		let com_dnskeys = com_dnskey().0;
713		let mattcorallo_ds = vec![DS {
714			name: "mattcorallo.com.".try_into().unwrap(), key_tag: 9033, alg: 13, digest_type: 2,
715			digest: Vec::from_hex("282511C1378832188575A172F29A89C09AC28C826FC4FE78534D4C6DF5EED2F0").unwrap(),
716		}, DS {
717			name: "mattcorallo.com.".try_into().unwrap(), key_tag: 58101, alg: 13, digest_type: 2,
718			digest: Vec::from_hex("F0E161567D468087FF27B051ABC94476178A7CB635DA1AA705E05C77CA81DE52").unwrap(),
719		}];
720		let ds_rrsig = RRSig {
721			name: "mattcorallo.com.".try_into().unwrap(), ty: DS::TYPE, alg: 13, labels: 2, orig_ttl: 86400,
722			expiration: 1786415920, inception: 1785806920, key_tag: 41446, key_name: "com.".try_into().unwrap(),
723			signature: base64::decode("e0j29NIzuHAvtSKf04LKShm2vVOiwkKlluoXyTsq9yB+prqyJ/RTU4Na/ZBHuH0ygnQUET6C5SEaQuM5gUd9+w==").unwrap(),
724		};
725		verify_rrsig(&ds_rrsig, &com_dnskeys, mattcorallo_ds.iter().collect()).unwrap();
726		let dnskeys = vec![DnsKey {
727			name: "mattcorallo.com.".try_into().unwrap(), flags: 256, protocol: 3, alg: 13,
728			pubkey: base64::decode("eEAgU/iS8VR7ubg5lArqTACdBHxK8ERx5TpTWCw9wc25pe2JiN0/iN3QgfmOBs6AUpVu+iF36abdUdct/TRLjQ==").unwrap(),
729		}, DnsKey {
730			name: "mattcorallo.com.".try_into().unwrap(), flags: 256, protocol: 3, alg: 13,
731			pubkey: base64::decode("yPAeYPanlAxAHZ9rb7LAqP2LrTZYVhECybfwXqn84b1kvhtBCS22I++mTIcYd681BKwv6WazOi03h8se5mK/KA==").unwrap(),
732		}, DnsKey {
733			name: "mattcorallo.com.".try_into().unwrap(), flags: 257, protocol: 3, alg: 13,
734			pubkey: base64::decode("yN2riWFvCTElBchzLyt0U1RjCcXW+evRcq7Ap5EU6gOacleOXft49H2oQDcRqK6C/dJDPbZ52EB5C1WlIYDY4Q==").unwrap(),
735		}];
736		let dnskey_rrsig = RRSig {
737			name: "mattcorallo.com.".try_into().unwrap(), ty: DnsKey::TYPE, alg: 13, labels: 2, orig_ttl: 604800,
738			expiration: 1787071850, inception: 1785856850, key_tag: 9033, key_name: "mattcorallo.com.".try_into().unwrap(),
739			signature: base64::decode("njvuObzGh/mdUjX5mmJKI+hw7ByPlKe6OKI5m/zhQloLXEg9DUxgS2ShV+twWZuuun5x7RXs1FzEIHxf8W6ikQ==").unwrap(),
740		};
741		verify_dnskeys([&dnskey_rrsig], &mattcorallo_ds, dnskeys.iter().collect()).unwrap();
742		let rrs = vec![mattcorallo_ds[0].clone().into(), mattcorallo_ds[1].clone().into(),
743			ds_rrsig.into(),
744			dnskeys[0].clone().into(), dnskeys[1].clone().into(), dnskeys[2].clone().into(),
745			dnskey_rrsig.into()];
746		(dnskeys, rrs)
747	}
748
749	fn mattcorallo_txt_record() -> (Vec<Txt>, RRSig) {
750		let txts = vec![Txt {
751			name: "matt.user._bitcoin-payment.mattcorallo.com.".try_into().unwrap(),
752			data: "as long as it doesn't start with bitcoin:, other records should be ignored".try_into().unwrap(),
753		}, Txt {
754			name: "matt.user._bitcoin-payment.mattcorallo.com.".try_into().unwrap(),
755			data: "bitcoin:bc1qztwy6xen3zdtt7z0vrgapmjtfz8acjkfp5fp7l?lno=lno1zr5qyugqgskrk70kqmuq7v3dnr2fnmhukps9n8hut48vkqpqnskt2svsqwjakp7k6pyhtkuxw7y2kqmsxlwruhzqv0zsnhh9q3t9xhx39suc6qsr07ekm5esdyum0w66mnx8vdquwvp7dp5jp7j3v5cp6aj0w329fnkqqv60q96sz5nkrc5r95qffx002q53tqdk8x9m2tmt85jtpmcycvfnrpx3lr45h2g7na3sec7xguctfzzcm8jjqtj5ya27te60j03vpt0vq9tm2n9yxl2hngfnmygesa25s4u4zlxewqpvp94xt7rur4rhxunwkthk9vly3lm5hh0pqv4aymcqejlgssnlpzwlggykkajp7yjs5jvr2agkyypcdlj280cy46jpynsezrcj2kwa2lyr8xvd6lfkph4xrxtk2xc3lpq".try_into().unwrap(),
756		}];
757		let txt_rrsig = RRSig {
758			name: "matt.user._bitcoin-payment.mattcorallo.com.".try_into().unwrap(),
759			ty: Txt::TYPE, alg: 13, labels: 5, orig_ttl: 3600, expiration: 1787055757,
760			inception: 1785840757, key_tag: 41141, key_name: "mattcorallo.com.".try_into().unwrap(),
761			signature: base64::decode("u1Sl6uNJvSZcxoEsew6/MM7GI8smdPkuDSS26t8tEiFece6d8ezI6wcalSbkLKObXeUmdbW5lHZjnBKkUck3MA==").unwrap(),
762		};
763		(txts, txt_rrsig)
764	}
765
766	fn cloudflare_dnskey() -> (Vec<DnsKey>, Vec<RR>) {
767		let com_dnskeys = com_dnskey().0;
768		let mut cloudflare_ds = vec![DS {
769			name: "cloudflare.com.".try_into().unwrap(), key_tag: 2371, alg: 13, digest_type: 2,
770			digest: Vec::from_hex("32996839A6D808AFE3EB4A795A0E6A7A39A76FC52FF228B22B76F6D63826F2B9").unwrap(),
771		}];
772		let ds_rrsig = RRSig {
773			name: "cloudflare.com.".try_into().unwrap(), ty: DS::TYPE, alg: 13, labels: 2, orig_ttl: 86400,
774			expiration: 1786582833, inception: 1785973833, key_tag: 41446, key_name: "com.".try_into().unwrap(),
775			signature: base64::decode("bj58c8zJI4N/l6M0CzR5/ANQTZBlUAzuxeBcz1Nt4M2B2UPM3LoYXQ8ASYZli5caWIkVq2B2kHs0N3dxmu2sUg==").unwrap(),
776		};
777		verify_rrsig(&ds_rrsig, &com_dnskeys, cloudflare_ds.iter().collect()).unwrap();
778		let dnskeys = vec![DnsKey {
779			name: "cloudflare.com.".try_into().unwrap(), flags: 256, protocol: 3, alg: 13,
780			pubkey: base64::decode("oJMRESz5E4gYzS/q6XDrvU1qMPYIjCWzJaOau8XNEZeqCYKD5ar0IRd8KqXXFJkqmVfRvMGPmM1x8fGAa2XhSA==").unwrap(),
781		}, DnsKey {
782			name: "cloudflare.com.".try_into().unwrap(), flags: 257, protocol: 3, alg: 13,
783			pubkey: base64::decode("mdsswUyr3DPW132mOi8V9xESWE8jTo0dxCjjnopKl+GqJxpVXckHAeF+KkxLbxILfDLUT0rAK9iUzy1L53eKGQ==").unwrap(),
784		}];
785		let dnskey_rrsig = RRSig {
786			name: "cloudflare.com.".try_into().unwrap(), ty: DnsKey::TYPE, alg: 13, labels: 2, orig_ttl: 3600,
787			expiration: 1790916935, inception: 1785646535, key_tag: 2371, key_name: "cloudflare.com.".try_into().unwrap(),
788			signature: base64::decode("14eL8GoAJFkezWYWQr6pcVSq55HRaaykMGstMqOW1bHXDb3Ll0gYj79po1sBCvqUAFEYXZmIBVcYfSytQ63MPw==").unwrap(),
789		};
790		verify_dnskeys([&dnskey_rrsig], &cloudflare_ds, dnskeys.iter().collect()).unwrap();
791		let rrs = vec![cloudflare_ds.pop().unwrap().into(), ds_rrsig.into(),
792			dnskeys[0].clone().into(), dnskeys[1].clone().into(), dnskey_rrsig.into()];
793		(dnskeys, rrs)
794	}
795
796	fn cloudflare_online_signed_nsec() -> (NSec, RRSig) {
797		let nsec = NSec {
798			name: "online_signing_test.cloudflare.com.".try_into().unwrap(),
799			next_name: b"\x00.online_signing_test.cloudflare.com.".to_vec(),
800			types: NSecTypeMask::from_types(&[RRSig::TYPE, NSec::TYPE, 128]),
801		};
802		let nsec_rrsig = RRSig {
803			name: "online_signing_test.cloudflare.com.".try_into().unwrap(),
804			ty: NSec::TYPE, alg: 13, labels: 3, orig_ttl: 300, expiration: 1786137946,
805			inception: 1785957946, key_tag: 34505, key_name: "cloudflare.com.".try_into().unwrap(),
806			signature: base64::decode("wUIukIrWxEzb0SJOTEC7Kg7e/SbDzIIGZMWfOS7/IpToUYGuJj1Io13ufs5shP4MnfAAWxrfFLa6asyurF0OFg==").unwrap(),
807		};
808		(nsec, nsec_rrsig)
809	}
810
811	fn bitcoin_ninja_dnskey() -> (Vec<DnsKey>, Vec<RR>) {
812		let ninja_dnskeys = ninja_dnskey().0;
813		let bitcoin_ninja_ds = vec![DS {
814			name: "bitcoin.ninja.".try_into().unwrap(), key_tag: 29036, alg: 13, digest_type: 2,
815			digest: Vec::from_hex("3F7AD5A303E9C1CD1474B8DF2AE56F3F82DA8637CA55DB4D9A2BB85960CA698E").unwrap(),
816		}, DS {
817			name: "bitcoin.ninja.".try_into().unwrap(), key_tag: 30142, alg: 13, digest_type: 2,
818			digest: Vec::from_hex("FB445ADFE3314DAAE4884B53592BED42DF1DD0B147B5930E8B16AF2AEAE94504").unwrap(),
819		}];
820		let ds_rrsig = RRSig {
821			name: "bitcoin.ninja.".try_into().unwrap(), ty: DS::TYPE, alg: 8, labels: 2, orig_ttl: 3600,
822			expiration: 1787414103, inception: 1785596103, key_tag: 19731, key_name: "ninja.".try_into().unwrap(),
823			signature: base64::decode("n1wGkPRW8dbBvCD55EAMHR4WaZAxcXJW9Se8xUFr/D3qtJt6ydH5X/+9TqQMvgU/LYcWc0FeIKun5pTLzbDOkufEYfbE3tu+diI9y53zw6qjj0i2NRCj4BdyQletGVDqFfBKQIMs8lrwa+8eqm0lut30iOvu8FyS5RpyISwMkLw=").unwrap(),
824		};
825		verify_rrsig(&ds_rrsig, &ninja_dnskeys, bitcoin_ninja_ds.iter().collect()).unwrap();
826		let dnskeys = vec![DnsKey {
827			name: "bitcoin.ninja.".try_into().unwrap(), flags: 256, protocol: 3, alg: 13,
828			pubkey: base64::decode("jQGaU+vc1XEIa4507tYhI7tVsKUp+Bd3zdtNMVVxIWSW0QJlJvsXBLnsw+vLHL9HYtYN68ECo1eInYfYk1fILg==").unwrap(),
829		}, DnsKey {
830			name: "bitcoin.ninja.".try_into().unwrap(), flags: 257, protocol: 3, alg: 13,
831			pubkey: base64::decode("T0XjCLwz4dUK2iOKsFAo3j2CW0nEumfvpA5w6gKAAujcD11ewT/6+aRIejB64LG2HGw6J14wiRvxq7uREj972g==").unwrap(),
832		}];
833		let dnskey_rrsig = RRSig {
834			name: "bitcoin.ninja.".try_into().unwrap(), ty: DnsKey::TYPE, alg: 13, labels: 2, orig_ttl: 604800,
835			expiration: 1786846836, inception: 1785631836, key_tag: 30142, key_name: "bitcoin.ninja.".try_into().unwrap(),
836			signature: base64::decode("3jrUWhXokJjCCqhUbE6pac2cYHhW+gjZToZh7MaEWz+avKFHh8gC4BleAcrLUCAspemmNbGPU2ZMhW6L9w4o0w==").unwrap(),
837		};
838		verify_dnskeys([&dnskey_rrsig], &bitcoin_ninja_ds, dnskeys.iter().collect()).unwrap();
839		let rrs = vec![bitcoin_ninja_ds[0].clone().into(), bitcoin_ninja_ds[1].clone().into(),
840			ds_rrsig.into(),
841			dnskeys[0].clone().into(), dnskeys[1].clone().into(), dnskey_rrsig.into()];
842		(dnskeys, rrs)
843	}
844
845	fn bitcoin_ninja_txt_record() -> (Txt, RRSig) {
846		let txt_resp = Txt {
847			name: "txt_test.dnssec_proof_tests.bitcoin.ninja.".try_into().unwrap(),
848			data: "dnssec_prover_test".try_into().unwrap(),
849		};
850		let txt_rrsig = RRSig {
851			name: "txt_test.dnssec_proof_tests.bitcoin.ninja.".try_into().unwrap(),
852			ty: Txt::TYPE, alg: 13, labels: 4, orig_ttl: 30, expiration: 1787178279,
853			inception: 1785963279, key_tag: 62306, key_name: "bitcoin.ninja.".try_into().unwrap(),
854			signature: base64::decode("MtmxM8RekiyS716n4Oz4dmiO32VfHtdLoWVwMPD7o7hwAo3TZoFSi1QOmHEFsrn/wtjghuZTBz/IwVY/3Os1CQ==").unwrap(),
855		};
856		(txt_resp, txt_rrsig)
857	}
858
859	fn bitcoin_ninja_cname_record() -> (CName, RRSig) {
860		let cname_resp = CName {
861			name: "cname_test.dnssec_proof_tests.bitcoin.ninja.".try_into().unwrap(),
862			canonical_name: "txt_test.dnssec_proof_tests.bitcoin.ninja.".try_into().unwrap(),
863		};
864		let cname_rrsig = RRSig {
865			name: "cname_test.dnssec_proof_tests.bitcoin.ninja.".try_into().unwrap(),
866			ty: CName::TYPE, alg: 13, labels: 4, orig_ttl: 30, expiration: 1787178279,
867			inception: 1785963279, key_tag: 62306, key_name: "bitcoin.ninja.".try_into().unwrap(),
868			signature: base64::decode("67U3kvg35HyzESYD4oQ6NtIklzuMUDGkmrP+Bu0Yt0ZWJ249a1+pOMD3IiltgV+kidT6e/17d1HUM7zIKROAJA==").unwrap(),
869		};
870		(cname_resp, cname_rrsig)
871	}
872
873	fn bitcoin_ninja_dname_record() -> (DName, RRSig) {
874		let dname = DName {
875			name: "dname_test.dnssec_proof_tests.bitcoin.ninja.".try_into().unwrap(),
876			delegation_name: "cname_wildcard_test.dnssec_proof_tests.bitcoin.ninja.".try_into().unwrap(),
877		};
878		let dname_rrsig = RRSig {
879			name: "dname_test.dnssec_proof_tests.bitcoin.ninja.".try_into().unwrap(),
880			ty: DName::TYPE, alg: 13, labels: 4, orig_ttl: 30, expiration: 1787178279,
881			inception: 1785963279, key_tag: 62306, key_name: "bitcoin.ninja.".try_into().unwrap(),
882			signature: base64::decode("vxE+K/VVKWpnTPGSDYavj2WTWxCti4TV3KAhzExSaEQBCHbTh7qTptUT33mnOanYtcRV5X46IQRNqZUlHRrDtw==").unwrap(),
883		};
884		(dname, dname_rrsig)
885	}
886
887	fn bitcoin_ninja_dname_sibling_records() -> (Txt, RRSig, Txt, RRSig) {
888		let sibling = Txt {
889			name: "notdname_test.dnssec_proof_tests.bitcoin.ninja.".try_into().unwrap(),
890			data: "not_dnamed".try_into().unwrap(),
891		};
892		let sibling_rrsig = RRSig {
893			name: "notdname_test.dnssec_proof_tests.bitcoin.ninja.".try_into().unwrap(),
894			ty: Txt::TYPE, alg: 13, labels: 4, orig_ttl: 30, expiration: 1787258898,
895			inception: 1786043898, key_tag: 62306, key_name: "bitcoin.ninja.".try_into().unwrap(),
896			signature: base64::decode("CTnx0YktprRMA14f434Zwyu9NBYxti3NOfIcv5cPCnJxQvU7UmOOuBMtKpnSvvJBKpsbhXfnZCZFazG1Z6fdow==").unwrap(),
897		};
898		let redirected = Txt {
899			name: "notcname_wildcard_test.dnssec_proof_tests.bitcoin.ninja.".try_into().unwrap(),
900			data: "wrongly_dnamed".try_into().unwrap(),
901		};
902		let redirected_rrsig = RRSig {
903			name: "notcname_wildcard_test.dnssec_proof_tests.bitcoin.ninja.".try_into().unwrap(),
904			ty: Txt::TYPE, alg: 13, labels: 4, orig_ttl: 30, expiration: 1787258898,
905			inception: 1786043898, key_tag: 62306, key_name: "bitcoin.ninja.".try_into().unwrap(),
906			signature: base64::decode("uwLsAhkqQHwafsp+mt5X0cwbNXEuIM0zCT+qEJaC65TxS5FNi9PFOS8zo730q70W53OMvDBSkw6xKdf4/mEsSg==").unwrap(),
907		};
908		(sibling, sibling_rrsig, redirected, redirected_rrsig)
909	}
910
911	fn bitcoin_ninja_txt_sort_edge_cases_records() -> (Vec<Txt>, RRSig) {
912		let txts = vec![Txt {
913			name: "txt_sort_order.dnssec_proof_tests.bitcoin.ninja.".try_into().unwrap(),
914			data: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa".try_into().unwrap(),
915		}, Txt {
916			name: "txt_sort_order.dnssec_proof_tests.bitcoin.ninja.".try_into().unwrap(),
917			data: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa".try_into().unwrap(),
918		}, Txt {
919			name: "txt_sort_order.dnssec_proof_tests.bitcoin.ninja.".try_into().unwrap(),
920			data: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaab".try_into().unwrap(),
921		}, Txt {
922			name: "txt_sort_order.dnssec_proof_tests.bitcoin.ninja.".try_into().unwrap(),
923			data: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa".try_into().unwrap(),
924		}, Txt {
925			name: "txt_sort_order.dnssec_proof_tests.bitcoin.ninja.".try_into().unwrap(),
926			data: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaba".try_into().unwrap(),
927		}, Txt {
928			name: "txt_sort_order.dnssec_proof_tests.bitcoin.ninja.".try_into().unwrap(),
929			data: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaab".try_into().unwrap(),
930		}, Txt {
931			name: "txt_sort_order.dnssec_proof_tests.bitcoin.ninja.".try_into().unwrap(),
932			data: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaba".try_into().unwrap(),
933		}, Txt {
934			name: "txt_sort_order.dnssec_proof_tests.bitcoin.ninja.".try_into().unwrap(),
935			data: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaa".try_into().unwrap(),
936		}];
937		let rrsig = RRSig {
938			name: "txt_sort_order.dnssec_proof_tests.bitcoin.ninja.".try_into().unwrap(),
939			ty: Txt::TYPE, alg: 13, labels: 4, orig_ttl: 30, expiration: 1787178279,
940			inception: 1785963279, key_tag: 62306, key_name: "bitcoin.ninja.".try_into().unwrap(),
941			signature: base64::decode("EllHDC5WHigdu+L4HfKRN3wpXz+lfMLJim+qrIE8DTMvrkiNUFHHLnuayOihrl13MJ5FQPWsX3HgPCKfThvYjg==").unwrap(),
942		};
943		(txts, rrsig)
944	}
945
946	/// Note that the NSEC3 proof here is for asdf., any other prefix may fail NSEC checks.
947	fn bitcoin_ninja_wildcard_record(pfx: &str) -> (Txt, RRSig, NSec3, RRSig) {
948		let name: Name = (pfx.to_owned() + ".wildcard_test.dnssec_proof_tests.bitcoin.ninja.").try_into().unwrap();
949		let txt_resp = Txt {
950			name: name.clone(),
951			data: "wildcard_test".try_into().unwrap(),
952		};
953		let txt_rrsig = RRSig {
954			name: name.clone(),
955			ty: Txt::TYPE, alg: 13, labels: 4, orig_ttl: 30, expiration: 1787178279,
956			inception: 1785963279, key_tag: 62306, key_name: "bitcoin.ninja.".try_into().unwrap(),
957			signature: base64::decode("4oXWLvyfOvNunXWIWNEDDTjSKV6N7o8l4DAaZYT9xz0cqfaT2qA0iQ3JeIqwBsKGWVejPHlNhBvdd71Mfo1ZUw==").unwrap(),
958		};
959		let nsec3 = NSec3 {
960			name: "gtcdha57vlrv1q1qrjf7vujvlfajtasp.bitcoin.ninja.".try_into().unwrap(),
961			hash_algo: 1, flags: 0, hash_iterations: 0, salt: Vec::from_hex("057B0C54D4647530").unwrap(),
962			next_name_hash: Vec::from_hex("958BD48D5AED2B834731866E15F2751AE7A920C9").unwrap(),
963			types: NSecTypeMask::from_types(&[16, 46]),
964		};
965		let nsec3_rrsig = RRSig {
966			name: "gtcdha57vlrv1q1qrjf7vujvlfajtasp.bitcoin.ninja.".try_into().unwrap(),
967			ty: NSec3::TYPE, alg: 13, labels: 3, orig_ttl: 60, expiration: 1787229760,
968			inception: 1786014760, key_tag: 62306, key_name: "bitcoin.ninja.".try_into().unwrap(),
969			signature: base64::decode("trwirnNf4Z4MKuUGQhD6ylyGF7zeOJdPuJeogKJ0bwH7dr9qcAQy9ZhaZI5TwdDhd2g85JvJ8hLGCjO5LzNCMQ==").unwrap(),
970		};
971		(txt_resp, txt_rrsig, nsec3, nsec3_rrsig)
972	}
973
974	fn bitcoin_ninja_cname_wildcard_record() -> (CName, RRSig, Txt, RRSig, [(NSec3, RRSig); 2]) {
975		let cname_resp = CName {
976			name: "asdf.cname_wildcard_test.dnssec_proof_tests.bitcoin.ninja.".try_into().unwrap(),
977			canonical_name: "cname.wildcard_test.dnssec_proof_tests.bitcoin.ninja.".try_into().unwrap(),
978		};
979		let cname_rrsig = RRSig {
980			name: "asdf.cname_wildcard_test.dnssec_proof_tests.bitcoin.ninja.".try_into().unwrap(),
981			ty: CName::TYPE, alg: 13, labels: 4, orig_ttl: 30, expiration: 1787178279,
982			inception: 1785963279, key_tag: 62306, key_name: "bitcoin.ninja.".try_into().unwrap(),
983			signature: base64::decode("zZ/uy4AS7tPB1mjW0qAiRe7eqaI54VOt9MIeaQcjL+xls+OBK9Agtp2t9Z0bmyr5/7wo4l2uIKTFEYQyV4XSRg==").unwrap(),
984		};
985		let nsec3_a = NSec3 {
986			name: "g1ae0emluagb7uci39ts11cgoh4sk2l0.bitcoin.ninja.".try_into().unwrap(),
987			hash_algo: 1, flags: 0, hash_iterations: 0,
988			salt: Vec::from_hex("057B0C54D4647530").unwrap(),
989			next_name_hash: Vec::from_hex("832AF5BE3608C56209E4B83FD341299A4269854C").unwrap(),
990			types: NSecTypeMask::from_types(&[1, 28, 46]),
991		};
992		let nsec3_a_rrsig = RRSig {
993			name: "g1ae0emluagb7uci39ts11cgoh4sk2l0.bitcoin.ninja.".try_into().unwrap(),
994			ty: NSec3::TYPE, alg: 13, labels: 3, orig_ttl: 60, expiration: 1787208167,
995			inception: 1785993167, key_tag: 62306, key_name: "bitcoin.ninja.".try_into().unwrap(),
996			signature: base64::decode("sha0PpAzWuFHHXRa2umvNGYuZMy1JaLiDevBJyJmpsyLoskGBA5aLCvhXiQlT913pAPvhXzeXO7JkdPTTpjLag==").unwrap(),
997		};
998		let (txt_resp, txt_rrsig, nsec3_b, nsec3_b_rrsig) = bitcoin_ninja_wildcard_record("asdf");
999		(cname_resp, cname_rrsig, txt_resp, txt_rrsig,
1000			[(nsec3_a, nsec3_a_rrsig), (nsec3_b, nsec3_b_rrsig)])
1001	}
1002
1003	fn bitcoin_ninja_x_domain_cname_wildcard_record() -> (CName, RRSig, NSec3, RRSig) {
1004		let name = "wildcard.x_domain_cname_wild.dnssec_proof_tests.bitcoin.ninja.";
1005		let cname = CName {
1006			name: name.try_into().unwrap(),
1007			canonical_name: "matt.user._bitcoin-payment.mattcorallo.com.".try_into().unwrap(),
1008		};
1009		let cname_rrsig = RRSig {
1010			name: name.try_into().unwrap(), ty: CName::TYPE, alg: 13, labels: 4, orig_ttl: 30,
1011			expiration: 1786663415, inception: 1785448415, key_tag: 62306,
1012			key_name: "bitcoin.ninja.".try_into().unwrap(),
1013			signature: base64::decode("HN8yF2eG5pUMWdUHBXK1PoUSTsCvJ89PrdvRIaTpOfdonxWUB4Mw093TefbGfXAWJYmLRRW4jd0B7LPIc3Jv7A==").unwrap(),
1014		};
1015		let nsec3 = NSec3 {
1016			name: "vpbup1es8cqpj16tgtgtas6fifbu8u73.bitcoin.ninja.".try_into().unwrap(),
1017			hash_algo: 1, flags: 0, hash_iterations: 0,
1018			salt: Vec::from_hex("180E9D1B9F24BAD7").unwrap(),
1019			next_name_hash: Vec::from_hex("02417EDCE26669601E9407D9B0A0E3AB1741B8E8").unwrap(),
1020			types: NSecTypeMask::from_types(&[33, RRSig::TYPE]),
1021		};
1022		let nsec3_rrsig = RRSig {
1023			name: "vpbup1es8cqpj16tgtgtas6fifbu8u73.bitcoin.ninja.".try_into().unwrap(),
1024			ty: NSec3::TYPE, alg: 13, labels: 3, orig_ttl: 60,
1025			expiration: 1786974157, inception: 1785759157, key_tag: 62306,
1026			key_name: "bitcoin.ninja.".try_into().unwrap(),
1027			signature: base64::decode("F3GPW+yCZjHy8VA6BMhlH01ZyDHf94dD1r5bTN+AWDuDw2bPsCtVbrzvS0gNenxKBtOgg9kE3+WYiZkrGhaM5A==").unwrap(),
1028		};
1029		(cname, cname_rrsig, nsec3, nsec3_rrsig)
1030	}
1031
1032	fn bitcoin_ninja_cname_target_wildcard_record() -> (Txt, RRSig, NSec3, RRSig) {
1033		let name = "cname.wildcard_test.dnssec_proof_tests.bitcoin.ninja.";
1034		let txt = Txt {
1035			name: name.try_into().unwrap(),
1036			data: "wildcard_test".try_into().unwrap(),
1037		};
1038		let txt_rrsig = RRSig {
1039			name: name.try_into().unwrap(), ty: Txt::TYPE, alg: 13, labels: 4, orig_ttl: 30,
1040			expiration: 1786663415, inception: 1785448415, key_tag: 62306,
1041			key_name: "bitcoin.ninja.".try_into().unwrap(),
1042			signature: base64::decode("NvmdgkpvkZvnAOZbW9+XwJmV8HqMqreTKsLmrtC0q5GJI/26Z7u8PG3/hjPtEd5wShFfTd1xD3FYywOwkp682Q==").unwrap(),
1043		};
1044		let nsec3 = NSec3 {
1045			name: "k0k9mke3fif9p0oq11707a2oe440utb4.bitcoin.ninja.".try_into().unwrap(),
1046			hash_algo: 1, flags: 0, hash_iterations: 0,
1047			salt: Vec::from_hex("180E9D1B9F24BAD7").unwrap(),
1048			next_name_hash: Vec::from_hex("A62CCD9A3F946729008413B647E0E191E7713009").unwrap(),
1049			types: NSecTypeMask::from_types(&[Txt::TYPE, RRSig::TYPE]),
1050		};
1051		let nsec3_rrsig = RRSig {
1052			name: "k0k9mke3fif9p0oq11707a2oe440utb4.bitcoin.ninja.".try_into().unwrap(),
1053			ty: NSec3::TYPE, alg: 13, labels: 3, orig_ttl: 60, expiration: 1786987366,
1054			inception: 1785772366, key_tag: 62306, key_name: "bitcoin.ninja.".try_into().unwrap(),
1055			signature: base64::decode("WhCMAfJ1hZ+QsOXhbbwKpBk9VhsZdfYhjUMXcQHLDolrmj3SETPPAkMiGtm2oBdPXZeWrhpPEmNFnpquEgI3Fg==").unwrap(),
1056		};
1057		(txt, txt_rrsig, nsec3, nsec3_rrsig)
1058	}
1059
1060	/// The NSEC3 matching `override.wildcard_test.dnssec_proof_tests.bitcoin.ninja.`.
1061	///
1062	/// Takes the `bitcoin.ninja.` keys so that we can check the zone really did sign this.
1063	fn bitcoin_ninja_override_matching_nsec3(dnskeys: &[&DnsKey]) -> (NSec3, RRSig) {
1064		let overridden = "override.wildcard_test.dnssec_proof_tests.bitcoin.ninja.";
1065		let nsec3 = NSec3 {
1066			name: "i35lj8ddq83urbvibgi143qtdiu5kqlp.bitcoin.ninja.".try_into().unwrap(),
1067			hash_algo: 1, flags: 0, hash_iterations: 0,
1068			salt: Vec::from_hex("180E9D1B9F24BAD7").unwrap(),
1069			next_name_hash: crate::base32::decode("JO06SMGEHDFJ0I3MNHB1HMMNATA3TPIP").unwrap(),
1070			types: NSecTypeMask::from_types(&[Txt::TYPE, RRSig::TYPE]),
1071		};
1072		let nsec3_rrsig = RRSig {
1073			name: "i35lj8ddq83urbvibgi143qtdiu5kqlp.bitcoin.ninja.".try_into().unwrap(),
1074			ty: NSec3::TYPE, alg: 13, labels: 3, orig_ttl: 60,
1075			expiration: 1786987366, inception: 1785772366, key_tag: 62306,
1076			key_name: "bitcoin.ninja.".try_into().unwrap(),
1077			signature: base64::decode("fYjOjpA3JKI1AlVmpmDemxzGxksQ2t3k2ChO0YCNVkp1Z1tvc91pb2SIBC/AZak4Do4qNl7OK9SkFa4otkU4jQ==").unwrap(),
1078		};
1079		verify_rrsig(&nsec3_rrsig, dnskeys.iter().copied(), vec![&nsec3]).unwrap();
1080
1081		let mut hasher = crypto::hash::Hasher::sha1();
1082		write_name(&mut hasher, overridden);
1083		hasher.update(&nsec3.salt);
1084		let (owner_hash_base32, _) = nsec3.name.split_once('.').unwrap();
1085		assert_eq!(&base32::decode(owner_hash_base32).unwrap()[..], hasher.finish().as_ref(),
1086			"NSEC3 no longer matches the name it is supposed to match");
1087		assert!(nsec3.types.contains_type(Txt::TYPE),
1088			"NSEC3 must list TXT as present, else it proves absence by bitmap instead");
1089
1090		(nsec3, nsec3_rrsig)
1091	}
1092
1093	fn bitcoin_ninja_nsec_dnskey() -> (Vec<DnsKey>, Vec<RR>) {
1094		let bitcoin_ninja_dnskeys = bitcoin_ninja_dnskey().0;
1095		let mut bitcoin_ninja_ds = vec![DS {
1096			name: "nsec_tests.dnssec_proof_tests.bitcoin.ninja.".try_into().unwrap(),
1097			key_tag: 8036, alg: 13, digest_type: 2,
1098			digest: Vec::from_hex("8EC0DAE4501233979196EBED206212BCCC49E40E086EC2E56558EC1F6FB62715").unwrap(),
1099		}];
1100		let ds_rrsig = RRSig {
1101			name: "nsec_tests.dnssec_proof_tests.bitcoin.ninja.".try_into().unwrap(), ty: DS::TYPE, alg: 13, labels: 4, orig_ttl: 30,
1102			expiration: 1787178279, inception: 1785963279, key_tag: 62306, key_name: "bitcoin.ninja.".try_into().unwrap(),
1103			signature: base64::decode("a+selfmR4paAGLLAhJVT9x9tkCUDnmzM6yi40A6vKwB8G6vU4VR35gVfMgJjb3weWRAuk8AHoKDf4d0fTiESZA==").unwrap(),
1104		};
1105		verify_rrsig(&ds_rrsig, &bitcoin_ninja_dnskeys, bitcoin_ninja_ds.iter().collect()).unwrap();
1106		let dnskeys = vec![DnsKey {
1107			name: "nsec_tests.dnssec_proof_tests.bitcoin.ninja.".try_into().unwrap(), flags: 256, protocol: 3, alg: 13,
1108			pubkey: base64::decode("cpJjguqPE/pALvfrer/FgTsU+Z/lqlzP0jR2uH1GJZ3XhScsAP2YLWwL+J+v0TQJNBBLxLIchjpxe7pYo0l16w==").unwrap(),
1109		}, DnsKey {
1110			name: "nsec_tests.dnssec_proof_tests.bitcoin.ninja.".try_into().unwrap(), flags: 256, protocol: 3, alg: 13,
1111			pubkey: base64::decode("j4xLO1IMaoL6fNuB8lssMVTg4CvK8GZpyf5KVCSjmSueXJPMrpAvDvMpEgcrpi5nZr0CR132Sml9BT5E1cPVPg==").unwrap(),
1112		}, DnsKey {
1113			name: "nsec_tests.dnssec_proof_tests.bitcoin.ninja.".try_into().unwrap(), flags: 257, protocol: 3, alg: 13,
1114			pubkey: base64::decode("MUnIhm31ySIr9WXIBVQc38wlSHHvYaKIOFR8WYl4O9MJBlywWeUdx16oGinCe2FjjMkUkKn9kV5zzWhGmrdIbQ==").unwrap(),
1115		}];
1116		let dnskey_rrsig = RRSig {
1117			name: "nsec_tests.dnssec_proof_tests.bitcoin.ninja.".try_into().unwrap(),
1118			ty: DnsKey::TYPE, alg: 13, labels: 4, orig_ttl: 604800, expiration: 1786993013,
1119			inception: 1785778013, key_tag: 8036, key_name: "nsec_tests.dnssec_proof_tests.bitcoin.ninja.".try_into().unwrap(),
1120			signature: base64::decode("o+ve+8i6LwCpqZN5yS9CVsJOVIkjR5baM8Nf6m6HLp5Zfjkbi2ux3PjmwhKe7k1SitH29nK59xc5+8St4pO4iw==").unwrap(),
1121		};
1122		verify_dnskeys([&dnskey_rrsig], &bitcoin_ninja_ds, dnskeys.iter().collect()).unwrap();
1123		let rrs = vec![bitcoin_ninja_ds.pop().unwrap().into(), ds_rrsig.into(),
1124			dnskeys[0].clone().into(), dnskeys[1].clone().into(), dnskeys[2].clone().into(),
1125			dnskey_rrsig.into()];
1126		(dnskeys, rrs)
1127	}
1128
1129	fn bitcoin_ninja_nsec_record() -> (Txt, RRSig) {
1130		let txt_resp = Txt {
1131			name: "a.nsec_tests.dnssec_proof_tests.bitcoin.ninja.".try_into().unwrap(),
1132			data: "txt_a".try_into().unwrap(),
1133		};
1134		let txt_rrsig = RRSig {
1135			name: "a.nsec_tests.dnssec_proof_tests.bitcoin.ninja.".try_into().unwrap(),
1136			ty: Txt::TYPE, alg: 13, labels: 5, orig_ttl: 30, expiration: 1787000213,
1137			inception: 1785785213, key_tag: 37278, key_name: "nsec_tests.dnssec_proof_tests.bitcoin.ninja.".try_into().unwrap(),
1138			signature: base64::decode("+H03kpGz5pum4HmL1EgQXq8EF1QSbWsoiWA5NFWlX+bhd3mnQFZ8WyZzvkFI/T6KPhBsoEwAECh6FWtACLFkKA==").unwrap(),
1139		};
1140		(txt_resp, txt_rrsig)
1141	}
1142
1143	fn bitcoin_ninja_nsec_wildcard_record(pfx: &str) -> (Txt, RRSig, NSec, RRSig) {
1144		let name: Name = (pfx.to_owned() + ".wildcard_test.nsec_tests.dnssec_proof_tests.bitcoin.ninja.").try_into().unwrap();
1145		let txt_resp = Txt {
1146			name: name.clone(),
1147			data: "wildcard_test".try_into().unwrap(),
1148		};
1149		let txt_rrsig = RRSig {
1150			name: name.clone(),
1151			ty: Txt::TYPE, alg: 13, labels: 5, orig_ttl: 30, expiration: 1787000213,
1152			inception: 1785785213, key_tag: 37278, key_name: "nsec_tests.dnssec_proof_tests.bitcoin.ninja.".try_into().unwrap(),
1153			signature: base64::decode("5+BP2xI1BraLWhRS6RED4Ji8SlbK0rO0OpnrDohCpwQpd8HTPzhznOXZwfptN+GGruHJ1jHDmPsvWINf9n5tUg==").unwrap(),
1154		};
1155		let nsec = NSec {
1156			name: "*.wildcard_test.nsec_tests.dnssec_proof_tests.bitcoin.ninja.".try_into().unwrap(),
1157			next_name: "override.wildcard_test.nsec_tests.dnssec_proof_tests.bitcoin.ninja.".try_into().unwrap(),
1158			types: NSecTypeMask::from_types(&[Txt::TYPE, RRSig::TYPE, NSec::TYPE]),
1159		};
1160		let nsec_rrsig = RRSig {
1161			name: "*.wildcard_test.nsec_tests.dnssec_proof_tests.bitcoin.ninja.".try_into().unwrap(),
1162			ty: NSec::TYPE, alg: 13, labels: 5, orig_ttl: 60, expiration: 1787232803,
1163			inception: 1786017803, key_tag: 37278, key_name: "nsec_tests.dnssec_proof_tests.bitcoin.ninja.".try_into().unwrap(),
1164			signature: base64::decode("eDqJ3Ly6uVa1phZPle8wChh5AYuy67TRIkeccJRUOv6uZlKcU+2CZ6UcesMjgXnjBbIlXuw0B0/hN78KT61LGQ==").unwrap(),
1165		};
1166		(txt_resp, txt_rrsig, nsec, nsec_rrsig)
1167	}
1168
1169	fn bitcoin_ninja_nsec_post_override_wildcard_record(pfx: &str) -> (Txt, RRSig, NSec, RRSig) {
1170		let name: Name = (pfx.to_owned() + ".wildcard_test.nsec_tests.dnssec_proof_tests.bitcoin.ninja.").try_into().unwrap();
1171		let txt_resp = Txt {
1172			name: name.clone(),
1173			data: "wildcard_test".try_into().unwrap(),
1174		};
1175		let txt_rrsig = RRSig {
1176			name: name.clone(),
1177			ty: Txt::TYPE, alg: 13, labels: 5, orig_ttl: 30, expiration: 1787000213,
1178			inception: 1785785213, key_tag: 37278, key_name: "nsec_tests.dnssec_proof_tests.bitcoin.ninja.".try_into().unwrap(),
1179			signature: base64::decode("5+BP2xI1BraLWhRS6RED4Ji8SlbK0rO0OpnrDohCpwQpd8HTPzhznOXZwfptN+GGruHJ1jHDmPsvWINf9n5tUg==").unwrap(),
1180		};
1181		let nsec = NSec {
1182			name: "override.wildcard_test.nsec_tests.dnssec_proof_tests.bitcoin.ninja.".try_into().unwrap(),
1183			next_name: "nested.zent_test.wildcard_test.nsec_tests.dnssec_proof_tests.bitcoin.ninja.".try_into().unwrap(),
1184			types: NSecTypeMask::from_types(&[16, 46, 47]),
1185		};
1186		let nsec_rrsig = RRSig {
1187			name: "override.wildcard_test.nsec_tests.dnssec_proof_tests.bitcoin.ninja.".try_into().unwrap(),
1188			ty: NSec::TYPE, alg: 13, labels: 6, orig_ttl: 60, expiration: 1787232803,
1189			inception: 1786017803, key_tag: 37278, key_name: "nsec_tests.dnssec_proof_tests.bitcoin.ninja.".try_into().unwrap(),
1190			signature: base64::decode("FFmyd7Bmq8hmFmZOyNMfvJkCpdU9po4ogA41xUw+nZNLvtad/CN/7p9nJTcHTvmp0UEyg5t2K9yV9KhNRlaASg==").unwrap(),
1191		};
1192		(txt_resp, txt_rrsig, nsec, nsec_rrsig)
1193	}
1194
1195	fn bitcoin_ninja_nsec_wraparound_wildcard_record(pfx: &str) -> (Txt, RRSig, NSec, RRSig) {
1196		let name: Name = (pfx.to_owned() + ".wildcard_test.nsec_tests.dnssec_proof_tests.bitcoin.ninja.").try_into().unwrap();
1197		let txt_resp = Txt {
1198			name: name.clone(),
1199			data: "wildcard_test".try_into().unwrap(),
1200		};
1201		let txt_rrsig = RRSig {
1202			name: name.clone(),
1203			ty: Txt::TYPE, alg: 13, labels: 5, orig_ttl: 30, expiration: 1787000213,
1204			inception: 1785785213, key_tag: 37278, key_name: "nsec_tests.dnssec_proof_tests.bitcoin.ninja.".try_into().unwrap(),
1205			signature: base64::decode("5+BP2xI1BraLWhRS6RED4Ji8SlbK0rO0OpnrDohCpwQpd8HTPzhznOXZwfptN+GGruHJ1jHDmPsvWINf9n5tUg==").unwrap(),
1206		};
1207		let nsec = NSec {
1208			name: "nested.zent_test.wildcard_test.nsec_tests.dnssec_proof_tests.bitcoin.ninja.".try_into().unwrap(),
1209			next_name: "nsec_tests.dnssec_proof_tests.bitcoin.ninja.".try_into().unwrap(),
1210			types: NSecTypeMask::from_types(&[Txt::TYPE, RRSig::TYPE, NSec::TYPE]),
1211		};
1212		let nsec_rrsig = RRSig {
1213			name: "nested.zent_test.wildcard_test.nsec_tests.dnssec_proof_tests.bitcoin.ninja.".try_into().unwrap(),
1214			ty: NSec::TYPE, alg: 13, labels: 7, orig_ttl: 60, expiration: 1787232803,
1215			inception: 1786017803, key_tag: 37278, key_name: "nsec_tests.dnssec_proof_tests.bitcoin.ninja.".try_into().unwrap(),
1216			signature: base64::decode("y11K2pyk6NVCbXtdI4fFt8J9ox1kFUnpjMn8kocY3HYuU1eNdpPvOkEmVRmXyULDvgsksRdJ4yYqmrkeMQx9Lw==").unwrap(),
1217		};
1218		(txt_resp, txt_rrsig, nsec, nsec_rrsig)
1219	}
1220
1221	#[test]
1222	fn check_txt_record_a() {
1223		let dnskeys = mattcorallo_dnskey().0;
1224		let (txts, txt_rrsig) = mattcorallo_txt_record();
1225		verify_rrsig(&txt_rrsig, &dnskeys, txts.iter().collect()).unwrap();
1226	}
1227
1228	#[test]
1229	fn check_revoked_dnskey_is_not_used() {
1230		let dnskeys = bitcoin_ninja_dnskey().0;
1231		let (txt, txt_rrsig) = bitcoin_ninja_txt_record();
1232		let txt_resp = [txt];
1233		verify_rrsig(&txt_rrsig, &dnskeys, txt_resp.iter().collect()).unwrap();
1234
1235		let signing_key = dnskeys.iter().find(|key| key.key_tag() == txt_rrsig.key_tag).unwrap();
1236
1237		// Build a REVOKEd key which keeps the same key tag as the real signing key but whose
1238		// public key is corrupted. Setting REVOKE adds 0x80 to the key tag, so we take that back
1239		// out of an odd-indexed public key byte, which `key_tag` sums in un-shifted.
1240		let mut revoked = signing_key.clone();
1241		revoked.flags |= 0b0_1000_0000;
1242		let fixup_idx = revoked.pubkey.iter().enumerate()
1243			.position(|(idx, byte)| idx % 2 == 1 && *byte >= 0x80).unwrap();
1244		revoked.pubkey[fixup_idx] -= 0x80;
1245		assert_eq!(revoked.key_tag(), signing_key.key_tag());
1246		assert_ne!(revoked.pubkey, signing_key.pubkey);
1247
1248		// A revoked key has to be skipped outright. If it isn't, it gets picked ahead of the real
1249		// key - it matches on key tag, protocol, ZONE flag and algorithm alike - and its corrupted
1250		// public key then fails the signature check, taking the whole proof down with it.
1251		verify_rrsig(&txt_rrsig, vec![&revoked, signing_key], txt_resp.iter().collect()).unwrap();
1252	}
1253
1254	#[test]
1255	fn check_single_txt_proof() {
1256		let mut rr_stream = Vec::new();
1257		for rr in root_dnskey().1 { write_rr(&rr, 1, &mut rr_stream); }
1258		for rr in com_dnskey().1 { write_rr(&rr, 1, &mut rr_stream); }
1259		for rr in mattcorallo_dnskey().1 { write_rr(&rr, 1, &mut rr_stream); }
1260		let (txts, txt_rrsig) = mattcorallo_txt_record();
1261		for txt in txts { write_rr(&RR::Txt(txt), 1, &mut rr_stream); }
1262		write_rr(&RR::RRSig(txt_rrsig), 1, &mut rr_stream);
1263
1264		let mut rrs = parse_rr_stream(&rr_stream).unwrap();
1265		rrs.shuffle(&mut rand::rngs::OsRng);
1266		let verified_rrs = verify_rr_stream(&rrs).unwrap();
1267		let mut txts = verified_rrs.verified_rrs.iter()
1268			.map(|rr| if let RR::Txt(txt) = rr { txt } else { panic!() })
1269			.collect::<Vec<_>>();
1270		txts.sort();
1271		assert_eq!(txts.len(), 2);
1272		assert!(txts.iter().all(|txt| txt.name.as_str() == "matt.user._bitcoin-payment.mattcorallo.com."));
1273		assert_eq!(txts[0].data.as_vec(),
1274			b"as long as it doesn't start with bitcoin:, other records should be ignored");
1275		assert_eq!(txts[1].data.as_vec(),
1276			b"bitcoin:bc1qztwy6xen3zdtt7z0vrgapmjtfz8acjkfp5fp7l?lno=lno1zr5qyugqgskrk70kqmuq7v3dnr2fnmhukps9n8hut48vkqpqnskt2svsqwjakp7k6pyhtkuxw7y2kqmsxlwruhzqv0zsnhh9q3t9xhx39suc6qsr07ekm5esdyum0w66mnx8vdquwvp7dp5jp7j3v5cp6aj0w329fnkqqv60q96sz5nkrc5r95qffx002q53tqdk8x9m2tmt85jtpmcycvfnrpx3lr45h2g7na3sec7xguctfzzcm8jjqtj5ya27te60j03vpt0vq9tm2n9yxl2hngfnmygesa25s4u4zlxewqpvp94xt7rur4rhxunwkthk9vly3lm5hh0pqv4aymcqejlgssnlpzwlggykkajp7yjs5jvr2agkyypcdlj280cy46jpynsezrcj2kwa2lyr8xvd6lfkph4xrxtk2xc3lpq");
1277		assert_eq!(verified_rrs.valid_from, 1785988800); // The com. DS RRSig was created last
1278		assert_eq!(verified_rrs.expires, 1786415920); // The mattcorallo.com. DNSKEY RRSig expires first
1279		assert_eq!(verified_rrs.max_cache_ttl, 3600); // The TXT record had the shortest TTL
1280	}
1281
1282	#[test]
1283	fn check_txt_record_b() {
1284		let dnskeys = bitcoin_ninja_dnskey().0;
1285		let (txt, txt_rrsig) = bitcoin_ninja_txt_record();
1286		let txt_resp = [txt];
1287		verify_rrsig(&txt_rrsig, &dnskeys, txt_resp.iter().collect()).unwrap();
1288	}
1289
1290	#[test]
1291	fn check_cname_record() {
1292		let dnskeys = bitcoin_ninja_dnskey().0;
1293		let (cname, cname_rrsig) = bitcoin_ninja_cname_record();
1294		let cname_resp = [cname];
1295		verify_rrsig(&cname_rrsig, &dnskeys, cname_resp.iter().collect()).unwrap();
1296	}
1297
1298	#[test]
1299	fn check_dname_does_not_capture_sibling_name() {
1300		// We previously had a bug where we used string `ends_with` rather than label-based
1301		// `ends_with` to check for children, including in DNAME records, which we test here.
1302		let resolve = |extra: Vec<RR>| -> Vec<RR> {
1303			let mut rr_stream = Vec::new();
1304			for rr in root_dnskey().1 { write_rr(&rr, 1, &mut rr_stream); }
1305			for rr in ninja_dnskey().1 { write_rr(&rr, 1, &mut rr_stream); }
1306			for rr in bitcoin_ninja_dnskey().1 { write_rr(&rr, 1, &mut rr_stream); }
1307			let (dname, dname_rrsig) = bitcoin_ninja_dname_record();
1308			for rr in [RR::DName(dname), RR::RRSig(dname_rrsig)] {
1309				write_rr(&rr, 1, &mut rr_stream);
1310			}
1311			for rr in extra { write_rr(&rr, 1, &mut rr_stream); }
1312
1313			let mut rrs = parse_rr_stream(&rr_stream).unwrap();
1314			rrs.shuffle(&mut rand::rngs::OsRng);
1315			let verified_rrs = verify_rr_stream(&rrs).unwrap();
1316			let name = "notdname_test.dnssec_proof_tests.bitcoin.ninja.".try_into().unwrap();
1317			verified_rrs.resolve_name(&name).into_iter().cloned().collect()
1318		};
1319
1320		let (sibling, sibling_rrsig, redirected, redirected_rrsig) = bitcoin_ninja_dname_sibling_records();
1321
1322		assert_eq!(resolve(Vec::new()), Vec::new());
1323		assert_eq!(resolve(vec![RR::Txt(redirected.clone()), RR::RRSig(redirected_rrsig.clone())]), Vec::new());
1324
1325		let resolved = resolve(vec![RR::Txt(sibling.clone()), RR::RRSig(sibling_rrsig.clone())]);
1326		let resolved_full = resolve(vec![
1327                    RR::Txt(sibling), RR::RRSig(sibling_rrsig), RR::Txt(redirected), RR::RRSig(redirected_rrsig),
1328                ]);
1329		assert_eq!(resolved, resolved_full);
1330		assert_eq!(resolved.len(), 1);
1331		if let RR::Txt(txt) = &resolved[0] {
1332			assert_eq!(txt.name.as_str(), "notdname_test.dnssec_proof_tests.bitcoin.ninja.");
1333			assert_eq!(txt.data.as_vec(), b"not_dnamed");
1334		} else { panic!(); }
1335	}
1336
1337	#[test]
1338	fn check_dname_label_boundaries() {
1339		// The substitution cases from RFC 6672 section 2.2's Table 1. A TXT is placed wherever
1340		// the RFC says we should end up, so a wrong substitution finds nothing.
1341		let cases: &[(&str, &str, &str, Option<&str>)] = &[
1342			// query, DNAME owner, DNAME target, where the RFC says we land
1343			("a.example.com.", "example.com.", "example.net.", Some("a.example.net.")),
1344			("a.b.example.com.", "example.com.", "example.net.", Some("a.b.example.net.")),
1345			("a.x.example.com.", "x.example.com.", "example.net.", Some("a.example.net.")),
1346			("a.example.com.", "example.com.", "y.example.net.", Some("a.y.example.net.")),
1347			// Only whole labels are replaced, so this is no match at all.
1348			("ab.example.com.", "b.example.com.", "example.net.", None),
1349			// A DNAME does not redirect its own owner name (RFC 6672 section 2.3).
1350			("example.com.", "example.com.", "example.net.", None),
1351			// A root delegation name must not leave a trailing empty label behind.
1352			("a.x.", "x.", ".", Some("a.")),
1353		];
1354		for (query, owner, target, expected) in cases {
1355			// For the no-match rows the name resolves to itself, so that is where the TXT goes.
1356			let landing = expected.unwrap_or(query);
1357			let rrs = vec![
1358				RR::DName(DName {
1359					name: (*owner).try_into().unwrap(),
1360					delegation_name: (*target).try_into().unwrap(),
1361				}),
1362				RR::Txt(Txt {
1363					name: landing.try_into().unwrap(),
1364					data: (*landing).try_into().unwrap(),
1365				}),
1366			];
1367			let stream = VerifiedRRStream {
1368				verified_rrs: rrs.iter().collect(),
1369				valid_from: 0, expires: u64::MAX, max_cache_ttl: 0,
1370			};
1371			// Note that a DNAME's own owner name may carry other records, so for the rows which
1372			// resolve to the owner itself the DNAME comes back alongside the TXT.
1373			let resolved = stream.resolve_name(&(*query).try_into().unwrap());
1374			let txts: Vec<_> = resolved.iter()
1375				.filter_map(|rr| if let RR::Txt(txt) = rr { Some(txt) } else { None }).collect();
1376			assert_eq!(txts.len(), 1, "{} + DNAME {} -> {}", query, owner, target);
1377			assert_eq!(txts[0].name.as_str(), landing, "{} + DNAME {} -> {}", query, owner, target);
1378		}
1379	}
1380
1381	#[test]
1382	fn check_alias_loops_terminate() {
1383		// A zone may legitimately sign a CName cycle or a self-referential DName (RFC 6672
1384		// section 2.2 gives the latter as a corner case), and we may be handed one in a proof.
1385		let cname_cycle = vec![
1386			RR::CName(CName {
1387				name: "a.example.com.".try_into().unwrap(),
1388				canonical_name: "b.example.com.".try_into().unwrap(),
1389			}),
1390			RR::CName(CName {
1391				name: "b.example.com.".try_into().unwrap(),
1392				canonical_name: "a.example.com.".try_into().unwrap(),
1393			}),
1394		];
1395		let self_dname = vec![
1396			RR::DName(DName {
1397				name: "example.com.".try_into().unwrap(),
1398				delegation_name: "example.com.".try_into().unwrap(),
1399			}),
1400		];
1401		for (rrs, query) in [(cname_cycle, "a.example.com."), (self_dname, "cyc.example.com.")] {
1402			let stream = VerifiedRRStream {
1403				verified_rrs: rrs.iter().collect(),
1404				valid_from: 0, expires: u64::MAX, max_cache_ttl: 0,
1405			};
1406			assert!(stream.resolve_name(&query.try_into().unwrap()).is_empty());
1407		}
1408	}
1409
1410	#[test]
1411	fn check_multi_zone_proof() {
1412		let mut rr_stream = Vec::new();
1413		for rr in root_dnskey().1 { write_rr(&rr, 1, &mut rr_stream); }
1414		for rr in com_dnskey().1 { write_rr(&rr, 1, &mut rr_stream); }
1415		for rr in ninja_dnskey().1 { write_rr(&rr, 1, &mut rr_stream); }
1416		for rr in mattcorallo_dnskey().1 { write_rr(&rr, 1, &mut rr_stream); }
1417		let (txts, txt_rrsig) = mattcorallo_txt_record();
1418		for txt in txts { write_rr(&RR::Txt(txt), 1, &mut rr_stream); }
1419		write_rr(&RR::RRSig(txt_rrsig), 1, &mut rr_stream);
1420		for rr in bitcoin_ninja_dnskey().1 { write_rr(&rr, 1, &mut rr_stream); }
1421		let (txt, txt_rrsig) = bitcoin_ninja_txt_record();
1422		for rr in [RR::Txt(txt), RR::RRSig(txt_rrsig)] { write_rr(&rr, 1, &mut rr_stream); }
1423		let (cname, cname_rrsig) = bitcoin_ninja_cname_record();
1424		for rr in [RR::CName(cname), RR::RRSig(cname_rrsig)] { write_rr(&rr, 1, &mut rr_stream); }
1425
1426		let mut rrs = parse_rr_stream(&rr_stream).unwrap();
1427		rrs.shuffle(&mut rand::rngs::OsRng);
1428		let mut verified_rrs = verify_rr_stream(&rrs).unwrap();
1429		verified_rrs.verified_rrs.sort();
1430		assert_eq!(verified_rrs.verified_rrs.len(), 4);
1431		if let RR::Txt(txt) = &verified_rrs.verified_rrs[0] {
1432			assert_eq!(txt.name.as_str(), "matt.user._bitcoin-payment.mattcorallo.com.");
1433			assert_eq!(txt.data.as_vec(),
1434				b"as long as it doesn't start with bitcoin:, other records should be ignored");
1435		} else { panic!(); }
1436		if let RR::Txt(txt) = &verified_rrs.verified_rrs[1] {
1437			assert_eq!(txt.name.as_str(), "matt.user._bitcoin-payment.mattcorallo.com.");
1438			assert_eq!(txt.data.as_vec(),
1439				b"bitcoin:bc1qztwy6xen3zdtt7z0vrgapmjtfz8acjkfp5fp7l?lno=lno1zr5qyugqgskrk70kqmuq7v3dnr2fnmhukps9n8hut48vkqpqnskt2svsqwjakp7k6pyhtkuxw7y2kqmsxlwruhzqv0zsnhh9q3t9xhx39suc6qsr07ekm5esdyum0w66mnx8vdquwvp7dp5jp7j3v5cp6aj0w329fnkqqv60q96sz5nkrc5r95qffx002q53tqdk8x9m2tmt85jtpmcycvfnrpx3lr45h2g7na3sec7xguctfzzcm8jjqtj5ya27te60j03vpt0vq9tm2n9yxl2hngfnmygesa25s4u4zlxewqpvp94xt7rur4rhxunwkthk9vly3lm5hh0pqv4aymcqejlgssnlpzwlggykkajp7yjs5jvr2agkyypcdlj280cy46jpynsezrcj2kwa2lyr8xvd6lfkph4xrxtk2xc3lpq");
1440		} else { panic!(); }
1441		if let RR::Txt(txt) = &verified_rrs.verified_rrs[2] {
1442			assert_eq!(txt.name.as_str(), "txt_test.dnssec_proof_tests.bitcoin.ninja.");
1443			assert_eq!(txt.data.as_vec(), b"dnssec_prover_test");
1444		} else { panic!(); }
1445		if let RR::CName(cname) = &verified_rrs.verified_rrs[3] {
1446			assert_eq!(cname.name.as_str(), "cname_test.dnssec_proof_tests.bitcoin.ninja.");
1447			assert_eq!(cname.canonical_name.as_str(), "txt_test.dnssec_proof_tests.bitcoin.ninja.");
1448		} else { panic!(); }
1449
1450		let filtered_rrs =
1451			verified_rrs.resolve_name(&"cname_test.dnssec_proof_tests.bitcoin.ninja.".try_into().unwrap());
1452		assert_eq!(filtered_rrs.len(), 1);
1453		if let RR::Txt(txt) = &filtered_rrs[0] {
1454			assert_eq!(txt.name.as_str(), "txt_test.dnssec_proof_tests.bitcoin.ninja.");
1455			assert_eq!(txt.data.as_vec(), b"dnssec_prover_test");
1456		} else { panic!(); }
1457	}
1458
1459	#[test]
1460	fn check_wildcard_record() {
1461		// Wildcard proof works for any name, even multiple names
1462		let dnskeys = bitcoin_ninja_dnskey().0;
1463		let (txt, txt_rrsig, _, _) = bitcoin_ninja_wildcard_record("name");
1464		let txt_resp = [txt];
1465		verify_rrsig(&txt_rrsig, &dnskeys, txt_resp.iter().collect()).unwrap();
1466
1467		let (txt, txt_rrsig, _, _) = bitcoin_ninja_wildcard_record("anoter_name");
1468		let txt_resp = [txt];
1469		verify_rrsig(&txt_rrsig, &dnskeys, txt_resp.iter().collect()).unwrap();
1470
1471		let (txt, txt_rrsig, _, _) = bitcoin_ninja_wildcard_record("multiple.names");
1472		let txt_resp = [txt];
1473		verify_rrsig(&txt_rrsig, &dnskeys, txt_resp.iter().collect()).unwrap();
1474	}
1475
1476	#[test]
1477	fn check_wildcard_proof() {
1478		let mut rr_stream = Vec::new();
1479		for rr in root_dnskey().1 { write_rr(&rr, 1, &mut rr_stream); }
1480		for rr in ninja_dnskey().1 { write_rr(&rr, 1, &mut rr_stream); }
1481		for rr in bitcoin_ninja_dnskey().1 { write_rr(&rr, 1, &mut rr_stream); }
1482		let (cname, cname_rrsig, txt, txt_rrsig, nsec3s) = bitcoin_ninja_cname_wildcard_record();
1483		for rr in [RR::CName(cname), RR::RRSig(cname_rrsig)] { write_rr(&rr, 1, &mut rr_stream); }
1484		for rr in [RR::Txt(txt), RR::RRSig(txt_rrsig)] { write_rr(&rr, 1, &mut rr_stream); }
1485		for (rra, rrb) in nsec3s { write_rr(&rra, 1, &mut rr_stream); write_rr(&rrb, 1, &mut rr_stream); }
1486
1487		let mut rrs = parse_rr_stream(&rr_stream).unwrap();
1488		rrs.shuffle(&mut rand::rngs::OsRng);
1489		let mut verified_rrs = verify_rr_stream(&rrs).unwrap();
1490		verified_rrs.verified_rrs.sort();
1491		assert_eq!(verified_rrs.verified_rrs.len(), 2);
1492		if let RR::Txt(txt) = &verified_rrs.verified_rrs[0] {
1493			assert_eq!(txt.name.as_str(), "asdf.wildcard_test.dnssec_proof_tests.bitcoin.ninja.");
1494			assert_eq!(txt.data.as_vec(), b"wildcard_test");
1495		} else { panic!(); }
1496		if let RR::CName(cname) = &verified_rrs.verified_rrs[1] {
1497			assert_eq!(cname.name.as_str(), "asdf.cname_wildcard_test.dnssec_proof_tests.bitcoin.ninja.");
1498			assert_eq!(cname.canonical_name.as_str(), "cname.wildcard_test.dnssec_proof_tests.bitcoin.ninja.");
1499		} else { panic!(); }
1500
1501		let filtered_rrs =
1502			verified_rrs.resolve_name(&"asdf.wildcard_test.dnssec_proof_tests.bitcoin.ninja.".try_into().unwrap());
1503		assert_eq!(filtered_rrs.len(), 1);
1504		if let RR::Txt(txt) = &filtered_rrs[0] {
1505			assert_eq!(txt.name.as_str(), "asdf.wildcard_test.dnssec_proof_tests.bitcoin.ninja.");
1506			assert_eq!(txt.data.as_vec(), b"wildcard_test");
1507		} else { panic!(); }
1508	}
1509
1510	#[test]
1511	fn check_simple_nsec_zone_proof() {
1512		let mut rr_stream = Vec::new();
1513		for rr in root_dnskey().1 { write_rr(&rr, 1, &mut rr_stream); }
1514		for rr in ninja_dnskey().1 { write_rr(&rr, 1, &mut rr_stream); }
1515		for rr in bitcoin_ninja_dnskey().1 { write_rr(&rr, 1, &mut rr_stream); }
1516		for rr in bitcoin_ninja_nsec_dnskey().1 { write_rr(&rr, 1, &mut rr_stream); }
1517		let (txt, txt_rrsig) = bitcoin_ninja_nsec_record();
1518		for rr in [RR::Txt(txt), RR::RRSig(txt_rrsig)] { write_rr(&rr, 1, &mut rr_stream); }
1519
1520		let mut rrs = parse_rr_stream(&rr_stream).unwrap();
1521		rrs.shuffle(&mut rand::rngs::OsRng);
1522		let verified_rrs = verify_rr_stream(&rrs).unwrap();
1523		let filtered_rrs =
1524			verified_rrs.resolve_name(&"a.nsec_tests.dnssec_proof_tests.bitcoin.ninja.".try_into().unwrap());
1525		assert_eq!(filtered_rrs.len(), 1);
1526		if let RR::Txt(txt) = &filtered_rrs[0] {
1527			assert_eq!(txt.name.as_str(), "a.nsec_tests.dnssec_proof_tests.bitcoin.ninja.");
1528			assert_eq!(txt.data.as_vec(), b"txt_a");
1529		} else { panic!(); }
1530	}
1531
1532	#[test]
1533	fn check_nsec_wildcard_proof() {
1534		let check_proof = |pfx: &str, post_override: bool| -> Result<(), ()> {
1535			let mut rr_stream = Vec::new();
1536			for rr in root_dnskey().1 { write_rr(&rr, 1, &mut rr_stream); }
1537			for rr in ninja_dnskey().1 { write_rr(&rr, 1, &mut rr_stream); }
1538			for rr in bitcoin_ninja_dnskey().1 { write_rr(&rr, 1, &mut rr_stream); }
1539			for rr in bitcoin_ninja_nsec_dnskey().1 { write_rr(&rr, 1, &mut rr_stream); }
1540			let (txt, txt_rrsig, nsec, nsec_rrsig) = if post_override {
1541				bitcoin_ninja_nsec_post_override_wildcard_record(pfx)
1542			} else {
1543				bitcoin_ninja_nsec_wildcard_record(pfx)
1544			};
1545			for rr in [RR::Txt(txt), RR::RRSig(txt_rrsig)] { write_rr(&rr, 1, &mut rr_stream); }
1546			for rr in [RR::NSec(nsec), RR::RRSig(nsec_rrsig)] { write_rr(&rr, 1, &mut rr_stream); }
1547
1548			let mut rrs = parse_rr_stream(&rr_stream).unwrap();
1549			rrs.shuffle(&mut rand::rngs::OsRng);
1550			// If the post_override flag is wrong (or the pfx is override), this will fail. No
1551			// other calls in this lambda should fail.
1552			let verified_rrs = verify_rr_stream(&rrs).map_err(|_| ())?;
1553			let name: Name =
1554				(pfx.to_owned() + ".wildcard_test.nsec_tests.dnssec_proof_tests.bitcoin.ninja.").try_into().unwrap();
1555			let filtered_rrs = verified_rrs.resolve_name(&name);
1556			assert_eq!(filtered_rrs.len(), 1);
1557			if let RR::Txt(txt) = &filtered_rrs[0] {
1558				assert_eq!(txt.name, name);
1559				assert_eq!(txt.data.as_vec(), b"wildcard_test");
1560			} else { panic!(); }
1561			Ok(())
1562		};
1563		// Records up to override will only work with the pre-override NSEC, and afterwards with
1564		// the post-override NSEC. The literal override will always fail.
1565		check_proof("a", false).unwrap();
1566		check_proof("a", true).unwrap_err();
1567		check_proof("a.b", false).unwrap();
1568		check_proof("a.b", true).unwrap_err();
1569		check_proof("o", false).unwrap();
1570		check_proof("o", true).unwrap_err();
1571		check_proof("a.o", false).unwrap();
1572		check_proof("a.o", true).unwrap_err();
1573		check_proof("override", false).unwrap_err();
1574		check_proof("override", true).unwrap_err();
1575		// Subdomains of override are also overridden by the override TXT entry and cannot use the
1576		// wildcard record.
1577		check_proof("b.override", false).unwrap_err();
1578		check_proof("b.override", true).unwrap_err();
1579		check_proof("z", false).unwrap_err();
1580		check_proof("z", true).unwrap();
1581		check_proof("a.z", false).unwrap_err();
1582		check_proof("a.z", true).unwrap();
1583	}
1584
1585	#[test]
1586	fn check_online_signed_nsec_next_name() {
1587		let dnskeys = cloudflare_dnskey().0;
1588		let (nsec, nsec_rrsig) = cloudflare_online_signed_nsec();
1589		assert_eq!(&nsec.next_name[..], b"\x00.online_signing_test.cloudflare.com.");
1590
1591		// Check that the RR with a `\000` label survives round-trip
1592		let mut rr_stream = Vec::new();
1593		write_rr(&nsec, 300, &mut rr_stream);
1594		assert_eq!(parse_rr_stream(&rr_stream).unwrap(), vec![RR::NSec(nsec.clone())]);
1595
1596		verify_rrsig(&nsec_rrsig, &dnskeys, vec![&nsec]).unwrap();
1597
1598		assert_eq!(StaticRecord::json(&nsec), "{\"type\":\"nsec\",\
1599			\"name\":\"online_signing_test.cloudflare.com.\",\
1600			\"next_name\":\"\\u0000.online_signing_test.cloudflare.com.\",\
1601			\"types\":[\"NSEC\",\"RRSIG\",128]}");
1602	}
1603
1604	#[test]
1605	fn check_nsec_wraparound_wildcard_proof() {
1606		let check_proof = |pfx: &str| -> Result<(), ()> {
1607			let mut rr_stream = Vec::new();
1608			for rr in root_dnskey().1 { write_rr(&rr, 1, &mut rr_stream); }
1609			for rr in ninja_dnskey().1 { write_rr(&rr, 1, &mut rr_stream); }
1610			for rr in bitcoin_ninja_dnskey().1 { write_rr(&rr, 1, &mut rr_stream); }
1611			for rr in bitcoin_ninja_nsec_dnskey().1 { write_rr(&rr, 1, &mut rr_stream); }
1612			let (txt, txt_rrsig, nsec, nsec_rrsig) =
1613				bitcoin_ninja_nsec_wraparound_wildcard_record(pfx);
1614			for rr in [RR::Txt(txt), RR::RRSig(txt_rrsig)] { write_rr(&rr, 1, &mut rr_stream); }
1615			for rr in [RR::NSec(nsec), RR::RRSig(nsec_rrsig)] { write_rr(&rr, 1, &mut rr_stream); }
1616
1617			let mut rrs = parse_rr_stream(&rr_stream).unwrap();
1618			rrs.shuffle(&mut rand::rngs::OsRng);
1619			let verified_rrs = verify_rr_stream(&rrs).map_err(|_| ())?;
1620			let name: Name =
1621				(pfx.to_owned() + ".wildcard_test.nsec_tests.dnssec_proof_tests.bitcoin.ninja.").try_into().unwrap();
1622			let filtered_rrs = verified_rrs.resolve_name(&name);
1623			assert_eq!(filtered_rrs.len(), 1);
1624			if let RR::Txt(txt) = &filtered_rrs[0] {
1625				assert_eq!(txt.name, name);
1626				assert_eq!(txt.data.as_vec(), b"wildcard_test");
1627			} else { panic!(); }
1628			Ok(())
1629		};
1630
1631		// `zz` sorts after `zent_test` (the last record) so sits in the wraparound NSEC.
1632		check_proof("zz").unwrap();
1633		check_proof("a.zz").unwrap();
1634		// `a` sorts after the zone apex record, so is past the wraparound NSEC.
1635		check_proof("a").unwrap_err();
1636		check_proof("nested.zent_test").unwrap_err();
1637	}
1638
1639	#[test]
1640	fn check_descendant_zone_denial_rejected() {
1641		// A wildcard TXT signed by `bitcoin.ninja.`, whose next closer name therefore has to be
1642		// shown not to exist in `bitcoin.ninja.`.
1643		let build = |proof: Vec<RR>| {
1644			let mut rr_stream = Vec::new();
1645			for rr in root_dnskey().1 { write_rr(&rr, 1, &mut rr_stream); }
1646			for rr in ninja_dnskey().1 { write_rr(&rr, 1, &mut rr_stream); }
1647			for rr in bitcoin_ninja_dnskey().1 { write_rr(&rr, 1, &mut rr_stream); }
1648			for rr in bitcoin_ninja_nsec_dnskey().1 { write_rr(&rr, 1, &mut rr_stream); }
1649			let (txt, txt_rrsig, _, _) = bitcoin_ninja_wildcard_record("asdf");
1650			for rr in [RR::Txt(txt), RR::RRSig(txt_rrsig)] { write_rr(&rr, 1, &mut rr_stream); }
1651			for rr in proof { write_rr(&rr, 1, &mut rr_stream); }
1652			let mut rrs = parse_rr_stream(&rr_stream).unwrap();
1653			rrs.shuffle(&mut rand::rngs::OsRng);
1654			verify_rr_stream(&rrs).map(|_| ())
1655		};
1656
1657		// `bitcoin.ninja.`'s own NSEC3 covering the name is the proof which actually applies.
1658		let (_, _, nsec3, nsec3_rrsig) = bitcoin_ninja_wildcard_record("asdf");
1659		build(vec![RR::NSec3(nsec3), RR::RRSig(nsec3_rrsig)]).unwrap();
1660
1661		// The same query "proven" with a record from `nsec_tests.dnssec_proof_tests.bitcoin.ninja.`
1662		// instead - a zone delegated *beneath* `bitcoin.ninja.`, and so one whose names are still
1663		// suffixed by it. Its last NSEC wraps back to its own apex and so spans everything outside
1664		// that zone, which is not its to speak for. Note that this has to be the *wrapping* NSEC:
1665		// with any other one the name simply falls outside the range and the test would pass
1666		// without the zone check below ever being consulted.
1667		let (_, _, nsec, nsec_rrsig) = bitcoin_ninja_nsec_wraparound_wildcard_record("a");
1668		assert_eq!(build(vec![RR::NSec(nsec), RR::RRSig(nsec_rrsig)]).unwrap_err(),
1669			ValidationError::Invalid);
1670	}
1671
1672	#[test]
1673	fn check_nsec_empty_non_terminal_is_not_nonexistence() {
1674		// `nested.zent_test.wildcard_test...` exists, which makes `zent_test.wildcard_test...`
1675		// an empty non-terminal: it exists, but owns no RRset. RFC 4035 section 2.3 forbids an
1676		// NSEC at such a name, so the zone's chain steps straight over it and the NSEC from
1677		// `bitcoin_ninja_nsec_post_override_wildcard_record` spans it. That NSEC must not be
1678		// read as proof the name is absent - RFC 4592 section 2.2.2 is explicit that empty
1679		// non-terminals "exist", and RFC 8198 Appendix B gives the test for spotting one: the
1680		// next name is a subdomain of the name in question.
1681		//
1682		// This is exploitable because an RRSIG's Labels field, not its owner name, decides what
1683		// was signed, and the owner name is not part of the signed data. The wildcard TXT's one
1684		// signature therefore verifies against any name under `wildcard_test...`, and the only
1685		// thing keeping it off names the zone never published it at is the next closer denial.
1686		let build = |pfx: &str| {
1687			let mut rr_stream = Vec::new();
1688			for rr in root_dnskey().1 { write_rr(&rr, 1, &mut rr_stream); }
1689			for rr in ninja_dnskey().1 { write_rr(&rr, 1, &mut rr_stream); }
1690			for rr in bitcoin_ninja_dnskey().1 { write_rr(&rr, 1, &mut rr_stream); }
1691			for rr in bitcoin_ninja_nsec_dnskey().1 { write_rr(&rr, 1, &mut rr_stream); }
1692			let (txt, txt_rrsig, nsec, nsec_rrsig) =
1693				bitcoin_ninja_nsec_post_override_wildcard_record(pfx);
1694			for rr in [RR::Txt(txt), RR::RRSig(txt_rrsig)] { write_rr(&rr, 1, &mut rr_stream); }
1695			for rr in [RR::NSec(nsec), RR::RRSig(nsec_rrsig)] { write_rr(&rr, 1, &mut rr_stream); }
1696			let mut rrs = parse_rr_stream(&rr_stream).unwrap();
1697			rrs.shuffle(&mut rand::rngs::OsRng);
1698			verify_rr_stream(&rrs).map(|_| ())
1699		};
1700
1701		// `z.wildcard_test...` is genuinely absent and the same NSEC spans it, so that wildcard
1702		// expansion is real and must still validate - the empty non-terminal check must not cost
1703		// us legitimate wildcard proofs.
1704		build("z").unwrap();
1705
1706		// `zent_test.wildcard_test...` is the empty non-terminal, so no denial of it is possible
1707		// and the RRset must not be accepted below it. Without the check this returns Ok, with
1708		// the TXT attributed to a name the zone never published it at.
1709		assert_eq!(build("x.zent_test").unwrap_err(), ValidationError::Invalid);
1710		// The depth of the forged name below the empty non-terminal is unconstrained.
1711		assert_eq!(build("y.x.zent_test").unwrap_err(), ValidationError::Invalid);
1712	}
1713
1714
1715	#[test]
1716	fn check_txt_sort_order() {
1717		let mut rr_stream = Vec::new();
1718		for rr in root_dnskey().1 { write_rr(&rr, 1, &mut rr_stream); }
1719		for rr in ninja_dnskey().1 { write_rr(&rr, 1, &mut rr_stream); }
1720		for rr in bitcoin_ninja_dnskey().1 { write_rr(&rr, 1, &mut rr_stream); }
1721		let (mut txts, rrsig) = bitcoin_ninja_txt_sort_edge_cases_records();
1722		write_rr(&rrsig, 1, &mut rr_stream);
1723		for txt in txts.iter() { write_rr(txt, 1, &mut rr_stream); }
1724
1725		let mut rrs = parse_rr_stream(&rr_stream).unwrap();
1726		rrs.shuffle(&mut rand::rngs::OsRng);
1727		let verified_rrs = verify_rr_stream(&rrs).unwrap();
1728		let mut verified_txts = verified_rrs.verified_rrs
1729			.iter().map(|rr| if let RR::Txt(txt) = rr { txt.clone() } else { panic!(); })
1730			.collect::<Vec<_>>();
1731		verified_txts.sort();
1732		txts.sort();
1733		assert_eq!(verified_txts, txts);
1734	}
1735
1736	#[test]
1737	fn rfc9102_parse_test() {
1738		// Note that this is the `AuthenticationChain` field only, and ignores the
1739		// `ExtSupportLifetime` field (stripping the top two 0 bytes from the front).
1740let rfc9102_test_vector = Vec::from_hex("045f343433045f74637003777777076578616d706c6503636f6d000034000100000e1000230301018bd1da95272f7fa4ffb24137fc0ed03aae67e5c4d8b3c50734e1050a7920b922045f343433045f74637003777777076578616d706c6503636f6d00002e000100000e10005f00340d0500000e105fc6d9005bfdda80074e076578616d706c6503636f6d00ce1d3adeb7dc7cee656d61cfb472c5977c8c9caeae9b765155c518fb107b6a1fe0355fbaaf753c192832fa621fa73a8b85ed79d374117387598fcc812e1ef3fb076578616d706c6503636f6d000030000100000e1000440101030d2670355e0c894d9cfea6c5af6eb7d458b57a50ba88272512d8241d8541fd54adf96ec956789a51ceb971094b3bb3f4ec49f64c686595be5b2e89e8799c7717cc076578616d706c6503636f6d00002e000100000e10005f00300d0200000e105fc6d9005bfdda80074e076578616d706c6503636f6d004628383075b8e34b743a209b27ae148d110d4e1a246138a91083249cb4a12a2d9bc4c2d7ab5eb3afb9f5d1037e4d5da8339c162a9298e9be180741a8ca74accc076578616d706c6503636f6d00002b00010002a3000024074e0d02e9b533a049798e900b5c29c90cd25a986e8a44f319ac3cd302bafc08f5b81e16076578616d706c6503636f6d00002e00010002a3000057002b0d020002a3005fc6d9005bfdda80861703636f6d00a203e704a6facbeb13fc9384fdd6de6b50de5659271f38ce81498684e6363172d47e2319fdb4a22a58a231edc2f1ff4fb2811a1807be72cb5241aa26fdaee03903636f6d00003000010002a30000440100030dec8204e43a25f2348c52a1d3bce3a265aa5d11b43dc2a471162ff341c49db9f50a2e1a41caf2e9cd20104ea0968f7511219f0bdc56b68012cc3995336751900b03636f6d00003000010002a30000440101030d45b91c3bef7a5d99a7a7c8d822e33896bc80a777a04234a605a4a8880ec7efa4e6d112c73cd3d4c65564fa74347c873723cc5f643370f166b43dedff836400ff03636f6d00003000010002a30000440101030db3373b6e22e8e49e0e1e591a9f5bd9ac5e1a0f86187fe34703f180a9d36c958f71c4af48ce0ebc5c792a724e11b43895937ee53404268129476eb1aed323939003636f6d00002e00010002a300005700300d010002a3005fc6d9005bfdda8049f303636f6d0018a948eb23d44f80abc99238fcb43c5a18debe57004f7343593f6deb6ed71e04654a433f7aa1972130d9bd921c73dcf63fcf665f2f05a0aaebafb059dc12c96503636f6d00002e00010002a300005700300d010002a3005fc6d9005bfdda80708903636f6d006170e6959bd9ed6e575837b6f580bd99dbd24a44682b0a359626a246b1812f5f9096b75e157e77848f068ae0085e1a609fc19298c33b736863fbccd4d81f5eb203636f6d00002b000100015180002449f30d0220f7a9db42d0e2042fbbb9f9ea015941202f9eabb94487e658c188e7bcb5211503636f6d00002b000100015180002470890d02ad66b3276f796223aa45eda773e92c6d98e70643bbde681db342a9e5cf2bb38003636f6d00002e0001000151800053002b0d01000151805fc6d9005bfdda807cae00122e276d45d9e9816f7922ad6ea2e73e82d26fce0a4b718625f314531ac92f8ae82418df9b898f989d32e80bc4deaba7c4a7c8f172adb57ced7fb5e77a784b0700003000010001518000440100030dccacfe0c25a4340fefba17a254f706aac1f8d14f38299025acc448ca8ce3f561f37fc3ec169fe847c8fcbe68e358ff7c71bb5ee1df0dbe518bc736d4ce8dfe1400003000010001518000440100030df303196789731ddc8a6787eff24cacfeddd032582f11a75bb1bcaa5ab321c1d7525c2658191aec01b3e98ab7915b16d571dd55b4eae51417110cc4cdd11d171100003000010001518000440101030dcaf5fe54d4d48f16621afb6bd3ad2155bacf57d1faad5bac42d17d948c421736d9389c4c4011666ea95cf17725bd0fa00ce5e714e4ec82cfdfacc9b1c863ad4600002e000100015180005300300d00000151805fc6d9005bfdda80b79d00de7a6740eeecba4bda1e5c2dd4899b2c965893f3786ce747f41e50d9de8c0a72df82560dfb48d714de3283ae99a49c0fcb50d3aaadb1a3fc62ee3a8a0988b6be").unwrap();
1741
1742		let mut rrs = parse_rr_stream(&rfc9102_test_vector).unwrap();
1743		rrs.shuffle(&mut rand::rngs::OsRng);
1744		let verified_rrs = verify_rr_stream(&rrs).unwrap();
1745		assert_eq!(verified_rrs.verified_rrs.len(), 1);
1746		if let RR::TLSA(tlsa) = &verified_rrs.verified_rrs[0] {
1747			assert_eq!(tlsa.cert_usage, 3);
1748			assert_eq!(tlsa.selector, 1);
1749			assert_eq!(tlsa.data_ty, 1);
1750			assert_eq!(tlsa.data, Vec::from_hex("8bd1da95272f7fa4ffb24137fc0ed03aae67e5c4d8b3c50734e1050a7920b922").unwrap());
1751		} else { panic!(); }
1752	}
1753
1754	#[test]
1755	fn check_nsec3_wraparound_wildcard_proof() {
1756		// A wildcard-generated CNAME whose "next closer" name hashes *above* the highest hash in
1757		// bitcoin.ninja's NSEC3 chain. It is therefore covered by the chain's last NSEC3, whose
1758		// `next_name_hash` wraps back around to the zone's lowest hash (RFC 5155 section 3.1.7).
1759		// We used to reject such proofs, only accepting non-wrapping [start, next) ranges.
1760		let mut rr_stream = Vec::new();
1761		for rr in root_dnskey().1 { write_rr(&rr, 1, &mut rr_stream); }
1762		for rr in com_dnskey().1 { write_rr(&rr, 1, &mut rr_stream); }
1763		for rr in ninja_dnskey().1 { write_rr(&rr, 1, &mut rr_stream); }
1764		for rr in mattcorallo_dnskey().1 { write_rr(&rr, 1, &mut rr_stream); }
1765		for rr in bitcoin_ninja_dnskey().1 { write_rr(&rr, 1, &mut rr_stream); }
1766		let (cname, cname_rrsig, nsec3, nsec3_rrsig) = bitcoin_ninja_x_domain_cname_wildcard_record();
1767		for rr in [RR::CName(cname), RR::RRSig(cname_rrsig)] { write_rr(&rr, 1, &mut rr_stream); }
1768		for rr in [RR::NSec3(nsec3), RR::RRSig(nsec3_rrsig)] { write_rr(&rr, 1, &mut rr_stream); }
1769		let (txts, txt_rrsig) = mattcorallo_txt_record();
1770		for txt in txts { write_rr(&txt, 1, &mut rr_stream); }
1771		write_rr(&txt_rrsig, 1, &mut rr_stream);
1772
1773		let mut rrs = parse_rr_stream(&rr_stream).unwrap();
1774
1775		// Ensure the fixture still actually exercises the wrap-around, in case it is regenerated.
1776		let mut wrapping_nsec3s = 0;
1777		for nsec3 in rrs.iter().filter_map(|rr| if let RR::NSec3(n) = rr { Some(n) } else { None }) {
1778			let (start_hash_base32, _) = nsec3.name.split_once('.').unwrap();
1779			let start_hash = crate::base32::decode(start_hash_base32).unwrap();
1780			if start_hash[..] > nsec3.next_name_hash[..] { wrapping_nsec3s += 1; }
1781		}
1782		assert_eq!(wrapping_nsec3s, 1, "fixture no longer covers the NSEC3 wrap-around case");
1783
1784		rrs.shuffle(&mut rand::rngs::OsRng);
1785		let verified_rrs = verify_rr_stream(&rrs).unwrap();
1786
1787		let name = "wildcard.x_domain_cname_wild.dnssec_proof_tests.bitcoin.ninja.";
1788		let filtered_rrs = verified_rrs.resolve_name(&name.try_into().unwrap());
1789		assert!(filtered_rrs.iter().any(|rr| if let RR::Txt(txt) = rr {
1790			txt.name.as_str() == "matt.user._bitcoin-payment.mattcorallo.com."
1791				&& txt.data.as_vec().starts_with(b"bitcoin:")
1792		} else { false }));
1793	}
1794
1795	#[test]
1796	fn check_nsec3_matching_name_does_not_cover_it() {
1797		// An NSEC3 whose owner hash *matches* the name we want a non-existence proof for shows
1798		// that the name exists, so it must not be treated as covering that name. Otherwise a
1799		// wildcard-signed RRSet could be substituted for the real records at an existing name.
1800		let mut rr_stream = Vec::new();
1801		for rr in root_dnskey().1 { write_rr(&rr, 1, &mut rr_stream); }
1802		for rr in ninja_dnskey().1 { write_rr(&rr, 1, &mut rr_stream); }
1803		for rr in bitcoin_ninja_dnskey().1 { write_rr(&rr, 1, &mut rr_stream); }
1804		let (txt, txt_rrsig, nsec3, nsec3_rrsig) = bitcoin_ninja_cname_target_wildcard_record();
1805		for rr in [RR::Txt(txt), RR::RRSig(txt_rrsig)] { write_rr(&rr, 1, &mut rr_stream); }
1806		for rr in [RR::NSec3(nsec3), RR::RRSig(nsec3_rrsig)] { write_rr(&rr, 1, &mut rr_stream); }
1807
1808		// As built, this is a perfectly good wildcard proof - which also confirms the whole
1809		// chain and all the signature validity windows line up.
1810		let rrs = parse_rr_stream(&rr_stream).unwrap();
1811		verify_rr_stream(&rrs).unwrap();
1812
1813		// Now relabel the wildcard-signed TXT onto `override.wildcard_test...`, which really
1814		// exists in the zone, and supply only the NSEC3 matching that name.
1815		let overridden: Name =
1816			"override.wildcard_test.dnssec_proof_tests.bitcoin.ninja.".try_into().unwrap();
1817		let mut rrs: Vec<RR> = rrs.into_iter()
1818			.filter(|rr| rr.ty() != NSec3::TYPE)
1819			.filter(|rr| !matches!(rr, RR::RRSig(sig) if sig.ty == NSec3::TYPE))
1820			.map(|rr| match rr {
1821				RR::Txt(mut txt) => { txt.name = overridden.clone(); RR::Txt(txt) },
1822				RR::RRSig(mut sig) if sig.ty == Txt::TYPE => {
1823					sig.name = overridden.clone();
1824					RR::RRSig(sig)
1825				},
1826				rr => rr,
1827			})
1828			.collect();
1829
1830		// The relabelled records still carry a valid wildcard signature, so any rejection below
1831		// has to come from the NSEC3 handling rather than from a broken signature.
1832		let dnskeys = rrs.iter()
1833			.filter_map(|rr| if let RR::DnsKey(k) = rr { Some(k) } else { None })
1834			.filter(|k| k.name.as_str() == "bitcoin.ninja.")
1835			.collect::<Vec<_>>();
1836		let txt = rrs.iter()
1837			.find_map(|rr| if let RR::Txt(txt) = rr { Some(txt) } else { None }).unwrap();
1838		let txt_rrsig = rrs.iter()
1839			.find_map(|rr| if let RR::RRSig(sig) = rr {
1840				if sig.ty == Txt::TYPE { Some(sig) } else { None }
1841			} else { None }).unwrap();
1842		assert_eq!(txt.name, overridden);
1843		verify_rrsig(txt_rrsig, dnskeys.iter().copied(), vec![txt]).unwrap();
1844
1845		// Likewise, this NSEC3 is genuinely signed by the zone and genuinely matches the name, so
1846		// the rejection below can only come from it matching rather than covering.
1847		let (nsec3, nsec3_rrsig) = bitcoin_ninja_override_matching_nsec3(&dnskeys);
1848		rrs.push(RR::NSec3(nsec3));
1849		rrs.push(RR::RRSig(nsec3_rrsig));
1850		rrs.shuffle(&mut rand::rngs::OsRng);
1851		assert_eq!(verify_rr_stream(&rrs).unwrap_err(), ValidationError::Invalid);
1852	}
1853
1854}