Namespace split the dirsync request/response

There was no big problem, but it was messy

Signed-off-by: Luca Fulchir <luca.fulchir@runesauth.com>
This commit is contained in:
Luca Fulchir 2023-06-19 21:57:27 +02:00
parent bf877cf86e
commit 5dff5c8c9a
Signed by: luca.fulchir
GPG Key ID: 8F6440603D13A78E
7 changed files with 312 additions and 282 deletions

View File

@ -0,0 +1,77 @@
//! Directory synchronized handshake
//! 1-RTT connection
//!
//! The simplest, fastest handshake supported by Fenrir
//! Downside: It does not offer protection from DDos,
//! no perfect forward secrecy
//!
//! To grant a form of perfect forward secrecy, the server should periodically
//! change the DNSSEC public/private keys
use crate::enc::{
sym::{NonceLen, TagLen},
Random,
};
pub mod req;
pub mod resp;
// TODO: merge with crate::enc::sym::Nonce
/// random nonce
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Nonce(pub(crate) [u8; 16]);
impl Nonce {
/// Create a new random Nonce
pub fn new(rnd: &Random) -> Self {
use ::core::mem::MaybeUninit;
let mut out: MaybeUninit<[u8; 16]>;
#[allow(unsafe_code)]
unsafe {
out = MaybeUninit::uninit();
let _ = rnd.fill(out.assume_init_mut());
Self(out.assume_init())
}
}
/// Length of the serialized Nonce
pub const fn len() -> usize {
16
}
}
impl From<&[u8; 16]> for Nonce {
fn from(raw: &[u8; 16]) -> Self {
Self(raw.clone())
}
}
/// Parsed handshake
#[derive(Debug, Clone, PartialEq)]
pub enum DirSync {
/// Directory synchronized handshake: client request
Req(req::Req),
/// Directory synchronized handshake: server response
Resp(resp::Resp),
}
impl DirSync {
/// actual length of the dirsync handshake data
pub fn len(&self, head_len: NonceLen, tag_len: TagLen) -> usize {
match self {
DirSync::Req(req) => req.len(),
DirSync::Resp(resp) => resp.len(head_len, tag_len),
}
}
/// Serialize into raw bytes
/// NOTE: assumes that there is exactly asa much buffer as needed
pub fn serialize(
&self,
head_len: NonceLen,
tag_len: TagLen,
out: &mut [u8],
) {
match self {
DirSync::Req(req) => req.serialize(head_len, tag_len, out),
DirSync::Resp(resp) => resp.serialize(head_len, tag_len, out),
}
}
}

View File

