mirror of
https://github.com/luxfi/zwing.git
synced 2026-09-24 00:24:38 +00:00
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.
69 lines
2.6 KiB
Python
69 lines
2.6 KiB
Python
"""Z-Wing — Lux post-quantum secure channel (Python port).
|
|
|
|
Z-Wing is a composition: IETF X-Wing KEM at the bottom (X25519 + ML-KEM-768
|
|
with the SHA3-256 combiner from draft-connolly-cfrg-xwing-kem) plus Lux
|
|
Ed25519 + ML-DSA-65 hybrid identity signatures plus ChaCha20-Poly1305
|
|
channel records. The KEM combiner is byte-for-byte the IETF spec for full
|
|
interop with the Go implementation in this same repo (luxfi/zwing).
|
|
|
|
X-Wing combiner (IETF spec, exact)::
|
|
|
|
shared_secret = SHA3-256( "\\./" || "X-Wing" || ss_M || ss_X || ct_X || pk_X )
|
|
|
|
The label is ``"X-Wing"``, not ``"Z-Wing"``. Z-Wing's domain separation
|
|
lives one layer up in the HKDF info string ``"lux.zwing.v1/{i2r,r2i}"``
|
|
that derives the channel keys, and in the ML-DSA context
|
|
``"lux.zwing.v1"`` used for hybrid identity signatures.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import hashlib
|
|
|
|
# ML-KEM-768 public key size in bytes.
|
|
MLKEM768_PUBLIC_KEY_SIZE = 1184
|
|
# ML-KEM-768 ciphertext size in bytes.
|
|
MLKEM768_CIPHERTEXT_SIZE = 1088
|
|
# X25519 public key (and ciphertext) size in bytes.
|
|
X25519_POINT_SIZE = 32
|
|
|
|
# X-Wing public key wire size: ML-KEM-768 pk || X25519 pk.
|
|
XWING_PUBLIC_KEY_SIZE = MLKEM768_PUBLIC_KEY_SIZE + X25519_POINT_SIZE
|
|
# X-Wing ciphertext wire size: ML-KEM-768 ct || X25519 ephemeral pk.
|
|
XWING_CIPHERTEXT_SIZE = MLKEM768_CIPHERTEXT_SIZE + X25519_POINT_SIZE
|
|
# Z-Wing / X-Wing shared-secret size in bytes.
|
|
XWING_SHARED_SIZE = 32
|
|
|
|
# IETF X-Wing combiner labels (exact bytes from draft-connolly-cfrg-xwing-kem).
|
|
# Three-byte prefix `\./` plus the six-byte ASCII protocol name `X-Wing`.
|
|
_XWING_LABEL_PREFIX = b"\\./"
|
|
_XWING_LABEL_NAME = b"X-Wing"
|
|
|
|
|
|
def combine_xwing(ss_m: bytes, ss_x: bytes, ct_x: bytes, pk_x: bytes) -> bytes:
|
|
"""Combine the X-Wing KEM ingredients into a single 32-byte shared secret.
|
|
|
|
All inputs are 32 bytes:
|
|
ss_m: ML-KEM-768 shared secret
|
|
ss_x: X25519 shared secret
|
|
ct_x: X25519 ephemeral public key from encapsulator
|
|
pk_x: X25519 static public key of recipient
|
|
"""
|
|
if len(ss_m) != XWING_SHARED_SIZE:
|
|
raise ValueError(f"ss_m must be {XWING_SHARED_SIZE} bytes")
|
|
if len(ss_x) != X25519_POINT_SIZE:
|
|
raise ValueError(f"ss_x must be {X25519_POINT_SIZE} bytes")
|
|
if len(ct_x) != X25519_POINT_SIZE:
|
|
raise ValueError(f"ct_x must be {X25519_POINT_SIZE} bytes")
|
|
if len(pk_x) != X25519_POINT_SIZE:
|
|
raise ValueError(f"pk_x must be {X25519_POINT_SIZE} bytes")
|
|
|
|
h = hashlib.sha3_256()
|
|
h.update(_XWING_LABEL_PREFIX)
|
|
h.update(_XWING_LABEL_NAME)
|
|
h.update(ss_m)
|
|
h.update(ss_x)
|
|
h.update(ct_x)
|
|
h.update(pk_x)
|
|
return h.digest()
|