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 +21 -0
- package/NOTICE +6 -0
- package/README.md +4 -4
- package/bin/kxco-verify.js +1 -1
- package/package.json +7 -2
- package/src/index.js +13 -5
- package/src/parse.js +4 -0
- package/src/verify.js +5 -5
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
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
|
|
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
|
|
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
|
|
341
|
+
Apache-2.0 © 2026 Knightsbridge Financial Ltd, trading as KXCO. See [LICENSE](./LICENSE) and [NOTICE](./NOTICE).
|
|
342
342
|
|
|
343
343
|
## Maintainers
|
|
344
344
|
|
package/bin/kxco-verify.js
CHANGED
|
@@ -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
|
|
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.
|
|
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
|
|
9
|
-
//
|
|
10
|
-
// whether its content
|
|
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
|
|
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
|
-
|
|
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
|
|