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 +3 -3
- package/CHANGELOG.md +28 -0
- package/README.md +11 -10
- package/package.json +3 -2
- package/src/audit-log.js +3 -1
- package/src/index.d.ts +3 -2
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,
|
|
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)
|
|
11
11
|
[](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 {
|
|
55
|
+
import { mlDsa87 } from 'kxco-post-quantum'
|
|
56
56
|
|
|
57
|
-
const keypair =
|
|
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-
|
|
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-
|
|
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-
|
|
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.
|
|
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
|
|
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.
|
|
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.
|
|
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-
|
|
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
|
|
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-
|
|
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,
|