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 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
+ }
@@ -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
@@ -0,0 +1,6 @@
1
+ export class KxcoPqAuditError extends Error {
2
+ constructor(msg) {
3
+ super(msg)
4
+ this.name = 'KxcoPqAuditError'
5
+ }
6
+ }
package/src/index.js ADDED
@@ -0,0 +1,3 @@
1
+ export { AuditLog } from './audit-log.js'
2
+ export { FileAuditLog } from './backends/file.js'
3
+ export { KxcoPqAuditError } from './errors.js'