Modules declare go 1.27.1 and the build images follow. go fix applied the
modernizers, and SHA3-256 now comes from the standard library's crypto/sha3;
omitzero is not applied, since it changes what the JSON says.
Co-authored-by: Hanzo Dev <[email protected]>
Two thousand and twenty-one packages carried more than one spec across this
estate — eslint alone in a hundred and four forms — and a package resolved twice
is two copies at run time, which is how react-hook-form came to report "two
different types with this name" from files nobody had edited. None do now.
The number lives in the catalog: a manifest names the package, the workspace
names the version. Where a manifest cannot reach a catalog it still carries the
one version, because the table is the source either way.
Co-authored-by: Hanzo Dev <[email protected]>
A version written into a manifest is written into every manifest that wants it,
which is how eslint came to exist in a hundred and four forms across this estate
and how a package came to resolve twice at run time. The catalog moves the number
out of the manifest: a manifest names the package, the workspace names the
version.
Four are held below their latest because the newer release cannot be used, and
the reason is recorded rather than the version alone: @noble/hashes 2 renamed
every subpath the web3 stack imports, @splinetool/runtime 2 depends on a package
that was never published, @dynamic-labs 5.4.1 the same, and react-resizable-panels
3 points its types condition at the bundle.
Co-authored-by: Hanzo Dev <[email protected]>
Lux Partners Limited is not the entity behind this work. Every place the
old name stood — copyright lines, site footers, brand config, legal and
compliance text — now names the company that actually holds it.
lux.fund is the exception and is not renamed to match: that site belongs
to Lux Assets Management LLC, a different company, and it says so.
Two kinds of line were left for a person rather than rewritten. A dated
news item recording that Lux Partners Limited was established in the Isle
of Man is a historical fact, and renaming it would falsify the record. And
a sentence asserting that the company is domiciled in the Isle of Man is a
claim about a jurisdiction, not a name — an Inc or an LLC is not an Isle
of Man company, so swapping the name there would assert something untrue.
Co-authored-by: Hanzo Dev <[email protected]>
github.com held commits this host lacked, which refuses the mirror push
("fetch first") and stops github receiving anything at all. Merging rather
than overwriting, so the mirror is a fast-forward again and neither side's
work is dropped.
Adds .hanzo/workflows/cicd.yml calling the canonical pipeline and drops the
GitHub-hosted workflows it replaces. Lands here because this is where the repo
is written — git.hanzo.ai is upstream and github.com is its push mirror, so the
same change made on github.com never reaches the runner fleet.
LLM.md is the source and stays the source; CLAUDE.md is a symlink to it, so there is
one file and it cannot drift from itself. What changes is that the link is committed
rather than left for each checkout to recreate.
Left untracked, the doc was conditional on a step outside the repo: a fresh clone had
no CLAUDE.md, nothing loaded LLM.md, and an agent worked here having never read what
this repo already explains. Most of the estate already tracks theirs; this is the rest
agreeing with them.
The previous "full PQ" claims for Python and TS were tested only within
each language. This commit proves the wire bytes actually match Go.
What was wrong:
Both Python and TS were calling ml_dsa65.sign(secretKey, message)
WITHOUT the FIPS 204 ctx parameter, while Go was calling
DSAPriv.SignCtx(rand, digest, []byte("lux.zwing.v1")). The signed
messages diverged → handshake verify failed across language pairs.
Fix:
Python: ML_DSA_65.sign(sk, digest, ctx=ZWING_DOMAIN) and matching
verify with ctx=ZWING_DOMAIN.
TS: ml_dsa65.sign(digest, sk, { context: ZWING_DOMAIN }) and
matching verify { context: ZWING_DOMAIN }.
Real interop tests added:
cmd/zwing-test-server/main.go — minimal Go server: prints listen
addr + identity hex on stdout,
accepts ONE handshake, echoes one
payload, exits.
py/test_interop_go.py — Python client connects via TCP,
runs run_initiator + Channel echo
against the live Go server.
js/interop_go.test.ts — TS client over node:net.Socket
runs runInitiator + Channel echo
against the live Go server.
Both interop tests pass:
Python ↔ Go: Ran 1 test (and 43 within-Python tests)
TypeScript ↔ Go: 1 interop test passes alongside 32 within-TS tests
Wire-byte compatibility now PROVEN end-to-end for X-Wing KEM, hybrid
identity (Ed25519 + ML-DSA-65 with lux.zwing.v1 ctx), HKDF channel
keys, and ChaCha20-Poly1305 records across Go ↔ Python ↔ TypeScript.
Drive every reachable error branch in identity, KEM, handshake, wire,
channel, dial, listen, zap integration. Coverage went 77.4% → 100.0%.
Refactors that came out of the audit:
* Convert chacha20poly1305.New into mustChaCha20: the only failure
mode is wrong-size key, which never happens with our 32-byte HKDF
output. The construction is now infallible by precondition.
* Drop the Sign() error return: ML-DSA-65 SignCtx with a valid
identity never errors. Sign is now (id) -> []byte. callers in
runInitiator / runResponder simplified.
* derive() drops the impossible io.ReadFull error branch — HKDF over
32-byte output is infallible.
* parseHandshakeResponse precheck guarantees the ct/encLen reads
cannot short-fail, so those error returns are elided.
* XWingEncapsulate / GenerateXWingKey drop curve25519.X25519 error
branches that only fire on wrong-length scalars (we always pass 32
bytes).
Defensive seams:
* parseTestableDSAPub (identity.go) wraps mldsa.PublicKeyFromBytes so
tests can drive the defensive parse-error path even though current
upstream cannot produce one.
* decapsulateMLKEM (kem.go) wraps ML-KEM Decapsulate similarly. Both
seams default to the real upstream call in production.
Coverage tests added (coverage_test.go, ~36 tests):
identity: GenerateIdentityFrom short-reader, Sign/Verify error paths,
ParseIdentityPublic size + ml-dsa + roundtrip, Equal
nil + mismatched DSA + mismatched X-Wing.
kem: ParseXWingPublicKey size, GenerateXWingKey nil + short reader,
XWingEncapsulate short reader, XWingDecapsulate size +
low-order ephemeral.
wire: writeFrame oversize + writer error + partial-fail,
readFrame header EOF + zero-length + max-length + body EOF.
handshake: parseHandshakeInit short reads + trailing garbage,
parseHandshakeResponse all 4 error paths,
splitRespIDPayload all 5 error paths,
runInitiator missing-config + write/read failures + bad
response (4 separate adversarial responses),
runResponder missing-config + read failure + write failure +
bad signature + low-order X-Wing peer pubkey.
channel: newConn happy path, mustChaCha20 panic on bad key,
Read/Write sequence-exhausted, multi-frame split,
corrupted-ciphertext, double close, deadline getters,
nonceFor edge cases.
dial/listen: missing-config, bogus addr, dial unreachable, accept
handshake fail + post-close, HandshakeTimeout > 0 path.
zap: DialZAP / ListenZAP happy path + missing-config + dial fail.
Wire format and KEM combiner output are unchanged. Cross-language
KAT (72df...fe4ed) still pins.
Add canonical Python and TypeScript ports of the IETF X-Wing combiner
under py/ and js/ inside this repo. Both implement
shared_secret = SHA3-256("\./X-Wing" || ss_M || ss_X || ct_X || pk_X)
byte-for-byte and pin the same KAT as the Go (kat_test.go) and Rust
(zap-protocol/zap zap::zwing) ports:
inputs: ssM=0x01..x32, ssX=0x02..x32, ctX=0x03..x32, pkX=0x04..x32
output: 72df2088314a73de80c21d9593f13fcd5629c800c70b1507f0dd918fde5fe4ed
Layout:
py/ pyproject.toml + zwing.py + test_zwing.py (stdlib hashlib)
js/ package.json + zwing.ts + zwing.test.ts (@noble/hashes ^2.2.0)
Z-Wing now has 4 byte-compatible implementations under one canonical
roof (luxfi/zwing). The Rust port lives at zap-protocol/zap as
zap::zwing because that's the canonical Rust home for the schema +
runtime. All four pin the same hex.
Pin SHA3-256("\./X-Wing" || ssM || ssX || ctX || pkX) for the fixed
inputs ssM=0x01...x32, ssX=0x02...x32, ctX=0x03...x32, pkX=0x04...x32.
Expected hex:
72df2088314a73de80c21d9593f13fcd5629c800c70b1507f0dd918fde5fe4ed
Same KAT is duplicated in:
- ~/work/zap/zap/src/zwing.rs (Rust crate)
- ~/work/zap/zap-py/tests/test_zwing.py
- ~/work/zap/zap-js/tests/zwing.test.ts
If any language port diverges on label, hash, or input ordering, that
test fails first. Cross-implementation interop drift caught at compile
time, not in production.
Composing PQ transport with ZAP RPC was already a 2-line pattern:
raw, _ := zwing.Dial(ctx, addr, cfg)
conn := zap.NewConn(raw, nil)
Two lines means two ways to mess it up (forget to wrap, leak the
raw conn). Add the canonical one-call wrappers so there is exactly
one way to do it:
conn, _ := zwing.DialZAP(ctx, addr, cfg)
ln, _ := zwing.ListenZAP(":9999", cfg)
srv := zap.NewServer(ln, handler)
Pairs with luxfi/api v1.0.10 zap.NewListener. No new wire format,
no new dep — these are sugar over the existing primitives.
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.