kxco-verify 1.2.2 → 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 +34 -0
- package/README.md +51 -2
- package/bin/kxco-verify.js +184 -0
- package/package.json +16 -4
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.
|
|
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.
|
|
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
|
-
"
|
|
67
|
-
"README.md",
|
|
66
|
+
"CHANGELOG.md",
|
|
68
67
|
"LICENSE",
|
|
69
|
-
"
|
|
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
|
}
|