kxco-pq-audit 1.5.0 → 1.5.2

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/ASSESSMENT.md CHANGED
@@ -11,13 +11,13 @@ and publishes the lot. Cited here, proven there.
11
11
  ## What this package is
12
12
 
13
13
  An append-only record whose tampering is detectable and locatable. Entries are
14
- SHA-256 hash-chained and ML-DSA-65 signed, so **an edited or deleted entry
15
- fails `verify()` and the failure names the entry**. That is the guarantee, and
14
+ SHA-256 hash-chained and ML-DSA-87 or ML-DSA-65 signed, as the key decides, so
15
+ **an edited or deleted entry fails `verify()` and the failure names the entry**. That is the guarantee, and
16
16
  it is the reason to use this rather than a table with a timestamp column.
17
17
 
18
18
  **Two modes, and the second is what makes it viable at volume.** By default
19
19
  every entry carries its own signature. In sealed mode entries are chained and
20
- `seal()` signs the run once. Measured over a 10,000 entry run:
20
+ `seal()` signs the run once. Measured over a 10,000 entry run with an ML-DSA-65 key:
21
21
 
22
22
  | | entries/s | bytes/entry | 10k run | verify |
23
23
  |---|---|---|---|---|
package/CHANGELOG.md CHANGED
@@ -1,5 +1,33 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.5.2 (2026-10-07)
4
+
5
+ Documentation. No source change.
6
+
7
+ ML-DSA-87 is the parameter set KXCO recommends for every new key. The quick
8
+ start now makes an ML-DSA-87 keypair with `mlDsa87.ml_dsa87.keygen()`, and its
9
+ `key.rotate` example names `ml-dsa-87`. The entry format example is an
10
+ ML-DSA-87 entry, with the `alg` field it carries.
11
+
12
+ "Sealed logs" said every entry carries its own ML-DSA-65 signature by default.
13
+ The key decides the set; the default is one signature per entry. It now says
14
+ so, and that the figures were measured with an ML-DSA-65 key.
15
+ An ML-DSA-87 signature is 4,627 bytes, 6,170 base64url characters against 4,412 for ML-DSA-65, per FIPS 204, table 2.
16
+ ASSESSMENT.md and the type declarations name ML-DSA-87 first.
17
+
18
+ `DEFAULT_ALG` stays `ML-DSA-65`. It is how a record with no `alg` reads, and
19
+ every such record predates the field. An ML-DSA-65 key still writes v1 records
20
+ with no `alg`, so they verify under 1.4.x. It never picks the set a log signs
21
+ with, and its comment now says so.
22
+
23
+ ## 1.5.1
24
+ Documentation. No source change.
25
+
26
+ The npm description and the README lead name ML-DSA-87 beside ML-DSA-65: the
27
+ log has signed and verified both since 1.5.0. The description no longer quotes
28
+ the sealed-mode rate, which was measured with an ML-DSA-65 key; the README keeps
29
+ it with that context.
30
+
3
31
  ## 1.5.0
4
32
  **ML-DSA-87 logs.** A log given an ML-DSA-87 keypair signs its entries, or its
5
33
  seals, with ML-DSA-87; `log.signingAlg` reports the set. Each such record
