Red-team review of the (byte-exact) v1 port found replay, cross-session frame portability, pre-auth allocation amplification, and a symmetric key that let either peer forge as itself — flaws inherent to the v1 wire format. v2 closes them: - fixed 69-byte OVX2 hello; server validates before replying; v1 and wrong-version peers see an immediate close - HKDF(ephemeral X25519 DH, PSK, transcript) -> direction-separated session keys; forward secrecy per connection - strictly increasing u64 sequence nonces: any replay or reorder drops the connection (regression tests in tests/protocol_v2.rs) - Config: message-size cap (64 KiB default), connection semaphore, idle/body-stall/handshake/send timeouts on both roles (client-side timers retire the old no-client-timeouts quirk) - message processing inline per connection; user callbacks panic- isolated; poison-tolerant mutexes; demo key refused in release builds - PROTOCOL-V2.md supersedes PARITY-CONTRACT.md (retired, kept as port history); Cereal payload encoding is unchanged Payload serialization remains byte-exact with the original C++ (the golden scrypt/cereal vectors still govern). Zero unsafe. Verified live: server<->client handshake, encrypted echo, clean teardown. Cuts over from the C++ implementation, which is deleted (phase 5 of the port contract); it remains in the original gitea repository.
43 lines
1.5 KiB
Rust
43 lines
1.5 KiB
Rust
//! Shared helpers for the integration tests: v2 client sessions against a
|
|
//! live server, and raw frame construction.
|
|
|
|
use omnivox::{Cereal, CodecError, Session, encrypt_with_nonce, frame_nonce, handshake::perform};
|
|
use tokio::net::TcpStream;
|
|
use tokio::net::tcp::{OwnedReadHalf, OwnedWriteHalf};
|
|
|
|
#[derive(Debug, Default)]
|
|
pub struct Msg(pub Vec<u8>);
|
|
|
|
impl Cereal for Msg {
|
|
fn encode(&self, out: &mut Vec<u8>) {
|
|
omnivox::write_byte_vec(out, &self.0);
|
|
}
|
|
fn decode(bytes: &[u8]) -> Result<Self, CodecError> {
|
|
Ok(Self(omnivox::Reader::new(bytes).byte_vec()?))
|
|
}
|
|
}
|
|
|
|
pub const TEST_PSK: [u8; 32] = [42u8; 32];
|
|
|
|
/// Builds one framed message with the given sequence number.
|
|
pub fn frame(key: &[u8; 32], counter: u64, msg: &[u8]) -> Vec<u8> {
|
|
let mut plain = Vec::new();
|
|
Msg(msg.to_vec()).encode(&mut plain);
|
|
let pkt = encrypt_with_nonce(key, &frame_nonce(counter), &plain);
|
|
let mut bytes = Vec::new();
|
|
pkt.encode(&mut bytes);
|
|
let mut out = Vec::new();
|
|
out.extend_from_slice(&(bytes.len() as u32).to_le_bytes());
|
|
out.extend_from_slice(&bytes);
|
|
out
|
|
}
|
|
|
|
/// Connects and completes the v2 handshake as the client role, returning
|
|
/// the raw halves plus the session keys for manual framing.
|
|
pub async fn client_session(port: u16, psk: &[u8; 32]) -> (OwnedReadHalf, OwnedWriteHalf, Session) {
|
|
let stream = TcpStream::connect(("127.0.0.1", port)).await.unwrap();
|
|
let (mut r, mut w) = stream.into_split();
|
|
let session = perform(&mut r, &mut w, psk, false).await.unwrap();
|
|
(r, w, session)
|
|
}
|