kxco-verify 1.2.3 → 1.3.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/CHANGELOG.md CHANGED
@@ -1,5 +1,39 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.3.0
4
+
5
+ Additive. The library is byte-for-byte unchanged: same one dependency, same
6
+ offline behaviour, no notion of a chain anywhere in `src/`.
7
+
8
+ ### A command line
9
+
10
+ `npx kxco-verify <url>` fetches and verifies an attestation, with `--file` for
11
+ a manifest you already have and `--json` for CI. Exit codes: 0 valid, 1 invalid
12
+ or revoked, 2 fetch or parse error, 3 rotated.
13
+
14
+ The default path contacts nothing but the site being checked. No KXCO server,
15
+ no licence, no account. That is what this package is for and it is not
16
+ changing.
17
+
18
+ ### `--live`
19
+
20
+ Asks the KXCO key registry whether the signing kid is still active, after the
21
+ maths has passed. A signature made by a key revoked an hour ago is still a
22
+ perfectly valid signature, and no offline check can tell you otherwise.
23
+
24
+ It **fails closed**: an unreachable registry means not valid. A check that
25
+ could not run has not passed.
26
+
27
+ It needs `kxco-pq-network`, declared as an **optional** peer dependency so it
28
+ is not installed by default. Without it the flag prints what to install and
29
+ exits 2; everything else is unaffected. This is deliberate: making it a real
30
+ dependency would put a chain-aware package in the install path of a library
31
+ whose whole claim is that it has neither.
32
+
33
+ The registry is asked only once the signature has verified. A forged manifest
34
+ is a forged manifest whatever the registry says about the key it names, and
35
+ reporting it as a revocation would mislead.
36
+
3
37
  ## 1.2.1
4
38
 
5
39
  Released to carry an npm provenance attestation. **No functional change**: no
