Files
zeekay 66f1cb5d18 zwing: keep IETF X-Wing labels, frame Z-Wing as composition
Z-Wing v0.1.0 briefly relabelled the X-Wing KDF combiner with a
"Z-Wing" domain separator. That was wrong: the only reason to use
IETF X-Wing is exact interop with anything that speaks the IETF
draft, and rebranding the combiner breaks that interop while adding
zero security. The Lux contribution lives ONE layer up.

This commit restores the IETF labels everywhere:

* kem.go: combineXWing writes "\./" || "X-Wing" || ss_M || ss_X ||
  ct_X || pk_X. The label slice names go back to xwingLabelPrefix /
  xwingLabelName.
* LLM.md: rewritten to position Z-Wing as a composition (real X-Wing
  KEM + Ed25519+ML-DSA-65 hybrid identity + ChaCha20-Poly1305 +
  RNS-or-TCP transport), not a separate KEM. Z-Wing's domain
  separation lives in the HKDF info string "lux.zwing.v1/{i2r,r2i}"
  and the ML-DSA context "lux.zwing.v1", not in the KEM.

Tests pass with the IETF labels (TestHandshakeRoundTrip,
TestZAPOverZWing, TestForwardSecrecy, TestSequencedNonces,
TestIdentityMismatch, TestDialListen).

The package is still called zwing and the construction is still
called Z-Wing. The KEM at the bottom of the stack is and stays IETF
X-Wing.
2026-05-06 12:48:27 -07:00

9.8 KiB

Z-Wing — Lux Post-Quantum Secure Channel

What Z-Wing is

Z-Wing is a composition, not a new cryptographic primitive. It bolts three ingredients into a single net.Conn:

┌────────────────────────────────────────────────────────────────────┐
│ Z-Wing                                                             │
│                                                                    │
│   KEM         = IETF X-Wing  (X25519 + ML-KEM-768; SHA3-256        │
│                 combiner; the "X-Wing" label is the IETF spec —    │
│                 this is exact interop, not a fork)                 │
│   Identity    = Ed25519 + ML-DSA-65 hybrid signatures              │
│   Transport   = any net.Conn (TCP today; RNS in the LP-9702        │
│                 deployment path so Z-Wing can ride a mesh routed   │
│                 link)                                              │
│   AEAD        = ChaCha20-Poly1305 with sequence-numbered nonces    │
│                                                                    │
└────────────────────────────────────────────────────────────────────┘

