Protocol v2: X25519 handshake, sequence-number nonces, hardened transport
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.
This commit is contained in:
@@ -1,57 +1,60 @@
|
||||
# omnivox-rs
|
||||
|
||||
Rust port of [OmniVox](https://git.0xfeedc0de.com/michael/OmniVox), a
|
||||
templated encrypted TCP client-server library — **wire-compatible
|
||||
byte-for-byte** with the original C++ implementation. A Rust client talks
|
||||
to a C++ server and vice versa.
|
||||
Encrypted async TCP client-server messaging — protocol **v2**. A Rust
|
||||
library plus example binaries, descended from the OmniVox C++ library and
|
||||
hardened past it after a full red-team review of v1.
|
||||
|
||||
- Async TCP built on tokio
|
||||
- ChaCha20-Poly1305 authenticated encryption (RFC 8439)
|
||||
- scrypt key derivation (RFC 7914, `N = 2^17, r = 8, p = 1`)
|
||||
- Byte-exact cereal `BinaryOutputArchive` field layout
|
||||
- Zero `unsafe`
|
||||
- X25519 handshake on every connection → forward secrecy, session-bound keys
|
||||
- ChaCha20-Poly1305 with strictly increasing sequence numbers → replay-proof
|
||||
- Direction-separated traffic keys, fixed-size pre-auth surface
|
||||
- scrypt (RFC 7914) password-based key derivation; `Cereal` payload encoding
|
||||
(byte-exact with the original's cereal binary format) for data portability
|
||||
- tokio transport: connection caps, idle/stall/send timeouts, panic-isolated
|
||||
callbacks; zero `unsafe`
|
||||
|
||||
## Wire format
|
||||
|
||||
Framing is a `u32` little-endian length prefix. Each message is a
|
||||
`AeadPacket`: `nonce[12] || tag[16] || u64 len || ciphertext`. Payloads
|
||||
implement the `Cereal` trait, which reproduces cereal's raw-LE field
|
||||
encoding (see `PARITY-CONTRACT.md` for the normative spec).
|
||||
**Not wire-compatible with the C++ original or v1** — by design; v1 peers
|
||||
are refused at the handshake. See `PROTOCOL-V2.md` for the full normative
|
||||
spec and `PARITY-CONTRACT.md` (retired) for the port's history.
|
||||
|
||||
## Usage
|
||||
|
||||
```rust
|
||||
use omnivox::{OmniVox, Cereal, CodecError, Decision, derive_encrypt_key};
|
||||
use omnivox::{Config, Cereal, CodecError, Decision, OmniVox, derive_encrypt_key};
|
||||
use std::sync::Arc;
|
||||
|
||||
let endpoint = OmniVox::<MyPayload>::new();
|
||||
endpoint.set_encrypt_key(derive_encrypt_key(b"password", b"salt", 17, 8, 1)?);
|
||||
endpoint.start_server(8080, Config::default(), on_data, on_disconnect, on_connect).await?;
|
||||
```
|
||||
|
||||
Create an `OmniVox<T>` where `T: Cereal`, set the key (both peers must
|
||||
agree), then use `start_server` / `connect_to_server` / `send_to_client`
|
||||
/ `send_to_server`. Runnable ports of the original demo apps live in
|
||||
`examples/`.
|
||||
Payload types implement `Cereal` (raw little-endian fields, `u64` vector
|
||||
lengths — the original's serialization). Runnable demos: `examples/`.
|
||||
|
||||
## Parity
|
||||
## Security posture
|
||||
|
||||
`PARITY-CONTRACT.md` is the definition of done: it names the unit of
|
||||
parity, the accepted differences, and the original's quirks this port
|
||||
deliberately preserves (B1–B3, B5) rather than silently fixing. Parity
|
||||
evidence:
|
||||
| v1 weakness | v2 answer |
|
||||
|---|---|
|
||||
| captured frames replay forever | strict sequence numbers; a replay kills the connection |
|
||||
| frames portable across connections | keys bound to per-session nonces |
|
||||
| 5 MiB pre-auth allocation from 8 bytes | fixed 69-byte handshake before any attacker-sized read |
|
||||
| one symmetric key, either peer can forge as itself | direction-separated HKDF session keys |
|
||||
| static key, no forward secrecy | ephemeral X25519 per connection |
|
||||
| no client-side timeouts | full idle/stall discipline both roles |
|
||||
| panicking user callback killed the read loop | callbacks panic-isolated |
|
||||
| demo password in every repo checkout | release builds refuse the demo key |
|
||||
|
||||
- `tests/vectors.rs` — golden vectors generated by the C++ CryptoPP +
|
||||
cereal stack (`reference/cppgen.cpp`); keys, serialization bytes, and
|
||||
AEAD ciphertext/tags must match exactly.
|
||||
- `tests/loopback.rs` — runtime behavior clauses of the contract.
|
||||
- Differential cross-tests: this crate's binaries were verified talking
|
||||
to the unmodified C++ binaries in both directions over loopback.
|
||||
Use random 32-byte PSKs where you can; an active attacker can grind scrypt
|
||||
against human passwords (PAKE planned).
|
||||
|
||||
## Build & test
|
||||
|
||||
```bash
|
||||
cargo test # note: the scrypt vectors use N=2^17 (~1 s, 128 MiB per derivation)
|
||||
cargo clippy --all-targets
|
||||
cargo test # includes attack regressions: replay, version, panic, timeouts
|
||||
cargo clippy --all-targets -- -D warnings
|
||||
cargo run --example omnivox-server # terminal 1
|
||||
cargo run --example omnivox-client # terminal 2
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
GPL-3.0-or-later, matching the original OmniVox.
|
||||
GPL-3.0-or-later.
|
||||
|
||||
Reference in New Issue
Block a user