@ -1,85 +1,22 @@
//! Directory synchronized handshake
//! 1-RTT connection
//!
//! The simplest, fastest handshake supported by Fenrir
//! Downside: It does not offer protection from DDos,
//! no perfect forward secrecy
//!
//! To grant a form of perfect forward secrecy, the server should periodically
//! change the DNSSEC public/private keys
//! Directory synchronized handshake, Request parsing
use super::Error;
use crate::{
auth,
connection::{handshake, ProtocolVersion, ID},
connection::{
handshake::{
self,
dirsync::{DirSync, Nonce},
Error,
},
ProtocolVersion, ID,
},
enc::{
asym::{ExchangePubKey, KeyExchangeKind, KeyID},
hkdf,
sym::{self, NonceLen, TagLen},
Random, Secret,
},
};
// TODO: merge with crate::enc::sym::Nonce
/// random nonce
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Nonce(pub(crate) [u8; 16]);
impl Nonce {
/// Create a new random Nonce
pub fn new(rnd: &Random) -> Self {
use ::core::mem::MaybeUninit;
let mut out: MaybeUninit<[u8; 16]>;
#[allow(unsafe_code)]
unsafe {
out = MaybeUninit::uninit();
let _ = rnd.fill(out.assume_init_mut());
Self(out.assume_init())
}
}
/// Length of the serialized Nonce
pub const fn len() -> usize {
16
}
}
impl From<&[u8; 16]> for Nonce {
fn from(raw: &[u8; 16]) -> Self {
Self(raw.clone())
}
}
/// Parsed handshake
#[derive(Debug, Clone, PartialEq)]
pub enum DirSync {
/// Directory synchronized handshake: client request
Req(Req),
/// Directory synchronized handshake: server response
Resp(Resp),
}
impl DirSync {
/// actual length of the dirsync handshake data
pub fn len(&self, head_len: NonceLen, tag_len: TagLen) -> usize {
match self {
DirSync::Req(req) => req.len(),
DirSync::Resp(resp) => resp.len(head_len, tag_len),
}
}
/// Serialize into raw bytes
/// NOTE: assumes that there is exactly asa much buffer as needed
pub fn serialize(
&self,
head_len: NonceLen,
tag_len: TagLen,
out: &mut [u8],
) {
match self {
DirSync::Req(req) => req.serialize(head_len, tag_len, out),
DirSync::Resp(resp) => resp.serialize(head_len, tag_len, out),
}
}
}
/// Client request of a directory synchronized handshake
#[derive(Debug, Clone, PartialEq)]
pub struct Req {
@ -94,7 +31,7 @@ pub struct Req {
/// Client ephemeral public key used for key exchanges
pub exchange_key: ExchangePubKey,
/// encrypted data
pub data: ReqState,
pub data: State,
// SECURITY: TODO: Add padding to min: 1200 bytes
// to avoid amplification attaks
// also: 1200 < 1280 to allow better vpn compatibility
@ -119,8 +56,8 @@ impl Req {
tag_len: TagLen,
) -> usize {
match &self.data {
ReqState::ClearText(data) => data.len() + head_len.0 + tag_len.0,
ReqState::CipherText(length) => *length,
State::ClearText(data) => data.len() + head_len.0 + tag_len.0,
State::CipherText(length) => *length,
}
}
/// actual length of the directory synchronized request
@ -150,7 +87,7 @@ impl Req {
let written_next = 5 + key_len;
self.exchange_key.serialize_into(&mut out[5..written_next]);
let written = written_next;
if let ReqState::ClearText(data) = &self.data {
if let State::ClearText(data) = &self.data {
let from = written + head_len.0;
let to = out.len() - tag_len.0;
data.serialize(&mut out[from..to]);
@ -190,7 +127,7 @@ impl handshake::Parsing for Req {
Ok(exchange_key) => exchange_key,
Err(e) => return Err(e.into()),
};
let data = ReqState::CipherText(raw.len() - (CURR_SIZE + len));
let data = State::CipherText(raw.len() - (CURR_SIZE + len));
Ok(handshake::Data::DirSync(DirSync::Req(Self {
key_id,
exchange,
@ -204,18 +141,18 @@ impl handshake::Parsing for Req {
/// Quick way to avoid mixing cipher and clear text
#[derive(Debug, Clone, PartialEq)]
pub enum ReqState {
pub enum State {
/// Data is still encrytped, we only keep the length
CipherText(usize),
/// Client data, decrypted and parsed
ClearText(ReqData),
ClearText(Data),
}
impl ReqState {
impl State {
/// The length of the data
pub fn len(&self) -> usize {
match self {
ReqState::CipherText(len) => *len,
ReqState::ClearText(data) => data.len(),
State::CipherText(len) => *len,
State::ClearText(data) => data.len(),
}
}
/// parse the cleartext
@ -224,19 +161,19 @@ impl ReqState {
raw: &[u8],
) -> Result<(), Error> {
let clear = match self {
ReqState::CipherText(len) => {
State::CipherText(len) => {
assert!(
*len > raw.len(),
"DirSync::ReqState::CipherText length mismatch"
"DirSync::State::CipherText length mismatch"
);
match ReqData::deserialize(raw) {
match Data::deserialize(raw) {
Ok(clear) => clear,
Err(e) => return Err(e),
}
}
_ => return Err(Error::Parsing),
};
*self = ReqState::ClearText(clear);
*self = State::ClearText(clear);
Ok(())
}
}
@ -321,7 +258,7 @@ impl AuthInfo {
/// Decrypted request data
#[derive(Debug, Clone, PartialEq)]
pub struct ReqData {
pub struct Data {
/// Random nonce, the client can use this to track multiple key exchanges
pub nonce: Nonce,
/// Client key id so the client can use and rotate keys
@ -331,7 +268,7 @@ pub struct ReqData {
/// Authentication data
pub auth: AuthInfo,
}
impl ReqData {
impl Data {
/// actual length of the request data
pub fn len(&self) -> usize {
Nonce::len() + KeyID::len() + ID::len() + self.auth.len()
@ -383,177 +320,3 @@ impl ReqData {
})
}
}
/// Quick way to avoid mixing cipher and clear text
#[derive(Debug, Clone, PartialEq)]
pub enum RespState {
/// Server data, still in ciphertext
CipherText(usize),
/// Parsed, cleartext server data
ClearText(RespData),
}
impl RespState {
/// The length of the data
pub fn len(&self) -> usize {
match self {
RespState::CipherText(len) => *len,
RespState::ClearText(_) => RespData::len(),
}
}
/// parse the cleartext
pub fn deserialize_as_cleartext(
&mut self,
raw: &[u8],
) -> Result<(), Error> {
let clear = match self {
RespState::CipherText(len) => {
assert!(
*len > raw.len(),
"DirSync::RespState::CipherText length mismatch"
);
match RespData::deserialize(raw) {
Ok(clear) => clear,
Err(e) => return Err(e),
}
}
_ => return Err(Error::Parsing),
};
*self = RespState::ClearText(clear);
Ok(())
}
/// Serialize the still cleartext data
pub fn serialize(&self, out: &mut [u8]) {
if let RespState::ClearText(clear) = &self {
clear.serialize(out);
}
}
}
/// Server response in a directory synchronized handshake
#[derive(Debug, Clone, PartialEq)]
pub struct Resp {
/// Tells the client with which key the exchange was done
pub client_key_id: KeyID,
/// actual response data, might be encrypted
pub data: RespState,
}
impl handshake::Parsing for Resp {
fn deserialize(raw: &[u8]) -> Result<handshake::Data, Error> {
const MIN_PKT_LEN: usize = 68;
if raw.len() < MIN_PKT_LEN {
return Err(Error::NotEnoughData);
}
let client_key_id: KeyID =
KeyID(u16::from_le_bytes(raw[0..KeyID::len()].try_into().unwrap()));
Ok(handshake::Data::DirSync(DirSync::Resp(Self {
client_key_id,
data: RespState::CipherText(raw[KeyID::len()..].len()),
})))
}
}
impl Resp {
/// return the offset of the encrypted data
/// NOTE: starts from the beginning of the fenrir packet
pub fn encrypted_offset(&self) -> usize {
ProtocolVersion::len() + handshake::ID::len() + KeyID::len()
}
/// return the total length of the cleartext data
pub fn encrypted_length(
&self,
head_len: NonceLen,
tag_len: TagLen,
) -> usize {
match &self.data {
RespState::ClearText(_data) => {
RespData::len() + head_len.0 + tag_len.0
}
RespState::CipherText(len) => *len,
}
}
/// Total length of the response handshake
pub fn len(&self, head_len: NonceLen, tag_len: TagLen) -> usize {
KeyID::len() + head_len.0 + self.data.len() + tag_len.0
}
/// Serialize into raw bytes
/// NOTE: assumes that there is exactly as much buffer as needed
pub fn serialize(
&self,
head_len: NonceLen,
_tag_len: TagLen,
out: &mut [u8],
) {
out[0..KeyID::len()]
.copy_from_slice(&self.client_key_id.0.to_le_bytes());
let start_data = KeyID::len() + head_len.0;
let end_data = start_data + self.data.len();
self.data.serialize(&mut out[start_data..end_data]);
}
}
/// Decrypted response data
#[derive(Debug, Clone, PartialEq)]
pub struct RespData {
/// Client nonce, copied from the request
pub client_nonce: Nonce,
/// Server Connection ID
pub id: ID,
/// Service Connection ID
pub service_connection_id: ID,
/// Service encryption key
pub service_key: Secret,
}
impl RespData {
/// Return the expected length for buffer allocation
pub fn len() -> usize {
Nonce::len() + ID::len() + ID::len() + Secret::len()
}
/// Serialize the data into a buffer
/// NOTE: assumes that there is exactly asa much buffer as needed
pub fn serialize(&self, out: &mut [u8]) {
let mut start = 0;
let mut end = Nonce::len();
out[start..end].copy_from_slice(&self.client_nonce.0);
start = end;
end = end + ID::len();
self.id.serialize(&mut out[start..end]);
start = end;
end = end + ID::len();
self.service_connection_id.serialize(&mut out[start..end]);
start = end;
end = end + Secret::len();
out[start..end].copy_from_slice(self.service_key.as_ref());
}
/// Parse the cleartext raw data
pub fn deserialize(raw: &[u8]) -> Result<Self, Error> {
let raw_sized: &[u8; 16] = raw[..Nonce::len()].try_into().unwrap();
let client_nonce: Nonce = raw_sized.into();
let end = Nonce::len() + ID::len();
let id: ID =
u64::from_le_bytes(raw[Nonce::len()..end].try_into().unwrap())
.into();
if id.is_handshake() {
return Err(Error::Parsing);
}
let parsed = end;
let end = parsed + ID::len();
let service_connection_id: ID =
u64::from_le_bytes(raw[parsed..end].try_into().unwrap()).into();
if service_connection_id.is_handshake() {
return Err(Error::Parsing);
}
let parsed = end;
let end = parsed + Secret::len();
let raw_secret: &[u8; 32] = raw[parsed..end].try_into().unwrap();
let service_key = raw_secret.into();
Ok(Self {
client_nonce,
id,
service_connection_id,
service_key,
})
}
}

View File

@ -0,0 +1,189 @@
//! Directory synchronized handshake, Response parsing
use crate::{
connection::{
handshake::{
self,
dirsync::{DirSync, Nonce},
Error,
},
ProtocolVersion, ID,
},
enc::{
asym::KeyID,
sym::{NonceLen, TagLen},
Secret,
},
};
/// Server response in a directory synchronized handshake
#[derive(Debug, Clone, PartialEq)]
pub struct Resp {
/// Tells the client with which key the exchange was done
pub client_key_id: KeyID,
/// actual response data, might be encrypted
pub data: State,
}
impl handshake::Parsing for Resp {
fn deserialize(raw: &[u8]) -> Result<handshake::Data, Error> {
const MIN_PKT_LEN: usize = 68;
if raw.len() < MIN_PKT_LEN {
return Err(Error::NotEnoughData);
}
let client_key_id: KeyID =
KeyID(u16::from_le_bytes(raw[0..KeyID::len()].try_into().unwrap()));
Ok(handshake::Data::DirSync(DirSync::Resp(Self {
client_key_id,
data: State::CipherText(raw[KeyID::len()..].len()),
})))
}
}
impl Resp {
/// return the offset of the encrypted data
/// NOTE: starts from the beginning of the fenrir packet
pub fn encrypted_offset(&self) -> usize {
ProtocolVersion::len() + handshake::ID::len() + KeyID::len()
}
/// return the total length of the cleartext data
pub fn encrypted_length(
&self,
head_len: NonceLen,
tag_len: TagLen,
) -> usize {
match &self.data {
State::ClearText(_data) => Data::len() + head_len.0 + tag_len.0,
State::CipherText(len) => *len,
}
}
/// Total length of the response handshake
pub fn len(&self, head_len: NonceLen, tag_len: TagLen) -> usize {
KeyID::len() + head_len.0 + self.data.len() + tag_len.0
}
/// Serialize into raw bytes
/// NOTE: assumes that there is exactly as much buffer as needed
pub fn serialize(
&self,
head_len: NonceLen,
_tag_len: TagLen,
out: &mut [u8],
) {
out[0..KeyID::len()]
.copy_from_slice(&self.client_key_id.0.to_le_bytes());
let start_data = KeyID::len() + head_len.0;
let end_data = start_data + self.data.len();
self.data.serialize(&mut out[start_data..end_data]);
}
}
/// Quick way to avoid mixing cipher and clear text
#[derive(Debug, Clone, PartialEq)]
pub enum State {
/// Server data, still in ciphertext
CipherText(usize),
/// Parsed, cleartext server data
ClearText(Data),
}
impl State {
/// The length of the data
pub fn len(&self) -> usize {
match self {
State::CipherText(len) => *len,
State::ClearText(_) => Data::len(),
}
}
/// parse the cleartext
pub fn deserialize_as_cleartext(
&mut self,
raw: &[u8],
) -> Result<(), Error> {
let clear = match self {
State::CipherText(len) => {
assert!(
*len > raw.len(),
"DirSync::State::CipherText length mismatch"
);
match Data::deserialize(raw) {
Ok(clear) => clear,
Err(e) => return Err(e),
}
}
_ => return Err(Error::Parsing),
};
*self = State::ClearText(clear);
Ok(())
}
/// Serialize the still cleartext data
pub fn serialize(&self, out: &mut [u8]) {
if let State::ClearText(clear) = &self {
clear.serialize(out);
}
}
}
/// Decrypted response data
#[derive(Debug, Clone, PartialEq)]
pub struct Data {
/// Client nonce, copied from the request
pub client_nonce: Nonce,
/// Server Connection ID
pub id: ID,
/// Service Connection ID
pub service_connection_id: ID,
/// Service encryption key
pub service_key: Secret,
}
impl Data {
/// Return the expected length for buffer allocation
pub fn len() -> usize {
Nonce::len() + ID::len() + ID::len() + Secret::len()
}
/// Serialize the data into a buffer
/// NOTE: assumes that there is exactly asa much buffer as needed
pub fn serialize(&self, out: &mut [u8]) {
let mut start = 0;
let mut end = Nonce::len();
out[start..end].copy_from_slice(&self.client_nonce.0);
start = end;
end = end + ID::len();
self.id.serialize(&mut out[start..end]);
start = end;
end = end + ID::len();
self.service_connection_id.serialize(&mut out[start..end]);
start = end;
end = end + Secret::len();
out[start..end].copy_from_slice(self.service_key.as_ref());
}
/// Parse the cleartext raw data
pub fn deserialize(raw: &[u8]) -> Result<Self, Error> {
let raw_sized: &[u8; 16] = raw[..Nonce::len()].try_into().unwrap();
let client_nonce: Nonce = raw_sized.into();
let end = Nonce::len() + ID::len();
let id: ID =
u64::from_le_bytes(raw[Nonce::len()..end].try_into().unwrap())
.into();
if id.is_handshake() {
return Err(Error::Parsing);
}
let parsed = end;
let end = parsed + ID::len();
let service_connection_id: ID =
u64::from_le_bytes(raw[parsed..end].try_into().unwrap()).into();
if service_connection_id.is_handshake() {
return Err(Error::Parsing);
}
let parsed = end;
let end = parsed + Secret::len();
let raw_secret: &[u8; 32] = raw[parsed..end].try_into().unwrap();
let service_key = raw_secret.into();
Ok(Self {
client_nonce,
id,
service_connection_id,
service_key,
})
}
}

View File

@ -166,9 +166,11 @@ impl Handshake {
None => return Err(Error::Parsing),
};
let data = match handshake_kind {
HandshakeKind::DirSyncReq => dirsync::Req::deserialize(&raw[2..])?,
HandshakeKind::DirSyncReq => {
dirsync::req::Req::deserialize(&raw[2..])?
}
HandshakeKind::DirSyncResp => {
dirsync::Resp::deserialize(&raw[2..])?
dirsync::resp::Resp::deserialize(&raw[2..])?
}
};
Ok(Self {

View File

@ -22,11 +22,11 @@ fn test_handshake_dirsync_req() {
}
};
let data = dirsync::ReqState::ClearText(dirsync::ReqData {
let data = dirsync::req::State::ClearText(dirsync::req::Data {
nonce: dirsync::Nonce::new(&rand),
client_key_id: KeyID(2424),
id: ID::ID(::core::num::NonZeroU64::new(424242).unwrap()),
auth: dirsync::AuthInfo {
auth: dirsync::req::AuthInfo {
user: auth::UserID::new(&rand),
token: auth::Token::new_anonymous(&rand),
service_id: auth::SERVICEID_AUTH,
@ -35,7 +35,7 @@ fn test_handshake_dirsync_req() {
});
let h_req = Handshake::new(handshake::Data::DirSync(
dirsync::DirSync::Req(dirsync::Req {
dirsync::DirSync::Req(dirsync::req::Req {
key_id: KeyID(4224),
exchange: enc::asym::KeyExchangeKind::X25519DiffieHellman,
hkdf: enc::hkdf::Kind::Sha3,
@ -81,7 +81,7 @@ fn test_handshake_dirsync_reqsp() {
let service_key = enc::Secret::new_rand(&rand);
let data = dirsync::RespState::ClearText(dirsync::RespData {
let data = dirsync::resp::State::ClearText(dirsync::resp::Data {
client_nonce: dirsync::Nonce::new(&rand),
id: ID::ID(::core::num::NonZeroU64::new(424242).unwrap()),
service_connection_id: ID::ID(
@ -91,7 +91,7 @@ fn test_handshake_dirsync_reqsp() {
});
let h_resp = Handshake::new(handshake::Data::DirSync(
dirsync::DirSync::Resp(dirsync::Resp {
dirsync::DirSync::Resp(dirsync::resp::Resp {
client_key_id: KeyID(4444),
data,
}),

View File

@ -53,7 +53,7 @@ fn test_encrypt_decrypt() {
let service_key = enc::Secret::new_rand(&rand);
let data = dirsync::RespState::ClearText(dirsync::RespData {
let data = dirsync::resp::State::ClearText(dirsync::resp::Data {
client_nonce: dirsync::Nonce::new(&rand),
id: ID::ID(::core::num::NonZeroU64::new(424242).unwrap()),
service_connection_id: ID::ID(
@ -62,7 +62,7 @@ fn test_encrypt_decrypt() {
service_key,
});
let resp = dirsync::Resp {
let resp = dirsync::resp::Resp {
client_key_id: KeyID(4444),
data,
};

View File

@ -326,25 +326,25 @@ impl Worker {
};
// build request
let auth_info = dirsync::AuthInfo {
let auth_info = dirsync::req::AuthInfo {
user: UserID::new_anonymous(),
token: Token::new_anonymous(&self.rand),
service_id: conn_info.service_id,
domain: conn_info.domain,
};
let req_data = dirsync::ReqData {
let req_data = dirsync::req::Data {
nonce: dirsync::Nonce::new(&self.rand),
client_key_id,
id: auth_recv_id.0, //FIXME: is zero
auth: auth_info,
};
let req = dirsync::Req {
let req = dirsync::req::Req {
key_id: key.0,
exchange,
hkdf: hkdf_selected,
cipher: cipher_selected,
exchange_key: pub_key,
data: dirsync::ReqState::ClearText(req_data),
data: dirsync::req::State::ClearText(req_data),
};
let encrypt_start =
connection::ID::len() + req.encrypted_offset();
@ -459,9 +459,8 @@ impl Worker {
::tracing::error!("AuthInfo on non DS::Req");
return;
}
use dirsync::ReqState;
let req_data = match req.data {
ReqState::ClearText(req_data) => req_data,
dirsync::req::State::ClearText(req_data) => req_data,
_ => {
::tracing::error!("AuthNeeded: expected ClearText");
assert!(false, "AuthNeeded: unreachable");
@ -527,7 +526,7 @@ impl Worker {
let auth_id_recv = self.connections.reserve_first();
auth_conn.id_recv = auth_id_recv;
let resp_data = dirsync::RespData {
let resp_data = dirsync::resp::Data {
client_nonce: req_data.nonce,
id: auth_conn.id_recv.0,
service_connection_id: srv_conn_id,
@ -537,10 +536,9 @@ impl Worker {
// no aad for now
let aad = AAD(&mut []);
use dirsync::RespState;
let resp = dirsync::Resp {
let resp = dirsync::resp::Resp {
client_key_id: req_data.client_key_id,
data: RespState::ClearText(resp_data),
data: dirsync::resp::State::ClearText(resp_data),
};
let encrypt_from =
connection::ID::len() + resp.encrypted_offset();
@ -579,7 +577,8 @@ impl Worker {
}
// track connection
let resp_data;
if let dirsync::RespState::ClearText(r_data) = ds_resp.data
if let dirsync::resp::State::ClearText(r_data) =
ds_resp.data
{
resp_data = r_data;
} else {