fixed nsec3 hashing and base32hex encoding

This commit is contained in:
Benjamin Fry 2016-03-30 21:25:18 -07:00
parent 79a9a4f2ca
commit 4c62e0349a
8 changed files with 313 additions and 57 deletions

View File

@ -5,6 +5,11 @@ This project adheres to [Semantic Versioning](http://semver.org/).
## [unreleased]
### Added
- NSEC3 resolver validation
- data-ecoding as a dependency (base32hex)
- trust-dns banner on boot of server
### Changed
- Changed the bin.rs to named.rs, more accurate, allow for other binaries
## 0.5.0 2016-03-22
### Added

6
Cargo.lock generated
View File

@ -3,6 +3,7 @@ name = "trust-dns"
version = "0.5.0"
dependencies = [
"chrono 0.2.20 (registry+https://github.com/rust-lang/crates.io-index)",
"data-encoding 1.1.2 (registry+https://github.com/rust-lang/crates.io-index)",
"docopt 0.6.78 (registry+https://github.com/rust-lang/crates.io-index)",
"log 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)",
"mio 0.4.4 (registry+https://github.com/rust-lang/crates.io-index)",
@ -44,6 +45,11 @@ dependencies = [
"time 0.1.34 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "data-encoding"
version = "1.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "docopt"
version = "0.6.78"

View File

@ -40,7 +40,7 @@ path = "src/lib.rs"
[[bin]]
name = "named"
path = "src/bin.rs"
path = "src/named.rs"
[profile.dev]
@ -62,11 +62,12 @@ debug-assertions = true
codegen-units = 1
[dependencies]
rustc-serialize = "^0.3.16"
log = "^0.3.1"
docopt = "^0.6.72"
mio = "^0.4.4"
toml = "^0.1.22"
chrono = "^0.2.16"
data-encoding = "^1.1.2"
docopt = "^0.6.72"
log = "^0.3.1"
mio = "^0.4.4"
openssl = "^0.7.8"
openssl-sys = "^0.7.8"
rustc-serialize = "^0.3.16"
toml = "^0.1.22"

View File

@ -14,7 +14,9 @@
use std::cell::{Cell, RefCell};
use std::collections::HashSet;
use std::sync::Arc as Rc;
use data_encoding::base32hex;
use openssl::crypto::pkey::Role;
use ::error::*;
@ -103,6 +105,7 @@ impl<C: ClientConnection> Client<C> {
},
rt @ RecordType::NSEC3 => {
try!(self.verify_nsec3(query_name, query_type, query_class,
record_response.get_name_servers().iter().filter(|rr| rr.get_rr_type() == RecordType::SOA).next(),
record_response.get_name_servers().iter().filter(|rr| rr.get_rr_type() == rt).collect()));
validated_nx = true;
},
@ -325,11 +328,119 @@ impl<C: ClientConnection> Client<C> {
Err(ClientError::InvalidNsec)
}
// Laurie, et al. Standards Track [Page 22]
//
// RFC 5155 NSEC3 March 2008
//
//
// 8. Validator Considerations
//
// 8.1. Responses with Unknown Hash Types
//
// A validator MUST ignore NSEC3 RRs with unknown hash types. The
// practical result of this is that responses containing only such NSEC3
// RRs will generally be considered bogus.
//
// 8.2. Verifying NSEC3 RRs
//
// A validator MUST ignore NSEC3 RRs with a Flag fields value other than
// zero or one.
//
// A validator MAY treat a response as bogus if the response contains
// NSEC3 RRs that contain different values for hash algorithm,
// iterations, or salt from each other for that zone.
//
// 8.3. Closest Encloser Proof
//
// In order to verify a closest encloser proof, the validator MUST find
// the longest name, X, such that
//
// o X is an ancestor of QNAME that is matched by an NSEC3 RR present
// in the response. This is a candidate for the closest encloser,
// and
//
// o The name one label longer than X (but still an ancestor of -- or
// equal to -- QNAME) is covered by an NSEC3 RR present in the
// response.
//
// One possible algorithm for verifying this proof is as follows:
//
// 1. Set SNAME=QNAME. Clear the flag.
//
// 2. Check whether SNAME exists:
//
// * If there is no NSEC3 RR in the response that matches SNAME
// (i.e., an NSEC3 RR whose owner name is the same as the hash of
// SNAME, prepended as a single label to the zone name), clear
// the flag.
//
// * If there is an NSEC3 RR in the response that covers SNAME, set
// the flag.
//
// * If there is a matching NSEC3 RR in the response and the flag
// was set, then the proof is complete, and SNAME is the closest
// encloser.
//
// * If there is a matching NSEC3 RR in the response, but the flag
// is not set, then the response is bogus.
//
// 3. Truncate SNAME by one label from the left, go to step 2.
//
// Once the closest encloser has been discovered, the validator MUST
// check that the NSEC3 RR that has the closest encloser as the original
// owner name is from the proper zone. The DNAME type bit must not be
// set and the NS type bit may only be set if the SOA type bit is set.
// If this is not the case, it would be an indication that an attacker
// is using them to falsely deny the existence of RRs for which the
// server is not authoritative.
//
// In the following descriptions, the phrase "a closest (provable)
// encloser proof for X" means that the algorithm above (or an
// equivalent algorithm) proves that X does not exist by proving that an
// ancestor of X is its closest encloser.
fn verify_nsec3(&self, query_name: &domain::Name, query_type: RecordType,
query_class: DNSClass, nsec3: Vec<&Record>) -> ClientResult<()> {
query_class: DNSClass, soa: Option<&Record>,
nsec3s: Vec<&Record>) -> ClientResult<()> {
// the search name is the one to look for
let zone_name = try!(soa.ok_or(ClientError::NoSOARecord(query_name.clone()))).get_name();
unimplemented!();
println!("nsec3s: {:?}", nsec3s);
for nsec3 in nsec3s {
// for each nsec3 we search for matching hashed names
let mut search_name: domain::Name = query_name.clone();
// hash the search name
if let &RData::NSEC3{hash_algorithm, opt_out, iterations, ref salt, ref type_bit_maps, ..} = nsec3.get_rdata() {
println!("nsec3_name: {}", nsec3.get_name());
// search all the name options
while search_name.num_labels() >= zone_name.num_labels() {
// TODO: cache hashes across nsec3 validations
let hash = hash_algorithm.hash(salt, &search_name, iterations);
let hash_label = base32hex::encode(&hash).to_lowercase();
let hash_name = zone_name.prepend_label(Rc::new(hash_label));
println!("hash_name: {}", hash_name);
if &hash_name == nsec3.get_name() {
// like nsec, if there is a name that matches, then we have proof that the name does
// not exist
if &search_name == query_name {
if !type_bit_maps.contains(&query_type) { return Ok(()) }
}
return Ok(())
}
// need to continue up the chain
search_name = search_name.base_name();
}
}
}
Err(ClientError::InvalidNsec3)
}
// send a DNS query to the name_server specified in Client.
@ -540,37 +651,19 @@ mod test {
assert!(response.get_response_code() == ResponseCode::NXDomain);
}
// // TODO: use this site for verifying nsec3
// #[test]
// #[cfg(feature = "ftest")]
// fn test_secure_query_sdsmt() {
// use std::net::*;
//
// use ::rr::dns_class::DNSClass;
// use ::rr::record_type::RecordType;
// use ::rr::domain;
// use ::rr::record_data::RData;
// use ::udp::Client;
//
// let name = domain::Name::with_labels(vec!["www".to_string(), "sdsmt".to_string(), "edu".to_string()]);
// let client = Client::new(("8.8.8.8").parse().unwrap()).unwrap();
//
// let response = client.secure_query(&name, DNSClass::IN, RecordType::A);
// assert!(response.is_ok(), "query failed: {}", response.unwrap_err());
//
// let response = response.unwrap();
//
// println!("response records: {:?}", response);
//
// let record = &response.get_answers()[0];
// assert_eq!(record.get_name(), &name);
// assert_eq!(record.get_rr_type(), RecordType::A);
// assert_eq!(record.get_dns_class(), DNSClass::IN);
//
// if let &RData::A{ ref address } = record.get_rdata() {
// assert_eq!(address, &Ipv4Addr::new(93,184,216,34))
// } else {
// assert!(false);
// }
// }
// TODO: use this site for verifying nsec3
#[test]
#[cfg(feature = "ftest")]
fn test_secure_query_sdsmt() {
let addr: SocketAddr = ("75.75.75.75",53).to_socket_addrs().unwrap().next().unwrap();
let conn = TcpClientConnection::new(addr).unwrap();
let name = domain::Name::with_labels(vec!["www".to_string(), "sdsmt".to_string(), "edu".to_string()]);
let client = Client::new(conn);
let response = client.secure_query(&name, DNSClass::IN, RecordType::NS);
assert!(response.is_ok(), "query failed: {}", response.unwrap_err());
let response = response.unwrap();
assert!(response.get_response_code() == ResponseCode::NXDomain);
}
}

View File

@ -14,12 +14,14 @@
* limitations under the License.
*/
extern crate chrono;
extern crate data_encoding;
#[macro_use] extern crate log;
extern crate mio;
extern crate openssl;
extern crate openssl_sys;
extern crate toml;
extern crate rustc_serialize;
extern crate toml;
pub mod logger;
pub mod rr;

View File

@ -130,9 +130,9 @@ pub fn main() {
for tcp_listener in tcp_listeners {
info!("listening for TCP on {:?}", tcp_listener);
server.register_listener(tcp_listener);
}
banner();
if let Err(e) = server.listen() {
error!("failed to listen: {}", e);
}
@ -143,3 +143,12 @@ pub fn main() {
// we're exiting for some reason...
info!("Trust-DNS {} stopping", trust_dns::version());
}
fn banner() {
println!(" o o o ");
println!(" | | | ");
println!(" -o- o-o o o o-o -o- o-o o-O o-o o-o ");
println!(" | | | | \\ | | | | | \\ ");
println!(" o o o--o o-o o o-o o o o-o ");
println!(" ");
}

View File

@ -13,7 +13,13 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
use std::io::Write;
use openssl::crypto::hash;
use ::error::*;
use ::rr::dnssec::DigestType;
use ::rr::Name;
use ::serialize::binary::{BinEncoder, BinSerializable};
// RFC 5155 NSEC3 March 2008
//
@ -62,12 +68,6 @@ use ::error::*;
// registry is named "DNSSEC NSEC3PARAM Flags". The initial contents of
// this registry are:
//
//
//
// Laurie, et al. Standards Track [Page 29]
//
//
//
// 0 1 2 3 4 5 6 7
// +---+---+---+---+---+---+---+---+
// | | | | | | | | 0 |
@ -106,8 +106,65 @@ impl Nsec3HashAlgorithm {
_ => Err(DecodeError::UnknownAlgorithmTypeValue(value)),
}
}
}
// Laurie, et al. Standards Track [Page 14]
//
// RFC 5155 NSEC3 March 2008
//
// Define H(x) to be the hash of x using the Hash Algorithm selected by
// the NSEC3 RR, k to be the number of Iterations, and || to indicate
// concatenation. Then define:
//
// IH(salt, x, 0) = H(x || salt), and
//
// IH(salt, x, k) = H(IH(salt, x, k-1) || salt), if k > 0
//
// Then the calculated hash of an owner name is
//
// IH(salt, owner name, iterations),
//
// where the owner name is in the canonical form, defined as:
//
// The wire format of the owner name where:
//
// 1. The owner name is fully expanded (no DNS name compression) and
// fully qualified;
//
// 2. All uppercase US-ASCII letters are replaced by the corresponding
// lowercase US-ASCII letters;
//
// 3. If the owner name is a wildcard name, the owner name is in its
// original unexpanded form, including the "*" label (no wildcard
// substitution);
pub fn hash(&self, salt: &[u8], name: &Name, iterations: u16) -> Vec<u8> {
match *self {
// if there ever is more than just SHA1 support, this should be a genericized method
Nsec3HashAlgorithm::SHA1 => {
let mut buf: Vec<u8> = Vec::new();
{
let mut encoder: BinEncoder = BinEncoder::new(&mut buf);
encoder.set_canonical_names(true);
name.emit(&mut encoder).expect("could not encode Name");
}
Self::sha1_recursive_hash(salt, buf, iterations)
},
}
}
// until there is another supported algorithm, just hardcoded to this.
fn sha1_recursive_hash(salt: &[u8], bytes: Vec<u8>, iterations: u16) -> Vec<u8> {
let mut hasher: hash::Hasher = hash::Hasher::new(DigestType::SHA1.to_hash());
if iterations > 0 {
hasher.write_all(&Self::sha1_recursive_hash(salt, bytes, iterations - 1)).expect("hasher failed");
} else {
hasher.write_all(&bytes).expect("hasher failed");
}
hasher.write_all(salt).expect("hasher failed");
hasher.finish()
}
}
impl From<Nsec3HashAlgorithm> for u8 {
fn from(a: Nsec3HashAlgorithm) -> u8 {
@ -116,3 +173,61 @@ impl From<Nsec3HashAlgorithm> for u8 {
}
}
}
#[test]
fn test_hash() {
let name = Name::new().label("www").label("example").label("com");
let salt: Vec<u8> = vec![1,2,3,4];
assert_eq!(Nsec3HashAlgorithm::SHA1.hash(&salt, &name, 0).len(), 20);
assert_eq!(Nsec3HashAlgorithm::SHA1.hash(&salt, &name, 1).len(), 20);
assert_eq!(Nsec3HashAlgorithm::SHA1.hash(&salt, &name, 3).len(), 20);
}
#[test]
fn test_known_hashes() {
// H(example) = 0p9mhaveqvm6t7vbl5lop2u3t2rp3tom
assert_eq!(hash_with_base32("example"), "0p9mhaveqvm6t7vbl5lop2u3t2rp3tom");
// H(a.example) = 35mthgpgcu1qg68fab165klnsnk3dpvl
assert_eq!(hash_with_base32("a.example"), "35mthgpgcu1qg68fab165klnsnk3dpvl");
// H(ai.example) = gjeqe526plbf1g8mklp59enfd789njgi
assert_eq!(hash_with_base32("ai.example"), "gjeqe526plbf1g8mklp59enfd789njgi");
// H(ns1.example) = 2t7b4g4vsa5smi47k61mv5bv1a22bojr
assert_eq!(hash_with_base32("ns1.example"), "2t7b4g4vsa5smi47k61mv5bv1a22bojr");
// H(ns2.example) = q04jkcevqvmu85r014c7dkba38o0ji5r
assert_eq!(hash_with_base32("ns2.example"), "q04jkcevqvmu85r014c7dkba38o0ji5r");
// H(w.example) = k8udemvp1j2f7eg6jebps17vp3n8i58h
assert_eq!(hash_with_base32("w.example"), "k8udemvp1j2f7eg6jebps17vp3n8i58h");
// H(*.w.example) = r53bq7cc2uvmubfu5ocmm6pers9tk9en
assert_eq!(hash_with_base32("*.w.example"), "r53bq7cc2uvmubfu5ocmm6pers9tk9en");
// H(x.w.example) = b4um86eghhds6nea196smvmlo4ors995
assert_eq!(hash_with_base32("x.w.example"), "b4um86eghhds6nea196smvmlo4ors995");
// H(y.w.example) = ji6neoaepv8b5o6k4ev33abha8ht9fgc
assert_eq!(hash_with_base32("y.w.example"), "ji6neoaepv8b5o6k4ev33abha8ht9fgc");
// H(x.y.w.example) = 2vptu5timamqttgl4luu9kg21e0aor3s
assert_eq!(hash_with_base32("x.y.w.example"), "2vptu5timamqttgl4luu9kg21e0aor3s");
// H(xx.example) = t644ebqk9bibcna874givr6joj62mlhv
assert_eq!(hash_with_base32("xx.example"), "t644ebqk9bibcna874givr6joj62mlhv");
}
#[cfg(test)]
fn hash_with_base32(name: &str) -> String {
use data_encoding::base32hex;
// NSEC3PARAM 1 0 12 aabbccdd
let known_name = Name::parse(name, Some(&Name::new())).unwrap();
let known_salt = [0xAAu8, 0xBBu8, 0xCCu8, 0xDDu8,];
let hash = Nsec3HashAlgorithm::SHA1.hash(&known_salt, &known_name, 12);
base32hex::encode(&hash).to_lowercase()
}

