use alloc::vec;
use core::cmp::{self, Ordering};
-use ring::signature;
-
use crate::base32;
use crate::crypto;
use crate::rr::*;
Invalid,
}
-pub(crate) fn bytes_to_rsa_pk<'a>(pubkey: &'a [u8])
--> Result<signature::RsaPublicKeyComponents<&'a [u8]>, ()> {
- if pubkey.len() <= 3 { return Err(()); }
-
- let mut pos = 0;
- let exponent_length;
- if pubkey[0] == 0 {
- exponent_length = ((pubkey[1] as usize) << 8) | (pubkey[2] as usize);
- pos += 3;
- } else {
- exponent_length = pubkey[0] as usize;
- pos += 1;
- }
-
- if pubkey.len() <= pos + exponent_length { return Err(()); }
- Ok(signature::RsaPublicKeyComponents {
- n: &pubkey[pos + exponent_length..],
- e: &pubkey[pos..pos + exponent_length]
- })
-}
-
-fn verify_rrsig<'a, RR: Record, Keys>(sig: &RRSig, dnskeys: Keys, mut records: Vec<&RR>)
+fn verify_rrsig<'a, RR: WriteableRecord, Keys>(sig: &RRSig, dnskeys: Keys, mut records: Vec<&RR>)
-> Result<(), ValidationError>
where Keys: IntoIterator<Item = &'a DnsKey> {
for record in records.iter() {
if dnskey.flags & 0b1_0000_0000 == 0 { continue; }
if dnskey.alg != sig.alg { continue; }
- let mut signed_data = Vec::with_capacity(2048);
- signed_data.extend_from_slice(&sig.ty.to_be_bytes());
- signed_data.extend_from_slice(&sig.alg.to_be_bytes());
- signed_data.extend_from_slice(&sig.labels.to_be_bytes());
- signed_data.extend_from_slice(&sig.orig_ttl.to_be_bytes());
- signed_data.extend_from_slice(&sig.expiration.to_be_bytes());
- signed_data.extend_from_slice(&sig.inception.to_be_bytes());
- signed_data.extend_from_slice(&sig.key_tag.to_be_bytes());
- write_name(&mut signed_data, &sig.key_name);
+ let mut hash_ctx = match sig.alg {
+ 8 => crypto::hash::Hasher::sha256(),
+ 10 => crypto::hash::Hasher::sha512(),
+ 13 => crypto::hash::Hasher::sha256(),
+ 14 => crypto::hash::Hasher::sha384(),
+ 15 => crypto::hash::Hasher::sha512(),
+ _ => return Err(ValidationError::UnsupportedAlgorithm),
+ };
+
+ hash_ctx.update(&sig.ty.to_be_bytes());
+ hash_ctx.update(&sig.alg.to_be_bytes());
+ hash_ctx.update(&sig.labels.to_be_bytes());
+ hash_ctx.update(&sig.orig_ttl.to_be_bytes());
+ hash_ctx.update(&sig.expiration.to_be_bytes());
+ hash_ctx.update(&sig.inception.to_be_bytes());
+ hash_ctx.update(&sig.key_tag.to_be_bytes());
+ write_name(&mut hash_ctx, &sig.key_name);
records.sort_unstable();
let signed_name = record.name().trailing_n_labels(sig.labels);
debug_assert!(signed_name.is_some());
if let Some(name) = signed_name {
- signed_data.extend_from_slice(b"\x01*");
- write_name(&mut signed_data, name);
+ hash_ctx.update(b"\x01*");
+ write_name(&mut hash_ctx, name);
} else { return Err(ValidationError::Invalid); }
} else {
- write_name(&mut signed_data, record.name());
+ write_name(&mut hash_ctx, record.name());
}
- signed_data.extend_from_slice(&record.ty().to_be_bytes());
- signed_data.extend_from_slice(&1u16.to_be_bytes()); // The INternet class
- signed_data.extend_from_slice(&sig.orig_ttl.to_be_bytes());
- record.write_u16_len_prefixed_data(&mut signed_data);
+ hash_ctx.update(&record.ty().to_be_bytes());
+ hash_ctx.update(&1u16.to_be_bytes()); // The INternet class
+ hash_ctx.update(&sig.orig_ttl.to_be_bytes());
+ record.serialize_u16_len_prefixed(&mut hash_ctx);
}
+ let hash = hash_ctx.finish();
let sig_validation = match sig.alg {
- 8|10 => {
- let alg = if sig.alg == 8 {
- &signature::RSA_PKCS1_1024_8192_SHA256_FOR_LEGACY_USE_ONLY
- } else {
- &signature::RSA_PKCS1_1024_8192_SHA512_FOR_LEGACY_USE_ONLY
- };
- bytes_to_rsa_pk(&dnskey.pubkey).map_err(|_| ValidationError::Invalid)?
- .verify(alg, &signed_data, &sig.signature)
- .map_err(|_| ValidationError::Invalid)
- },
- 13|14 => {
- let alg = if sig.alg == 13 {
- &signature::ECDSA_P256_SHA256_FIXED
- } else {
- &signature::ECDSA_P384_SHA384_FIXED
- };
-
- // Add 0x4 identifier to the ECDSA pubkey as expected by ring.
- let mut key = Vec::with_capacity(dnskey.pubkey.len() + 1);
- key.push(0x4);
- key.extend_from_slice(&dnskey.pubkey);
-
- signature::UnparsedPublicKey::new(alg, &key)
- .verify(&signed_data, &sig.signature)
- .map_err(|_| ValidationError::Invalid)
- },
- 15 => {
- signature::UnparsedPublicKey::new(&signature::ED25519, &dnskey.pubkey)
- .verify(&signed_data, &sig.signature)
- .map_err(|_| ValidationError::Invalid)
- },
+ 8|10 => crypto::rsa::validate_rsa(&dnskey.pubkey, &sig.signature, hash.as_ref())
+ .map_err(|_| ValidationError::Invalid),
+ 13 => crypto::secp256r1::validate_ecdsa(&dnskey.pubkey, &sig.signature, hash.as_ref())
+ .map_err(|_| ValidationError::Invalid),
+ 14 => crypto::secp384r1::validate_ecdsa(&dnskey.pubkey, &sig.signature, hash.as_ref())
+ .map_err(|_| ValidationError::Invalid),
+ // TODO: 15 => ED25519
_ => return Err(ValidationError::UnsupportedAlgorithm),
};
#[cfg(fuzzing)] {
return Ok(());
}
}
+
+ // Note that technically there could be a key tag collision here, causing spurious
+ // verification failure. In most zones, there's only 2-4 DNSKEY entries, meaning a
+ // spurious collision shouldn't be much more often than one every billion zones. Much
+ // more likely in such a case, someone is just trying to do a KeyTrap attack, so we
+ // simply hard-fail and return an error immediately.
sig_validation?;
return Ok(());
Err(ValidationError::Invalid)
}
-fn verify_dnskey_rrsig<'a, T, I>(sig: &RRSig, dses: T, records: Vec<&DnsKey>)
--> Result<(), ValidationError>
-where T: IntoIterator<IntoIter = I>, I: Iterator<Item = &'a DS> + Clone {
+/// Verify [`RRSig`]s over [`DnsKey`], returning a reference to the [`RRSig`] that matched, if any.
+fn verify_dnskeys<'r, 'd, RI, R, DI, D>(sigs: RI, dses: DI, records: Vec<&DnsKey>)
+-> Result<&'r RRSig, ValidationError>
+where RI: IntoIterator<IntoIter = R>, R: Iterator<Item = &'r RRSig>,
+ DI: IntoIterator<IntoIter = D>, D: Iterator<Item = &'d DS> + Clone {
let mut validated_dnskeys = Vec::with_capacity(records.len());
let dses = dses.into_iter();
let mut ctx = match ds.digest_type {
1 if trust_sha1 => crypto::hash::Hasher::sha1(),
2 => crypto::hash::Hasher::sha256(),
- // TODO: 4 => crypto::hash::Hasher::sha384(),
+ 4 => crypto::hash::Hasher::sha384(),
_ => continue,
};
write_name(&mut ctx, &dnskey.name);
}
}
}
- verify_rrsig(sig, validated_dnskeys.iter().map(|k| *k), records)
+
+ let mut found_unsupported_alg = false;
+ for sig in sigs {
+ match verify_rrsig(sig, validated_dnskeys.iter().map(|k| *k), records.clone()) {
+ Ok(()) => return Ok(sig),
+ Err(ValidationError::UnsupportedAlgorithm) => {
+ // There may be redundant signatures by different keys, where one we don't
+ // supprt and another we do. Ignore ones we don't support, but if there are
+ // no more, return UnsupportedAlgorithm
+ found_unsupported_alg = true;
+ },
+ Err(ValidationError::Invalid) => {
+ // If a signature is invalid, just immediately fail, avoiding KeyTrap issues.
+ return Err(ValidationError::Invalid);
+ },
+ }
+ }
+
+ if found_unsupported_alg {
+ Err(ValidationError::UnsupportedAlgorithm)
+ } else {
+ Err(ValidationError::Invalid)
+ }
}
/// Given a set of [`RR`]s, [`verify_rr_stream`] checks what it can and returns the set of
let mut earliest_expiry = u64::MAX;
let mut min_ttl = u32::MAX;
'next_zone: while zone == "." || !pending_ds_sets.is_empty() {
- let mut found_unsupported_alg = false;
let next_ds_set;
if let Some((next_zone, ds_set)) = pending_ds_sets.pop() {
next_ds_set = Some(ds_set);
next_ds_set = None;
}
+ let dnskey_rrsigs = inp.iter()
+ .filter_map(|rr| if let RR::RRSig(sig) = rr { Some(sig) } else { None })
+ .filter(|rrsig| rrsig.name.as_str() == zone && rrsig.ty == DnsKey::TYPE);
+ let dnskeys = inp.iter()
+ .filter_map(|rr| if let RR::DnsKey(dnskey) = rr { Some(dnskey) } else { None })
+ .filter(move |dnskey| dnskey.name.as_str() == zone);
+ let root_hints = root_hints();
+ let verified_dnskey_rrsig = if zone == "." {
+ verify_dnskeys(dnskey_rrsigs, &root_hints, dnskeys.clone().collect())?
+ } else {
+ debug_assert!(next_ds_set.is_some());
+ if next_ds_set.is_none() { break 'next_zone; }
+ verify_dnskeys(dnskey_rrsigs, next_ds_set.clone().unwrap(), dnskeys.clone().collect())?
+ };
+ latest_inception = cmp::max(latest_inception, resolve_time(verified_dnskey_rrsig.inception));
+ earliest_expiry = cmp::min(earliest_expiry, resolve_time(verified_dnskey_rrsig.expiration));
+ min_ttl = cmp::min(min_ttl, verified_dnskey_rrsig.orig_ttl);
for rrsig in inp.iter()
.filter_map(|rr| if let RR::RRSig(sig) = rr { Some(sig) } else { None })
- .filter(|rrsig| rrsig.name.as_str() == zone && rrsig.ty == DnsKey::TYPE)
+ .filter(move |rrsig| rrsig.key_name.as_str() == zone && rrsig.ty != DnsKey::TYPE)
{
- let dnskeys = inp.iter()
- .filter_map(|rr| if let RR::DnsKey(dnskey) = rr { Some(dnskey) } else { None })
- .filter(move |dnskey| dnskey.name.as_str() == zone);
- let dnskeys_verified = if zone == "." {
- verify_dnskey_rrsig(rrsig, &root_hints(), dnskeys.clone().collect())
- } else {
- debug_assert!(next_ds_set.is_some());
- if next_ds_set.is_none() { break 'next_zone; }
- verify_dnskey_rrsig(rrsig, next_ds_set.clone().unwrap(), dnskeys.clone().collect())
- };
- if dnskeys_verified.is_ok() {
- latest_inception = cmp::max(latest_inception, resolve_time(rrsig.inception));
- earliest_expiry = cmp::min(earliest_expiry, resolve_time(rrsig.expiration));
- min_ttl = cmp::min(min_ttl, rrsig.orig_ttl);
- for rrsig in inp.iter()
- .filter_map(|rr| if let RR::RRSig(sig) = rr { Some(sig) } else { None })
- .filter(move |rrsig| rrsig.key_name.as_str() == zone && rrsig.ty != DnsKey::TYPE)
- {
- if !rrsig.name.ends_with(zone) { return Err(ValidationError::Invalid); }
- let signed_records = inp.iter()
- .filter(|rr| rr.name() == &rrsig.name && rr.ty() == rrsig.ty);
- verify_rrsig(rrsig, dnskeys.clone(), signed_records.clone().collect())?;
- latest_inception = cmp::max(latest_inception, resolve_time(rrsig.inception));
- earliest_expiry = cmp::min(earliest_expiry, resolve_time(rrsig.expiration));
- min_ttl = cmp::min(min_ttl, rrsig.orig_ttl);
- match rrsig.ty {
- // RRSigs shouldn't cover child `DnsKey`s or other `RRSig`s
- RRSig::TYPE|DnsKey::TYPE => return Err(ValidationError::Invalid),
- DS::TYPE => {
- if !pending_ds_sets.iter().any(|(pending_zone, _)| pending_zone == &rrsig.name.as_str()) {
- pending_ds_sets.push((
- &rrsig.name,
- signed_records.filter_map(|rr|
- if let RR::DS(ds) = rr { Some(ds) }
- else { debug_assert!(false, "We already filtered by type"); None })
- ));
- }
- },
- _ => {
- if rrsig.labels != rrsig.name.labels() && rrsig.ty != NSec::TYPE {
- if rrsig.ty == NSec3::TYPE {
- // NSEC3 records should never appear on wildcards, so treat the
- // whole proof as invalid
- return Err(ValidationError::Invalid);
- }
- // If the RR used a wildcard, we need an NSEC/NSEC3 proof, which we
- // check for at the end. Note that the proof should be for the
- // "next closest" name, i.e. if the name here is a.b.c and it was
- // signed as *.c, we want a proof for nothing being in b.c.
- // Alternatively, if it was signed as *.b.c, we'd want a proof for
- // a.b.c.
- let proof_name = rrsig.name.trailing_n_labels(rrsig.labels + 1)
- .ok_or(ValidationError::Invalid)?;
- rrs_needing_non_existence_proofs.push((proof_name, &rrsig.key_name, rrsig.ty));
- }
- for record in signed_records {
- if !res.contains(&record) { res.push(record); }
- }
- },
- }
+ if !rrsig.name.ends_with(zone) { return Err(ValidationError::Invalid); }
+ let signed_records = inp.iter()
+ .filter(|rr| rr.name() == &rrsig.name && rr.ty() == rrsig.ty);
+ match verify_rrsig(rrsig, dnskeys.clone(), signed_records.clone().collect()) {
+ Ok(()) => {},
+ Err(ValidationError::UnsupportedAlgorithm) => continue,
+ Err(ValidationError::Invalid) => {
+ // If a signature is invalid, just immediately fail, avoiding KeyTrap issues.
+ return Err(ValidationError::Invalid);
}
- continue 'next_zone;
- } else if dnskeys_verified == Err(ValidationError::UnsupportedAlgorithm) {
- // There may be redundant signatures by different keys, where one we don't supprt
- // and another we do. Ignore ones we don't support, but if there are no more,
- // return UnsupportedAlgorithm
- found_unsupported_alg = true;
- } else {
- // We don't explicitly handle invalid signatures here, instead we move on to the
- // next RRSig (if there is one) and return `Invalid` if no `RRSig`s match.
+ }
+ latest_inception = cmp::max(latest_inception, resolve_time(rrsig.inception));
+ earliest_expiry = cmp::min(earliest_expiry, resolve_time(rrsig.expiration));
+ min_ttl = cmp::min(min_ttl, rrsig.orig_ttl);
+ match rrsig.ty {
+ // RRSigs shouldn't cover child `DnsKey`s or other `RRSig`s
+ RRSig::TYPE|DnsKey::TYPE => return Err(ValidationError::Invalid),
+ DS::TYPE => {
+ if !pending_ds_sets.iter().any(|(pending_zone, _)| pending_zone == &rrsig.name.as_str()) {
+ pending_ds_sets.push((
+ &rrsig.name,
+ signed_records.filter_map(|rr|
+ if let RR::DS(ds) = rr { Some(ds) }
+ else { debug_assert!(false, "We already filtered by type"); None })
+ ));
+ }
+ },
+ _ => {
+ if rrsig.labels != rrsig.name.labels() && rrsig.ty != NSec::TYPE {
+ if rrsig.ty == NSec3::TYPE {
+ // NSEC3 records should never appear on wildcards, so treat the
+ // whole proof as invalid
+ return Err(ValidationError::Invalid);
+ }
+ // If the RR used a wildcard, we need an NSEC/NSEC3 proof, which we
+ // check for at the end. Note that the proof should be for the
+ // "next closest" name, i.e. if the name here is a.b.c and it was
+ // signed as *.c, we want a proof for nothing being in b.c.
+ // Alternatively, if it was signed as *.b.c, we'd want a proof for
+ // a.b.c.
+ let proof_name = rrsig.name.trailing_n_labels(rrsig.labels + 1)
+ .ok_or(ValidationError::Invalid)?;
+ rrs_needing_non_existence_proofs.push((proof_name, &rrsig.key_name, rrsig.ty));
+ }
+ for record in signed_records {
+ if !res.contains(&record) { res.push(record); }
+ }
+ },
}
}
- // No RRSigs were able to verify our DnsKey set
- if found_unsupported_alg {
- return Err(ValidationError::UnsupportedAlgorithm);
- } else {
- return Err(ValidationError::Invalid);
- }
+ continue 'next_zone;
}
if res.is_empty() { return Err(ValidationError::Invalid) }
if latest_inception >= earliest_expiry { return Err(ValidationError::Invalid) }
signature: base64::decode("GIgwndRLXgt7GX/JNEqSvpYw5ij6EgeQivdC/hmNNuOd2MCQRSxZx2DdLZUoK0tmn2XmOd0vYP06DgkIMUpIXcBstw/Um55WQhvBkBTPIhuB3UvKYJstmq+8hFHWVJwKHTg9xu38JA43VgCV2AbzurbzNOLSgq+rDPelRXzpLr5aYE3y+EuvL+I5gusm4MMajnp5S+ioWOL+yWOnQE6XKoDmlrfcTrYfRSxRtJewPmGeCbNdwEUBOoLUVdkCjQG4uFykcKL40cY8EOhVmM3kXAyuPuNe2Xz1QrIcVad/U4FDns+hd8+W+sWnr8QAtIUFT5pBjXooGS02m6eMdSeU6g==").unwrap(),
};
let root_hints = root_hints();
- verify_dnskey_rrsig(&dnskey_rrsig, &root_hints, dnskeys.iter().collect()).unwrap();
+ verify_dnskeys([&dnskey_rrsig], &root_hints, dnskeys.iter().collect()).unwrap();
let rrs = vec![dnskeys[0].clone().into(), dnskeys[1].clone().into(), dnskey_rrsig.into()];
(dnskeys, rrs)
}
expiration: 1710342155, inception: 1709045855, key_tag: 19718, key_name: "com.".try_into().unwrap(),
signature: base64::decode("lF2B9nXZn0CgytrHH6xB0NTva4G/aWvg/ypnSxJ8+ZXlvR0C4974yB+nd2ZWzWMICs/oPYMKoQHqxVjnGyu8nA==").unwrap(),
};
- verify_dnskey_rrsig(&dnskey_rrsig, &com_ds, dnskeys.iter().collect()).unwrap();
+ verify_dnskeys([&dnskey_rrsig], &com_ds, dnskeys.iter().collect()).unwrap();
let rrs = vec![com_ds.pop().unwrap().into(), ds_rrsig.into(),
dnskeys[0].clone().into(), dnskeys[1].clone().into(), dnskey_rrsig.into()];
(dnskeys, rrs)
expiration: 1710689605, inception: 1708871605, key_tag: 46082, key_name: "ninja.".try_into().unwrap(),
signature: base64::decode("kYxV1z+9Ikxqbr13N+8HFWWnAUcvHkr/dmkdf21mliUhH4cxeYCXC6a95X+YzjYQEQi3fU+S346QBDJkbFYCca5q/TzUdE7ej1B/0uTzhgNrQznm0O6sg6DI3HuqDfZp2oaBQm2C/H4vjkcUW9zxgKP8ON0KKLrZUuYelGazeGSOscjDDlmuNMD7tHhFrmK9BiiX+8sp8Cl+IE5ArP+CPXsII+P+R2QTmTqw5ovJch2FLRMRqCliEzTR/IswBI3FfegZR8h9xJ0gfyD2rDqf6lwJhD1K0aS5wxia+bgzpRIKwiGfP87GDYzkygHr83QbmZS2YG1nxlnQ2rgkqTGgXA==").unwrap(),
};
- verify_dnskey_rrsig(&dnskey_rrsig, &ninja_ds, dnskeys.iter().collect()).unwrap();
+ verify_dnskeys([&dnskey_rrsig], &ninja_ds, dnskeys.iter().collect()).unwrap();
let rrs = vec![ninja_ds.pop().unwrap().into(), ds_rrsig.into(),
dnskeys[0].clone().into(), dnskeys[1].clone().into(), dnskeys[2].clone().into(),
dnskey_rrsig.into()];
expiration:1710262250, inception: 1709047250, key_tag: 25630, key_name: "mattcorallo.com.".try_into().unwrap(),
signature: base64::decode("dMLDvNU96m+tfgpDIQPxMBJy7T0xyZDj3Wws4b4E6+g3nt5iULdWJ8Eqrj+86KLerOVt7KH4h/YcHP18hHdMGA==").unwrap(),
};
- verify_dnskey_rrsig(&dnskey_rrsig, &mattcorallo_ds, dnskeys.iter().collect()).unwrap();
+ verify_dnskeys([&dnskey_rrsig], &mattcorallo_ds, dnskeys.iter().collect()).unwrap();
let rrs = vec![mattcorallo_ds.pop().unwrap().into(), ds_rrsig.into(),
dnskeys[0].clone().into(), dnskeys[1].clone().into(), dnskeys[2].clone().into(),
dnskey_rrsig.into()];
expiration: 1709947337, inception: 1708732337, key_tag: 63175, key_name: "bitcoin.ninja.".try_into().unwrap(),
signature: base64::decode("Y3To5FZoZuBDUMtIBZXqzRtufyRqOlDqbHVcoZQitXxerCgNQ1CsVdmoFVMmZqRV5n4itINX2x+9G/31j410og==").unwrap(),
};
- verify_dnskey_rrsig(&dnskey_rrsig, &bitcoin_ninja_ds, dnskeys.iter().collect()).unwrap();
+ verify_dnskeys([&dnskey_rrsig], &bitcoin_ninja_ds, dnskeys.iter().collect()).unwrap();
let rrs = vec![bitcoin_ninja_ds.pop().unwrap().into(), ds_rrsig.into(),
dnskeys[0].clone().into(), dnskeys[1].clone().into(), dnskey_rrsig.into()];
(dnskeys, rrs)
key_tag: 8036, key_name: "nsec_tests.dnssec_proof_tests.bitcoin.ninja.".try_into().unwrap(),
signature: base64::decode("nX+hkH14Kvjp26Z8x/pjYh5CQW3p9lZQQ+FVJcKHyfjAilEubpw6ihlPpb3Ddh9BbyxhCEFhXDMG2g4od9Y2ow==").unwrap(),
};
- verify_dnskey_rrsig(&dnskey_rrsig, &bitcoin_ninja_ds, dnskeys.iter().collect()).unwrap();
+ verify_dnskeys([&dnskey_rrsig], &bitcoin_ninja_ds, dnskeys.iter().collect()).unwrap();
let rrs = vec![bitcoin_ninja_ds.pop().unwrap().into(), ds_rrsig.into(),
dnskeys[0].clone().into(), dnskeys[1].clone().into(), dnskey_rrsig.into()];
(dnskeys, rrs)