kxco-post-quantum 1.0.2 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +116 -63
- package/README.md +40 -5
- package/SECURITY.md +22 -89
- package/package.json +39 -15
- package/src/derive.d.ts +20 -0
- package/src/derive.js +24 -10
- package/src/index.d.ts +7 -0
- package/src/kid.d.ts +21 -0
- package/src/kid.js +24 -9
- package/src/ml-dsa.d.ts +45 -0
- package/src/ml-dsa.js +36 -16
- package/src/ml-kem.d.ts +49 -0
- package/src/ml-kem.js +18 -16
- package/src/webhook.d.ts +119 -0
- package/src/webhook.js +50 -52
- package/AUDIT.md +0 -110
- package/test/run-vectors.js +0 -155
- package/test/vectors.json +0 -137
package/test/run-vectors.js
DELETED
|
@@ -1,155 +0,0 @@
|
|
|
1
|
-
// Run-vectors — verifies every entry in test/vectors.json against the library.
|
|
2
|
-
//
|
|
3
|
-
// Anyone can `git clone && npm install && node test/run-vectors.js` and see
|
|
4
|
-
// the same bit-for-bit outputs. This is the reproducibility claim.
|
|
5
|
-
//
|
|
6
|
-
// Exit code 0 = all pass. Exit code 1 = at least one mismatch.
|
|
7
|
-
|
|
8
|
-
import { readFileSync } from 'node:fs'
|
|
9
|
-
import { createHash } from 'node:crypto'
|
|
10
|
-
import { fileURLToPath } from 'node:url'
|
|
11
|
-
import { dirname, join } from 'node:path'
|
|
12
|
-
|
|
13
|
-
import {
|
|
14
|
-
mlDsa, mlKem, deriveSeed, fingerprint, webhook,
|
|
15
|
-
} from '../src/index.js'
|
|
16
|
-
|
|
17
|
-
const here = dirname(fileURLToPath(import.meta.url))
|
|
18
|
-
const vectors = JSON.parse(readFileSync(join(here, 'vectors.json'), 'utf8'))
|
|
19
|
-
|
|
20
|
-
const sha256hex = (b) => createHash('sha256').update(b).digest('hex')
|
|
21
|
-
|
|
22
|
-
let pass = 0
|
|
23
|
-
let fail = 0
|
|
24
|
-
const failures = []
|
|
25
|
-
|
|
26
|
-
function check(group, name, expected, actual) {
|
|
27
|
-
const ok = expected === actual ||
|
|
28
|
-
(typeof expected === 'boolean' && expected === actual) ||
|
|
29
|
-
(typeof expected === 'number' && expected === actual)
|
|
30
|
-
if (ok) {
|
|
31
|
-
pass++
|
|
32
|
-
} else {
|
|
33
|
-
fail++
|
|
34
|
-
failures.push({ group, name, expected, actual })
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
// deriveSeed
|
|
39
|
-
for (const v of vectors.deriveSeed) {
|
|
40
|
-
const master = Buffer.from(v.master_hex, 'hex')
|
|
41
|
-
const got = deriveSeed(master, v.info, v.length).toString('hex')
|
|
42
|
-
check('deriveSeed', v.name, v.expect_hex, got)
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
// mlDsa keypairFromMaster — pin SHA-256 of pub + secret bytes
|
|
46
|
-
for (const v of vectors.mlDsa_keypairFromMaster) {
|
|
47
|
-
const master = Buffer.from(v.master_hex, 'hex')
|
|
48
|
-
const kp = mlDsa.keypairFromMaster(master, v.info)
|
|
49
|
-
check('mlDsa.keypairFromMaster', v.name + ' [publicKey_sha256]', v.publicKey_sha256, sha256hex(kp.publicKey))
|
|
50
|
-
check('mlDsa.keypairFromMaster', v.name + ' [secretKey_sha256]', v.secretKey_sha256, sha256hex(kp.secretKey))
|
|
51
|
-
check('mlDsa.keypairFromMaster', v.name + ' [publicKey_bytes]', v.publicKey_bytes, kp.publicKey.length)
|
|
52
|
-
check('mlDsa.keypairFromMaster', v.name + ' [secretKey_bytes]', v.secretKey_bytes, kp.secretKey.length)
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
// mlDsa sign round-trip
|
|
56
|
-
for (const v of vectors.mlDsa_sign_roundtrip) {
|
|
57
|
-
const master = Buffer.from(v.master_hex, 'hex')
|
|
58
|
-
const kp = mlDsa.keypairFromMaster(master, v.info)
|
|
59
|
-
const sig = mlDsa.sign(kp.secretKey, v.message_utf8)
|
|
60
|
-
const verified = mlDsa.verify(kp.publicKey, v.message_utf8, sig)
|
|
61
|
-
check('mlDsa.sign_roundtrip', v.name + ' [verify]', v.expect_verify, verified)
|
|
62
|
-
check('mlDsa.sign_roundtrip', v.name + ' [sig_hex_length]', v.expect_sig_hex_length, sig.length)
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
// mlKem keypairFromMaster
|
|
66
|
-
for (const v of vectors.mlKem_keypairFromMaster) {
|
|
67
|
-
const master = Buffer.from(v.master_hex, 'hex')
|
|
68
|
-
const kp = mlKem.keypairFromMaster(master, v.info)
|
|
69
|
-
check('mlKem.keypairFromMaster', v.name + ' [publicKey_sha256]', v.publicKey_sha256, sha256hex(kp.publicKey))
|
|
70
|
-
check('mlKem.keypairFromMaster', v.name + ' [secretKey_sha256]', v.secretKey_sha256, sha256hex(kp.secretKey))
|
|
71
|
-
check('mlKem.keypairFromMaster', v.name + ' [publicKey_bytes]', v.publicKey_bytes, kp.publicKey.length)
|
|
72
|
-
check('mlKem.keypairFromMaster', v.name + ' [secretKey_bytes]', v.secretKey_bytes, kp.secretKey.length)
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
// mlKem encapsulate round-trip
|
|
76
|
-
for (const v of vectors.mlKem_encapsulate_roundtrip) {
|
|
77
|
-
const master = Buffer.from(v.master_hex, 'hex')
|
|
78
|
-
const kp = mlKem.keypairFromMaster(master, v.info)
|
|
79
|
-
const { ciphertext, sharedSecret } = mlKem.encapsulate(kp.publicKey)
|
|
80
|
-
const recovered = mlKem.decapsulate(ciphertext, kp.secretKey)
|
|
81
|
-
check('mlKem.encapsulate_roundtrip', v.name + ' [secret_bytes]', v.expect_sharedSecret_bytes, sharedSecret.length)
|
|
82
|
-
check('mlKem.encapsulate_roundtrip', v.name + ' [ct_bytes]', v.expect_ciphertext_bytes, ciphertext.length)
|
|
83
|
-
check('mlKem.encapsulate_roundtrip', v.name + ' [recovered_eq]', sha256hex(sharedSecret), sha256hex(recovered))
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
// fingerprint
|
|
87
|
-
for (const v of vectors.fingerprint) {
|
|
88
|
-
let input
|
|
89
|
-
if (v.input_hex) input = Buffer.from(v.input_hex, 'hex')
|
|
90
|
-
else if (v.input_utf8) input = Buffer.from(v.input_utf8, 'utf8')
|
|
91
|
-
else if (v.input_source && v.input_source.includes('mlDsa.keypairFromMaster')) {
|
|
92
|
-
input = mlDsa.keypairFromMaster(Buffer.from('00'.repeat(32), 'hex'), 'platform-v1').publicKey
|
|
93
|
-
}
|
|
94
|
-
const got = fingerprint(input)
|
|
95
|
-
check('fingerprint', v.name, v.expect_kid, got)
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
// webhook envelope
|
|
99
|
-
for (const v of vectors.webhook_envelope) {
|
|
100
|
-
const got = webhook.envelope(v.timestamp, v.body_utf8).toString('hex')
|
|
101
|
-
check('webhook.envelope', v.name, v.expect_envelope_hex, got)
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
// webhook hmac
|
|
105
|
-
for (const v of vectors.webhook_hmac) {
|
|
106
|
-
const got = webhook.hmacHex(v.secret_utf8, v.timestamp, v.body_utf8)
|
|
107
|
-
check('webhook.hmac', v.name, v.expect_hmac_hex, got)
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
// webhook hybrid round-trip with FRESH timestamp (vector's timestamp is stale on purpose; we use Date.now() here)
|
|
111
|
-
for (const v of vectors.webhook_hybrid_roundtrip) {
|
|
112
|
-
const master = Buffer.from(v.master_hex, 'hex')
|
|
113
|
-
const kp = mlDsa.keypairFromMaster(master, v.info)
|
|
114
|
-
const kid = fingerprint(kp.publicKey)
|
|
115
|
-
const freshTs = Math.floor(Date.now() / 1000).toString()
|
|
116
|
-
const headers = webhook.signDelivery({
|
|
117
|
-
rawBody: v.body_utf8,
|
|
118
|
-
hmacSecret: v.hmac_secret_utf8,
|
|
119
|
-
pqSecretKey: kp.secretKey,
|
|
120
|
-
pqKid: kid,
|
|
121
|
-
})
|
|
122
|
-
const lower = Object.fromEntries(Object.entries(headers).map(([k, v]) => [k.toLowerCase(), v]))
|
|
123
|
-
const r = webhook.verifyDelivery({
|
|
124
|
-
headers: lower,
|
|
125
|
-
rawBody: v.body_utf8,
|
|
126
|
-
hmacSecret: v.hmac_secret_utf8,
|
|
127
|
-
pqPublicKey: kp.publicKey,
|
|
128
|
-
pinnedKid: kid,
|
|
129
|
-
})
|
|
130
|
-
check('webhook.hybrid_roundtrip', v.name + ' [hmac_ok]', v.expect_hmac_ok, r.hmacOk)
|
|
131
|
-
check('webhook.hybrid_roundtrip', v.name + ' [pq_ok]', v.expect_pq_ok, r.pqOk)
|
|
132
|
-
check('webhook.hybrid_roundtrip', v.name + ' [ts_ok]', v.expect_timestamp_ok_when_fresh, r.timestampOk)
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
// Report
|
|
136
|
-
console.log('')
|
|
137
|
-
console.log(`kxco-post-quantum test vectors`)
|
|
138
|
-
console.log(`Generated: ${vectors.generated_at}`)
|
|
139
|
-
console.log(`Library: ${vectors.library}`)
|
|
140
|
-
console.log(`Underlying: ${vectors.underlying_primitive}`)
|
|
141
|
-
console.log('')
|
|
142
|
-
|
|
143
|
-
if (fail === 0) {
|
|
144
|
-
console.log(`✓ All ${pass} checks pass — library output matches pinned vectors bit-for-bit.`)
|
|
145
|
-
process.exit(0)
|
|
146
|
-
} else {
|
|
147
|
-
console.log(`✗ ${fail} failed, ${pass} passed.`)
|
|
148
|
-
for (const f of failures) {
|
|
149
|
-
console.log(`\n GROUP: ${f.group}`)
|
|
150
|
-
console.log(` NAME: ${f.name}`)
|
|
151
|
-
console.log(` EXPECTED: ${f.expected}`)
|
|
152
|
-
console.log(` ACTUAL: ${f.actual}`)
|
|
153
|
-
}
|
|
154
|
-
process.exit(1)
|
|
155
|
-
}
|
package/test/vectors.json
DELETED
|
@@ -1,137 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"version": "1.0",
|
|
3
|
-
"generated_at": "2026-05-20T18:58:39.432Z",
|
|
4
|
-
"description": "Deterministic test vectors for kxco-post-quantum. Run test/run-vectors.js to verify these match the library output bit-for-bit.",
|
|
5
|
-
"library": "kxco-post-quantum",
|
|
6
|
-
"underlying_primitive": "@noble/post-quantum",
|
|
7
|
-
"notes": [
|
|
8
|
-
"ML-DSA-65 signatures are randomized — we cannot pin signature bytes. We pin keypair hashes and verify round-trip.",
|
|
9
|
-
"ML-KEM encapsulation is randomized — we pin keypair hashes and verify decapsulate round-trip in the runner.",
|
|
10
|
-
"All hex is lowercase. All UTF-8 strings are explicit."
|
|
11
|
-
],
|
|
12
|
-
"deriveSeed": [
|
|
13
|
-
{
|
|
14
|
-
"name": "zero master, info=role-a, length=32",
|
|
15
|
-
"master_hex": "0000000000000000000000000000000000000000000000000000000000000000",
|
|
16
|
-
"info": "role-a",
|
|
17
|
-
"length": 32,
|
|
18
|
-
"expect_hex": "873c6de3e076daede530be45f1f7953a29fec20648557429d2607e08c3ad8d7c"
|
|
19
|
-
},
|
|
20
|
-
{
|
|
21
|
-
"name": "zero master, info=role-b, length=32 (different info → different seed)",
|
|
22
|
-
"master_hex": "0000000000000000000000000000000000000000000000000000000000000000",
|
|
23
|
-
"info": "role-b",
|
|
24
|
-
"length": 32,
|
|
25
|
-
"expect_hex": "80f852c16846b2a44c64c7742cd8df83f9b1a98f969dd0496904c3bdd08cd8cc"
|
|
26
|
-
},
|
|
27
|
-
{
|
|
28
|
-
"name": "0x11 master, info=platform-v1, length=64",
|
|
29
|
-
"master_hex": "1111111111111111111111111111111111111111111111111111111111111111",
|
|
30
|
-
"info": "platform-v1",
|
|
31
|
-
"length": 64,
|
|
32
|
-
"expect_hex": "a9df8abe6efa342d56b911f8eeea67bb553d675e4411ea046b9aca2fa409fcc2b92166b6117606627faed0a66f890403ad2fa6d9d3dadc69bd5559f3da92eec3"
|
|
33
|
-
}
|
|
34
|
-
],
|
|
35
|
-
"mlDsa_keypairFromMaster": [
|
|
36
|
-
{
|
|
37
|
-
"name": "zero master, info=platform-v1",
|
|
38
|
-
"master_hex": "0000000000000000000000000000000000000000000000000000000000000000",
|
|
39
|
-
"info": "platform-v1",
|
|
40
|
-
"publicKey_sha256": "dc29825b2f9cc9f1671ca499c27d013c2b4ac86e1c6ce365f40556ec82916e57",
|
|
41
|
-
"publicKey_bytes": 1952,
|
|
42
|
-
"secretKey_sha256": "fad19856458d742f62d79dbd772cfaea7ad2e432111a9663d159b079d15f51eb",
|
|
43
|
-
"secretKey_bytes": 4032
|
|
44
|
-
},
|
|
45
|
-
{
|
|
46
|
-
"name": "0xff master, info=test",
|
|
47
|
-
"master_hex": "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
|
|
48
|
-
"info": "test",
|
|
49
|
-
"publicKey_sha256": "b5efe3602d7d87e29ce642ff44a682ded5408e43b965b2a22e5aedec0d8ab9aa",
|
|
50
|
-
"publicKey_bytes": 1952,
|
|
51
|
-
"secretKey_sha256": "abd46a2431e7e82249e798e7ca634785ba1f43e052e4649a4e0e02536db47770",
|
|
52
|
-
"secretKey_bytes": 4032
|
|
53
|
-
}
|
|
54
|
-
],
|
|
55
|
-
"mlDsa_sign_roundtrip": [
|
|
56
|
-
{
|
|
57
|
-
"name": "sign known message with keypair from zero master; verify true",
|
|
58
|
-
"master_hex": "0000000000000000000000000000000000000000000000000000000000000000",
|
|
59
|
-
"info": "platform-v1",
|
|
60
|
-
"message_utf8": "hello kxco",
|
|
61
|
-
"expect_verify": true,
|
|
62
|
-
"expect_sig_hex_length": 6618
|
|
63
|
-
}
|
|
64
|
-
],
|
|
65
|
-
"mlKem_keypairFromMaster": [
|
|
66
|
-
{
|
|
67
|
-
"name": "zero master, info=platform-v1",
|
|
68
|
-
"master_hex": "0000000000000000000000000000000000000000000000000000000000000000",
|
|
69
|
-
"info": "platform-v1",
|
|
70
|
-
"publicKey_sha256": "8393aadd8e9070c8b43b65a1e05eb43f9c9e7e32ba7880261a2cbcabda58c188",
|
|
71
|
-
"publicKey_bytes": 1184,
|
|
72
|
-
"secretKey_sha256": "8d21f316efc198ba3cab8e7c9270f57efe351bed4d0f66c66f6d454d4282423e",
|
|
73
|
-
"secretKey_bytes": 2400
|
|
74
|
-
}
|
|
75
|
-
],
|
|
76
|
-
"mlKem_encapsulate_roundtrip": [
|
|
77
|
-
{
|
|
78
|
-
"name": "encapsulate to keypair from zero master; decapsulate recovers same secret",
|
|
79
|
-
"master_hex": "0000000000000000000000000000000000000000000000000000000000000000",
|
|
80
|
-
"info": "platform-v1",
|
|
81
|
-
"expect_sharedSecret_bytes": 32,
|
|
82
|
-
"expect_ciphertext_bytes": 1088
|
|
83
|
-
}
|
|
84
|
-
],
|
|
85
|
-
"fingerprint": [
|
|
86
|
-
{
|
|
87
|
-
"name": "fingerprint of 32 zero bytes",
|
|
88
|
-
"input_hex": "0000000000000000000000000000000000000000000000000000000000000000",
|
|
89
|
-
"expect_kid": "66687aadf862bd77"
|
|
90
|
-
},
|
|
91
|
-
{
|
|
92
|
-
"name": "fingerprint of 32 0xff bytes",
|
|
93
|
-
"input_hex": "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
|
|
94
|
-
"expect_kid": "af9613760f72635f"
|
|
95
|
-
},
|
|
96
|
-
{
|
|
97
|
-
"name": "fingerprint of UTF-8 string 'kxco'",
|
|
98
|
-
"input_utf8": "kxco",
|
|
99
|
-
"expect_kid": "00932b8f81e33fa2"
|
|
100
|
-
},
|
|
101
|
-
{
|
|
102
|
-
"name": "fingerprint of ML-DSA-65 platform pubkey from zero master",
|
|
103
|
-
"input_source": "mlDsa.keypairFromMaster(0x00*32, \"platform-v1\").publicKey",
|
|
104
|
-
"expect_kid": "dc29825b2f9cc9f1"
|
|
105
|
-
}
|
|
106
|
-
],
|
|
107
|
-
"webhook_envelope": [
|
|
108
|
-
{
|
|
109
|
-
"name": "envelope concatenation",
|
|
110
|
-
"timestamp": "1748000000",
|
|
111
|
-
"body_utf8": "{\"event\":\"payment.settled\",\"amount\":1000}",
|
|
112
|
-
"expect_envelope_hex": "313734383030303030302e7b226576656e74223a227061796d656e742e736574746c6564222c22616d6f756e74223a313030307d"
|
|
113
|
-
}
|
|
114
|
-
],
|
|
115
|
-
"webhook_hmac": [
|
|
116
|
-
{
|
|
117
|
-
"name": "HMAC-SHA-256 over envelope",
|
|
118
|
-
"secret_utf8": "shared-secret-bytes",
|
|
119
|
-
"timestamp": "1748000000",
|
|
120
|
-
"body_utf8": "{\"event\":\"payment.settled\",\"amount\":1000}",
|
|
121
|
-
"expect_hmac_hex": "63ed31450be6c8771c2eb9a92888aecd814431360ac06aaac95faa94cf5bd665"
|
|
122
|
-
}
|
|
123
|
-
],
|
|
124
|
-
"webhook_hybrid_roundtrip": [
|
|
125
|
-
{
|
|
126
|
-
"name": "sign + verify with platform keypair from zero master",
|
|
127
|
-
"master_hex": "0000000000000000000000000000000000000000000000000000000000000000",
|
|
128
|
-
"info": "platform-v1",
|
|
129
|
-
"timestamp": "1748000000",
|
|
130
|
-
"body_utf8": "{\"event\":\"payment.settled\",\"amount\":1000}",
|
|
131
|
-
"hmac_secret_utf8": "shared-secret-bytes",
|
|
132
|
-
"expect_hmac_ok": true,
|
|
133
|
-
"expect_pq_ok": true,
|
|
134
|
-
"expect_timestamp_ok_when_fresh": true
|
|
135
|
-
}
|
|
136
|
-
]
|
|
137
|
-
}
|