kxco-post-quantum 1.2.1 → 1.4.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/MIGRATION.md ADDED
@@ -0,0 +1,143 @@
1
+ # Migration guide
2
+
3
+ How to move an existing system onto post-quantum signatures and key
4
+ establishment with this package, and how to move between versions of it.
5
+
6
+ Two separate problems, in order:
7
+
8
+ 1. [Choosing a parameter set](#choosing-a-parameter-set)
9
+ 2. [Migrating a classical system](#migrating-a-classical-system) — RSA or ECDSA today
10
+ 3. [Migrating between versions](#migrating-between-versions) of this package
11
+
12
+ ---
13
+
14
+ ## Choosing a parameter set
15
+
16
+ | You need | Use | Why |
17
+ |---|---|---|
18
+ | Signatures, general use | `mlDsa` (ML-DSA-65) | Category 3. Fast, 3309-byte signatures. The default. |
19
+ | Signatures, Category 5 required | `mlDsa87` (ML-DSA-87) | When a counterparty specifies Category 5 or names ML-DSA-87. Signatures are 4627 bytes. |
20
+ | Signatures, no lattice assumption | `slhDsa` (SLH-DSA-SHA2-192s) | Hash-based, so it does not share ML-DSA's underlying assumption. Signatures are 16 KB and signing takes seconds. |
21
+ | Key establishment | `mlKem` (ML-KEM-768) | Category 3, matching ML-DSA-65. |
22
+ | Key establishment, Category 5 required | `mlKem1024` (ML-KEM-1024) | Public key and ciphertext are 1568 bytes each. The shared secret stays 32 bytes. |
23
+
24
+ **On CNSA 2.0.** It names ML-DSA-87 and ML-KEM-1024, and both are available
25
+ here. Availability is not compliance: CNSA 2.0 compliance is a property of a
26
+ deployment, and picking `mlDsa87` for one call path does not confer it on a
27
+ system whose other signatures, identities and anchors are Category 3. Use the
28
+ Category 5 sets when someone asks for the parameter set. Do not use their
29
+ presence as the basis for a compliance statement.
30
+
31
+ Two points that catch people out:
32
+
33
+ **Sizes are the migration cost, not speed.** An ML-DSA-65 signature is 3309
34
+ bytes against 64 for Ed25519, roughly 50 times larger. Anything with a size
35
+ limit on the signature field needs looking at before anything else: database
36
+ columns, cookie and header size limits, QR codes, embedded firmware slots,
37
+ protocol frames with a fixed-width length. Signing and verification are fast
38
+ enough that throughput is rarely the blocker.
39
+
40
+ **SLH-DSA is slow enough to change designs.** Signing with a `...s` (small)
41
+ parameter set takes on the order of seconds. It suits infrequent, high-value
42
+ signatures such as firmware releases or root attestations. It does not suit
43
+ per-request signing. The `...f` (fast) sets trade signature size for speed.
44
+
45
+ ---
46
+
47
+ ## Migrating a classical system
48
+
49
+ ### Do not swap. Add, then remove.
50
+
51
+ The failure mode is replacing RSA or ECDSA with ML-DSA in one release, then
52
+ finding that data signed before the switch no longer verifies, or that a peer
53
+ you do not control has not migrated. Run both, then retire the old one.
54
+
55
+ **Stage 1: verify both, sign classically.** Deploy verification for ML-DSA
56
+ alongside your existing scheme. Nothing signs with it yet. This is the cheap
57
+ stage and it proves your storage and transport handle the larger signatures
58
+ before anything depends on them.
59
+
60
+ **Stage 2: sign both.** Attach both signatures. Consumers verify whichever they
61
+ support. Both must be bound to the same message, and a consumer that verifies
62
+ only one must not be able to be tricked into treating a valid classical
63
+ signature as covering a message the PQ signature does not, so sign identical
64
+ bytes with both.
65
+
66
+ **Stage 3: require both.** Verification fails unless both are present and valid.
67
+ This is the point at which you have post-quantum security and have not yet lost
68
+ compatibility. It is a good place to stay for a long time.
69
+
70
+ **Stage 4: drop the classical signature.** Only once every producer and consumer
71
+ is on stage 3, and only once you no longer need to verify anything signed before
72
+ stage 2.
73
+
74
+ For key establishment the same shape applies, and the hybrid step is not
75
+ optional in the same way: combine the ML-KEM shared secret with a classical
76
+ ECDH secret through a KDF so the session is secure if *either* holds. Do not
77
+ use a raw ML-KEM shared secret as a key. Run it through HKDF with a context
78
+ label, which is what `deriveSeed` is for.
79
+
80
+ ### Store what you will need later
81
+
82
+ Migrations stall on missing metadata, not on cryptography. From stage 1, record
83
+ alongside every signature:
84
+
85
+ - **Which algorithm and parameter set** produced it. Do not infer it from
86
+ signature length; ML-DSA-65 and some SLH-DSA sets are distinguishable by
87
+ length today but that is not a property to depend on.
88
+ - **Which key** produced it. The KXCO ID from `kid` identifies a public key
89
+ compactly and is safe to publish.
90
+ - **The context string**, if any. A signature made under a context does not
91
+ verify without it, so a lost context is a lost signature.
92
+
93
+ Adding these fields later means backfilling them for data you can no longer
94
+ attribute.
95
+
96
+ ### Test the negative cases
97
+
98
+ The interop matrix in this repository tests that a tampered signature is
99
+ rejected, for the reason that a verifier which returns true unconditionally
100
+ passes every positive test. Your integration deserves the same check: assert
101
+ that a flipped bit fails, that a signature under the wrong context fails, and
102
+ that a signature from the wrong key fails. Do this before stage 3, not after.
103
+
104
+ ---
105
+
106
+ ## Migrating between versions
107
+
108
+ The `CHANGELOG.md` is authoritative. The notes below cover the changes that
109
+ require action rather than a version bump.
110
+
111
+ ### 1.2.x to 1.3.0
112
+
113
+ **The FIPS 204 / FIPS 205 context parameter arrived.** `sign` and `verify` take
114
+ an optional `{ context }`. Existing calls that pass no context are unaffected:
115
+ no context means the empty context, which is what earlier versions produced.
116
+
117
+ A context is part of the signature. Adding one to a signing call invalidates
118
+ verification by any caller that does not pass the same context, so roll context
119
+ adoption out to verifiers first, exactly as in stage 1 above.
120
+
121
+ **The backend moved to `@noble/post-quantum` 0.7.0**, exact-pinned. If your
122
+ project also depends on `@noble/post-quantum` directly, align it. Two copies of
123
+ a cryptographic backend in one dependency tree is a hazard worth removing, and
124
+ `falcon.js` and `hybrid.js` from that release are not part of what this package
125
+ tests or supports.
126
+
127
+ ### Upgrading across any version
128
+
129
+ 1. Read `CHANGELOG.md` for the versions you are skipping, not just the target.
130
+ 2. Run your own negative tests, above, against the new version.
131
+ 3. Verify a signature produced by the old version with the new one, and the
132
+ reverse. This is the check that catches an encoding change.
133
+
134
+ ---
135
+
136
+ ## Verifying what you deploy
137
+
138
+ Release integrity is covered in [SECURITY.md](SECURITY.md); conformance evidence,
139
+ including the cross-implementation matrix, is in
140
+ [CONFORMANCE.md](CONFORMANCE.md). Before putting a key in production, read
141
+ [THREAT-MODEL.md](THREAT-MODEL.md), specifically the section on where the
142
+ residual risk sits. It will tell you whether the key should be in this library
143
+ at all or in an HSM.
package/README.md CHANGED
@@ -1,140 +1,215 @@
1
- # kxco-post-quantum
2
-
3
- Post-quantum cryptography primitives for the KXCO stack.
4
-
5
- [![npm](https://img.shields.io/npm/v/kxco-post-quantum)](https://www.npmjs.com/package/kxco-post-quantum)
6
- [![CI](https://github.com/KnightsbridgeAIQ/kxco-post-quantum/actions/workflows/ci.yml/badge.svg)](https://github.com/KnightsbridgeAIQ/kxco-post-quantum/actions/workflows/ci.yml)
7
- [![license](https://img.shields.io/badge/license-Apache--2.0-blue)](./LICENSE)
8
-
9
- ML-DSA-65 (FIPS 204) and SLH-DSA-SHA2-192s (FIPS 205) signatures, ML-KEM-768 (FIPS 203) key encapsulation, and key fingerprinting utilities. Wraps [`@noble/post-quantum`](https://github.com/paulmillr/noble-post-quantum) — the NIST reference implementation. All other `kxco-pq-*` packages depend on this one.
10
-
11
- ---
12
-
13
- ## Install
14
-
15
- ```bash
16
- npm install kxco-post-quantum
17
- ```
18
-
19
- Requires Node.js 20.19+. ESM-only.
20
-
21
- ---
22
-
23
- ## Quick start
24
-
25
- ```js
26
- import { mlDsa, mlKem, slhDsa, fingerprint, kidEquals } from 'kxco-post-quantum'
27
-
28
- // ML-DSA-65 — sign and verify
29
- const { publicKey, secretKey } = mlDsa.keypairFromMaster(masterSecret, 'signing-v1')
30
- const sig = mlDsa.sign(secretKey, 'hello')
31
- const ok = mlDsa.verify(publicKey, 'hello', sig) // true
32
-
33
- // SLH-DSA-SHA2-192s — hash-based signatures (same API shape as mlDsa)
34
- const slh = slhDsa.keypairFromMaster(masterSecret, 'signing-v1')
35
- const slhSig = slhDsa.sign(slh.secretKey, 'hello')
36
- const slhOk = slhDsa.verify(slh.publicKey, 'hello', slhSig) // true
37
-
38
- // Key fingerprint
39
- const kid = fingerprint(publicKey) // e.g. '4a7c9e2f1b3d5680'
40
- kidEquals(kid, kid) // true (constant-time)
41
-
42
- // ML-KEM-768 — key encapsulation
43
- const kemKeys = mlKem.keypairFromMaster(masterSecret, 'encryption-v1')
44
- const { ciphertext, sharedSecret } = mlKem.encapsulate(kemKeys.publicKey)
45
- const recovered = mlKem.decapsulate(ciphertext, kemKeys.secretKey)
46
- // sharedSecret and recovered are the same 32 bytes
47
- ```
48
-
49
- `masterSecret` is a `Buffer` or `Uint8Array` with at least 16 bytes of entropy (typically 32–64 bytes from an env var or KMS).
50
-
51
- ---
52
-
53
- ## API
54
-
55
- ### `mlDsa` — ML-DSA-65 (NIST FIPS 204)
56
-
57
- | Export | Signature | Description |
58
- |---|---|---|
59
- | `keypairFromMaster` | `(master, info?) → { publicKey, secretKey }` | Deterministic keypair via HKDF-SHA-512. `info` defaults to `'ml-dsa-65-v1'`. |
60
- | `sign` | `(secretKey, message) → string` | Signs a message. Returns a hex-encoded signature (6618 chars). |
61
- | `verify` | `(publicKey, message, sigHex) → boolean` | Verifies a hex-encoded signature. Returns `false` on any failure. |
62
- | `ml_dsa65` | raw primitive | The underlying `@noble/post-quantum` primitive, re-exported. |
63
-
64
- `publicKey` is 1952 bytes. `secretKey` is 4032 bytes. `message` accepts `Buffer`, `Uint8Array`, or `string`.
65
-
66
- ### `slhDsa` — SLH-DSA-SHA2-192s (NIST FIPS 205)
67
-
68
- Hash-based, stateless signatures. Security Category 3 (matching ML-DSA-65), but security rests only on the SHA-2 hash function — no lattice or number-theoretic assumptions. Use this as a conservative hedge alongside `mlDsa`. Tradeoff: signatures are ~5× larger (16224 vs 3309 bytes) and signing is slower.
69
-
70
- | Export | Signature | Description |
71
- |---|---|---|
72
- | `keypairFromMaster` | `(master, info?) → { publicKey, secretKey }` | Deterministic keypair via HKDF-SHA-512. `info` defaults to `'slh-dsa-sha2-192s-v1'`. |
73
- | `sign` | `(secretKey, message) → string` | Signs a message. Returns a hex-encoded signature (32448 chars). |
74
- | `verify` | `(publicKey, message, sigHex) → boolean` | Verifies a hex-encoded signature. Returns `false` on any failure. |
75
- | `slh_dsa_sha2_192s` | raw primitive | The underlying `@noble/post-quantum` primitive, re-exported. |
76
-
77
- `publicKey` is 48 bytes. `secretKey` is 96 bytes. `message` accepts `Buffer`, `Uint8Array`, or `string`.
78
-
79
- ### `mlKem` — ML-KEM-768 (NIST FIPS 203)
80
-
81
- | Export | Signature | Description |
82
- |---|---|---|
83
- | `keypairFromMaster` | `(master, info?) → { publicKey, secretKey }` | Deterministic keypair via HKDF-SHA-512. `info` defaults to `'ml-kem-768-v1'`. |
84
- | `encapsulate` | `(publicKey) → { ciphertext, sharedSecret }` | Generates a shared secret and ciphertext to send to the key holder. |
85
- | `decapsulate` | `(ciphertext, secretKey) → Buffer` | Recovers the shared secret from a ciphertext. Returns 32 bytes. |
86
- | `ml_kem768` | raw primitive | The underlying `@noble/post-quantum` primitive, re-exported. |
87
-
88
- `publicKey` is 1184 bytes. `ciphertext` is 1088 bytes. `sharedSecret` is 32 bytes.
89
-
90
- ### `fingerprint(publicKey)` → `string`
91
-
92
- First 16 hex characters of SHA-256 of the public key. Stable for the lifetime of the key. Accepts raw bytes or a hex string.
93
-
94
- ### `kidEquals(a, b)` → `boolean`
95
-
96
- Constant-time comparison of two kid strings. Use this when comparing user-supplied input — not `===`.
97
-
98
- ### `deriveSeed(master, info, length)` → `Buffer`
99
-
100
- HKDF-SHA-512 derivation. `master` must be at least 16 bytes. `info` is a required domain-separation string. Returns `length` bytes.
101
-
102
- ### `webhook` — hybrid HMAC + ML-DSA-65 delivery signing
103
-
104
- Low-level helpers for the KXCO hybrid webhook pattern: `envelope`, `hmacHex`, `verifyHmac`, `pqSign`, `verifyPq`, `signDelivery`, `verifyDelivery`. HMAC-SHA-256 gives symmetric verification with no library dependency; ML-DSA-65 adds non-repudiation over the same `${timestamp}.${body}` envelope. The full identity/credential surface lives in `kxco-pq-sdk`.
105
-
106
- ---
107
-
108
- ## What this does NOT do
109
-
110
- - No identity credentials or verifiable claims (those are in `kxco-pq-sdk`)
111
- - No relay, transport, or network layer
112
- - No key storage or KMS integration
113
- - No FIPS 140-3 module validation (the algorithms are FIPS-standardised; the module is not validated)
114
-
115
- ---
116
-
117
- ## Part of the KXCO stack
118
-
119
- `kxco-post-quantum` is the primitive layer. Everything else builds on it:
120
-
121
- - **`kxco-pq-sdk`** — identity credentials, webhook signing, verifiable claims
122
- - Other `kxco-pq-*` packages — domain-specific integrations
123
-
124
- Install this package directly when you need ML-DSA or ML-KEM without the rest of the identity stack.
125
-
126
- ---
127
-
128
- ## Security
129
-
130
- Cryptographic operations delegate entirely to [`@noble/post-quantum`](https://github.com/paulmillr/noble-post-quantum) and [`@noble/hashes`](https://github.com/paulmillr/noble-hashes) — this package does not reimplement any NIST primitive. `@noble/hashes` falls under Cure53's 2023 audit of the `@noble` ecosystem (`ciphers`, `curves`, `hashes`); `@noble/post-quantum` was **not** in that audit's scope and has been self-audited by its maintainer. See [AUDIT.md](./AUDIT.md) for the full posture.
131
-
132
- To report a vulnerability: [open a private security advisory](https://github.com/KnightsbridgeAIQ/kxco-post-quantum/security/advisories/new) or email **security@kxco.ai**.
133
-
134
- ## License
135
-
136
- Apache-2.0. See [LICENSE](./LICENSE).
137
-
138
- ## Maintainers
139
-
140
- Shayne Heffernan and John Heffernan — [KXCO by Knightsbridge](https://kxco.ai)
1
+ # kxco-post-quantum
2
+
3
+ Post-quantum cryptography primitives for the KXCO stack.
4
+
5
+ [![npm](https://img.shields.io/npm/v/kxco-post-quantum)](https://www.npmjs.com/package/kxco-post-quantum)
6
+ [![CI](https://github.com/KnightsbridgeAIQ/kxco-post-quantum/actions/workflows/ci.yml/badge.svg)](https://github.com/KnightsbridgeAIQ/kxco-post-quantum/actions/workflows/ci.yml)
7
+ [![conformance](https://github.com/KnightsbridgeAIQ/kxco-post-quantum/actions/workflows/conformance.yml/badge.svg)](https://github.com/KnightsbridgeAIQ/kxco-post-quantum/actions/workflows/conformance.yml)
8
+ [![license](https://img.shields.io/badge/license-Apache--2.0-blue)](./LICENSE)
9
+
10
+ ML-DSA-65 (FIPS 204) and SLH-DSA-SHA2-192s (FIPS 205) signatures, ML-KEM-768 (FIPS 203) key encapsulation, and key fingerprinting utilities. Category 5 sets ML-DSA-87 and ML-KEM-1024 are also available. Wraps [`@noble/post-quantum`](https://github.com/paulmillr/noble-post-quantum). All other `kxco-pq-*` packages depend on this one.
11
+
12
+ **Evidence, not adjectives:**
13
+
14
+ - [CONFORMANCE.md](./CONFORMANCE.md) — NIST ACVP vectors for FIPS 203/204/205, and a cross-implementation interop matrix against Bouncy Castle and two pure-Python implementations. 134 interop checks, both directions, with negative controls. Reproducible: `npm run conformance:acvp`, `npm run conformance:interop`.
15
+ - [THREAT-MODEL.md](./THREAT-MODEL.md) — what this defends against and what it does not. Read the side-channel section before deciding where a signing key lives.
16
+ - [MIGRATION.md](./MIGRATION.md) — moving an RSA or ECDSA system across, and moving between versions of this package.
17
+ - [SECURITY.md](./SECURITY.md) — reporting, and release integrity.
18
+
19
+ ---
20
+
21
+ ## Install
22
+
23
+ ```bash
24
+ npm install kxco-post-quantum
25
+ ```
26
+
27
+ Requires Node.js 20.19+. ESM-only.
28
+
29
+ ---
30
+
31
+ ## Quick start
32
+
33
+ ```js
34
+ import { mlDsa, mlKem, slhDsa, fingerprint, kidEquals } from 'kxco-post-quantum'
35
+
36
+ // ML-DSA-65 — sign and verify
37
+ const { publicKey, secretKey } = mlDsa.keypairFromMaster(masterSecret, 'signing-v1')
38
+ const sig = mlDsa.sign(secretKey, 'hello')
39
+ const ok = mlDsa.verify(publicKey, 'hello', sig) // true
40
+
41
+ // SLH-DSA-SHA2-192s — hash-based signatures (same API shape as mlDsa)
42
+ const slh = slhDsa.keypairFromMaster(masterSecret, 'signing-v1')
43
+ const slhSig = slhDsa.sign(slh.secretKey, 'hello')
44
+ const slhOk = slhDsa.verify(slh.publicKey, 'hello', slhSig) // true
45
+
46
+ // Key fingerprint
47
+ const kid = fingerprint(publicKey) // e.g. '4a7c9e2f1b3d5680'
48
+ kidEquals(kid, kid) // true (constant-time)
49
+
50
+ // ML-KEM-768 — key encapsulation
51
+ const kemKeys = mlKem.keypairFromMaster(masterSecret, 'encryption-v1')
52
+ const { ciphertext, sharedSecret } = mlKem.encapsulate(kemKeys.publicKey)
53
+ const recovered = mlKem.decapsulate(ciphertext, kemKeys.secretKey)
54
+ // sharedSecret and recovered are the same 32 bytes
55
+ ```
56
+
57
+ `masterSecret` is a `Buffer` or `Uint8Array` with at least 16 bytes of entropy (typically 32–64 bytes from an env var or KMS).
58
+
59
+ ### Category 5 parameter sets
60
+
61
+ `mlDsa87` (ML-DSA-87) and `mlKem1024` (ML-KEM-1024) have the same API as `mlDsa`
62
+ and `mlKem`, one security category higher. Reach for them when a counterparty
63
+ specifies Category 5 or names the parameter set. The KXCO default stays
64
+ Category 3.
65
+
66
+ ```js
67
+ import { mlDsa87, mlKem1024 } from 'kxco-post-quantum'
68
+
69
+ const { publicKey, secretKey } = mlDsa87.keypairFromMaster(masterSecret, 'signing-v1')
70
+ const sig = mlDsa87.sign(secretKey, 'hello') // 4627 bytes, 9254 hex chars
71
+ mlDsa87.verify(publicKey, 'hello', sig) // true
72
+ ```
73
+
74
+ | | Category 3 (default) | Category 5 |
75
+ |---|---|---|
76
+ | Signatures | `mlDsa` — pk 1952, sig 3309 | `mlDsa87` — pk 2592, sig 4627 |
77
+ | Key encapsulation | `mlKem` — pk 1184, ct 1088 | `mlKem1024` — pk 1568, ct 1568 |
78
+
79
+ The two sets do not mix, deliberately. Default derivation info differs, so one
80
+ master yields unrelated keys for each; and a signature from one set does not
81
+ verify under the other. Sizes are the migration cost, so check any fixed-width
82
+ signature or key field before mixing sets in one system.
83
+
84
+ **CNSA 2.0 names ML-DSA-87 and ML-KEM-1024, and supporting them is not a CNSA
85
+ 2.0 compliance claim.** Compliance is a property of a deployment, not of an
86
+ available function. See [CONFORMANCE.md](./CONFORMANCE.md).
87
+
88
+ ### Context strings (FIPS 204 / FIPS 205)
89
+
90
+ `sign` and `verify` take an optional context string, at most 255 bytes. A
91
+ signature made under a context does not verify without it, or under a different
92
+ one.
93
+
94
+ ```js
95
+ const sig = mlDsa.sign(secretKey, 'hello', { context: 'kxco-nexus-v1' })
96
+
97
+ mlDsa.verify(publicKey, 'hello', sig, { context: 'kxco-nexus-v1' }) // true
98
+ mlDsa.verify(publicKey, 'hello', sig) // false
99
+ mlDsa.verify(publicKey, 'hello', sig, { context: 'other-v1' }) // false
100
+ ```
101
+
102
+ The parameter is optional and defaults to no context, so every existing call
103
+ site is unaffected. An empty context is identical to omitting it. `slhDsa` takes
104
+ the same option.
105
+
106
+ **Context separates at the signature level; `keypairFromMaster(master, info)`
107
+ separates at the key level.** They are complementary. Use a context when one key
108
+ legitimately signs for several purposes and you need a signature from one
109
+ purpose to be unusable in another. Use a distinct derived key when the purposes
110
+ should not share a key at all.
111
+
112
+ Strings are encoded as UTF-8, so the 255-byte limit is bytes and not
113
+ characters. Over-length or wrongly typed input throws (`RangeError` /
114
+ `TypeError`) rather than returning `false`, because that is a caller bug and not
115
+ a failed verification:
116
+
117
+ ```js
118
+ mlDsa.sign(secretKey, 'hello', 'kxco-nexus-v1') // throws TypeError
119
+ // (needs { context: ... })
120
+ ```
121
+
122
+ That last case is worth guarding: without the throw it would silently sign with
123
+ *no* context and produce a valid-looking signature carrying none of the intended
124
+ separation.
125
+
126
+ ---
127
+
128
+ ## API
129
+
130
+ ### `mlDsa` — ML-DSA-65 (NIST FIPS 204)
131
+
132
+ | Export | Signature | Description |
133
+ |---|---|---|
134
+ | `keypairFromMaster` | `(master, info?) → { publicKey, secretKey }` | Deterministic keypair via HKDF-SHA-512. `info` defaults to `'ml-dsa-65-v1'`. |
135
+ | `sign` | `(secretKey, message) → string` | Signs a message. Returns a hex-encoded signature (6618 chars). |
136
+ | `verify` | `(publicKey, message, sigHex) → boolean` | Verifies a hex-encoded signature. Returns `false` on any failure. |
137
+ | `ml_dsa65` | raw primitive | The underlying `@noble/post-quantum` primitive, re-exported. |
138
+
139
+ `publicKey` is 1952 bytes. `secretKey` is 4032 bytes. `message` accepts `Buffer`, `Uint8Array`, or `string`.
140
+
141
+ ### `slhDsa` — SLH-DSA-SHA2-192s (NIST FIPS 205)
142
+
143
+ Hash-based, stateless signatures. Security Category 3 (matching ML-DSA-65), but security rests only on the SHA-2 hash function — no lattice or number-theoretic assumptions. Use this as a conservative hedge alongside `mlDsa`. Tradeoff: signatures are ~5× larger (16224 vs 3309 bytes) and signing is slower.
144
+
145
+ | Export | Signature | Description |
146
+ |---|---|---|
147
+ | `keypairFromMaster` | `(master, info?) → { publicKey, secretKey }` | Deterministic keypair via HKDF-SHA-512. `info` defaults to `'slh-dsa-sha2-192s-v1'`. |
148
+ | `sign` | `(secretKey, message) → string` | Signs a message. Returns a hex-encoded signature (32448 chars). |
149
+ | `verify` | `(publicKey, message, sigHex) → boolean` | Verifies a hex-encoded signature. Returns `false` on any failure. |
150
+ | `slh_dsa_sha2_192s` | raw primitive | The underlying `@noble/post-quantum` primitive, re-exported. |
151
+
152
+ `publicKey` is 48 bytes. `secretKey` is 96 bytes. `message` accepts `Buffer`, `Uint8Array`, or `string`.
153
+
154
+ ### `mlKem` — ML-KEM-768 (NIST FIPS 203)
155
+
156
+ | Export | Signature | Description |
157
+ |---|---|---|
158
+ | `keypairFromMaster` | `(master, info?) → { publicKey, secretKey }` | Deterministic keypair via HKDF-SHA-512. `info` defaults to `'ml-kem-768-v1'`. |
159
+ | `encapsulate` | `(publicKey) → { ciphertext, sharedSecret }` | Generates a shared secret and ciphertext to send to the key holder. |
160
+ | `decapsulate` | `(ciphertext, secretKey) → Buffer` | Recovers the shared secret from a ciphertext. Returns 32 bytes. |
161
+ | `ml_kem768` | raw primitive | The underlying `@noble/post-quantum` primitive, re-exported. |
162
+
163
+ `publicKey` is 1184 bytes. `ciphertext` is 1088 bytes. `sharedSecret` is 32 bytes.
164
+
165
+ ### `fingerprint(publicKey)` → `string`
166
+
167
+ First 16 hex characters of SHA-256 of the public key. Stable for the lifetime of the key. Accepts raw bytes or a hex string.
168
+
169
+ ### `kidEquals(a, b)` → `boolean`
170
+
171
+ Constant-time comparison of two kid strings. Use this when comparing user-supplied input — not `===`.
172
+
173
+ ### `deriveSeed(master, info, length)` → `Buffer`
174
+
175
+ HKDF-SHA-512 derivation. `master` must be at least 16 bytes. `info` is a required domain-separation string. Returns `length` bytes.
176
+
177
+ ### `webhook` — hybrid HMAC + ML-DSA-65 delivery signing
178
+
179
+ Low-level helpers for the KXCO hybrid webhook pattern: `envelope`, `hmacHex`, `verifyHmac`, `pqSign`, `verifyPq`, `signDelivery`, `verifyDelivery`. HMAC-SHA-256 gives symmetric verification with no library dependency; ML-DSA-65 adds non-repudiation over the same `${timestamp}.${body}` envelope. The full identity/credential surface lives in `kxco-pq-sdk`.
180
+
181
+ ---
182
+
183
+ ## What this does NOT do
184
+
185
+ - No identity credentials or verifiable claims (those are in `kxco-pq-sdk`)
186
+ - No relay, transport, or network layer
187
+ - No key storage or KMS integration
188
+ - No FIPS 140-3 module validation (the algorithms are FIPS-standardised; the module is not validated)
189
+
190
+ ---
191
+
192
+ ## Part of the KXCO stack
193
+
194
+ `kxco-post-quantum` is the primitive layer. Everything else builds on it:
195
+
196
+ - **`kxco-pq-sdk`** — identity credentials, webhook signing, verifiable claims
197
+ - Other `kxco-pq-*` packages — domain-specific integrations
198
+
199
+ Install this package directly when you need ML-DSA or ML-KEM without the rest of the identity stack.
200
+
201
+ ---
202
+
203
+ ## Security
204
+
205
+ Cryptographic operations delegate entirely to [`@noble/post-quantum`](https://github.com/paulmillr/noble-post-quantum) and [`@noble/hashes`](https://github.com/paulmillr/noble-hashes) — this package does not reimplement any NIST primitive. `@noble/hashes` falls under Cure53's 2023 audit of the `@noble` ecosystem (`ciphers`, `curves`, `hashes`); `@noble/post-quantum` was **not** in that audit's scope and has been self-audited by its maintainer. See [AUDIT.md](./AUDIT.md) for the full posture.
206
+
207
+ To report a vulnerability: [open a private security advisory](https://github.com/KnightsbridgeAIQ/kxco-post-quantum/security/advisories/new) or email **john@knightsbridgelaw.com**. Acknowledgement within 2 business days, triage decision within 5. Full policy, including safe harbour for good-faith research: <https://kxco.ai/security>.
208
+
209
+ ## License
210
+
211
+ Apache-2.0. See [LICENSE](./LICENSE).
212
+
213
+ ## Maintainers
214
+
215
+ Shayne Heffernan and John Heffernan — [KXCO by Knightsbridge](https://kxco.ai)
package/SECURITY.md CHANGED
@@ -1,28 +1,41 @@
1
- # Security Policy
2
-
3
- ## Reporting a vulnerability
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.
7
-
8
- ## Scope
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, SLH-DSA-SHA2-192s, 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
- - SLH-DSA-SHA2-192s — NIST FIPS 205 (hash-based signatures)
23
- - HMAC-SHA-256
24
- - HKDF-SHA-512 (RFC 5869)
25
-
26
- ## Disclosure
27
- We follow coordinated disclosure with a 90-day default window.
28
- For actively-exploited issues we ship a patch release within 48 hours.
1
+ # Security Policy
2
+
3
+ ## Reporting a vulnerability
4
+ Email **john@knightsbridgelaw.com**. Do not open public issues for security reports.
5
+ PGP key available on request. We credit reporters in `CHANGELOG.md` unless they
6
+ request otherwise.
7
+
8
+ Acknowledgement within **2 business days**. Triage decision within **5 business days**.
9
+
10
+ Full policy: <https://kxco.ai/security>
11
+
12
+ ## Safe harbour
13
+ If you make a good-faith effort to comply with this policy, we will treat your
14
+ research as authorised, and we will not pursue or support legal action against
15
+ you. Good faith means: do not access, modify, exfiltrate, or destroy data that
16
+ is not yours; use test accounts where possible; do not degrade service for
17
+ others; and stop and report as soon as you have established that a
18
+ vulnerability exists. This cannot bind third parties, and it does not cover
19
+ extortion, data sale, or public disclosure ahead of the window below.
20
+
21
+ ## Scope
22
+ In scope:
23
+ - Cryptographic correctness of the wrappers in this package
24
+ - Constant-time guarantees on signature/HMAC comparison
25
+ - Replay-window enforcement in `webhook.verify`
26
+ - HKDF domain separation in `derive`
27
+ - Kid fingerprint collision behaviour
28
+
29
+ Out of scope (report upstream to https://github.com/paulmillr/noble-post-quantum):
30
+ - Bugs in the underlying ML-DSA-65, ML-KEM-768, SLH-DSA-SHA2-192s, or HKDF primitives
31
+
32
+ ## Algorithms used
33
+ - ML-DSA-65 — NIST FIPS 204 (lattice signatures)
34
+ - ML-KEM-768 — NIST FIPS 203 (key encapsulation)
35
+ - SLH-DSA-SHA2-192s — NIST FIPS 205 (hash-based signatures)
36
+ - HMAC-SHA-256
37
+ - HKDF-SHA-512 (RFC 5869)
38
+
39
+ ## Disclosure
40
+ We follow coordinated disclosure with a 90-day default window.
41
+ For actively-exploited issues we ship a patch release within 48 hours.