kxco-verify 1.2.0 → 1.2.2
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 +83 -71
- package/LICENSE +202 -202
- package/README.md +275 -252
- package/package.json +87 -87
- package/src/fetch.d.ts +28 -28
- package/src/fetch.js +100 -100
- package/src/index.d.ts +57 -57
- package/src/index.js +212 -212
- package/src/parse.d.ts +31 -31
- package/src/parse.js +116 -116
- package/src/verify.d.ts +24 -24
- package/src/verify.js +103 -103
package/README.md
CHANGED
|
@@ -1,252 +1,275 @@
|
|
|
1
|
-
# kxco-verify
|
|
2
|
-
|
|
3
|
-
[](https://www.npmjs.com/package/kxco-verify)
|
|
4
|
-
[](https://socket.dev/npm/package/kxco-verify)
|
|
5
|
-
[](./LICENSE)
|
|
6
|
-
[](https://nodejs.org)
|
|
7
|
-
[](https://verify.kxco.ai)
|
|
8
|
-
|
|
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+.
|
|
10
|
-
|
|
11
|
-
---
|
|
12
|
-
|
|
13
|
-
##
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
-
|
|
21
|
-
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
```
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
```
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
```
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
1
|
+
# kxco-verify
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/kxco-verify)
|
|
4
|
+
[](https://socket.dev/npm/package/kxco-verify)
|
|
5
|
+
[](./LICENSE)
|
|
6
|
+
[](https://nodejs.org)
|
|
7
|
+
[](https://verify.kxco.ai)
|
|
8
|
+
|
|
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+.
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## Release integrity
|
|
14
|
+
|
|
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.
|
|
35
|
+
|
|
36
|
+
## When to use this
|
|
37
|
+
|
|
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.
|
|
39
|
+
|
|
40
|
+
Use this if you are:
|
|
41
|
+
|
|
42
|
+
- A regulator or auditor who received a signed document and needs to confirm it cryptographically
|
|
43
|
+
- A counterparty checking that an institution's attestation is valid before acting on it
|
|
44
|
+
- Building a browser-based verification UI (the public [verify.kxco.ai](https://verify.kxco.ai) runs this library entirely client-side)
|
|
45
|
+
- Writing a minimal verification script with no heavy dependencies
|
|
46
|
+
- An end user who wants to verify a credential independently, without trusting any intermediary server
|
|
47
|
+
|
|
48
|
+
If you need to **sign** attestations, see the packages listed under [Part of the KXCO stack](#part-of-the-kxco-stack).
|
|
49
|
+
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
## Install
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
npm install kxco-verify
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Node 18+. ESM only.
|
|
59
|
+
|
|
60
|
+
---
|
|
61
|
+
|
|
62
|
+
## Quick start
|
|
63
|
+
|
|
64
|
+
```js
|
|
65
|
+
import { verifyUrl } from 'kxco-verify'
|
|
66
|
+
|
|
67
|
+
const result = await verifyUrl('https://www.target150.com/api/attestation')
|
|
68
|
+
|
|
69
|
+
console.log(result.state) // 'valid' | 'rotated' | 'invalid' | 'error'
|
|
70
|
+
console.log(result.algorithm) // 'ML-DSA-65'
|
|
71
|
+
console.log(result.manifestKid) // '680f9af0bb44de3f'
|
|
72
|
+
console.log(result.site) // 'target150.com'
|
|
73
|
+
console.log(result.deployment) // { git_commit: '...', env: 'production', ... }
|
|
74
|
+
console.log(result.verifiedAtMs) // timestamp (Date.now()) when verification completed
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
If you already have the manifest body in hand (from a prior fetch, a file, or user paste):
|
|
78
|
+
|
|
79
|
+
```js
|
|
80
|
+
import { verifyManifest } from 'kxco-verify'
|
|
81
|
+
|
|
82
|
+
const body = await fetch('https://example.com/api/attestation').then(r => r.text())
|
|
83
|
+
const result = await verifyManifest(body)
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### Result states
|
|
87
|
+
|
|
88
|
+
| `state` | Meaning |
|
|
89
|
+
|---|---|
|
|
90
|
+
| `"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. |
|
|
92
|
+
| `"invalid"` | The signature does not verify under the manifest-declared key, or the key identifier is inconsistent with the published key bytes. |
|
|
93
|
+
| `"error"` | The verifier could not run: network failure, malformed JSON, or unsupported algorithm. |
|
|
94
|
+
|
|
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).
|
|
96
|
+
|
|
97
|
+
---
|
|
98
|
+
|
|
99
|
+
## API
|
|
100
|
+
|
|
101
|
+
All exports are re-exported from the main entry point (`import ... from 'kxco-verify'`). Lower-level helpers are also available from their sub-paths.
|
|
102
|
+
|
|
103
|
+
### `verifyUrl(attestationUrl, opts?) → Promise<VerifyResult>`
|
|
104
|
+
|
|
105
|
+
Fetches the attestation at `attestationUrl`, verifies the ML-DSA-65 signature, then fetches the live well-known pubkey endpoint declared in the manifest (`publicKey.pinAt`) to detect key rotation. May return `"rotated"` if the live endpoint now serves a different key identifier.
|
|
106
|
+
|
|
107
|
+
```ts
|
|
108
|
+
verifyUrl(
|
|
109
|
+
attestationUrl: string,
|
|
110
|
+
opts?: {
|
|
111
|
+
timeoutMs?: number // default: no timeout
|
|
112
|
+
maxBytes?: number // max response size to accept
|
|
113
|
+
fetchImpl?: typeof fetch // override the fetch implementation
|
|
114
|
+
skipLivePubkey?: boolean // skip rotation check (no live fetch)
|
|
115
|
+
}
|
|
116
|
+
): Promise<VerifyResult>
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
### `verifyManifest(manifestBody) → Promise<VerifyResult>`
|
|
120
|
+
|
|
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"`.
|
|
122
|
+
|
|
123
|
+
```ts
|
|
124
|
+
verifyManifest(manifestBody: string | object): Promise<VerifyResult>
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
### `VerifyResult`
|
|
128
|
+
|
|
129
|
+
```ts
|
|
130
|
+
interface VerifyResult {
|
|
131
|
+
state: 'valid' | 'rotated' | 'invalid' | 'error'
|
|
132
|
+
algorithm?: 'ML-DSA-65'
|
|
133
|
+
manifestKid?: string // key identifier declared in the manifest
|
|
134
|
+
livePubkeyKid?: string // key identifier currently at the well-known endpoint
|
|
135
|
+
site?: string // site identifier declared by the manifest
|
|
136
|
+
deployment?: Record<string, unknown> // opaque deployment metadata from the manifest
|
|
137
|
+
manifestRaw?: Record<string, unknown> // full parsed manifest JSON
|
|
138
|
+
error?: VerifyResultError // present when state is 'error', 'invalid', or 'rotated'
|
|
139
|
+
attestationUrl?: string
|
|
140
|
+
pubkeyUrl?: string // well-known endpoint URL (when fetched)
|
|
141
|
+
verifiedAtMs?: number // Date.now() when verification completed
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
interface VerifyResultError {
|
|
145
|
+
kind: 'parse' | 'fetch' | 'signature' | 'consistency' | 'rotation'
|
|
146
|
+
code: string
|
|
147
|
+
message: string
|
|
148
|
+
soft?: boolean // true when the math succeeded but a soft check failed
|
|
149
|
+
}
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
### Low-level helpers
|
|
153
|
+
|
|
154
|
+
These are exported for callers who want to compose their own verification logic.
|
|
155
|
+
|
|
156
|
+
#### `parseManifest(input) → ParseResult`
|
|
157
|
+
|
|
158
|
+
Parse and validate a raw manifest body without running signature verification.
|
|
159
|
+
|
|
160
|
+
```ts
|
|
161
|
+
parseManifest(input: string | object): ParseResult
|
|
162
|
+
// ParseResult is { ok: true; manifest: ParsedManifest } | { ok: false; error: ParseError }
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
#### `verifySignature(publicKey, message, signature) → boolean`
|
|
166
|
+
|
|
167
|
+
Run ML-DSA-65 signature verification directly.
|
|
168
|
+
|
|
169
|
+
```ts
|
|
170
|
+
verifySignature(
|
|
171
|
+
publicKey: Uint8Array | string, // hex or bytes
|
|
172
|
+
message: Uint8Array | string, // UTF-8 string or bytes
|
|
173
|
+
signature: Uint8Array | string, // hex or bytes
|
|
174
|
+
): boolean
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
#### `computeKid(publicKey) → Promise<string>`
|
|
178
|
+
|
|
179
|
+
Compute the KXCO key identifier: first 16 hex characters of SHA-256 of the raw public key bytes.
|
|
180
|
+
|
|
181
|
+
```ts
|
|
182
|
+
computeKid(publicKey: Uint8Array | string): Promise<string>
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
#### `getJsonBody(url, opts?) → Promise<FetchOk | FetchErr>`
|
|
186
|
+
|
|
187
|
+
Fetch a URL with timeout and size limits, returning the raw body string.
|
|
188
|
+
|
|
189
|
+
```ts
|
|
190
|
+
getJsonBody(url: string, opts?: GetJsonBodyOpts): Promise<FetchOk | FetchErr>
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
#### Utility: `hexToBytes`, `bytesToHex`, `hexEquals`
|
|
194
|
+
|
|
195
|
+
```ts
|
|
196
|
+
hexToBytes(hex: string): Uint8Array
|
|
197
|
+
bytesToHex(bytes: Uint8Array): string
|
|
198
|
+
hexEquals(a: string, b: string): boolean
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
---
|
|
202
|
+
|
|
203
|
+
## Browser usage
|
|
204
|
+
|
|
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.
|
|
206
|
+
|
|
207
|
+
Use a bundler (Vite, esbuild, Rollup, webpack), or load as ESM via an import map:
|
|
208
|
+
|
|
209
|
+
```html
|
|
210
|
+
<script type="importmap">
|
|
211
|
+
{
|
|
212
|
+
"imports": {
|
|
213
|
+
"@noble/post-quantum/ml-dsa": "/lib/@noble/post-quantum/ml-dsa.js",
|
|
214
|
+
"kxco-verify": "/lib/kxco-verify/src/index.js"
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
</script>
|
|
218
|
+
<script type="module">
|
|
219
|
+
import { verifyUrl } from 'kxco-verify'
|
|
220
|
+
const result = await verifyUrl('https://example.com/api/attestation')
|
|
221
|
+
console.log(result.state)
|
|
222
|
+
</script>
|
|
223
|
+
```
|
|
224
|
+
|
|
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
|
+
---
|
|
228
|
+
|
|
229
|
+
## What this does NOT do
|
|
230
|
+
|
|
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.
|
|
236
|
+
|
|
237
|
+
---
|
|
238
|
+
|
|
239
|
+
## Part of the KXCO stack
|
|
240
|
+
|
|
241
|
+
This package is the receiving end of the KXCO post-quantum signing pipeline.
|
|
242
|
+
|
|
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) |
|
|
250
|
+
|
|
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.
|
|
252
|
+
|
|
253
|
+
---
|
|
254
|
+
|
|
255
|
+
## Security
|
|
256
|
+
|
|
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.
|
|
258
|
+
|
|
259
|
+
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
|
+
|
|
261
|
+
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
|
+
|
|
263
|
+
---
|
|
264
|
+
|
|
265
|
+
## License
|
|
266
|
+
|
|
267
|
+
Apache 2.0 — see [LICENSE](./LICENSE).
|
|
268
|
+
|
|
269
|
+
---
|
|
270
|
+
|
|
271
|
+
## Maintainers
|
|
272
|
+
|
|
273
|
+
Shayne Heffernan and John Heffernan — [KXCO by Knightsbridge](https://kxco.ai)
|
|
274
|
+
|
|
275
|
+
[knightsbridgelaw.com](https://knightsbridgelaw.com) · [target150.com](https://target150.com) · [livetradingnews.com](https://livetradingnews.com)
|