kxco-verify 1.3.1 → 1.3.3

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,26 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.3.3
4
+
5
+ A manifest body that is valid JSON but not an object is refused with
6
+ `invalid_input` by parseManifest, verifyManifest and verifyUrl, and `--file`
7
+ exits 2 with the parse error. hexToBytes refuses characters that are not hex
8
+ digits. verifyUrl reports a live key endpoint serving `null` as
9
+ `live_pubkey_missing`, and a key that is not hex as the soft error
10
+ `invalid_live_pubkey`.
11
+
12
+ The README and the command line state exactly what a `"valid"` result covers:
13
+ the manifest's `signedMessage`, which for KXCO manifests is the key identifier,
14
+ commit and environment.
15
+
16
+ ## 1.3.2
17
+
18
+ Documentation. No source change.
19
+
20
+ A NOTICE file names the copyright owner, Knightsbridge Financial Ltd, trading
21
+ as KXCO, and ships in the package, so anyone who redistributes it carries the
22
+ attribution, as section 4(d) of the Apache License requires.
23
+
3
24
  ## 1.3.1
4
25
 
5
26
  Documentation. No source change.
package/NOTICE ADDED
@@ -0,0 +1,6 @@
1
+ kxco-verify
2
+ Copyright 2026 Knightsbridge Financial Ltd, trading as KXCO.
3
+ Registered in England and Wales, company number 15684975.
4
+
5
+ Licensed under the Apache License, Version 2.0. See LICENSE.
6
+ https://kxco.ai
package/README.md CHANGED
@@ -62,7 +62,7 @@ VALID target150.com
62
62
  algorithm ML-DSA-65
63
63
  kid 680f9af0bb44de3f
64
64
 
65
- This means the site signed its own manifest with a key it published.
65
+ This means the key this manifest publishes signed its signedMessage.
66
66
  It is not an endorsement of the site, its owner, or its content.
