@motebit/crypto 1.2.0 → 1.2.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/dist/index.js +6732 -1075
- package/dist/suite-dispatch.js +3223 -189
- package/package.json +1 -1
- package/dist/artifacts.js +0 -1158
- package/dist/artifacts.js.map +0 -1
- package/dist/credential-anchor.js +0 -200
- package/dist/credential-anchor.js.map +0 -1
- package/dist/credentials.js +0 -212
- package/dist/credentials.js.map +0 -1
- package/dist/deletion-certificate.js +0 -562
- package/dist/deletion-certificate.js.map +0 -1
- package/dist/hardware-attestation.js +0 -400
- package/dist/hardware-attestation.js.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/merkle.js +0 -84
- package/dist/merkle.js.map +0 -1
- package/dist/signing.js +0 -314
- package/dist/signing.js.map +0 -1
- package/dist/skills.js +0 -228
- package/dist/skills.js.map +0 -1
- package/dist/suite-dispatch.js.map +0 -1
- package/dist/witness-omission-dispute.js +0 -237
- package/dist/witness-omission-dispute.js.map +0 -1
package/dist/artifacts.js
DELETED
|
@@ -1,1158 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Protocol artifact signing — receipts, delegations, successions, collaborative receipts.
|
|
3
|
-
*
|
|
4
|
-
* These functions define the canonical signing format for all Motebit protocol
|
|
5
|
-
* artifacts. A third party needs these to produce valid signed artifacts that
|
|
6
|
-
* any verifier will accept.
|
|
7
|
-
*
|
|
8
|
-
* Moved from BSL @motebit/encryption to the permissive floor in @motebit/crypto (Apache-2.0).
|
|
9
|
-
*/
|
|
10
|
-
import { canonicalJson, canonicalSha256, toBase64Url, fromBase64Url, bytesToHex, hexToBytes, hash, isScopeNarrowed, signBySuite, verifyBySuite, } from "./signing.js";
|
|
11
|
-
/**
|
|
12
|
-
* Diagnostic flag for cryptographic-artifact debugging. Reads from
|
|
13
|
-
* `process.env.DEBUG_RECEIPT_BYTES` in Node and from
|
|
14
|
-
* `globalThis.__motebit_debug_receipt_bytes` in browsers. When truthy,
|
|
15
|
-
* `signExecutionReceipt` and `verifyExecutionReceipt*` log the canonical
|
|
16
|
-
* SHA-256 and a short preview of the canonical JSON, so a verification
|
|
17
|
-
* mismatch can be byte-diffed against the producer's intended bytes
|
|
18
|
-
* without re-instrumenting either end. Off by default; zero overhead when
|
|
19
|
-
* disabled.
|
|
20
|
-
*
|
|
21
|
-
* Pattern source: NIST SP 800-57 §5.4 — minimum observability for any
|
|
22
|
-
* signed-artifact pipeline that crosses a process boundary.
|
|
23
|
-
*/
|
|
24
|
-
function isReceiptDebugEnabled() {
|
|
25
|
-
const g = globalThis;
|
|
26
|
-
if (g.__motebit_debug_receipt_bytes === true)
|
|
27
|
-
return true;
|
|
28
|
-
const flag = g.process?.env?.DEBUG_RECEIPT_BYTES;
|
|
29
|
-
return flag === "1" || flag === "true";
|
|
30
|
-
}
|
|
31
|
-
/** The one suite ExecutionReceipts sign under today. */
|
|
32
|
-
export const EXECUTION_RECEIPT_SUITE = "motebit-jcs-ed25519-b64-v1";
|
|
33
|
-
/**
|
|
34
|
-
* Sign an execution receipt. Stamps the cryptosuite discriminator into
|
|
35
|
-
* the receipt body, canonicalizes with JCS, dispatches the primitive
|
|
36
|
-
* signature through `signBySuite`, and encodes as base64url per the
|
|
37
|
-
* suite's rules.
|
|
38
|
-
*
|
|
39
|
-
* Callers pass a receipt *without* `signature` or `suite`; the signer
|
|
40
|
-
* owns both. The returned object is a full `SignableReceipt` with
|
|
41
|
-
* `suite` and `signature` set.
|
|
42
|
-
*/
|
|
43
|
-
export async function signExecutionReceipt(receipt, privateKey, publicKey) {
|
|
44
|
-
// Embed the public key for portable verification (no relay lookup needed)
|
|
45
|
-
// and stamp the suite into the signed body.
|
|
46
|
-
const withKey = publicKey ? { ...receipt, public_key: bytesToHex(publicKey) } : receipt;
|
|
47
|
-
const body = { ...withKey, suite: EXECUTION_RECEIPT_SUITE };
|
|
48
|
-
const canonical = canonicalJson(body);
|
|
49
|
-
const message = new TextEncoder().encode(canonical);
|
|
50
|
-
const sig = await signBySuite(EXECUTION_RECEIPT_SUITE, message, privateKey);
|
|
51
|
-
const signed = { ...body, signature: toBase64Url(sig) };
|
|
52
|
-
if (isReceiptDebugEnabled()) {
|
|
53
|
-
const sha = await canonicalSha256(body);
|
|
54
|
-
// eslint-disable-next-line no-console -- opt-in diagnostic, off by default
|
|
55
|
-
console.debug(`[motebit/crypto] signExecutionReceipt canonical_sha256=${sha} chain=${Array.isArray(body.delegation_receipts)
|
|
56
|
-
? body.delegation_receipts.length
|
|
57
|
-
: 0} bytes=${canonical.length}`);
|
|
58
|
-
}
|
|
59
|
-
// Freeze the returned signed receipt. Receipts are immutable evidence by
|
|
60
|
-
// contract — the type system already says `readonly` is the intent. Freeze
|
|
61
|
-
// makes the runtime enforce it: any post-sign mutation throws TypeError
|
|
62
|
-
// at the mutation site (Node 20 strict mode, browser strict by default),
|
|
63
|
-
// catching the bug at the producer instead of as wire-corruption noise on
|
|
64
|
-
// the consumer five hops downstream.
|
|
65
|
-
return Object.freeze(signed);
|
|
66
|
-
}
|
|
67
|
-
/**
|
|
68
|
-
* Verify an execution receipt's signature by dispatching through the
|
|
69
|
-
* recipe named in `receipt.suite`. Reconstructs the canonical JSON from
|
|
70
|
-
* all fields except `signature` (the suite IS part of the signed body,
|
|
71
|
-
* so tampering with it breaks verification).
|
|
72
|
-
*
|
|
73
|
-
* Fail-closed on:
|
|
74
|
-
* - unknown suite value (dispatcher rejects)
|
|
75
|
-
* - suite other than `EXECUTION_RECEIPT_SUITE` (until a PQ variant
|
|
76
|
-
* lands in the registry, this narrow check rejects any other
|
|
77
|
-
* value — widens when the union widens)
|
|
78
|
-
* - base64url decode errors
|
|
79
|
-
* - primitive-level verification failure
|
|
80
|
-
*/
|
|
81
|
-
export async function verifyExecutionReceipt(receipt, publicKey) {
|
|
82
|
-
if (receipt.suite !== EXECUTION_RECEIPT_SUITE) {
|
|
83
|
-
if (isReceiptDebugEnabled()) {
|
|
84
|
-
// eslint-disable-next-line no-console -- opt-in diagnostic
|
|
85
|
-
console.debug(`[motebit/crypto] verifyExecutionReceipt EARLY_RETURN suite_mismatch actual=${JSON.stringify(receipt.suite)} expected=${JSON.stringify(EXECUTION_RECEIPT_SUITE)}`);
|
|
86
|
-
}
|
|
87
|
-
return false;
|
|
88
|
-
}
|
|
89
|
-
const { signature, ...body } = receipt;
|
|
90
|
-
const canonical = canonicalJson(body);
|
|
91
|
-
const message = new TextEncoder().encode(canonical);
|
|
92
|
-
let valid = false;
|
|
93
|
-
try {
|
|
94
|
-
const sig = fromBase64Url(signature);
|
|
95
|
-
valid = await verifyBySuite(receipt.suite, message, sig, publicKey);
|
|
96
|
-
}
|
|
97
|
-
catch {
|
|
98
|
-
valid = false;
|
|
99
|
-
}
|
|
100
|
-
if (isReceiptDebugEnabled()) {
|
|
101
|
-
const sha = await canonicalSha256(body);
|
|
102
|
-
// eslint-disable-next-line no-console -- opt-in diagnostic, off by default
|
|
103
|
-
console.debug(`[motebit/crypto] verifyExecutionReceipt canonical_sha256=${sha} valid=${valid} bytes=${canonical.length}`);
|
|
104
|
-
}
|
|
105
|
-
return valid;
|
|
106
|
-
}
|
|
107
|
-
export async function verifyExecutionReceiptDetailed(receipt, publicKey) {
|
|
108
|
-
if (receipt.suite !== EXECUTION_RECEIPT_SUITE) {
|
|
109
|
-
const { signature: _drop, ...bodyForHash } = receipt;
|
|
110
|
-
return {
|
|
111
|
-
valid: false,
|
|
112
|
-
canonical_sha256: await canonicalSha256(bodyForHash),
|
|
113
|
-
canonical_preview: canonicalJson(bodyForHash).slice(0, 256),
|
|
114
|
-
reason: "wrong_suite",
|
|
115
|
-
};
|
|
116
|
-
}
|
|
117
|
-
const { signature, ...body } = receipt;
|
|
118
|
-
const canonical = canonicalJson(body);
|
|
119
|
-
const message = new TextEncoder().encode(canonical);
|
|
120
|
-
let sigBytes;
|
|
121
|
-
try {
|
|
122
|
-
sigBytes = fromBase64Url(signature);
|
|
123
|
-
}
|
|
124
|
-
catch {
|
|
125
|
-
return {
|
|
126
|
-
valid: false,
|
|
127
|
-
canonical_sha256: await hash(message),
|
|
128
|
-
canonical_preview: canonical.slice(0, 256),
|
|
129
|
-
reason: "bad_base64",
|
|
130
|
-
};
|
|
131
|
-
}
|
|
132
|
-
const valid = await verifyBySuite(receipt.suite, message, sigBytes, publicKey);
|
|
133
|
-
return {
|
|
134
|
-
valid,
|
|
135
|
-
canonical_sha256: await hash(message),
|
|
136
|
-
canonical_preview: canonical.slice(0, 256),
|
|
137
|
-
reason: valid ? "ok" : "ed25519_mismatch",
|
|
138
|
-
};
|
|
139
|
-
}
|
|
140
|
-
/** The one suite ToolInvocationReceipts sign under today. */
|
|
141
|
-
export const TOOL_INVOCATION_RECEIPT_SUITE = "motebit-jcs-ed25519-b64-v1";
|
|
142
|
-
/**
|
|
143
|
-
* Compute the `args_hash` / `result_hash` for a tool-invocation receipt.
|
|
144
|
-
* JCS-canonicalizes the value, then SHA-256s the UTF-8 bytes. Returns
|
|
145
|
-
* hex. Use on both sides of the wire: the producer computes the hash at
|
|
146
|
-
* sign time; a verifier with the raw value recomputes and matches.
|
|
147
|
-
*
|
|
148
|
-
* For `string` values (e.g., a plain result string), the canonicalization
|
|
149
|
-
* is the value itself wrapped with JSON escaping rules; `canonicalJson`
|
|
150
|
-
* handles both scalar and object inputs uniformly.
|
|
151
|
-
*/
|
|
152
|
-
export async function hashToolPayload(value) {
|
|
153
|
-
return canonicalSha256(value);
|
|
154
|
-
}
|
|
155
|
-
/**
|
|
156
|
-
* Sign a tool-invocation receipt. Mirrors `signExecutionReceipt`:
|
|
157
|
-
* stamps the cryptosuite into the body, canonicalizes with JCS,
|
|
158
|
-
* dispatches through `signBySuite`, and encodes as base64url.
|
|
159
|
-
*
|
|
160
|
-
* Callers pass a receipt *without* `signature` or `suite`; the signer
|
|
161
|
-
* owns both. Also embeds the public key (hex) so the receipt is
|
|
162
|
-
* independently verifiable with no relay lookup.
|
|
163
|
-
*/
|
|
164
|
-
export async function signToolInvocationReceipt(receipt, privateKey, publicKey) {
|
|
165
|
-
const withKey = publicKey ? { ...receipt, public_key: bytesToHex(publicKey) } : receipt;
|
|
166
|
-
const body = { ...withKey, suite: TOOL_INVOCATION_RECEIPT_SUITE };
|
|
167
|
-
const canonical = canonicalJson(body);
|
|
168
|
-
const message = new TextEncoder().encode(canonical);
|
|
169
|
-
const sig = await signBySuite(TOOL_INVOCATION_RECEIPT_SUITE, message, privateKey);
|
|
170
|
-
const signed = { ...body, signature: toBase64Url(sig) };
|
|
171
|
-
if (isReceiptDebugEnabled()) {
|
|
172
|
-
const sha = await canonicalSha256(body);
|
|
173
|
-
// eslint-disable-next-line no-console -- opt-in diagnostic, off by default
|
|
174
|
-
console.debug(`[motebit/crypto] signToolInvocationReceipt canonical_sha256=${sha} tool=${body.tool_name} bytes=${canonical.length}`);
|
|
175
|
-
}
|
|
176
|
-
return Object.freeze(signed);
|
|
177
|
-
}
|
|
178
|
-
/**
|
|
179
|
-
* Verify a tool-invocation receipt. Fails closed on unknown suite, bad
|
|
180
|
-
* base64, or signature mismatch — same rules as `verifyExecutionReceipt`.
|
|
181
|
-
*/
|
|
182
|
-
export async function verifyToolInvocationReceipt(receipt, publicKey) {
|
|
183
|
-
if (receipt.suite !== TOOL_INVOCATION_RECEIPT_SUITE) {
|
|
184
|
-
if (isReceiptDebugEnabled()) {
|
|
185
|
-
// eslint-disable-next-line no-console -- opt-in diagnostic
|
|
186
|
-
console.debug(`[motebit/crypto] verifyToolInvocationReceipt EARLY_RETURN suite_mismatch actual=${JSON.stringify(receipt.suite)} expected=${JSON.stringify(TOOL_INVOCATION_RECEIPT_SUITE)}`);
|
|
187
|
-
}
|
|
188
|
-
return false;
|
|
189
|
-
}
|
|
190
|
-
const { signature, ...body } = receipt;
|
|
191
|
-
const canonical = canonicalJson(body);
|
|
192
|
-
const message = new TextEncoder().encode(canonical);
|
|
193
|
-
let valid = false;
|
|
194
|
-
try {
|
|
195
|
-
const sig = fromBase64Url(signature);
|
|
196
|
-
valid = await verifyBySuite(receipt.suite, message, sig, publicKey);
|
|
197
|
-
}
|
|
198
|
-
catch {
|
|
199
|
-
valid = false;
|
|
200
|
-
}
|
|
201
|
-
if (isReceiptDebugEnabled()) {
|
|
202
|
-
const sha = await canonicalSha256(body);
|
|
203
|
-
// eslint-disable-next-line no-console -- opt-in diagnostic, off by default
|
|
204
|
-
console.debug(`[motebit/crypto] verifyToolInvocationReceipt canonical_sha256=${sha} valid=${valid} bytes=${canonical.length}`);
|
|
205
|
-
}
|
|
206
|
-
return valid;
|
|
207
|
-
}
|
|
208
|
-
/**
|
|
209
|
-
* Construct, canonicalize, and sign a sovereign payment receipt with
|
|
210
|
-
* the payee's Ed25519 identity key. Returns a fully-formed
|
|
211
|
-
* `ExecutionReceipt` that can be passed to any standard verifier and
|
|
212
|
-
* fed into `bumpTrustFromReceipt` on the payer's runtime.
|
|
213
|
-
*
|
|
214
|
-
* No relay is contacted at any point. The resulting receipt is
|
|
215
|
-
* self-verifiable forever from the embedded `public_key` field.
|
|
216
|
-
*/
|
|
217
|
-
export async function signSovereignPaymentReceipt(input, privateKey, publicKey) {
|
|
218
|
-
const receipt = {
|
|
219
|
-
task_id: `${input.rail}:tx:${input.tx_hash}`,
|
|
220
|
-
motebit_id: input.payee_motebit_id,
|
|
221
|
-
device_id: input.payee_device_id,
|
|
222
|
-
submitted_at: input.submitted_at,
|
|
223
|
-
completed_at: input.completed_at,
|
|
224
|
-
status: "completed",
|
|
225
|
-
result: `${input.service_description} | paid by ${input.payer_motebit_id}: ${input.amount_micro.toString()} micro-${input.asset} via ${input.rail}`,
|
|
226
|
-
tools_used: input.tools_used ?? [],
|
|
227
|
-
memories_formed: 0,
|
|
228
|
-
prompt_hash: input.prompt_hash,
|
|
229
|
-
result_hash: input.result_hash,
|
|
230
|
-
// relay_task_id intentionally omitted — sovereign rail, no relay binding
|
|
231
|
-
// suite is stamped by signExecutionReceipt
|
|
232
|
-
};
|
|
233
|
-
return signExecutionReceipt(receipt, privateKey, publicKey);
|
|
234
|
-
}
|
|
235
|
-
/**
|
|
236
|
-
* Recursively verify an execution receipt and all its delegation receipts.
|
|
237
|
-
* Each receipt is verified against the public key found in `knownKeys` for its `motebit_id`.
|
|
238
|
-
* Returns a tree of verification results mirroring the delegation structure.
|
|
239
|
-
*/
|
|
240
|
-
export async function verifyReceiptChain(receipt, knownKeys) {
|
|
241
|
-
const { task_id, motebit_id } = receipt;
|
|
242
|
-
// Use embedded public key if available, otherwise look up from known keys.
|
|
243
|
-
let publicKey = knownKeys.get(motebit_id);
|
|
244
|
-
if (!publicKey && receipt.public_key) {
|
|
245
|
-
publicKey = hexToBytes(receipt.public_key);
|
|
246
|
-
}
|
|
247
|
-
if (!publicKey) {
|
|
248
|
-
const delegations = await verifyDelegations(receipt, knownKeys);
|
|
249
|
-
return { task_id, motebit_id, verified: false, error: "unknown motebit_id", delegations };
|
|
250
|
-
}
|
|
251
|
-
let verified;
|
|
252
|
-
let error;
|
|
253
|
-
try {
|
|
254
|
-
verified = await verifyExecutionReceipt(receipt, publicKey);
|
|
255
|
-
}
|
|
256
|
-
catch (err) {
|
|
257
|
-
/* v8 ignore next 3 */
|
|
258
|
-
verified = false;
|
|
259
|
-
error = err instanceof Error ? err.message : String(err);
|
|
260
|
-
}
|
|
261
|
-
const delegations = await verifyDelegations(receipt, knownKeys);
|
|
262
|
-
const result = { task_id, motebit_id, verified, delegations };
|
|
263
|
-
if (error) {
|
|
264
|
-
/* v8 ignore next */
|
|
265
|
-
result.error = error;
|
|
266
|
-
}
|
|
267
|
-
return result;
|
|
268
|
-
}
|
|
269
|
-
async function verifyDelegations(receipt, knownKeys) {
|
|
270
|
-
if (!receipt.delegation_receipts || receipt.delegation_receipts.length === 0) {
|
|
271
|
-
return [];
|
|
272
|
-
}
|
|
273
|
-
return Promise.all(receipt.delegation_receipts.map((dr) => verifyReceiptChain(dr, knownKeys)));
|
|
274
|
-
}
|
|
275
|
-
/**
|
|
276
|
-
* Verify a flat sequence of execution receipts.
|
|
277
|
-
*
|
|
278
|
-
* A valid sequence means:
|
|
279
|
-
* 1. Each receipt's signature is valid against its signer's public key.
|
|
280
|
-
* 2. Adjacent receipts are temporally ordered: receipt[i].completed_at <= receipt[i+1].submitted_at.
|
|
281
|
-
*
|
|
282
|
-
* An empty sequence is considered valid.
|
|
283
|
-
* Use `verifyReceiptChain` for nested/tree-structured delegation receipts.
|
|
284
|
-
*/
|
|
285
|
-
export async function verifyReceiptSequence(chain) {
|
|
286
|
-
if (chain.length === 0)
|
|
287
|
-
return { valid: true };
|
|
288
|
-
for (let i = 0; i < chain.length; i++) {
|
|
289
|
-
const entry = chain[i];
|
|
290
|
-
const sigValid = await verifyExecutionReceipt(entry.receipt, entry.signer_public_key);
|
|
291
|
-
if (!sigValid) {
|
|
292
|
-
return { valid: false, error: `Receipt ${i} has invalid signature`, index: i };
|
|
293
|
-
}
|
|
294
|
-
}
|
|
295
|
-
for (let i = 1; i < chain.length; i++) {
|
|
296
|
-
const prev = chain[i - 1];
|
|
297
|
-
const curr = chain[i];
|
|
298
|
-
if (prev.receipt.completed_at > curr.receipt.submitted_at) {
|
|
299
|
-
return {
|
|
300
|
-
valid: false,
|
|
301
|
-
error: `Receipt ${i} submitted_at (${curr.receipt.submitted_at}) is before receipt ${i - 1} completed_at (${prev.receipt.completed_at})`,
|
|
302
|
-
index: i,
|
|
303
|
-
};
|
|
304
|
-
}
|
|
305
|
-
}
|
|
306
|
-
return { valid: true };
|
|
307
|
-
}
|
|
308
|
-
/** The one suite DelegationTokens sign under today. */
|
|
309
|
-
export const DELEGATION_TOKEN_SUITE = "motebit-jcs-ed25519-b64-v1";
|
|
310
|
-
/**
|
|
311
|
-
* Sign a delegation token. The delegator authorizes the delegate to act
|
|
312
|
-
* within the given scope. Stamps the cryptosuite into the signed body,
|
|
313
|
-
* dispatches the primitive signature through `signBySuite`.
|
|
314
|
-
*
|
|
315
|
-
* Callers pass the token without `signature` or `suite`; the signer owns
|
|
316
|
-
* both. Public keys must already be hex-encoded — this signer does not
|
|
317
|
-
* transcode, so the input carries the same encoding the output will.
|
|
318
|
-
*/
|
|
319
|
-
export async function signDelegation(delegation, delegatorPrivateKey) {
|
|
320
|
-
const body = { ...delegation, suite: DELEGATION_TOKEN_SUITE };
|
|
321
|
-
const canonical = canonicalJson(body);
|
|
322
|
-
const message = new TextEncoder().encode(canonical);
|
|
323
|
-
const sig = await signBySuite(DELEGATION_TOKEN_SUITE, message, delegatorPrivateKey);
|
|
324
|
-
return { ...body, signature: toBase64Url(sig) };
|
|
325
|
-
}
|
|
326
|
-
/**
|
|
327
|
-
* Verify a delegation token's signature and (optionally) expiration.
|
|
328
|
-
*
|
|
329
|
-
* Rejects fail-closed on:
|
|
330
|
-
* - missing or unknown `suite` value (anything other than `DELEGATION_TOKEN_SUITE`)
|
|
331
|
-
* - expired token (unless `options.checkExpiry === false`)
|
|
332
|
-
* - malformed hex public key or base64url signature
|
|
333
|
-
* - primitive-level verification failure
|
|
334
|
-
*
|
|
335
|
-
* @param delegation - The delegation token to verify
|
|
336
|
-
* @param options.checkExpiry - If true (default), reject expired tokens. Pass false
|
|
337
|
-
* only when verifying historical chains where expiration is irrelevant.
|
|
338
|
-
* @param options.now - Current time in ms (default: Date.now()). For testing.
|
|
339
|
-
*/
|
|
340
|
-
export async function verifyDelegation(delegation, options) {
|
|
341
|
-
if (delegation.suite !== DELEGATION_TOKEN_SUITE)
|
|
342
|
-
return false;
|
|
343
|
-
const checkExpiry = options?.checkExpiry ?? true;
|
|
344
|
-
if (checkExpiry) {
|
|
345
|
-
const now = options?.now ?? Date.now();
|
|
346
|
-
if (delegation.expires_at < now)
|
|
347
|
-
return false;
|
|
348
|
-
}
|
|
349
|
-
const { signature, ...body } = delegation;
|
|
350
|
-
const canonical = canonicalJson(body);
|
|
351
|
-
const message = new TextEncoder().encode(canonical);
|
|
352
|
-
try {
|
|
353
|
-
const pubKey = hexToBytes(delegation.delegator_public_key);
|
|
354
|
-
const sig = fromBase64Url(signature);
|
|
355
|
-
return await verifyBySuite(delegation.suite, message, sig, pubKey);
|
|
356
|
-
}
|
|
357
|
-
catch {
|
|
358
|
-
return false;
|
|
359
|
-
}
|
|
360
|
-
}
|
|
361
|
-
/**
|
|
362
|
-
* Verify a chain of delegation tokens.
|
|
363
|
-
*
|
|
364
|
-
* A valid chain means:
|
|
365
|
-
* 1. Each delegation's signature is valid (signed by the delegator's key).
|
|
366
|
-
* 2. Adjacent delegations are linked: delegation[i].delegate_id === delegation[i+1].delegator_id
|
|
367
|
-
* and delegation[i].delegate_public_key === delegation[i+1].delegator_public_key.
|
|
368
|
-
*
|
|
369
|
-
* An empty chain is considered valid (no delegations to verify).
|
|
370
|
-
*/
|
|
371
|
-
export async function verifyDelegationChain(chain) {
|
|
372
|
-
if (chain.length === 0)
|
|
373
|
-
return { valid: true };
|
|
374
|
-
for (let i = 0; i < chain.length; i++) {
|
|
375
|
-
const delegation = chain[i];
|
|
376
|
-
// Chain verification is historical — don't reject expired tokens in the chain
|
|
377
|
-
const sigValid = await verifyDelegation(delegation, { checkExpiry: false });
|
|
378
|
-
if (!sigValid) {
|
|
379
|
-
return { valid: false, error: `Delegation ${i} has invalid signature` };
|
|
380
|
-
}
|
|
381
|
-
if (i > 0) {
|
|
382
|
-
const prev = chain[i - 1];
|
|
383
|
-
if (prev.delegate_id !== delegation.delegator_id) {
|
|
384
|
-
return {
|
|
385
|
-
valid: false,
|
|
386
|
-
error: `Chain break at ${i}: delegate_id "${prev.delegate_id}" !== delegator_id "${delegation.delegator_id}"`,
|
|
387
|
-
};
|
|
388
|
-
}
|
|
389
|
-
if (prev.delegate_public_key !== delegation.delegator_public_key) {
|
|
390
|
-
return {
|
|
391
|
-
valid: false,
|
|
392
|
-
error: `Chain break at ${i}: delegate_public_key mismatch`,
|
|
393
|
-
};
|
|
394
|
-
}
|
|
395
|
-
// Scope narrowing: each delegation must not widen scope beyond its parent
|
|
396
|
-
if (!isScopeNarrowed(prev.scope, delegation.scope)) {
|
|
397
|
-
return {
|
|
398
|
-
valid: false,
|
|
399
|
-
error: `Delegation ${i} widens scope: parent="${prev.scope}", child="${delegation.scope}"`,
|
|
400
|
-
};
|
|
401
|
-
}
|
|
402
|
-
}
|
|
403
|
-
}
|
|
404
|
-
return { valid: true };
|
|
405
|
-
}
|
|
406
|
-
/** The one suite AdjudicatorVotes sign under today — matches spec/dispute-v1.md §6.4. */
|
|
407
|
-
export const ADJUDICATOR_VOTE_SUITE = "motebit-jcs-ed25519-b64-v1";
|
|
408
|
-
/** The one suite DisputeResolutions sign under today — matches spec/dispute-v1.md §6.4. */
|
|
409
|
-
export const DISPUTE_RESOLUTION_SUITE = "motebit-jcs-ed25519-b64-v1";
|
|
410
|
-
/** The one suite DisputeRequest filings sign under today — spec/dispute-v1.md §4.2. */
|
|
411
|
-
export const DISPUTE_REQUEST_SUITE = "motebit-jcs-ed25519-b64-v1";
|
|
412
|
-
/** The one suite DisputeEvidence submissions sign under today — spec/dispute-v1.md §5.2. */
|
|
413
|
-
export const DISPUTE_EVIDENCE_SUITE = "motebit-jcs-ed25519-b64-v1";
|
|
414
|
-
/** The one suite DisputeAppeal filings sign under today — spec/dispute-v1.md §8.2. */
|
|
415
|
-
export const DISPUTE_APPEAL_SUITE = "motebit-jcs-ed25519-b64-v1";
|
|
416
|
-
/**
|
|
417
|
-
* Sign a federation peer's adjudication vote. The `dispute_id` IS part
|
|
418
|
-
* of the signed body — spec §6.5 Foundation Law: "Each AdjudicatorVote
|
|
419
|
-
* signature MUST cover its `dispute_id`. Votes are not portable across
|
|
420
|
-
* disputes — a malicious adjudicator collecting old votes from other
|
|
421
|
-
* disputes cannot stuff them into a new resolution because the
|
|
422
|
-
* dispute_id binding breaks the signature."
|
|
423
|
-
*
|
|
424
|
-
* Callers pass the body without `signature` or `suite`; the signer owns
|
|
425
|
-
* both.
|
|
426
|
-
*/
|
|
427
|
-
export async function signAdjudicatorVote(vote, peerPrivateKey) {
|
|
428
|
-
const body = { ...vote, suite: ADJUDICATOR_VOTE_SUITE };
|
|
429
|
-
const canonical = canonicalJson(body);
|
|
430
|
-
const message = new TextEncoder().encode(canonical);
|
|
431
|
-
const sig = await signBySuite(ADJUDICATOR_VOTE_SUITE, message, peerPrivateKey);
|
|
432
|
-
return { ...body, signature: toBase64Url(sig) };
|
|
433
|
-
}
|
|
434
|
-
/**
|
|
435
|
-
* Verify an adjudicator vote against the voting peer's public key.
|
|
436
|
-
* Fail-closed on unknown suite, base64url decode error, and primitive
|
|
437
|
-
* verification failure. Matching of `peer_id` to a legitimate federation
|
|
438
|
-
* peer is the caller's responsibility (this function verifies the
|
|
439
|
-
* signature; peer-membership is a trust decision).
|
|
440
|
-
*/
|
|
441
|
-
export async function verifyAdjudicatorVote(vote, peerPublicKey) {
|
|
442
|
-
if (vote.suite !== ADJUDICATOR_VOTE_SUITE)
|
|
443
|
-
return false;
|
|
444
|
-
const { signature, ...body } = vote;
|
|
445
|
-
const canonical = canonicalJson(body);
|
|
446
|
-
const message = new TextEncoder().encode(canonical);
|
|
447
|
-
try {
|
|
448
|
-
const sig = fromBase64Url(signature);
|
|
449
|
-
return await verifyBySuite(vote.suite, message, sig, peerPublicKey);
|
|
450
|
-
}
|
|
451
|
-
catch {
|
|
452
|
-
return false;
|
|
453
|
-
}
|
|
454
|
-
}
|
|
455
|
-
/**
|
|
456
|
-
* Sign a dispute resolution. For single-relay adjudication
|
|
457
|
-
* (`adjudicator_votes: []`) the relay signs with its own identity key.
|
|
458
|
-
* For federation resolutions, the leader collects signed
|
|
459
|
-
* `AdjudicatorVote` entries, then signs the aggregate.
|
|
460
|
-
*
|
|
461
|
-
* Callers pass the body without `signature` or `suite`; the signer
|
|
462
|
-
* owns both.
|
|
463
|
-
*
|
|
464
|
-
* Per spec §6.5 Foundation Law, a federation resolution MUST include
|
|
465
|
-
* individual `AdjudicatorVote` entries — aggregated-only verdicts are
|
|
466
|
-
* rejected. This signer does not enforce that at sign time (the
|
|
467
|
-
* orchestrator decides whether federation is required); the verifier
|
|
468
|
-
* re-checks every embedded vote signature when the array is non-empty.
|
|
469
|
-
*/
|
|
470
|
-
export async function signDisputeResolution(resolution, adjudicatorPrivateKey) {
|
|
471
|
-
const body = { ...resolution, suite: DISPUTE_RESOLUTION_SUITE };
|
|
472
|
-
const canonical = canonicalJson(body);
|
|
473
|
-
const message = new TextEncoder().encode(canonical);
|
|
474
|
-
const sig = await signBySuite(DISPUTE_RESOLUTION_SUITE, message, adjudicatorPrivateKey);
|
|
475
|
-
return { ...body, signature: toBase64Url(sig) };
|
|
476
|
-
}
|
|
477
|
-
/**
|
|
478
|
-
* Verify a dispute resolution. Two layers:
|
|
479
|
-
* 1. Outer signature verifies against `adjudicatorPublicKey`.
|
|
480
|
-
* 2. When `adjudicator_votes.length > 0`, every embedded
|
|
481
|
-
* AdjudicatorVote's signature is re-checked against the
|
|
482
|
-
* corresponding `peerKeys` entry (lookup by `peer_id`). Per §6.5,
|
|
483
|
-
* aggregated-only verdicts without individual peer signatures are
|
|
484
|
-
* rejected — a missing peer key in the lookup is treated as a
|
|
485
|
-
* verification failure.
|
|
486
|
-
*
|
|
487
|
-
* Fail-closed on unknown suite, decode errors, primitive verification
|
|
488
|
-
* failures, any missing peer key, and any invalid embedded vote.
|
|
489
|
-
*/
|
|
490
|
-
export async function verifyDisputeResolution(resolution, adjudicatorPublicKey, peerKeys) {
|
|
491
|
-
if (resolution.suite !== DISPUTE_RESOLUTION_SUITE)
|
|
492
|
-
return false;
|
|
493
|
-
const { signature, ...body } = resolution;
|
|
494
|
-
const canonical = canonicalJson(body);
|
|
495
|
-
const message = new TextEncoder().encode(canonical);
|
|
496
|
-
try {
|
|
497
|
-
const sig = fromBase64Url(signature);
|
|
498
|
-
const outerValid = await verifyBySuite(resolution.suite, message, sig, adjudicatorPublicKey);
|
|
499
|
-
if (!outerValid)
|
|
500
|
-
return false;
|
|
501
|
-
}
|
|
502
|
-
catch {
|
|
503
|
-
return false;
|
|
504
|
-
}
|
|
505
|
-
// Federation resolutions must carry signed peer votes. Verify every
|
|
506
|
-
// one against the caller-supplied peer-key map. Missing map or
|
|
507
|
-
// missing peer entry is a verification failure, not a pass-through.
|
|
508
|
-
if (resolution.adjudicator_votes.length > 0) {
|
|
509
|
-
if (!peerKeys)
|
|
510
|
-
return false;
|
|
511
|
-
for (const vote of resolution.adjudicator_votes) {
|
|
512
|
-
if (vote.dispute_id !== resolution.dispute_id)
|
|
513
|
-
return false;
|
|
514
|
-
const peerKey = peerKeys.get(vote.peer_id);
|
|
515
|
-
if (!peerKey)
|
|
516
|
-
return false;
|
|
517
|
-
const voteValid = await verifyAdjudicatorVote(vote, peerKey);
|
|
518
|
-
if (!voteValid)
|
|
519
|
-
return false;
|
|
520
|
-
}
|
|
521
|
-
}
|
|
522
|
-
return true;
|
|
523
|
-
}
|
|
524
|
-
/**
|
|
525
|
-
* Sign a DisputeRequest. Filing party signs over canonical JSON of
|
|
526
|
-
* every field except `signature`. The relay verifies against the
|
|
527
|
-
* filer's registered public key before accepting the filing — without
|
|
528
|
-
* the signature, anyone could file a dispute as anyone (foundation
|
|
529
|
-
* law §4.4: filing party must be a direct party to the task; without
|
|
530
|
-
* the signature binding, the relay cannot enforce that). Callers pass
|
|
531
|
-
* the body without `signature` or `suite`; the signer owns both.
|
|
532
|
-
*/
|
|
533
|
-
export async function signDisputeRequest(request, filerPrivateKey) {
|
|
534
|
-
const body = { ...request, suite: DISPUTE_REQUEST_SUITE };
|
|
535
|
-
const canonical = canonicalJson(body);
|
|
536
|
-
const message = new TextEncoder().encode(canonical);
|
|
537
|
-
const sig = await signBySuite(DISPUTE_REQUEST_SUITE, message, filerPrivateKey);
|
|
538
|
-
return { ...body, signature: toBase64Url(sig) };
|
|
539
|
-
}
|
|
540
|
-
/**
|
|
541
|
-
* Verify a DisputeRequest against the filing party's public key.
|
|
542
|
-
* Fail-closed on unknown suite, base64url decode error, and primitive
|
|
543
|
-
* verification failure. Eligibility checks (`filed_by` is a real party
|
|
544
|
-
* to `task_id`, trust threshold, evidence_refs non-empty) are the
|
|
545
|
-
* caller's responsibility — this verifies the signature only.
|
|
546
|
-
*/
|
|
547
|
-
export async function verifyDisputeRequest(request, filerPublicKey) {
|
|
548
|
-
if (request.suite !== DISPUTE_REQUEST_SUITE)
|
|
549
|
-
return false;
|
|
550
|
-
const { signature, ...body } = request;
|
|
551
|
-
const canonical = canonicalJson(body);
|
|
552
|
-
const message = new TextEncoder().encode(canonical);
|
|
553
|
-
try {
|
|
554
|
-
const sig = fromBase64Url(signature);
|
|
555
|
-
return await verifyBySuite(request.suite, message, sig, filerPublicKey);
|
|
556
|
-
}
|
|
557
|
-
catch {
|
|
558
|
-
return false;
|
|
559
|
-
}
|
|
560
|
-
}
|
|
561
|
-
/**
|
|
562
|
-
* Sign a DisputeEvidence submission. The submitting party — either
|
|
563
|
-
* the dispute's filer or respondent — signs over the canonical JSON
|
|
564
|
-
* of every field except `signature`. The relay verifies against the
|
|
565
|
-
* submitter's registered public key (foundation law §5.4: evidence
|
|
566
|
-
* must be cryptographically verifiable; unsigned/tampered evidence
|
|
567
|
-
* is rejected).
|
|
568
|
-
*/
|
|
569
|
-
export async function signDisputeEvidence(evidence, submitterPrivateKey) {
|
|
570
|
-
const body = { ...evidence, suite: DISPUTE_EVIDENCE_SUITE };
|
|
571
|
-
const canonical = canonicalJson(body);
|
|
572
|
-
const message = new TextEncoder().encode(canonical);
|
|
573
|
-
const sig = await signBySuite(DISPUTE_EVIDENCE_SUITE, message, submitterPrivateKey);
|
|
574
|
-
return { ...body, signature: toBase64Url(sig) };
|
|
575
|
-
}
|
|
576
|
-
/**
|
|
577
|
-
* Verify a DisputeEvidence submission against the submitting party's
|
|
578
|
-
* public key. Inner `evidence_data` validation against its own per-
|
|
579
|
-
* type schema (e.g. ExecutionReceiptSchema for `execution_receipt`)
|
|
580
|
-
* is the adjudicator's responsibility — this verifies the outer
|
|
581
|
-
* envelope signature only.
|
|
582
|
-
*/
|
|
583
|
-
export async function verifyDisputeEvidence(evidence, submitterPublicKey) {
|
|
584
|
-
if (evidence.suite !== DISPUTE_EVIDENCE_SUITE)
|
|
585
|
-
return false;
|
|
586
|
-
const { signature, ...body } = evidence;
|
|
587
|
-
const canonical = canonicalJson(body);
|
|
588
|
-
const message = new TextEncoder().encode(canonical);
|
|
589
|
-
try {
|
|
590
|
-
const sig = fromBase64Url(signature);
|
|
591
|
-
return await verifyBySuite(evidence.suite, message, sig, submitterPublicKey);
|
|
592
|
-
}
|
|
593
|
-
catch {
|
|
594
|
-
return false;
|
|
595
|
-
}
|
|
596
|
-
}
|
|
597
|
-
/**
|
|
598
|
-
* Sign a DisputeAppeal. The appealing party — filer or respondent —
|
|
599
|
-
* signs over the canonical JSON of every field except `signature`.
|
|
600
|
-
* Foundation law §8.4: one appeal per dispute; the post-appeal state
|
|
601
|
-
* is terminal. The relay verifies against the appealer's registered
|
|
602
|
-
* public key before transitioning the dispute to `appealed`.
|
|
603
|
-
*/
|
|
604
|
-
export async function signDisputeAppeal(appeal, appealerPrivateKey) {
|
|
605
|
-
const body = { ...appeal, suite: DISPUTE_APPEAL_SUITE };
|
|
606
|
-
const canonical = canonicalJson(body);
|
|
607
|
-
const message = new TextEncoder().encode(canonical);
|
|
608
|
-
const sig = await signBySuite(DISPUTE_APPEAL_SUITE, message, appealerPrivateKey);
|
|
609
|
-
return { ...body, signature: toBase64Url(sig) };
|
|
610
|
-
}
|
|
611
|
-
/**
|
|
612
|
-
* Verify a DisputeAppeal against the appealing party's public key.
|
|
613
|
-
* Fail-closed on unknown suite, base64url decode error, and primitive
|
|
614
|
-
* verification failure.
|
|
615
|
-
*/
|
|
616
|
-
export async function verifyDisputeAppeal(appeal, appealerPublicKey) {
|
|
617
|
-
if (appeal.suite !== DISPUTE_APPEAL_SUITE)
|
|
618
|
-
return false;
|
|
619
|
-
const { signature, ...body } = appeal;
|
|
620
|
-
const canonical = canonicalJson(body);
|
|
621
|
-
const message = new TextEncoder().encode(canonical);
|
|
622
|
-
try {
|
|
623
|
-
const sig = fromBase64Url(signature);
|
|
624
|
-
return await verifyBySuite(appeal.suite, message, sig, appealerPublicKey);
|
|
625
|
-
}
|
|
626
|
-
catch {
|
|
627
|
-
return false;
|
|
628
|
-
}
|
|
629
|
-
}
|
|
630
|
-
/** The one suite ConsolidationReceipts sign under today. */
|
|
631
|
-
export const CONSOLIDATION_RECEIPT_SUITE = "motebit-jcs-ed25519-b64-v1";
|
|
632
|
-
/**
|
|
633
|
-
* Sign a consolidation receipt. The motebit's Ed25519 identity key
|
|
634
|
-
* commits to the structural counts of work performed during a
|
|
635
|
-
* consolidation cycle. Receipt is self-attesting: any holder of the
|
|
636
|
-
* signer's public key verifies without contacting any relay.
|
|
637
|
-
*
|
|
638
|
-
* Callers pass the body without `signature` or `suite`; the signer
|
|
639
|
-
* owns both. Pass `publicKey` to embed it in the receipt for portable
|
|
640
|
-
* verification (recommended — third parties verify from the receipt
|
|
641
|
-
* alone).
|
|
642
|
-
*
|
|
643
|
-
* The signed receipt is `Object.freeze`d before return so any
|
|
644
|
-
* post-sign mutation throws synchronously at the producer instead of
|
|
645
|
-
* surfacing as wire-corruption noise on a downstream verifier.
|
|
646
|
-
*/
|
|
647
|
-
export async function signConsolidationReceipt(receipt, privateKey, publicKey) {
|
|
648
|
-
const withKey = publicKey
|
|
649
|
-
? { ...receipt, public_key: bytesToHex(publicKey) }
|
|
650
|
-
: receipt;
|
|
651
|
-
const body = { ...withKey, suite: CONSOLIDATION_RECEIPT_SUITE };
|
|
652
|
-
const canonical = canonicalJson(body);
|
|
653
|
-
const message = new TextEncoder().encode(canonical);
|
|
654
|
-
const sig = await signBySuite(CONSOLIDATION_RECEIPT_SUITE, message, privateKey);
|
|
655
|
-
return Object.freeze({ ...body, signature: toBase64Url(sig) });
|
|
656
|
-
}
|
|
657
|
-
/**
|
|
658
|
-
* Verify a consolidation receipt against the signer's public key.
|
|
659
|
-
* Fail-closed on unknown `suite`, base64url decode error, primitive
|
|
660
|
-
* verification failure. The caller is responsible for matching
|
|
661
|
-
* `motebit_id` to whoever they expect signed; the cryptographic
|
|
662
|
-
* property here is "this body was signed by the holder of this key."
|
|
663
|
-
*/
|
|
664
|
-
export async function verifyConsolidationReceipt(receipt, publicKey) {
|
|
665
|
-
if (receipt.suite !== CONSOLIDATION_RECEIPT_SUITE)
|
|
666
|
-
return false;
|
|
667
|
-
const { signature, ...body } = receipt;
|
|
668
|
-
const canonical = canonicalJson(body);
|
|
669
|
-
const message = new TextEncoder().encode(canonical);
|
|
670
|
-
try {
|
|
671
|
-
const sig = fromBase64Url(signature);
|
|
672
|
-
return await verifyBySuite(receipt.suite, message, sig, publicKey);
|
|
673
|
-
}
|
|
674
|
-
catch {
|
|
675
|
-
return false;
|
|
676
|
-
}
|
|
677
|
-
}
|
|
678
|
-
/** The one suite BalanceWaivers sign under today — matches spec/migration-v1.md §7.2. */
|
|
679
|
-
export const BALANCE_WAIVER_SUITE = "motebit-jcs-ed25519-b64-v1";
|
|
680
|
-
/**
|
|
681
|
-
* Sign a balance waiver. The agent forfeits a named micro-unit amount to
|
|
682
|
-
* expedite departure from a relay (spec/migration-v1.md §7.2 + §7.3 — a
|
|
683
|
-
* waiver is one of the two terminal authorizations the depart route will
|
|
684
|
-
* accept, the other being a confirmed withdrawal).
|
|
685
|
-
*
|
|
686
|
-
* Callers pass the body without `signature` or `suite`; the signer owns
|
|
687
|
-
* both. The agent's identity key signs canonical JSON of the unsigned
|
|
688
|
-
* body (with `suite` stamped in), base64url-encoded.
|
|
689
|
-
*/
|
|
690
|
-
export async function signBalanceWaiver(waiver, agentPrivateKey) {
|
|
691
|
-
const body = { ...waiver, suite: BALANCE_WAIVER_SUITE };
|
|
692
|
-
const canonical = canonicalJson(body);
|
|
693
|
-
const message = new TextEncoder().encode(canonical);
|
|
694
|
-
const sig = await signBySuite(BALANCE_WAIVER_SUITE, message, agentPrivateKey);
|
|
695
|
-
return { ...body, signature: toBase64Url(sig) };
|
|
696
|
-
}
|
|
697
|
-
/**
|
|
698
|
-
* Verify a balance waiver against the agent's public key. Rejects
|
|
699
|
-
* fail-closed on unknown `suite`, base64url decode error, and primitive
|
|
700
|
-
* verification failure. Matching of `motebit_id` to the authorizing
|
|
701
|
-
* agent, and `waived_amount` to the actual virtual-account balance, is
|
|
702
|
-
* the caller's responsibility (neither is a cryptographic property).
|
|
703
|
-
*/
|
|
704
|
-
export async function verifyBalanceWaiver(waiver, agentPublicKey) {
|
|
705
|
-
if (waiver.suite !== BALANCE_WAIVER_SUITE)
|
|
706
|
-
return false;
|
|
707
|
-
const { signature, ...body } = waiver;
|
|
708
|
-
const canonical = canonicalJson(body);
|
|
709
|
-
const message = new TextEncoder().encode(canonical);
|
|
710
|
-
try {
|
|
711
|
-
const sig = fromBase64Url(signature);
|
|
712
|
-
return await verifyBySuite(waiver.suite, message, sig, agentPublicKey);
|
|
713
|
-
}
|
|
714
|
-
catch {
|
|
715
|
-
return false;
|
|
716
|
-
}
|
|
717
|
-
}
|
|
718
|
-
/** The one suite SettlementRecords sign under today. */
|
|
719
|
-
export const SETTLEMENT_RECORD_SUITE = "motebit-jcs-ed25519-b64-v1";
|
|
720
|
-
/**
|
|
721
|
-
* Sign a settlement record. The issuing relay commits to the (amount,
|
|
722
|
-
* fee, rate, status) tuple; a malicious relay therefore cannot issue
|
|
723
|
-
* inconsistent records to different observers.
|
|
724
|
-
*
|
|
725
|
-
* Callers pass the record without `signature` or `suite`; the signer
|
|
726
|
-
* owns both.
|
|
727
|
-
*
|
|
728
|
-
* Foundation Law (services/relay/CLAUDE.md rule 6): every truth the
|
|
729
|
-
* relay asserts is independently verifiable. Per-agent settlements
|
|
730
|
-
* deliver this through the signature; federation settlements
|
|
731
|
-
* additionally get Merkle-batched and onchain-anchored.
|
|
732
|
-
*/
|
|
733
|
-
export async function signSettlement(settlement, issuerPrivateKey) {
|
|
734
|
-
const body = { ...settlement, suite: SETTLEMENT_RECORD_SUITE };
|
|
735
|
-
const canonical = canonicalJson(body);
|
|
736
|
-
const message = new TextEncoder().encode(canonical);
|
|
737
|
-
const sig = await signBySuite(SETTLEMENT_RECORD_SUITE, message, issuerPrivateKey);
|
|
738
|
-
return { ...body, signature: toBase64Url(sig) };
|
|
739
|
-
}
|
|
740
|
-
/**
|
|
741
|
-
* Verify a settlement record's signature. Reconstructs canonical JSON
|
|
742
|
-
* over all fields except `signature` and verifies Ed25519 against the
|
|
743
|
-
* issuing relay's public key.
|
|
744
|
-
*
|
|
745
|
-
* The caller supplies the public key — typically resolved from the
|
|
746
|
-
* `issuer_relay_id` via the federation peer registry or a known-keys
|
|
747
|
-
* store. The signature alone proves the record was issued by the
|
|
748
|
-
* holder of `issuerPublicKey`; trust in that key is a separate
|
|
749
|
-
* concern (federation membership, key rotation chain, etc).
|
|
750
|
-
*
|
|
751
|
-
* Fail-closed on:
|
|
752
|
-
* - missing or unknown `suite` value
|
|
753
|
-
* - base64url decode errors
|
|
754
|
-
* - primitive-level verification failure
|
|
755
|
-
*/
|
|
756
|
-
export async function verifySettlement(settlement, issuerPublicKey) {
|
|
757
|
-
if (settlement.suite !== SETTLEMENT_RECORD_SUITE)
|
|
758
|
-
return false;
|
|
759
|
-
const { signature, ...body } = settlement;
|
|
760
|
-
const canonical = canonicalJson(body);
|
|
761
|
-
const message = new TextEncoder().encode(canonical);
|
|
762
|
-
try {
|
|
763
|
-
const sig = fromBase64Url(signature);
|
|
764
|
-
return await verifyBySuite(settlement.suite, message, sig, issuerPublicKey);
|
|
765
|
-
}
|
|
766
|
-
catch {
|
|
767
|
-
return false;
|
|
768
|
-
}
|
|
769
|
-
}
|
|
770
|
-
// === Key Succession (Rotation) ===
|
|
771
|
-
/** The one suite KeySuccessionRecords sign under today. */
|
|
772
|
-
export const KEY_SUCCESSION_SUITE = "motebit-jcs-ed25519-hex-v1";
|
|
773
|
-
/**
|
|
774
|
-
* Build the canonical payload for key succession signing. The `suite`
|
|
775
|
-
* field is stamped into the signed body so verifiers dispatch the
|
|
776
|
-
* primitive via `verifyBySuite` rather than assuming Ed25519 implicitly.
|
|
777
|
-
*/
|
|
778
|
-
function keySuccessionPayload(oldPublicKeyHex, newPublicKeyHex, timestamp, reason, recovery) {
|
|
779
|
-
const obj = {
|
|
780
|
-
old_public_key: oldPublicKeyHex,
|
|
781
|
-
new_public_key: newPublicKeyHex,
|
|
782
|
-
timestamp,
|
|
783
|
-
suite: KEY_SUCCESSION_SUITE,
|
|
784
|
-
};
|
|
785
|
-
if (reason !== undefined) {
|
|
786
|
-
obj.reason = reason;
|
|
787
|
-
}
|
|
788
|
-
if (recovery) {
|
|
789
|
-
obj.recovery = true;
|
|
790
|
-
}
|
|
791
|
-
return canonicalJson(obj);
|
|
792
|
-
}
|
|
793
|
-
/**
|
|
794
|
-
* Create a key succession record signed by both the old and new keys.
|
|
795
|
-
* Dispatches primitive signing through `signBySuite` per the
|
|
796
|
-
* `motebit-jcs-ed25519-hex-v1` suite.
|
|
797
|
-
*/
|
|
798
|
-
export async function signKeySuccession(oldPrivateKey, newPrivateKey, newPublicKey, oldPublicKey, reason) {
|
|
799
|
-
const timestamp = Date.now();
|
|
800
|
-
const oldPublicKeyHex = bytesToHex(oldPublicKey);
|
|
801
|
-
const newPublicKeyHex = bytesToHex(newPublicKey);
|
|
802
|
-
const payload = keySuccessionPayload(oldPublicKeyHex, newPublicKeyHex, timestamp, reason);
|
|
803
|
-
const message = new TextEncoder().encode(payload);
|
|
804
|
-
const oldSig = await signBySuite(KEY_SUCCESSION_SUITE, message, oldPrivateKey);
|
|
805
|
-
const newSig = await signBySuite(KEY_SUCCESSION_SUITE, message, newPrivateKey);
|
|
806
|
-
return {
|
|
807
|
-
old_public_key: oldPublicKeyHex,
|
|
808
|
-
new_public_key: newPublicKeyHex,
|
|
809
|
-
timestamp,
|
|
810
|
-
...(reason !== undefined ? { reason } : {}),
|
|
811
|
-
suite: KEY_SUCCESSION_SUITE,
|
|
812
|
-
old_key_signature: bytesToHex(oldSig),
|
|
813
|
-
new_key_signature: bytesToHex(newSig),
|
|
814
|
-
};
|
|
815
|
-
}
|
|
816
|
-
/**
|
|
817
|
-
* Sign a guardian recovery succession record (§3.8.3).
|
|
818
|
-
* The guardian key signs instead of the compromised old key.
|
|
819
|
-
* Reason MUST include "guardian_recovery".
|
|
820
|
-
*/
|
|
821
|
-
export async function signGuardianRecoverySuccession(guardianPrivateKey, newPrivateKey, oldPublicKey, newPublicKey, reason) {
|
|
822
|
-
const timestamp = Date.now();
|
|
823
|
-
const oldPublicKeyHex = bytesToHex(oldPublicKey);
|
|
824
|
-
const newPublicKeyHex = bytesToHex(newPublicKey);
|
|
825
|
-
const effectiveReason = reason ?? "guardian_recovery";
|
|
826
|
-
const payload = keySuccessionPayload(oldPublicKeyHex, newPublicKeyHex, timestamp, effectiveReason, true);
|
|
827
|
-
const message = new TextEncoder().encode(payload);
|
|
828
|
-
const guardianSig = await signBySuite(KEY_SUCCESSION_SUITE, message, guardianPrivateKey);
|
|
829
|
-
const newSig = await signBySuite(KEY_SUCCESSION_SUITE, message, newPrivateKey);
|
|
830
|
-
return {
|
|
831
|
-
old_public_key: oldPublicKeyHex,
|
|
832
|
-
new_public_key: newPublicKeyHex,
|
|
833
|
-
timestamp,
|
|
834
|
-
reason: effectiveReason,
|
|
835
|
-
suite: KEY_SUCCESSION_SUITE,
|
|
836
|
-
new_key_signature: bytesToHex(newSig),
|
|
837
|
-
recovery: true,
|
|
838
|
-
guardian_signature: bytesToHex(guardianSig),
|
|
839
|
-
};
|
|
840
|
-
}
|
|
841
|
-
/**
|
|
842
|
-
* Verify a key succession record. For normal rotation, checks
|
|
843
|
-
* old_key_signature + new_key_signature. For guardian recovery
|
|
844
|
-
* (recovery: true), checks guardian_signature + new_key_signature.
|
|
845
|
-
* Rejects records whose `suite` is missing or not the succession suite.
|
|
846
|
-
*/
|
|
847
|
-
export async function verifyKeySuccession(record, guardianPublicKeyHex) {
|
|
848
|
-
if (record.suite !== KEY_SUCCESSION_SUITE)
|
|
849
|
-
return false;
|
|
850
|
-
const payload = keySuccessionPayload(record.old_public_key, record.new_public_key, record.timestamp, record.reason, record.recovery);
|
|
851
|
-
const message = new TextEncoder().encode(payload);
|
|
852
|
-
try {
|
|
853
|
-
const newPubKey = hexToBytes(record.new_public_key);
|
|
854
|
-
const newSig = hexToBytes(record.new_key_signature);
|
|
855
|
-
const newValid = await verifyBySuite(record.suite, message, newSig, newPubKey);
|
|
856
|
-
if (!newValid)
|
|
857
|
-
return false;
|
|
858
|
-
if (record.recovery) {
|
|
859
|
-
if (!record.guardian_signature || !guardianPublicKeyHex)
|
|
860
|
-
return false;
|
|
861
|
-
const guardianPubKey = hexToBytes(guardianPublicKeyHex);
|
|
862
|
-
const guardianSig = hexToBytes(record.guardian_signature);
|
|
863
|
-
return await verifyBySuite(record.suite, message, guardianSig, guardianPubKey);
|
|
864
|
-
}
|
|
865
|
-
else {
|
|
866
|
-
if (!record.old_key_signature)
|
|
867
|
-
return false;
|
|
868
|
-
const oldPubKey = hexToBytes(record.old_public_key);
|
|
869
|
-
const oldSig = hexToBytes(record.old_key_signature);
|
|
870
|
-
return await verifyBySuite(record.suite, message, oldSig, oldPubKey);
|
|
871
|
-
}
|
|
872
|
-
}
|
|
873
|
-
catch {
|
|
874
|
-
/* v8 ignore next */
|
|
875
|
-
return false;
|
|
876
|
-
}
|
|
877
|
-
}
|
|
878
|
-
/**
|
|
879
|
-
* Verify a full key succession chain — an ordered array of KeySuccessionRecords
|
|
880
|
-
* representing a sequence of key rotations from a genesis key to the current active key.
|
|
881
|
-
*/
|
|
882
|
-
export async function verifySuccessionChain(chain, guardianPublicKeyHex) {
|
|
883
|
-
if (chain.length === 0) {
|
|
884
|
-
return {
|
|
885
|
-
valid: false,
|
|
886
|
-
genesis_public_key: "",
|
|
887
|
-
current_public_key: "",
|
|
888
|
-
length: 0,
|
|
889
|
-
error: { index: 0, message: "Empty succession chain" },
|
|
890
|
-
};
|
|
891
|
-
}
|
|
892
|
-
const genesisKey = chain[0].old_public_key;
|
|
893
|
-
const currentKey = chain[chain.length - 1].new_public_key;
|
|
894
|
-
for (let i = 0; i < chain.length; i++) {
|
|
895
|
-
const record = chain[i];
|
|
896
|
-
if (record.recovery && !guardianPublicKeyHex) {
|
|
897
|
-
return {
|
|
898
|
-
valid: false,
|
|
899
|
-
genesis_public_key: genesisKey,
|
|
900
|
-
current_public_key: currentKey,
|
|
901
|
-
length: chain.length,
|
|
902
|
-
error: {
|
|
903
|
-
index: i,
|
|
904
|
-
message: `Record ${i} is a guardian recovery but no guardian public key provided`,
|
|
905
|
-
},
|
|
906
|
-
};
|
|
907
|
-
}
|
|
908
|
-
const sigValid = await verifyKeySuccession(record, guardianPublicKeyHex);
|
|
909
|
-
if (!sigValid) {
|
|
910
|
-
return {
|
|
911
|
-
valid: false,
|
|
912
|
-
genesis_public_key: genesisKey,
|
|
913
|
-
current_public_key: currentKey,
|
|
914
|
-
length: chain.length,
|
|
915
|
-
error: { index: i, message: `Record ${i} has invalid signature` },
|
|
916
|
-
};
|
|
917
|
-
}
|
|
918
|
-
if (i < chain.length - 1) {
|
|
919
|
-
const next = chain[i + 1];
|
|
920
|
-
if (record.new_public_key !== next.old_public_key) {
|
|
921
|
-
return {
|
|
922
|
-
valid: false,
|
|
923
|
-
genesis_public_key: genesisKey,
|
|
924
|
-
current_public_key: currentKey,
|
|
925
|
-
length: chain.length,
|
|
926
|
-
error: {
|
|
927
|
-
index: i + 1,
|
|
928
|
-
message: `Chain break at ${i + 1}: expected old_public_key "${record.new_public_key}", got "${next.old_public_key}"`,
|
|
929
|
-
},
|
|
930
|
-
};
|
|
931
|
-
}
|
|
932
|
-
}
|
|
933
|
-
if (i < chain.length - 1) {
|
|
934
|
-
const next = chain[i + 1];
|
|
935
|
-
if (record.timestamp >= next.timestamp) {
|
|
936
|
-
return {
|
|
937
|
-
valid: false,
|
|
938
|
-
genesis_public_key: genesisKey,
|
|
939
|
-
current_public_key: currentKey,
|
|
940
|
-
length: chain.length,
|
|
941
|
-
error: {
|
|
942
|
-
index: i + 1,
|
|
943
|
-
message: `Temporal ordering violation at ${i + 1}: timestamp ${next.timestamp} is not after ${record.timestamp}`,
|
|
944
|
-
},
|
|
945
|
-
};
|
|
946
|
-
}
|
|
947
|
-
}
|
|
948
|
-
}
|
|
949
|
-
return {
|
|
950
|
-
valid: true,
|
|
951
|
-
genesis_public_key: genesisKey,
|
|
952
|
-
current_public_key: currentKey,
|
|
953
|
-
length: chain.length,
|
|
954
|
-
};
|
|
955
|
-
}
|
|
956
|
-
// === Guardian Revocation (§3.3.2) ===
|
|
957
|
-
/** Guardian revocation shares the identity-file suite (JCS + hex). */
|
|
958
|
-
export const GUARDIAN_REVOCATION_SUITE = "motebit-jcs-ed25519-hex-v1";
|
|
959
|
-
/**
|
|
960
|
-
* Sign a guardian revocation payload — requires BOTH identity and guardian keys.
|
|
961
|
-
* Neither party can unilaterally dissolve the custody relationship.
|
|
962
|
-
* Dispatches the primitive through `signBySuite`.
|
|
963
|
-
*/
|
|
964
|
-
export async function signGuardianRevocation(identityPrivateKey, guardianPrivateKey, timestamp) {
|
|
965
|
-
const ts = timestamp ?? Date.now();
|
|
966
|
-
const payload = canonicalJson({
|
|
967
|
-
action: "guardian_revoked",
|
|
968
|
-
timestamp: ts,
|
|
969
|
-
suite: GUARDIAN_REVOCATION_SUITE,
|
|
970
|
-
});
|
|
971
|
-
const message = new TextEncoder().encode(payload);
|
|
972
|
-
const identitySig = await signBySuite(GUARDIAN_REVOCATION_SUITE, message, identityPrivateKey);
|
|
973
|
-
const guardianSig = await signBySuite(GUARDIAN_REVOCATION_SUITE, message, guardianPrivateKey);
|
|
974
|
-
return {
|
|
975
|
-
payload,
|
|
976
|
-
identity_signature: bytesToHex(identitySig),
|
|
977
|
-
guardian_signature: bytesToHex(guardianSig),
|
|
978
|
-
timestamp: ts,
|
|
979
|
-
};
|
|
980
|
-
}
|
|
981
|
-
/**
|
|
982
|
-
* Verify a guardian revocation proof — both signatures must be valid.
|
|
983
|
-
* Dispatches primitive verification through `verifyBySuite`.
|
|
984
|
-
*/
|
|
985
|
-
export async function verifyGuardianRevocation(revocation, identityPublicKeyHex, guardianPublicKeyHex) {
|
|
986
|
-
const payload = canonicalJson({
|
|
987
|
-
action: "guardian_revoked",
|
|
988
|
-
timestamp: revocation.timestamp,
|
|
989
|
-
suite: GUARDIAN_REVOCATION_SUITE,
|
|
990
|
-
});
|
|
991
|
-
const message = new TextEncoder().encode(payload);
|
|
992
|
-
try {
|
|
993
|
-
const identityPub = hexToBytes(identityPublicKeyHex);
|
|
994
|
-
const guardianPub = hexToBytes(guardianPublicKeyHex);
|
|
995
|
-
const identitySig = hexToBytes(revocation.identity_signature);
|
|
996
|
-
const guardianSig = hexToBytes(revocation.guardian_signature);
|
|
997
|
-
const identityValid = await verifyBySuite(GUARDIAN_REVOCATION_SUITE, message, identitySig, identityPub);
|
|
998
|
-
const guardianValid = await verifyBySuite(GUARDIAN_REVOCATION_SUITE, message, guardianSig, guardianPub);
|
|
999
|
-
return identityValid && guardianValid;
|
|
1000
|
-
}
|
|
1001
|
-
catch {
|
|
1002
|
-
return false;
|
|
1003
|
-
}
|
|
1004
|
-
}
|
|
1005
|
-
// === Collaborative Receipt ===
|
|
1006
|
-
/** The one suite CollaborativeReceipts sign under today. */
|
|
1007
|
-
export const COLLABORATIVE_RECEIPT_SUITE = "motebit-jcs-ed25519-b64-v1";
|
|
1008
|
-
/**
|
|
1009
|
-
* Sign a collaborative receipt. Computes a content hash over the canonical
|
|
1010
|
-
* JSON of all participant receipts, then signs the aggregate through
|
|
1011
|
-
* `signBySuite` under `motebit-jcs-ed25519-b64-v1`.
|
|
1012
|
-
*/
|
|
1013
|
-
export async function signCollaborativeReceipt(receipt, initiatorPrivateKey) {
|
|
1014
|
-
const receiptsCanonical = canonicalJson(receipt.participant_receipts);
|
|
1015
|
-
const receiptsBytes = new TextEncoder().encode(receiptsCanonical);
|
|
1016
|
-
const contentHash = await hash(receiptsBytes);
|
|
1017
|
-
const sigPayload = canonicalJson({
|
|
1018
|
-
proposal_id: receipt.proposal_id,
|
|
1019
|
-
plan_id: receipt.plan_id,
|
|
1020
|
-
content_hash: contentHash,
|
|
1021
|
-
suite: COLLABORATIVE_RECEIPT_SUITE,
|
|
1022
|
-
});
|
|
1023
|
-
const sigMessage = new TextEncoder().encode(sigPayload);
|
|
1024
|
-
const sig = await signBySuite(COLLABORATIVE_RECEIPT_SUITE, sigMessage, initiatorPrivateKey);
|
|
1025
|
-
return {
|
|
1026
|
-
...receipt,
|
|
1027
|
-
content_hash: contentHash,
|
|
1028
|
-
suite: COLLABORATIVE_RECEIPT_SUITE,
|
|
1029
|
-
initiator_signature: toBase64Url(sig),
|
|
1030
|
-
};
|
|
1031
|
-
}
|
|
1032
|
-
/**
|
|
1033
|
-
* Verify a collaborative receipt:
|
|
1034
|
-
* 1. Rejects any record whose `suite` is missing or not the collaborative suite.
|
|
1035
|
-
* 2. Recomputes content hash from participant receipts and checks it matches.
|
|
1036
|
-
* 3. Verifies the initiator's Ed25519 signature over the aggregate via `verifyBySuite`.
|
|
1037
|
-
* 4. Optionally verifies each participant receipt against known keys.
|
|
1038
|
-
*/
|
|
1039
|
-
export async function verifyCollaborativeReceipt(receipt, initiatorPublicKey, participantKeys) {
|
|
1040
|
-
// 0. Suite discriminator check
|
|
1041
|
-
if (receipt.suite !== COLLABORATIVE_RECEIPT_SUITE) {
|
|
1042
|
-
return { valid: false, error: "Unknown or missing cryptosuite" };
|
|
1043
|
-
}
|
|
1044
|
-
// 1. Recompute content hash
|
|
1045
|
-
const receiptsCanonical = canonicalJson(receipt.participant_receipts);
|
|
1046
|
-
const receiptsBytes = new TextEncoder().encode(receiptsCanonical);
|
|
1047
|
-
const expectedHash = await hash(receiptsBytes);
|
|
1048
|
-
if (expectedHash !== receipt.content_hash) {
|
|
1049
|
-
return { valid: false, error: "Content hash mismatch" };
|
|
1050
|
-
}
|
|
1051
|
-
// 2. Verify initiator signature (suite stamped into the signed payload)
|
|
1052
|
-
const sigPayload = canonicalJson({
|
|
1053
|
-
proposal_id: receipt.proposal_id,
|
|
1054
|
-
plan_id: receipt.plan_id,
|
|
1055
|
-
content_hash: receipt.content_hash,
|
|
1056
|
-
suite: receipt.suite,
|
|
1057
|
-
});
|
|
1058
|
-
const sigMessage = new TextEncoder().encode(sigPayload);
|
|
1059
|
-
try {
|
|
1060
|
-
const sig = fromBase64Url(receipt.initiator_signature);
|
|
1061
|
-
const sigValid = await verifyBySuite(receipt.suite, sigMessage, sig, initiatorPublicKey);
|
|
1062
|
-
if (!sigValid) {
|
|
1063
|
-
return { valid: false, error: "Initiator signature invalid" };
|
|
1064
|
-
}
|
|
1065
|
-
}
|
|
1066
|
-
catch {
|
|
1067
|
-
return { valid: false, error: "Initiator signature decode failed" };
|
|
1068
|
-
}
|
|
1069
|
-
// 3. Verify participant receipts if keys provided
|
|
1070
|
-
if (participantKeys) {
|
|
1071
|
-
for (let i = 0; i < receipt.participant_receipts.length; i++) {
|
|
1072
|
-
const pr = receipt.participant_receipts[i];
|
|
1073
|
-
const pubKey = participantKeys.get(pr.motebit_id);
|
|
1074
|
-
if (!pubKey) {
|
|
1075
|
-
return {
|
|
1076
|
-
valid: false,
|
|
1077
|
-
error: `Unknown participant key for receipt ${i} (${pr.motebit_id})`,
|
|
1078
|
-
};
|
|
1079
|
-
}
|
|
1080
|
-
const prValid = await verifyExecutionReceipt(pr, pubKey);
|
|
1081
|
-
if (!prValid) {
|
|
1082
|
-
return {
|
|
1083
|
-
valid: false,
|
|
1084
|
-
error: `Participant receipt ${i} (${pr.motebit_id}) signature invalid`,
|
|
1085
|
-
};
|
|
1086
|
-
}
|
|
1087
|
-
}
|
|
1088
|
-
}
|
|
1089
|
-
return { valid: true };
|
|
1090
|
-
}
|
|
1091
|
-
// === Device Self-Registration ===
|
|
1092
|
-
//
|
|
1093
|
-
// Self-attesting registration: the device proves it controls a private key
|
|
1094
|
-
// by signing a canonical-JSON serialization of its own registration request.
|
|
1095
|
-
// The relay verifies against the public_key carried in the same request — no
|
|
1096
|
-
// prior trust anchor required. Wire format and verification recipe are
|
|
1097
|
-
// foundation law in `spec/device-self-registration-v1.md`.
|
|
1098
|
-
//
|
|
1099
|
-
// Trust posture: a self-registered device starts at trust zero. Trust accrues
|
|
1100
|
-
// through receipts, credentials, and onchain anchors — never through
|
|
1101
|
-
// registration alone. See `docs/doctrine/protocol-model.md`.
|
|
1102
|
-
/** The one suite device-registration requests sign under today. */
|
|
1103
|
-
export const DEVICE_REGISTRATION_SUITE = "motebit-jcs-ed25519-b64-v1";
|
|
1104
|
-
/**
|
|
1105
|
-
* Sign a device-registration request. Stamps the cryptosuite into the body,
|
|
1106
|
-
* canonicalizes with JCS, dispatches the primitive signature through
|
|
1107
|
-
* `signBySuite`, and encodes as base64url per the suite's rules.
|
|
1108
|
-
*
|
|
1109
|
-
* Callers pass the body without `signature` and (optionally) without `suite`;
|
|
1110
|
-
* the signer owns both. The returned object is a complete signed request
|
|
1111
|
-
* ready to POST to a relay's self-register endpoint.
|
|
1112
|
-
*/
|
|
1113
|
-
export async function signDeviceRegistration(body, privateKey) {
|
|
1114
|
-
const withSuite = { ...body, suite: DEVICE_REGISTRATION_SUITE };
|
|
1115
|
-
const canonical = canonicalJson(withSuite);
|
|
1116
|
-
const message = new TextEncoder().encode(canonical);
|
|
1117
|
-
const sig = await signBySuite(DEVICE_REGISTRATION_SUITE, message, privateKey);
|
|
1118
|
-
return { ...withSuite, signature: toBase64Url(sig) };
|
|
1119
|
-
}
|
|
1120
|
-
/** Maximum drift between the signer's claimed timestamp and the verifier's clock. */
|
|
1121
|
-
export const DEVICE_REGISTRATION_MAX_AGE_MS = 5 * 60 * 1000;
|
|
1122
|
-
export async function verifyDeviceRegistration(body, now = Date.now()) {
|
|
1123
|
-
// Step 1 — shape validation. Any missing / mistyped field is "malformed".
|
|
1124
|
-
if (typeof body.motebit_id !== "string" ||
|
|
1125
|
-
typeof body.device_id !== "string" ||
|
|
1126
|
-
typeof body.public_key !== "string" ||
|
|
1127
|
-
!/^[0-9a-f]{64}$/i.test(body.public_key) ||
|
|
1128
|
-
typeof body.timestamp !== "number" ||
|
|
1129
|
-
typeof body.suite !== "string" ||
|
|
1130
|
-
typeof body.signature !== "string") {
|
|
1131
|
-
return { valid: false, reason: "malformed" };
|
|
1132
|
-
}
|
|
1133
|
-
// Step 2 — replay window.
|
|
1134
|
-
if (Math.abs(now - body.timestamp) > DEVICE_REGISTRATION_MAX_AGE_MS) {
|
|
1135
|
-
return { valid: false, reason: "stale" };
|
|
1136
|
-
}
|
|
1137
|
-
// Step 3 — suite check. Only the registered suite is acceptable today;
|
|
1138
|
-
// future suites add a dispatch arm in suite-dispatch.ts.
|
|
1139
|
-
if (body.suite !== DEVICE_REGISTRATION_SUITE) {
|
|
1140
|
-
return { valid: false, reason: "unsupported_suite" };
|
|
1141
|
-
}
|
|
1142
|
-
// Step 4–7 — canonicalize, decode, verify.
|
|
1143
|
-
const { signature, ...bodyForSig } = body;
|
|
1144
|
-
const canonical = canonicalJson(bodyForSig);
|
|
1145
|
-
const message = new TextEncoder().encode(canonical);
|
|
1146
|
-
let sigBytes;
|
|
1147
|
-
let pkBytes;
|
|
1148
|
-
try {
|
|
1149
|
-
sigBytes = fromBase64Url(signature);
|
|
1150
|
-
pkBytes = hexToBytes(body.public_key);
|
|
1151
|
-
}
|
|
1152
|
-
catch {
|
|
1153
|
-
return { valid: false, reason: "malformed" };
|
|
1154
|
-
}
|
|
1155
|
-
const ok = await verifyBySuite(body.suite, message, sigBytes, pkBytes);
|
|
1156
|
-
return ok ? { valid: true } : { valid: false, reason: "bad_signature" };
|
|
1157
|
-
}
|
|
1158
|
-
//# sourceMappingURL=artifacts.js.map
|