View File

@ -44,31 +44,46 @@ impl Name {
self.labels.is_empty()
}
// inline builder
/// inline builder
pub fn label(mut self, label: &'static str) -> Self {
// TODO get_mut() on Arc was unstable when this was written
let mut new_labels: Vec<Rc<String>> = (*self.labels).clone();
new_labels.push(Rc::new(label.into()));
self.labels = Rc::new(new_labels);
assert!(self.labels.len() < 256);
assert!(self.labels.len() < 256); // this should be an error
self
}
// for mutating over time
/// for mutating over time
pub fn with_labels(labels: Vec<String>) -> Self {
assert!(labels.len() < 256);
assert!(labels.len() < 256); // this should be an error
Name { labels: Rc::new(labels.into_iter().map(|s|Rc::new(s)).collect()) }
}
/// prepend the String to the label
pub fn prepend_label(&self, label: Rc<String>) -> Self {
let mut new_labels: Vec<Rc<String>> = Vec::with_capacity(self.labels.len() + 1);
new_labels.push(label);
for label in &*self.labels {
new_labels.push(label.clone());
}
assert!(new_labels.len() < 256); // this should be an error
Name{ labels: Rc::new(new_labels) }
}
/// appends the String to this label at the end
pub fn add_label(&mut self, label: Rc<String>) -> &mut Self {
// TODO get_mut() on Arc was unstable when this was written
let mut new_labels: Vec<Rc<String>> = (*self.labels).clone();
new_labels.push(label);
self.labels = Rc::new(new_labels);
assert!(self.labels.len() < 256);
assert!(self.labels.len() < 256); // this should be an error
self
}
/// appends the other to this name
pub fn append(&mut self, other: &Self) -> &mut Self {
for rcs in &*other.labels {
self.add_label(rcs.clone());
@ -394,9 +409,11 @@ enum LabelParseState {
#[cfg(test)]
mod tests {
use super::*;
use std::sync::Arc as Rc;
use std::cmp::Ordering;
use ::serialize::binary::bin_tests::{test_read_data_set, test_emit_data_set};
use ::serialize::binary::*;
use std::cmp::Ordering;
fn get_data() -> Vec<(Name, Vec<u8>)> {
vec![
@ -468,6 +485,14 @@ mod tests {
assert!(zone.base_name().base_name().base_name().is_root());
}
#[test]
fn test_prepend() {
let zone = Name::new().label("example").label("com");
let www = zone.prepend_label(Rc::new("www".to_string()));
assert_eq!(www, Name::new().label("www").label("example").label("com"));
}
#[test]
fn test_zone_of() {
let zone = Name::new().label("example").label("com");