mirror of
https://github.com/luxfi/zwing.git
synced 2026-09-24 00:24:38 +00:00
Port the complete Z-Wing protocol to Python and TypeScript, no longer
just the X-Wing combiner. Each language now ships:
identity Hybrid Ed25519 + ML-DSA-65 + X-Wing keypair, marshal/parse,
sign/verify, constant-time equality
kem Full IETF X-Wing encapsulate/decapsulate (ML-KEM-768 +
X25519 + SHA3-256 combiner) with wire-byte compat
handshake 1-RTT mutual-auth handshake mirroring Go state machine —
same labels (lux.zwing.v1/handshake-init etc), same wire
format (HandshakeInit / HandshakeResponse / transcript hash
/ AEAD-sealed responder identity)
channel Post-handshake ChaCha20-Poly1305 with sequence-numbered
nonces (high 4 zero, low 8 BE counter), MAX 1 MiB frames
Python implementation:
- cryptography (Ed25519/X25519/ChaCha20-Poly1305) + kyber-py (ML-KEM)
+ dilithium-py (ML-DSA). Pure Python, zero native deps.
- test_full.py: 39 tests covering happy paths + every error branch
- py/__init__.py modules at 100% line coverage:
channel.py 51 stmts 100%
handshake.py 159 stmts 100%
identity.py 91 stmts 100%
kem.py 26 stmts 100%
zwing.py 27 stmts 100%
TypeScript implementation:
- @noble/post-quantum (ml-kem768, ml-dsa65) + @noble/curves (ed25519,
x25519) + @noble/ciphers (chacha20poly1305) + @noble/hashes
- full.test.ts: 32 tests covering full e2e + every wire-decode error
- vitest --coverage v8: 100% statements / 100% branches / 100%
functions / 100% lines across every source file (channel.ts,
handshake.ts, identity.ts, kem.ts, zwing.ts)
Cross-language KAT (72df...fe4ed) still pins on the X-Wing combiner.
Full handshake wire-byte interop is now possible Go↔Python (both use
FIPS 204 ML-DSA-65 with identical 3309-byte signatures); Rust uses
PQClean Dilithium3 which has the same byte sizes as Round 3 — full
Rust↔Go interop test pending a unified ML-DSA-65 source.
55 lines
1.9 KiB
Python
55 lines
1.9 KiB
Python
"""IETF X-Wing KEM (Python) — encap/decap matching the Go and Rust ports."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from cryptography.hazmat.primitives.asymmetric import x25519
|
|
from cryptography.hazmat.primitives.serialization import Encoding, PublicFormat
|
|
from kyber_py.ml_kem import ML_KEM_768
|
|
|
|
from zwing import (
|
|
MLKEM768_CIPHERTEXT_SIZE,
|
|
XWING_CIPHERTEXT_SIZE,
|
|
XWING_SHARED_SIZE,
|
|
X25519_POINT_SIZE,
|
|
combine_xwing,
|
|
)
|
|
from identity import Identity, IdentityPublic
|
|
|
|
|
|
def xwing_encapsulate(recipient: IdentityPublic) -> tuple[bytes, bytes]:
|
|
"""Encapsulate to a recipient. Returns (wire ciphertext, 32-byte shared)."""
|
|
ss_m, ct_m = ML_KEM_768.encaps(recipient.xwing_mlkem_pk)
|
|
|
|
eph_sk = x25519.X25519PrivateKey.generate()
|
|
eph_pk = eph_sk.public_key().public_bytes(
|
|
encoding=Encoding.Raw, format=PublicFormat.Raw
|
|
)
|
|
recipient_x25519 = x25519.X25519PublicKey.from_public_bytes(
|
|
recipient.xwing_x25519_pk
|
|
)
|
|
ss_x = eph_sk.exchange(recipient_x25519)
|
|
|
|
shared = combine_xwing(ss_m, ss_x, eph_pk, recipient.xwing_x25519_pk)
|
|
|
|
wire = bytearray(XWING_CIPHERTEXT_SIZE)
|
|
wire[:MLKEM768_CIPHERTEXT_SIZE] = ct_m
|
|
wire[MLKEM768_CIPHERTEXT_SIZE:] = eph_pk
|
|
|
|
return bytes(wire), shared
|
|
|
|
|
|
def xwing_decapsulate(identity: Identity, ciphertext: bytes) -> bytes:
|
|
"""Decapsulate using the identity's static X-Wing key."""
|
|
if len(ciphertext) != XWING_CIPHERTEXT_SIZE:
|
|
raise ValueError(
|
|
f"zwing: invalid X-Wing ciphertext size (got {len(ciphertext)})"
|
|
)
|
|
ct_m = ciphertext[:MLKEM768_CIPHERTEXT_SIZE]
|
|
eph_pk = ciphertext[MLKEM768_CIPHERTEXT_SIZE:]
|
|
|
|
ss_m = ML_KEM_768.decaps(identity.xwing_mlkem_sk, ct_m)
|
|
eph_pub = x25519.X25519PublicKey.from_public_bytes(eph_pk)
|
|
ss_x = identity.xwing_x25519_sk.exchange(eph_pub)
|
|
|
|
return combine_xwing(ss_m, ss_x, eph_pk, identity.xwing_x25519_pk_bytes)
|