Files
zeekayandGitHub 39b1807623 feat(quic): ZAP transport with X25519MLKEM768 PQ-TLS (#6)
New package github.com/luxfi/zap/quic provides the canonical QUIC
transport for the ZAP messaging substrate.

Wire format is identical to the TCP transport (length-prefixed
Cap'n Proto frames), so every consumer's wire-level expectations
are preserved.

What QUIC adds beyond TCP+TLS:

  - Multiplexed bidi/uni streams (one ZAP RPC per QUIC stream,
    no head-of-line blocking between RPCs)
  - Connection migration on local-IP changes (Wi-Fi to LTE,
    NAT rebind, validator re-homing)
  - 0-RTT resumption via TLS 1.3 session tickets
  - TLS 1.3 hybrid post-quantum key exchange X25519MLKEM768
    (IANA NamedGroup 0x11ec) as the default
  - ALPN allowlist 'zap/1'

Cryptography:

  - We use the IANA-registered hybrid X25519MLKEM768 verbatim;
    no custom combiner, no z-wing fork. Go stdlib's crypto/tls
    performs HKDF-Extract(X25519_ss || MLKEM_ss) internally.
  - Server cert sig stays classical (ECDSA / RSA) — Go stdlib
    TLS does not yet ship ML-DSA. The hybrid KEM defends against
    harvest-now/decrypt-later; the cert is the orthogonal
    auth-time concern, gated by today's trusted CA.
  - 0-RTT replay is documented; non-idempotent handlers must
    set RejectEarlyData=true.

Back-compat is sacred: TransportTCP is the default for
NodeConfig.Transport, so every existing consumer keeps working
untouched. TransportQUIC is opt-in and requires:

  import _ "github.com/luxfi/zap/quic"

so the QUIC TransportFactory registers itself at init.

Tests prove X25519MLKEM768 actually negotiates end-to-end:

  $ go test ./quic/... -run TestHandshake -v
  TestHandshake_NegotiatesX25519MLKEM768
      client negotiated CurveID = 0x11ec, ALPN = "zap/1"
  --- PASS

Also covered: classical-fallback negotiation when server excludes
the hybrid, ALPN allowlist rejection, multiplex of concurrent
streams, 0-RTT round-trip, unidirectional broadcast streams,
frame-size enforcement, QUICConfig propagation through Listen.

Benchmarks on Apple M1 Max (1-RPC ping-pong, single connection):

  RTT_QUIC/256B            0.40 MB/s   634 us/op
  RTT_QUIC/1MiB           44.94 MB/s    23 ms/op
  RTT_TCP_Baseline/256B    3.16 MB/s    81 us/op  (no TLS, no PQ)
  RTT_TCP_Baseline/1MiB  838.88 MB/s   1.2 ms/op
  Multistream concurrent (16 parallel): 5.02 MB/s @ 50us/op

QUIC pays for TLS 1.3 + PQ KEM relative to the plaintext-TCP
baseline; small-message latency is 8x, large-message throughput
is 18x slower. The multistream concurrency case is where QUIC
wins — independent RPCs do not block each other.

Files:

  quic/conn.go         transport-level Conn (Send/Recv/streams)
  quic/server.go       Listener wrapping quic-go Listener/EarlyListener
  quic/client.go       Dialer with 0-RTT-attempting DialEarly
  quic/tls.go          TLS defaults + self-signed test cert helper
  quic/factory.go      registers QUIC TransportFactory with parent zap pkg
  quic/config.go       Config wrapper exposed via NodeConfig.QUICConfig
  quic/doc.go          package documentation
  quic/README.md       deployment guide + threat model
  quic/handshake_test.go  X25519MLKEM768 negotiation + multiplex + 0-RTT
  quic/benchmark_test.go  QUIC vs TCP baseline RTT + throughput

  transport.go         new Transport enum + TransportFactory hook
  node_quic.go         QUIC-path Send/Call/ConnectDirect dispatch
  node.go              additive: Transport field on NodeConfig, branch
                       in Start/Send/Call/ConnectDirect
  LLM.md               QUIC transport section under PQ-TLS Support
  go.mod / go.sum      add github.com/quic-go/quic-go v0.59.1
2026-06-02 03:53:13 -07:00

60 lines
2.7 KiB
Go

// Copyright (C) 2026, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
// Package quic implements the QUIC transport for the ZAP messaging substrate.
//
// QUIC offers, compared to ZAP's existing TCP+TLS transport:
//
// - Stream multiplexing: N concurrent RPCs over one connection without
// head-of-line blocking.
// - Connection migration: the connection survives client-IP changes
// (Wi-Fi-to-LTE, NAT rebinding, etc.) because the QUIC connection ID
// is independent of the 5-tuple.
// - 0-RTT resumption: cached session tickets let the second handshake to
// a peer complete without a round trip before app data flows.
// - TLS 1.3 with the X25519MLKEM768 hybrid post-quantum key exchange
// (IANA NamedGroup 0x11ec) baked in.
//
// # Wire format
//
// One ZAP message is one length-prefixed Cap'n Proto frame on a QUIC stream.
// The frame format is identical to the TCP transport:
//
// [4-byte little-endian length][ZAP message bytes]
//
// Two stream patterns are used:
//
// - Bidirectional streams carry one request/response exchange. After the
// response is written the server side closes its half of the stream and
// the client closes its half on Recv, freeing the stream ID. This maps
// onto Node.Call.
// - Unidirectional streams from the server carry one-way notifications
// and subscription deliveries. The receiver routes each frame through
// the same handler dispatch as the TCP path.
//
// The control stream (the first bidirectional stream opened by the dialer)
// carries the 64-byte ZAP node-identity handshake exchanged before any RPC.
//
// # Cryptography
//
// The default TLS 1.3 configuration prefers X25519MLKEM768. That is the
// IANA-registered hybrid: the shared secret is HKDF-Extract of X25519_ss
// concatenated with ML-KEM-768 ciphertext-derived ss. The Go runtime
// performs the combination internally — this package does not roll its
// own KEM combiner. See ../papers/pq-hybrid-kem/main.pdf.
//
// The server certificate signature is still classical (ECDSA or RSA);
// post-quantum signature schemes are not yet wired into Go's stdlib TLS.
// The threat model that motivates X25519MLKEM768 is harvest-now /
// decrypt-later against the confidentiality of the key-exchange, not
// forgery against a trusted-today certificate authority.
//
// # 0-RTT replay
//
// QUIC permits 0-RTT application data, which can be replayed by a network
// adversary. The server defaults to accepting 0-RTT data, but application
// handlers MUST treat 0-RTT-carried RPCs as either idempotent or rejected.
// Use ServerConfig.RejectEarlyData to force a full 1-RTT handshake for
// every connection.
package quic