kxco-pq-audit 1.0.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/package.json +48 -0
- package/src/audit-log.js +72 -0
- package/src/backends/file.js +23 -0
- package/src/errors.js +6 -0
- package/src/index.js +3 -0
package/package.json
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "kxco-pq-audit",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Tamper-evident post-quantum audit log: ML-DSA-65-signed, hash-chained operation entries. Prove cryptographic operations happened, in order, and were not altered. Memory and append-only file backends.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"post-quantum",
|
|
7
|
+
"pqc",
|
|
8
|
+
"audit-log",
|
|
9
|
+
"ml-dsa",
|
|
10
|
+
"nist",
|
|
11
|
+
"fips-204",
|
|
12
|
+
"tamper-evident",
|
|
13
|
+
"compliance",
|
|
14
|
+
"kxco"
|
|
15
|
+
],
|
|
16
|
+
"license": "Apache-2.0",
|
|
17
|
+
"author": "KXCO by Knightsbridge <hello@kxco.ai>",
|
|
18
|
+
"homepage": "https://kxco.ai",
|
|
19
|
+
"repository": {
|
|
20
|
+
"type": "git",
|
|
21
|
+
"url": "https://github.com/JackKXCO/kxco-pq-audit.git"
|
|
22
|
+
},
|
|
23
|
+
"bugs": {
|
|
24
|
+
"url": "https://github.com/JackKXCO/kxco-pq-audit/issues"
|
|
25
|
+
},
|
|
26
|
+
"type": "module",
|
|
27
|
+
"main": "src/index.js",
|
|
28
|
+
"exports": {
|
|
29
|
+
".": "./src/index.js"
|
|
30
|
+
},
|
|
31
|
+
"files": [
|
|
32
|
+
"src",
|
|
33
|
+
"LICENSE"
|
|
34
|
+
],
|
|
35
|
+
"engines": {
|
|
36
|
+
"node": ">=20.19"
|
|
37
|
+
},
|
|
38
|
+
"dependencies": {
|
|
39
|
+
"@noble/hashes": "^1.7.0",
|
|
40
|
+
"kxco-post-quantum": "^1.1.6"
|
|
41
|
+
},
|
|
42
|
+
"scripts": {
|
|
43
|
+
"test": "node --test --test-timeout=30000 test/audit.test.js"
|
|
44
|
+
},
|
|
45
|
+
"publishConfig": {
|
|
46
|
+
"access": "public"
|
|
47
|
+
}
|
|
48
|
+
}
|
package/src/audit-log.js
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { mlDsa } from 'kxco-post-quantum'
|
|
2
|
+
import { sha256 } from '@noble/hashes/sha2'
|
|
3
|
+
import { KxcoPqAuditError } from './errors.js'
|
|
4
|
+
|
|
5
|
+
const enc = new TextEncoder()
|
|
6
|
+
|
|
7
|
+
function b64url(bytes) { return Buffer.from(bytes).toString('base64url') }
|
|
8
|
+
function fromB64url(s) { return new Uint8Array(Buffer.from(s, 'base64url')) }
|
|
9
|
+
|
|
10
|
+
function hashEntry(entry) {
|
|
11
|
+
return b64url(sha256(enc.encode(JSON.stringify(entry))))
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function signingBytes(seq, timestamp, operation, metadata, prevHash) {
|
|
15
|
+
return enc.encode(
|
|
16
|
+
`kxco-audit-v1\n${seq}\n${timestamp}\n${operation}\n${prevHash ?? 'null'}\n${JSON.stringify(metadata)}`
|
|
17
|
+
)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export class AuditLog {
|
|
21
|
+
#keypair
|
|
22
|
+
#entries = []
|
|
23
|
+
|
|
24
|
+
constructor({ keypair }) {
|
|
25
|
+
if (!keypair?.secretKey || !keypair?.publicKey) {
|
|
26
|
+
throw new KxcoPqAuditError('keypair with secretKey and publicKey is required')
|
|
27
|
+
}
|
|
28
|
+
this.#keypair = keypair
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async append(operation, metadata = {}) {
|
|
32
|
+
if (typeof operation !== 'string' || !operation) {
|
|
33
|
+
throw new KxcoPqAuditError('operation must be a non-empty string')
|
|
34
|
+
}
|
|
35
|
+
const all = await this._entries()
|
|
36
|
+
const seq = all.length
|
|
37
|
+
const ts = new Date().toISOString()
|
|
38
|
+
const prev = seq === 0 ? null : hashEntry(all[seq - 1])
|
|
39
|
+
const msg = signingBytes(seq, ts, operation, metadata, prev)
|
|
40
|
+
const sig = mlDsa.ml_dsa65.sign(new Uint8Array(this.#keypair.secretKey), msg)
|
|
41
|
+
|
|
42
|
+
const entry = { seq, timestamp: ts, operation, metadata, prevHash: prev, signature: b64url(sig) }
|
|
43
|
+
await this._store(entry)
|
|
44
|
+
return entry
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async verify(publicKey) {
|
|
48
|
+
const all = await this._entries()
|
|
49
|
+
for (let i = 0; i < all.length; i++) {
|
|
50
|
+
const e = all[i]
|
|
51
|
+
if (i === 0) {
|
|
52
|
+
if (e.prevHash !== null) return { valid: false, error: 'entry 0: prevHash must be null' }
|
|
53
|
+
} else {
|
|
54
|
+
const expected = hashEntry(all[i - 1])
|
|
55
|
+
if (e.prevHash !== expected) return { valid: false, error: `entry ${i}: prevHash mismatch` }
|
|
56
|
+
}
|
|
57
|
+
const msg = signingBytes(e.seq, e.timestamp, e.operation, e.metadata, e.prevHash)
|
|
58
|
+
let ok
|
|
59
|
+
try { ok = mlDsa.ml_dsa65.verify(new Uint8Array(publicKey), msg, fromB64url(e.signature)) }
|
|
60
|
+
catch { ok = false }
|
|
61
|
+
if (!ok) return { valid: false, error: `entry ${i}: signature invalid` }
|
|
62
|
+
}
|
|
63
|
+
return { valid: true, count: all.length }
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async export() {
|
|
67
|
+
return this._entries()
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async _entries() { return [...this.#entries] }
|
|
71
|
+
async _store(entry) { this.#entries.push(entry) }
|
|
72
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { readFile, appendFile } from 'node:fs/promises'
|
|
2
|
+
import { AuditLog } from '../audit-log.js'
|
|
3
|
+
import { KxcoPqAuditError } from '../errors.js'
|
|
4
|
+
|
|
5
|
+
export class FileAuditLog extends AuditLog {
|
|
6
|
+
#path
|
|
7
|
+
|
|
8
|
+
constructor({ keypair, path }) {
|
|
9
|
+
super({ keypair })
|
|
10
|
+
if (!path) throw new KxcoPqAuditError('FileAuditLog: path is required')
|
|
11
|
+
this.#path = path
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
async _entries() {
|
|
15
|
+
let text
|
|
16
|
+
try { text = await readFile(this.#path, 'utf8') } catch { return [] }
|
|
17
|
+
return text.trim().split('\n').filter(Boolean).map(line => JSON.parse(line))
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async _store(entry) {
|
|
21
|
+
await appendFile(this.#path, JSON.stringify(entry) + '\n', 'utf8')
|
|
22
|
+
}
|
|
23
|
+
}
|
package/src/errors.js
ADDED
package/src/index.js
ADDED