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 CHANGED
@@ -1,82 +1,135 @@
1
1
  # Changelog
2
2
 
3
- All notable changes to `kxco-post-quantum` are documented here. This project follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
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.1.0] — 2026-05-21
11
+
12
+ Same API. Same byte-for-byte outputs (all 29 pinned vectors still match).
13
+ The package now runs in **browsers** as well as Node.
14
+
15
+ ### Added
16
+ - Isomorphic runtime — every module works identically in modern browsers
17
+ (Chromium, Firefox, Safari) and Node, served from CDNs like esm.sh
18
+ with zero polyfill burden
19
+ - `test/browser-smoke.test.js` runs the public API with `globalThis.Buffer`
20
+ removed, asserts plain `Uint8Array` outputs and a clean hybrid-signing
21
+ round trip — proves browser compatibility in CI
22
+
23
+ ### Changed
24
+ - HKDF-SHA-512 now sourced from `@noble/hashes/hkdf` (was `node:crypto`)
25
+ - HMAC-SHA-256 now sourced from `@noble/hashes/hmac` (was `node:crypto`)
26
+ - SHA-256 for kid fingerprints now sourced from `@noble/hashes/sha256`
27
+ (was `node:crypto`)
28
+ - Constant-time comparisons are portable byte loops (replaces
29
+ `node:crypto.timingSafeEqual`) — identical security property,
30
+ runs in browsers
31
+ - Functions return `Buffer` on Node (when `globalThis.Buffer` is defined)
32
+ and plain `Uint8Array` in browsers. **Backwards compatible** for Node
33
+ callers; `Buffer extends Uint8Array` so any code accepting `Uint8Array`
34
+ already works.
35
+ - `engines.node` bumped to `>=20.19` to match the underlying
36
+ `@noble/hashes@2` requirement (Node 18 is past EOL)
37
+
38
+ ### Dependencies
39
+ - Added `@noble/hashes ^2.2.0` (peer of `@noble/post-quantum`)
40
+ - `@noble/post-quantum ^0.2.1` unchanged
41
+
42
+ ### Verification
43
+ - 9 node tests pass
44
+ - 6 browser-smoke tests pass
45
+ - 29 pinned vectors still match — no cryptographic surface changes,
46
+ bit-for-bit identical to 1.0.3 in Node
47
+
48
+ ## [1.0.3] — 2026-05-21
49
+
50
+ First release ships with SLSA Level 2 provenance attestation tied to a
51
+ public GitHub Actions workflow run. No cryptographic surface changes
52
+ vs `1.0.2` — every diff is metadata, types, CI, and hygiene.
53
+
54
+ ### Added
55
+ - SLSA Level 2 provenance on every published release via GitHub Actions OIDC
56
+ (`publishConfig.provenance: true`)
57
+ - `.github/workflows/publish.yml` triggered by `v*` tags — runs tests then
58
+ `npm publish --provenance --access public`
59
+ - `.github/workflows/ci.yml` matrix over Node 18 / 20 / 22 on every push and PR
60
+ - Hand-written TypeScript declarations (`.d.ts`) for all six modules; wired
61
+ into `exports[*].types` so TypeScript consumers get full typings without
62
+ any build step
63
+ - `.github/dependabot.yml` — weekly npm + github-actions ecosystem checks
64
+ - `sideEffects: false` for tree-shaking
65
+ - `funding` field in `package.json`
66
+ - Top-level `"types"` field in `package.json` pointing at `./src/index.d.ts`
67
+
68
+ ### Changed
69
+ - `package.json` `files` allowlist tightened to `["src", "README.md", "LICENSE",
70
+ "SECURITY.md", "CHANGELOG.md"]` — locks down what ships to npm
71
+ - `package.json` `exports` now declares per-subpath `types` + `import` keys
72
+ - `SECURITY.md` rewritten in the standard short-form template with explicit
73
+ in-scope / out-of-scope split delegating primitive bugs upstream to
74
+ `@noble/post-quantum`
75
+ - All third-party actions in workflows pinned by 40-char commit SHA, never
76
+ floating tags
77
+ - README badge row trimmed to four (`npm`, `license`, `Socket`, `production-live`)
78
+ and a 60-second live-verify quickstart added under the title
79
+
80
+ ### Security
81
+ - No cryptographic code changed in this release — every change is metadata,
82
+ types, CI, and documentation. Production behaviour is bit-for-bit identical
83
+ to `1.0.2`.
4
84
 
