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 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
  [![npm](https://img.shields.io/npm/v/kxco-verify?label=npm&color=b0964f)](https://www.npmjs.com/package/kxco-verify)
6
+ [![downloads](https://img.shields.io/npm/dm/kxco-verify?label=downloads&color=b0964f)](https://www.npmjs.com/package/kxco-verify)
7
+ [![NIST ACVP](https://img.shields.io/badge/NIST_ACVP-1,793_passed,_0_failed-2ea44f)](https://github.com/KnightsbridgeAIQ/kxco-post-quantum/blob/main/CONFORMANCE.md)
8
+ [![npm provenance](https://img.shields.io/badge/npm-provenance-2ea44f)](https://www.npmjs.com/package/kxco-verify)
4
9
  [![Socket](https://socket.dev/api/badge/npm/package/kxco-verify)](https://socket.dev/npm/package/kxco-verify)
5
10
  [![license](https://img.shields.io/badge/license-Apache--2.0-blue)](./LICENSE)
6
11
  [![node](https://img.shields.io/node/v/kxco-verify.svg)](https://nodejs.org)
7
12
  [![verify.kxco.ai](https://img.shields.io/badge/verify.kxco.ai-live-22c55e)](https://verify.kxco.ai)
8
13
 
9
- Standalone post-quantum credential and attestation verifier for KXCO ML-DSA-65 signed documents. Zero heavy dependencies. Works in any modern browser and Node 18+.
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
- ## Release integrity
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
- Every release of this package is checkable without asking us for anything.
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 — anyone who needs to confirm that a signature is genuine without being a KXCO institution or running the full SDK.
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
- If you need to **sign** attestations, see the packages listed under [Part of the KXCO stack](#part-of-the-kxco-stack).
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 18+. ESM only.
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://example.com/api/attestation').then(r => r.text())
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 — retry shortly. |
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 means the signature math checks out. It does not mean KXCO has vetted the site, its operator, or its content. See [`docs/threat-model.md`](./docs/threat-model.md).
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: no timeout
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. Does not make any network requests — so the result is `"valid"`, `"invalid"`, or `"error"` only, never `"rotated"`.
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`, no `node:crypto`, and no `process`. SHA-256 is taken from `crypto.subtle` where available (every modern browser and Node 20+), with a `node:crypto` fallback on Node 18.
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/ml-dsa": "/lib/@noble/post-quantum/ml-dsa.js",
214
- "kxco-verify": "/lib/kxco-verify/src/index.js"
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 — no server receives your request; verification runs entirely in the browser.
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
- ## What this does NOT do
284
+ ## The KXCO post-quantum family
230
285
 
231
- - **Cannot sign.** To produce ML-DSA-65 attestations, use [`kxco-pq-sdk`](https://www.npmjs.com/package/kxco-post-quantum) or `kxco-pq-attest`.
232
- - **Cannot issue credentials.** Credential issuance — including KYC-backed identity documents — is handled by the full KXCO SDK and identity pipeline, not this package.
233
- - **Not a full identity client.** This package verifies one thing: whether a given ML-DSA-65 signature is mathematically valid and matches the published key. It has no concept of users, sessions, or identity records.
234
- - **No key registry.** There is no mapping from domain to approved key identifier. Anyone can generate an ML-DSA-65 keypair and publish a self-signed manifest; this library will mark it `"valid"`. A `"valid"` result is a math claim, not an endorsement.
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
- ## Part of the KXCO stack
312
+ ## Release integrity
240
313
 
241
- This package is the receiving end of the KXCO post-quantum signing pipeline.
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
- | Role | Package |
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
- The verifier is architecturally independent of the signer — the two share no code. That separation makes this package auditable in isolation: a change to the signing pipeline cannot influence the verifier's behaviour.
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
- ## Security
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
- 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.
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 — see [LICENSE](./LICENSE).
268
-
269
- ---
341
+ Apache 2.0. See [LICENSE](./LICENSE).
270
342
 
271
343
  ## Maintainers
272
344
 
273
- Shayne Heffernan and John Heffernan — [KXCO by Knightsbridge](https://kxco.ai)
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.2.3",
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.",
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
- "src",
67
- "README.md",
72
+ "CHANGELOG.md",
68
73
  "LICENSE",
69
- "CHANGELOG.md"
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
  }