Skip to content

Commit

Permalink
Parse PQ v1 errors from the server
Browse files Browse the repository at this point in the history
PQ upgrader returns errors encoded in the 5th byte of the response.
The response in that case is exactly 5 bytes long.

Server errors are mapped to an enum and printed with an error message.
  • Loading branch information
LukasPukenis committed Feb 11, 2025
1 parent 519763f commit 91b5400
Show file tree
Hide file tree
Showing 3 changed files with 122 additions and 8 deletions.
Empty file.
4 changes: 4 additions & 0 deletions crates/telio-pq/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ use std::io;
use std::net::SocketAddr;

pub use entity::Entity;
use proto::PqProtoV1Status;

/// Post quantum keys retrived from hanshake
#[derive(Clone)]
Expand Down Expand Up @@ -36,6 +37,9 @@ pub enum Error {
/// IO error
#[error("IO: {0:?}")]
Io(#[from] io::Error),
/// Server errors
#[error("Server error: {0:?}")]
Server(PqProtoV1Status),
/// Generic unrecoverable error
#[error("Generic: {0}")]
Generic(String),
Expand Down
126 changes: 118 additions & 8 deletions crates/telio-pq/src/proto.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ use pnet_packet::{
use pqcrypto_kyber::kyber768;
use pqcrypto_traits::kem::{Ciphertext, PublicKey, SharedSecret};
use rand::{prelude::Distribution, rngs::OsRng};
use telio_utils::{telio_log_debug, telio_log_warn};
use telio_utils::{telio_log_debug, telio_log_error, telio_log_warn};
use tokio::net::{ToSocketAddrs, UdpSocket};

const SERVICE_PORT: u16 = 6480;
Expand All @@ -30,6 +30,35 @@ const CIPHERTEXT_LEN: u32 = kyber768::ciphertext_bytes() as _;
const IPV4_HEADER_LEN: usize = 20;
const UDP_HEADER_LEN: usize = 8;

#[derive(Debug, Eq, PartialEq)]
pub enum PqProtoV1Status {
ServerError,
DeviceError,
PeerOrDeviceNotFound,
CouldNotReadTimestamp,
CouldNotReadVersion,
CouldNotReadMessageType,
Failure,
UnhandledError,
NoData,
}

// Enums are copy-pasted from pq-upgrader
impl From<u8> for PqProtoV1Status {
fn from(c: u8) -> Self {
match c {
1 => PqProtoV1Status::ServerError,
2 => PqProtoV1Status::DeviceError,
3 => PqProtoV1Status::PeerOrDeviceNotFound,
4 => PqProtoV1Status::CouldNotReadTimestamp,
5 => PqProtoV1Status::CouldNotReadVersion,
6 => PqProtoV1Status::CouldNotReadMessageType,
7 => PqProtoV1Status::Failure,
_ => PqProtoV1Status::UnhandledError,
}
}
}

struct TunnelSock {
tunn: noise::Tunn,
sock: telio_sockets::External<UdpSocket>,
Expand Down Expand Up @@ -221,7 +250,7 @@ async fn handshake(
Ok(TunnelSock { tunn, sock })
}

pub fn parse_get_response(pkgbuf: &[u8], local_port: u16) -> io::Result<kyber768::Ciphertext> {
pub fn parse_get_response(pkgbuf: &[u8], local_port: u16) -> super::Result<kyber768::Ciphertext> {
let ip = Ipv4Packet::new(pkgbuf).ok_or(io::Error::new(
io::ErrorKind::InvalidData,
"Invalid PQ keys IP packet received",
Expand Down Expand Up @@ -288,28 +317,42 @@ fn validate_get_response_udp(
/// ---------------------------------
/// Ciphertext bytes , [u8]
/// ---------------------------------
pub fn parse_response_payload(payload: &[u8]) -> io::Result<kyber768::Ciphertext> {
pub fn parse_response_payload(payload: &[u8]) -> super::Result<kyber768::Ciphertext> {
let mut data = io::Cursor::new(payload);

let mut version = [0u8; 4];
data.read_exact(&mut version)?;
let version = u32::from_le_bytes(version);
if version != PQ_PROTO_VERSION {
return Err(io::Error::new(
return Err(super::Error::Io(io::Error::new(
io::ErrorKind::InvalidData,
"Server responded with invalid PQ handshake version",
));
)));
}

if PQ_PROTO_VERSION == 1 {
// v1 reports error as a single(5th) byte with no further payload.
#[allow(clippy::comparison_chain)]
if payload.len() < 5 {
return Err(super::Error::Server(PqProtoV1Status::NoData));
} else if payload.len() == 5 {
#[allow(index_access_check)]
let error_code = payload[4];
let status = PqProtoV1Status::from(error_code);
telio_log_error!("PQ upgrader responded with: {}: {:?}", error_code, status);
return Err(super::Error::Server(status));
}
}

let mut ciphertext_len = [0u8; 4];
data.read_exact(&mut ciphertext_len)?;
let ciphertext_len = u32::from_le_bytes(ciphertext_len);

if ciphertext_len != CIPHERTEXT_LEN {
return Err(io::Error::new(
return Err(super::Error::Io(io::Error::new(
io::ErrorKind::InvalidData,
"Server responded with invalid PQ handshake ciphertext lenght",
));
"Server responded with invalid PQ handshake ciphertext length",
)));
}

let mut cipherbuf = [0; CIPHERTEXT_LEN as usize];
Expand Down Expand Up @@ -459,6 +502,7 @@ fn random_port(rng: &mut impl rand::Rng) -> u16 {

#[cfg(test)]
mod tests {
use crate::{proto::PqProtoV1Status, Error};
use base64::prelude::*;
use pqcrypto_kyber::kyber768;
use pqcrypto_traits::kem::{Ciphertext, SecretKey as _, SharedSecret};
Expand Down Expand Up @@ -511,4 +555,70 @@ mod tests {
let expected = b"\x67\xA6\x9C\x16\x98\xFF\x14\x9E\xEF\xBA\xDE\x97\x4F\x08\x42\x23\xCD\x4B\xD3\x35\xBE\x77\x80\x65\x31\x57\x4F\x28\x8E\x44\x8E\x8D\xB8\xA5\xB5\xDF\x02\x53\x33\x53\x2A\xDC\x84\x83\x01\x67\x6B\x66\xAD\x44\xAE\x77\x33\xA1\x0A\x92\x8A\xDA\xF1\xAD\x1B\xFF\x39\xB0\xCA\x28\x0C\xA7\x05\xD7\xCF\x8A\x57\xD2\x08\x2F\x4C\xA7\xB7\xF3\x9B\xEE\x0D\xA5\x09\x5B\xF9\xB3\x35\x95\x35\x07\x9D\x83\xE5\xE0\x3C\x9D\x77\xB9\xF7\x96\xF3\x76\x93\x43\x61\x67\xD3\xED\x61\x39\xB8\x71\xEA\x54\xD2\xFD\xCE\xDE\x98\xFF\x7A\x05\x01\x57\x35\xB0\x47\x3A\xC9\x67\x52\x51\xAD\xE2\x6A\x2A\x2E\x80\xD6\xBB\x25\x5E\x69\xAC\x34\x65\xFF\xC4\xD4\x09\x35\x0B\x09\xD3\x4B\xCE\xC2\x40\xAD\xD8\xDF\x9F\x34\x20\x9A\xA6\xEC\xCF\x81\x52\xBD\xE6\x5C\xBD\xE9\x8C\xD6\xD4\xAD\x5D\x5A\x57\xB4\x64\x61\x9E\x51\xC7\x6D\xE7\x4D\x6A\xBF\x23\x71\x9D\xEB\x42\x4E\xF7\x8D\xD6\x84\x31\xED\x3F\x15\x70\xAB\xA5\xA9\x0D\x80\x80\xAA\xA3\xA8\x7E\x17\x4B\x99\x8B\xA9\x39\xB5\x2E\x61\x67\xE1\xCD\x59\xD8\x0D\x21\xA5\xFA\x5E\xF1\x9C\x34\x67\x44\xB8\x2B\xDB\xD8\x19\x8B\xE2\x15\xA2\x30\x5E\x0D\x6A\xD4\x45\x5A\xF4\x0C\x91\x55\x4D\xFA\xB6\xDB\xDD\x69\xE2\x96\x75\xEE\xA0\x32\x4E\x5D\x39\xA9\x27\xF6\x64\xF1\x98\x05\x39\x71\x0F\x3E\x3B\x4E\x19\x0C\x21\x4B\x39\xC5\xAC\x8C\xC1\xF6\xE3\x6D\x13\x66\xDF\x35\xD9\x0E\xB0\x8D\x81\x94\xD6\x0B\xCA\x3C\x3A\xF2\x66\xF4\xF7\x40\xFE\x59\x39\x26\x44\x75\x7D\x4A\xAD\xEE\x4E\x8C\xD8\xB4\xCB\xFE\xEA\xE9\xA4\x5A\x9C\x6C\x3F\x0E\xE1\xCD\x64\x7E\xDA\x47\x4E\x07\xCC\x78\x2F\x50\x6F\x5B\x52\x22\x29\x23\x5A\xEA\x2D\xEB\x3F\x9E\xEC\x15\xDE\x1F\x44\x5C\x16\x95\xC0\x1F\xA2\x90\x5F\xA3\x31\x8F\xFE\x4A\x31\xA8\x34\xBC\x3A\xF9\x1D\x7F\x34\x02\xDF\xD7\xD3\x4F\x96\x73\x73\x18\x16\x9C\x87\x97\xD4\xCE\x63\xC2\x83\x90\x2D\xC8\xDF\x6A\xAB\xFD\x81\x74\x8F\xDF\x09\x6D\xA3\xCD\xB7\x50\xE1\x88\xA6\x75\xCD\x8B\x55\x75\xD2\x26\x49\xC4\x6E\x9A\x2B\xA5\x13\xDB\x8F\xC7\x9E\xE9\x6E\xE2\xEE\x9F\x1E\xAF\x77\xF8\x89\x17\xF2\xD5\xF7\x89\x3F\xC3\x18\x16\x86\x57\x1F\x9F\xD0\xF1\xC3\xCC\x45\x67\xA2\x45\x6A\x16\x6B\x2B\xF5\xAA\x56\x6E\x80\xC0\x91\x1D\x2B\x0A\xCB\xCF\x1F\x80\x20\x18\x71\x6B\x6E\x46\x5C\x05\xE4\x73\x7E\xB4\x2B\x98\x40\x23\xC8\x6C\xA4\xCB\xD6\x12\xF6\xF4\xCB\x06\x75\xBC\x6B\xDC\x44\x71\xBB\x11\x69\x97\x8B\xD2\x15\xAD\x98\xBB\xCD\xA2\x5A\x77\x3D\xFC\xC3\x43\x79\xC8\xF9\x33\x87\x22\x9E\x20\x02\x63\x23\x48\xDD\xC7\x45\x44\x06\x10\x16\x4C\x35\x26\xB0\xAC\x5C\x98\x24\x9D\xC2\x1A\x48\x48\x49\x0F\x93\xE8\x6E\xE7\xB3\x77\xD2\xE5\x64\xDD\x49\x1C\x87\x77\x98\x11\xF6\xD0\x0C\xEB\x95\x73\x46\x51\x9F\xFC\x10\x23\x19\xD3\x73\x08\xFA\xFF\xCF\x70\x5C\x03\x34\x53\xC9\x65\x76\x00\xB9\x7C\x1C\x30\x1A\x9E\x0E\xD6\x2B\x8F\xB5\xC9\x50\xDA\x4B\x37\xF2\xC2\x86\x07\xB4\xE1\x70\x42\x1A\xAB\x70\x9F\x06\x72\xED\xBF\x45\x1D\xEA\x3E\x6C\xCF\xC6\x74\x0C\xA8\x9B\xAB\xCF\xEC\x62\xA9\xAB\x70\xF9\x1C\xA0\xBF\x99\x86\x3D\x1F\xE0\xA9\xCC\x9A\x6E\xD2\x8B\xB4\xBB\x29\xFA\xC3\x7D\xAC\xF9\x3C\x44\x06\xC8\xB2\x49\x3F\x26\x86\xA7\x8B\x13\x8E\x3A\xDF\x73\xEC\x94\xAE\xA2\x0C\x4C\x19\x13\x85\xED\x50\xF3\xCA\x53\xA5\x8E\x9F\xC6\x00\x44\xD8\x73\x08\x2C\xA0\x4D\x7A\xB0\xF7\xE5\x25\xD0\x22\x78\x47\x08\xB1\x55\x01\x98\x5A\xCE\xB8\x6B\x4B\x2F\x0B\x83\x54\x83\x70\xC8\xEB\xCE\x41\xA7\xBF\x33\x9A\x58\xDA\x36\x79\x56\xFD\x88\x30\x94\x31\x48\xF5\x9E\xA6\x2D\xEA\x05\x03\x27\x9E\x76\x72\xA6\xC8\x45\xFD\xEF\xB4\xCB\xBF\xC5\xC3\x02\x13\x33\x37\x02\xD8\x8A\x3C\x8A\x46\xC3\x3C\xBA\x0A\xEB\x9D\x46\x81\xF2\x97\xD5\x38\xFD\xC8\xF4\x6A\x7B\x56\x23\xED\x70\xA6\x58\x40\x61\x0A\x3C\x48\xE3\x01\xE4\x32\xFA\xC5\xE9\x80\xAB\x1B\x37\x04\x45\x0D\x10\x6E\x54\x18\xDE\xAA\x4E\xF0\x0A\x56\x45\xA4\x27\xE2\xC2\xA3\x0D\xB4\x57\xDE\xD0\x08\xE5\xE0\xBE\xF8\xC9\x8F\x1D\x09\x2D\x18\x83\xB4\xBD\x64\xD2\x52\x6C\x16\x81\x7C\x6F\x0F\x04\x62\x6D\x38\xFF\x11\xA1\xED\x86\xF2\xB0\xE1\x72\x33\xF0\x99\xBD\xC5\xA6\x00\xF5\x2C\x3D\x73\xFE\xE8\xBB\x75\xF5\xF5\x5C\x8D\x71\xE8\x90\xF7\x5D\xFE\x3B\x6D\xD3\xCE\x02\x6E\x4F\x07\x6E\x89\xBD\x62\x15\xCB\xB5\xFE\x8E\xCE\x28\x34\xFC\xA0\xC5\xFE\x4A\x8C\x6E\xFE\x8C\xE0\x5B\x3B\x72\x9F\x26\x46\xA3\x62\x36\x4B\xDA\x1F\xB1\xC6\xC2\x31\x4B\xB6\x5A\x95\xF9\x5F\x74\x38\x65\x42\xF5\x6D\xB8\x9B\xFB\x95\xDA\xCE\xEB\x47\xC8\x00\xFC\x15\x29\x23\x1A\xD0\xD7\x84\x4F\xBA\x0F\x03\xBE\x78\x51\x03\x8E\x89\xA5\xBF\xD0\x26\x75\xA5\x27\x2F\x97\x98\x01\x68\x33\x88\x4A\x62\x8B\x49\x8E\x18\x33\xA9\x0C\x5C\x07\x0D\x9C\xAC\x11\xD9\x39\x60\xAA\xD8\x28\x64\x19\xE6\xDE\x61\xEC\xC4\x0B\x72\x21\xED\xAA\x54\xDD\xC8\xE6\x0F\x0C\x51\x8D\xF7";
assert_eq!(cipher.as_bytes(), expected);
}

#[test]
fn parse_response_error_v1() {
assert!(matches!(
super::parse_response_payload(b"\x01\x00\x00\x00\x01"),
Err(Error::Server(PqProtoV1Status::ServerError))
));
assert!(matches!(
super::parse_response_payload(b"\x01\x00\x00\x00\x02"),
Err(Error::Server(PqProtoV1Status::DeviceError))
));
assert!(matches!(
super::parse_response_payload(b"\x01\x00\x00\x00\x03"),
Err(Error::Server(PqProtoV1Status::PeerOrDeviceNotFound))
));

assert!(matches!(
super::parse_response_payload(b"\x01\x00\x00\x00\x04"),
Err(Error::Server(PqProtoV1Status::CouldNotReadTimestamp))
));

assert!(matches!(
super::parse_response_payload(b"\x01\x00\x00\x00\x05"),
Err(Error::Server(PqProtoV1Status::CouldNotReadVersion))
));

assert!(matches!(
super::parse_response_payload(b"\x01\x00\x00\x00\x06"),
Err(Error::Server(PqProtoV1Status::CouldNotReadMessageType))
));

assert!(matches!(
super::parse_response_payload(b"\x01\x00\x00\x00\x07"),
Err(Error::Server(PqProtoV1Status::Failure))
));

// Pass unknown errors
assert!(matches!(
super::parse_response_payload(b"\x01\x00\x00\x00\x10"),
Err(Error::Server(PqProtoV1Status::UnhandledError))
));

assert!(matches!(
super::parse_response_payload(b"\x01\x00\x00\x00\x20"),
Err(Error::Server(PqProtoV1Status::UnhandledError))
));

assert!(matches!(
super::parse_response_payload(b"\x01\x00\x00\x00"),
Err(Error::Server(PqProtoV1Status::NoData))
));

// Pass another version and don't expect v1 status codes
assert!(matches!(
super::parse_response_payload(b"\x02\x00\x00\x00\x02"),
Err(Error::Io(_))
));
assert!(matches!(
super::parse_response_payload(b"\x02\x00\x00\x00\x01"),
Err(Error::Io(_))
));
assert!(matches!(
super::parse_response_payload(b"\x02\x00\x00\x00\x40"),
Err(Error::Io(_))
));
}
}

0 comments on commit 91b5400

Please sign in to comment.