mirror of
https://github.com/luxfi/zwing.git
synced 2026-09-24 00:24:38 +00:00
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:
@@ -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)
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user