Skip to content

Commit

Permalink
Merge branch 'dnssec-integration' into develop
Browse files Browse the repository at this point in the history
  • Loading branch information
Litr0 committed Aug 7, 2024
2 parents be0552f + 25d579b commit 50a689f
Show file tree
Hide file tree
Showing 27 changed files with 2,272 additions and 249 deletions.
593 changes: 439 additions & 154 deletions Cargo.lock

Large diffs are not rendered by default.

5 changes: 5 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,11 @@ tokio-stream = "0.1"
thiserror = "1.0.20"
futures-util = "0.3.28"
async-trait = "0.1.77"
sha2 = "0.10.2"
hmac = "0.12.1"
rust-crypto = "0.2"
base64 = "0.22.1"

lru = "0.12.3"

[lib]
Expand Down
2 changes: 0 additions & 2 deletions src/async_resolver.rs
Original file line number Diff line number Diff line change
Expand Up @@ -313,7 +313,6 @@ impl AsyncResolver {
/// answer section, it is always preferred.
fn store_data_cache(&self, response: DnsMessage) {
let truncated = response.get_header().get_tc();
let rcode = response.get_header().get_rcode();
{
let mut cache = self.cache.lock().unwrap();
cache.timeout();
Expand Down Expand Up @@ -2005,7 +2004,6 @@ mod async_resolver_test {

resolver.save_negative_answers(dns_response.clone());

let rrtype_search = Rrtype::A;
assert_eq!(dns_response.get_answer().len(), 0);
assert_eq!(dns_response.get_additional().len(), 1);
assert_eq!(
Expand Down
49 changes: 48 additions & 1 deletion src/async_resolver/config.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
use crate::client::{udp_connection::ClientUDPConnection, tcp_connection::ClientTCPConnection,client_connection::ClientConnection };
use crate::client::client_connection::ConnectionProtocol;
use crate::message::DnsMessage;
use crate::tsig::tsig_algorithm::TsigAlgorithm;
use std::cmp::max;
use std::option;
use std::{net::{IpAddr,SocketAddr,Ipv4Addr}, time::Duration};

use super::server_info::ServerInfo;
Expand Down Expand Up @@ -73,6 +73,14 @@ pub struct ResolverConfig {
ends0_flags: u16,
/// edns0 options for the resolver.
ends0_options: Vec<u16>,
/// This is whether tsig is enabled or not.
tsig: bool,
/// This is the tsig keyname for the resolver.
key_name: Option<String>,
/// This is the tsig key for the resolver.
key: Vec<u8>,
/// algorithm for the tsig key
algorithm: TsigAlgorithm,
}

impl ResolverConfig {
Expand Down Expand Up @@ -113,6 +121,10 @@ impl ResolverConfig {
ends0_version: 0,
ends0_flags: 0,
ends0_options: Vec::new(),
tsig: false,
key_name: None,
key: Vec::new(),
algorithm: TsigAlgorithm::HmacSha256,
};
resolver_config
}
Expand Down Expand Up @@ -154,6 +166,10 @@ impl ResolverConfig {
ends0_version: 0,
ends0_flags: 0,
ends0_options: Vec::new(),
tsig: false,
key_name: None,
key: Vec::new(),
algorithm: TsigAlgorithm::HmacSha256,
};
resolver_config
}
Expand Down Expand Up @@ -245,6 +261,37 @@ impl ResolverConfig {
message.add_edns0(Some(self.get_max_payload()), self.get_ends0_version(), self.get_ends0_flags(), Some(self.get_ends0_options()));
}
}

/// add tsig to the resolver
///
/// # Examples
/// ```
/// let mut resolver_config = ResolverConfig::default();
/// resolver_config.add_tsig("keyname".to_string(), b"key".to_vec(), Some(TsigAlgorithm::HmacSha256));
/// ```
pub fn add_tsig(&mut self, key_name: String, key: Vec<u8>, algorithm: Option<TsigAlgorithm>) {
self.tsig = true;
self.key_name = Some(key_name);
self.key = key;
if let Some(algorithm) = algorithm {
self.algorithm = algorithm;
}
}

/// add tsig from the resolver to a dns message
///
/// # Examples
/// ```
/// let mut resolver_config = ResolverConfig::default();
/// resolver_config.add_tsig("keyname".to_string(), b"key".to_vec(), Some(TsigAlgorithm::HmacSha256));
/// let message = Message::new();
/// resolver_config.add_tsig_to_message(&message, 300, vec![]);
/// ```
pub fn add_tsig_to_message(&self, message: &mut DnsMessage, fudge: u16, mac_request: Vec<u8>) {
if self.tsig {
message.add_tsig(self.key.clone(), self.algorithm.clone(), fudge, self.key_name.clone(), mac_request);
}
}
}

///Getters
Expand Down
4 changes: 4 additions & 0 deletions src/dnssec.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
pub mod dnssec_message;
pub mod dnssec_message_processing;
pub mod dnssec_fetch;
pub mod rrset_signature;
20 changes: 20 additions & 0 deletions src/dnssec/dnssec_fetch.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
use crate::message::DnsMessage;
use crate::message::rdata::Rdata;
use crate::message::rdata::dnskey_rdata::DnskeyRdata;
use crate::message::resource_record::ResourceRecord;
use crate::dnssec::dnssec_message_processing::extract_dnssec_records;
use crate::dnssec::rrset_signature::{verify_rrsig, verify_ds};

use crate::client::client_error::ClientError;

pub async fn fetch_dnskey_records(dns_response: &DnsMessage) -> Result<Vec<DnskeyRdata>, ClientError> {
let mut dnskey_records = Vec::new();

for record in dns_response.get_answer() {
if let Rdata::DNSKEY(dnskey) = &record.get_rdata() {
dnskey_records.push(dnskey.clone());
}
}

Ok(dnskey_records)
}
86 changes: 86 additions & 0 deletions src/dnssec/dnssec_message.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
use std::str::FromStr;
use crate::domain_name::DomainName;
use crate::message::rclass::Rclass;
use crate::message::DnsMessage;
use crate::message::rdata::opt_rdata::OptRdata;
use crate::message::rdata::Rdata;
use crate::message::resource_record::{FromBytes, ResourceRecord, ToBytes};
use crate::message::rcode;
use crate::message::rcode::Rcode;
use crate::message::rrtype::Rrtype;

const EDNS_VERSION: u8 = 0;
const REQUESTED_UDP_LEN: u16 = 4096;
/*
The mechanism chosen for the explicit notification of the ability of
the client to accept (if not understand) DNSSEC security RRs is using
the most significant bit of the Z field on the EDNS0 OPT header in
the query. This bit is referred to as the "DNSSEC OK" (DO) bit. In
the context of the EDNS0 OPT meta-RR, the DO bit is the first bit of
the third and fourth bytes of the "extended RCODE and flags" portion
of the EDNS0 OPT meta-RR, structured as follows:
+0 (MSB) +1 (LSB)
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
0: | EXTENDED-RCODE | VERSION |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
2: |DO| Z |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
*/
fn create_opt_rr(capacity: u16 ,e_rcode :u8, version: u8, do_bit: bool) -> ResourceRecord {
let opt_rdata = OptRdata::new();
let rdata = Rdata::OPT(opt_rdata);
let mut rr = ResourceRecord::new(rdata);

let do_val: u16 = if do_bit {0x8000} else {0x0};
let ttl: u32 = (e_rcode as u32) << 24 | (version as u32) << 16| (do_val as u32);
rr.set_ttl(ttl);
rr.set_rclass(Rclass::UNKNOWN(capacity));
println!("EL ttl es: {:#05x?}", ttl);
rr
}

fn read_opt_rr(opt_rr: ResourceRecord) -> String {
let requested_udp_len = Rclass::from(opt_rr.get_rclass());
let data = opt_rr.get_ttl().to_be_bytes();
let (e_rcode, version) = (data[0], data[1]);
let z = u16::from_be_bytes([data[2], data[3]]);

let do_bit = ((z & 0x8000) > 0) as u8 ;
format!("OPT PSEUDO-RR\n\trequested_udp_len: {requested_udp_len}\n\terror code: {e_rcode}\n\tversion: EDNS{version}\n\tuse dnssec: {do_bit}")
}

/*
A security-aware resolver MUST include an EDNS ([RFC2671]) OPT
pseudo-RR with the DO ([RFC3225]) bit set when sending queries.
*/
fn create_dns_message_with_dnssec(mut msg: DnsMessage) -> DnsMessage {
// We create a opt rr with the do bit set to 1
// with NOERR as rcode and EDNS0
let rr = create_opt_rr(REQUESTED_UDP_LEN,
Rcode::from(Rcode::NOERROR).into(),
EDNS_VERSION,
true);

let vec = vec![rr];
msg.add_additionals(vec);
msg
}

#[test]
fn see_dnssec_message() {
let query = DnsMessage::new_query_message(
DomainName::new_from_str("example.com"),
Rrtype::A,
Rclass::UNKNOWN(4096),
1,
true,
2000
);
let query= create_dns_message_with_dnssec(query);
assert_eq!(String::from_str
("OPT PSEUDO-RR\n\trequested_udp_len: 4096\n\terror code: 0\n\tversion: EDNS0\n\tuse dnssec: 1")
.expect("Not a utf8 str"),
read_opt_rr(query.get_additional().pop().expect("No OPT Record!"))
)
}
21 changes: 21 additions & 0 deletions src/dnssec/dnssec_message_processing.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
use crate::message::DnsMessage;
use crate::message::rdata::Rdata;
use crate::message::resource_record::ResourceRecord;

pub fn extract_dnssec_records(dns_response: &DnsMessage) -> (Vec<ResourceRecord>, Vec<ResourceRecord>) {
let answers = dns_response.get_answer();
let additionals = dns_response.get_additional();

let mut dnskey_records = Vec::new();
let mut rrsig_records = Vec::new();

for record in answers.iter().chain(additionals.iter()) {
match record.get_rdata() {
Rdata::DNSKEY(_) => dnskey_records.push(record.clone()),
Rdata::RRSIG(_) => rrsig_records.push(record.clone()),
_ => {}
}
}

(dnskey_records, rrsig_records)
}
72 changes: 72 additions & 0 deletions src/dnssec/rrset_signature.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
use sha2::{Sha256, Digest};
use crypto::digest::Digest as RustDigest;
use crypto::sha1::Sha1;
use base64::encode;
use crate::message::rdata::Rdata;
use crate::message::rdata::dnskey_rdata::DnskeyRdata;
use crate::message::rdata::rrsig_rdata::RRSIGRdata;
use crate::message::rrtype::Rrtype;
use crate::message::resource_record::{ResourceRecord, ToBytes};
use crate::client::client_error::ClientError;

pub fn verify_rrsig(rrsig: &RRSIGRdata, dnskey: &DnskeyRdata, rrset: &[ResourceRecord]) -> Result<bool, ClientError> {
let mut rrsig_data = Vec::new();
rrsig_data.extend_from_slice(&u16::from(rrsig.get_type_covered()).to_be_bytes());
rrsig_data.push(rrsig.get_algorithm());
rrsig_data.push(rrsig.get_labels());
rrsig_data.extend_from_slice(&rrsig.get_original_ttl().to_be_bytes());
rrsig_data.extend_from_slice(&rrsig.get_signature_expiration().to_be_bytes());
rrsig_data.extend_from_slice(&rrsig.get_signature_inception().to_be_bytes());
rrsig_data.extend_from_slice(&rrsig.get_key_tag().to_be_bytes());
rrsig_data.extend_from_slice(&rrsig.get_signer_name().to_bytes());//Try?

let mut rrset_sorted = rrset.to_vec();
rrset_sorted.sort_by(|a, b| a.get_name().cmp(&b.get_name()));

for rr in rrset_sorted.iter() {
rrsig_data.extend_from_slice(&rr.get_name().to_bytes()); //Try?
rrsig_data.extend_from_slice(&rr.get_ttl().to_be_bytes());
rrsig_data.extend_from_slice(&(rr.get_rdata().to_bytes().len() as u16).to_be_bytes());
rrsig_data.extend_from_slice(&rr.get_rdata().to_bytes());//Try?
}

let signature = rrsig.get_signature().clone();
let hashed = Sha256::digest(&rrsig_data);

match dnskey.algorithm {
3 | 5 => {
// (DSA/RSA)/SHA1
let mut sha1 = Sha1::new();
sha1.input(&rrsig_data);
let digest = sha1.result_str();
Ok(digest == encode(&signature))
},
8 => {
// RSA/SHA256
Ok(encode(&hashed) == encode(&signature))
},
_ => Err(ClientError::NotImplemented("Unknown DNSKEY algorithm")),
}
}

pub fn verify_ds(ds_record: &ResourceRecord, dnskey: &DnskeyRdata) -> Result<bool, ClientError> {
if let Rdata::DS(ds_rdata) = &ds_record.get_rdata() {
let dnskey_bytes = dnskey.to_bytes(); //Try?
let hashed_key = match ds_rdata.algorithm {
1 => {
let mut hasher = Sha1::new();
hasher.input(&dnskey_bytes);
hasher.result_str()
},
2 => {
let hashed = Sha256::digest(&dnskey_bytes);
encode(&hashed)
},
_ => return Err(ClientError::NotImplemented("Unknown DS algorithm")),
};

Ok(ds_rdata.digest == hashed_key.as_bytes())
} else {
Err(ClientError::FormatError("Provided record is not a DS record"))
}
}
6 changes: 2 additions & 4 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,5 @@ pub mod domain_name;
pub mod message;
pub mod async_resolver;
pub mod truncated_dns_message;




pub mod tsig;
pub mod dnssec;
Loading

0 comments on commit 50a689f

Please sign in to comment.