Files
quicproquo/crates/quicproquo-rpc/src/auth_handshake.rs
Christian Nennemann f09dbe10ce feat(rpc): auth handshake, server-push broker, audit logging
- auth_handshake.rs: connection-init protocol (magic 0x01, token, ack)
- push.rs: PushBroker manages per-identity push connections with gc
- server.rs: ConnectionState, auth handshake on first bi-stream, pass
  identity_key/session_token to RequestContext per stream
- client.rs: session_token in RpcClientConfig, auto auth handshake on connect
- middleware.rs: log_rpc_call with SHA-256 redaction, hex_prefix helper
- lib.rs: export auth_handshake and push modules
2026-03-04 12:08:20 +01:00

136 lines
3.6 KiB
Rust

//! Auth handshake protocol for QUIC connections.
//!
//! The session token is sent as a "connection init" message on the first
//! bi-stream before any RPC calls.
//!
//! ## Protocol
//! ```text
//! Client → Server: [0x01 magic][token_len: u16 BE][token bytes]
//! Server → Client: [0x01 magic][0x00 status OK]
//! ```
use crate::error::RpcError;
/// Magic byte identifying an auth init frame.
pub const AUTH_INIT_MAGIC: u8 = 0x01;
/// Status byte: auth accepted.
const AUTH_STATUS_OK: u8 = 0x00;
/// Maximum token length (64 KiB).
const MAX_TOKEN_LEN: usize = 65535;
/// Write an auth init frame to a QUIC send stream.
pub async fn send_auth_init(
send: &mut quinn::SendStream,
token: &[u8],
) -> Result<(), RpcError> {
if token.len() > MAX_TOKEN_LEN {
return Err(RpcError::Encode(format!(
"auth token too large: {} bytes (max {MAX_TOKEN_LEN})",
token.len()
)));
}
let token_len = token.len() as u16;
let mut buf = Vec::with_capacity(1 + 2 + token.len());
buf.push(AUTH_INIT_MAGIC);
buf.extend_from_slice(&token_len.to_be_bytes());
buf.extend_from_slice(token);
send.write_all(&buf)
.await
.map_err(|e| RpcError::Connection(format!("send auth init: {e}")))?;
Ok(())
}
/// Read an auth init frame from a QUIC recv stream.
pub async fn recv_auth_init(
recv: &mut quinn::RecvStream,
) -> Result<Vec<u8>, RpcError> {
// Read magic byte.
let mut header = [0u8; 3];
recv.read_exact(&mut header)
.await
.map_err(|e| RpcError::Connection(format!("read auth init header: {e}")))?;
if header[0] != AUTH_INIT_MAGIC {
return Err(RpcError::Decode(format!(
"bad auth init magic: expected 0x{AUTH_INIT_MAGIC:02x}, got 0x{:02x}",
header[0]
)));
}
let token_len = u16::from_be_bytes([header[1], header[2]]) as usize;
if token_len > MAX_TOKEN_LEN {
return Err(RpcError::Decode(format!(
"auth token length {token_len} exceeds max {MAX_TOKEN_LEN}"
)));
}
let mut token = vec![0u8; token_len];
if token_len > 0 {
recv.read_exact(&mut token)
.await
.map_err(|e| RpcError::Connection(format!("read auth token: {e}")))?;
}
Ok(token)
}
/// Send auth ack (success response).
pub async fn send_auth_ack(
send: &mut quinn::SendStream,
) -> Result<(), RpcError> {
let buf = [AUTH_INIT_MAGIC, AUTH_STATUS_OK];
send.write_all(&buf)
.await
.map_err(|e| RpcError::Connection(format!("send auth ack: {e}")))?;
Ok(())
}
/// Read auth ack from the server.
pub async fn recv_auth_ack(
recv: &mut quinn::RecvStream,
) -> Result<(), RpcError> {
let mut buf = [0u8; 2];
recv.read_exact(&mut buf)
.await
.map_err(|e| RpcError::Connection(format!("read auth ack: {e}")))?;
if buf[0] != AUTH_INIT_MAGIC {
return Err(RpcError::Decode(format!(
"bad auth ack magic: expected 0x{AUTH_INIT_MAGIC:02x}, got 0x{:02x}",
buf[0]
)));
}
if buf[1] != AUTH_STATUS_OK {
return Err(RpcError::Decode(format!(
"auth rejected with status 0x{:02x}",
buf[1]
)));
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn auth_init_magic_is_0x01() {
assert_eq!(AUTH_INIT_MAGIC, 0x01);
}
#[test]
fn token_too_large_returns_encode_error() {
// We cannot call send_auth_init without a real stream, but we can
// verify the length check logic by constructing the guard condition.
let big_token = vec![0u8; MAX_TOKEN_LEN + 1];
assert!(big_token.len() > MAX_TOKEN_LEN);
}
}