package/README.md CHANGED
@@ -55,7 +55,56 @@ If you need to **sign** attestations, see the packages listed under [Part of the
55
55
  npm install kxco-verify
56
56
  ```
57
57
 
58
- Node 18+. ESM only.
58
+ Node 18+. ESM only. One dependency, and no KXCO server in the trust path.
59
+
60
+ ---
61
+
62
+ ## Command line
63
+
64
+ ```bash
65
+ npx kxco-verify https://example.com/.well-known/kxco-pq-attestation
66
+ ```
67
+
68
+ ```
69
+ VALID example.com
70
+ algorithm ML-DSA-65
71
+ kid aa29f37ab7f4b2cf
72
+
73
+ This means the site signed its own manifest with a key it published.
74
+ It is not an endorsement of the site, its owner, or its content.
75
+ ```
76
+
77
+ `--file <path>` verifies a manifest you already have. `--json` prints a machine-readable result.
78
+
79
+ Exit codes are meant for CI: **0** valid, **1** invalid or revoked, **2** could not fetch or parse, **3** rotated.
80
+
81
+ ### `--live`
82
+
83
+ A signature made by a key that was revoked an hour ago is still a perfectly valid signature. The maths cannot tell you so. `--live` asks the KXCO key registry whether the signing kid is still active:
84
+
85
+ ```bash
86
+ npx kxco-verify https://example.com/.well-known/kxco-pq-attestation --live
87
+ ```
88
+
89
+ ```
90
+ VALID example.com
91
+ kid aa29f37ab7f4b2cf
92
+ registry revoked
93
+ ```
94
+
95
+ That exits **1**. The signature is still valid; the key is not trusted any more, and those are different facts.
96
+
97
+ `--live` **fails closed**: if the registry cannot be reached, the result is not valid. A check that could not run has not passed.
98
+
99
+ It needs [`kxco-pq-network`](https://www.npmjs.com/package/kxco-pq-network), an **optional** peer dependency. It is not installed by default and nothing else here needs it — install it only if you want the flag:
100
+
101
+ ```bash
102
+ npm install kxco-pq-network
103
+ ```
104
+
105
+ Registry base URL defaults to `https://chain.kxco.ai` and can be changed with `--registry`. A licence key, via `--licence` or `KXCO_LICENCE_KEY`, meters the reads; reads without one are allowed and rate-limited.
106
+
107
+ Everything above the `--live` heading works offline, forever, with no account and no licence. That is not changing.
59
108
 
60
109
  ---
61
110
 
@@ -254,7 +303,7 @@ The verifier is architecturally independent of the signer — the two share no c
254
303
 
255
304
  ## Security
256
305
 
257
- Signature verification uses [`@noble/post-quantum`](https://github.com/paulmillr/noble-post-quantum) ML-DSA-65, with no transitive dependencies. That package has **not** been independently audited by a third party; it has been self-audited by its maintainer. Cure53's 2023 NDS-01 audit of the `@noble` ecosystem covered `ciphers`, `curves` and `hashes`, and did not cover `@noble/post-quantum`. We state this plainly because you should not adopt a verification library on the strength of an audit that does not exist.
306
+ Signature verification uses [`@noble/post-quantum`](https://github.com/paulmillr/noble-post-quantum) ML-DSA-65, with no transitive dependencies. That package has **not** been independently audited by a third party; it has been self-audited by its maintainer. The other Noble packages have been audited, but separately and at different times: `@noble/hashes` by Cure53 in January 2022, `@noble/curves` by Trail of Bits in February 2023, Kudelski in September 2023 and Cure53 in September 2024, and `@noble/ciphers` by Cure53 in September 2024. None of those engagements covered the post-quantum package. We state this plainly because you should not adopt a verification library on the strength of an audit that does not exist.
258
307
 
259
308
  The library makes no outbound requests beyond the attestation URL you supply and the `pinAt` endpoint declared in the manifest. No data is sent to KXCO.
260
309
 
@@ -0,0 +1,184 @@
1
+ #!/usr/bin/env node
2
+ // kxco-verify — check a site's post-quantum deploy attestation from a terminal.
3
+ //
4
+ // The default is the whole point of this package: the maths, and nothing else.
5
+ // No KXCO server is contacted, no licence is needed, no account exists. It
6
+ // fetches the manifest the site publishes and checks the signature against the
7
+ // key the site published. That path must keep working forever, offline, for
8
+ // anyone, which is why the library this CLI wraps has one dependency and no
9
+ // notion of a chain.
10
+ //
11
+ // `--live` is the other thing. A signature made by a key that was revoked an
12
+ // hour ago is still a perfectly valid signature, and the maths cannot tell you
13
+ // so. `--live` asks the KXCO key registry whether the kid is still active.
14
+ // That needs kxco-pq-network, which is an OPTIONAL peer: install it and the
15
+ // flag works, leave it out and everything else is unaffected.
16
+ //
17
+ // Exit codes are meant for CI:
18
+ // 0 valid
19
+ // 1 invalid, or revoked under --live
20
+ // 2 could not reach or parse the target
21
+ // 3 rotated (the signature checks, but the live key differs)
22
+
23
+ import { verifyUrl, verifyManifest } from '../src/index.js'
24
+ import { readFileSync } from 'node:fs'
25
+
26
+ const ANSI = process.stdout.isTTY
27
+ ? { reset: '\x1b[0m', bold: '\x1b[1m', green: '\x1b[32m', yellow: '\x1b[33m', red: '\x1b[31m', dim: '\x1b[2m' }
28
+ : { reset: '', bold: '', green: '', yellow: '', red: '', dim: '' }
29
+
30
+ const USAGE = `
31
+ kxco-verify — verify a post-quantum deploy attestation
32
+
33
+ kxco-verify <url> fetch and verify. No KXCO server involved.
34
+ kxco-verify --file <path> verify a manifest you already have
35
+ kxco-verify <url> --live also ask the registry whether the key is still active
36
+ kxco-verify <url> --json machine-readable output
37
+
38
+ Options
39
+ --live check the signing kid against the KXCO key registry.
40
+ Needs kxco-pq-network installed. Fails CLOSED: if the
41
+ registry cannot be reached, the result is not valid.
42
+ --registry <url> registry base URL (default https://chain.kxco.ai)
43
+ --licence <key> licence key for the registry, or set KXCO_LICENCE_KEY
44
+ --timeout <ms> network timeout (default 10000)
45
+ --json print JSON instead of a human summary
46
+ --help
47
+
48
+ Exit codes
49
+ 0 valid 1 invalid or revoked 2 fetch/parse error 3 rotated
50
+ `.trimStart()
51
+
52
+ function parseArgs(argv) {
53
+ const opts = { live: false, json: false, timeout: 10_000, registry: undefined, licence: undefined, file: undefined }
54
+ const positional = []
55
+ for (let i = 0; i < argv.length; i++) {
56
+ const arg = argv[i]
57
+ switch (arg) {
58
+ case '--help': case '-h': opts.help = true; break
59
+ case '--live': opts.live = true; break
60
+ case '--json': opts.json = true; break
61
+ case '--file': opts.file = argv[++i]; break
62
+ case '--registry': opts.registry = argv[++i]; break
63
+ case '--licence': case '--license': opts.licence = argv[++i]; break
64
+ case '--timeout': opts.timeout = Number(argv[++i]); break
65
+ default:
66
+ if (arg.startsWith('-')) throw new Error(`unknown option ${arg}`)
67
+ positional.push(arg)
68
+ }
69
+ }
70
+ opts.url = positional[0]
71
+ return opts
72
+ }
73
+
74
+ // Loaded only when --live is asked for, so the default path has no chance of
75
+ // reaching for a package that may not be installed.
76
+ async function loadNetwork() {
77
+ try {
78
+ return await import('kxco-pq-network')
79
+ } catch {
80
+ throw new Error(
81
+ '--live needs kxco-pq-network, which is an optional peer dependency.\n' +
82
+ ' npm install kxco-pq-network\n' +
83
+ 'Without it, kxco-verify still checks the signature offline, which is what it is for.',
84
+ )
85
+ }
86
+ }
87
+
88
+ async function checkLive(kid, opts) {
89
+ const { networkConfig, KeyRegistry, KxcoPqNetworkError } = await loadNetwork()
90
+ const config = networkConfig({
91
+ registryUrl: opts.registry,
92
+ licenceKey: opts.licence ?? process.env.KXCO_LICENCE_KEY ?? null,
93
+ timeoutMs: opts.timeout,
94
+ })
95
+ try {
96
+ return { ok: true, record: await new KeyRegistry(config).lookup(kid) }
97
+ } catch (err) {
98
+ if (!(err instanceof KxcoPqNetworkError)) throw err
99
+ // Fails closed. A check that cannot run has not passed.
100
+ return { ok: false, error: err.message }
101
+ }
102
+ }
103
+
104
+ function render(result, live, opts) {
105
+ if (opts.json) {
106
+ console.log(JSON.stringify({ ...result, live }, null, 2))
107
+ return
108
+ }
109
+
110
+ const colour = { valid: ANSI.green, rotated: ANSI.yellow }[result.state] ?? ANSI.red
111
+ console.log(`${ANSI.bold}${colour}${result.state.toUpperCase()}${ANSI.reset} ${result.site ?? result.attestationUrl ?? ''}`)
112
+ if (result.algorithm) console.log(` algorithm ${result.algorithm}`)
113
+ if (result.manifestKid) console.log(` kid ${result.manifestKid}`)
114
+ if (result.livePubkeyKid && result.livePubkeyKid !== result.manifestKid) {
115
+ console.log(` live kid ${result.livePubkeyKid}`)
116
+ }
117
+ if (result.error) console.log(` ${ANSI.dim}${result.error.message}${ANSI.reset}`)
118
+
119
+ if (live) {
120
+ const label = live.ok ? live.record.status : 'unreachable'
121
+ const c = live.ok && live.record.status === 'active' ? ANSI.green : ANSI.red
122
+ console.log(` registry ${c}${label}${ANSI.reset}${live.ok && live.record.cached ? ' (cached)' : ''}`)
123
+ if (!live.ok) console.log(` ${ANSI.dim}${live.error}${ANSI.reset}`)
124
+ if (live.ok && live.record.rotatedTo) console.log(` rotated to ${live.record.rotatedTo}`)
125
+ }
126
+
127
+ // A valid result means one thing, and people read more into it than it says.
128
+ if (result.state === 'valid') {
129
+ console.log(`\n${ANSI.dim}This means the site signed its own manifest with a key it published.`)
130
+ console.log(`It is not an endorsement of the site, its owner, or its content.${ANSI.reset}`)
131
+ }
132
+ }
133
+
134
+ async function main() {
135
+ let opts
136
+ try {
137
+ opts = parseArgs(process.argv.slice(2))
138
+ } catch (err) {
139
+ console.error(err.message)
140
+ process.exit(2)
141
+ }
142
+
143
+ if (opts.help || (!opts.url && !opts.file)) {
144
+ console.log(USAGE)
145
+ process.exit(opts.help ? 0 : 2)
146
+ }
147
+
148
+ let result
149
+ if (opts.file) {
150
+ result = await verifyManifest(readFileSync(opts.file, 'utf8'))
151
+ } else {
152
+ result = await verifyUrl(opts.url, { timeoutMs: opts.timeout })
153
+ }
154
+
155
+ if (result.state === 'error') {
156
+ render(result, null, opts)
157
+ process.exit(2)
158
+ }
159
+
160
+ // The registry is only asked once the maths has passed. A forged manifest is
161
+ // a forged manifest whatever the registry says about the key it names, and
162
+ // reporting it as a revocation would mislead.
163
+ let live = null
164
+ if (opts.live && result.state !== 'invalid') {
165
+ try {
166
+ live = await checkLive(result.manifestKid, opts)
167
+ } catch (err) {
168
+ console.error(err.message)
169
+ process.exit(2)
170
+ }
171
+ }
172
+
173
+ render(result, live, opts)
174
+
175
+ if (result.state === 'invalid') process.exit(1)
176
+ if (live && (!live.ok || live.record.status !== 'active')) process.exit(1)
177
+ if (result.state === 'rotated') process.exit(3)
178
+ process.exit(0)
179
+ }
180
+
181
+ main().catch((err) => {
182
+ console.error(err.stack ?? err.message)
183
+ process.exit(2)
184
+ })
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kxco-verify",
3
- "version": "1.2.3",
3
+ "version": "1.3.0",
4
4
  "description": "Standalone, browser-safe verifier for KXCO ML-DSA-65 post-quantum signed attestations and credentials, for auditors, regulators, counterparties, and anyone who needs to confirm a signature without running the full KXCO SDK.",
5
5
  "keywords": [
6
6
  "post-quantum",
@@ -63,10 +63,11 @@
63
63
  }
64
64
  },
65
65
  "files": [
66
- "src",
67
- "README.md",
66
+ "CHANGELOG.md",
68
67
  "LICENSE",
69
- "CHANGELOG.md"
68
+ "README.md",
69
+ "bin",
70
+ "src"
70
71
  ],
71
72
  "engines": {
72
73
  "node": ">=20.19"
@@ -83,5 +84,16 @@
83
84
  "publishConfig": {
84
85
  "provenance": true,
85
86
  "access": "public"
87
+ },
88
+ "bin": {
89
+ "kxco-verify": "bin/kxco-verify.js"
90
+ },
91
+ "peerDependencies": {
92
+ "kxco-pq-network": "^1.0.0"
93
+ },
94
+ "peerDependenciesMeta": {
95
+ "kxco-pq-network": {
96
+ "optional": true
97
+ }
86
98
  }
87
99
  }