From 6e954a979561a8332d7b8af7aa3354109a74f165 Mon Sep 17 00:00:00 2001 From: Andy Pack Date: Sat, 10 Feb 2024 11:53:51 +0000 Subject: [PATCH] handling key submission from client and responding with server public key --- dnstp/src/clients.rs | 43 +++++++++ dnstp/src/crypto/mod.rs | 2 +- dnstp/src/lib.rs | 3 +- dnstp/src/message/message.rs | 34 ++++++- dnstp/src/message/question/mod.rs | 23 +++++ dnstp/src/message/record/cname_rdata.rs | 30 ++++++ dnstp/src/message/record/mod.rs | 2 + dnstp/src/processor/request/encryption.rs | 108 ++++++++++++++++++++-- dnstp/src/processor/request/mod.rs | 46 +++++++-- dnstp/src/processor/response/mod.rs | 7 +- dnstp/src/string.rs | 22 +++++ dnstp/tests/key_swap.rs | 75 +++++++++++++++ 12 files changed, 372 insertions(+), 23 deletions(-) create mode 100644 dnstp/src/clients.rs create mode 100644 dnstp/src/message/record/cname_rdata.rs create mode 100644 dnstp/tests/key_swap.rs diff --git a/dnstp/src/clients.rs b/dnstp/src/clients.rs new file mode 100644 index 0000000..91e1a50 --- /dev/null +++ b/dnstp/src/clients.rs @@ -0,0 +1,43 @@ +use std::collections::HashMap; +use std::time::SystemTime; +use aes_gcm_siv::Aes256GcmSiv; + +pub struct Client { + pub first_seen: SystemTime, + pub shared_key: Aes256GcmSiv +} + +impl Client { + + pub fn new(shared_key: Aes256GcmSiv) -> Client + { + Client { + first_seen: SystemTime::now(), + shared_key + } + } +} + +pub struct Clients { + client_map: HashMap +} + +impl Clients { + + pub fn new() -> Clients + { + Clients { + client_map: HashMap::new() + } + } + + // pub fn add_from(&mut self, client_id: String, shared_key: Aes256GcmSiv) + // { + // self.client_map.insert(client_id, Client::new(shared_key)); + // } + + pub fn add(&mut self, client_id: String, client:Client) + { + self.client_map.insert(client_id, client); + } +} \ No newline at end of file diff --git a/dnstp/src/crypto/mod.rs b/dnstp/src/crypto/mod.rs index 6526c5c..9e8df8b 100644 --- a/dnstp/src/crypto/mod.rs +++ b/dnstp/src/crypto/mod.rs @@ -18,7 +18,7 @@ mod tests; use std::str::FromStr; -use p256::{EncodedPoint, PublicKey, ecdh::EphemeralSecret, NistP256}; +use p256::{PublicKey, ecdh::EphemeralSecret, NistP256}; use p256::elliptic_curve::ecdh::SharedSecret; use aes_gcm_siv::{aead::{Aead, KeyInit}, AeadCore, Aes256GcmSiv, Nonce}; diff --git a/dnstp/src/lib.rs b/dnstp/src/lib.rs index 3f97e56..e5cfe1c 100644 --- a/dnstp/src/lib.rs +++ b/dnstp/src/lib.rs @@ -7,8 +7,9 @@ mod byte; pub mod processor; pub mod message; pub mod net; -mod string; +pub mod string; pub mod config; pub mod crypto; +mod clients; pub use config::DomainConfig; \ No newline at end of file diff --git a/dnstp/src/message/message.rs b/dnstp/src/message/message.rs index c856798..efb33a3 100644 --- a/dnstp/src/message/message.rs +++ b/dnstp/src/message/message.rs @@ -75,18 +75,18 @@ impl DNSMessage { } } - pub fn a_resp_from_request(request: &DNSMessage, ip: impl Fn(&DNSQuestion) -> Ipv4Addr) -> DNSMessage + pub fn a_resp_from_request(&self, ip: impl Fn(&DNSQuestion) -> Ipv4Addr) -> DNSMessage { let mut response = DNSMessage{ - header: request.header.clone(), - questions: request.questions.clone(), + header: self.header.clone(), + questions: self.questions.clone(), answer_records: vec![], authority_records: vec![], additional_records: vec![], - peer: request.peer + peer: self.peer }; - response.answer_records = request.questions + response.answer_records = self.questions .iter() .map(|x| ResourceRecord::from_query(x, @@ -107,4 +107,28 @@ impl DNSMessage { response } + + pub fn empty_resp_from_request(&self) -> DNSMessage + { + let mut response = DNSMessage{ + header: self.header.clone(), + questions: self.questions.clone(), + answer_records: vec![], + authority_records: vec![], + additional_records: vec![], + peer: self.peer + }; + + response.header.direction = Direction::Response; + response.header.response = ResponseCode::NoError; + response.header.answer_record_count = 0; + response.header.authority_record_count = 0; + response.header.additional_record_count = 0; + + if response.header.recursion_desired { + response.header.recursion_available = true; + } + + response + } } \ No newline at end of file diff --git a/dnstp/src/message/question/mod.rs b/dnstp/src/message/question/mod.rs index 2a42369..0258153 100644 --- a/dnstp/src/message/question/mod.rs +++ b/dnstp/src/message/question/mod.rs @@ -1,6 +1,7 @@ #[cfg(test)] mod tests; +use std::fmt; use urlencoding::decode; use crate::byte::{push_split_bytes, two_byte_combine}; use crate::string::encode_domain_name; @@ -25,6 +26,28 @@ pub enum QType { ANY = 255, } +impl fmt::Display for QType { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + QType::A => write!(f, "A"), + QType::NS => write!(f, "NS"), + QType::CNAME => write!(f, "CNAME"), + QType::SOA => write!(f, "SOA"), + QType::WKS => write!(f, "WKS"), + QType::PTR => write!(f, "PTR"), + QType::HINFO => write!(f, "HINFO"), + QType::MINFO => write!(f, "MINFO"), + QType::MX => write!(f, "MX"), + QType::TXT => write!(f, "TXT"), + QType::RP => write!(f, "RP"), + QType::AAAA => write!(f, "AAAA"), + QType::SRV => write!(f, "SRV"), + QType::OPT => write!(f, "OPT"), + QType::ANY => write!(f, "ANY"), + } + } +} + impl TryFrom for QType { type Error = u16; diff --git a/dnstp/src/message/record/cname_rdata.rs b/dnstp/src/message/record/cname_rdata.rs new file mode 100644 index 0000000..57f7057 --- /dev/null +++ b/dnstp/src/message/record/cname_rdata.rs @@ -0,0 +1,30 @@ +use std::fmt::{Debug, Formatter}; +use crate::message::record::RData; +use crate::string::encode_domain_name; + +pub struct CnameRdata { + pub rdata: String +} + +impl Debug for CnameRdata { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + f.debug_struct("CNAME") + .field("Host", &self.rdata) + .finish() + } +} + +impl RData for CnameRdata { + fn to_bytes(&self) -> Vec { + encode_domain_name(&self.rdata) + } +} + +impl CnameRdata { + pub fn from(rdata: String) -> CnameRdata + { + CnameRdata { + rdata + } + } +} \ No newline at end of file diff --git a/dnstp/src/message/record/mod.rs b/dnstp/src/message/record/mod.rs index 9b44ece..413b43a 100644 --- a/dnstp/src/message/record/mod.rs +++ b/dnstp/src/message/record/mod.rs @@ -10,6 +10,8 @@ pub use aaaa_rdata::AAAARdata; mod txt_rdata; pub use txt_rdata::TXTRdata; +mod cname_rdata; +pub use cname_rdata::CnameRdata; #[cfg(test)] mod tests; diff --git a/dnstp/src/processor/request/encryption.rs b/dnstp/src/processor/request/encryption.rs index ba52da7..2eac5b1 100644 --- a/dnstp/src/processor/request/encryption.rs +++ b/dnstp/src/processor/request/encryption.rs @@ -1,16 +1,112 @@ use std::net::Ipv4Addr; use p256::ecdh::EphemeralSecret; -use crate::crypto::{get_random_asym_pair, trim_public_key}; -use crate::message::DNSMessage; +use crate::clients::Client; +use crate::crypto::{asym_to_sym_key, fatten_public_key, get_random_asym_pair, get_shared_asym_secret, trim_public_key}; +use crate::message::{ARdata, DNSMessage, DNSQuestion, QClass, QType, ResourceRecord}; +use crate::message::record::CnameRdata; +use crate::string::{append_base_domain_to_key, strip_base_domain_from_key}; -pub fn get_key_response(request: DNSMessage) -> DNSMessage -{ - DNSMessage::a_resp_from_request(&request, |_| Ipv4Addr::from([127, 0, 0, 1])) +pub struct KeySwapContext { + pub new_client: Client, + pub response: DNSMessage, + pub server_public: String, + pub client_public: String } pub fn get_key_request_with_base_domain(base_domain: String) -> (EphemeralSecret, String) { let (private, public) = get_random_asym_pair(); - (private, vec![trim_public_key(&public), base_domain].join(".")) + (private, append_base_domain_to_key(trim_public_key(&public), &base_domain)) +} + +pub fn get_fattened_public_key(key_question: &DNSQuestion) -> (String, String) +{ + let public_key = &key_question.qname; + let (trimmed_public_key, base_domain) = strip_base_domain_from_key(public_key); + + (fatten_public_key(&trimmed_public_key), base_domain) } + +#[derive(Ord, PartialOrd, Eq, PartialEq, Debug, Copy, Clone)] +pub enum KeyDecodeError { + QuestionCount(usize), + FirstQuestionNotA(QType), + SecondQuestionNotA(QType), + SharedSecretDerivation, +} + +pub fn decode_key_request(message: DNSMessage) -> Result +{ + if message.questions.len() == 2 { + + if message.questions[0].qtype != QType::A + { + return Err(KeyDecodeError::FirstQuestionNotA(message.questions[0].qtype)); + } + + let key_question = &message.questions[1]; + + if key_question.qtype != QType::A + { + return Err(KeyDecodeError::SecondQuestionNotA(key_question.qtype)); + } + + let (fattened_public_key, base_domain) = get_fattened_public_key(&key_question); + let (server_private, server_public) = get_random_asym_pair(); + + let shared_secret = get_shared_asym_secret(server_private, fattened_public_key); + + match shared_secret { + Ok(secret) => { + + let sym_key = asym_to_sym_key(&secret); + let new_client = Client::new(sym_key); + let mut response = message.empty_resp_from_request(); + + let first_record = ResourceRecord { + name_offset: 12, + answer_type: QType::A, + class: QClass::Internet, + ttl: 0, + rd_length: 4, + r_data: Box::new(ARdata::from(Ipv4Addr::from([127,0,0,1]))) + }; + + let second_record = ResourceRecord { + name_offset: 12 + (&message.questions[0]).to_bytes().len() as u16, + answer_type: QType::CNAME, + class: QClass::Internet, + ttl: 0, + rd_length: 4, + r_data: Box::new( + CnameRdata::from( + append_base_domain_to_key( + trim_public_key(&server_public), + &base_domain + ) + ) + ) + }; + + response.answer_records = vec![ + first_record, second_record + ]; + + return Ok(KeySwapContext { + new_client, + response, + server_public, + client_public: key_question.qname.to_string() + }); + } + Err(_) => { + return Err(KeyDecodeError::SharedSecretDerivation); + } + } + } + else + { + return Err(KeyDecodeError::QuestionCount(message.questions.len())); + } +} \ No newline at end of file diff --git a/dnstp/src/processor/request/mod.rs b/dnstp/src/processor/request/mod.rs index 56e9e86..5e67e09 100644 --- a/dnstp/src/processor/request/mod.rs +++ b/dnstp/src/processor/request/mod.rs @@ -1,14 +1,16 @@ use std::net::Ipv4Addr; -use std::sync::mpsc; +use std::sync::{Arc, mpsc, Mutex}; use std::sync::mpsc::{Receiver, Sender}; use std::thread; -use log::info; +use log::{error, info}; +use crate::clients::Clients; use crate::config::DomainConfig; use crate::message::DNSMessage; use crate::net::{NetworkMessage, NetworkMessagePtr}; use crate::message_parser::parse_message; use crate::processor::print_error; +use crate::processor::request::encryption::{decode_key_request, KeyDecodeError}; pub mod encryption; @@ -18,7 +20,9 @@ mod tests; pub struct RequestProcesor { message_channel: Option>, domain_config: DomainConfig, - encryption_endpoint: String + encryption_endpoint: String, + + clients: Arc> } impl RequestProcesor { @@ -28,7 +32,8 @@ impl RequestProcesor { RequestProcesor { message_channel: None, domain_config, - encryption_endpoint: fq_key_endpoint + encryption_endpoint: fq_key_endpoint, + clients: Arc::new(Mutex::new(Clients::new())) } } @@ -39,13 +44,13 @@ impl RequestProcesor { let mut base_domain_equality = self.domain_config.base_domain.clone(); base_domain_equality.insert_str(0, "."); - let base_domain_len = base_domain_equality.len() + 1; let fq_key_endpoint = self.encryption_endpoint.clone(); + let clients = self.clients.clone(); thread::spawn(move || { - // let fq_key_endpoint = fq_key_endpoint; + let clients = clients; for m in rx { @@ -61,7 +66,36 @@ impl RequestProcesor { { info!("[{}] received encryption key request", peer); + match decode_key_request(r) + { + Ok(context) => { + + clients.lock().unwrap().add(context.client_public, context.new_client); + sending_channel.send(Box::new( + NetworkMessage { + buffer: Box::new(context.response.to_bytes()), + peer: context.response.peer + } + )); + } + Err(e) => { + match e { + KeyDecodeError::QuestionCount(qc) => { + error!("[{}] failed to parse public key, wrong question count [{}]", peer, qc); + } + KeyDecodeError::FirstQuestionNotA(qtype) => { + error!("[{}] failed to parse public key, first question wasn't an A request [{}]", peer, qtype); + } + KeyDecodeError::SecondQuestionNotA(qtype) => { + error!("[{}] failed to parse public key, second question wasn't an A request [{}]", peer, qtype); + } + KeyDecodeError::SharedSecretDerivation => { + error!("[{}] failed to parse public key, failed to derived shared secret", peer); + } + } + } + } } else { diff --git a/dnstp/src/processor/response/mod.rs b/dnstp/src/processor/response/mod.rs index a556cd1..b0ac2af 100644 --- a/dnstp/src/processor/response/mod.rs +++ b/dnstp/src/processor/response/mod.rs @@ -1,10 +1,9 @@ use std::sync::mpsc; use std::sync::mpsc::{Receiver, Sender}; use std::thread; -use log::{error, info}; -use crate::message::{QuestionParseError, RecordParseError}; +use log::info; use crate::net::raw_request::NetworkMessagePtr; -use crate::message_parser::{HeaderParseError, parse_message, MessageParseError}; +use crate::message_parser::parse_message; use crate::processor::print_error; pub struct ResponseProcesor { @@ -25,7 +24,7 @@ impl ResponseProcesor { thread::spawn(move || { - for mut m in rx + for m in rx { let peer = m.peer.clone(); diff --git a/dnstp/src/string.rs b/dnstp/src/string.rs index e2cba13..e20b3c5 100644 --- a/dnstp/src/string.rs +++ b/dnstp/src/string.rs @@ -20,4 +20,26 @@ pub fn encode_domain_name(name: &String) -> Vec ret.push(0); ret +} + +pub fn strip_base_domain_from_key(public_key: &String) -> (String, String) +{ + let periods: Vec<_> = public_key.rmatch_indices(".").collect(); + + if periods.len() >= 2 { + (public_key[0 .. periods[1].0].to_string(), + public_key[periods[1].0 .. ].to_string()) + } + else if periods.len() == 1 { + (public_key[0 .. periods[0].0].to_string(), + public_key[periods[0].0 .. ].to_string()) + } + else { + (public_key.to_string(), String::new()) + } +} + +pub fn append_base_domain_to_key(trimmed_key: String, base_domain: &String) -> String +{ + vec![trimmed_key, base_domain.to_string()].join(".") } \ No newline at end of file diff --git a/dnstp/tests/key_swap.rs b/dnstp/tests/key_swap.rs new file mode 100644 index 0000000..15534d0 --- /dev/null +++ b/dnstp/tests/key_swap.rs @@ -0,0 +1,75 @@ +use std::net::{Ipv4Addr, SocketAddr, SocketAddrV4}; +use dnstplib::crypto::{asym_to_sym_key, decrypt, encrypt, generate_aes_nonce, get_random_asym_pair, get_shared_asym_secret, trim_public_key}; +use dnstplib::message::{DNSHeader, DNSMessage, DNSQuestion}; +use dnstplib::message::QClass::Internet; +use dnstplib::message::QType::A; +use dnstplib::processor::request::encryption::decode_key_request; +use dnstplib::string::append_base_domain_to_key; +#[test] +fn test_key_swap() +{ + //////////// + // CLIENT + //////////// + + // generate pair + let (client_private, client_public) = get_random_asym_pair(); + + // generate public key submission domain + let serialised_client_public = append_base_domain_to_key( + trim_public_key(&client_public), + &"sarsoo.xyz".to_string() + ); + + let message = DNSMessage { + header: DNSHeader::new_request(1, Some(1)), + questions: vec![ + DNSQuestion { + qname: "static.sarsoo.xyz".to_string(), + qtype: A, + qclass: Internet + }, + DNSQuestion { + qname: serialised_client_public, + qtype: A, + qclass: Internet + } + ], + answer_records: vec![], + authority_records: vec![], + additional_records: vec![], + peer: SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::from([127,0,0,1]), 5000)), + }; + + ///////////////// + // SERVER + ///////////////// + + // handle message "received by client" + let resp = decode_key_request(message).unwrap(); + + //////////// + // CLIENT + //////////// + + // client has received message from above and constructs shared secret + let shared_secret_client = asym_to_sym_key(&get_shared_asym_secret(client_private, resp.server_public).unwrap()); + + /////////////////////////////// + // TEST ENCRYPTION/DECRYPTION + /////////////////////////////// + + let nonce = generate_aes_nonce(); + let payload = "hello world!".to_string(); + + // CLIENT encrypts something + let encrypted = encrypt(&shared_secret_client, &nonce, &payload.clone().into_bytes()).unwrap(); + + // SERVER decrypts it + let decrypted = decrypt(&resp.new_client.shared_key, &nonce, &encrypted).unwrap(); + + let decrypted_payload = String::from_utf8(decrypted).unwrap(); + + // is it the same? + assert_eq!(payload, decrypted_payload); +} \ No newline at end of file