burnledger 0.8.1 → 0.9.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/README.md +139 -2
- package/dist/cjs/anchor.d.ts +5 -3
- package/dist/cjs/anchor.d.ts.map +1 -1
- package/dist/cjs/anchor.js +10 -4
- package/dist/cjs/anchor.js.map +1 -1
- package/dist/cjs/client.d.ts +95 -0
- package/dist/cjs/client.d.ts.map +1 -1
- package/dist/cjs/client.js +133 -2
- package/dist/cjs/client.js.map +1 -1
- package/dist/cjs/customer-keys.d.ts +73 -1
- package/dist/cjs/customer-keys.d.ts.map +1 -1
- package/dist/cjs/customer-keys.js +329 -3
- package/dist/cjs/customer-keys.js.map +1 -1
- package/dist/cjs/enclave-registration.d.ts +153 -0
- package/dist/cjs/enclave-registration.d.ts.map +1 -0
- package/dist/cjs/enclave-registration.js +275 -0
- package/dist/cjs/enclave-registration.js.map +1 -0
- package/dist/cjs/enclave-seal.d.ts +43 -0
- package/dist/cjs/enclave-seal.d.ts.map +1 -1
- package/dist/cjs/enclave-seal.js +62 -1
- package/dist/cjs/enclave-seal.js.map +1 -1
- package/dist/cjs/errors.d.ts +10 -0
- package/dist/cjs/errors.d.ts.map +1 -1
- package/dist/cjs/errors.js +11 -1
- package/dist/cjs/errors.js.map +1 -1
- package/dist/cjs/index.browser.d.ts +7 -3
- package/dist/cjs/index.browser.d.ts.map +1 -1
- package/dist/cjs/index.browser.js +6 -3
- package/dist/cjs/index.browser.js.map +1 -1
- package/dist/cjs/index.d.ts +22 -10
- package/dist/cjs/index.d.ts.map +1 -1
- package/dist/cjs/index.js +30 -6
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/key-group.d.ts +80 -0
- package/dist/cjs/key-group.d.ts.map +1 -0
- package/dist/cjs/key-group.js +136 -0
- package/dist/cjs/key-group.js.map +1 -0
- package/dist/cjs/models.d.ts +60 -0
- package/dist/cjs/models.d.ts.map +1 -1
- package/dist/cjs/models.js +63 -0
- package/dist/cjs/models.js.map +1 -1
- package/dist/cjs/status-document.d.ts +25 -0
- package/dist/cjs/status-document.d.ts.map +1 -0
- package/dist/cjs/status-document.js +62 -0
- package/dist/cjs/status-document.js.map +1 -0
- package/dist/cjs/verify.d.ts +108 -3
- package/dist/cjs/verify.d.ts.map +1 -1
- package/dist/cjs/verify.js +389 -146
- package/dist/cjs/verify.js.map +1 -1
- package/dist/cjs/web-verifier.d.ts +23 -3
- package/dist/cjs/web-verifier.d.ts.map +1 -1
- package/dist/cjs/web-verifier.js +31 -5
- package/dist/cjs/web-verifier.js.map +1 -1
- package/dist/esm/anchor.d.ts +5 -3
- package/dist/esm/anchor.d.ts.map +1 -1
- package/dist/esm/anchor.js +10 -4
- package/dist/esm/anchor.js.map +1 -1
- package/dist/esm/cli.d.ts +13 -19
- package/dist/esm/cli.d.ts.map +1 -1
- package/dist/esm/cli.js +88 -81
- package/dist/esm/cli.js.map +1 -1
- package/dist/esm/client.d.ts +95 -0
- package/dist/esm/client.d.ts.map +1 -1
- package/dist/esm/client.js +133 -2
- package/dist/esm/client.js.map +1 -1
- package/dist/esm/customer-keys.d.ts +73 -1
- package/dist/esm/customer-keys.d.ts.map +1 -1
- package/dist/esm/customer-keys.js +323 -3
- package/dist/esm/customer-keys.js.map +1 -1
- package/dist/esm/enclave-registration.d.ts +153 -0
- package/dist/esm/enclave-registration.d.ts.map +1 -0
- package/dist/esm/enclave-registration.js +265 -0
- package/dist/esm/enclave-registration.js.map +1 -0
- package/dist/esm/enclave-seal.d.ts +43 -0
- package/dist/esm/enclave-seal.d.ts.map +1 -1
- package/dist/esm/enclave-seal.js +61 -1
- package/dist/esm/enclave-seal.js.map +1 -1
- package/dist/esm/errors.d.ts +10 -0
- package/dist/esm/errors.d.ts.map +1 -1
- package/dist/esm/errors.js +10 -0
- package/dist/esm/errors.js.map +1 -1
- package/dist/esm/index.browser.d.ts +7 -3
- package/dist/esm/index.browser.d.ts.map +1 -1
- package/dist/esm/index.browser.js +6 -3
- package/dist/esm/index.browser.js.map +1 -1
- package/dist/esm/index.d.ts +22 -10
- package/dist/esm/index.d.ts.map +1 -1
- package/dist/esm/index.js +18 -8
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/key-group.d.ts +80 -0
- package/dist/esm/key-group.d.ts.map +1 -0
- package/dist/esm/key-group.js +130 -0
- package/dist/esm/key-group.js.map +1 -0
- package/dist/esm/models.d.ts +60 -0
- package/dist/esm/models.d.ts.map +1 -1
- package/dist/esm/models.js +59 -0
- package/dist/esm/models.js.map +1 -1
- package/dist/esm/status-document.d.ts +25 -0
- package/dist/esm/status-document.d.ts.map +1 -0
- package/dist/esm/status-document.js +59 -0
- package/dist/esm/status-document.js.map +1 -0
- package/dist/esm/verify.d.ts +108 -3
- package/dist/esm/verify.d.ts.map +1 -1
- package/dist/esm/verify.js +386 -146
- package/dist/esm/verify.js.map +1 -1
- package/dist/esm/web-verifier.d.ts +23 -3
- package/dist/esm/web-verifier.d.ts.map +1 -1
- package/dist/esm/web-verifier.js +27 -5
- package/dist/esm/web-verifier.js.map +1 -1
- package/package.json +1 -1
- package/src/anchor.ts +10 -4
- package/src/cli.ts +96 -79
- package/src/client.ts +198 -6
- package/src/customer-keys.ts +373 -3
- package/src/enclave-registration.ts +390 -0
- package/src/enclave-seal.ts +108 -1
- package/src/errors.ts +11 -0
- package/src/index.browser.ts +9 -3
- package/src/index.ts +35 -6
- package/src/key-group.ts +181 -0
- package/src/models.ts +131 -0
- package/src/status-document.ts +59 -0
- package/src/verify.ts +499 -151
- package/src/web-verifier.ts +36 -3
package/src/customer-keys.ts
CHANGED
|
@@ -18,8 +18,9 @@
|
|
|
18
18
|
* generator writes by calling core itself.
|
|
19
19
|
*/
|
|
20
20
|
|
|
21
|
-
import { canonicalJson } from "./verify.js";
|
|
21
|
+
import { canonicalJson, formatTimestamp } from "./verify.js";
|
|
22
22
|
import type { CryptoOps } from "./crypto.js";
|
|
23
|
+
import { VerificationError } from "./errors.js";
|
|
23
24
|
|
|
24
25
|
/**
|
|
25
26
|
* Mirrors core.CustomerKeyIDPrefix. It distinguishes a customer's key id from
|
|
@@ -35,7 +36,11 @@ export const CUSTOMER_KEY_ID_PREFIX = "cust_k_";
|
|
|
35
36
|
*/
|
|
36
37
|
const PAYLOAD_TYPE_CUSTOMER_KEY_GROUP = "burnledger.customer_key_group.v1";
|
|
37
38
|
|
|
38
|
-
|
|
39
|
+
/**
|
|
40
|
+
* Mirrors core.PayloadTypeKeyRotationRequest. v2 carries not_after, which bounds
|
|
41
|
+
* the request itself; a protocol-12 enclave refuses v1.
|
|
42
|
+
*/
|
|
43
|
+
export const PAYLOAD_TYPE_KEY_ROTATION_REQUEST = "burnledger.key_rotation_request.v2";
|
|
39
44
|
|
|
40
45
|
/** Mirrors core.MaxCustomerKeyGroupMembers. */
|
|
41
46
|
export const MAX_CUSTOMER_KEY_GROUP_MEMBERS = 16;
|
|
@@ -103,6 +108,15 @@ export async function validateKeyGroup(
|
|
|
103
108
|
`every member must be a ${ED25519_PUBLIC_KEY_SIZE}-byte Ed25519 key`,
|
|
104
109
|
);
|
|
105
110
|
}
|
|
111
|
+
if (!isNotSmallOrder(member)) {
|
|
112
|
+
// A small-order key is held by nobody: with the identity point as the
|
|
113
|
+
// public key, R = identity and S = 0 verify over ANY message, so it
|
|
114
|
+
// would pass proof of possession and count toward a threshold for free.
|
|
115
|
+
throw new InvalidKeyGroupError(
|
|
116
|
+
"every member must be an Ed25519 public key on the curve and not of small order " +
|
|
117
|
+
"(small-order and off-curve keys are refused)",
|
|
118
|
+
);
|
|
119
|
+
}
|
|
106
120
|
const fingerprint = await memberFingerprint(crypto, member);
|
|
107
121
|
if (seen.has(fingerprint)) {
|
|
108
122
|
// A repeated member would let one holder satisfy a threshold meant to
|
|
@@ -145,6 +159,17 @@ export async function customerKeyId(
|
|
|
145
159
|
return CUSTOMER_KEY_ID_PREFIX + toHex(await crypto.sha256(canonicalJson(document)));
|
|
146
160
|
}
|
|
147
161
|
|
|
162
|
+
/**
|
|
163
|
+
* A request's `not_after` as Go signs it (core.KeyTimeFormat): UTC, whole
|
|
164
|
+
* seconds, the fraction truncated as Go's Format truncates it.
|
|
165
|
+
*/
|
|
166
|
+
export function keyTime(value: Date): string {
|
|
167
|
+
if (!(value instanceof Date) || Number.isNaN(value.getTime())) {
|
|
168
|
+
throw new Error("not_after is required");
|
|
169
|
+
}
|
|
170
|
+
return value.toISOString().slice(0, 19) + "Z";
|
|
171
|
+
}
|
|
172
|
+
|
|
148
173
|
/**
|
|
149
174
|
* The canonical bytes the OUTGOING group's members sign to authorise a rotation.
|
|
150
175
|
*
|
|
@@ -160,10 +185,19 @@ export function buildKeyRotationRequestPayload(request: {
|
|
|
160
185
|
assertedTeamId: string;
|
|
161
186
|
prevKeyId: string;
|
|
162
187
|
nextKeyId: string;
|
|
188
|
+
/**
|
|
189
|
+
* Bounds the request: the enclave ends the successor's window no later than
|
|
190
|
+
* this and refuses the request once it has passed, so a replay cannot renew
|
|
191
|
+
* the window. Signed as whole seconds, UTC.
|
|
192
|
+
*/
|
|
193
|
+
notAfter: Date;
|
|
163
194
|
}): Uint8Array {
|
|
164
195
|
if (!request.prevKeyId || !request.nextKeyId) {
|
|
165
196
|
throw new Error("a rotation request names both the key being replaced and its replacement");
|
|
166
197
|
}
|
|
198
|
+
if (!(request.notAfter instanceof Date) || Number.isNaN(request.notAfter.getTime())) {
|
|
199
|
+
throw new Error("not_after is required");
|
|
200
|
+
}
|
|
167
201
|
if (request.prevKeyId === request.nextKeyId) {
|
|
168
202
|
// A self-rotation is a cycle in the chain the enclave walks, and it walks
|
|
169
203
|
// without a visited set.
|
|
@@ -172,9 +206,345 @@ export function buildKeyRotationRequestPayload(request: {
|
|
|
172
206
|
);
|
|
173
207
|
}
|
|
174
208
|
return canonicalJson({
|
|
175
|
-
asserted_team_id: request.assertedTeamId,
|
|
209
|
+
asserted_team_id: canonicalUuid(request.assertedTeamId, "assertedTeamId"),
|
|
176
210
|
next_key_id: request.nextKeyId,
|
|
211
|
+
not_after: keyTime(request.notAfter),
|
|
177
212
|
payload_type: PAYLOAD_TYPE_KEY_ROTATION_REQUEST,
|
|
178
213
|
prev_key_id: request.prevKeyId,
|
|
179
214
|
});
|
|
180
215
|
}
|
|
216
|
+
|
|
217
|
+
// Ed25519 arithmetic for one question only: is a member key a curve point
|
|
218
|
+
// outside the small-order subgroup? It mirrors the decoding Go's
|
|
219
|
+
// filippo.io/edwards25519 performs in core.CustomerKeyGroup.Validate, so both
|
|
220
|
+
// sides refuse exactly the same keys: bit 255 of y is ignored and y is reduced
|
|
221
|
+
// mod p (non-canonical encodings are accepted), a y with no matching x is
|
|
222
|
+
// refused, and the sign bit selects the negative root even when x = 0.
|
|
223
|
+
const FIELD_P = 2n ** 255n - 19n;
|
|
224
|
+
const fieldMod = (a: bigint): bigint => ((a % FIELD_P) + FIELD_P) % FIELD_P;
|
|
225
|
+
function fieldPow(base: bigint, exp: bigint): bigint {
|
|
226
|
+
let result = 1n;
|
|
227
|
+
let b = fieldMod(base);
|
|
228
|
+
for (let e = exp; e > 0n; e >>= 1n) {
|
|
229
|
+
if (e & 1n) result = (result * b) % FIELD_P;
|
|
230
|
+
b = (b * b) % FIELD_P;
|
|
231
|
+
}
|
|
232
|
+
return result;
|
|
233
|
+
}
|
|
234
|
+
const fieldInv = (a: bigint): bigint => fieldPow(a, FIELD_P - 2n);
|
|
235
|
+
const CURVE_D = fieldMod(-121665n * fieldInv(121666n));
|
|
236
|
+
const SQRT_MINUS_ONE = fieldPow(2n, (FIELD_P - 1n) / 4n);
|
|
237
|
+
|
|
238
|
+
function decodePoint(bytes: Uint8Array): [bigint, bigint] | null {
|
|
239
|
+
// validateKeyGroup has already required exactly 32 bytes.
|
|
240
|
+
const top = bytes[31] ?? 0;
|
|
241
|
+
let y = 0n;
|
|
242
|
+
for (let i = 31; i >= 0; i--) {
|
|
243
|
+
y = (y << 8n) | BigInt(i === 31 ? top & 0x7f : (bytes[i] ?? 0));
|
|
244
|
+
}
|
|
245
|
+
y = fieldMod(y);
|
|
246
|
+
const u = fieldMod(y * y - 1n);
|
|
247
|
+
const v = fieldMod(CURVE_D * y * y + 1n);
|
|
248
|
+
let x = fieldMod(u * fieldPow(v, 3n) * fieldPow(u * fieldPow(v, 7n), (FIELD_P - 5n) / 8n));
|
|
249
|
+
const vx2 = fieldMod(v * x * x);
|
|
250
|
+
if (vx2 === fieldMod(-u)) {
|
|
251
|
+
x = fieldMod(x * SQRT_MINUS_ONE);
|
|
252
|
+
} else if (vx2 !== u) {
|
|
253
|
+
return null;
|
|
254
|
+
}
|
|
255
|
+
if (Number(x & 1n) !== top >> 7) x = fieldMod(-x);
|
|
256
|
+
return [x, y];
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// The complete twisted-Edwards addition law (a = -1); its denominators are
|
|
260
|
+
// never zero because d is not a square.
|
|
261
|
+
function addPoints(p: [bigint, bigint], q: [bigint, bigint]): [bigint, bigint] {
|
|
262
|
+
const t = fieldMod(CURVE_D * p[0] * q[0] * p[1] * q[1]);
|
|
263
|
+
return [
|
|
264
|
+
fieldMod((p[0] * q[1] + p[1] * q[0]) * fieldInv(1n + t)),
|
|
265
|
+
fieldMod((p[1] * q[1] + p[0] * q[0]) * fieldInv(fieldMod(1n - t))),
|
|
266
|
+
];
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Whether key decodes to a curve point that is not of small order (8·P ≠ identity).
|
|
271
|
+
* Not a prime-order check: a mixed-order key passes, as it does in Go.
|
|
272
|
+
*/
|
|
273
|
+
function isNotSmallOrder(key: Uint8Array): boolean {
|
|
274
|
+
let point = decodePoint(key);
|
|
275
|
+
if (point === null) return false;
|
|
276
|
+
for (let i = 0; i < 3; i++) point = addPoints(point, point);
|
|
277
|
+
return !(point[0] === 0n && point[1] === 1n);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/** Mirrors core.PayloadTypeKeyEnrollmentRequest. */
|
|
281
|
+
export const PAYLOAD_TYPE_KEY_ENROLLMENT_REQUEST = "burnledger.key_enrollment_request.v1";
|
|
282
|
+
/** Mirrors core.PayloadTypeSystemRegistrationRequest. */
|
|
283
|
+
export const PAYLOAD_TYPE_SYSTEM_REGISTRATION_REQUEST = "burnledger.system_registration_request.v1";
|
|
284
|
+
|
|
285
|
+
const UUID_RE = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/;
|
|
286
|
+
const NIL_UUID = "00000000-0000-0000-0000-000000000000";
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* A UUID in the one form Go prints it (uuid.UUID.String()): lowercase and
|
|
290
|
+
* hyphenated. The enclave rebuilds every customer-signed payload from the
|
|
291
|
+
* uuid.UUID it parsed, so a team id signed as the caller happened to type it —
|
|
292
|
+
* uppercase, say — would be different bytes, and the refusal would be a 401 with
|
|
293
|
+
* nothing in it to say why.
|
|
294
|
+
*/
|
|
295
|
+
function canonicalUuid(value: string, field: string): string {
|
|
296
|
+
if (typeof value !== "string" || !UUID_RE.test(value)) {
|
|
297
|
+
throw new Error(`${field} must be a UUID, got ${JSON.stringify(value)}`);
|
|
298
|
+
}
|
|
299
|
+
return value.toLowerCase();
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function requireSha256(value: Uint8Array, field: string): string {
|
|
303
|
+
if (!(value instanceof Uint8Array) || value.length !== 32) {
|
|
304
|
+
throw new Error(`${field} must be a 32-byte SHA-256 digest`);
|
|
305
|
+
}
|
|
306
|
+
return toHex(value);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* The canonical bytes EVERY member of a group signs to enrol it for a team.
|
|
311
|
+
*
|
|
312
|
+
* Port of core.BuildKeyEnrollmentRequestPayload. Without these signatures
|
|
313
|
+
* whoever relays the request could enrol public keys it does not hold — the
|
|
314
|
+
* customer's included — so the enclave demands one from each member, not a
|
|
315
|
+
* threshold: the statement it signs vouches for every key. `notAfter` is the
|
|
316
|
+
* latest the resulting authorization may end, and when the request itself stops
|
|
317
|
+
* being usable: the enclave cannot recognize a replay, so this is what keeps a
|
|
318
|
+
* relayed copy from renewing the window.
|
|
319
|
+
*/
|
|
320
|
+
export function buildKeyEnrollmentRequestPayload(request: {
|
|
321
|
+
assertedTeamId: string;
|
|
322
|
+
customerKeyId: string;
|
|
323
|
+
notAfter: Date;
|
|
324
|
+
}): Uint8Array {
|
|
325
|
+
if (!request.customerKeyId) throw new Error("customer_key_id is required");
|
|
326
|
+
return canonicalJson({
|
|
327
|
+
asserted_team_id: canonicalUuid(request.assertedTeamId, "assertedTeamId"),
|
|
328
|
+
customer_key_id: request.customerKeyId,
|
|
329
|
+
not_after: keyTime(request.notAfter),
|
|
330
|
+
payload_type: PAYLOAD_TYPE_KEY_ENROLLMENT_REQUEST,
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* The canonical bytes an enrolled group signs, at its threshold, to register one
|
|
336
|
+
* system under it.
|
|
337
|
+
*
|
|
338
|
+
* Port of core.BuildSystemRegistrationRequestPayload. Both digests are over bytes
|
|
339
|
+
* the CUSTOMER holds: `configDigest` is SHA-256 of the exact config bytes
|
|
340
|
+
* submitted when the system was created (the plaintext that was sealed), and
|
|
341
|
+
* `queryTemplateSha256` is SHA-256 of the template exactly as it will be sent —
|
|
342
|
+
* raw, not normalized. The enclave recomputes both from what it decrypted and
|
|
343
|
+
* was given, so a relay that substitutes either changes the bytes and the
|
|
344
|
+
* signatures fail.
|
|
345
|
+
*/
|
|
346
|
+
export function buildSystemRegistrationRequestPayload(request: {
|
|
347
|
+
assertedTeamId: string;
|
|
348
|
+
systemId: string;
|
|
349
|
+
configDigest: Uint8Array;
|
|
350
|
+
queryTemplateSha256: Uint8Array;
|
|
351
|
+
connectorType: string;
|
|
352
|
+
customerKeyId: string;
|
|
353
|
+
}): Uint8Array {
|
|
354
|
+
if (!request.customerKeyId) throw new Error("customer_key_id is required");
|
|
355
|
+
const systemId = canonicalUuid(request.systemId, "systemId");
|
|
356
|
+
if (systemId === NIL_UUID) throw new Error("system_id is required");
|
|
357
|
+
return canonicalJson({
|
|
358
|
+
asserted_team_id: canonicalUuid(request.assertedTeamId, "assertedTeamId"),
|
|
359
|
+
config_digest: requireSha256(request.configDigest, "configDigest"),
|
|
360
|
+
connector_type: request.connectorType,
|
|
361
|
+
customer_key_id: request.customerKeyId,
|
|
362
|
+
payload_type: PAYLOAD_TYPE_SYSTEM_REGISTRATION_REQUEST,
|
|
363
|
+
query_template_sha256: requireSha256(request.queryTemplateSha256, "queryTemplateSha256"),
|
|
364
|
+
system_id: systemId,
|
|
365
|
+
});
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// --- The documents the ENCLAVE signs -----------------------------------------
|
|
369
|
+
//
|
|
370
|
+
// Ports of core.VerifyKeyEnrollment, core.VerifyTeamAuthorization and
|
|
371
|
+
// core.VerifySystemRegistration. Each takes the key rather than finding one, as
|
|
372
|
+
// Go's do: a document that names the key vouching for it proves only that its
|
|
373
|
+
// author holds that key. The key to pass is the signing key of an enclave you
|
|
374
|
+
// attested against your pinned PCR0 (EnclaveIdentity.signingKey) -- the client's
|
|
375
|
+
// enrol, rotate and register calls use exactly that. Each reads the document as
|
|
376
|
+
// the API returns it, because the signed bytes are rebuilt from the wire
|
|
377
|
+
// strings (whole-second UTC timestamps, lowercase UUIDs), and throws
|
|
378
|
+
// VerificationError on anything that does not verify.
|
|
379
|
+
|
|
380
|
+
// v2 from wire protocol 12, and v2 is the only version a protocol-12 enclave
|
|
381
|
+
// issues or honors: a v1 document could have been minted by an earlier image
|
|
382
|
+
// without the customer's signature.
|
|
383
|
+
const PAYLOAD_TYPE_KEY_ENROLLMENT = "burnledger.key_enrollment.v2";
|
|
384
|
+
const PAYLOAD_TYPE_TEAM_AUTHORIZATION = "burnledger.team_authorization.v2";
|
|
385
|
+
const PAYLOAD_TYPE_SYSTEM_REGISTRATION = "burnledger.system_registration.v2";
|
|
386
|
+
|
|
387
|
+
type Doc = Record<string, unknown>;
|
|
388
|
+
|
|
389
|
+
function docObject(value: unknown, what: string): Doc {
|
|
390
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
391
|
+
throw new VerificationError(`${what} is not an object`);
|
|
392
|
+
}
|
|
393
|
+
return value as Doc;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
function docString(doc: Doc, what: string, field: string): string {
|
|
397
|
+
const value = doc[field];
|
|
398
|
+
if (typeof value !== "string") throw new VerificationError(`${what}.${field} is not a string`);
|
|
399
|
+
return value;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
function docKeyId(doc: Doc, what: string): string {
|
|
403
|
+
const value = docString(doc, what, "customer_key_id");
|
|
404
|
+
if (value === "") throw new VerificationError(`${what}.customer_key_id is empty`);
|
|
405
|
+
return value;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
function docUuid(doc: Doc, what: string, field: string): string {
|
|
409
|
+
const value = docString(doc, what, field);
|
|
410
|
+
if (!UUID_RE.test(value)) throw new VerificationError(`${what}.${field} is not a UUID`);
|
|
411
|
+
return value.toLowerCase();
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/** Go encodes [N]byte as an array of numbers. */
|
|
415
|
+
function docBytes(doc: Doc, what: string, field: string, length: number): Uint8Array {
|
|
416
|
+
const value = doc[field];
|
|
417
|
+
if (
|
|
418
|
+
!Array.isArray(value) ||
|
|
419
|
+
value.length !== length ||
|
|
420
|
+
!value.every((b) => Number.isInteger(b) && b >= 0 && b <= 255)
|
|
421
|
+
) {
|
|
422
|
+
throw new VerificationError(`${what}.${field} is not ${length} bytes`);
|
|
423
|
+
}
|
|
424
|
+
return new Uint8Array(value as number[]);
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
/** Go encodes []byte as padded standard base64. */
|
|
428
|
+
function base64Bytes(value: unknown, what: string): Uint8Array {
|
|
429
|
+
if (typeof value !== "string" || value.length % 4 !== 0 || !/^[A-Za-z0-9+/]*={0,2}$/.test(value)) {
|
|
430
|
+
throw new VerificationError(`${what} is not base64`);
|
|
431
|
+
}
|
|
432
|
+
const bin = atob(value);
|
|
433
|
+
const out = new Uint8Array(bin.length);
|
|
434
|
+
for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);
|
|
435
|
+
return out;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
async function requireEnclaveSignature(
|
|
439
|
+
crypto: CryptoOps,
|
|
440
|
+
enclaveKey: Uint8Array,
|
|
441
|
+
payload: Uint8Array,
|
|
442
|
+
signature: Uint8Array,
|
|
443
|
+
what: string,
|
|
444
|
+
): Promise<void> {
|
|
445
|
+
if (!(enclaveKey instanceof Uint8Array) || enclaveKey.length !== ED25519_PUBLIC_KEY_SIZE) {
|
|
446
|
+
throw new TypeError(`enclaveKey must be a ${ED25519_PUBLIC_KEY_SIZE}-byte Ed25519 public key`);
|
|
447
|
+
}
|
|
448
|
+
if (!(await crypto.ed25519Verify(enclaveKey, payload, signature))) {
|
|
449
|
+
const keyId = "dp_k_" + toHex(await crypto.sha256(enclaveKey));
|
|
450
|
+
throw new VerificationError(
|
|
451
|
+
`the ${what} is not signed by enclave key ${keyId}. Do not use it: a relay that ` +
|
|
452
|
+
"forged or altered one would produce exactly this.",
|
|
453
|
+
);
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
/**
|
|
458
|
+
* Verify the enclave's signature on a key enrolment statement (the `enrollment`
|
|
459
|
+
* object ENROLL_KEY and ROTATE_KEY return). Also refuses a statement whose
|
|
460
|
+
* `customer_key_id` is not the id its own group derives, as Go's payload
|
|
461
|
+
* builder does -- so a statement that verifies names exactly its members and
|
|
462
|
+
* threshold, and comparing its key id with yours compares those too.
|
|
463
|
+
*/
|
|
464
|
+
export async function verifyKeyEnrollmentStatement(
|
|
465
|
+
crypto: CryptoOps,
|
|
466
|
+
statement: unknown,
|
|
467
|
+
enclaveKey: Uint8Array,
|
|
468
|
+
): Promise<void> {
|
|
469
|
+
const what = "enrollment";
|
|
470
|
+
const doc = docObject(statement, what);
|
|
471
|
+
const rawGroup = docObject(doc.group, `${what}.group`);
|
|
472
|
+
if (!Array.isArray(rawGroup.members)) throw new VerificationError(`${what}.group.members is not an array`);
|
|
473
|
+
const group: CustomerKeyGroup = {
|
|
474
|
+
threshold: rawGroup.threshold as number,
|
|
475
|
+
members: rawGroup.members.map((m, i) => base64Bytes(m, `${what}.group.members[${i}]`)),
|
|
476
|
+
};
|
|
477
|
+
let derived: string;
|
|
478
|
+
try {
|
|
479
|
+
derived = await customerKeyId(crypto, group);
|
|
480
|
+
} catch (err) {
|
|
481
|
+
if (err instanceof InvalidKeyGroupError) throw new VerificationError(`${what}.group: ${err.message}`);
|
|
482
|
+
throw err;
|
|
483
|
+
}
|
|
484
|
+
const keyId = docKeyId(doc, what);
|
|
485
|
+
if (keyId !== derived) {
|
|
486
|
+
throw new VerificationError(`${what}.customer_key_id ${keyId} is not the id its own group derives (${derived})`);
|
|
487
|
+
}
|
|
488
|
+
const payload = canonicalJson({
|
|
489
|
+
asserted_team_id: docUuid(doc, what, "asserted_team_id"),
|
|
490
|
+
customer_key_id: keyId,
|
|
491
|
+
enclave_pcr0: docString(doc, what, "enclave_pcr0"),
|
|
492
|
+
enrolled_at: formatTimestamp(doc.enrolled_at, `${what}.enrolled_at`),
|
|
493
|
+
member_key_ids: await memberFingerprints(crypto, group),
|
|
494
|
+
payload_type: PAYLOAD_TYPE_KEY_ENROLLMENT,
|
|
495
|
+
threshold: group.threshold,
|
|
496
|
+
});
|
|
497
|
+
await requireEnclaveSignature(crypto, enclaveKey, payload, docBytes(doc, what, "signature", 64), "enrollment statement");
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
/**
|
|
501
|
+
* Verify the enclave's signature on a team authorization (the `authorization`
|
|
502
|
+
* object ENROLL_KEY and ROTATE_KEY return). The signature only: whether it is
|
|
503
|
+
* in force now is a separate question, as in Go.
|
|
504
|
+
*/
|
|
505
|
+
export async function verifyTeamAuthorization(
|
|
506
|
+
crypto: CryptoOps,
|
|
507
|
+
certificate: unknown,
|
|
508
|
+
enclaveKey: Uint8Array,
|
|
509
|
+
): Promise<void> {
|
|
510
|
+
const what = "authorization";
|
|
511
|
+
const doc = docObject(certificate, what);
|
|
512
|
+
const notBefore = formatTimestamp(doc.not_before, `${what}.not_before`);
|
|
513
|
+
const notAfter = formatTimestamp(doc.not_after, `${what}.not_after`);
|
|
514
|
+
// Go compares at full precision; the whole-second strings disagree with it
|
|
515
|
+
// only for a window shorter than a second, which no enclave issues.
|
|
516
|
+
if (notAfter <= notBefore) throw new VerificationError(`${what}.not_after is not after not_before`);
|
|
517
|
+
// prev_key_id is omitempty in Go: absent on a first enrolment, and absent
|
|
518
|
+
// from the signed payload too.
|
|
519
|
+
const prevKeyId = doc.prev_key_id == null ? "" : docString(doc, what, "prev_key_id");
|
|
520
|
+
const payload = canonicalJson({
|
|
521
|
+
asserted_team_id: docUuid(doc, what, "asserted_team_id"),
|
|
522
|
+
customer_key_id: docKeyId(doc, what),
|
|
523
|
+
not_after: notAfter,
|
|
524
|
+
not_before: notBefore,
|
|
525
|
+
payload_type: PAYLOAD_TYPE_TEAM_AUTHORIZATION,
|
|
526
|
+
...(prevKeyId === "" ? {} : { prev_key_id: prevKeyId }),
|
|
527
|
+
});
|
|
528
|
+
await requireEnclaveSignature(crypto, enclaveKey, payload, docBytes(doc, what, "signature", 64), "team authorization");
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
/** Verify the enclave's signature on a system registration (the
|
|
532
|
+
* `registration` object REGISTER_SYSTEM returns). */
|
|
533
|
+
export async function verifySystemRegistration(
|
|
534
|
+
crypto: CryptoOps,
|
|
535
|
+
certificate: unknown,
|
|
536
|
+
enclaveKey: Uint8Array,
|
|
537
|
+
): Promise<void> {
|
|
538
|
+
const what = "registration";
|
|
539
|
+
const doc = docObject(certificate, what);
|
|
540
|
+
const payload = canonicalJson({
|
|
541
|
+
config_digest: toHex(docBytes(doc, what, "config_digest", 32)),
|
|
542
|
+
connector_type: docString(doc, what, "connector_type"),
|
|
543
|
+
customer_key_id: docKeyId(doc, what),
|
|
544
|
+
payload_type: PAYLOAD_TYPE_SYSTEM_REGISTRATION,
|
|
545
|
+
query_template_hash: toHex(docBytes(doc, what, "query_template_hash", 32)),
|
|
546
|
+
registered_at: formatTimestamp(doc.registered_at, `${what}.registered_at`),
|
|
547
|
+
system_id: docUuid(doc, what, "system_id"),
|
|
548
|
+
});
|
|
549
|
+
await requireEnclaveSignature(crypto, enclaveKey, payload, docBytes(doc, what, "signature", 64), "system registration");
|
|
550
|
+
}
|