kxco-pq-network 1.0.0
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 +84 -0
- package/LICENSE +202 -0
- package/README.md +184 -0
- package/SALES-SKU.md +134 -0
- package/package.json +67 -0
- package/src/config.js +126 -0
- package/src/errors.js +45 -0
- package/src/index.d.ts +169 -0
- package/src/index.js +27 -0
- package/src/meter.js +65 -0
- package/src/registry.js +232 -0
- package/src/verify-mode.js +152 -0
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
// The three modes, applied to an envelope whose signature has already been
|
|
2
|
+
// checked.
|
|
3
|
+
//
|
|
4
|
+
// This module does not do cryptography. Each package owns its own envelope
|
|
5
|
+
// shape and its own signing message, and duplicating that here would mean two
|
|
6
|
+
// definitions of what a valid signature is. Instead the caller checks the
|
|
7
|
+
// maths and hands the outcome in, and this decides what the mode requires on
|
|
8
|
+
// top of it.
|
|
9
|
+
//
|
|
10
|
+
// The order matters: signature first, always. A revoked key that signed
|
|
11
|
+
// nothing should be reported as a bad signature, not as a revocation, because
|
|
12
|
+
// the two lead a reader to different conclusions about what happened.
|
|
13
|
+
|
|
14
|
+
import { CHAIN_ID } from './config.js'
|
|
15
|
+
import { FAILURE, KxcoPqNetworkError } from './errors.js'
|
|
16
|
+
import { KeyRegistry } from './registry.js'
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Read the anchor out of an envelope, whichever of the two shapes it uses.
|
|
20
|
+
*
|
|
21
|
+
* kxco-pq-attest wrote `chainAnchor: { txHash, blockNumber }` before this
|
|
22
|
+
* package existed, and the current shape is `chainId` alongside
|
|
23
|
+
* `anchor: { txHash, blockNumber }`. Both are accepted so that envelopes
|
|
24
|
+
* already in customers' archives keep verifying.
|
|
25
|
+
*
|
|
26
|
+
* @param {object} envelope
|
|
27
|
+
* @returns {{ txHash: string, blockNumber?: number, chainId: number|undefined }|null}
|
|
28
|
+
*/
|
|
29
|
+
export function readAnchor(envelope) {
|
|
30
|
+
if (!envelope || typeof envelope !== 'object') return null
|
|
31
|
+
|
|
32
|
+
const anchor = envelope.anchor ?? envelope.chainAnchor
|
|
33
|
+
if (!anchor || typeof anchor !== 'object') return null
|
|
34
|
+
if (typeof anchor.txHash !== 'string' || !/^0x[0-9a-fA-F]{64}$/.test(anchor.txHash)) return null
|
|
35
|
+
|
|
36
|
+
return {
|
|
37
|
+
txHash: anchor.txHash,
|
|
38
|
+
blockNumber: anchor.blockNumber,
|
|
39
|
+
// The envelope-level chainId is the current shape. An older envelope with
|
|
40
|
+
// only chainAnchor and no chainId is treated as unstated, not as wrong:
|
|
41
|
+
// it predates the field, and rejecting it would break archives.
|
|
42
|
+
chainId: envelope.chainId ?? anchor.chainId,
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Apply a verification mode.
|
|
48
|
+
*
|
|
49
|
+
* @param {object} opts
|
|
50
|
+
* @param {object} opts.envelope — the envelope being verified
|
|
51
|
+
* @param {boolean} opts.signatureValid — the caller's own signature check
|
|
52
|
+
* @param {string} opts.kid — the signing kid
|
|
53
|
+
* @param {object} opts.config — from networkConfig()
|
|
54
|
+
* @param {KeyRegistry} [opts.registry] — reuse one to share its cache
|
|
55
|
+
* @returns {Promise<{ valid: boolean, mode: string, reason?: string,
|
|
56
|
+
* detail?: string, anchor?: object, registry?: object }>}
|
|
57
|
+
*/
|
|
58
|
+
export async function applyVerifyMode({ envelope, signatureValid, kid, config, registry }) {
|
|
59
|
+
const mode = config.verifyMode
|
|
60
|
+
|
|
61
|
+
if (!signatureValid) {
|
|
62
|
+
return { valid: false, mode, reason: FAILURE.SIGNATURE_INVALID }
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (mode === 'signature') {
|
|
66
|
+
return { valid: true, mode }
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// ── anchored ──────────────────────────────────────────────────────────────
|
|
70
|
+
|
|
71
|
+
const anchor = readAnchor(envelope)
|
|
72
|
+
if (!anchor) {
|
|
73
|
+
return {
|
|
74
|
+
valid: false,
|
|
75
|
+
mode,
|
|
76
|
+
reason: FAILURE.NOT_ANCHORED,
|
|
77
|
+
detail:
|
|
78
|
+
'this envelope carries no Armature L1 anchor. It was signed in signature mode; ' +
|
|
79
|
+
're-issue it with anchor: true, or verify it in signature mode.',
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (anchor.chainId !== undefined && anchor.chainId !== CHAIN_ID) {
|
|
84
|
+
return {
|
|
85
|
+
valid: false,
|
|
86
|
+
mode,
|
|
87
|
+
reason: FAILURE.WRONG_CHAIN,
|
|
88
|
+
detail: `anchor names chain ${anchor.chainId}; this verifier accepts only ${CHAIN_ID} (Armature L1)`,
|
|
89
|
+
anchor,
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (mode === 'anchored') {
|
|
94
|
+
return { valid: true, mode, anchor }
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// ── anchored+live ─────────────────────────────────────────────────────────
|
|
98
|
+
|
|
99
|
+
if (!config.licenceKey) {
|
|
100
|
+
return {
|
|
101
|
+
valid: false,
|
|
102
|
+
mode,
|
|
103
|
+
reason: FAILURE.LICENCE_REQUIRED,
|
|
104
|
+
detail:
|
|
105
|
+
'anchored+live performs a live key-registry lookup and needs a licence key. ' +
|
|
106
|
+
'Set KXCO_LICENCE_KEY, or use anchored, which needs none.',
|
|
107
|
+
anchor,
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const client = registry ?? new KeyRegistry(config)
|
|
112
|
+
|
|
113
|
+
let record
|
|
114
|
+
try {
|
|
115
|
+
record = await client.lookup(kid)
|
|
116
|
+
} catch (err) {
|
|
117
|
+
if (!(err instanceof KxcoPqNetworkError)) throw err
|
|
118
|
+
// Fails closed. The point of this mode is to catch a key that is no longer
|
|
119
|
+
// good; degrading to "probably fine" when the registry is unreachable
|
|
120
|
+
// would remove exactly the protection the customer is paying for, at
|
|
121
|
+
// exactly the moment it is most likely to matter.
|
|
122
|
+
return {
|
|
123
|
+
valid: false,
|
|
124
|
+
mode,
|
|
125
|
+
reason: FAILURE.REGISTRY_UNREACHABLE,
|
|
126
|
+
detail: `${err.message}. anchored+live fails closed; use anchored for an offline answer.`,
|
|
127
|
+
anchor,
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (record.status === 'active') {
|
|
132
|
+
return { valid: true, mode, anchor, registry: record }
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const reason = {
|
|
136
|
+
revoked: FAILURE.KID_REVOKED,
|
|
137
|
+
rotated: FAILURE.KID_ROTATED,
|
|
138
|
+
expired: FAILURE.KID_EXPIRED,
|
|
139
|
+
}[record.status] ?? FAILURE.KID_UNKNOWN
|
|
140
|
+
|
|
141
|
+
return {
|
|
142
|
+
valid: false,
|
|
143
|
+
mode,
|
|
144
|
+
reason,
|
|
145
|
+
detail:
|
|
146
|
+
record.status === 'rotated' && record.rotatedTo
|
|
147
|
+
? `kid ${kid} was rotated to ${record.rotatedTo}`
|
|
148
|
+
: `registry reports kid ${kid} as ${record.status}`,
|
|
149
|
+
anchor,
|
|
150
|
+
registry: record,
|
|
151
|
+
}
|
|
152
|
+
}
|