kxco-post-quantum 1.0.2 → 1.0.3
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 +78 -63
- package/README.md +40 -5
- package/SECURITY.md +22 -89
- package/package.json +33 -11
- package/src/derive.d.ts +20 -0
- package/src/index.d.ts +7 -0
- package/src/kid.d.ts +21 -0
- package/src/ml-dsa.d.ts +45 -0
- package/src/ml-kem.d.ts +49 -0
- package/src/webhook.d.ts +119 -0
- package/AUDIT.md +0 -110
- package/test/run-vectors.js +0 -155
- package/test/vectors.json +0 -137
package/CHANGELOG.md
CHANGED
|
@@ -1,82 +1,97 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
All notable changes to
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [Unreleased]
|
|
9
|
+
|
|
10
|
+
## [1.0.3] — 2026-05-21
|
|
11
|
+
|
|
12
|
+
First release ships with SLSA Level 2 provenance attestation tied to a
|
|
13
|
+
public GitHub Actions workflow run. No cryptographic surface changes
|
|
14
|
+
vs `1.0.2` — every diff is metadata, types, CI, and hygiene.
|
|
15
|
+
|
|
16
|
+
### Added
|
|
17
|
+
- SLSA Level 2 provenance on every published release via GitHub Actions OIDC
|
|
18
|
+
(`publishConfig.provenance: true`)
|
|
19
|
+
- `.github/workflows/publish.yml` triggered by `v*` tags — runs tests then
|
|
20
|
+
`npm publish --provenance --access public`
|
|
21
|
+
- `.github/workflows/ci.yml` matrix over Node 18 / 20 / 22 on every push and PR
|
|
22
|
+
- Hand-written TypeScript declarations (`.d.ts`) for all six modules; wired
|
|
23
|
+
into `exports[*].types` so TypeScript consumers get full typings without
|
|
24
|
+
any build step
|
|
25
|
+
- `.github/dependabot.yml` — weekly npm + github-actions ecosystem checks
|
|
26
|
+
- `sideEffects: false` for tree-shaking
|
|
27
|
+
- `funding` field in `package.json`
|
|
28
|
+
- Top-level `"types"` field in `package.json` pointing at `./src/index.d.ts`
|
|
29
|
+
|
|
30
|
+
### Changed
|
|
31
|
+
- `package.json` `files` allowlist tightened to `["src", "README.md", "LICENSE",
|
|
32
|
+
"SECURITY.md", "CHANGELOG.md"]` — locks down what ships to npm
|
|
33
|
+
- `package.json` `exports` now declares per-subpath `types` + `import` keys
|
|
34
|
+
- `SECURITY.md` rewritten in the standard short-form template with explicit
|
|
35
|
+
in-scope / out-of-scope split delegating primitive bugs upstream to
|
|
36
|
+
`@noble/post-quantum`
|
|
37
|
+
- All third-party actions in workflows pinned by 40-char commit SHA, never
|
|
38
|
+
floating tags
|
|
39
|
+
- README badge row trimmed to four (`npm`, `license`, `Socket`, `production-live`)
|
|
40
|
+
and a 60-second live-verify quickstart added under the title
|
|
41
|
+
|
|
42
|
+
### Security
|
|
43
|
+
- No cryptographic code changed in this release — every change is metadata,
|
|
44
|
+
types, CI, and documentation. Production behaviour is bit-for-bit identical
|
|
45
|
+
to `1.0.2`.
|
|
4
46
|
|
|
5
47
|
## [1.0.2] — 2026-05-21
|
|
6
48
|
|
|
7
49
|
### Changed
|
|
8
|
-
- Repository URL on the npm package now points to
|
|
50
|
+
- Repository URL on the npm package metadata now points to
|
|
51
|
+
`github.com/JackKXCO/kxco-post-quantum`. No code change.
|
|
9
52
|
|
|
10
53
|
## [1.0.1] — 2026-05-21
|
|
11
54
|
|
|
12
|
-
Substance additions to the v1.0 release. No API changes. Same code on the cryptographic path; auditability and reproducibility infrastructure expanded.
|
|
13
|
-
|
|
14
55
|
### Added
|
|
15
|
-
- `AUDIT.md` — self-attested audit posture
|
|
16
|
-
|
|
17
|
-
- `test/
|
|
18
|
-
|
|
19
|
-
- `
|
|
20
|
-
- `npm
|
|
21
|
-
-
|
|
56
|
+
- `AUDIT.md` — self-attested audit posture with roadmap (external audit
|
|
57
|
+
Q3 2026, public bug bounty Q4 2026, FIPS 140-3 CMVP application 2027)
|
|
58
|
+
- `test/vectors.json` — 29 deterministic test vectors pinning every primitive
|
|
59
|
+
output bit-for-bit
|
|
60
|
+
- `test/run-vectors.js` — runner anyone can use to verify reproducibility
|
|
61
|
+
- `npm test` runs both the functional tests and vector verification
|
|
62
|
+
- `npm run test:vectors` for vector check only
|
|
22
63
|
|
|
23
64
|
### Changed
|
|
24
|
-
- `SECURITY.md`
|
|
25
|
-
-
|
|
65
|
+
- `SECURITY.md` sharpened with explicit threat model and pinned upstream
|
|
66
|
+
`@noble/post-quantum@0.2.1` integrity hash
|
|
67
|
+
- `README.md` "Used in production at" section with file refs to chain.kxco.ai
|
|
26
68
|
|
|
27
|
-
|
|
28
|
-
No API surface changes. Same exports, same signatures, same behaviour. The diff is documentation, vectors, and infrastructure that lets reviewers verify the library without trusting the publisher.
|
|
69
|
+
No API changes from `1.0.0`.
|
|
29
70
|
|
|
30
71
|
## [1.0.0] — 2026-05-21
|
|
31
72
|
|
|
32
|
-
First stable release.
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
| Export | Signature | Notes |
|
|
48
|
-
|---|---|---|
|
|
49
|
-
| `mlDsa.keypairFromMaster(master, info='ml-dsa-65-v1')` | → `{ publicKey: Buffer, secretKey: Buffer }` | Deterministic. Stable across versions. |
|
|
50
|
-
| `mlDsa.sign(secretKey, message)` | → hex string (6618 chars) | `message` accepts Buffer or string |
|
|
51
|
-
| `mlDsa.verify(publicKey, message, sigHex)` | → boolean | Catches exceptions, returns false |
|
|
52
|
-
| `mlDsa.ml_dsa65` | re-export | Raw `@noble/post-quantum` primitive |
|
|
53
|
-
| `mlKem.keypairFromMaster(master, info='ml-kem-768-v1')` | → `{ publicKey, secretKey }` | Deterministic |
|
|
54
|
-
| `mlKem.encapsulate(publicKey)` | → `{ ciphertext, sharedSecret, cipherText }` | `cipherText` (camelCase) alias for noble compatibility |
|
|
55
|
-
| `mlKem.decapsulate(ciphertext, secretKey)` | → Buffer (32 bytes) | |
|
|
56
|
-
| `mlKem.ml_kem768` | re-export | Raw primitive |
|
|
57
|
-
| `deriveSeed(master, info, length)` | → Buffer | HKDF-SHA-512 with empty salt; requires `master.length ≥ 16` |
|
|
58
|
-
| `fingerprint(publicKey)` | → 16-hex string | First 8 bytes of SHA-256(pubkey) |
|
|
59
|
-
| `kidEquals(a, b)` | → boolean | Constant-time string compare |
|
|
60
|
-
| `webhook.envelope(timestamp, rawBody)` | → Buffer | `timestamp + "." + rawBody` |
|
|
61
|
-
| `webhook.hmacHex(secret, timestamp, rawBody)` | → hex string | HMAC-SHA-256 over envelope |
|
|
62
|
-
| `webhook.verifyHmac(secret, timestamp, rawBody, sigHeader)` | → boolean | Constant-time. Accepts `sha256=` prefix. |
|
|
63
|
-
| `webhook.pqSign(secretKey, timestamp, rawBody)` | → `ml-dsa-65=<hex>` | Ready-to-send header value |
|
|
64
|
-
| `webhook.verifyPq(publicKey, timestamp, rawBody, sigHeader)` | → boolean | Accepts `ml-dsa-65=` prefix |
|
|
65
|
-
| `webhook.signDelivery({ rawBody, hmacSecret, pqSecretKey, pqKid, event?, deliveryId? })` | → HTTP header map | Full sender helper |
|
|
66
|
-
| `webhook.verifyDelivery({ headers, rawBody, hmacSecret?, pqPublicKey?, pinnedKid?, windowSeconds=300 })` | → `{ hmacOk, pqOk, timestampOk, kidOk }` | Receiver helper. Either signature can be omitted to skip that check. |
|
|
67
|
-
|
|
68
|
-
### What "committed" means
|
|
69
|
-
We will not break any of the above signatures in a 1.x release. We will not change algorithm choices in a 1.x release. We will not change the envelope format `${timestamp}.${rawBody}` in a 1.x release. We may add new exports; existing ones won't move.
|
|
70
|
-
|
|
71
|
-
### Verified at release
|
|
72
|
-
- 9/9 functional tests pass (sign/verify, encapsulate/decapsulate, hybrid delivery, replay rejection, tamper rejection)
|
|
73
|
-
- 29/29 test vectors pass
|
|
74
|
-
- Underlying primitives: `@noble/post-quantum@0.2.1` (audited by Cure53, 2024)
|
|
75
|
-
- Node.js 18+ ESM-only
|
|
76
|
-
|
|
77
|
-
### Initial release notes
|
|
78
|
-
ESM-only. Node.js 18+. FIPS-aligned (implements FIPS 203/204 algorithms). Not CMVP-validated as a module. TLS guidance: use OpenSSL 3.5+ for `X25519MLKEM768` hybrid at the edge — this package does not handle TLS.
|
|
73
|
+
First stable release. Committed public API surface:
|
|
74
|
+
|
|
75
|
+
- `mlDsa.keypairFromMaster(master, info?)`, `mlDsa.sign`, `mlDsa.verify`
|
|
76
|
+
- `mlKem.keypairFromMaster(master, info?)`, `mlKem.encapsulate`, `mlKem.decapsulate`
|
|
77
|
+
- `deriveSeed(master, info, length)`
|
|
78
|
+
- `fingerprint(publicKey)`, `kidEquals(a, b)`
|
|
79
|
+
- `webhook.envelope`, `webhook.hmacHex`, `webhook.verifyHmac`,
|
|
80
|
+
`webhook.pqSign`, `webhook.verifyPq`, `webhook.signDelivery`,
|
|
81
|
+
`webhook.verifyDelivery`
|
|
82
|
+
|
|
83
|
+
Verified at release: 9/9 functional tests + 29/29 vector checks pass.
|
|
84
|
+
|
|
85
|
+
Underlying primitives via `@noble/post-quantum@^0.2.1` (audited by Cure53, 2024).
|
|
86
|
+
ESM-only. Node.js 18+.
|
|
79
87
|
|
|
80
88
|
## [0.1.0] — 2026-05-21
|
|
81
89
|
|
|
82
|
-
Initial pre-release.
|
|
90
|
+
Initial pre-release.
|
|
91
|
+
|
|
92
|
+
[Unreleased]: https://github.com/JackKXCO/kxco-post-quantum/compare/v1.0.3...HEAD
|
|
93
|
+
[1.0.3]: https://github.com/JackKXCO/kxco-post-quantum/compare/v1.0.2...v1.0.3
|
|
94
|
+
[1.0.2]: https://github.com/JackKXCO/kxco-post-quantum/compare/v1.0.1...v1.0.2
|
|
95
|
+
[1.0.1]: https://github.com/JackKXCO/kxco-post-quantum/compare/v1.0.0...v1.0.1
|
|
96
|
+
[1.0.0]: https://github.com/JackKXCO/kxco-post-quantum/compare/v0.1.0...v1.0.0
|
|
97
|
+
[0.1.0]: https://github.com/JackKXCO/kxco-post-quantum/releases/tag/v0.1.0
|
package/README.md
CHANGED
|
@@ -1,14 +1,49 @@
|
|
|
1
|
-
#
|
|
1
|
+
# kxco-post-quantum
|
|
2
2
|
|
|
3
3
|
**Production-tested post-quantum cryptography patterns.** Deterministic key derivation, hybrid webhook signing, and kid fingerprinting — the integration patterns KXCO uses in production across KnightsVault, KXCO Bank, KnightsBot, The Exchequer, and Armature L1.
|
|
4
4
|
|
|
5
|
-
[](https://github.com/JackKXCO/kxco-post-quantum/actions/workflows/ci.yml)
|
|
6
|
+
[](https://socket.dev/npm/package/kxco-post-quantum)
|
|
7
|
+
[](https://www.npmjs.com/package/kxco-post-quantum)
|
|
8
|
+
[](https://chain.kxco.ai/wallet/verify)
|
|
9
9
|
|
|
10
10
|
---
|
|
11
11
|
|
|
12
|
+
## 60-second quickstart
|
|
13
|
+
|
|
14
|
+
Install the package, fetch a freshly signed test vector from the live KXCO platform, post it back to verify — **200 OK**. The same code path every production webhook runs.
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
# 1. Install
|
|
18
|
+
npm install kxco-post-quantum
|
|
19
|
+
|
|
20
|
+
# 2. Fetch a freshly signed test vector from the live KXCO platform
|
|
21
|
+
curl -s https://chain.kxco.ai/wallet/api/verify-demo > vector.json
|
|
22
|
+
|
|
23
|
+
# 3. Post it back — the server verifies HMAC + ML-DSA-65 against the live key
|
|
24
|
+
node --input-type=module -e '
|
|
25
|
+
import fs from "node:fs"
|
|
26
|
+
const v = JSON.parse(fs.readFileSync("vector.json", "utf8"))
|
|
27
|
+
const h = v.headers
|
|
28
|
+
const res = await fetch("https://chain.kxco.ai/wallet/api/verify-demo", {
|
|
29
|
+
method: "POST",
|
|
30
|
+
headers: { "Content-Type": "application/json" },
|
|
31
|
+
body: JSON.stringify({
|
|
32
|
+
timestamp: h["X-KXCO-Timestamp"],
|
|
33
|
+
body: v.body,
|
|
34
|
+
hmacSecret: v.hmacSecret,
|
|
35
|
+
hmacSig: h["X-KXCO-Signature"],
|
|
36
|
+
pqSig: h["X-KXCO-PQ-Signature"].replace(/^ml-dsa-65=/, ""),
|
|
37
|
+
pqKid: h["X-KXCO-PQ-Kid"],
|
|
38
|
+
}),
|
|
39
|
+
})
|
|
40
|
+
console.log(res.status, await res.json())
|
|
41
|
+
'
|
|
42
|
+
# → 200 { kidMatch: true, hmac: { ok: true }, pq: { ok: true }, ... }
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
The platform signs every fetch fresh — no cached fixtures, no replay. `verify-demo` exposes the HMAC secret for the demo only; in production the HMAC secret never leaves the receiving server.
|
|
46
|
+
|
|
12
47
|
## Used in production at
|
|
13
48
|
|
|
14
49
|
This is not a sample library. It runs in production at KXCO across multiple products. You can verify this yourself without any cooperation from us:
|
package/SECURITY.md
CHANGED
|
@@ -1,94 +1,27 @@
|
|
|
1
1
|
# Security Policy
|
|
2
2
|
|
|
3
3
|
## Reporting a vulnerability
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
- Acknowledgement within 48 hours
|
|
8
|
-
- Initial triage of critical findings within 5 business days
|
|
9
|
-
- Coordinated disclosure: we patch first, credit the reporter, publish a CVE / GHSA
|
|
4
|
+
Email **security@kxco.ai**. Do not open public issues for security reports.
|
|
5
|
+
PGP key available on request. We respond within 48 hours and credit reporters
|
|
6
|
+
in `CHANGELOG.md` unless they request otherwise.
|
|
10
7
|
|
|
11
8
|
## Scope
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
| SHA-256 (for kid) | [FIPS 180-4](https://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.180-4.pdf) | Truncated to 8 bytes |
|
|
32
|
-
|
|
33
|
-
### Pinned upstream
|
|
34
|
-
|
|
35
|
-
This package is pinned to:
|
|
36
|
-
|
|
37
|
-
```
|
|
38
|
-
@noble/post-quantum@0.2.1
|
|
39
|
-
integrity: sha512-ImgfMp9notXSEocz464o1AefYfFWEkkszKMGO+ZiTn73yIBFeNyEHKQUMS+SheJwSNymldSts6YyVcQDjcnVVg==
|
|
40
|
-
```
|
|
41
|
-
|
|
42
|
-
`@noble/post-quantum` was independently audited by **Cure53** in 2024. Audit report: https://github.com/paulmillr/noble-post-quantum#security
|
|
43
|
-
|
|
44
|
-
### FIPS posture (be honest)
|
|
45
|
-
|
|
46
|
-
- ✅ Algorithms implemented are NIST FIPS-standardised (203, 204).
|
|
47
|
-
- ❌ This module is **not** FIPS 140-3 CMVP-validated. There is no FIPS module certificate.
|
|
48
|
-
- ⚠️ "FIPS-aligned" is the correct way to describe this package; "FIPS-certified" is not.
|
|
49
|
-
|
|
50
|
-
For deployments where FIPS 140-3 module validation is mandatory, run cryptographic operations inside a validated HSM (AWS CloudHSM, Thales Luna, YubiHSM 2, etc.) and use this library only for envelope construction, kid generation, and verification of incoming signatures.
|
|
51
|
-
|
|
52
|
-
## Threat model
|
|
53
|
-
|
|
54
|
-
What this library defends against and what it does not.
|
|
55
|
-
|
|
56
|
-
### In scope
|
|
57
|
-
|
|
58
|
-
- ✅ **Quantum adversary against signature non-repudiation** — ML-DSA-65 is the FIPS 204 standard for this.
|
|
59
|
-
- ✅ **Quantum adversary against confidentiality of new sessions** — ML-KEM-768 (FIPS 203) for KEM; combine with AES-256-GCM (or equivalent AEAD) for the symmetric envelope.
|
|
60
|
-
- ✅ **Forgery of webhook deliveries** — hybrid HMAC + ML-DSA defends against both classical and quantum forgery. ML-DSA adds non-repudiation HMAC cannot give.
|
|
61
|
-
- ✅ **Replay attacks** — `webhook.verifyDelivery` enforces a default 5-minute timestamp window.
|
|
62
|
-
- ✅ **Tampering with body** — both signatures cover the exact `timestamp + "." + raw_body` envelope.
|
|
63
|
-
- ✅ **Wrong-key acceptance** — `pinnedKid` check fails fast before any signature verification.
|
|
64
|
-
|
|
65
|
-
### Out of scope (caller's responsibility)
|
|
66
|
-
|
|
67
|
-
- ❌ **Master secret storage**. Keep `KXCO_KEY_MASTER` (or equivalent) in environment variables, a KMS, or an HSM. This library does not store secrets.
|
|
68
|
-
- ❌ **Key rotation procedures**. Rotating the master changes every derived keypair; downstream consumers must refresh their pinned kid.
|
|
69
|
-
- ❌ **TLS / transport encryption**. Use hybrid `X25519MLKEM768` at the public edge — outside this library's scope.
|
|
70
|
-
- ❌ **Receiving raw bodies byte-for-byte**. If you re-stringify JSON before verifying, you will mangle the signature input. Use Express `express.raw({ type: 'application/json' })` or equivalent.
|
|
71
|
-
- ❌ **Domain separation hygiene**. If you reuse the same `info` string for signing and encryption keypairs, you create a cross-protocol attack surface. Use distinct `info` per purpose.
|
|
72
|
-
- ❌ **Constant-time comparison of secrets outside this library**. We provide `kidEquals` and `verifyHmac`; if you compare HMAC tags yourself with `===`, that's on you.
|
|
73
|
-
|
|
74
|
-
### Acknowledged limitations
|
|
75
|
-
|
|
76
|
-
- **Default replay window is 300 seconds**. Configurable via `windowSeconds`. Tighten for high-security paths.
|
|
77
|
-
- **`fingerprint` uses 8 bytes (16 hex)**. SHA-256 truncation. Collision-resistant for the scale of keys in use today (one platform key, occasionally rotated). If you anticipate billions of distinct keys, extend to a longer prefix.
|
|
78
|
-
- **Side-channel posture**. `@noble/post-quantum` targets constant-time execution; this wrapper adds constant-time string comparisons. Higher-assurance deployments should run operations inside a hardware boundary.
|
|
79
|
-
- **`signDelivery` calls `Date.now()`**. If your system clock is wrong, verifiers will reject your deliveries. Run NTP.
|
|
80
|
-
|
|
81
|
-
## Reproducibility
|
|
82
|
-
|
|
83
|
-
Test vectors in `test/vectors.json` pin the library's outputs bit-for-bit. Anyone can run `npm test` to verify the library produces identical outputs to the recorded vectors. If a future release changes any output, the run-vectors check fails — this is a deliberate tripwire on cryptographic regressions.
|
|
84
|
-
|
|
85
|
-
## Disclosure history
|
|
86
|
-
|
|
87
|
-
None yet. This is a v1 release. Vulnerabilities will be disclosed here as GHSA advisories with CVE numbers where applicable.
|
|
88
|
-
|
|
89
|
-
## Versioning and support
|
|
90
|
-
|
|
91
|
-
- We support the latest minor release on the latest major (currently 1.x).
|
|
92
|
-
- Critical security fixes are backported to the previous minor for 6 months.
|
|
93
|
-
- Major version bumps (2.x → 3.x) only occur when the cryptographic primitives or signed envelope format changes; minor bumps add features without breaking the API.
|
|
94
|
-
- `npm install kxco-post-quantum` always pulls the supported version.
|
|
9
|
+
In scope:
|
|
10
|
+
- Cryptographic correctness of the wrappers in this package
|
|
11
|
+
- Constant-time guarantees on signature/HMAC comparison
|
|
12
|
+
- Replay-window enforcement in `webhook.verify`
|
|
13
|
+
- HKDF domain separation in `derive`
|
|
14
|
+
- Kid fingerprint collision behaviour
|
|
15
|
+
|
|
16
|
+
Out of scope (report upstream to https://github.com/paulmillr/noble-post-quantum):
|
|
17
|
+
- Bugs in the underlying ML-DSA-65, ML-KEM-768, or HKDF primitives
|
|
18
|
+
|
|
19
|
+
## Algorithms used
|
|
20
|
+
- ML-DSA-65 — NIST FIPS 204 (lattice signatures)
|
|
21
|
+
- ML-KEM-768 — NIST FIPS 203 (key encapsulation)
|
|
22
|
+
- HMAC-SHA-256
|
|
23
|
+
- HKDF-SHA-512 (RFC 5869)
|
|
24
|
+
|
|
25
|
+
## Disclosure
|
|
26
|
+
We follow coordinated disclosure with a 90-day default window.
|
|
27
|
+
For actively-exploited issues we ship a patch release within 48 hours.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kxco-post-quantum",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.3",
|
|
4
4
|
"description": "Production-tested post-quantum cryptography patterns: deterministic key derivation, hybrid webhook signing, and kid fingerprinting. Built on @noble/post-quantum. Used in production at KXCO.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"post-quantum",
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
"license": "MIT",
|
|
20
20
|
"author": "KXCO by Knightsbridge <hello@kxco.ai>",
|
|
21
21
|
"homepage": "https://kxco.ai",
|
|
22
|
+
"funding": "https://kxco.ai",
|
|
22
23
|
"repository": {
|
|
23
24
|
"type": "git",
|
|
24
25
|
"url": "https://github.com/JackKXCO/kxco-post-quantum.git"
|
|
@@ -27,23 +28,40 @@
|
|
|
27
28
|
"url": "https://github.com/JackKXCO/kxco-post-quantum/issues"
|
|
28
29
|
},
|
|
29
30
|
"type": "module",
|
|
31
|
+
"sideEffects": false,
|
|
30
32
|
"main": "./src/index.js",
|
|
33
|
+
"types": "./src/index.d.ts",
|
|
31
34
|
"exports": {
|
|
32
|
-
".":
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
"./
|
|
37
|
-
|
|
35
|
+
".": {
|
|
36
|
+
"types": "./src/index.d.ts",
|
|
37
|
+
"import": "./src/index.js"
|
|
38
|
+
},
|
|
39
|
+
"./ml-dsa": {
|
|
40
|
+
"types": "./src/ml-dsa.d.ts",
|
|
41
|
+
"import": "./src/ml-dsa.js"
|
|
42
|
+
},
|
|
43
|
+
"./ml-kem": {
|
|
44
|
+
"types": "./src/ml-kem.d.ts",
|
|
45
|
+
"import": "./src/ml-kem.js"
|
|
46
|
+
},
|
|
47
|
+
"./derive": {
|
|
48
|
+
"types": "./src/derive.d.ts",
|
|
49
|
+
"import": "./src/derive.js"
|
|
50
|
+
},
|
|
51
|
+
"./webhook": {
|
|
52
|
+
"types": "./src/webhook.d.ts",
|
|
53
|
+
"import": "./src/webhook.js"
|
|
54
|
+
},
|
|
55
|
+
"./kid": {
|
|
56
|
+
"types": "./src/kid.d.ts",
|
|
57
|
+
"import": "./src/kid.js"
|
|
58
|
+
}
|
|
38
59
|
},
|
|
39
60
|
"files": [
|
|
40
|
-
"src
|
|
41
|
-
"test/vectors.json",
|
|
42
|
-
"test/run-vectors.js",
|
|
61
|
+
"src",
|
|
43
62
|
"README.md",
|
|
44
63
|
"LICENSE",
|
|
45
64
|
"SECURITY.md",
|
|
46
|
-
"AUDIT.md",
|
|
47
65
|
"CHANGELOG.md"
|
|
48
66
|
],
|
|
49
67
|
"engines": {
|
|
@@ -56,5 +74,9 @@
|
|
|
56
74
|
"test": "node --test test/basic.test.js && node test/run-vectors.js",
|
|
57
75
|
"test:vectors": "node test/run-vectors.js",
|
|
58
76
|
"generate:vectors": "node test/generate-vectors.js > test/vectors.json"
|
|
77
|
+
},
|
|
78
|
+
"publishConfig": {
|
|
79
|
+
"provenance": true,
|
|
80
|
+
"access": "public"
|
|
59
81
|
}
|
|
60
82
|
}
|
package/src/derive.d.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/// <reference types="node" />
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Derive a deterministic seed from a master secret + an info string,
|
|
5
|
+
* using HKDF-SHA-512 with an empty salt.
|
|
6
|
+
*
|
|
7
|
+
* Same `master + info + length` always produces the same seed.
|
|
8
|
+
*
|
|
9
|
+
* @param master — high-entropy input keying material (≥16 bytes)
|
|
10
|
+
* @param info — domain separation tag (eg. 'kxco-platform-ml-dsa-65-v1')
|
|
11
|
+
* @param length — output seed length in bytes (32 for ML-DSA, 64 for ML-KEM)
|
|
12
|
+
*
|
|
13
|
+
* @throws {Error} if `master` is shorter than 16 bytes
|
|
14
|
+
* @throws {Error} if `info` is empty or not a string
|
|
15
|
+
*/
|
|
16
|
+
export function deriveSeed(
|
|
17
|
+
master: Buffer | Uint8Array,
|
|
18
|
+
info: string,
|
|
19
|
+
length: number,
|
|
20
|
+
): Buffer
|
package/src/index.d.ts
ADDED
package/src/kid.d.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/// <reference types="node" />
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Compute a 16-hex-character fingerprint of a public key:
|
|
5
|
+
* the first 8 bytes of `SHA-256(public_key)`, hex-encoded.
|
|
6
|
+
*
|
|
7
|
+
* Stable for the lifetime of the keypair. Used to identify which
|
|
8
|
+
* platform key signed an outbound delivery without including the
|
|
9
|
+
* full 1952-byte public key in every request.
|
|
10
|
+
*
|
|
11
|
+
* @param publicKey — raw bytes or hex string
|
|
12
|
+
*/
|
|
13
|
+
export function fingerprint(publicKey: Buffer | Uint8Array | string): string
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Constant-time comparison of two kid strings.
|
|
17
|
+
*
|
|
18
|
+
* Use this instead of `===` when comparing kids that may be
|
|
19
|
+
* influenced by untrusted input — eg. an `X-KXCO-PQ-Kid` header.
|
|
20
|
+
*/
|
|
21
|
+
export function kidEquals(a: string, b: string): boolean
|
package/src/ml-dsa.d.ts
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/// <reference types="node" />
|
|
2
|
+
|
|
3
|
+
export interface MlDsaKeypair {
|
|
4
|
+
/** 1952-byte public key */
|
|
5
|
+
publicKey: Buffer
|
|
6
|
+
/** 4032-byte secret key */
|
|
7
|
+
secretKey: Buffer
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Generate an ML-DSA-65 (NIST FIPS 204) keypair deterministically
|
|
12
|
+
* from a master + domain-separation info string.
|
|
13
|
+
*
|
|
14
|
+
* Same inputs always produce the same keypair — no state, no DB row.
|
|
15
|
+
*/
|
|
16
|
+
export function keypairFromMaster(
|
|
17
|
+
master: Buffer | Uint8Array,
|
|
18
|
+
info?: string,
|
|
19
|
+
): MlDsaKeypair
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Sign a message under an ML-DSA-65 secret key. Returns the signature
|
|
23
|
+
* as a hex string (3309 bytes = 6618 hex characters).
|
|
24
|
+
*/
|
|
25
|
+
export function sign(
|
|
26
|
+
secretKey: Buffer | Uint8Array,
|
|
27
|
+
message: Buffer | string,
|
|
28
|
+
): string
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Verify a hex-encoded ML-DSA-65 signature against a public key + message.
|
|
32
|
+
* Returns `false` on any error (invalid hex, wrong length, mismatch).
|
|
33
|
+
*/
|
|
34
|
+
export function verify(
|
|
35
|
+
publicKey: Buffer | Uint8Array,
|
|
36
|
+
message: Buffer | string,
|
|
37
|
+
sigHex: string,
|
|
38
|
+
): boolean
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Raw `@noble/post-quantum` ML-DSA-65 primitive, re-exported for callers
|
|
42
|
+
* who want the lower-level API. The wrapper functions above are
|
|
43
|
+
* recommended for production use.
|
|
44
|
+
*/
|
|
45
|
+
export const ml_dsa65: typeof import('@noble/post-quantum/ml-dsa').ml_dsa65
|
package/src/ml-kem.d.ts
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/// <reference types="node" />
|
|
2
|
+
|
|
3
|
+
export interface MlKemKeypair {
|
|
4
|
+
/** 1184-byte public key */
|
|
5
|
+
publicKey: Buffer
|
|
6
|
+
/** 2400-byte secret key */
|
|
7
|
+
secretKey: Buffer
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface MlKemEncapsulation {
|
|
11
|
+
/** 1088-byte KEM ciphertext to transmit to the recipient */
|
|
12
|
+
ciphertext: Buffer
|
|
13
|
+
/** Alias for `ciphertext` (mirrors @noble/post-quantum's camelCase) */
|
|
14
|
+
cipherText: Buffer
|
|
15
|
+
/** 32-byte shared secret to use as a symmetric AEAD key */
|
|
16
|
+
sharedSecret: Buffer
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Generate an ML-KEM-768 (NIST FIPS 203) keypair deterministically
|
|
21
|
+
* from a master + domain-separation info string.
|
|
22
|
+
*/
|
|
23
|
+
export function keypairFromMaster(
|
|
24
|
+
master: Buffer | Uint8Array,
|
|
25
|
+
info?: string,
|
|
26
|
+
): MlKemKeypair
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Encapsulate a shared secret to the recipient's ML-KEM-768 public key.
|
|
30
|
+
*
|
|
31
|
+
* The recipient calls `decapsulate(ciphertext, secretKey)` to recover
|
|
32
|
+
* the same shared secret. Use the shared secret as a symmetric AEAD
|
|
33
|
+
* key (eg. AES-256-GCM).
|
|
34
|
+
*/
|
|
35
|
+
export function encapsulate(publicKey: Buffer | Uint8Array): MlKemEncapsulation
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Decapsulate: recover the shared secret from a KEM ciphertext
|
|
39
|
+
* using the recipient's secret key.
|
|
40
|
+
*/
|
|
41
|
+
export function decapsulate(
|
|
42
|
+
ciphertext: Buffer | Uint8Array,
|
|
43
|
+
secretKey: Buffer | Uint8Array,
|
|
44
|
+
): Buffer
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Raw `@noble/post-quantum` ML-KEM-768 primitive, re-exported.
|
|
48
|
+
*/
|
|
49
|
+
export const ml_kem768: typeof import('@noble/post-quantum/ml-kem').ml_kem768
|
package/src/webhook.d.ts
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/// <reference types="node" />
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Build the canonical signed envelope: `timestamp + "." + raw_body`.
|
|
5
|
+
*
|
|
6
|
+
* Receivers MUST construct the envelope from the timestamp header and the
|
|
7
|
+
* RAW request body bytes as received. Re-serialising a parsed JSON object
|
|
8
|
+
* will not produce the same bytes and signature verification will fail.
|
|
9
|
+
*/
|
|
10
|
+
export function envelope(
|
|
11
|
+
timestamp: string | number,
|
|
12
|
+
rawBody: string | Buffer,
|
|
13
|
+
): Buffer
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Compute the hex HMAC-SHA-256 of the envelope using the shared secret.
|
|
17
|
+
* Returns the hex value WITHOUT the `sha256=` prefix.
|
|
18
|
+
*/
|
|
19
|
+
export function hmacHex(
|
|
20
|
+
secret: string | Buffer,
|
|
21
|
+
timestamp: string | number,
|
|
22
|
+
rawBody: string | Buffer,
|
|
23
|
+
): string
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Constant-time verify of the X-KXCO-Signature header.
|
|
27
|
+
* Accepts the header value with or without the `sha256=` prefix.
|
|
28
|
+
*/
|
|
29
|
+
export function verifyHmac(
|
|
30
|
+
secret: string | Buffer,
|
|
31
|
+
timestamp: string | number,
|
|
32
|
+
rawBody: string | Buffer,
|
|
33
|
+
sigHeader: string,
|
|
34
|
+
): boolean
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Produce the X-KXCO-PQ-Signature header value: the hex ML-DSA-65
|
|
38
|
+
* signature over the envelope, prefixed with `ml-dsa-65=`.
|
|
39
|
+
*/
|
|
40
|
+
export function pqSign(
|
|
41
|
+
secretKey: Buffer | Uint8Array,
|
|
42
|
+
timestamp: string | number,
|
|
43
|
+
rawBody: string | Buffer,
|
|
44
|
+
): string
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Verify a hex ML-DSA-65 signature header.
|
|
48
|
+
* Accepts the value with or without the `ml-dsa-65=` prefix.
|
|
49
|
+
*/
|
|
50
|
+
export function verifyPq(
|
|
51
|
+
publicKey: Buffer | Uint8Array,
|
|
52
|
+
timestamp: string | number,
|
|
53
|
+
rawBody: string | Buffer,
|
|
54
|
+
sigHeader: string,
|
|
55
|
+
): boolean
|
|
56
|
+
|
|
57
|
+
export interface SignDeliveryArgs {
|
|
58
|
+
/** The exact body bytes that will be transmitted */
|
|
59
|
+
rawBody: string | Buffer
|
|
60
|
+
/** Per-endpoint shared secret for HMAC */
|
|
61
|
+
hmacSecret: string | Buffer
|
|
62
|
+
/** Raw ML-DSA-65 secret key */
|
|
63
|
+
pqSecretKey: Buffer | Uint8Array
|
|
64
|
+
/** 16-hex kid fingerprint of the matching public key */
|
|
65
|
+
pqKid: string
|
|
66
|
+
/** Optional event name (eg. "payment.settled") */
|
|
67
|
+
event?: string
|
|
68
|
+
/** Optional delivery / idempotency identifier */
|
|
69
|
+
deliveryId?: string
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export interface SignDeliveryHeaders {
|
|
73
|
+
'Content-Type': 'application/json'
|
|
74
|
+
'X-KXCO-Timestamp': string
|
|
75
|
+
'X-KXCO-Signature': string // sha256=<hex>
|
|
76
|
+
'X-KXCO-PQ-Signature': string // ml-dsa-65=<hex>
|
|
77
|
+
'X-KXCO-PQ-Kid': string
|
|
78
|
+
'X-KXCO-Event'?: string
|
|
79
|
+
'X-KXCO-Delivery'?: string
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Sign a webhook delivery. Returns the full set of headers a sender
|
|
84
|
+
* should attach to the HTTP request.
|
|
85
|
+
*
|
|
86
|
+
* The caller is responsible for sending `rawBody` byte-for-byte
|
|
87
|
+
* unchanged. The receiver verifies against the bytes as transmitted.
|
|
88
|
+
*/
|
|
89
|
+
export function signDelivery(args: SignDeliveryArgs): SignDeliveryHeaders
|
|
90
|
+
|
|
91
|
+
export interface VerifyDeliveryArgs {
|
|
92
|
+
/** HTTP headers with LOWERCASE keys */
|
|
93
|
+
headers: Record<string, string | undefined>
|
|
94
|
+
/** The EXACT request body bytes as received */
|
|
95
|
+
rawBody: string | Buffer
|
|
96
|
+
/** Optional: enable HMAC verification by providing the shared secret */
|
|
97
|
+
hmacSecret?: string | Buffer
|
|
98
|
+
/** Optional: enable PQ verification by providing the platform public key */
|
|
99
|
+
pqPublicKey?: Buffer | Uint8Array
|
|
100
|
+
/** Required when `pqPublicKey` is provided */
|
|
101
|
+
pinnedKid?: string
|
|
102
|
+
/** Replay-window in seconds. Default 300 (5 minutes). */
|
|
103
|
+
windowSeconds?: number
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export interface VerifyDeliveryResult {
|
|
107
|
+
hmacOk: boolean
|
|
108
|
+
pqOk: boolean
|
|
109
|
+
timestampOk: boolean
|
|
110
|
+
kidOk: boolean
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Verify a webhook delivery on the receiving side.
|
|
115
|
+
*
|
|
116
|
+
* Returns a breakdown of which predicates passed. A delivery is
|
|
117
|
+
* acceptable when `(hmacOk || pqOk) && timestampOk && kidOk`.
|
|
118
|
+
*/
|
|
119
|
+
export function verifyDelivery(args: VerifyDeliveryArgs): VerifyDeliveryResult
|
package/AUDIT.md
DELETED
|
@@ -1,110 +0,0 @@
|
|
|
1
|
-
# Audit Posture
|
|
2
|
-
|
|
3
|
-
**Status as of v1.0.1 release (2026-05-21).** Self-attested. No third-party audit of this wrapper library has been performed yet. This document exists to make our posture **legible to reviewers** so the right questions get asked of the right party.
|
|
4
|
-
|
|
5
|
-
If you are doing institutional due diligence, read this end-to-end before the README.
|
|
6
|
-
|
|
7
|
-
---
|
|
8
|
-
|
|
9
|
-
## 1. What has been audited (upstream)
|
|
10
|
-
|
|
11
|
-
**`@noble/post-quantum@0.2.1`** — the underlying NIST primitives we wrap — has been independently audited.
|
|
12
|
-
|
|
13
|
-
| Auditor | Year | Scope | Report |
|
|
14
|
-
|---|---|---|---|
|
|
15
|
-
| **Cure53** | 2024 | `@noble/post-quantum` cryptographic primitives | https://github.com/paulmillr/noble-post-quantum#security |
|
|
16
|
-
|
|
17
|
-
This audit covers the actual cryptographic operations: ML-DSA-65 sign/verify, ML-KEM-768 keygen/encapsulate/decapsulate, the constant-time properties, the test vector compliance with NIST's reference outputs.
|
|
18
|
-
|
|
19
|
-
When you `npm install kxco-post-quantum`, the audited code is what runs the math. This wrapper does not reimplement the primitives.
|
|
20
|
-
|
|
21
|
-
The exact upstream we pin:
|
|
22
|
-
|
|
23
|
-
```
|
|
24
|
-
@noble/post-quantum@0.2.1
|
|
25
|
-
integrity: sha512-ImgfMp9notXSEocz464o1AefYfFWEkkszKMGO+ZiTn73yIBFeNyEHKQUMS+SheJwSNymldSts6YyVcQDjcnVVg==
|
|
26
|
-
```
|
|
27
|
-
|
|
28
|
-
## 2. What has NOT been audited (this wrapper)
|
|
29
|
-
|
|
30
|
-
The integration patterns in this package — `pqSigner` derivation, kid fingerprinting, webhook envelope construction, hybrid HMAC + ML-DSA signing, timestamp replay enforcement — have not been independently audited.
|
|
31
|
-
|
|
32
|
-
What we have done:
|
|
33
|
-
|
|
34
|
-
- **Internal review** by the KXCO engineering team and KXCO Cybersecurity (lead: Sean O'Coiligh, 30+ years cybersecurity, formerly led Offensive Cyber at the DTCC Cyber Threat Fusion Center)
|
|
35
|
-
- **Reproducible test vectors** in `test/vectors.json` covering every primitive — anyone running `npm test` gets the same outputs the maintainers see
|
|
36
|
-
- **Production deployment** across the KXCO platform (KnightsVault, KXCO Bank, KnightsBot, The Exchequer, Armature L1) since November 2025
|
|
37
|
-
- **Public verifiable proof** of the signing identity at https://chain.kxco.ai/wallet/api/.well-known/kxco-pq-pubkey — anyone can fetch the kid, install this library, and verify signatures from the production fleet
|
|
38
|
-
|
|
39
|
-
What we have **not** done:
|
|
40
|
-
|
|
41
|
-
- ❌ Engaged a third-party auditor for this wrapper
|
|
42
|
-
- ❌ Held a public security review window with bug bounty
|
|
43
|
-
- ❌ Obtained CMVP FIPS 140-3 module certification
|
|
44
|
-
- ❌ Submitted to ENISA / NCSC / BSI evaluation schemes
|
|
45
|
-
|
|
46
|
-
## 3. Audit roadmap
|
|
47
|
-
|
|
48
|
-
| Milestone | Target | Owner |
|
|
49
|
-
|---|---|---|
|
|
50
|
-
| Engage external auditor for wrapper integration patterns | Q3 2026 | KXCO Engineering |
|
|
51
|
-
| Public bug bounty programme | Q4 2026 | KXCO Security |
|
|
52
|
-
| Apply for FIPS 140-3 CMVP validation of a cryptographic module deployment using this library + an HSM | 2027 | KXCO Compliance |
|
|
53
|
-
| NIST PQC Workshop presentation (production lessons) | When workshop opens for 2026/27 | Shayne Heffernan + Sean O'Coiligh |
|
|
54
|
-
|
|
55
|
-
The exact dates depend on engineering and budget capacity. The order is committed.
|
|
56
|
-
|
|
57
|
-
## 4. Reproducibility checks (run these yourself)
|
|
58
|
-
|
|
59
|
-
You do not have to trust us. Run these to verify:
|
|
60
|
-
|
|
61
|
-
```bash
|
|
62
|
-
# Clone and install
|
|
63
|
-
git clone https://github.com/JackKXCO/kxco-post-quantum
|
|
64
|
-
npm install
|
|
65
|
-
|
|
66
|
-
# Run the full test suite — primitives + vectors
|
|
67
|
-
npm test
|
|
68
|
-
|
|
69
|
-
# Run vector verification only
|
|
70
|
-
npm run test:vectors
|
|
71
|
-
|
|
72
|
-
# Fetch the live production platform key and verify offline
|
|
73
|
-
curl https://chain.kxco.ai/wallet/api/.well-known/kxco-pq-pubkey
|
|
74
|
-
```
|
|
75
|
-
|
|
76
|
-
Expected: `npm test` reports `✓ All 29 checks pass — library output matches pinned vectors bit-for-bit.`
|
|
77
|
-
|
|
78
|
-
## 5. Threat model summary
|
|
79
|
-
|
|
80
|
-
See [SECURITY.md](./SECURITY.md) for the full threat model. In short:
|
|
81
|
-
|
|
82
|
-
- **In scope:** quantum signature non-repudiation, quantum-safe KEM, webhook forgery resistance, replay rejection, body tamper detection, wrong-key rejection.
|
|
83
|
-
- **Out of scope:** master secret storage (use KMS/HSM), TLS termination (use OpenSSL 3.5+), receiving raw bodies byte-for-byte (use `express.raw` or equivalent), key rotation procedures (caller's responsibility).
|
|
84
|
-
|
|
85
|
-
## 6. Bug-finding signals
|
|
86
|
-
|
|
87
|
-
If you are evaluating this library, look at:
|
|
88
|
-
|
|
89
|
-
- **Test coverage:** 9 functional tests + 29 vector checks = 38 distinct assertions covering every export
|
|
90
|
-
- **Code size:** ~280 source lines across 6 modules — small enough to review end-to-end in an afternoon
|
|
91
|
-
- **Dependency surface:** one runtime dependency (`@noble/post-quantum`), itself audited
|
|
92
|
-
- **Determinism:** every output is reproducible from inputs — no hidden state, no globals beyond a lazy cache, no network
|
|
93
|
-
- **API stability:** v1.0 commits to the public surface listed in CHANGELOG.md
|
|
94
|
-
|
|
95
|
-
## 7. Reviewer checklist
|
|
96
|
-
|
|
97
|
-
For institutional reviewers, the smallest version of "did they actually do the work":
|
|
98
|
-
|
|
99
|
-
- [ ] `npm view kxco-post-quantum dist.signatures` returns a signed package
|
|
100
|
-
- [ ] `npm test` passes after fresh clone + install
|
|
101
|
-
- [ ] `npm run test:vectors` matches the pinned vectors
|
|
102
|
-
- [ ] `curl https://chain.kxco.ai/wallet/api/.well-known/kxco-pq-pubkey` returns a valid ML-DSA-65 public key
|
|
103
|
-
- [ ] The kid (`kid` field above) matches `fingerprint()` of the returned `publicKey` field
|
|
104
|
-
- [ ] An outbound webhook from `chain.kxco.ai` verifies with `webhook.verifyDelivery` against the pinned kid
|
|
105
|
-
|
|
106
|
-
All six are reproducible without any cooperation from KXCO. That's the standard we hold ourselves to.
|
|
107
|
-
|
|
108
|
-
---
|
|
109
|
-
|
|
110
|
-
**Contact:** security@kxco.ai for vulnerability reports. audit@kxco.ai for due-diligence and review requests.
|
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
|
-
}
|