Files
quicproquo/crates/quicprochat-p2p/src/mesh_protocol.rs
Christian Nennemann eee1e9f278 feat(mesh): add KeyPackage distribution over mesh
Implements announce-based KeyPackage distribution for serverless MLS:

- MeshAnnounce now includes optional `keypackage_hash` field (8 bytes)
- CAP_MLS_READY capability flag for nodes with KeyPackages
- KeyPackageCache for storing received KeyPackages:
  - Indexed by mesh address
  - Multiple per address (for rotation)
  - TTL-based expiry
  - Capacity-bounded with LRU eviction
- Mesh protocol messages:
  - KeyPackageRequest (request by address or hash)
  - KeyPackageResponse (KeyPackage + hash)
  - KeyPackageUnavailable (negative response)

Protocol flow:
1. Bob announces with keypackage_hash
2. Alice requests KeyPackage via mesh
3. Bob (or relay) responds with full KeyPackage
4. Alice creates MLS Welcome, sends to Bob via mesh
2026-04-01 08:57:49 +02:00

270 lines
8.7 KiB
Rust

//! Mesh protocol messages for peer-to-peer communication.
//!
//! This module defines the control messages used for mesh coordination:
//! - KeyPackage request/response for MLS group setup
//! - Future: route requests, capability queries, etc.
use serde::{Deserialize, Serialize};
use crate::address::MeshAddress;
/// Protocol message type discriminator.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[repr(u8)]
pub enum MessageType {
/// Request a KeyPackage from a node.
KeyPackageRequest = 0x10,
/// Response with KeyPackage data.
KeyPackageResponse = 0x11,
/// Node has no KeyPackage available.
KeyPackageUnavailable = 0x12,
}
/// Request a KeyPackage from a peer.
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct KeyPackageRequest {
/// Who is requesting.
pub requester_addr: MeshAddress,
/// Whose KeyPackage is requested.
pub target_addr: MeshAddress,
/// Optional: specific hash to request (from announce).
pub hash: Option<[u8; 8]>,
/// Request ID for correlation.
pub request_id: u32,
}
impl KeyPackageRequest {
/// Create a new request.
pub fn new(requester: MeshAddress, target: MeshAddress) -> Self {
Self {
requester_addr: requester,
target_addr: target,
hash: None,
request_id: rand::random(),
}
}
/// Create with specific hash.
pub fn with_hash(requester: MeshAddress, target: MeshAddress, hash: [u8; 8]) -> Self {
Self {
requester_addr: requester,
target_addr: target,
hash: Some(hash),
request_id: rand::random(),
}
}
/// Serialize to CBOR.
pub fn to_wire(&self) -> Vec<u8> {
let mut buf = Vec::new();
buf.push(MessageType::KeyPackageRequest as u8);
ciborium::into_writer(self, &mut buf).expect("CBOR serialization");
buf
}
/// Deserialize from CBOR (after type byte).
pub fn from_wire(bytes: &[u8]) -> anyhow::Result<Self> {
if bytes.is_empty() || bytes[0] != MessageType::KeyPackageRequest as u8 {
anyhow::bail!("not a KeyPackageRequest");
}
let req: Self = ciborium::from_reader(&bytes[1..])?;
Ok(req)
}
}
/// Response with KeyPackage data.
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct KeyPackageResponse {
/// Whose KeyPackage this is.
pub owner_addr: MeshAddress,
/// The serialized MLS KeyPackage.
pub keypackage_bytes: Vec<u8>,
/// Hash of the KeyPackage (for verification).
pub hash: [u8; 8],
/// Matching request ID.
pub request_id: u32,
}
impl KeyPackageResponse {
/// Create a new response.
pub fn new(
owner: MeshAddress,
keypackage_bytes: Vec<u8>,
request_id: u32,
) -> Self {
let hash = crate::announce::compute_keypackage_hash(&keypackage_bytes);
Self {
owner_addr: owner,
keypackage_bytes,
hash,
request_id,
}
}
/// Serialize to CBOR.
pub fn to_wire(&self) -> Vec<u8> {
let mut buf = Vec::new();
buf.push(MessageType::KeyPackageResponse as u8);
ciborium::into_writer(self, &mut buf).expect("CBOR serialization");
buf
}
/// Deserialize from CBOR (after type byte).
pub fn from_wire(bytes: &[u8]) -> anyhow::Result<Self> {
if bytes.is_empty() || bytes[0] != MessageType::KeyPackageResponse as u8 {
anyhow::bail!("not a KeyPackageResponse");
}
let resp: Self = ciborium::from_reader(&bytes[1..])?;
Ok(resp)
}
/// Verify the hash matches the KeyPackage.
pub fn verify_hash(&self) -> bool {
let computed = crate::announce::compute_keypackage_hash(&self.keypackage_bytes);
computed == self.hash
}
}
/// Response indicating no KeyPackage available.
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct KeyPackageUnavailable {
/// Whose KeyPackage was requested.
pub target_addr: MeshAddress,
/// Matching request ID.
pub request_id: u32,
}
impl KeyPackageUnavailable {
/// Create a new unavailable response.
pub fn new(target: MeshAddress, request_id: u32) -> Self {
Self {
target_addr: target,
request_id,
}
}
/// Serialize to CBOR.
pub fn to_wire(&self) -> Vec<u8> {
let mut buf = Vec::new();
buf.push(MessageType::KeyPackageUnavailable as u8);
ciborium::into_writer(self, &mut buf).expect("CBOR serialization");
buf
}
/// Deserialize from CBOR (after type byte).
pub fn from_wire(bytes: &[u8]) -> anyhow::Result<Self> {
if bytes.is_empty() || bytes[0] != MessageType::KeyPackageUnavailable as u8 {
anyhow::bail!("not a KeyPackageUnavailable");
}
let resp: Self = ciborium::from_reader(&bytes[1..])?;
Ok(resp)
}
}
/// Parse the message type from wire bytes.
pub fn parse_message_type(bytes: &[u8]) -> Option<MessageType> {
if bytes.is_empty() {
return None;
}
match bytes[0] {
0x10 => Some(MessageType::KeyPackageRequest),
0x11 => Some(MessageType::KeyPackageResponse),
0x12 => Some(MessageType::KeyPackageUnavailable),
_ => None,
}
}
#[cfg(test)]
mod tests {
use super::*;
fn make_address(seed: u8) -> MeshAddress {
MeshAddress::from_bytes([seed; 16])
}
#[test]
fn request_roundtrip() {
let req = KeyPackageRequest::new(make_address(1), make_address(2));
let wire = req.to_wire();
let restored = KeyPackageRequest::from_wire(&wire).expect("parse");
assert_eq!(req.requester_addr, restored.requester_addr);
assert_eq!(req.target_addr, restored.target_addr);
assert_eq!(req.request_id, restored.request_id);
}
#[test]
fn request_with_hash_roundtrip() {
let hash = [0xAB; 8];
let req = KeyPackageRequest::with_hash(make_address(1), make_address(2), hash);
let wire = req.to_wire();
let restored = KeyPackageRequest::from_wire(&wire).expect("parse");
assert_eq!(req.hash, restored.hash);
assert_eq!(Some(hash), restored.hash);
}
#[test]
fn response_roundtrip() {
let kp_bytes = vec![0x42; 100];
let resp = KeyPackageResponse::new(make_address(3), kp_bytes.clone(), 12345);
let wire = resp.to_wire();
let restored = KeyPackageResponse::from_wire(&wire).expect("parse");
assert_eq!(resp.owner_addr, restored.owner_addr);
assert_eq!(resp.keypackage_bytes, restored.keypackage_bytes);
assert_eq!(resp.hash, restored.hash);
assert_eq!(resp.request_id, restored.request_id);
assert!(restored.verify_hash());
}
#[test]
fn unavailable_roundtrip() {
let resp = KeyPackageUnavailable::new(make_address(4), 99999);
let wire = resp.to_wire();
let restored = KeyPackageUnavailable::from_wire(&wire).expect("parse");
assert_eq!(resp.target_addr, restored.target_addr);
assert_eq!(resp.request_id, restored.request_id);
}
#[test]
fn parse_message_type_works() {
let req = KeyPackageRequest::new(make_address(1), make_address(2));
let wire = req.to_wire();
assert_eq!(parse_message_type(&wire), Some(MessageType::KeyPackageRequest));
let resp = KeyPackageResponse::new(make_address(3), vec![0x42], 1);
let wire = resp.to_wire();
assert_eq!(parse_message_type(&wire), Some(MessageType::KeyPackageResponse));
let unavail = KeyPackageUnavailable::new(make_address(4), 2);
let wire = unavail.to_wire();
assert_eq!(parse_message_type(&wire), Some(MessageType::KeyPackageUnavailable));
assert_eq!(parse_message_type(&[]), None);
assert_eq!(parse_message_type(&[0xFF]), None);
}
#[test]
fn measure_protocol_overhead() {
let req = KeyPackageRequest::new(make_address(1), make_address(2));
let wire = req.to_wire();
println!("KeyPackageRequest: {} bytes", wire.len());
let kp_bytes = vec![0x42; 306]; // Typical MLS KeyPackage size
let resp = KeyPackageResponse::new(make_address(3), kp_bytes.clone(), 12345);
let wire = resp.to_wire();
println!("KeyPackageResponse (306B payload): {} bytes", wire.len());
println!("Response overhead: {} bytes", wire.len() - 306);
let unavail = KeyPackageUnavailable::new(make_address(4), 99999);
let wire = unavail.to_wire();
println!("KeyPackageUnavailable: {} bytes", wire.len());
// Assertions
assert!(req.to_wire().len() < 100, "request should be compact");
assert!(unavail.to_wire().len() < 50, "unavailable should be compact");
}
}