mirror of
https://github.com/luxfi/zwing.git
synced 2026-09-24 00:24:38 +00:00
zwing: Python (zwing/py) + TS (zwing/js) ports with cross-language KAT
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.
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
node_modules/
|
||||
dist/
|
||||
pnpm-lock.yaml
|
||||
@@ -0,0 +1,50 @@
|
||||
{
|
||||
"name": "@luxfi/zwing",
|
||||
"version": "0.2.2",
|
||||
"description": "Z-Wing — Lux post-quantum secure channel (TypeScript). IETF X-Wing KEM combiner.",
|
||||
"type": "module",
|
||||
"main": "./dist/zwing.js",
|
||||
"module": "./dist/zwing.js",
|
||||
"types": "./dist/zwing.d.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/zwing.d.ts",
|
||||
"import": "./dist/zwing.js"
|
||||
}
|
||||
},
|
||||
"files": [
|
||||
"dist",
|
||||
"zwing.ts",
|
||||
"README.md",
|
||||
"LICENSE"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "tsc -p tsconfig.json",
|
||||
"test": "vitest run",
|
||||
"test:watch": "vitest"
|
||||
},
|
||||
"keywords": [
|
||||
"post-quantum",
|
||||
"kem",
|
||||
"x-wing",
|
||||
"ml-kem",
|
||||
"zwing",
|
||||
"lux"
|
||||
],
|
||||
"author": "Lux Industries <[email protected]>",
|
||||
"license": "MIT OR Apache-2.0",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/luxfi/zwing.git",
|
||||
"directory": "js"
|
||||
},
|
||||
"homepage": "https://github.com/luxfi/zwing/tree/main/js#readme",
|
||||
"bugs": "https://github.com/luxfi/zwing/issues",
|
||||
"dependencies": {
|
||||
"@noble/hashes": "^2.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5.7.0",
|
||||
"vitest": "^2.0.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "ES2022",
|
||||
"moduleResolution": "bundler",
|
||||
"lib": ["ES2022", "DOM"],
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./",
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"sourceMap": true,
|
||||
"strict": true,
|
||||
"noImplicitAny": true,
|
||||
"strictNullChecks": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"resolveJsonModule": true,
|
||||
"allowImportingTsExtensions": false
|
||||
},
|
||||
"include": ["zwing.ts"],
|
||||
"exclude": ["node_modules", "dist", "*.test.ts"]
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { XWING_SHARED_SIZE, combineXWing } from "./zwing.js";
|
||||
|
||||
// Cross-language KAT shared with Go (../kat_test.go), Rust
|
||||
// (~/work/zap/zap/src/zwing.rs), and Python (../py/test_zwing.py). If
|
||||
// this hex changes, Z-Wing has diverged across implementations.
|
||||
const KAT_HEX =
|
||||
"72df2088314a73de80c21d9593f13fcd5629c800c70b1507f0dd918fde5fe4ed";
|
||||
|
||||
function repeat(byte: number, len: number): Uint8Array {
|
||||
return new Uint8Array(len).fill(byte);
|
||||
}
|
||||
|
||||
function toHex(bytes: Uint8Array): string {
|
||||
return Array.from(bytes)
|
||||
.map((b) => b.toString(16).padStart(2, "0"))
|
||||
.join("");
|
||||
}
|
||||
|
||||
describe("X-Wing combiner KAT (cross-language)", () => {
|
||||
it("matches the canonical KAT for repeated 0x01..0x04 inputs", () => {
|
||||
const got = combineXWing(
|
||||
repeat(0x01, 32),
|
||||
repeat(0x02, 32),
|
||||
repeat(0x03, 32),
|
||||
repeat(0x04, 32),
|
||||
);
|
||||
expect(got.length).toBe(XWING_SHARED_SIZE);
|
||||
expect(toHex(got)).toBe(KAT_HEX);
|
||||
});
|
||||
|
||||
it("is deterministic", () => {
|
||||
const a = combineXWing(
|
||||
repeat(0x01, 32),
|
||||
repeat(0x02, 32),
|
||||
repeat(0x03, 32),
|
||||
repeat(0x04, 32),
|
||||
);
|
||||
const b = combineXWing(
|
||||
repeat(0x01, 32),
|
||||
repeat(0x02, 32),
|
||||
repeat(0x03, 32),
|
||||
repeat(0x04, 32),
|
||||
);
|
||||
expect(toHex(a)).toBe(toHex(b));
|
||||
});
|
||||
|
||||
it("changes on any input flip", () => {
|
||||
const base = combineXWing(
|
||||
repeat(0x01, 32),
|
||||
repeat(0x02, 32),
|
||||
repeat(0x03, 32),
|
||||
repeat(0x04, 32),
|
||||
);
|
||||
const flipped = repeat(0x01, 32);
|
||||
flipped[0] ^= 0x01;
|
||||
const mutated = combineXWing(
|
||||
flipped,
|
||||
repeat(0x02, 32),
|
||||
repeat(0x03, 32),
|
||||
repeat(0x04, 32),
|
||||
);
|
||||
expect(toHex(base)).not.toBe(toHex(mutated));
|
||||
});
|
||||
|
||||
it("rejects wrong-size inputs", () => {
|
||||
expect(() =>
|
||||
combineXWing(
|
||||
repeat(0x00, 31),
|
||||
repeat(0x00, 32),
|
||||
repeat(0x00, 32),
|
||||
repeat(0x00, 32),
|
||||
),
|
||||
).toThrow();
|
||||
});
|
||||
});
|
||||
+80
@@ -0,0 +1,80 @@
|
||||
/**
|
||||
* Z-Wing — Lux post-quantum secure channel (TypeScript 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) and any third-party
|
||||
* X-Wing peer.
|
||||
*
|
||||
* 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.
|
||||
*
|
||||
* @module
|
||||
*/
|
||||
|
||||
import { sha3_256 } from "@noble/hashes/sha3.js";
|
||||
|
||||
/** ML-KEM-768 public key size in bytes. */
|
||||
export const MLKEM768_PUBLIC_KEY_SIZE = 1184;
|
||||
/** ML-KEM-768 ciphertext size in bytes. */
|
||||
export const MLKEM768_CIPHERTEXT_SIZE = 1088;
|
||||
/** X25519 public key (and ciphertext) size in bytes. */
|
||||
export const X25519_POINT_SIZE = 32;
|
||||
|
||||
/** X-Wing public key wire size: ML-KEM-768 pk || X25519 pk. */
|
||||
export const XWING_PUBLIC_KEY_SIZE =
|
||||
MLKEM768_PUBLIC_KEY_SIZE + X25519_POINT_SIZE;
|
||||
/** X-Wing ciphertext wire size: ML-KEM-768 ct || X25519 ephemeral pk. */
|
||||
export const XWING_CIPHERTEXT_SIZE =
|
||||
MLKEM768_CIPHERTEXT_SIZE + X25519_POINT_SIZE;
|
||||
/** Z-Wing / X-Wing shared-secret size in bytes. */
|
||||
export const 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`.
|
||||
const XWING_LABEL_PREFIX = new Uint8Array([0x5c, 0x2e, 0x2f]); // "\./"
|
||||
const XWING_LABEL_NAME = new TextEncoder().encode("X-Wing");
|
||||
|
||||
/**
|
||||
* Combine the X-Wing KEM ingredients into a single 32-byte shared
|
||||
* secret. Inputs are all 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.
|
||||
*/
|
||||
export function combineXWing(
|
||||
ss_m: Uint8Array,
|
||||
ss_x: Uint8Array,
|
||||
ct_x: Uint8Array,
|
||||
pk_x: Uint8Array,
|
||||
): Uint8Array {
|
||||
if (ss_m.length !== XWING_SHARED_SIZE) {
|
||||
throw new Error(`ss_m must be ${XWING_SHARED_SIZE} bytes`);
|
||||
}
|
||||
if (ss_x.length !== X25519_POINT_SIZE) {
|
||||
throw new Error(`ss_x must be ${X25519_POINT_SIZE} bytes`);
|
||||
}
|
||||
if (ct_x.length !== X25519_POINT_SIZE) {
|
||||
throw new Error(`ct_x must be ${X25519_POINT_SIZE} bytes`);
|
||||
}
|
||||
if (pk_x.length !== X25519_POINT_SIZE) {
|
||||
throw new Error(`pk_x must be ${X25519_POINT_SIZE} bytes`);
|
||||
}
|
||||
|
||||
const h = sha3_256.create();
|
||||
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();
|
||||
}
|
||||
Binary file not shown.
@@ -0,0 +1,31 @@
|
||||
[project]
|
||||
name = "zwing"
|
||||
version = "0.2.2"
|
||||
description = "Z-Wing — Lux post-quantum secure channel (Python). IETF X-Wing KEM combiner."
|
||||
readme = "README.md"
|
||||
license = { text = "MIT OR Apache-2.0" }
|
||||
authors = [{ name = "Lux Industries", email = "[email protected]" }]
|
||||
requires-python = ">=3.10"
|
||||
keywords = ["post-quantum", "kem", "x-wing", "ml-kem", "zwing", "lux"]
|
||||
classifiers = [
|
||||
"Development Status :: 4 - Beta",
|
||||
"Intended Audience :: Developers",
|
||||
"Topic :: Security :: Cryptography",
|
||||
"Programming Language :: Python :: 3",
|
||||
"Programming Language :: Python :: 3.10",
|
||||
"Programming Language :: Python :: 3.11",
|
||||
"Programming Language :: Python :: 3.12",
|
||||
"Programming Language :: Python :: 3.13",
|
||||
]
|
||||
|
||||
[project.urls]
|
||||
Homepage = "https://github.com/luxfi/zwing"
|
||||
Repository = "https://github.com/luxfi/zwing"
|
||||
Issues = "https://github.com/luxfi/zwing/issues"
|
||||
|
||||
[build-system]
|
||||
requires = ["hatchling"]
|
||||
build-backend = "hatchling.build"
|
||||
|
||||
[tool.hatch.build.targets.wheel]
|
||||
packages = ["zwing.py"]
|
||||
@@ -0,0 +1,49 @@
|
||||
"""Z-Wing X-Wing combiner KAT — must match Go/Rust/JS impls byte-for-byte.
|
||||
|
||||
Run with:
|
||||
|
||||
cd py && python3 test_zwing.py -v
|
||||
"""
|
||||
|
||||
import unittest
|
||||
|
||||
from zwing import (
|
||||
XWING_SHARED_SIZE,
|
||||
combine_xwing,
|
||||
)
|
||||
|
||||
|
||||
# Cross-language KAT shared with Go (../kat_test.go), Rust
|
||||
# (~/work/zap/zap/src/zwing.rs), and JS (../js/zwing.test.ts). If
|
||||
# this hex changes, Z-Wing has diverged across implementations.
|
||||
KAT_HEX = "72df2088314a73de80c21d9593f13fcd5629c800c70b1507f0dd918fde5fe4ed"
|
||||
|
||||
|
||||
class XWingCombinerKAT(unittest.TestCase):
|
||||
def test_xwing_combiner_kat_constant_inputs(self):
|
||||
ss_m = b"\x01" * 32
|
||||
ss_x = b"\x02" * 32
|
||||
ct_x = b"\x03" * 32
|
||||
pk_x = b"\x04" * 32
|
||||
got = combine_xwing(ss_m, ss_x, ct_x, pk_x)
|
||||
self.assertEqual(len(got), XWING_SHARED_SIZE)
|
||||
self.assertEqual(got.hex(), KAT_HEX)
|
||||
|
||||
def test_xwing_combiner_is_deterministic(self):
|
||||
a = combine_xwing(b"\x01" * 32, b"\x02" * 32, b"\x03" * 32, b"\x04" * 32)
|
||||
b = combine_xwing(b"\x01" * 32, b"\x02" * 32, b"\x03" * 32, b"\x04" * 32)
|
||||
self.assertEqual(a, b)
|
||||
|
||||
def test_xwing_combiner_changes_on_any_input_change(self):
|
||||
base = combine_xwing(b"\x01" * 32, b"\x02" * 32, b"\x03" * 32, b"\x04" * 32)
|
||||
flipped = bytes([0x01 ^ 0x01]) + b"\x01" * 31
|
||||
mutated = combine_xwing(flipped, b"\x02" * 32, b"\x03" * 32, b"\x04" * 32)
|
||||
self.assertNotEqual(base, mutated)
|
||||
|
||||
def test_xwing_combiner_rejects_wrong_size(self):
|
||||
with self.assertRaises(ValueError):
|
||||
combine_xwing(b"\x00" * 31, b"\x00" * 32, b"\x00" * 32, b"\x00" * 32)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
+68
@@ -0,0 +1,68 @@
|
||||
"""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()
|
||||
Reference in New Issue
Block a user