package/README.md CHANGED
@@ -10,7 +10,7 @@
10
10
  [![license](https://img.shields.io/badge/license-Apache--2.0-blue)](./LICENSE)
11
11
  [![node](https://img.shields.io/node/v/kxco-pq-audit.svg)](https://nodejs.org)
12
12
 
13
- Every operation produces a signed entry: an ML-DSA-65 signature, SHA-256 chained to the previous entry. `verify()` replays the entire log, and any gap, reorder or edit breaks the chain or a signature.
13
+ Every operation produces a signed entry: an ML-DSA-87 or ML-DSA-65 signature (the key decides), SHA-256 chained to the previous entry. `verify()` replays the entire log, and any gap, reorder or edit breaks the chain or a signature.
14
14
 
15
15
  - **Tampering is detectable and locatable.** `verify()` replays the log from entry 0 and names the first entry that fails.
16
16
  - **Fast enough for real volume.** Sealed mode signs once per run: 46,544 entries a second at 367 bytes an entry, with 10,000 entries verified in 0.11 s, per [Sealed logs](#sealed-logs).
@@ -52,9 +52,9 @@ npm install kxco-pq-audit
52
52
  ```js
53
53
  import { FileAuditLog } from 'kxco-pq-audit'
54
54
  import { KxcoChain } from 'kxco-pq-chain' // optional, for anchoring
55
- import { mlDsa } from 'kxco-post-quantum'
55
+ import { mlDsa87 } from 'kxco-post-quantum'
56
56
 
57
- const keypair = mlDsa.ml_dsa65.keygen()
57
+ const keypair = mlDsa87.ml_dsa87.keygen()
58
58
 
59
59
  // Persist to disk, anchor a checkpoint on Armature L1 every 50 entries
60
60
  const chain = new KxcoChain({
@@ -70,7 +70,7 @@ const log = new FileAuditLog({
70
70
 
71
71
  await log.append('user.login', { userId: 'u_001', ip: '10.0.0.1' })
72
72
  await log.append('wire.auth', { txId: 'tx_abc', amount: 50000, currency: 'USD' })
73
- await log.append('key.rotate', { keyId: 'signing-key-v2', alg: 'ml-dsa-65' })
73
+ await log.append('key.rotate', { keyId: 'signing-key-v2', alg: 'ml-dsa-87' })
74
74
 
75
75
  const result = await log.verify(keypair.publicKey)
76
76
  // { valid: true, count: 3 }
@@ -109,7 +109,7 @@ In-memory log, for tests and short-lived processes. Use `FileAuditLog` to persis
109
109
 
110
110
  | Option | Type | Required | Default | Description |
111
111
  |---|---|---|---|---|
112
- | `keypair` | `{ secretKey, publicKey }` | Yes | none | ML-DSA-65 or ML-DSA-87 keypair from `kxco-post-quantum`. The key decides which set signs; `log.signingAlg` reports it |
112
+ | `keypair` | `{ secretKey, publicKey }` | Yes | none | ML-DSA-87 or ML-DSA-65 keypair from `kxco-post-quantum`. The key decides which set signs; `log.signingAlg` reports it |
113
113
  | `chain` | `KxcoChain` | No | `null` | Chain instance for checkpoint anchoring |
114
114
  | `checkpointEvery` | `number` | No | `100` | Anchor a checkpoint every N entries |
115
115
  | `institutionKid` | `string` | No | `null` | Identifier included in checkpoint metadata |
@@ -236,22 +236,23 @@ Sealed logs only. Entries appended since the last seal, without replaying the lo
236
236
  "operation": "wire.auth",
237
237
  "metadata": { "txId": "tx_abc", "amount": 50000 },
238
238
  "prevHash": null,
239
- "signature": "<base64url ML-DSA-65>"
239
+ "signature": "<base64url ML-DSA-87>",
240
+ "alg": "ML-DSA-87"
240
241
  }
241
242
  ```
242
243
 
243
- `prevHash` is the SHA-256 of the complete previous entry (signature included). The first entry always has `prevHash: null`. The signing message covers every field except `signature` itself. An entry signed with ML-DSA-87 also carries `"alg": "ML-DSA-87"`, which the signing message covers.
244
+ `prevHash` is the SHA-256 of the complete previous entry (signature included). The first entry always has `prevHash: null`. The signing message covers every field except `signature` itself. Only an entry signed with ML-DSA-87 carries `alg`, which the signing message covers. An entry signed with ML-DSA-65 carries none, exactly as before.
244
245
 
245
246
  ## Sealed logs
246
247
 
247
- By default every entry carries its own ML-DSA-65 signature. That signature is also the entire cost of the log. Measured over 10,000 entries, per [ASSESSMENT.md](./ASSESSMENT.md):
248
+ By default every entry carries its own signature, made under the set of the log's key. That signature is also the entire cost of the log. Measured over 10,000 entries with an ML-DSA-65 key, per [ASSESSMENT.md](./ASSESSMENT.md):
248
249
 
249
250
  | | entries/s | bytes/entry | 10k run | verify |
250
251
  |---|---|---|---|---|
251
252
  | signature per entry | 129 | 4,794 | 45.7 MB | 20.1 s |
252
253
  | signature per run | 46,544 | 367 | 3.5 MB | 0.11 s |
253
254
 
254
- Of those 4,794 bytes, per [ASSESSMENT.md](./ASSESSMENT.md), 4,412 are the base64url-encoded signature, which is why sealed mode is the one for real entry volume.
255
+ Of those 4,794 bytes, per [ASSESSMENT.md](./ASSESSMENT.md), 4,412 are the base64url-encoded signature, which is why sealed mode is the one for real entry volume. An ML-DSA-87 signature is 4,627 bytes ([FIPS 204](https://csrc.nist.gov/pubs/fips/204/final), table 2), or 6,170 base64url characters, so an entry signed with it is larger and sealed mode saves more.
255
256
 
256
257
  `sealed: true` keeps the hash chain on every entry and moves the signature to the run:
257
258
 
@@ -323,7 +324,7 @@ above it.
323
324
 
324
325
  ## Security
325
326
 
326
- **ML-DSA-65** (NIST FIPS 204) via [`kxco-post-quantum`](https://www.npmjs.com/package/kxco-post-quantum), running on the OpenSSL 3.5 primitives where the runtime provides them. No custom cryptography.
327
+ **ML-DSA-87 and ML-DSA-65** (NIST FIPS 204) via [`kxco-post-quantum`](https://www.npmjs.com/package/kxco-post-quantum), running on the OpenSSL 3.5 primitives where the runtime provides them. No custom cryptography.
327
328
 
328
329
  Evidenced, and reproducible on your own machine:
329
330
 
package/package.json CHANGED
@@ -1,8 +1,9 @@
1
1
  {
2
2
  "name": "kxco-pq-audit",
3
- "version": "1.5.0",
4
- "description": "Tamper-evident post-quantum audit trail for regulated institutions. ML-DSA-65 signed, SHA-256 hash-chained entries: any edit, gap or reorder fails verification and names the entry. 46,544 entries a second sealed, with checkpoints anchored on Armature L1.",
3
+ "version": "1.5.2",
4
+ "description": "Tamper-evident post-quantum audit trail for regulated institutions. ML-DSA-87 or ML-DSA-65 signed, SHA-256 hash-chained entries: any edit, gap or reorder fails verification and names the entry. Checkpoints anchored on Armature L1.",
5
5
  "keywords": [
6
+ "ml-dsa-87",
6
7
  "post-quantum",
7
8
  "pqc",
8
9
  "audit-log",
package/src/audit-log.js CHANGED
@@ -18,6 +18,8 @@ const SETS = Object.freeze({
18
18
  'ML-DSA-65': Object.freeze({ module: mlDsa, publicKeyBytes: 1952, secretKeyBytes: 4032 }),
19
19
  'ML-DSA-87': Object.freeze({ module: mlDsa87, publicKeyBytes: 2592, secretKeyBytes: 4896 }),
20
20
  })
21
+ // How a record with no `alg` reads, and so the set whose records carry none.
22
+ // It never picks the set a log signs with: the key's length does.
21
23
  const DEFAULT_ALG = 'ML-DSA-65'
22
24
 
23
25
  function algForKey(key, field) {
@@ -233,7 +235,7 @@ export class AuditLog {
233
235
  return { signature: b64url(Buffer.from(sig, 'hex')), stated }
234
236
  }
235
237
 
236
- /** The parameter set this log signs with: 'ML-DSA-65' or 'ML-DSA-87'. */
238
+ /** The parameter set this log signs with: 'ML-DSA-87' or 'ML-DSA-65'. */
237
239
  get signingAlg() { return this.#signingAlg() }
238
240
 
239
241
  /**
package/src/index.d.ts CHANGED
@@ -143,7 +143,8 @@ export interface AuditLogOptions {
143
143
  * In-memory tamper-evident audit log.
144
144
  *
145
145
  * Every entry is SHA-256 hash-chained to its predecessor, and signed with
146
- * ML-DSA-65 either per entry or, with `sealed: true`, once per run.
146
+ * ML-DSA-87 or ML-DSA-65, as the key decides, either per entry or, with
147
+ * `sealed: true`, once per run.
147
148
  * `verify()` replays the entire chain — any gap, reorder, or edit breaks either
148
149
  * the chain or a signature.
149
150
  *
@@ -185,7 +186,7 @@ export declare class AuditLog {
185
186
  readonly signingKid: string
186
187
 
187
188
  /** The parameter set this log signs with, decided by its secret key. */
188
- readonly signingAlg: 'ML-DSA-65' | 'ML-DSA-87'
189
+ readonly signingAlg: 'ML-DSA-87' | 'ML-DSA-65'
189
190
 
190
191
  /**
191
192
  * Sign everything appended since the last seal, as one run. Returns the seal,