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.
81 lines
2.3 KiB
TypeScript
81 lines
2.3 KiB
TypeScript
/**
|
|
* IETF X-Wing KEM — encapsulate / decapsulate built on ML-KEM-768 +
|
|
* X25519 with the SHA3-256 combiner. Wire bytes match the Go and Rust
|
|
* implementations.
|
|
*/
|
|
|
|
import { ml_kem768 } from "@noble/post-quantum/ml-kem.js";
|
|
import { x25519 } from "@noble/curves/ed25519.js";
|
|
|
|
import {
|
|
combineXWing,
|
|
MLKEM768_CIPHERTEXT_SIZE,
|
|
X25519_POINT_SIZE,
|
|
XWING_CIPHERTEXT_SIZE,
|
|
XWING_SHARED_SIZE,
|
|
} from "./zwing.js";
|
|
import type { Identity, IdentityPublic } from "./identity.js";
|
|
|
|
/**
|
|
* Encapsulate a fresh X-Wing shared secret to a recipient. Returns the
|
|
* combined wire ciphertext (ML-KEM-768 ct || X25519 ephemeral pk) and
|
|
* the 32-byte shared secret.
|
|
*/
|
|
export function xwingEncapsulate(recipient: IdentityPublic): {
|
|
ciphertext: Uint8Array;
|
|
shared: Uint8Array;
|
|
} {
|
|
const { sharedSecret: ssM, cipherText: ctM } = ml_kem768.encapsulate(
|
|
recipient.xwingMlkemPk,
|
|
);
|
|
|
|
const ephSk = crypto.getRandomValues(new Uint8Array(32));
|
|
const ephPk = x25519.getPublicKey(ephSk);
|
|
const ssX = x25519.getSharedSecret(ephSk, recipient.xwingX25519Pk);
|
|
|
|
const ssM32: Uint8Array = ssM.slice(0, 32);
|
|
const ssX32: Uint8Array = ssX.slice(0, 32);
|
|
const shared = combineXWing(ssM32, ssX32, ephPk, recipient.xwingX25519Pk);
|
|
|
|
const wire = new Uint8Array(XWING_CIPHERTEXT_SIZE);
|
|
wire.set(ctM, 0);
|
|
wire.set(ephPk, MLKEM768_CIPHERTEXT_SIZE);
|
|
|
|
// Best-effort wipe of intermediate secrets.
|
|
ephSk.fill(0);
|
|
ssM.fill(0);
|
|
ssX.fill(0);
|
|
|
|
return { ciphertext: wire, shared };
|
|
}
|
|
|
|
/**
|
|
* Decapsulate an X-Wing wire ciphertext using `id`'s static keypair.
|
|
* Throws on size mismatch or low-order ephemeral.
|
|
*/
|
|
export function xwingDecapsulate(id: Identity, ciphertext: Uint8Array): Uint8Array {
|
|
if (ciphertext.length !== XWING_CIPHERTEXT_SIZE) {
|
|
throw new Error(
|
|
`zwing: invalid X-Wing ciphertext size (got ${ciphertext.length})`,
|
|
);
|
|
}
|
|
const ctM = ciphertext.slice(0, MLKEM768_CIPHERTEXT_SIZE);
|
|
const ephPk = ciphertext.slice(MLKEM768_CIPHERTEXT_SIZE);
|
|
|
|
const ssM = ml_kem768.decapsulate(ctM, id.xwingMlkemSk);
|
|
const ssX = x25519.getSharedSecret(id.xwingX25519Sk, ephPk);
|
|
|
|
const shared = combineXWing(
|
|
ssM.slice(0, 32),
|
|
ssX.slice(0, 32),
|
|
ephPk,
|
|
id.xwingX25519Pk,
|
|
);
|
|
|
|
ssM.fill(0);
|
|
ssX.fill(0);
|
|
return shared;
|
|
}
|
|
|
|
export { XWING_CIPHERTEXT_SIZE, XWING_SHARED_SIZE, X25519_POINT_SIZE };
|