Files
quicproquo/docs/src/protocol-layers/quic-tls.md
Christian Nennemann 2e081ead8e chore: rename quicproquo → quicprochat in docs, Docker, CI, and packaging
Rename all project references from quicproquo/qpq to quicprochat/qpc
across documentation, Docker configuration, CI workflows, packaging
scripts, operational configs, and build tooling.

- Docker: crate paths, binary names, user/group, data dirs, env vars
- CI: workflow crate references, binary names, artifact names
- Docs: all markdown files under docs/, SDK READMEs, book.toml
- Packaging: OpenWrt Makefile, init script, UCI config (file renames)
- Scripts: justfile, dev-shell, screenshot, cross-compile, ai_team
- Operations: Prometheus config, alert rules, Grafana dashboard
- Config: .env.example (QPQ_* → QPC_*), CODEOWNERS paths
- Top-level: README, CONTRIBUTING, ROADMAP, CLAUDE.md
2026-03-21 19:14:06 +01:00

8.4 KiB

QUIC + TLS 1.3

quicprochat 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.
  • Per-call concurrency: The v2 RPC framework opens one bidirectional stream per RPC call. Multiple calls run concurrently without blocking each other.
  • Push streams: Server-to-client push events use QUIC uni-directional streams, avoiding any request-response overhead.

Crate integration

quicprochat uses the following crates for QUIC and TLS:

  • quinn 0.11 -- The async QUIC implementation for Tokio. Provides Endpoint, Connection, and bidirectional/uni-directional 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. quicprochat 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 with:

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"qpc".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 quicprochat 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 OPAQUE password authentication and Ed25519 identity keys. This is a deliberate design choice -- OPAQUE provides stronger authentication properties than TLS client certificates without requiring PKI infrastructure.

  3. ALPN negotiation: The Application-Layer Protocol Negotiation extension is set to b"qpc", advertising that this endpoint speaks the quicprochat v2 Protobuf framing protocol. 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 mut tls = rustls::ClientConfig::builder_with_protocol_versions(&[&TLS13])
    .with_root_certificates(roots)
    .with_no_client_auth();
tls.alpn_protocols = vec![b"qpc".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

The v2 server accepts connections and handles streams concurrently:

// Accept a QUIC connection
let connection = endpoint.accept().await?;

// For each incoming bidirectional stream (one per RPC call):
let (send, recv) = connection.accept_bi().await?;
// Read RequestFrame, dispatch, write ResponseFrame
tokio::spawn(handle_rpc(send, recv, server_state));

// For server-initiated push events:
let send = connection.open_uni().await?;
// Write PushFrame
tokio::spawn(send_push(send, event));

Unlike the v1 Cap'n Proto RPC (which required tokio::task::LocalSet due to !Send internals), the v2 framework uses Arc-based shared state and tokio::spawn for full multi-threaded concurrency.

Certificate trust model

quicprochat 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 QPC_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 OPAQUE password authentication (methods 100-103) and Ed25519 identity keys at the application layer. See Service Architecture.
  • End-to-end encryption: TLS terminates at the server. The server can read the Protobuf 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.

Configuration reference

Server

Environment Variable CLI Flag Default Description
QPC_LISTEN --listen 0.0.0.0:5001 QUIC listen address
QPC_TLS_CERT --tls-cert data/server-cert.der TLS certificate path
QPC_TLS_KEY --tls-key data/server-key.der TLS private key path
QPC_DATA_DIR --data-dir data Persistent storage directory

Client

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

Further reading