67
67
  ```
68
68
 
@@ -126,12 +126,12 @@ const result = await verifyManifest(body)
126
126
 
127
127
  | `state` | Meaning |
128
128
  |---|---|
129
- | `"valid"` | Signature checks out against the manifest-declared key, and that key matches the live well-known endpoint. |
129
+ | `"valid"` | Signature checks out against the manifest-declared key, and, where the manifest declares `pinAt`, that key matches the live well-known endpoint. |
130
130
  | `"rotated"` | Signature checks out, but the live well-known endpoint now serves a different key. The site is mid key-rotation: retry shortly. |
131
131
  | `"invalid"` | The signature does not verify under the manifest-declared key, or the key identifier is inconsistent with the published key bytes. |
132
132
  | `"error"` | The verifier could not run: network failure, malformed JSON, or unsupported algorithm. |
133
133
 
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).
134
+ A `"valid"` result is a precise cryptographic statement: the key this manifest publishes signed its `signedMessage`, and where the manifest declares `pinAt`, `verifyUrl` found that key served there now. For KXCO's manifests `signedMessage` is `{kid}.{deployment.git_commit}.{deployment.env}`, so the key identifier, commit and environment are signed. `site`, `msgFormat` and the other `deployment` fields are read from the manifest as served. Who operates the site is a separate question, covered in [`docs/threat-model.md`](./docs/threat-model.md).
135
135
 
136
136
  ## For institutions
137
137
 
@@ -338,7 +338,7 @@ To report a vulnerability, open a [private security advisory](https://github.com
338
338
 
339
339
  ## License
340
340
 
341
- Apache 2.0. See [LICENSE](./LICENSE).
341
+ Apache-2.0 © 2026 Knightsbridge Financial Ltd, trading as KXCO. See [LICENSE](./LICENSE) and [NOTICE](./NOTICE).
342
342
 
343
343
  ## Maintainers
344
344
 
@@ -126,7 +126,7 @@ function render(result, live, opts) {
126
126
 
127
127
  // A valid result means one thing, and people read more into it than it says.
128
128
  if (result.state === 'valid') {
129
- console.log(`\n${ANSI.dim}This means the site signed its own manifest with a key it published.`)
129
+ console.log(`\n${ANSI.dim}This means the key this manifest publishes signed its signedMessage.`)
130
130
  console.log(`It is not an endorsement of the site, its owner, or its content.${ANSI.reset}`)
131
131
  }
132
132
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kxco-verify",
3
- "version": "1.3.1",
3
+ "version": "1.3.3",
4
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",
@@ -71,6 +71,7 @@
71
71
  "files": [
72
72
  "CHANGELOG.md",
73
73
  "LICENSE",
74
+ "NOTICE",
74
75
  "README.md",
75
76
  "bin",
76
77
  "src"
@@ -82,7 +83,7 @@
82
83
  "@noble/post-quantum": "0.7.0"
83
84
  },
84
85
  "scripts": {
85
- "test": "node --test --test-reporter=spec test/parse.test.js test/verify.test.js test/fetch.test.js test/index.test.js",
86
+ "test": "node --test --test-reporter=spec test/parse.test.js test/verify.test.js test/fetch.test.js test/index.test.js test/property.test.js",
86
87
  "test:cov": "node --test --experimental-test-coverage test/parse.test.js test/verify.test.js test/fetch.test.js test/index.test.js",
87
88
  "smoke": "node scripts/smoke.js",
88
89
  "lint": "node -c src/index.js && node -c src/parse.js && node -c src/verify.js && node -c src/fetch.js"
@@ -101,5 +102,9 @@
101
102
  "kxco-pq-network": {
102
103
  "optional": true
103
104
  }
105
+ },
106
+ "devDependencies": {
107
+ "fast-check": "4.10.2",
108
+ "kxco-post-quantum": "1.7.7"
104
109
  }
105
110
  }
package/src/index.js CHANGED
@@ -5,9 +5,10 @@
5
5
  // whether a site's post-quantum deploy attestation is mathematically valid.
6
6
  //
7
7
  // What it deliberately does NOT do:
8
- // - Endorse any site. A "valid" result means "the site signed its own
9
- // manifest with a key it published" — nothing about who the site is or
10
- // whether its content is trustworthy.
8
+ // - Endorse any site. A "valid" result means "the key this manifest
9
+ // publishes signed its signedMessage" (for KXCO manifests, the kid, commit
10
+ // and environment), nothing about who the site is or whether its content
11
+ // is trustworthy.
11
12
  // - Maintain a registry of approved (domain, kid) pairs. That is a future
12
13
  // phase of kxco-post-quantum / verify.kxco.ai and not in this library.
13
14
  // - Speak any algorithm other than ML-DSA-65 with hex encoding in this
@@ -174,7 +175,7 @@ export async function verifyUrl(attestationUrl, opts = {}) {
174
175
 
175
176
  // Two ways the publisher may serve the kid: explicit "kid" field, or we
176
177
  // recompute from the publicKey hex. Prefer recompute as the source of truth.
177
- const livePubkeyHex = typeof liveJson.publicKey === 'string'
178
+ const livePubkeyHex = typeof liveJson?.publicKey === 'string'
178
179
  ? liveJson.publicKey
179
180
  : (typeof liveJson?.value === 'string' ? liveJson.value : null)
180
181
  if (!livePubkeyHex) {
@@ -183,7 +184,14 @@ export async function verifyUrl(attestationUrl, opts = {}) {
183
184
  result.verifiedAtMs = Date.now()
184
185
  return result
185
186
  }
186
- const liveKid = await computeKid(livePubkeyHex)
187
+ let liveKid
188
+ try { liveKid = await computeKid(livePubkeyHex) }
189
+ catch (err) {
190
+ result.pubkeyUrl = pubkeyUrl
191
+ result.error = { kind: 'parse', code: 'invalid_live_pubkey', message: `live pubkey is not hex: ${err.message}`, soft: true }
192
+ result.verifiedAtMs = Date.now()
193
+ return result
194
+ }
187
195
  result.pubkeyUrl = pubkeyUrl
188
196
  result.livePubkeyKid = liveKid
189
197
 
package/src/parse.js CHANGED
@@ -60,6 +60,10 @@ export function parseManifest(input) {
60
60
  if (typeof input === 'string') {
61
61
  try { raw = JSON.parse(input) }
62
62
  catch (err) { return err_('invalid_json', `body is not valid JSON: ${err.message}`) }
63
+ // Valid JSON is not yet a manifest. `null`, a number, a string or a
64
+ // boolean has no fields to read, so it is refused here like any other
65
+ // input that is not an object.
66
+ if (!raw || typeof raw !== 'object') return err_('invalid_input', 'body must be a JSON object')
63
67
  } else if (input && typeof input === 'object') {
64
68
  raw = input
65
69
  } else {
package/src/verify.js CHANGED
@@ -16,12 +16,12 @@ export function hexToBytes(hex) {
16
16
  if (typeof hex !== 'string' || hex.length % 2 !== 0) {
17
17
  throw new TypeError('hex input must be a string of even length')
18
18
  }
19
+ // Checked up front because parseInt reads a prefix: on its own it would take
20
+ // '0g' as 0, '+a' as 10 and '-1' as 255, giving one byte several spellings.
21
+ const bad = hex.search(/[^0-9a-fA-F]/)
22
+ if (bad !== -1) throw new TypeError(`malformed hex at offset ${bad}`)
19
23
  const out = new Uint8Array(hex.length / 2)
20
- for (let i = 0; i < out.length; i++) {
21
- const byte = parseInt(hex.substr(i * 2, 2), 16)
22
- if (Number.isNaN(byte)) throw new TypeError(`malformed hex at offset ${i * 2}`)
23
- out[i] = byte
24
- }
24
+ for (let i = 0; i < out.length; i++) out[i] = parseInt(hex.substr(i * 2, 2), 16)
25
25
  return out
26
26
  }
27
27