If you strip the layers, the cryptography is real X-Wing byte-for- byte. Two Z-Wing peers can interop with anything that speaks the IETF draft-connolly-cfrg-xwing-kem KEM. The Lux contribution is what wraps the KEM:

  • a hybrid Ed25519 + ML-DSA-65 identity signed into the handshake transcript (so an attacker who breaks one of the two signatures still can't forge a session);
  • a single canonical wire format with no negotiation (no downgrade);
  • a net.Conn output so any Lux service that already speaks ZAP gets PQ on the wire by swapping net.Dial for zwing.Dial;
  • a planned RNS routed link as the canonical transport (LP-9702) so the channel survives mesh-routed networks, not just point-to-point TCP.

If two endpoints don't both speak Z-Wing v1, they don't talk. There is exactly one way to do this.

Layering with ZAP

Application:  ZAP RPC (luxfi/api/zap)            <- unchanged
Channel:      Z-Wing                              <- this package
  KEM:        IETF X-Wing (X25519 + ML-KEM-768)
  Identity:   Ed25519 + ML-DSA-65 hybrid signatures
  AEAD:       ChaCha20-Poly1305 with sequence-numbered nonces
Routing:      Any net.Conn — TCP (Dial), Unix socket, RNS link, or any
              other transport. Use `Client(conn, cfg)` / `Server(conn, cfg)`
              for non-TCP transports.

The Conn returned by Dial / Listener.Accept() implements net.Conn, so any Lux service that already speaks ZAP gets PQ on the wire by simply swapping its dialer:

raw, _ := zwing.Dial(ctx, "host:9999", &zwing.Config{LocalIdentity: id})
zapConn := zap.NewConn(raw, nil)   // ZAP unchanged

Default port

Z-Wing inherits the canonical Lux ZAP port: TCP 9999. Override only when running multiple Z-Wing servers on the same host.

Wire format

Frame

Every transport frame on a Z-Wing channel:

[4 bytes BE: payload length][payload bytes]

MaxFrameSize = 1 MiB. The payload is either a handshake message (during the handshake) or an AEAD-sealed application record (after).

Handshake (1-RTT)

Initiator -> Responder : HandshakeInit
    [2 BE idLen][IdentityPublic bytes][2 BE sigLen][hybrid signature]

Responder -> Initiator : HandshakeResponse
    [XWingCiphertextSize bytes: ML-KEM ct || X25519 ephemeral pk]
    [2 BE encLen][AEAD-sealed responder identity + signature]
  • The initiator signs (label || IdentityPublic) so a passive observer cannot lift the public identity onto another protocol.
  • The responder signs the transcript hash that covers the initiator's public identity AND the X-Wing ciphertext, binding the signature to the session.
  • The responder identity is encrypted under an HKDF-derived key from the X-Wing shared secret, so passive observers do not learn the responder's long-term identity.

IdentityPublic

[Ed25519 pk: 32][ML-DSA-65 pk: 1952][X-Wing pk: 1216]
              total: 3200 bytes

X-Wing pk = ML-KEM-768 pk (1184) || X25519 pk (32).

X-Wing combiner (IETF spec, exact)

Z-Wing uses the IETF X-Wing combiner verbatim — same labels, same hash, same input order. No Lux-specific tweaks at the KEM layer:

shared_secret = SHA3-256( "\./" || "X-Wing" || ss_M || ss_X || ct_X || pk_X )
Input Bytes Source
"\./" 3 label prefix (backslash, dot, slash)
"X-Wing" 6 ASCII protocol name (IETF spec)
ss_M 32 ML-KEM-768 shared secret
ss_X 32 X25519 shared secret
ct_X 32 X25519 ephemeral public key from encapsulator
pk_X 32 X25519 static public key of recipient

Hash construction: SHA3-256, no HKDF. Inputs are fixed-length so no length separators are required.

In the Z-Wing handshake the responder is the encapsulator (it sends ct_X) and the initiator is the recipient (its X-Wing static is the target of the encapsulation).

The label is "X-Wing", not "Z-Wing". The whole point of using the IETF KEM is to interop with any X-Wing peer; rebranding the combiner would break that interop and add zero security.

Channel keys

After the handshake:

keyI2R = HKDF-SHA256(shared, "lux.zwing.v1/i2r", 32)
keyR2I = HKDF-SHA256(shared, "lux.zwing.v1/r2i", 32)

Each direction has its own ChaCha20-Poly1305 key. Nonce = 12 bytes, high 4 bytes zero, low 8 bytes are the big-endian sequence counter. A 64-bit counter would last 5.8e11 years at one record per nanosecond; exhausting it returns ErrSequenceExhausted.

The lux.zwing.v1 HKDF info string is where the Lux value-add lives — not in the KEM combiner. Z-Wing channel keys are domain-separated from any other system that might also use IETF X-Wing.

Forward secrecy comes from the X25519 ephemeral inside X-Wing. The ephemeral private key and intermediate ss_M / ss_X values are zeroed once the shared secret is derived. Channel keys are zeroed after the AEADs have been constructed.

Identity signatures

The Identity type holds three keypairs: Ed25519, ML-DSA-65, and X-Wing. Signature wire format:

[Ed25519 sig: 64][ML-DSA-65 sig: 3309]   total: 3373 bytes

Both must verify or ErrSignatureInvalid is returned. ML-DSA is signed with the lux.zwing.v1 context per FIPS 204 §5.2 so signatures are not portable across protocols that share identity keys.

Public API

// Identity
id, _ := zwing.GenerateIdentity()
pub  := id.Public()                   // serialise + hand to a peer

// Server
ln, _ := zwing.Listen(":9999", &zwing.Config{LocalIdentity: id})
for {
    conn, err := ln.Accept()          // post-handshake net.Conn
    if err != nil { break }
    go handle(conn)
}

// Client
conn, _ := zwing.Dial(ctx, "host:9999", &zwing.Config{
    LocalIdentity:  clientID,
    ExpectedRemote: pinnedServerPub,  // optional; nil = any valid peer
})

Client(net.Conn, *Config) and Server(net.Conn, *Config) wrap an existing connection (Unix socket, TLS conn, in-memory pipe, RNS link) without TCP Dial/Listen.

What's NOT in this version

  • RNS routing. Z-Wing-over-RNS is the LP-9702 deployment path; v0.2.0 ships Z-Wing-over-TCP only. Once RNS is in, the same Client(conn, cfg) entry point will accept an RNS link as conn — no protocol changes required.
  • AES-256-GCM AEAD. Single ciphersuite; ChaCha20-Poly1305 only.
  • Cipher / KEM negotiation. There is one Z-Wing v1.

Testing

GOWORK=off go test ./... -count=1

Six tests cover the spec:

Test What it proves
TestHandshakeRoundTrip end-to-end handshake; both peers see correct remote identity; bidirectional traffic works
TestDialListen TCP Dial + Listener.Accept, payload round-trip
TestIdentityMismatch Config.ExpectedRemote = wrong returns ErrIdentityMismatch
TestForwardSecrecy channel keys are zeroed after the AEAD is constructed
TestSequencedNonces three sequential records each decrypt cleanly under increasing nonces
TestZAPOverZWing zap.NewConn over a Z-Wing client; one ZAP RPC round-trip succeeds

Files

File Purpose
doc.go package documentation
errors.go typed errors
kem.go IETF X-Wing KEM (X25519 + ML-KEM-768 + SHA3-256 combiner with exact "X-Wing" label)
identity.go hybrid Ed25519 + ML-DSA-65 identity with X-Wing static key
wire.go length-prefixed framing
handshake.go initiator/responder state machines
channel.go post-handshake net.Conn with ChaCha20-Poly1305
config.go Config and DefaultPort
dial.go Dial / Client
listen.go Listen / Listener
zwing_test.go core protocol tests
zap_test.go ZAP-over-Z-Wing integration

Naming

  • The package is zwing and the construction is Z-Wing.
  • The KEM at the bottom of the stack is IETF X-Wing by name and by bytes. We keep the IETF name there because that's the interop contract; renaming the combiner would be misleading and break cross-stack compatibility.
  • lux.zwing.v1 appears as the HKDF context for channel keys and the ML-DSA context for identity signatures. That is where Z-Wing's domain-separation lives.

See also

  • ~/work/lux/api/zap/LLM.md — the application protocol that runs on top
  • ~/work/zap/zap/src/crypto.rs — Rust hybrid handshake reference
  • ~/work/lux/crypto — ML-KEM, ML-DSA, Ed25519 primitives used here
  • ~/work/lux/papers/lps/lp-9702-z-wing.tex — Z-Wing composition spec
  • draft-connolly-cfrg-xwing-kem — IETF X-Wing combiner spec (the KEM Z-Wing wraps)