routex-settlement 0.1.3 → 0.1.5

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.
Files changed (4) hide show
  1. package/index.d.ts +9 -9
  2. package/index.js +378 -268
  3. package/package.json +7 -4
  4. package/util.d.ts +40 -0
package/index.d.ts CHANGED
@@ -3,8 +3,9 @@ export declare class KeySettlement {
3
3
  private _onErrorResponse;
4
4
  private _onRequestError;
5
5
  private _secretKey;
6
- private _requirements;
7
- private _yaxiSigningKeys;
6
+ private _publicKey;
7
+ private _requirements?;
8
+ private _yaxiSigningKeys?;
8
9
  private _serverKey?;
9
10
  private _settlementPromise?;
10
11
  private _measurement?;
@@ -13,12 +14,11 @@ export declare class KeySettlement {
13
14
  measurement(): Uint8Array | undefined;
14
15
  private _getServerKey;
15
16
  getBase64SessionId(settlementHeaders?: HeadersInit): Promise<string>;
16
- seal(plaintext: Uint8Array, settlementHeaders?: HeadersInit): Promise<Uint8Array>;
17
- unseal(ciphertext: Uint8Array): Uint8Array;
18
- private _verifyAttestation;
19
- private _verifyTcbVersion;
20
- private _verifyReport;
21
- private _verifyVcekChain;
17
+ seal(plaintext: Uint8Array, settlementHeaders?: HeadersInit): Promise<Uint8Array<ArrayBuffer>>;
18
+ unseal(ciphertext: Uint8Array): Uint8Array<ArrayBuffer>;
22
19
  }
23
- export declare function binaryStringToBytes(binaryString: string): Uint8Array<ArrayBuffer>;
20
+ export declare function binaryStringToBytes(binaryString: string): Uint8Array;
24
21
  export declare function bytesToBinaryString(bytes: Uint8Array): string;
22
+ export declare const _testing: {
23
+ [Key: string]: unknown;
24
+ };
package/index.js CHANGED
@@ -1,37 +1,59 @@
1
- var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
2
- function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
3
- return new (P || (P = Promise))(function (resolve, reject) {
4
- function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
5
- function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
6
- function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
7
- step((generator = generator.apply(thisArg, _arguments || [])).next());
8
- });
1
+ import { chacha20poly1305 } from '@noble/ciphers/chacha.js';
2
+ import { blake2b } from '@noble/hashes/blake2.js';
3
+ import { hkdf } from '@noble/hashes/hkdf.js';
4
+ import { createHasher } from '@noble/hashes/utils.js';
5
+ import { equalBytes } from '@noble/curves/utils.js';
6
+ import { x25519, ed25519 } from '@noble/curves/ed25519.js';
7
+ import { p384 } from '@noble/curves/nist.js';
8
+ import { sha256 } from '@noble/hashes/sha2.js';
9
+ import { AsnConvert } from '@peculiar/asn1-schema';
10
+ import { SubjectPublicKeyInfo } from '@peculiar/asn1-x509';
11
+ import * as x509 from '@peculiar/x509';
12
+
13
+ /******************************************************************************
14
+ Copyright (c) Microsoft Corporation.
15
+
16
+ Permission to use, copy, modify, and/or distribute this software for any
17
+ purpose with or without fee is hereby granted.
18
+
19
+ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
20
+ REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
21
+ AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
22
+ INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
23
+ LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
24
+ OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
25
+ PERFORMANCE OF THIS SOFTWARE.
26
+ ***************************************************************************** */
27
+ /* global Reflect, Promise, SuppressedError, Symbol, Iterator */
28
+
29
+
30
+ function __awaiter(thisArg, _arguments, P, generator) {
31
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
32
+ return new (P || (P = Promise))(function (resolve, reject) {
33
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
34
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
35
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
36
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
37
+ });
38
+ }
39
+
40
+ typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) {
41
+ var e = new Error(message);
42
+ return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
9
43
  };
10
- import { chacha20poly1305 } from "@noble/ciphers/chacha";
11
- import { bytesToUtf8 } from "@noble/ciphers/utils";
12
- import { equalBytes } from "@noble/curves/utils";
13
- import { ed25519, x25519 } from "@noble/curves/ed25519";
14
- import { blake2b } from "@noble/hashes/blake2";
15
- import { p384 } from "@noble/curves/nist";
16
- import { hkdf } from "@noble/hashes/hkdf";
17
- import { createHasher } from "@noble/hashes/utils";
18
- import { sha256 } from "@noble/hashes/sha2";
19
- import * as x509 from "@peculiar/x509";
20
- import { AsnConvert } from "@peculiar/asn1-schema";
21
- import { SubjectPublicKeyInfo } from "@peculiar/asn1-x509";
44
+
22
45
  const REQUIREMENTS = {
23
46
  Genoa: {
24
- minCommittedVersion: [0x1, 0x37, 0x26],
25
- minCommittedTcbSnp: 0x16,
47
+ minCommittedVersion: [0x1, 0x37, 0x31],
48
+ minCommittedTcbSnp: 0x1b,
26
49
  },
27
50
  Milan: {
28
- minCommittedVersion: [0x1, 0x37, 0x16],
29
- minCommittedTcbSnp: 0x17,
51
+ minCommittedVersion: [0x1, 0x37, 0x23],
52
+ minCommittedTcbSnp: 0x1b,
30
53
  },
31
- // for Turin: SB-3019 doesn't list a TCB[SNP] version in the mitigation, only firmware version
32
54
  Turin: {
33
- minCommittedVersion: [0x1, 0x37, 0x3b],
34
- minCommittedTcbSnp: 0,
55
+ minCommittedVersion: [0x1, 0x37, 0x41],
56
+ minCommittedTcbSnp: 0x04,
35
57
  },
36
58
  };
