Files
quicproquo/docs/src/protocol-layers/quic-tls.md
Christian Nennemann 9fdb37876a 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>
2026-02-22 08:25:23 +01:00

8.5 KiB

QUIC + TLS 1.3

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

QUIC provides several advantages over traditional TCP-based transports:

  • 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

quicnprotochat uses the following crates for QUIC and TLS:

  • quinn 0.11 -- The async QUIC implementation for Tokio. Provides Endpoint, Connection, and bidirectional stream types.
  • quinn-proto 0.11 -- The protocol-level types, including QuicServerConfig and QuicClientConfig wrappers that bridge rustls into quinn.
  • rustls 0.23 -- The TLS implementation. quicnprotochat uses it in strict TLS 1.3 mode with no fallback to TLS 1.2.
  • rcgen 0.13 -- Self-signed certificate generation for development and testing.

Server configuration

The server builds its QUIC endpoint configuration in build_server_config() (in quicnprotochat-server/src/main.rs):

let mut tls = rustls::ServerConfig::builder_with_protocol_versions(&[&TLS13])
    .with_no_client_auth()
    .with_single_cert(cert_chain, key)?;
tls.alpn_protocols = vec![b"capnp".to_vec()];

let crypto = QuicServerConfig::try_from(tls)?;
Ok(ServerConfig::with_crypto(Arc::new(crypto)))

Key points:

  1. TLS 1.3 strict mode: builder_with_protocol_versions(&[&TLS13]) ensures no TLS 1.2 fallback. This is a hard requirement: TLS 1.2 lacks the 0-RTT and full forward secrecy guarantees that quicnprotochat relies on.

  2. No client certificate authentication: with_no_client_auth() means the server does not verify client certificates at the TLS layer. Client authentication is handled at the application layer via Ed25519 identity keys and MLS credentials. This is a deliberate design choice -- MLS provides stronger authentication properties than TLS client certificates.

  3. ALPN negotiation: The Application-Layer Protocol Negotiation extension is set to b"capnp", advertising that this endpoint speaks Cap'n Proto RPC. Both client and server must agree on this protocol identifier or the TLS handshake fails.

  4. QuicServerConfig bridge: The quinn-proto crate provides QuicServerConfig::try_from(tls) to adapt the rustls::ServerConfig for use with QUIC. This handles the QUIC-specific TLS parameters (transport parameters, QUIC header protection keys) automatically.

Client configuration

The client performs the mirror operation. It loads the server's DER-encoded certificate from a local file and constructs a rustls::ClientConfig:

let mut roots = rustls::RootCertStore::empty();
roots.add(CertificateDer::from(cert_bytes))?;

let tls = rustls::ClientConfig::builder_with_protocol_versions(&[&TLS13])
    .with_root_certificates(roots)
    .with_no_client_auth();
tls.alpn_protocols = vec![b"capnp".to_vec()];

let crypto = QuicClientConfig::try_from(tls)?;

The client trusts exactly one certificate: the server's self-signed cert loaded from disk. There is no system trust store involved, which simplifies the trust model but requires out-of-band distribution of the server certificate.

Per-connection handling

Each accepted QUIC connection spawns a handler task:

let (send, recv) = connection.accept_bi().await?;
let (reader, writer) = (recv.compat(), send.compat_write());

let network = twoparty::VatNetwork::new(reader, writer, Side::Server, Default::default());
let service: node_service::Client = capnp_rpc::new_client(NodeServiceImpl { store, waiters });
RpcSystem::new(Box::new(network), Some(service.client)).await?;

The tokio-util compat layer (compat() and compat_write()) converts Quinn's RecvStream and SendStream into types that implement futures::AsyncRead and futures::AsyncWrite, which capnp-rpc's VatNetwork requires. The entire Cap'n Proto RPC system then runs over this single QUIC bidirectional stream.

Because capnp-rpc uses Rc<RefCell<>> internally (making it !Send), all RPC tasks run on a tokio::task::LocalSet. The server spawns each connection handler via tokio::task::spawn_local.

Certificate trust model

quicnprotochat currently uses a trust-on-first-use (TOFU) model with self-signed certificates:

  1. On first start, the server generates a self-signed certificate using rcgen::generate_simple_self_signed with SANs for localhost, 127.0.0.1, and ::1.
  2. The certificate and private key are persisted to disk as DER files (default: data/server-cert.der and data/server-key.der).
  3. Clients must obtain the server's certificate file out-of-band and reference it via the --ca-cert flag or QUICNPROTOCHAT_CA_CERT environment variable.

This model is adequate for development and single-server deployments. The roadmap includes:

  • ACME integration (Let's Encrypt) for production deployments with publicly-routable servers.
  • Certificate pinning to detect MITM attacks even when a CA is compromised.
  • Certificate transparency log monitoring for detecting misissued certificates.

Self-signed certificate generation

The server's generate_self_signed() function:

let subject_alt_names = vec![
    "localhost".to_string(),
    "127.0.0.1".to_string(),
    "::1".to_string(),
];
let issued = generate_simple_self_signed(subject_alt_names)?;

fs::write(cert_path, issued.cert.der())?;
fs::write(key_path, &issued.key_pair.serialize_der())?;

The generated certificate includes both DNS and IP SANs so that clients can connect using either localhost or an IP address. The client specifies the expected server name via --server-name (default: localhost), which must match one of the certificate's SANs.

Security properties

The QUIC + TLS 1.3 layer provides:

Property Mechanism
Transport confidentiality All application data is encrypted with AES-128-GCM or ChaCha20-Poly1305 (negotiated during the TLS handshake)
Server authentication The client verifies the server's certificate against the locally-trusted DER file
Forward secrecy TLS 1.3 exclusively uses ephemeral Diffie-Hellman key exchange; session keys are not derivable from the server's long-term key
Replay protection QUIC packet numbers and TLS 1.3's anti-replay mechanism prevent replay attacks
Connection migration QUIC connection IDs allow the client to change IP addresses without re-handshaking

What TLS does not provide

  • Client authentication: Handled by MLS identity credentials at the application layer. See MLS (RFC 9420).
  • 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).
  • Post-quantum resistance: TLS 1.3 key exchange uses classical ECDHE. Post-quantum protection of application data is provided by the Hybrid KEM layer (M5 milestone).

Configuration reference

Server

Environment Variable CLI Flag Default Description
QUICNPROTOCHAT_LISTEN --listen 0.0.0.0:7000 QUIC listen address
QUICNPROTOCHAT_TLS_CERT --tls-cert data/server-cert.der TLS certificate path
QUICNPROTOCHAT_TLS_KEY --tls-key data/server-key.der TLS private key path
QUICNPROTOCHAT_DATA_DIR --data-dir data Persistent storage directory

Client

Environment Variable CLI Flag Default Description
QUICNPROTOCHAT_CA_CERT --ca-cert data/server-cert.der Server certificate to trust
QUICNPROTOCHAT_SERVER_NAME --server-name localhost Expected TLS server name (must match certificate SAN)
QUICNPROTOCHAT_SERVER --server 127.0.0.1:7000 Server address (per-subcommand)

Further reading