feat(proto): implement Wave 2 — hybrid PKI handshake + session

aura-proto: 5-byte wire header + Frame codec (§6.1/§6.3); transport-agnostic
handshake state machine (§6.2) over split tokio AsyncRead/AsyncWrite —
hybrid X25519+ML-KEM-768 KEM, SHA-256 transcript, mutual X.509 auth with
ECDSA-P256 transcript signatures (ring), constant-time HMAC Finished;
Session with sliding-window replay protection. 13 tests green, clippy clean.

Handshake message order pinned (resolves spec diagram ambiguity); reader/writer
taken by value since Session owns both halves.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
xah30
2026-05-25 18:05:11 +03:00
parent b8ce58ddf0
commit bb835e4ca7
11 changed files with 1710 additions and 1 deletions
+136
View File
@@ -0,0 +1,136 @@
//! `test_data_exchange_1000pkts` — after the handshake, exchange 1000 Data frames in each
//! direction and assert payload integrity and ordering.
mod common;
use aura_proto::{client_handshake, server_handshake, Frame};
use bytes::Bytes;
use tokio::io::split;
const N: u32 = 1000;
/// Build the deterministic payload for frame `i` from `who`.
fn payload_for(who: &str, i: u32) -> Bytes {
Bytes::from(format!(
"{who}-packet-{i}-{}",
"x".repeat((i % 37) as usize)
))
}
#[tokio::test]
async fn test_data_exchange_1000pkts() {
let pki = common::mint_pki("vpn.aura.example", "client-alpha");
let client_cfg = pki.client_config();
let server_cfg = pki.server_config();
let (client_end, server_end) = tokio::io::duplex(64 * 1024);
let (c_read, c_write) = split(client_end);
let (s_read, s_write) = split(server_end);
let client = tokio::spawn(async move {
let mut sess = client_handshake(c_read, c_write, &client_cfg)
.await
.expect("client handshake");
// Interleave send + recv in lockstep to avoid filling the duplex buffer.
for i in 0..N {
sess.send_frame(Frame::Data {
stream_id: 1,
payload: payload_for("client", i),
})
.await
.expect("client send");
match sess.recv_frame().await.expect("client recv") {
Frame::Data { stream_id, payload } => {
assert_eq!(stream_id, 2, "wrong stream id at i={i}");
assert_eq!(
payload,
payload_for("server", i),
"payload mismatch at i={i}"
);
}
other => panic!("client expected Data, got {other:?}"),
}
}
});
let server = tokio::spawn(async move {
let mut sess = server_handshake(s_read, s_write, &server_cfg)
.await
.expect("server handshake");
for i in 0..N {
// Receive the client's i-th packet first, then reply, mirroring the client's lockstep.
match sess.recv_frame().await.expect("server recv") {
Frame::Data { stream_id, payload } => {
assert_eq!(stream_id, 1, "wrong stream id at i={i}");
assert_eq!(
payload,
payload_for("client", i),
"payload mismatch at i={i}"
);
}
other => panic!("server expected Data, got {other:?}"),
}
sess.send_frame(Frame::Data {
stream_id: 2,
payload: payload_for("server", i),
})
.await
.expect("server send");
}
});
let (c, s) = tokio::join!(client, server);
c.expect("client task");
s.expect("server task");
}
#[tokio::test]
async fn ping_pong_and_close_frames_roundtrip() {
let pki = common::mint_pki("vpn.aura.example", "c1");
let client_cfg = pki.client_config();
let server_cfg = pki.server_config();
let (client_end, server_end) = tokio::io::duplex(64 * 1024);
let (c_read, c_write) = split(client_end);
let (s_read, s_write) = split(server_end);
let client = tokio::spawn(async move {
let mut sess = client_handshake(c_read, c_write, &client_cfg)
.await
.unwrap();
sess.send_frame(Frame::Ping { seq: 7 }).await.unwrap();
match sess.recv_frame().await.unwrap() {
Frame::Pong { seq } => assert_eq!(seq, 7),
other => panic!("expected Pong, got {other:?}"),
}
sess.send_frame(Frame::Close {
code: 0,
reason: "bye".into(),
})
.await
.unwrap();
});
let server = tokio::spawn(async move {
let mut sess = server_handshake(s_read, s_write, &server_cfg)
.await
.unwrap();
match sess.recv_frame().await.unwrap() {
Frame::Ping { seq } => sess.send_frame(Frame::Pong { seq }).await.unwrap(),
other => panic!("expected Ping, got {other:?}"),
}
match sess.recv_frame().await.unwrap() {
Frame::Close { code, reason } => {
assert_eq!(code, 0);
assert_eq!(reason, "bye");
}
other => panic!("expected Close, got {other:?}"),
}
});
let (c, s) = tokio::join!(client, server);
c.unwrap();
s.unwrap();
}