37
59
  const YAXI_SIGNING_KEYS = {
@@ -288,17 +310,332 @@ zpacMwFusA==
288
310
  `),
289
311
  ],
290
312
  });
291
- export class KeySettlement {
313
+ function generateKey() {
314
+ const secret = x25519.utils.randomSecretKey();
315
+ return { secret, public: x25519.getPublicKey(secret) };
316
+ }
317
+ function seal(serverPublicKey, plaintext) {
318
+ return __awaiter(this, void 0, void 0, function* () {
319
+ const tagLength = 16;
320
+ const ephemeralSecretKey = x25519.utils.randomSecretKey();
321
+ const ephemeralPublicKey = x25519.getPublicKey(ephemeralSecretKey);
322
+ const nonce = _getSealNonce(ephemeralPublicKey, serverPublicKey);
323
+ const info = _getInfo(ephemeralPublicKey, serverPublicKey);
324
+ const cipher = _createCipher(serverPublicKey, ephemeralSecretKey, info, nonce);
325
+ const ciphertext = new Uint8Array(ephemeralPublicKey.length + tagLength + plaintext.length);
326
+ ciphertext.set(ephemeralPublicKey);
327
+ ciphertext.set(cipher.encrypt(plaintext), ephemeralPublicKey.length);
328
+ return ciphertext;
329
+ });
330
+ }
331
+ function unseal(clientKey, ciphertext) {
332
+ const ephemeralPublicKey = ciphertext.subarray(0, 32);
333
+ const publicKey = x25519.getPublicKey(clientKey.secret);
334
+ const nonce = _getSealNonce(ephemeralPublicKey, publicKey);
335
+ const info = _getInfo(ephemeralPublicKey, publicKey);
336
+ const cipher = _createCipher(ephemeralPublicKey, clientKey.secret, info, nonce);
337
+ return cipher.decrypt(ciphertext.subarray(32), new Uint8Array(ciphertext.length - 32 - chacha20poly1305.tagLength));
338
+ }
339
+ function verifyAttestation(response, requirements, signingKeys) {
340
+ return __awaiter(this, void 0, void 0, function* () {
341
+ const attestationReport = binaryStringToBytes$1(atob(response.attestationReport));
342
+ yield _verifyReport(attestationReport, response.vcek, requirements !== null && requirements !== void 0 ? requirements : REQUIREMENTS);
343
+ if (!equalBytes(attestationReport.slice(REPORT_OFFSETS.reportData, REPORT_OFFSETS.reportData + 32), sha256(binaryStringToBytes$1(atob(response.chachaBox))))) {
344
+ throw new Error("Data in attestation report doesn't match chacha box");
345
+ }
346
+ const encoder = new TextEncoder();
347
+ const launchMeasurementInSystemVersion = binaryStringToBytes$1(atob(response.systemVersion.launchMeasurement));
348
+ const input = new Uint8Array([
349
+ ...encoder.encode(response.systemVersion.kind),
350
+ ...encoder.encode(response.systemVersion.generation.toString()),
351
+ ...encoder.encode(
352
+ // clerk-report used to use Z for serialization, but not for the signature input
353
+ response.systemVersion.createdAt.replace("Z", "+00:00")),
354
+ ...encoder.encode(response.systemVersion.ref),
355
+ ...launchMeasurementInSystemVersion,
356
+ ]);
357
+ const key = (signingKeys !== null && signingKeys !== void 0 ? signingKeys : YAXI_SIGNING_KEYS)[response.systemVersion.signature.keyId];
358
+ if (key == null) {
359
+ throw new Error("The system version's signature key is unknown");
360
+ }
361
+ if (!ed25519.verify(binaryStringToBytes$1(atob(response.systemVersion.signature.value)), input, key)) {
362
+ throw new Error("Verification failed: Invalid system version signature");
363
+ }
364
+ if (!equalBytes(attestationReport.slice(REPORT_OFFSETS.measurement, REPORT_OFFSETS.measurement + 48), launchMeasurementInSystemVersion)) {
365
+ throw new Error("Reported measurement doesn't match expected measurement");
366
+ }
367
+ return launchMeasurementInSystemVersion;
368
+ });
369
+ }
370
+ function _verifyTcbVersion(attestationReport, vcekTcb) {
371
+ return __awaiter(this, void 0, void 0, function* () {
372
+ const versionBytes = attestationReport.slice(0, 4);
373
+ const version = new DataView(versionBytes.buffer).getInt32(0, true);
374
+ let turinLike;
375
+ switch (version) {
376
+ case 0:
377
+ case 1:
378
+ throw new Error("Unsupported Attestation Report Version");
379
+ case 2: {
380
+ const chipId = attestationReport.slice(REPORT_OFFSETS.chipId, 64);
381
+ if (equalBytes(chipId, new Uint8Array(64))) {
382
+ throw new Error("Could not derive CPU family: MASK_CHIP_ID enabled");
383
+ }
384
+ turinLike = equalBytes(chipId.slice(8), new Uint8Array(56));
385
+ break;
386
+ }
387
+ default: // from version 3 onwards CPUID_FAM_ID field should exist
388
+ turinLike = attestationReport[REPORT_OFFSETS.cpuIdFamily] == 0x1a;
389
+ }
390
+ const reportedTcb = attestationReport.slice(REPORT_OFFSETS.reportedTcb, REPORT_OFFSETS.reportedTcb + 8);
391
+ let tcb;
392
+ if (turinLike) {
393
+ tcb = {
394
+ microcode: reportedTcb[7],
395
+ snp: reportedTcb[3],
396
+ tee: reportedTcb[2],
397
+ bootloader: reportedTcb[1],
398
+ fmc: reportedTcb[0],
399
+ };
400
+ }
401
+ else {
402
+ tcb = {
403
+ microcode: reportedTcb[7],
404
+ snp: reportedTcb[6],
405
+ tee: reportedTcb[1],
406
+ bootloader: reportedTcb[0],
407
+ fmc: 0,
408
+ };
409
+ }
410
+ if (!(tcb.microcode === vcekTcb.microcode &&
411
+ tcb.snp === vcekTcb.snp &&
412
+ tcb.tee === vcekTcb.tee &&
413
+ tcb.bootloader === vcekTcb.bootloader &&
414
+ tcb.fmc === vcekTcb.fmc)) {
415
+ throw new Error("Verification failed: REPORTED_TCB doesn't match with VCEK's TCB_VERSION");
416
+ }
417
+ });
418
+ }
419
+ function _verifyReport(attestationReport, vcekChain, requirementsMap) {
420
+ return __awaiter(this, void 0, void 0, function* () {
421
+ if (attestationReport.length != 1184) {
422
+ throw new Error(`Attestation report has unexpected length: ${attestationReport.length}`);
423
+ }
424
+ const { product, publicKey, vcekTcb } = yield _verifyVcekChain(vcekChain, requirementsMap);
425
+ const requirements = requirementsMap[product];
426
+ if (!equalBytes(attestationReport.slice(REPORT_OFFSETS.signatureR + 48, REPORT_OFFSETS.signatureR + 72), new Uint8Array(24)) ||
427
+ !equalBytes(attestationReport.slice(REPORT_OFFSETS.signatureS + 48, REPORT_OFFSETS.signatureS + 72), new Uint8Array(24))) {
428
+ throw new Error("Unexpected signature bits");
429
+ }
430
+ const sig = new Uint8Array(96);
431
+ sig.set(attestationReport
432
+ .slice(REPORT_OFFSETS.signatureR, REPORT_OFFSETS.signatureR + 48)
433
+ // Change endianness
434
+ .reverse(), 0);
435
+ sig.set(attestationReport
436
+ .slice(REPORT_OFFSETS.signatureS, REPORT_OFFSETS.signatureS + 48)
437
+ // Change endianness
438
+ .reverse(), 48);
439
+ let valid;
440
+ try {
441
+ valid = p384.verify(sig, attestationReport.slice(0, 672), new Uint8Array(publicKey), { lowS: false });
442
+ }
443
+ catch (_a) {
444
+ valid = false;
445
+ }
446
+ if (!valid) {
447
+ throw new Error("Verification failed: Invalid attestation report signature");
448
+ }
449
+ if ((attestationReport[REPORT_OFFSETS.platInfo] &
450
+ (1 << PLATFORM_INFO_BITS.aliasCheckComplete)) ===
451
+ 0) {
452
+ throw new Error("Verification failed: Alias check complete is false");
453
+ }
454
+ if (attestationReport[REPORT_OFFSETS.committedMajor] <
455
+ requirements.minCommittedVersion[0] ||
456
+ attestationReport[REPORT_OFFSETS.committedMinor] <
457
+ requirements.minCommittedVersion[1] ||
458
+ attestationReport[REPORT_OFFSETS.committedBuild] <
459
+ requirements.minCommittedVersion[2]) {
460
+ throw new Error("Verification failed: Firmware version too small");
461
+ }
462
+ if (attestationReport[REPORT_OFFSETS.committedTcbSnp] <
463
+ requirements.minCommittedTcbSnp) {
464
+ throw new Error("Verification failed: SNP patch level too small");
465
+ }
466
+ const cpuModel = attestationReport[REPORT_OFFSETS.cpuIdModel];
467
+ const cpuStep = attestationReport[REPORT_OFFSETS.cpuIdStep];
468
+ let requiredMicrocode;
469
+ switch (product) {
470
+ case "Milan": {
471
+ if (cpuModel === 1 && cpuStep === 1) {
472
+ // Milan
473
+ requiredMicrocode = 0xde;
474
+ }
475
+ else if (cpuModel === 1 && cpuStep === 2) {
476
+ // Milan-X
477
+ requiredMicrocode = 0x45;
478
+ }
479
+ break;
480
+ }
481
+ case "Genoa": {
482
+ if (cpuModel === 0x11) {
483
+ if (cpuStep === 1) {
484
+ // Genoa
485
+ requiredMicrocode = 0x56;
486
+ }
487
+ else if (cpuStep === 2) {
488
+ // Genoa-X
489
+ requiredMicrocode = 0x51;
490
+ }
491
+ }
492
+ else if (cpuModel === 0xa0 && cpuStep === 2) {
493
+ // Bergamo/Siena
494
+ requiredMicrocode = 0x1b;
495
+ }
496
+ break;
497
+ }
498
+ case "Turin":
499
+ if (cpuModel === 2 && cpuStep === 1) {
500
+ // Turin Classic
501
+ requiredMicrocode = 0x50;
502
+ }
503
+ else if (cpuModel === 0x11 && cpuStep === 0) {
504
+ // Turin Dense
505
+ requiredMicrocode = 0x4d;
506
+ }
507
+ else if (cpuModel > 2 ||
508
+ (cpuModel === 2 && cpuStep > 1) ||
509
+ (cpuModel === 0x11 && cpuStep > 0)) {
510
+ // Accept any newer models/steppings for Turin
511
+ requiredMicrocode = 0;
512
+ }
513
+ }
514
+ if (requiredMicrocode === undefined) {
515
+ throw new Error("Verification failed: Report doesn't match any known CPU family");
516
+ }
517
+ const reportedMicrocode = attestationReport.slice(REPORT_OFFSETS.reportedTcb, REPORT_OFFSETS.reportedTcb + 8)[7];
518
+ if (reportedMicrocode < requiredMicrocode) {
519
+ throw new Error("Verification failed: Reported microcode version too small");
520
+ }
521
+ yield _verifyTcbVersion(attestationReport, vcekTcb);
522
+ });
523
+ }
524
+ function _verifyVcekChain(vcek, requirements) {
525
+ return __awaiter(this, void 0, void 0, function* () {
526
+ let cert;
527
+ try {
528
+ cert = new x509.X509Certificate(vcek);
529
+ }
530
+ catch (e) {
531
+ throw new Error(`Invalid VCEK: ${e}\n\nVCEK: ${vcek}`);
532
+ }
533
+ const chain = yield ROOT_STORE.build(cert);
534
+ if (chain.length != 3) {
535
+ throw new Error(`Certificate chain verification failed\n\nChain: ${chain}`);
536
+ }
537
+ const find_extension = (oid, type) => {
538
+ const ext = chain[0].extensions.find((ext) => ext.type == oid);
539
+ if (!ext) {
540
+ throw new Error(`Could not find ${type}\n\nExtensions: ${chain[0].extensions}`);
541
+ }
542
+ return new Uint8Array(ext.value);
543
+ };
544
+ const productIA5String = find_extension("1.3.6.1.4.1.3704.1.2", "product name");
545
+ if (productIA5String[0] !== 0x16) {
546
+ throw new Error(`Unexpected product extension: ${productIA5String}`);
547
+ }
548
+ const product = String.fromCharCode(...productIA5String.slice(2)).split("-", 1)[0];
549
+ if (!Object.prototype.hasOwnProperty.call(requirements, product)) {
550
+ throw new Error(`Unexpected product: ${product}`);
551
+ }
552
+ const subjectPublicKeyInfo = AsnConvert.parse(chain[0].publicKey.rawData, SubjectPublicKeyInfo);
553
+ const bootloader = find_extension("1.3.6.1.4.1.3704.1.3.1", "bootloader version")[2];
554
+ const tee = find_extension("1.3.6.1.4.1.3704.1.3.2", "TEE")[2];
555
+ const snp = find_extension("1.3.6.1.4.1.3704.1.3.3", "SNP version")[2];
556
+ const microcode = find_extension("1.3.6.1.4.1.3704.1.3.8", "microcode")[2];
557
+ const tcb = {
558
+ bootloader: bootloader,
559
+ tee: tee,
560
+ snp: snp,
561
+ microcode: microcode,
562
+ fmc: 0,
563
+ };
564
+ const fmc = chain[0].extensions.find((ext) => ext.type == "1.3.6.1.4.1.3704.1.3.9");
565
+ if (fmc) {
566
+ tcb.fmc = new Uint8Array(fmc.value)[2];
567
+ }
568
+ return {
569
+ product: product,
570
+ publicKey: new Uint8Array(subjectPublicKeyInfo.subjectPublicKey),
571
+ vcekTcb: tcb,
572
+ };
573
+ });
574
+ }
575
+ function binaryStringToBytes$1(binaryString) {
576
+ return Uint8Array.from(binaryString, (c) => c.charCodeAt(0));
577
+ }
578
+ function _createCipher(publicKey, secretKey, info, nonce) {
579
+ const sharedSecret = x25519.getSharedSecret(secretKey, publicKey);
580
+ const key = hkdf(createHasher(() => blake2b.create({ dkLen: 64 })), sharedSecret, Uint8Array.from([]), info, 32);
581
+ return chacha20poly1305(key, nonce);
582
+ }
583
+ function _getInfo(ephemeralPublicKey, recipientPublicKey) {
584
+ const info = new Uint8Array(ephemeralPublicKey.length + recipientPublicKey.length);
585
+ info.set(ephemeralPublicKey);
586
+ info.set(recipientPublicKey, ephemeralPublicKey.length);
587
+ return info;
588
+ }
589
+ function _getSealNonce(ephemeralPublicKey, recipientPublicKey) {
590
+ return blake2b
591
+ .create({ dkLen: 12 })
592
+ .update(ephemeralPublicKey)
593
+ .update(recipientPublicKey)
594
+ .digest();
595
+ }
596
+ const REPORT_OFFSETS = {
597
+ platInfo: 64,
598
+ reportData: 80,
599
+ measurement: 144,
600
+ reportedTcb: 384,
601
+ cpuIdFamily: 392,
602
+ cpuIdModel: 393,
603
+ cpuIdStep: 394,
604
+ chipId: 416,
605
+ committedTcbSnp: 486,
606
+ committedBuild: 492,
607
+ committedMinor: 493,
608
+ committedMajor: 494,
609
+ signatureR: 672,
610
+ signatureS: 744,
611
+ };
612
+ const PLATFORM_INFO_BITS = {
613
+ aliasCheckComplete: 5,
614
+ };
615
+ function bytesToUtf8(bytes) {
616
+ return __awaiter(this, void 0, void 0, function* () {
617
+ return new TextDecoder().decode(bytes);
618
+ });
619
+ }
620
+ const _testing$1 = {};
621
+ if (typeof process !== "undefined" && process.env.NODE_ENV === "test") {
622
+ _testing$1.REQUIREMENTS = REQUIREMENTS;
623
+ _testing$1.YAXI_SIGNING_KEYS = YAXI_SIGNING_KEYS;
624
+ _testing$1.verifyReport = _verifyReport;
625
+ _testing$1.verifyTcbVersion = _verifyTcbVersion;
626
+ }
627
+
628
+ class KeySettlement {
292
629
  constructor(url, onErrorResponse, onRequestError) {
293
- this._requirements = REQUIREMENTS;
294
- this._yaxiSigningKeys = YAXI_SIGNING_KEYS;
295
630
  this._url = url;
296
631
  this._onErrorResponse = onErrorResponse;
297
632
  this._onRequestError =
298
633
  onRequestError !== null && onRequestError !== void 0 ? onRequestError : ((e) => {
299
634
  throw e;
300
635
  });
301
- this._secretKey = x25519.utils.randomSecretKey();
636
+ const { secret, public: pub } = generateKey();
637
+ this._secretKey = secret;
638
+ this._publicKey = pub;
302
639
  }
303
640
  _settle(headers) {
304
641
  return __awaiter(this, void 0, void 0, function* () {
@@ -306,7 +643,7 @@ export class KeySettlement {
306
643
  method: "POST",
307
644
  headers: headers,
308
645
  body: JSON.stringify({
309
- publicKey: btoa(bytesToBinaryString(x25519.getPublicKey(this._secretKey))),
646
+ publicKey: btoa(bytesToBinaryString(this._publicKey)),
310
647
  }),
311
648
  })
312
649
  .catch(this._onRequestError)
@@ -317,8 +654,8 @@ export class KeySettlement {
317
654
  throw yield this._onErrorResponse(response);
318
655
  }
319
656
  const responseData = yield response.json();
320
- const measurement = yield this._verifyAttestation(responseData);
321
- const settlementBoxMessage = JSON.parse(bytesToUtf8(this.unseal(binaryStringToBytes(atob(responseData.chachaBox)))));
657
+ const measurement = yield verifyAttestation(responseData, this._requirements, this._yaxiSigningKeys);
658
+ const settlementBoxMessage = JSON.parse(yield bytesToUtf8(this.unseal(binaryStringToBytes(atob(responseData.chachaBox)))));
322
659
  this._serverKey = {
323
660
  publicKey: binaryStringToBytes(atob(settlementBoxMessage.publicKey)),
324
661
  base64SessionId: settlementBoxMessage.sessionId,
@@ -349,246 +686,19 @@ export class KeySettlement {
349
686
  return __awaiter(this, arguments, void 0, function* (plaintext, settlementHeaders = {}) {
350
687
  const serverPublicKey = (yield this._getServerKey(settlementHeaders))
351
688
  .publicKey;
352
- const tagLength = 16;
353
- const ephemeralSecretKey = x25519.utils.randomSecretKey();
354
- const ephemeralPublicKey = x25519.getPublicKey(ephemeralSecretKey);
355
- const nonce = _getSealNonce(ephemeralPublicKey, serverPublicKey);
356
- const info = _getInfo(ephemeralPublicKey, serverPublicKey);
357
- const cipher = _createCipher(serverPublicKey, ephemeralSecretKey, info, nonce);
358
- const ciphertext = new Uint8Array(ephemeralPublicKey.length + tagLength + plaintext.length);
359
- ciphertext.set(ephemeralPublicKey);
360
- ciphertext.set(cipher.encrypt(plaintext), ephemeralPublicKey.length);
361
- return ciphertext;
689
+ return yield seal(serverPublicKey, plaintext);
362
690
  });
363
691
  }
364
692
  unseal(ciphertext) {
365
- const ephemeralPublicKey = ciphertext.subarray(0, 32);
366
- const publicKey = x25519.getPublicKey(this._secretKey);
367
- const nonce = _getSealNonce(ephemeralPublicKey, publicKey);
368
- const info = _getInfo(ephemeralPublicKey, publicKey);
369
- const cipher = _createCipher(ephemeralPublicKey, this._secretKey, info, nonce);
370
- return cipher.decrypt(ciphertext.subarray(32));
371
- }
372
- _verifyAttestation(response) {
373
- return __awaiter(this, void 0, void 0, function* () {
374
- const attestationReport = binaryStringToBytes(atob(response.attestationReport));
375
- yield this._verifyReport(attestationReport, response.vcek);
376
- if (!equalBytes(attestationReport.slice(REPORT_OFFSETS.reportData, REPORT_OFFSETS.reportData + 32), sha256(binaryStringToBytes(atob(response.chachaBox))))) {
377
- throw new Error("Data in attestation report doesn't match chacha box");
378
- }
379
- const encoder = new TextEncoder();
380
- const launchMeasurementInSystemVersion = binaryStringToBytes(atob(response.systemVersion.launchMeasurement));
381
- const input = new Uint8Array([
382
- ...encoder.encode(response.systemVersion.kind),
383
- ...encoder.encode(response.systemVersion.generation.toString()),
384
- ...encoder.encode(
385
- // clerk-report used to use Z for serialization, but not for the signature input
386
- response.systemVersion.createdAt.replace("Z", "+00:00")),
387
- ...encoder.encode(response.systemVersion.ref),
388
- ...launchMeasurementInSystemVersion,
389
- ]);
390
- const key = this._yaxiSigningKeys[response.systemVersion.signature.keyId];
391
- if (key == null) {
392
- throw new Error("The system version's signature key is unknown");
393
- }
394
- if (!ed25519.verify(binaryStringToBytes(atob(response.systemVersion.signature.value)), input, key)) {
395
- throw new Error("Verification failed: Invalid system version signature");
396
- }
397
- if (!equalBytes(attestationReport.slice(REPORT_OFFSETS.measurement, REPORT_OFFSETS.measurement + 48), launchMeasurementInSystemVersion)) {
398
- throw new Error("Reported measurement doesn't match expected measurement");
399
- }
400
- return launchMeasurementInSystemVersion;
401
- });
402
- }
403
- _verifyTcbVersion(attestationReport, vcekTcb) {
404
- return __awaiter(this, void 0, void 0, function* () {
405
- const versionBytes = attestationReport.slice(0, 4);
406
- const version = new DataView(versionBytes.buffer).getInt32(0, true);
407
- let turinLike;
408
- switch (version) {
409
- case 0:
410
- case 1:
411
- throw new Error("Unsupported Attestation Report Version");
412
- case 2: {
413
- const chipId = attestationReport.slice(REPORT_OFFSETS.chipId, 64);
414
- if (equalBytes(chipId, new Uint8Array(64))) {
415
- throw new Error("Could not derive CPU family: MASK_CHIP_ID enabled");
416
- }
417
- turinLike = equalBytes(chipId.slice(8), new Uint8Array(56));
418
- break;
419
- }
420
- default: // from version 3 onwards CPUID_FAM_ID field should exist
421
- turinLike = attestationReport[REPORT_OFFSETS.cpuIdFamily] == 0x1a;
422
- }
423
- const reportedTcb = attestationReport.slice(REPORT_OFFSETS.reportedTcb, REPORT_OFFSETS.reportedTcb + 8);
424
- let tcb;
425
- if (turinLike) {
426
- tcb = {
427
- microcode: reportedTcb[7],
428
- snp: reportedTcb[3],
429
- tee: reportedTcb[2],
430
- bootloader: reportedTcb[1],
431
- fmc: reportedTcb[0],
432
- };
433
- }
434
- else {
435
- tcb = {
436
- microcode: reportedTcb[7],
437
- snp: reportedTcb[6],
438
- tee: reportedTcb[1],
439
- bootloader: reportedTcb[0],
440
- fmc: 0,
441
- };
442
- }
443
- if (!(tcb.microcode === vcekTcb.microcode &&
444
- tcb.snp === vcekTcb.snp &&
445
- tcb.tee === vcekTcb.tee &&
446
- tcb.bootloader === vcekTcb.bootloader &&
447
- tcb.fmc === vcekTcb.fmc)) {
448
- throw new Error("Verification failed: REPORTED_TCB doesn't match with VCEK's TCB_VERSION");
449
- }
450
- });
451
- }
452
- _verifyReport(attestationReport, vcekChain) {
453
- return __awaiter(this, void 0, void 0, function* () {
454
- if (attestationReport.length != 1184) {
455
- throw new Error(`Attestation report has unexpected length: ${attestationReport.length}`);
456
- }
457
- const { requirements, publicKey, vcekTcb } = yield this._verifyVcekChain(vcekChain);
458
- if (!equalBytes(attestationReport.slice(REPORT_OFFSETS.signatureR + 48, REPORT_OFFSETS.signatureR + 72), new Uint8Array(24)) ||
459
- !equalBytes(attestationReport.slice(REPORT_OFFSETS.signatureS + 48, REPORT_OFFSETS.signatureS + 72), new Uint8Array(24))) {
460
- throw new Error("Unexpected signature bits");
461
- }
462
- const sig = new Uint8Array(96);
463
- sig.set(attestationReport
464
- .slice(REPORT_OFFSETS.signatureR, REPORT_OFFSETS.signatureR + 48)
465
- // Change endianness
466
- .reverse(), 0);
467
- sig.set(attestationReport
468
- .slice(REPORT_OFFSETS.signatureS, REPORT_OFFSETS.signatureS + 48)
469
- // Change endianness
470
- .reverse(), 48);
471
- let valid;
472
- try {
473
- valid = p384.verify(sig, attestationReport.slice(0, 672), new Uint8Array(publicKey), { lowS: false });
474
- }
475
- catch (_a) {
476
- valid = false;
477
- }
478
- if (!valid) {
479
- throw new Error("Verification failed: Invalid attestation report signature");
480
- }
481
- if ((attestationReport[REPORT_OFFSETS.platInfo] &
482
- (1 << PLATFORM_INFO_BITS.aliasCheckComplete)) ===
483
- 0) {
484
- throw new Error("Verification failed: Alias check complete is false");
485
- }
486
- if (attestationReport[REPORT_OFFSETS.committedMajor] <
487
- requirements.minCommittedVersion[0] ||
488
- attestationReport[REPORT_OFFSETS.committedMinor] <
489
- requirements.minCommittedVersion[1] ||
490
- attestationReport[REPORT_OFFSETS.committedBuild] <
491
- requirements.minCommittedVersion[2]) {
492
- throw new Error("Verification failed: Firmware version too small");
493
- }
494
- if (attestationReport[REPORT_OFFSETS.committedTcbSnp] <
495
- requirements.minCommittedTcbSnp) {
496
- throw new Error("Verification failed: SNP patch level too small");
497
- }
498
- yield this._verifyTcbVersion(attestationReport, vcekTcb);
499
- });
500
- }
501
- _verifyVcekChain(vcek) {
502
- return __awaiter(this, void 0, void 0, function* () {
503
- let cert;
504
- try {
505
- cert = new x509.X509Certificate(vcek);
506
- }
507
- catch (e) {
508
- throw new Error(`Invalid VCEK: ${e}\n\nVCEK: ${vcek}`);
509
- }
510
- const chain = yield ROOT_STORE.build(cert);
511
- if (chain.length != 3) {
512
- throw new Error(`Certificate chain verification failed\n\nChain: ${chain}`);
513
- }
514
- const find_extension = (oid, type) => {
515
- const ext = chain[0].extensions.find((ext) => ext.type == oid);
516
- if (!ext) {
517
- throw new Error(`Could not find ${type}\n\nExtensions: ${chain[0].extensions}`);
518
- }
519
- return new Uint8Array(ext.value);
520
- };
521
- const productIA5String = find_extension("1.3.6.1.4.1.3704.1.2", "product name");
522
- if (productIA5String[0] !== 0x16) {
523
- throw new Error(`Unexpected product extension: ${productIA5String}`);
524
- }
525
- const product = String.fromCharCode(...productIA5String.slice(2)).split("-", 1)[0];
526
- if (!Object.prototype.hasOwnProperty.call(this._requirements, product)) {
527
- throw new Error(`Unexpected product: ${product}`);
528
- }
529
- const subjectPublicKeyInfo = AsnConvert.parse(chain[0].publicKey.rawData, SubjectPublicKeyInfo);
530
- const bootloader = find_extension("1.3.6.1.4.1.3704.1.3.1", "bootloader version")[2];
531
- const tee = find_extension("1.3.6.1.4.1.3704.1.3.2", "TEE")[2];
532
- const snp = find_extension("1.3.6.1.4.1.3704.1.3.3", "SNP version")[2];
533
- const microcode = find_extension("1.3.6.1.4.1.3704.1.3.8", "microcode")[2];
534
- const tcb = {
535
- bootloader: bootloader,
536
- tee: tee,
537
- snp: snp,
538
- microcode: microcode,
539
- fmc: 0,
540
- };
541
- const fmc = chain[0].extensions.find((ext) => ext.type == "1.3.6.1.4.1.3704.1.3.9");
542
- if (fmc) {
543
- tcb.fmc = new Uint8Array(fmc.value)[2];
544
- }
545
- return {
546
- requirements: this._requirements[product],
547
- publicKey: new Uint8Array(subjectPublicKeyInfo.subjectPublicKey),
548
- vcekTcb: tcb,
549
- };
550
- });
693
+ return unseal({ secret: this._secretKey, public: this._publicKey }, ciphertext);
551
694
  }
552
695
  }
553
- export function binaryStringToBytes(binaryString) {
696
+ function binaryStringToBytes(binaryString) {
554
697
  return Uint8Array.from(binaryString, (c) => c.charCodeAt(0));
555
698
  }
556
- export function bytesToBinaryString(bytes) {
699
+ function bytesToBinaryString(bytes) {
557
700
  return String.fromCharCode.apply(null, [...bytes]);
558
701
  }
559
- function _createCipher(publicKey, secretKey, info, nonce) {
560
- const sharedSecret = x25519.getSharedSecret(secretKey, publicKey);
561
- const key = hkdf(createHasher(() => blake2b.create({ dkLen: 64 })), sharedSecret, Uint8Array.from([]), info, 32);
562
- return chacha20poly1305(key, nonce);
563
- }
564
- function _getInfo(ephemeralPublicKey, recipientPublicKey) {
565
- const info = new Uint8Array(ephemeralPublicKey.length + recipientPublicKey.length);
566
- info.set(ephemeralPublicKey);
567
- info.set(recipientPublicKey, ephemeralPublicKey.length);
568
- return info;
569
- }
570
- function _getSealNonce(ephemeralPublicKey, recipientPublicKey) {
571
- return blake2b
572
- .create({ dkLen: 12 })
573
- .update(ephemeralPublicKey)
574
- .update(recipientPublicKey)
575
- .digest();
576
- }
577
- const REPORT_OFFSETS = {
578
- sigAlgo: 52,
579
- platInfo: 64,
580
- reportData: 80,
581
- measurement: 144,
582
- reportedTcb: 384,
583
- cpuIdFamily: 392,
584
- chipId: 416,
585
- committedTcbSnp: 486,
586
- committedBuild: 492,
587
- committedMinor: 493,
588
- committedMajor: 494,
589
- signatureR: 672,
590
- signatureS: 744,
591
- };
592
- const PLATFORM_INFO_BITS = {
593
- aliasCheckComplete: 5,
594
- };
702
+ const _testing = _testing$1;
703
+
704
+ export { KeySettlement, _testing, binaryStringToBytes, bytesToBinaryString };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "routex-settlement",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
4
4
  "description": "Key settlement for the YAXI routex client",
5
5
  "homepage": "https://yaxi.tech",
6
6
  "author": "YAXI GmbH",
@@ -11,14 +11,15 @@
11
11
  "types": "./index.d.ts",
12
12
  "files": [
13
13
  "index.d.ts",
14
- "index.js"
14
+ "index.js",
15
+ "util.d.ts"
15
16
  ],
16
17
  "scripts": {
17
- "build": "tsc --build",
18
+ "build": "rollup --input index.ts --dir . --plugin typescript --external @noble/ciphers/chacha.js,@noble/curves/ed25519.js,@noble/curves/nist.js,@noble/curves/utils.js,@noble/hashes/blake2.js,@noble/hashes/hkdf.js,@noble/hashes/sha2.js,@noble/hashes/utils.js,@peculiar/asn1-schema,@peculiar/asn1-x509,@peculiar/x509",
18
19
  "clean": "tsc --build --clean",
19
20
  "fmt": "prettier --write .",
20
21
  "lint": "eslint",
21
- "test": "tsc --build && NODE_OPTIONS=\"$NODE_OPTIONS --experimental-vm-modules\" jest"
22
+ "test": "npm run build && NODE_OPTIONS=\"$NODE_OPTIONS --experimental-vm-modules\" jest"
22
23
  },
23
24
  "dependencies": {
24
25
  "@noble/ciphers": "^2.0.0",
@@ -30,10 +31,12 @@
30
31
  },
31
32
  "devDependencies": {
32
33
  "@eslint/js": "^9.21.0",
34
+ "@rollup/plugin-typescript": "^12.1.4",
33
35
  "eslint": "^9.21.0",
34
36
  "globals": "^16.0.0",
35
37
  "jest": "^30.1.3",
36
38
  "prettier": "^3.6.2",
39
+ "rollup": "^4.52.2",
37
40
  "typescript": "^5.7.3",
38
41
  "typescript-eslint": "^8.24.1"
39
42
  }
package/util.d.ts ADDED
@@ -0,0 +1,40 @@
1
+ declare const YAXI_SIGNING_KEYS: {
2
+ [id: string]: Uint8Array | undefined;
3
+ };
4
+ export declare function generateKey(): {
5
+ secret: Uint8Array;
6
+ public: Uint8Array;
7
+ };
8
+ export declare function seal(serverPublicKey: Uint8Array, plaintext: Uint8Array): Promise<Uint8Array<ArrayBuffer>>;
9
+ export declare function unseal(clientKey: {
10
+ secret: Uint8Array;
11
+ public: Uint8Array;
12
+ }, ciphertext: Uint8Array): Uint8Array<ArrayBuffer>;
13
+ type SettlementResponse = {
14
+ attestationReport: string;
15
+ vcek: string;
16
+ chachaBox: string;
17
+ systemVersion: {
18
+ kind: string;
19
+ generation: number;
20
+ createdAt: string;
21
+ ref: string;
22
+ launchMeasurement: string;
23
+ signature: {
24
+ keyId: string;
25
+ value: string;
26
+ };
27
+ };
28
+ };
29
+ type Requirements = {
30
+ [id: string]: {
31
+ minCommittedVersion: [number, number, number];
32
+ minCommittedTcbSnp: number;
33
+ };
34
+ };
35
+ export declare function verifyAttestation(response: SettlementResponse, requirements?: Requirements, signingKeys?: typeof YAXI_SIGNING_KEYS): Promise<Uint8Array>;
36
+ export declare function bytesToUtf8(bytes: Uint8Array): Promise<string>;
37
+ export declare const _testing: {
38
+ [Key: string]: unknown;
39
+ };
40
+ export {};