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.
This commit is contained in:
2026-05-06 12:48:27 -07:00
parent f21120ce2f
commit 66f1cb5d18
2 changed files with 79 additions and 14 deletions
+73 -12
View File
@@ -1,20 +1,55 @@
# Z-Wing — Lux Post-Quantum Secure Channel
## Overview
## What Z-Wing is
Z-Wing is the canonical Lux PQ secure channel. One construction, one wire
format, no negotiation. If two endpoints don't both speak Z-Wing v1, they
don't talk.
Z-Wing is a **composition**, not a new cryptographic primitive. It bolts
three ingredients into a single net.Conn:
Layering:
```
┌────────────────────────────────────────────────────────────────────┐
│ 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: X-Wing (X25519 + ML-KEM-768, IETF draft-connolly-cfrg-xwing-kem)
KEM: IETF X-Wing (X25519 + ML-KEM-768)
Identity: Ed25519 + ML-DSA-65 hybrid signatures
AEAD: ChaCha20-Poly1305 with sequence-numbered nonces
Routing: Plain TCP (this version) or RNS (follow-up)
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`,
@@ -75,6 +110,9 @@ Responder -> Initiator : HandshakeResponse
## 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 )
```
@@ -82,7 +120,7 @@ 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 |
| `"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 |
@@ -95,6 +133,10 @@ 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:
@@ -109,6 +151,10 @@ 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
@@ -155,8 +201,10 @@ without TCP `Dial`/`Listen`.
## What's NOT in this version
* RNS routing. Z-Wing-over-RNS is a follow-up package (LP-9702 deployment
path); v0.1.0 ships Z-Wing-over-TCP only.
* 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.
@@ -183,7 +231,7 @@ Six tests cover the spec:
|---|---|
| `doc.go` | package documentation |
| `errors.go` | typed errors |
| `kem.go` | X-Wing KEM (X25519 + ML-KEM-768 + SHA3-256 combiner) |
| `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 |
@@ -194,9 +242,22 @@ Six tests cover the spec:
| `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
* `draft-connolly-cfrg-xwing-kem` — IETF X-Wing combiner spec
* `~/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)
+6 -2
View File
@@ -13,7 +13,11 @@ import (
"golang.org/x/crypto/sha3"
)
// X-Wing combiner constants per draft-connolly-cfrg-xwing-kem.
// X-Wing KEM combiner per IETF draft-connolly-cfrg-xwing-kem.
// EXACT labels — Z-Wing uses real IETF X-Wing, no rebranding,
// for full interop with any X-Wing peer. The Lux value-add lives
// in the layers ABOVE the KEM (identity binding, RNS transport),
// not by forking the KEM itself.
//
// shared_secret = SHA3-256( "\./" || "X-Wing" || ss_M || ss_X || ct_X || pk_X )
//
@@ -23,7 +27,7 @@ import (
// pk_X X25519 static public key of recipient (32 bytes)
var (
xwingLabelPrefix = []byte{'\\', '.', '/'} // 3 bytes: backslash, dot, slash
xwingLabelName = []byte("X-Wing") // 6 bytes ASCII
xwingLabelName = []byte("X-Wing") // 6 bytes ASCII — IETF spec
)
// X-Wing wire sizes.