Remove Noise protocol references from wiki docs and tests

Delete 8 Noise-specific documentation pages (noise-xx.md,
transport-keys.md, adr-001/003/006, framing-codec.md) and update
~30 remaining wiki pages to reflect QUIC+TLS as the sole transport.
Remove obsolete Noise-based integration tests (auth_service.rs,
mls_group.rs). Code-side Noise removal was done in f334ed3.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-22 08:25:23 +01:00
parent f334ed3d43
commit 9fdb37876a
36 changed files with 125 additions and 2201 deletions

View File

@@ -84,7 +84,7 @@ Consumers import types from these modules. For example, `node_capnp::node_servic
## The Envelope schema
The `Envelope` is the top-level wire message for all quicnprotochat traffic. Every frame exchanged between peers (whether over Noise or QUIC) is serialised as an Envelope:
The `Envelope` is the top-level wire message for all quicnprotochat traffic. Every frame exchanged between peers is serialised as an Envelope:
```capnp
struct Envelope {
@@ -149,7 +149,7 @@ Two functions handle the conversion between `ParsedEnvelope` and wire bytes:
pub fn build_envelope(env: &ParsedEnvelope) -> Result<Vec<u8>, capnp::Error>
```
Serialises a `ParsedEnvelope` to unpacked Cap'n Proto wire bytes. The output includes the Cap'n Proto segment table header followed by the message data. These bytes are suitable as the body of a length-prefixed frame (the `LengthPrefixedCodec` in `quicnprotochat-core` prepends the 4-byte length) or as a payload within a QUIC stream.
Serialises a `ParsedEnvelope` to unpacked Cap'n Proto wire bytes. The output includes the Cap'n Proto segment table header followed by the message data. These bytes are suitable as a payload within a QUIC stream.
Internally, it builds a `capnp::message::Builder`, populates an `Envelope` root, and serialises via `capnp::serialize::write_message`.
@@ -242,8 +242,6 @@ Client Server
The `tokio-util` compat layer converts Quinn stream types into `futures::AsyncRead + AsyncWrite`, which `capnp-rpc`'s `VatNetwork` expects. See [QUIC + TLS 1.3](quic-tls.md) for the full connection setup.
On the legacy Noise path, the `into_capnp_io()` bridge serves the same purpose -- converting a Noise-encrypted TCP connection into a byte stream for `VatNetwork`. See [Noise\_XX Handshake](noise-xx.md) for details.
## Comparison with alternatives
### vs Protocol Buffers + gRPC
@@ -275,4 +273,3 @@ These constraints keep the serialisation layer thin and auditable.
- [Auth Schema](../wire-format/auth-schema.md) -- Auth token structure and versioning.
- [MLS (RFC 9420)](mls.md) -- How MLS messages are carried as opaque payloads inside Cap'n Proto Envelopes.
- [ADR-002: Cap'n Proto over MessagePack](../design-rationale/adr-002-capnproto.md) -- Design rationale for choosing Cap'n Proto.
- [ADR-003: RPC Inside the Noise Tunnel](../design-rationale/adr-003-rpc-inside-noise.md) -- Why RPC runs inside the encrypted transport.

View File

@@ -233,7 +233,7 @@ The hybrid KEM module is:
The M5 milestone will integrate the hybrid KEM, likely as an outer encryption layer. Until then, MLS application data is protected by classical X25519 ECDH (128-bit security against classical computers, vulnerable to quantum computers).
The post-quantum gap in the transport layer ([QUIC + TLS 1.3](quic-tls.md) and [Noise\_XX](noise-xx.md)) is a separate concern tracked in [ADR-006: PQ Gap in Noise Transport](../design-rationale/adr-006-pq-gap.md).
The post-quantum gap in the transport layer ([QUIC + TLS 1.3](quic-tls.md)) is a separate concern -- TLS 1.3 key exchange uses classical ECDHE and does not yet include post-quantum key agreement.
## Security analysis
@@ -276,6 +276,5 @@ The AEAD nonce is derived deterministically from the shared secrets via HKDF. Si
- [Post-Quantum Readiness](../cryptography/post-quantum-readiness.md) -- Broader discussion of quicnprotochat's PQ strategy.
- [MLS (RFC 9420)](mls.md) -- The MLS layer that the hybrid KEM will wrap.
- [Key Lifecycle and Zeroization](../cryptography/key-lifecycle.md) -- How hybrid key material is managed and cleared.
- [ADR-006: PQ Gap in Noise Transport](../design-rationale/adr-006-pq-gap.md) -- The accepted PQ gap in the transport layers.
- [Threat Model](../cryptography/threat-model.md) -- Where hybrid KEM fits in the overall threat model.
- [Milestone Tracker](../roadmap/milestones.md) -- M5 milestone for hybrid KEM integration into MLS.

View File

@@ -339,7 +339,7 @@ For quicnprotochat's target group sizes (2-100 members), this trade-off is accep
## Wire format
All MLS messages are serialised using TLS presentation language encoding (`tls_codec`). The TLS-encoded byte vectors are what the transport layer (Noise or QUIC) and the Delivery Service see. The DS routes these blobs without parsing them.
All MLS messages are serialised using TLS presentation language encoding (`tls_codec`). The TLS-encoded byte vectors are what the transport layer (QUIC + TLS 1.3) and the Delivery Service see. The DS routes these blobs without parsing them.
The key wire message types:

View File

@@ -1,227 +0,0 @@
# Noise\_XX Handshake
quicnprotochat's M1 milestone used the Noise Protocol Framework for transport-layer encryption between peers over raw TCP. The implementation lives in `quicnprotochat-core/src/noise.rs` and uses the `snow 0.9` crate. Although the M3 architecture migrated client-server communication to [QUIC + TLS 1.3](quic-tls.md), the Noise\_XX transport remains in the codebase for direct peer-to-peer connections and integration testing.
## The Noise\_XX pattern
quicnprotochat uses the `Noise_XX_25519_ChaChaPoly_BLAKE2s` parameter set:
| Component | Choice | Rationale |
|---|---|---|
| **Pattern** | XX | Mutual authentication with no pre-shared keys required |
| **DH** | X25519 | 128-bit security level; fast; widely reviewed |
| **AEAD** | ChaCha20-Poly1305 | Constant-time on all platforms (no AES-NI dependency) |
| **Hash** | BLAKE2s | Faster than SHA-256 on software; 256-bit security level |
The XX pattern involves a three-message handshake:
```text
XX handshake (3 messages):
-> e Initiator sends ephemeral public key
<- e, ee, s, es Responder replies: ephemeral, DH(ee), static key, DH(es)
-> s, se Initiator sends static key, DH(se)
```
### Message-by-message breakdown
**Message 1: `-> e` (Initiator to Responder)**
The initiator generates an ephemeral X25519 keypair and sends the public half. At this point, no encryption is active. The ephemeral key is sent in the clear, but it reveals nothing about the initiator's identity.
**Message 2: `<- e, ee, s, es` (Responder to Initiator)**
The responder:
1. Generates its own ephemeral X25519 keypair and sends the public half (`e`).
2. Performs `DH(e_init, e_resp)` to establish a shared secret (`ee`).
3. Sends its static (long-term) X25519 public key encrypted under the `ee` shared secret (`s`).
4. Performs `DH(e_init, s_resp)` for an additional shared secret (`es`).
After this message, the initiator knows the responder's static key and can authenticate it.
**Message 3: `-> s, se` (Initiator to Responder)**
The initiator:
1. Sends its static X25519 public key encrypted under the accumulated handshake secrets (`s`).
2. Performs `DH(s_init, e_resp)` for the final shared secret (`se`).
After this message, both parties have authenticated each other's static keys and derived a symmetric session key for ChaCha20-Poly1305.
### Why XX
The XX pattern was chosen over other Noise patterns for several reasons:
- **No pre-shared keys**: Unlike IK or KK, XX does not require either party to know the other's static key before the handshake. This simplifies bootstrapping -- peers can connect to each other using only a network address.
- **Identity hiding for the initiator**: The initiator's static key is not sent until message 3, after the session is already encrypted. An eavesdropper cannot determine who is initiating the connection.
- **Mutual authentication**: Both parties prove possession of their static private keys through DH operations. Unlike the NK or NX patterns, neither party is anonymous.
- **Responder identity protection (partial)**: The responder's static key is encrypted under the `ee` DH secret in message 2, providing protection against passive eavesdroppers (but not against an active attacker who controls the initiator's ephemeral key).
## Implementation
The core type is `NoiseTransport`, defined in `quicnprotochat-core/src/noise.rs`:
```rust
pub struct NoiseTransport {
framed: Framed<TcpStream, LengthPrefixedCodec>,
session: snow::TransportState,
remote_static: Option<Vec<u8>>,
}
```
The struct wraps three components:
1. **`framed`**: A `tokio_util::codec::Framed<TcpStream, LengthPrefixedCodec>` that handles length-prefixed byte framing over TCP. Each frame is prefixed with a 4-byte little-endian length field. See [Length-Prefixed Framing Codec](../wire-format/framing-codec.md) for details on the wire format.
2. **`session`**: A `snow::TransportState` that encrypts and decrypts Noise messages. This is obtained by calling `HandshakeState::into_transport_mode()` after the three-message handshake completes.
3. **`remote_static`**: The remote peer's static X25519 public key (32 bytes), captured from the `HandshakeState` before `into_transport_mode()` consumes it. This is stored explicitly because `snow` does not guarantee that `TransportState::get_remote_static()` survives the mode transition.
### Handshake functions
Two public async functions perform the handshake:
#### `handshake_initiator`
```rust
pub async fn handshake_initiator(
stream: TcpStream,
keypair: &NoiseKeypair,
) -> Result<NoiseTransport, CoreError>
```
The initiator:
1. Parses the Noise parameter string `Noise_XX_25519_ChaChaPoly_BLAKE2s` and builds a `snow::Builder` with the local private key.
2. Wraps the TCP stream in `Framed<TcpStream, LengthPrefixedCodec>`.
3. Allocates a scratch buffer of `NOISE_MAX_MSG` (65,535) bytes.
4. **Message 1** (`-> e`): Calls `session.write_message(&[], &mut buf)` to produce the ephemeral key, then sends it as a length-prefixed frame.
5. **Message 2** (`<- e, ee, s, es`): Receives a frame and calls `session.read_message()` to process it.
6. **Message 3** (`-> s, se`): Calls `session.write_message()` again and sends the result.
7. Zeroizes the scratch buffer (it contained plaintext key material during the handshake).
8. Captures the remote static key via `session.get_remote_static()`.
9. Transitions to transport mode via `session.into_transport_mode()`.
The private key bytes are held in a `Zeroizing` wrapper and dropped immediately after `snow::Builder` clones them internally.
#### `handshake_responder`
```rust
pub async fn handshake_responder(
stream: TcpStream,
keypair: &NoiseKeypair,
) -> Result<NoiseTransport, CoreError>
```
The responder mirrors the initiator but with reversed message directions:
1. Builds a `snow::Builder` with `build_responder()`.
2. **Message 1** (`<- e`): Receives and processes the initiator's ephemeral key.
3. **Message 2** (`-> e, ee, s, es`): Produces and sends the responder's reply.
4. **Message 3** (`<- s, se`): Receives and processes the initiator's static key.
5. Same zeroization, key capture, and mode transition as the initiator.
Both functions return `CoreError::HandshakeIncomplete` if the peer closes the connection mid-handshake, `CoreError::Noise` for any snow error, or `CoreError::Codec` for TCP I/O failures.
### Transport-layer I/O
After the handshake, `NoiseTransport` provides two levels of I/O:
**Frame-level** (raw bytes):
- `send_frame(&mut self, plaintext: &[u8])` -- Encrypts plaintext with ChaCha20-Poly1305 (adding a 16-byte AEAD tag) and sends it as a length-prefixed frame. Rejects payloads exceeding `MAX_PLAINTEXT_LEN` (65,519 bytes -- the Noise maximum of 65,535 minus the 16-byte AEAD tag).
- `recv_frame(&mut self)` -- Receives a length-prefixed frame and decrypts it.
**Envelope-level** (Cap'n Proto messages):
- `send_envelope(&mut self, env: &ParsedEnvelope)` -- Serialises a `ParsedEnvelope` to Cap'n Proto wire bytes via `build_envelope()`, then calls `send_frame()`.
- `recv_envelope(&mut self)` -- Calls `recv_frame()`, then deserialises the bytes via `parse_envelope()`.
## The capnp-rpc bridge: `into_capnp_io()`
The most architecturally interesting method on `NoiseTransport` is `into_capnp_io()`, which bridges the message-oriented Noise transport with the stream-oriented `capnp-rpc` library:
```rust
pub fn into_capnp_io(mut self) -> (ReadHalf<DuplexStream>, WriteHalf<DuplexStream>)
```
### Why this bridge exists
`capnp-rpc`'s `twoparty::VatNetwork` expects `AsyncRead + AsyncWrite` byte streams, but `NoiseTransport` is message-based -- each `send_frame`/`recv_frame` call encrypts/decrypts one discrete Noise message. These two models are incompatible: a byte stream has no inherent message boundaries, while Noise requires them for its AEAD authentication.
### How it works
The bridge uses `tokio::io::duplex` to create an in-process bidirectional byte channel:
```text
capnp-rpc duplex pipe NoiseTransport
┌─────────┐ ┌─────────────────┐ ┌───────────────────┐
│ VatNetwork │◄──►│ app_stream │◄──►│ bridge task │◄──► TCP
│ (reads/ │ │ (ReadHalf + │ │ (tokio::select!) │
│ writes) │ │ WriteHalf) │ │ │
└─────────┘ └─────────────────┘ └───────────────────┘
```
1. `into_capnp_io()` creates a `tokio::io::duplex(MAX_PLAINTEXT_LEN)` pipe.
2. It spawns a background Tokio task that uses `tokio::select!` to shuttle data bidirectionally:
- **Noise -> app**: Calls `self.recv_frame()`, writes the decrypted plaintext into the pipe.
- **App -> Noise**: Reads bytes from the pipe, calls `self.send_frame()` to encrypt and send them.
3. The returned `(ReadHalf, WriteHalf)` are the application ends of the pipe, suitable for passing to `VatNetwork::new()`.
The bridge task runs until either side of the pipe closes. When `capnp-rpc` drops the pipe halves, the bridge exits cleanly.
The pipe capacity is set to `MAX_PLAINTEXT_LEN` (65,519 bytes) so that one Noise frame's worth of plaintext can be buffered without blocking.
## Remote static key extraction
After a successful handshake, `NoiseTransport::remote_static_public_key()` returns the authenticated remote peer's X25519 public key:
```rust
pub fn remote_static_public_key(&self) -> Option<&[u8]> {
self.remote_static.as_deref()
}
```
This returns `Some(&[u8])` (32 bytes) in all normal cases. `None` would indicate a snow implementation bug where the XX handshake completed without exchanging static keys.
Applications use the remote static key to:
- Verify the peer's identity against a known-good key fingerprint.
- Index the peer in a roster or routing table.
- Derive additional key material for application-layer protocols.
## Post-quantum gap (ADR-006)
The Noise transport uses classical X25519 for all Diffie-Hellman operations. There is currently no standardised PQ-Noise extension in the `snow` crate. This means:
- **Handshake metadata** (ephemeral keys, encrypted static keys) could be harvested by a passive attacker and decrypted later with a quantum computer ("harvest now, decrypt later" attack).
- **Application data** encrypted by MLS is PQ-protected from the M5 milestone onward via the [Hybrid KEM](hybrid-kem.md) layer.
The residual risk (metadata exposure via handshake harvest) is accepted for M1 through M5. On the QUIC + TLS 1.3 path, the same gap exists: TLS 1.3 key exchange uses classical ECDHE. Both gaps are tracked in [ADR-006: PQ Gap in Noise Transport](../design-rationale/adr-006-pq-gap.md).
## Thread safety
`NoiseTransport` is `Send` but not `Clone` or `Sync`. It should be used from a single Tokio task. To share data across tasks, use channels or other message-passing mechanisms. The `Debug` implementation formats the first four bytes of the remote static key as hex for logging:
```rust
NoiseTransport { remote_static: Some("a1b2c3d4…"), .. }
```
## Error handling
All `NoiseTransport` methods return `Result<_, CoreError>` with these variants:
| Error | Meaning |
|---|---|
| `CoreError::HandshakeIncomplete` | Peer closed the connection during the handshake |
| `CoreError::Noise(snow::Error)` | Any Noise operation failed (pattern mismatch, bad DH, decryption failure) |
| `CoreError::Codec(CodecError)` | TCP I/O failure or frame size violation |
| `CoreError::ConnectionClosed` | Peer closed the connection during transport phase |
| `CoreError::MessageTooLarge { size }` | Plaintext exceeds `MAX_PLAINTEXT_LEN` (65,519 bytes) |
| `CoreError::Capnp(capnp::Error)` | Cap'n Proto serialisation error (envelope methods only) |
## Further reading
- [QUIC + TLS 1.3](quic-tls.md) -- The M3+ replacement for Noise\_XX on the client-server path.
- [Cap'n Proto Serialisation and RPC](capn-proto.md) -- The serialisation layer that rides on top of the Noise transport.
- [Length-Prefixed Framing Codec](../wire-format/framing-codec.md) -- The `LengthPrefixedCodec` used by `NoiseTransport`.
- [X25519 Transport Keys](../cryptography/transport-keys.md) -- Key generation and management for Noise static keys.
- [ADR-001: Noise\_XX for Transport Auth](../design-rationale/adr-001-noise-xx.md) -- Design rationale for choosing the XX pattern.
- [ADR-006: PQ Gap in Noise Transport](../design-rationale/adr-006-pq-gap.md) -- Accepted risk of classical-only key exchange.

View File

@@ -1,6 +1,6 @@
# Protocol Layers Overview
quicnprotochat composes five distinct protocol layers into a single security stack. Each layer addresses a specific class of threat and delegates everything else to the layers above or below it. No single layer is sufficient on its own; the composition is what delivers end-to-end confidentiality, mutual authentication, forward secrecy, post-compromise security, and post-quantum resistance.
quicnprotochat composes four distinct protocol layers into a single security stack. Each layer addresses a specific class of threat and delegates everything else to the layers above or below it. No single layer is sufficient on its own; the composition is what delivers end-to-end confidentiality, server authentication, forward secrecy, post-compromise security, and post-quantum resistance.
This page provides a high-level comparison and a suggested reading order. The deep-dive pages that follow contain implementation details drawn directly from the source code.
@@ -9,7 +9,6 @@ This page provides a high-level comparison and a suggested reading order. The de
| Layer | Standard / Spec | Crate(s) | Security Properties |
|---|---|---|---|
| **QUIC + TLS 1.3** | RFC 9000, RFC 9001 | `quinn 0.11`, `rustls 0.23` | Transport confidentiality, server authentication, 0-RTT resumption |
| **Noise\_XX** | [Noise Protocol Framework](https://noiseprotocol.org/noise.html) | `snow 0.9` | Mutual authentication, identity hiding, ChaCha20-Poly1305 session encryption |
| **Cap'n Proto** | [capnproto.org specification](https://capnproto.org/encoding.html) | `capnp 0.19`, `capnp-rpc 0.19` | Zero-copy deserialisation, schema-enforced types, canonical serialisation for signing, async RPC |
| **MLS** | [RFC 9420](https://www.rfc-editor.org/rfc/rfc9420.html) | `openmls 0.5` | Group key agreement, forward secrecy, post-compromise security (PCS) |
| **Hybrid KEM** | [draft-ietf-tls-hybrid-design](https://datatracker.ietf.org/doc/draft-ietf-tls-hybrid-design/) | `ml-kem 0.2`, `x25519-dalek 2` | Post-quantum resistance via ML-KEM-768 combined with X25519 |
@@ -33,33 +32,26 @@ Application plaintext
|
v
+-----------+
| Noise_XX | Per-session ChaCha20-Poly1305 encryption (M1 TCP path)
+-----------+ -- OR --
+-----------+
| QUIC+TLS | QUIC transport encryption (M3+ QUIC path)
| QUIC+TLS | QUIC transport encryption (TLS 1.3)
+-----------+
|
v
Network
```
In the current M3 architecture, the QUIC + TLS 1.3 layer has replaced the Noise\_XX layer for client-to-server transport. The Noise\_XX implementation remains in the codebase and is used for direct peer-to-peer connections in M1-era integration tests. Both paths carry Cap'n Proto messages as their inner payload.
The Hybrid KEM layer operates orthogonally: it wraps MLS payloads in an outer post-quantum encryption envelope before they enter the transport layer. It is implemented and tested but not yet integrated into the MLS ciphersuite (planned for the M5 milestone).
## Suggested reading order
The pages in this section are ordered to build understanding incrementally:
1. **[QUIC + TLS 1.3](quic-tls.md)** -- Start here. This is the outermost transport layer that every client-server connection uses today. Understanding QUIC stream multiplexing and the TLS 1.3 handshake is prerequisite to understanding how Cap'n Proto RPC rides on top.
1. **[QUIC + TLS 1.3](quic-tls.md)** -- Start here. This is the transport layer that every client-server connection uses. Understanding QUIC stream multiplexing and the TLS 1.3 handshake is prerequisite to understanding how Cap'n Proto RPC rides on top.
2. **[MLS (RFC 9420)](mls.md)** -- The core cryptographic innovation. MLS provides the group key agreement that makes quicnprotochat an E2E encrypted group messenger rather than just a transport-encrypted relay. This is the longest and most detailed page.
3. **[Cap'n Proto Serialisation and RPC](capn-proto.md)** -- The serialisation and RPC layer that bridges MLS application data with the transport. Understanding the Envelope schema, the ParsedEnvelope owned type, and the NodeService RPC interface is essential for reading the server and client source code.
4. **[Noise\_XX Handshake](noise-xx.md)** -- The M1-era transport encryption layer. Even though QUIC has replaced it for client-server communication, the Noise\_XX code remains in the codebase and the design decisions it embodies (mutual authentication, identity hiding) inform the overall architecture.
5. **[Hybrid KEM: X25519 + ML-KEM-768](hybrid-kem.md)** -- The post-quantum encryption layer. Read this last because it builds on concepts from all other layers: key encapsulation (from MLS), wire format conventions (from Cap'n Proto), and AEAD encryption (from Noise).
4. **[Hybrid KEM: X25519 + ML-KEM-768](hybrid-kem.md)** -- The post-quantum encryption layer. Read this last because it builds on concepts from all other layers: key encapsulation (from MLS), wire format conventions (from Cap'n Proto), and AEAD encryption.
## Cross-cutting concerns
@@ -69,7 +61,7 @@ Several topics span multiple layers and have their own dedicated pages elsewhere
- **Post-compromise security**: Provided by MLS Update proposals. See [Post-Compromise Security](../cryptography/post-compromise-security.md).
- **Post-quantum readiness**: Currently provided by the standalone Hybrid KEM module; integration into MLS is planned for M5. See [Post-Quantum Readiness](../cryptography/post-quantum-readiness.md).
- **Key lifecycle and zeroization**: Private key material is zeroized after use across all layers. See [Key Lifecycle and Zeroization](../cryptography/key-lifecycle.md).
- **Wire format details**: The length-prefixed framing codec and Cap'n Proto schema definitions are documented in the [Wire Format Reference](../wire-format/overview.md) section.
- **Wire format details**: The Cap'n Proto schema definitions are documented in the [Wire Format Reference](../wire-format/overview.md) section.
- **Design rationale**: The ADR pages explain *why* each layer was chosen. See [Design Decisions Overview](../design-rationale/overview.md).
## Crate mapping
@@ -79,7 +71,6 @@ Each protocol layer maps to one or more workspace crates:
| Layer | Primary Crate | Source File(s) |
|---|---|---|
| QUIC + TLS 1.3 | `quicnprotochat-server`, `quicnprotochat-client` | `main.rs` (server and client entry points) |
| Noise\_XX | `quicnprotochat-core` | `src/noise.rs`, `src/codec.rs` |
| Cap'n Proto | `quicnprotochat-proto` | `src/lib.rs`, `build.rs`, `schemas/*.capnp` |
| MLS | `quicnprotochat-core` | `src/group.rs`, `src/keystore.rs` |
| Hybrid KEM | `quicnprotochat-core` | `src/hybrid_kem.rs` |

View File

@@ -1,21 +1,16 @@
# QUIC + TLS 1.3
quicnprotochat uses QUIC (RFC 9000) with mandatory TLS 1.3 (RFC 9001) as its client-to-server transport layer. This page explains why QUIC was chosen over raw TCP, how the `quinn` and `rustls` crates are integrated, and what security properties the transport provides.
quicnprotochat uses QUIC (RFC 9000) with mandatory TLS 1.3 (RFC 9001) as its transport layer. This page explains how the `quinn` and `rustls` crates are integrated and what security properties the transport provides.
## Why QUIC over raw TCP
## Why QUIC
The M1 milestone used raw TCP sockets with a Noise\_XX handshake for transport encryption (see [Noise\_XX Handshake](noise-xx.md)). Starting from M3, the project migrated to QUIC for several reasons:
QUIC provides several advantages over traditional TCP-based transports:
| Property | Raw TCP + Noise | QUIC + TLS 1.3 |
|---|---|---|
| **Multiplexed streams** | Single stream; application must multiplex manually | Native bidirectional streams; each RPC call gets its own stream |
| **0-RTT resumption** | Not available; full handshake every time | Built-in; returning clients can send data in the first flight |
| **Head-of-line blocking** | A lost TCP segment blocks all subsequent data | Only the affected stream is blocked; other streams proceed |
| **NAT traversal** | TCP requires keep-alives; NAT rebinding breaks connections | UDP-based; connection migration survives NAT rebinding |
| **TLS integration** | Separate Noise handshake layered on top of TCP | TLS 1.3 is integral to the QUIC handshake; no extra round-trips |
| **Ecosystem support** | Custom framing codec required | `capnp-rpc` can use QUIC bidirectional streams directly via `tokio-util` compat layer |
The migration also simplified the codebase: the custom `LengthPrefixedCodec` framing layer and the `into_capnp_io()` bridge (documented in [Noise\_XX Handshake](noise-xx.md)) are no longer needed on the QUIC path because `capnp-rpc` reads and writes directly on the QUIC stream.
- **Multiplexed streams**: Native bidirectional streams; each RPC call gets its own stream without head-of-line blocking.
- **0-RTT resumption**: Returning clients can send data in the first flight, reducing connection setup latency.
- **Integrated encryption**: TLS 1.3 is integral to the QUIC handshake; no extra round-trips for transport security.
- **NAT traversal**: UDP-based; connection migration survives NAT rebinding.
- **Ecosystem support**: `capnp-rpc` can use QUIC bidirectional streams directly via the `tokio-util` compat layer.
## Crate integration
@@ -134,21 +129,6 @@ The QUIC + TLS 1.3 layer provides:
- **Client authentication**: Handled by MLS identity credentials at the application layer. See [MLS (RFC 9420)](mls.md).
- **End-to-end encryption**: TLS terminates at the server. The server can read the Cap'n Proto RPC framing and message routing metadata. Payload confidentiality is provided by MLS. See [MLS (RFC 9420)](mls.md).
- **Post-quantum resistance**: TLS 1.3 key exchange uses classical ECDHE. Post-quantum protection of application data is provided by the [Hybrid KEM](hybrid-kem.md) layer (M5 milestone).
- **Mutual peer authentication**: For peer-to-peer scenarios, the M1-era [Noise\_XX](noise-xx.md) transport provides mutual authentication with identity hiding.
## Comparison with Noise\_XX (M1 approach)
| Aspect | Noise\_XX (M1) | QUIC + TLS 1.3 (M3+) |
|---|---|---|
| **Transport** | Raw TCP | UDP (QUIC) |
| **Handshake** | 3-message Noise XX pattern | TLS 1.3 (1-RTT or 0-RTT) |
| **Mutual auth** | Both peers authenticate static X25519 keys | Server-only at TLS layer; mutual auth via MLS |
| **Identity hiding** | Initiator's identity hidden until message 3 | No identity hiding at TLS layer |
| **Stream multiplexing** | None (single stream) | Native QUIC streams |
| **RPC bridge** | `into_capnp_io()` with `tokio::io::duplex` | Direct `compat()` wrapper on QUIC stream |
| **Codebase location** | `quicnprotochat-core/src/noise.rs` | `quicnprotochat-server/src/main.rs`, client `lib.rs` |
The Noise\_XX path remains useful for direct peer-to-peer connections (without a central server) and as a fallback transport. Both paths carry identical Cap'n Proto message payloads, so the application layer is transport-agnostic.
## Configuration reference
@@ -171,7 +151,5 @@ The Noise\_XX path remains useful for direct peer-to-peer connections (without a
## Further reading
- [Noise\_XX Handshake](noise-xx.md) -- The M1-era transport layer that QUIC replaced.
- [Cap'n Proto Serialisation and RPC](capn-proto.md) -- The RPC layer that runs on top of QUIC streams.
- [Service Architecture](../architecture/service-architecture.md) -- How the server's `NodeServiceImpl` binds to the QUIC endpoint.
- [ADR-006: PQ Gap in Noise Transport](../design-rationale/adr-006-pq-gap.md) -- Discusses the post-quantum gap in both the Noise and TLS transport layers.