kxco-verify 1.2.3 → 1.3.1
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 +54 -0
- package/README.md +143 -71
- package/bin/kxco-verify.js +184 -0
- package/package.json +25 -7
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,59 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.3.1
|
|
4
|
+
|
|
5
|
+
Documentation. No source change.
|
|
6
|
+
|
|
7
|
+
**The npm page leads with what the package proves.** The first screen now says
|
|
8
|
+
what a verification checks and where it runs: in a browser or a terminal, from
|
|
9
|
+
the artefact alone, with no KXCO server in the trust path, on the
|
|
10
|
+
`@noble/post-quantum` release NIST's vectors pass, alongside the migration dates
|
|
11
|
+
set by NIST, Executive Order 14412, OMB M-26-15 and the UK NCSC.
|
|
12
|
+
|
|
13
|
+
A family table maps every KXCO package to the job it does, and a new For
|
|
14
|
+
institutions section sets out the operated services and how to reach us. The
|
|
15
|
+
evidence documents are unchanged and linked from the page.
|
|
16
|
+
|
|
17
|
+
The examples run as written. The command line and the quick start verify the
|
|
18
|
+
live attestation at target150.com, and the browser import map maps each package
|
|
19
|
+
folder so the library loads in a browser. The page gives the supported runtime
|
|
20
|
+
as Node.js 20.19 and later, and the fetch defaults as 3000 ms per request and
|
|
21
|
+
200,000 bytes.
|
|
22
|
+
|
|
23
|
+
## 1.3.0
|
|
24
|
+
|
|
25
|
+
Additive. The library is byte-for-byte unchanged: same one dependency, same
|
|
26
|
+
offline behaviour, no notion of a chain anywhere in `src/`.
|
|
27
|
+
|
|
28
|
+
### A command line
|
|
29
|
+
|
|
30
|
+
`npx kxco-verify <url>` fetches and verifies an attestation, with `--file` for
|
|
31
|
+
a manifest you already have and `--json` for CI. Exit codes: 0 valid, 1 invalid
|
|
32
|
+
or revoked, 2 fetch or parse error, 3 rotated.
|
|
33
|
+
|
|
34
|
+
The default path contacts nothing but the site being checked. No KXCO server,
|
|
35
|
+
no licence, no account. That is what this package is for and it is not
|
|
36
|
+
changing.
|
|
37
|
+
|
|
38
|
+
### `--live`
|
|
39
|
+
|
|
40
|
+
Asks the KXCO key registry whether the signing kid is still active, after the
|
|
41
|
+
maths has passed. A signature made by a key revoked an hour ago is still a
|
|
42
|
+
perfectly valid signature, and no offline check can tell you otherwise.
|
|
43
|
+
|
|
44
|
+
It **fails closed**: an unreachable registry means not valid. A check that
|
|
45
|
+
could not run has not passed.
|
|
46
|
+
|
|
47
|
+
It needs `kxco-pq-network`, declared as an **optional** peer dependency so it
|
|
48
|
+
is not installed by default. Without it the flag prints what to install and
|
|
49
|
+
exits 2; everything else is unaffected. This is deliberate: making it a real
|
|
50
|
+
dependency would put a chain-aware package in the install path of a library
|
|
51
|
+
whose whole claim is that it has neither.
|
|
52
|
+
|
|
53
|
+
The registry is asked only once the signature has verified. A forged manifest
|
|
54
|
+
is a forged manifest whatever the registry says about the key it names, and
|
|
55
|
+
reporting it as a revocation would mislead.
|
|
56
|
+
|
|
3
57
|
## 1.2.1
|
|
4
58
|
|
|
5
59
|
Released to carry an npm provenance attestation. **No functional change**: no
|
package/README.md
CHANGED
|
@@ -1,41 +1,37 @@
|
|
|
1
1
|
# kxco-verify
|
|
2
2
|
|
|
3
|
+
**Verify a KXCO post-quantum signature in any browser or terminal, from the artefact alone, with no KXCO server in the trust path.**
|
|
4
|
+
|
|
3
5
|
[](https://www.npmjs.com/package/kxco-verify)
|
|
6
|
+
[](https://www.npmjs.com/package/kxco-verify)
|
|
7
|
+
[](https://github.com/KnightsbridgeAIQ/kxco-post-quantum/blob/main/CONFORMANCE.md)
|
|
8
|
+
[](https://www.npmjs.com/package/kxco-verify)
|
|
4
9
|
[](https://socket.dev/npm/package/kxco-verify)
|
|
5
10
|
[](./LICENSE)
|
|
6
11
|
[](https://nodejs.org)
|
|
7
12
|
[](https://verify.kxco.ai)
|
|
8
13
|
|
|
9
|
-
Standalone post-quantum credential and attestation verifier for KXCO ML-DSA-65 signed documents.
|
|
14
|
+
Standalone post-quantum credential and attestation verifier for KXCO ML-DSA-65 signed documents. One dependency, pinned exactly. Works in any modern browser and in Node.js 20.19 and later.
|
|
10
15
|
|
|
11
|
-
|
|
16
|
+
- **No KXCO server in the trust path.** `verifyManifest()` makes no network request at all, and `verifyUrl()` contacts only the attestation URL you pass and the key endpoint the manifest names.
|
|
17
|
+
- **Runs in the browser as shipped.** No `Buffer`, no `process` and SHA-256 from WebCrypto, which is how [verify.kxco.ai](https://verify.kxco.ai) checks a signature entirely in the tab.
|
|
18
|
+
- **Catches key rotation.** `verifyUrl()` compares the signing key with the one the site serves now and reports `rotated`, a state of its own, so a site mid-rotation is never mistaken for a forgery.
|
|
19
|
+
- **Live revocation on request.** `--live` asks the KXCO key registry whether the signing kid is still active, and fails closed.
|
|
20
|
+
- **Made for CI.** `npx kxco-verify <url>` exits 0 for valid, 1 for invalid or revoked, 2 when it cannot fetch or parse and 3 for rotated, with `--json` for machines.
|
|
21
|
+
- **Independent of the signer.** It calls the primitive directly and shares none of the KXCO signing code, so it can be audited on its own.
|
|
22
|
+
- **The primitive NIST's vectors pass.** Verification runs `@noble/post-quantum` 0.7.0, pinned exactly: the version [`kxco-post-quantum`](https://github.com/KnightsbridgeAIQ/kxco-post-quantum/blob/main/CONFORMANCE.md) holds to 1,793 NIST ACVP vectors passed, 0 failed, and 225 interoperability checks against liboqs, Bouncy Castle and the Python reference implementations, 0 failed.
|
|
12
23
|
|
|
13
|
-
|
|
24
|
+
**The migration has dates.**
|
|
25
|
+
|
|
26
|
+
- **NIST** published [FIPS 203](https://csrc.nist.gov/pubs/fips/203/final), [FIPS 204](https://csrc.nist.gov/pubs/fips/204/final) and [FIPS 205](https://csrc.nist.gov/pubs/fips/205/final) in August 2024.
|
|
27
|
+
- **United States:** [Executive Order 14412](https://www.federalregister.gov/documents/2026/06/25/2026-12909/securing-the-nation-against-advanced-cryptographic-attacks), signed on 22 June 2026, moves federal high-value and high-impact systems to post-quantum key establishment by 31 December 2030 and to post-quantum signatures by 31 December 2031. [OMB M-26-15](https://www.whitehouse.gov/wp-content/uploads/2026/06/M-26-15-Execution-of-the-Migration-to-Post-Quantum-Cryptography.pdf) requires PQC-agile libraries for all new applications.
|
|
28
|
+
- **United Kingdom:** the [NCSC](https://www.ncsc.gov.uk/guidance/pqc-migration-timelines) sets 2028, 2031 and 2035 as its migration milestones.
|
|
14
29
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
- **Provenance.** Each release carries a SLSA provenance attestation tying the
|
|
18
|
-
published tarball to the commit and workflow that built it. Verify with
|
|
19
|
-
`npm audit signatures`, or read it directly from
|
|
20
|
-
`registry.npmjs.org/-/npm/v1/attestations/kxco-verify@<version>`.
|
|
21
|
-
- **Bill of materials.** A CycloneDX SBOM is published as a GitHub Release asset
|
|
22
|
-
at `releases/download/v<version>/sbom.cyclonedx.json`, a permanent
|
|
23
|
-
unauthenticated URL. Not an expiring build artifact.
|
|
24
|
-
- **Pinned, not floated.** Every runtime dependency is pinned to an exact
|
|
25
|
-
version, never a range, so the code that performs the cryptography cannot
|
|
26
|
-
change without a release of this package. Every GitHub Action is pinned by
|
|
27
|
-
40-character commit SHA.
|
|
28
|
-
- **Conformance.** The primitives are the same ones run against **2,103 NIST
|
|
29
|
-
ACVP vectors (0 failed)** and a **225-check cross-implementation
|
|
30
|
-
interoperability matrix** in
|
|
31
|
-
[`kxco-post-quantum`](https://www.npmjs.com/package/kxco-post-quantum),
|
|
32
|
-
against liboqs, Bouncy Castle and two pure-Python implementations. This
|
|
33
|
-
package deliberately shares no code with the signer: a verifier that reached
|
|
34
|
-
the primitives through the same wrapper would be checking its own work.
|
|
30
|
+
[Quick start](#quick-start) · [Command line](#command-line) · [Browser usage](#browser-usage) · [For institutions](#for-institutions) · [Changelog](./CHANGELOG.md) · [kxco.ai](https://kxco.ai)
|
|
35
31
|
|
|
36
32
|
## When to use this
|
|
37
33
|
|
|
38
|
-
This package is for the **receiving end** of a KXCO signed attestation
|
|
34
|
+
This package is for the **receiving end** of a KXCO signed attestation: anyone who needs to confirm that a signature is genuine without being a KXCO institution or running the full SDK.
|
|
39
35
|
|
|
40
36
|
Use this if you are:
|
|
41
37
|
|
|
@@ -45,9 +41,7 @@ Use this if you are:
|
|
|
45
41
|
- Writing a minimal verification script with no heavy dependencies
|
|
46
42
|
- An end user who wants to verify a credential independently, without trusting any intermediary server
|
|
47
43
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
---
|
|
44
|
+
To **sign** attestations, pick the signing package from [the KXCO post-quantum family](#the-kxco-post-quantum-family).
|
|
51
45
|
|
|
52
46
|
## Install
|
|
53
47
|
|
|
@@ -55,9 +49,54 @@ If you need to **sign** attestations, see the packages listed under [Part of the
|
|
|
55
49
|
npm install kxco-verify
|
|
56
50
|
```
|
|
57
51
|
|
|
58
|
-
Node
|
|
52
|
+
Node.js 20.19 and later. ESM only. One dependency, and no KXCO server in the trust path.
|
|
53
|
+
|
|
54
|
+
## Command line
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
npx kxco-verify https://www.target150.com/api/attestation
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
```
|
|
61
|
+
VALID target150.com
|
|
62
|
+
algorithm ML-DSA-65
|
|
63
|
+
kid 680f9af0bb44de3f
|
|
64
|
+
|
|
65
|
+
This means the site signed its own manifest with a key it published.
|
|
66
|
+
It is not an endorsement of the site, its owner, or its content.
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
`--file <path>` verifies a manifest you already have. `--json` prints a machine-readable result.
|
|
70
|
+
|
|
71
|
+
Exit codes are meant for CI: **0** valid, **1** invalid or revoked, **2** could not fetch or parse, **3** rotated.
|
|
72
|
+
|
|
73
|
+
### `--live`
|
|
74
|
+
|
|
75
|
+
A signature stays mathematically valid after its key is revoked, so `--live` asks the KXCO key registry whether the signing kid is still active:
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
npx kxco-verify https://example.com/.well-known/kxco-pq-attestation --live
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
```
|
|
82
|
+
VALID example.com
|
|
83
|
+
kid aa29f37ab7f4b2cf
|
|
84
|
+
registry revoked
|
|
85
|
+
```
|
|
59
86
|
|
|
60
|
-
|
|
87
|
+
That exits **1**: the signature verifies, the registry reports the key revoked, and `--live` reports both facts.
|
|
88
|
+
|
|
89
|
+
`--live` **fails closed**: if the registry cannot be reached, the result is not valid, so a check that could not run never reads as a pass.
|
|
90
|
+
|
|
91
|
+
It uses [`kxco-pq-network`](https://www.npmjs.com/package/kxco-pq-network), an **optional** peer dependency, so the default install stays at one dependency. Add it when you want the flag:
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
npm install kxco-pq-network
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
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.
|
|
98
|
+
|
|
99
|
+
Everything above the `--live` heading works offline, forever, with no account and no licence.
|
|
61
100
|
|
|
62
101
|
## Quick start
|
|
63
102
|
|
|
@@ -79,7 +118,7 @@ If you already have the manifest body in hand (from a prior fetch, a file, or us
|
|
|
79
118
|
```js
|
|
80
119
|
import { verifyManifest } from 'kxco-verify'
|
|
81
120
|
|
|
82
|
-
const body = await fetch('https://
|
|
121
|
+
const body = await fetch('https://www.target150.com/api/attestation').then(r => r.text())
|
|
83
122
|
const result = await verifyManifest(body)
|
|
84
123
|
```
|
|
85
124
|
|
|
@@ -88,13 +127,31 @@ const result = await verifyManifest(body)
|
|
|
88
127
|
| `state` | Meaning |
|
|
89
128
|
|---|---|
|
|
90
129
|
| `"valid"` | Signature checks out against the manifest-declared key, and that key matches the live well-known endpoint. |
|
|
91
|
-
| `"rotated"` | Signature checks out, but the live well-known endpoint now serves a different key. The site is mid key-rotation
|
|
130
|
+
| `"rotated"` | Signature checks out, but the live well-known endpoint now serves a different key. The site is mid key-rotation: retry shortly. |
|
|
92
131
|
| `"invalid"` | The signature does not verify under the manifest-declared key, or the key identifier is inconsistent with the published key bytes. |
|
|
93
132
|
| `"error"` | The verifier could not run: network failure, malformed JSON, or unsupported algorithm. |
|
|
94
133
|
|
|
95
|
-
A `"valid"` result
|
|
134
|
+
A `"valid"` result is a precise cryptographic statement: the site signed this manifest with the key it publishes. Who operates the site is a separate question, covered in [`docs/threat-model.md`](./docs/threat-model.md).
|
|
135
|
+
|
|
136
|
+
## For institutions
|
|
137
|
+
|
|
138
|
+
The cryptography is free under Apache-2.0, works offline and needs nothing from
|
|
139
|
+
KXCO, now or in ten years. What KXCO sells is the part that has to be operated:
|
|
140
|
+
an answer about the present.
|
|
141
|
+
|
|
142
|
+
| Service | What you get |
|
|
143
|
+
|---|---|
|
|
144
|
+
| Hosted key registry | Whether a key is active, revoked or rotated, answered at verification time |
|
|
145
|
+
| Meta-transaction relay | KXCO validates your signed intent, pays the gas and submits it, so you never hold a token or run a node |
|
|
146
|
+
| On-chain anchoring | A timestamp on Armature L1 that the chain itself has verified |
|
|
147
|
+
| Live revocation | `anchored+live` verification, which confirms the signing key is still trusted now |
|
|
148
|
+
| Support and SLA | Availability commitments, an escalation path and a named contact |
|
|
96
149
|
|
|
97
|
-
|
|
150
|
+
Priced in USD, per seat, per year. No tokens, no nodes and no wallets. The line
|
|
151
|
+
between free and paid is set out in
|
|
152
|
+
[LICENCE-PRODUCT.md](https://github.com/KnightsbridgeAIQ/kxco-post-quantum/blob/main/LICENCE-PRODUCT.md).
|
|
153
|
+
|
|
154
|
+
**Talk to us: [admin@kxco.ai](mailto:admin@kxco.ai)** · [kxco.ai](https://kxco.ai)
|
|
98
155
|
|
|
99
156
|
## API
|
|
100
157
|
|
|
@@ -108,8 +165,8 @@ Fetches the attestation at `attestationUrl`, verifies the ML-DSA-65 signature, t
|
|
|
108
165
|
verifyUrl(
|
|
109
166
|
attestationUrl: string,
|
|
110
167
|
opts?: {
|
|
111
|
-
timeoutMs?: number // default
|
|
112
|
-
maxBytes?: number // max response size to accept
|
|
168
|
+
timeoutMs?: number // per request, default 3000
|
|
169
|
+
maxBytes?: number // max response size to accept, default 200000
|
|
113
170
|
fetchImpl?: typeof fetch // override the fetch implementation
|
|
114
171
|
skipLivePubkey?: boolean // skip rotation check (no live fetch)
|
|
115
172
|
}
|
|
@@ -118,7 +175,7 @@ verifyUrl(
|
|
|
118
175
|
|
|
119
176
|
### `verifyManifest(manifestBody) → Promise<VerifyResult>`
|
|
120
177
|
|
|
121
|
-
Verify a manifest you already have. Accepts a raw JSON string or a parsed object.
|
|
178
|
+
Verify a manifest you already have. Accepts a raw JSON string or a parsed object. Makes no network requests, so the result is `"valid"`, `"invalid"`, or `"error"`, and `"rotated"` belongs to `verifyUrl`.
|
|
122
179
|
|
|
123
180
|
```ts
|
|
124
181
|
verifyManifest(manifestBody: string | object): Promise<VerifyResult>
|
|
@@ -198,20 +255,20 @@ bytesToHex(bytes: Uint8Array): string
|
|
|
198
255
|
hexEquals(a: string, b: string): boolean
|
|
199
256
|
```
|
|
200
257
|
|
|
201
|
-
---
|
|
202
|
-
|
|
203
258
|
## Browser usage
|
|
204
259
|
|
|
205
|
-
The library is browser-safe by construction. It uses no `Buffer
|
|
260
|
+
The library is browser-safe by construction. It uses no `Buffer` and no `process`, and takes SHA-256 from `crypto.subtle`, which every modern browser and Node.js 20.19 and later provide.
|
|
206
261
|
|
|
207
|
-
Use a bundler (Vite, esbuild, Rollup, webpack), or load as ESM via an import map:
|
|
262
|
+
Use a bundler (Vite, esbuild, Rollup, webpack), or load as ESM via an import map that points each package at its folder:
|
|
208
263
|
|
|
209
264
|
```html
|
|
210
265
|
<script type="importmap">
|
|
211
266
|
{
|
|
212
267
|
"imports": {
|
|
213
|
-
"@noble/post-quantum/
|
|
214
|
-
"
|
|
268
|
+
"@noble/post-quantum/": "/lib/@noble/post-quantum/",
|
|
269
|
+
"@noble/curves/": "/lib/@noble/curves/",
|
|
270
|
+
"@noble/hashes/": "/lib/@noble/hashes/",
|
|
271
|
+
"kxco-verify": "/lib/kxco-verify/src/index.js"
|
|
215
272
|
}
|
|
216
273
|
}
|
|
217
274
|
</script>
|
|
@@ -222,54 +279,69 @@ Use a bundler (Vite, esbuild, Rollup, webpack), or load as ESM via an import map
|
|
|
222
279
|
</script>
|
|
223
280
|
```
|
|
224
281
|
|
|
225
|
-
This is how [verify.kxco.ai](https://verify.kxco.ai) works
|
|
226
|
-
|
|
227
|
-
---
|
|
282
|
+
This is how [verify.kxco.ai](https://verify.kxco.ai) works: the browser fetches the manifest directly and runs the maths in the tab, with no KXCO server in between.
|
|
228
283
|
|
|
229
|
-
##
|
|
284
|
+
## The KXCO post-quantum family
|
|
230
285
|
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
- **ML-DSA-65 only.** SLH-DSA-128s and hybrid envelopes are not supported in this release.
|
|
286
|
+
This is the receiving end: given an artefact and a public key, is the signature
|
|
287
|
+
valid. Browser-safe, offline, with no server in the path, which is what lets an
|
|
288
|
+
auditor confirm something years after it was issued. The rest of the family
|
|
289
|
+
covers the jobs around it:
|
|
236
290
|
|
|
237
|
-
|
|
291
|
+
| You need to | Install |
|
|
292
|
+
|---|---|
|
|
293
|
+
| Put the whole stack in one install | [`kxco-pq`](https://www.npmjs.com/package/kxco-pq) |
|
|
294
|
+
| Use ML-DSA, ML-KEM and SLH-DSA directly | [`kxco-post-quantum`](https://www.npmjs.com/package/kxco-post-quantum) |
|
|
295
|
+
| Keep signing keys on the HSM you already run | [`kxco-pq-hsm`](https://www.npmjs.com/package/kxco-pq-hsm) |
|
|
296
|
+
| Sign a document or record anyone can verify offline | [`kxco-pq-attest`](https://www.npmjs.com/package/kxco-pq-attest) |
|
|
297
|
+
| Keep a tamper-evident audit trail | [`kxco-pq-audit`](https://www.npmjs.com/package/kxco-pq-audit) |
|
|
298
|
+
| Verify a signature in a browser, with no server | [`kxco-verify`](https://www.npmjs.com/package/kxco-verify) |
|
|
299
|
+
| Issue institution identity credentials | [`kxco-pq-sdk`](https://www.npmjs.com/package/kxco-pq-sdk) |
|
|
300
|
+
| Encrypt files and payloads to one or many recipients | [`kxco-pq-vault`](https://www.npmjs.com/package/kxco-pq-vault) |
|
|
301
|
+
| Encrypt Node streams and WebSockets | [`kxco-pq-tls`](https://www.npmjs.com/package/kxco-pq-tls) |
|
|
302
|
+
| Sign and verify webhooks | [`kxco-post-quantum-webhook`](https://www.npmjs.com/package/kxco-post-quantum-webhook) |
|
|
303
|
+
| Give an AI agent an identity a verified institution sponsors | [`kxco-pq-agent`](https://www.npmjs.com/package/kxco-pq-agent) |
|
|
304
|
+
| Have Armature L1 verify a signature in consensus | [`kxco-pq-chain`](https://www.npmjs.com/package/kxco-pq-chain) |
|
|
305
|
+
| Prove an envelope at three levels, offline to on-chain | [`kxco-pq-network`](https://www.npmjs.com/package/kxco-pq-network) |
|
|
306
|
+
| Generate and rotate keys from a terminal | [`kxco-pq-cli`](https://www.npmjs.com/package/kxco-pq-cli) |
|
|
307
|
+
| Find quantum-vulnerable cryptography in a dependency tree | [`kxco-pq-scan`](https://www.npmjs.com/package/kxco-pq-scan) |
|
|
308
|
+
| Fail the build when code reaches past the wrapper | [`eslint-plugin-kxco-pq`](https://www.npmjs.com/package/eslint-plugin-kxco-pq) |
|
|
309
|
+
|
|
310
|
+
The verifier is architecturally independent of the signer: it calls the primitive directly and shares none of the KXCO signing code. That separation makes this package auditable in isolation: a change to the signing pipeline leaves the verifier untouched.
|
|
238
311
|
|
|
239
|
-
##
|
|
312
|
+
## Release integrity
|
|
240
313
|
|
|
241
|
-
|
|
314
|
+
Every release since 1.2.1 carries a SLSA provenance attestation tying the published tarball to
|
|
315
|
+
the commit and workflow that built it: verify with `npm audit signatures`, or read
|
|
316
|
+
it from `registry.npmjs.org/-/npm/v1/attestations/kxco-verify@<version>`. A CycloneDX
|
|
317
|
+
SBOM is published, from v1.2.1, as a GitHub Release asset at
|
|
318
|
+
`releases/download/v<version>/sbom.cyclonedx.json`, a permanent unauthenticated
|
|
319
|
+
URL. The one runtime dependency, `@noble/post-quantum`, is pinned to an exact
|
|
320
|
+
version, and every GitHub Action is pinned by commit SHA.
|
|
242
321
|
|
|
243
|
-
|
|
244
|
-
|---|---|
|
|
245
|
-
| Sign and attest deployments | [`kxco-post-quantum`](https://www.npmjs.com/package/kxco-post-quantum) |
|
|
246
|
-
| Issue PQ-signed credentials | `kxco-pq-attest` |
|
|
247
|
-
| Agent-level signing and policy | `kxco-pq-agent` |
|
|
248
|
-
| **Verify any of the above** | **`kxco-verify`** (this package) |
|
|
249
|
-
| Public web verifier | [verify.kxco.ai](https://verify.kxco.ai) |
|
|
322
|
+
## Security
|
|
250
323
|
|
|
251
|
-
|
|
324
|
+
**ML-DSA-65** (NIST FIPS 204) via [`@noble/post-quantum`](https://github.com/paulmillr/noble-post-quantum) 0.7.0, pinned exactly. No custom cryptography.
|
|
252
325
|
|
|
253
|
-
|
|
326
|
+
Evidenced, and reproducible on your own machine:
|
|
254
327
|
|
|
255
|
-
|
|
328
|
+
- **1,793 NIST ACVP vectors passed, 0 failed** across FIPS 203, 204 and 205 on `@noble/post-quantum` 0.7.0, pinned by digest, per [CONFORMANCE.md](https://github.com/KnightsbridgeAIQ/kxco-post-quantum/blob/main/CONFORMANCE.md). The other 310 are pairings the library refuses as weaker than the parameter set
|
|
329
|
+
- **225 interoperability checks passed, 0 failed**, against OpenSSL 3.5, liboqs, Bouncy Castle and dilithium-py/kyber-py, in both directions
|
|
330
|
+
- **SLSA provenance** on every release since 1.2.1: verify with `npm audit signatures`
|
|
331
|
+
- **CycloneDX SBOM** published with every release since 1.2.1
|
|
256
332
|
|
|
257
|
-
|
|
333
|
+
The audit history of every upstream library is recorded in [AUDIT.md](https://github.com/KnightsbridgeAIQ/kxco-post-quantum/blob/main/AUDIT.md).
|
|
258
334
|
|
|
259
335
|
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
336
|
|
|
261
337
|
To report a vulnerability, open a [private security advisory](https://github.com/KnightsbridgeAIQ/kxco-verify/security/advisories/new) or email **john@knightsbridgelaw.com**. Acknowledgement within 2 business days, triage decision within 5. Full policy, including safe harbour for good-faith research: <https://kxco.ai/security>.
|
|
262
338
|
|
|
263
|
-
---
|
|
264
|
-
|
|
265
339
|
## License
|
|
266
340
|
|
|
267
|
-
Apache 2.0
|
|
268
|
-
|
|
269
|
-
---
|
|
341
|
+
Apache 2.0. See [LICENSE](./LICENSE).
|
|
270
342
|
|
|
271
343
|
## Maintainers
|
|
272
344
|
|
|
273
|
-
Shayne Heffernan and John Heffernan
|
|
345
|
+
Shayne Heffernan and John Heffernan, [KXCO by Knightsbridge](https://kxco.ai)
|
|
274
346
|
|
|
275
347
|
[knightsbridgelaw.com](https://knightsbridgelaw.com) · [target150.com](https://target150.com) · [livetradingnews.com](https://livetradingnews.com)
|
|
@@ -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,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kxco-verify",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "1.3.1",
|
|
4
|
+
"description": "Free, browser-safe verifier for KXCO ML-DSA-65 post-quantum signatures, with no KXCO server in the trust path. An auditor, regulator or counterparty confirms a signature from the artefact alone, years later, and can check the key is still live against the on-chain registry.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"post-quantum",
|
|
7
7
|
"pqc",
|
|
@@ -15,11 +15,17 @@
|
|
|
15
15
|
"kxco-post-quantum",
|
|
16
16
|
"browser",
|
|
17
17
|
"browser-safe",
|
|
18
|
-
"zero-dependencies",
|
|
19
18
|
"supply-chain",
|
|
20
19
|
"deploy-attestation",
|
|
21
20
|
"provenance",
|
|
22
|
-
"manifest-verification"
|
|
21
|
+
"manifest-verification",
|
|
22
|
+
"pqc-migration",
|
|
23
|
+
"digital-signature",
|
|
24
|
+
"signature-verification",
|
|
25
|
+
"non-repudiation",
|
|
26
|
+
"tamper-evident",
|
|
27
|
+
"offline-verification",
|
|
28
|
+
"cli"
|
|
23
29
|
],
|
|
24
30
|
"license": "Apache-2.0",
|
|
25
31
|
"author": "Shayne Heffernan and John Heffernan",
|
|
@@ -63,10 +69,11 @@
|
|
|
63
69
|
}
|
|
64
70
|
},
|
|
65
71
|
"files": [
|
|
66
|
-
"
|
|
67
|
-
"README.md",
|
|
72
|
+
"CHANGELOG.md",
|
|
68
73
|
"LICENSE",
|
|
69
|
-
"
|
|
74
|
+
"README.md",
|
|
75
|
+
"bin",
|
|
76
|
+
"src"
|
|
70
77
|
],
|
|
71
78
|
"engines": {
|
|
72
79
|
"node": ">=20.19"
|
|
@@ -83,5 +90,16 @@
|
|
|
83
90
|
"publishConfig": {
|
|
84
91
|
"provenance": true,
|
|
85
92
|
"access": "public"
|
|
93
|
+
},
|
|
94
|
+
"bin": {
|
|
95
|
+
"kxco-verify": "bin/kxco-verify.js"
|
|
96
|
+
},
|
|
97
|
+
"peerDependencies": {
|
|
98
|
+
"kxco-pq-network": "^1.0.0"
|
|
99
|
+
},
|
|
100
|
+
"peerDependenciesMeta": {
|
|
101
|
+
"kxco-pq-network": {
|
|
102
|
+
"optional": true
|
|
103
|
+
}
|
|
86
104
|
}
|
|
87
105
|
}
|