5
85
  ## [1.0.2] — 2026-05-21
6
86
 
7
87
  ### Changed
8
- - Repository URL on the npm package now points to `github.com/JackKXCO/kxco-post-quantum`. No code change.
88
+ - Repository URL on the npm package metadata now points to
89
+ `github.com/JackKXCO/kxco-post-quantum`. No code change.
9
90
 
10
91
  ## [1.0.1] — 2026-05-21
11
92
 
12
- Substance additions to the v1.0 release. No API changes. Same code on the cryptographic path; auditability and reproducibility infrastructure expanded.
13
-
14
93
  ### Added
15
- - `AUDIT.md` — self-attested audit posture, including explicit roadmap for third-party review (Q3 2026), bug bounty (Q4 2026), and FIPS 140-3 CMVP application (2027)
16
- - `test/vectors.json` — 29 deterministic test vectors pinning every primitive's output bit-for-bit
17
- - `test/run-vectors.js` — runner that verifies the library's output against `vectors.json` (exit code 0 = match)
18
- - `test/generate-vectors.js` — vector generator (run only when the library's output behaviour changes)
19
- - `npm test` — now runs both functional tests and vector verification
20
- - `npm run test:vectors` — vector check only
21
- - Production deployment proof: the wallet at `chain.kxco.ai` now imports `kxco-post-quantum` directly via npm. Real-world download count starts here.
94
+ - `AUDIT.md` — self-attested audit posture with roadmap (external audit
95
+ Q3 2026, public bug bounty Q4 2026, FIPS 140-3 CMVP application 2027)
96
+ - `test/vectors.json` — 29 deterministic test vectors pinning every primitive
97
+ output bit-for-bit
98
+ - `test/run-vectors.js` — runner anyone can use to verify reproducibility
99
+ - `npm test` runs both the functional tests and vector verification
100
+ - `npm run test:vectors` for vector check only
22
101
 
23
102
  ### Changed
24
- - `SECURITY.md` — sharpened with explicit threat model, in-scope vs out-of-scope responsibilities, pinned `@noble/post-quantum@0.2.1` integrity hash, Cure53 audit reference, FIPS posture clarification, replay-window and side-channel limitations
25
- - `README.md` — added "Used in production at" section with file refs to the live deployment
103
+ - `SECURITY.md` sharpened with explicit threat model and pinned upstream
104
+ `@noble/post-quantum@0.2.1` integrity hash
105
+ - `README.md` "Used in production at" section with file refs to chain.kxco.ai
26
106
 
27
- ### Why a patch and not a feature release
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.
107
+ No API changes from `1.0.0`.
29
108
 
30
109
  ## [1.0.0] — 2026-05-21
31
110
 
32
- First stable release. The public API below is committed to: future minor and patch releases will not break it without a major version bump.
33
-
34
- ### Committed public API
35
-
36
- ```js
37
- import {
38
- mlDsa, // namespace
39
- mlKem, // namespace
40
- deriveSeed,
41
- fingerprint,
42
- kidEquals,
43
- webhook, // namespace
44
- } from 'kxco-post-quantum'
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.
111
+ First stable release. Committed public API surface:
112
+
113
+ - `mlDsa.keypairFromMaster(master, info?)`, `mlDsa.sign`, `mlDsa.verify`
114
+ - `mlKem.keypairFromMaster(master, info?)`, `mlKem.encapsulate`, `mlKem.decapsulate`
115
+ - `deriveSeed(master, info, length)`
116
+ - `fingerprint(publicKey)`, `kidEquals(a, b)`
117
+ - `webhook.envelope`, `webhook.hmacHex`, `webhook.verifyHmac`,
118
+ `webhook.pqSign`, `webhook.verifyPq`, `webhook.signDelivery`,
119
+ `webhook.verifyDelivery`
120
+
121
+ Verified at release: 9/9 functional tests + 29/29 vector checks pass.
122
+
123
+ Underlying primitives via `@noble/post-quantum@^0.2.1` (audited by Cure53, 2024).
124
+ ESM-only. Node.js 18+.
79
125
 
80
126
  ## [0.1.0] — 2026-05-21
81
127
 
82
- Initial pre-release. Same surface as 1.0.0; promoted to 1.0.0 after verifying clean install and round-trip on a fresh environment.
128
+ Initial pre-release.
129
+
130
+ [Unreleased]: https://github.com/JackKXCO/kxco-post-quantum/compare/v1.0.3...HEAD
131
+ [1.0.3]: https://github.com/JackKXCO/kxco-post-quantum/compare/v1.0.2...v1.0.3
132
+ [1.0.2]: https://github.com/JackKXCO/kxco-post-quantum/compare/v1.0.1...v1.0.2
133
+ [1.0.1]: https://github.com/JackKXCO/kxco-post-quantum/compare/v1.0.0...v1.0.1
134
+ [1.0.0]: https://github.com/JackKXCO/kxco-post-quantum/compare/v0.1.0...v1.0.0
135
+ [0.1.0]: https://github.com/JackKXCO/kxco-post-quantum/releases/tag/v0.1.0
package/README.md CHANGED
@@ -1,14 +1,49 @@
1
- # @kxco/post-quantum
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
- [![npm](https://img.shields.io/npm/v/@kxco/post-quantum?color=d4a017)](https://www.npmjs.com/package/@kxco/post-quantum)
6
- [![license](https://img.shields.io/npm/l/@kxco/post-quantum)](./LICENSE)
7
- [![FIPS 203](https://img.shields.io/badge/FIPS%20203-ML--KEM--768-1e3a8a)](https://csrc.nist.gov/pubs/fips/203/final)
8
- [![FIPS 204](https://img.shields.io/badge/FIPS%20204-ML--DSA--65-1e3a8a)](https://csrc.nist.gov/pubs/fips/204/final)
5
+ [![CI](https://github.com/JackKXCO/kxco-post-quantum/actions/workflows/ci.yml/badge.svg)](https://github.com/JackKXCO/kxco-post-quantum/actions/workflows/ci.yml)
6
+ [![Socket](https://socket.dev/api/badge/npm/package/kxco-post-quantum)](https://socket.dev/npm/package/kxco-post-quantum)
7
+ [![npm provenance](https://img.shields.io/npm/v/kxco-post-quantum?label=npm%20%E2%9C%93%20provenance)](https://www.npmjs.com/package/kxco-post-quantum)
8
+ [![live verifier](https://img.shields.io/website?url=https%3A%2F%2Fchain.kxco.ai%2Fwallet%2Fverify&up_message=live&up_color=brightgreen&down_message=down&down_color=red&label=production)](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
- Email **security@kxco.ai** with details. Do not open public GitHub issues for vulnerabilities.
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
- This package's surface is the **integration patterns**: deterministic key derivation, kid fingerprinting, envelope construction, hybrid HMAC + ML-DSA-65 webhook signing, replay-window enforcement. Vulnerabilities in the **underlying NIST primitives** belong upstream.
14
-
15
- | Layer | Where it lives | Where to report |
16
- |---|---|---|
17
- | ML-DSA-65 / ML-KEM-768 algorithm bugs | `@noble/post-quantum` | https://github.com/paulmillr/noble-post-quantum/security |
18
- | HKDF-SHA-512 / HMAC-SHA-256 / SHA-256 | Node.js `crypto` module | https://github.com/nodejs/node/security |
19
- | Integration patterns (this library) | this repo | security@kxco.ai |
20
-
21
- ## Cryptographic posture
22
-
23
- ### Algorithms and standards
24
-
25
- | Primitive | Standard | Security level |
26
- |---|---|---|
27
- | ML-DSA-65 (Dilithium3) | [NIST FIPS 204](https://csrc.nist.gov/pubs/fips/204/final) | Category 3 (≈ AES-192) |
28
- | ML-KEM-768 (Kyber768) | [NIST FIPS 203](https://csrc.nist.gov/pubs/fips/203/final) | Category 3 (≈ AES-192) |
29
- | HKDF-SHA-512 | [NIST SP 800-56C](https://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-56Cr2.pdf), [RFC 5869](https://datatracker.ietf.org/doc/html/rfc5869) | n/a (KDF) |
30
- | HMAC-SHA-256 | [FIPS 198-1](https://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.198-1.pdf) | Post-quantum safe as MAC |
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.2",
3
+ "version": "1.1.0",
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,34 +28,57 @@
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
- ".": "./src/index.js",
33
- "./ml-dsa": "./src/ml-dsa.js",
34
- "./ml-kem": "./src/ml-kem.js",
35
- "./derive": "./src/derive.js",
36
- "./webhook": "./src/webhook.js",
37
- "./kid": "./src/kid.js"
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": {
50
- "node": ">=18"
68
+ "node": ">=20.19"
51
69
  },
52
70
  "dependencies": {
71
+ "@noble/hashes": "^2.2.0",
53
72
  "@noble/post-quantum": "^0.2.1"
54
73
  },
55
74
  "scripts": {
56
- "test": "node --test test/basic.test.js && node test/run-vectors.js",
57
- "test:vectors": "node test/run-vectors.js",
58
- "generate:vectors": "node test/generate-vectors.js > test/vectors.json"
75
+ "test": "node --test test/basic.test.js && node --test test/browser-smoke.test.js && node test/run-vectors.js",
76
+ "test:vectors": "node test/run-vectors.js",
77
+ "generate:vectors": "node test/generate-vectors.js > test/vectors.json",
78
+ "bench": "node bench/bench.js"
79
+ },
80
+ "publishConfig": {
81
+ "provenance": true,
82
+ "access": "public"
59
83
  }
60
84
  }
@@ -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/derive.js CHANGED
@@ -8,26 +8,40 @@
8
8
  // Domain separation through `info` is critical — using the same master key
9
9
  // for different purposes (signing vs encryption) MUST use distinct info
10
10
  // strings or you create a cross-protocol attack surface.
11
+ //
12
+ // Isomorphic: uses @noble/hashes/hkdf which runs identically in Node 18+ and
13
+ // modern browsers. Returns Buffer when running on Node (for backwards
14
+ // compatibility with existing callers), Uint8Array in browsers.
15
+
16
+ import { hkdf } from '@noble/hashes/hkdf.js'
17
+ import { sha512 } from '@noble/hashes/sha2.js'
11
18
 
12
- import { hkdfSync } from 'node:crypto'
19
+ const HAS_BUFFER = typeof Buffer !== 'undefined'
20
+ const enc = new TextEncoder()
21
+
22
+ function toBytes(input) {
23
+ if (input instanceof Uint8Array) return input
24
+ if (typeof input === 'string') return enc.encode(input)
25
+ throw new Error('expected Uint8Array or string')
26
+ }
13
27
 
14
28
  /**
15
29
  * Derive a deterministic seed from a master secret + an info string.
16
30
  *
17
- * @param {Buffer|Uint8Array} master — high-entropy input keying material
18
- * @param {string} info — domain separation tag (eg. 'kxco-platform-ml-dsa-65-v1')
19
- * @param {number} length — output seed length in bytes (32 for ML-DSA, 64 for ML-KEM)
20
- * @returns {Buffer}
31
+ * @param {Buffer|Uint8Array|string} master — high-entropy keying material (>= 16 bytes)
32
+ * @param {string} info — domain separation tag
33
+ * @param {number} length — output seed length in bytes
34
+ * @returns {Buffer|Uint8Array}
21
35
  */
22
36
  export function deriveSeed(master, info, length) {
23
- if (!master || master.length < 16) {
37
+ const ikm = toBytes(master)
38
+ if (!ikm || ikm.length < 16) {
24
39
  throw new Error('deriveSeed: master keying material must be at least 16 bytes')
25
40
  }
26
41
  if (!info || typeof info !== 'string') {
27
42
  throw new Error('deriveSeed: info string is required for domain separation')
28
43
  }
29
- // Empty salt with HKDF-SHA-512 is fine when master has high entropy.
30
- return Buffer.from(
31
- hkdfSync('sha512', master, Buffer.alloc(32), Buffer.from(info, 'utf8'), length)
32
- )
44
+ const salt = new Uint8Array(32) // 32 zero bytes — fine with high-entropy IKM
45
+ const out = hkdf(sha512, ikm, salt, enc.encode(info), length)
46
+ return HAS_BUFFER ? Buffer.from(out) : out
33
47
  }
package/src/index.d.ts ADDED
@@ -0,0 +1,7 @@
1
+ /// <reference types="node" />
2
+
3
+ export * as mlDsa from './ml-dsa.js'
4
+ export * as mlKem from './ml-kem.js'
5
+ export * from './derive.js'
6
+ export * from './kid.js'
7
+ export * as webhook from './webhook.js'
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/kid.js CHANGED
@@ -4,25 +4,40 @@
4
4
  // compare against an X-KXCO-PQ-Kid header on every webhook. Fast rejection of
5
5
  // stale or unknown keys without re-fetching the full 1952-byte public key on
6
6
  // every request.
7
+ //
8
+ // Isomorphic: uses @noble/hashes/sha256 — runs identically in Node and
9
+ // browsers.
10
+
11
+ import { sha256 } from '@noble/hashes/sha2.js'
7
12
 
8
- import { createHash } from 'node:crypto'
13
+ function hexToBytes(hex) {
14
+ if (hex.length % 2) throw new Error('odd hex length')
15
+ const b = new Uint8Array(hex.length / 2)
16
+ for (let i = 0; i < b.length; i++) b[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16)
17
+ return b
18
+ }
19
+
20
+ function bytesToHex(bytes) {
21
+ let s = ''
22
+ for (let i = 0; i < bytes.length; i++) s += bytes[i].toString(16).padStart(2, '0')
23
+ return s
24
+ }
9
25
 
10
26
  /**
11
- * Compute a stable, 16-hex-character fingerprint of a public key.
27
+ * Compute a stable 16-hex-character fingerprint of a public key.
12
28
  *
13
- * @param {Buffer|Uint8Array|string} publicKey — raw bytes or hex string
29
+ * @param {Buffer|Uint8Array|string} publicKey — raw bytes or hex string
14
30
  * @returns {string} 16 hex chars (8 bytes of SHA-256)
15
31
  */
16
32
  export function fingerprint(publicKey) {
17
- const buf = typeof publicKey === 'string'
18
- ? Buffer.from(publicKey, 'hex')
19
- : Buffer.from(publicKey)
20
- return createHash('sha256').update(buf).digest('hex').slice(0, 16)
33
+ const bytes = typeof publicKey === 'string'
34
+ ? hexToBytes(publicKey)
35
+ : (publicKey instanceof Uint8Array ? publicKey : new Uint8Array(publicKey))
36
+ return bytesToHex(sha256(bytes)).slice(0, 16)
21
37
  }
22
38
 
23
39
  /**
24
- * Constant-time comparison of two kid strings. Use this instead of `===`
25
- * when comparing kids from user input.
40
+ * Constant-time comparison of two kid strings.
26
41
  *
27
42
  * @param {string} a
28
43
  * @param {string} b