feat(proto,pki,cli): in-band CRL push (closes last v2 limitation)
Server now pushes its signed CRL to each connecting client right after the
handshake; the client verifies the signature against the CA and applies the
revocation list to its verifier (and caches it on disk for restarts).
Removes the v1 "CRL distributed out-of-band" honest limitation.
Wire (multiplexed over existing PacketConnection, no trait change):
control envelope = MAGIC[4]=[0xAA,0xAA,0xC0,0x01] || kind(u8) || u32_be(len)
|| payload. IPv4/IPv6 start with 0x4X/0x6X, so 0xAA cannot collide; an old
peer just drops it as a junk packet in the TUN — back-compat preserved.
- aura-proto: ControlKind { CrlPush, CrlAck, Unknown }, encode/decode_control_
envelope, CONTROL_ENVELOPE_MAGIC; 7 frame tests.
- aura-pki: CrlStore::{encode_signed, save_signed, decode_signed_verified,
load_signed_verified} — ECDSA-P256/SHA-256 from the CA private key against
a textual "CRL-Aura-v1" body + --SIGNATURE--; 7 signing tests. ring 0.17
added crate-local (already in lockfile via rustls-webpki).
- aura-cli: crl_push module — server pushes via conn.send_packet on accept;
client wraps the Arc<dyn PacketConnection> in AcceptPushedCrlConn which
sniffs the magic in recv_packet, verifies the signature, updates the
AuraCertVerifier, caches to disk. PkiSection gets ca_key, crl_push (default
true), accept_pushed_crl (default true).
- 5 in_band_crl integration tests via mock PacketConnection.
Workspace: 235 tests passed (+28), clippy -D warnings clean, fmt clean. v2
COMPLETE — all 9 honest v1 limitations resolved (except sing-box, per user).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -176,6 +176,48 @@ mod frame_tag {
|
||||
pub const CLOSE: u8 = 0x04;
|
||||
}
|
||||
|
||||
/// Kinds of in-band control message carried inside a [`CONTROL_ENVELOPE_MAGIC`]-prefixed payload.
|
||||
///
|
||||
/// The wire byte is the discriminant. Unknown values decode as [`ControlKind::Unknown`] so peers
|
||||
/// running older builds gracefully ignore future kinds without dropping the connection.
|
||||
///
|
||||
/// v2's CRL push reuses the existing post-handshake [`crate::PacketConnection::send_packet`] path
|
||||
/// rather than introducing a new [`Frame`] variant: a real IPv4/IPv6 packet always starts with
|
||||
/// `0x4X` / `0x6X`, so the 4-byte magic [`CONTROL_ENVELOPE_MAGIC`] (which starts with `0xAA`) can
|
||||
/// be safely multiplexed alongside ordinary packets without changing the on-wire frame schema or
|
||||
/// any transport-level `match Frame` that already exists.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum ControlKind {
|
||||
/// Server -> client: push the server's current CRL (signed payload).
|
||||
CrlPush,
|
||||
/// Client -> server: acknowledge a [`ControlKind::CrlPush`].
|
||||
CrlAck,
|
||||
/// Any byte the receiver does not recognise. The connection keeps running.
|
||||
Unknown(u8),
|
||||
}
|
||||
|
||||
impl ControlKind {
|
||||
/// Encode this control kind to its on-wire byte.
|
||||
#[must_use]
|
||||
pub fn to_u8(self) -> u8 {
|
||||
match self {
|
||||
ControlKind::CrlPush => 0x01,
|
||||
ControlKind::CrlAck => 0x02,
|
||||
ControlKind::Unknown(b) => b,
|
||||
}
|
||||
}
|
||||
|
||||
/// Decode an on-wire byte into a [`ControlKind`]. Unknown bytes yield [`ControlKind::Unknown`].
|
||||
#[must_use]
|
||||
pub fn from_u8(b: u8) -> Self {
|
||||
match b {
|
||||
0x01 => ControlKind::CrlPush,
|
||||
0x02 => ControlKind::CrlAck,
|
||||
other => ControlKind::Unknown(other),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Application-level frames carried inside encrypted [`MsgType::Data`] records (§6.3).
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub enum Frame {
|
||||
@@ -289,6 +331,64 @@ fn read_u32(buf: &[u8], what: &'static str) -> Result<u32, ProtoError> {
|
||||
Ok(u32::from_be_bytes(bytes))
|
||||
}
|
||||
|
||||
/// Magic prefix marking a v2 control-envelope multiplexed through [`PacketConnection::send_packet`].
|
||||
///
|
||||
/// An IPv4 packet's first byte is `0x4X` and an IPv6 packet's first byte is `0x6X`, so the four
|
||||
/// magic bytes `[0xAA, 0xAA, 0xC0, 0x01]` can never collide with a real IP packet — the TUN layer
|
||||
/// already rejects anything starting with a byte whose top nibble is not `4` or `6`.
|
||||
///
|
||||
/// Envelope layout:
|
||||
///
|
||||
/// ```text
|
||||
/// CONTROL_ENVELOPE_MAGIC (4 bytes) || kind (u8) || u32_be(payload_len) || payload
|
||||
/// ```
|
||||
pub const CONTROL_ENVELOPE_MAGIC: [u8; 4] = [0xAA, 0xAA, 0xC0, 0x01];
|
||||
|
||||
/// Build a control envelope around `kind` + `payload`, suitable for
|
||||
/// [`crate::PacketConnection::send_packet`].
|
||||
///
|
||||
/// Layout: `MAGIC(4) || kind(u8) || u32_be(payload_len) || payload`.
|
||||
#[must_use]
|
||||
pub fn encode_control_envelope(kind: ControlKind, payload: &[u8]) -> Vec<u8> {
|
||||
let mut out = Vec::with_capacity(CONTROL_ENVELOPE_MAGIC.len() + 1 + 4 + payload.len());
|
||||
out.extend_from_slice(&CONTROL_ENVELOPE_MAGIC);
|
||||
out.push(kind.to_u8());
|
||||
out.extend_from_slice(&(payload.len() as u32).to_be_bytes());
|
||||
out.extend_from_slice(payload);
|
||||
out
|
||||
}
|
||||
|
||||
/// Try to decode a buffer as a control envelope.
|
||||
///
|
||||
/// Returns `None` if `buf` does not start with [`CONTROL_ENVELOPE_MAGIC`] (i.e. it is a normal IP
|
||||
/// packet). Returns [`ProtoError::MalformedFrame`] if the buffer starts with the magic but is
|
||||
/// truncated or its length field overflows the buffer.
|
||||
pub fn decode_control_envelope(buf: &[u8]) -> Result<Option<(ControlKind, Vec<u8>)>, ProtoError> {
|
||||
if buf.len() < CONTROL_ENVELOPE_MAGIC.len() || &buf[..4] != CONTROL_ENVELOPE_MAGIC.as_slice() {
|
||||
return Ok(None);
|
||||
}
|
||||
let rest = &buf[CONTROL_ENVELOPE_MAGIC.len()..];
|
||||
let kind_byte = *rest
|
||||
.first()
|
||||
.ok_or(ProtoError::MalformedFrame("control envelope: missing kind"))?;
|
||||
let kind = ControlKind::from_u8(kind_byte);
|
||||
let len_bytes: [u8; 4] = rest
|
||||
.get(1..5)
|
||||
.ok_or(ProtoError::MalformedFrame(
|
||||
"control envelope: missing payload length",
|
||||
))?
|
||||
.try_into()
|
||||
.expect("slice of length 4 converts to [u8; 4]");
|
||||
let payload_len = u32::from_be_bytes(len_bytes) as usize;
|
||||
let payload = rest
|
||||
.get(5..5 + payload_len)
|
||||
.ok_or(ProtoError::MalformedFrame(
|
||||
"control envelope: truncated payload",
|
||||
))?
|
||||
.to_vec();
|
||||
Ok(Some((kind, payload)))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -368,4 +468,49 @@ mod tests {
|
||||
assert!(Frame::decode(&[frame_tag::PING, 0x00]).is_err()); // truncated u32
|
||||
assert!(Frame::decode(&[frame_tag::CLOSE]).is_err()); // missing code
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn control_envelope_roundtrip() {
|
||||
let env = encode_control_envelope(ControlKind::CrlPush, b"hello");
|
||||
assert_eq!(&env[..4], &CONTROL_ENVELOPE_MAGIC);
|
||||
let (kind, payload) = decode_control_envelope(&env).unwrap().unwrap();
|
||||
assert_eq!(kind, ControlKind::CrlPush);
|
||||
assert_eq!(payload, b"hello");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn control_envelope_skips_normal_ip_packets() {
|
||||
// IPv4 packet: first byte's top nibble is 4. Never collides with magic.
|
||||
let ipv4 = vec![0x45u8, 0x00, 0x00, 0x14];
|
||||
assert!(decode_control_envelope(&ipv4).unwrap().is_none());
|
||||
// IPv6 packet: first byte's top nibble is 6.
|
||||
let ipv6 = vec![0x60u8, 0x00, 0x00, 0x00];
|
||||
assert!(decode_control_envelope(&ipv6).unwrap().is_none());
|
||||
// Random short bytes that do not match the magic.
|
||||
let other = vec![0xAAu8, 0xAA, 0xC0, 0x02];
|
||||
assert!(decode_control_envelope(&other).unwrap().is_none());
|
||||
// Shorter than the magic.
|
||||
assert!(decode_control_envelope(&[0xAA, 0xAA]).unwrap().is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn control_envelope_rejects_truncated_payload() {
|
||||
let mut env = encode_control_envelope(ControlKind::CrlPush, b"payload-bytes");
|
||||
// Trim a few bytes from the end to truncate the payload claimed by the length field.
|
||||
env.truncate(env.len() - 3);
|
||||
assert!(decode_control_envelope(&env).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn control_envelope_unknown_kind_decodes_as_unknown() {
|
||||
// Hand-craft an envelope with a future kind byte.
|
||||
let mut env = Vec::new();
|
||||
env.extend_from_slice(&CONTROL_ENVELOPE_MAGIC);
|
||||
env.push(0x77); // unknown kind
|
||||
env.extend_from_slice(&3u32.to_be_bytes());
|
||||
env.extend_from_slice(b"abc");
|
||||
let (kind, payload) = decode_control_envelope(&env).unwrap().unwrap();
|
||||
assert_eq!(kind, ControlKind::Unknown(0x77));
|
||||
assert_eq!(payload, b"abc");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user