routex-settlement 0.1.5 → 0.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/index.d.ts CHANGED
@@ -8,10 +8,10 @@ export declare class KeySettlement {
8
8
  private _yaxiSigningKeys?;
9
9
  private _serverKey?;
10
10
  private _settlementPromise?;
11
- private _measurement?;
11
+ private _systemVersion?;
12
12
  constructor(url: URL, onErrorResponse: (response: Response) => void, onRequestError?: (e: Error) => never);
13
13
  private _settle;
14
- measurement(): Uint8Array | undefined;
14
+ systemVersion(): string | undefined;
15
15
  private _getServerKey;
16
16
  getBase64SessionId(settlementHeaders?: HeadersInit): Promise<string>;
17
17
  seal(plaintext: Uint8Array, settlementHeaders?: HeadersInit): Promise<Uint8Array<ArrayBuffer>>;
package/index.js CHANGED
@@ -9,6 +9,7 @@ import { sha256 } from '@noble/hashes/sha2.js';
9
9
  import { AsnConvert } from '@peculiar/asn1-schema';
10
10
  import { SubjectPublicKeyInfo } from '@peculiar/asn1-x509';
11
11
  import * as x509 from '@peculiar/x509';
12
+ import { fromBER, Integer, IA5String } from 'asn1js';
12
13
 
13
14
  /******************************************************************************
14
15
  Copyright (c) Microsoft Corporation.
@@ -46,14 +47,17 @@ const REQUIREMENTS = {
46
47
  Genoa: {
47
48
  minCommittedVersion: [0x1, 0x37, 0x31],
48
49
  minCommittedTcbSnp: 0x1b,
50
+ minMitVector: (1 << 0) | (1 << 1),
49
51
  },
50
52
  Milan: {
51
53
  minCommittedVersion: [0x1, 0x37, 0x23],
52
54
  minCommittedTcbSnp: 0x1b,
55
+ minMitVector: 1 << 1,
53
56
  },
54
57
  Turin: {
55
58
  minCommittedVersion: [0x1, 0x37, 0x41],
56
59
  minCommittedTcbSnp: 0x04,
60
+ minMitVector: (1 << 0) | (1 << 1) | (1 << 2) | (1 << 4) | (1 << 5),
57
61
  },
58
62
  };
59
63
  const YAXI_SIGNING_KEYS = {
@@ -364,49 +368,30 @@ function verifyAttestation(response, requirements, signingKeys) {
364
368
  if (!equalBytes(attestationReport.slice(REPORT_OFFSETS.measurement, REPORT_OFFSETS.measurement + 48), launchMeasurementInSystemVersion)) {
365
369
  throw new Error("Reported measurement doesn't match expected measurement");
366
370
  }
367
- return launchMeasurementInSystemVersion;
368
371
  });
369
372
  }
370
- function _verifyTcbVersion(attestationReport, vcekTcb) {
373
+ function parseTcbVersion(tcbBytes, product) {
374
+ if (product === "Turin") {
375
+ return {
376
+ fmc: tcbBytes[0],
377
+ bootloader: tcbBytes[1],
378
+ tee: tcbBytes[2],
379
+ snp: tcbBytes[3],
380
+ microcode: tcbBytes[7],
381
+ };
382
+ }
383
+ return {
384
+ bootloader: tcbBytes[0],
385
+ tee: tcbBytes[1],
386
+ snp: tcbBytes[6],
387
+ microcode: tcbBytes[7],
388
+ fmc: 0,
389
+ };
390
+ }
391
+ function _verifyTcbVersion(attestationReport, product, vcekTcb) {
371
392
  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
393
  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
- }
394
+ const tcb = parseTcbVersion(reportedTcb, product);
410
395
  if (!(tcb.microcode === vcekTcb.microcode &&
411
396
  tcb.snp === vcekTcb.snp &&
412
397
  tcb.tee === vcekTcb.tee &&
@@ -423,6 +408,10 @@ function _verifyReport(attestationReport, vcekChain, requirementsMap) {
423
408
  }
424
409
  const { product, publicKey, vcekTcb } = yield _verifyVcekChain(vcekChain, requirementsMap);
425
410
  const requirements = requirementsMap[product];
411
+ const sigAlgo = attestationReport[REPORT_OFFSETS.sigAlgo];
412
+ if (sigAlgo !== 0x1) {
413
+ throw new Error(`Verification failed: Unsupported signature algorithm ${sigAlgo}, only ECDSA P-384 with SHA-384 (0x1) is supported`);
414
+ }
426
415
  if (!equalBytes(attestationReport.slice(REPORT_OFFSETS.signatureR + 48, REPORT_OFFSETS.signatureR + 72), new Uint8Array(24)) ||
427
416
  !equalBytes(attestationReport.slice(REPORT_OFFSETS.signatureS + 48, REPORT_OFFSETS.signatureS + 72), new Uint8Array(24))) {
428
417
  throw new Error("Unexpected signature bits");
@@ -451,18 +440,49 @@ function _verifyReport(attestationReport, vcekChain, requirementsMap) {
451
440
  0) {
452
441
  throw new Error("Verification failed: Alias check complete is false");
453
442
  }
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]) {
443
+ const debugAllowed = attestationReport[REPORT_OFFSETS.policy + Math.floor(GUEST_POLICY_BITS.debugAllowed / 8)] &
444
+ (1 << GUEST_POLICY_BITS.debugAllowed % 8);
445
+ if (debugAllowed) {
446
+ throw new Error("Verification failed: Debug mode is enabled in guest policy");
447
+ }
448
+ const vmpl = new DataView(attestationReport.buffer, attestationReport.byteOffset + REPORT_OFFSETS.vmpl, 4).getUint32(0, true);
449
+ if (vmpl > 3) {
450
+ throw new Error(`Verification failed: Unexpected VMPL ${vmpl}`);
451
+ }
452
+ const signingKey = (attestationReport[REPORT_OFFSETS.keyInfo] >> KEY_INFO_BITS.signingKey) &
453
+ ((1 << 3) - 1);
454
+ if (signingKey !== 0) {
455
+ throw new Error("Verification failed: Report is not signed by a VCEK");
456
+ }
457
+ const reportedVersion = (attestationReport[REPORT_OFFSETS.committedMajor] << 16) |
458
+ (attestationReport[REPORT_OFFSETS.committedMinor] << 8) |
459
+ attestationReport[REPORT_OFFSETS.committedBuild];
460
+ const requiredVersion = (requirements.minCommittedVersion[0] << 16) |
461
+ (requirements.minCommittedVersion[1] << 8) |
462
+ requirements.minCommittedVersion[2];
463
+ if (reportedVersion < requiredVersion) {
460
464
  throw new Error("Verification failed: Firmware version too small");
461
465
  }
462
- if (attestationReport[REPORT_OFFSETS.committedTcbSnp] <
463
- requirements.minCommittedTcbSnp) {
466
+ const committedTcb = parseTcbVersion(attestationReport.slice(REPORT_OFFSETS.committedTcb, REPORT_OFFSETS.committedTcb + 8), product);
467
+ if (committedTcb.snp < requirements.minCommittedTcbSnp) {
464
468
  throw new Error("Verification failed: SNP patch level too small");
465
469
  }
470
+ if (requirements.minMitVector !== undefined) {
471
+ const reportView = new DataView(attestationReport.buffer, attestationReport.byteOffset, attestationReport.byteLength);
472
+ const reportVersion = reportView.getUint32(REPORT_OFFSETS.version, true);
473
+ const min = BigInt(requirements.minMitVector);
474
+ const checkMitVector = (offset, label) => {
475
+ if (reportVersion < 5) {
476
+ throw new Error(`Verification failed: ${label} mitigation vector not present in report`);
477
+ }
478
+ const value = reportView.getBigUint64(offset, true);
479
+ if ((value & min) !== min) {
480
+ throw new Error(`Verification failed: ${label} mitigation vector required bits not set`);
481
+ }
482
+ };
483
+ checkMitVector(REPORT_OFFSETS.currentMitVector, "Current");
484
+ checkMitVector(REPORT_OFFSETS.launchMitVector, "Launch");
485
+ }
466
486
  const cpuModel = attestationReport[REPORT_OFFSETS.cpuIdModel];
467
487
  const cpuStep = attestationReport[REPORT_OFFSETS.cpuIdStep];
468
488
  let requiredMicrocode;
@@ -474,7 +494,7 @@ function _verifyReport(attestationReport, vcekChain, requirementsMap) {
474
494
  }
475
495
  else if (cpuModel === 1 && cpuStep === 2) {
476
496
  // Milan-X
477
- requiredMicrocode = 0x45;
497
+ requiredMicrocode = 0x47;
478
498
  }
479
499
  break;
480
500
  }
@@ -498,29 +518,29 @@ function _verifyReport(attestationReport, vcekChain, requirementsMap) {
498
518
  case "Turin":
499
519
  if (cpuModel === 2 && cpuStep === 1) {
500
520
  // Turin Classic
501
- requiredMicrocode = 0x50;
521
+ requiredMicrocode = 0x51;
502
522
  }
503
523
  else if (cpuModel === 0x11 && cpuStep === 0) {
504
524
  // 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;
525
+ requiredMicrocode = 0x4e;
512
526
  }
513
527
  }
514
528
  if (requiredMicrocode === undefined) {
515
529
  throw new Error("Verification failed: Report doesn't match any known CPU family");
516
530
  }
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");
531
+ if (committedTcb.microcode < requiredMicrocode) {
532
+ throw new Error("Verification failed: Committed microcode version too small");
520
533
  }
521
- yield _verifyTcbVersion(attestationReport, vcekTcb);
534
+ yield _verifyTcbVersion(attestationReport, product, vcekTcb);
522
535
  });
523
536
  }
537
+ function readExtensionInt(ext) {
538
+ const { result } = fromBER(ext);
539
+ if (!(result instanceof Integer)) {
540
+ throw new Error("Expected ASN.1 INTEGER");
541
+ }
542
+ return result.valueBlock.valueDec;
543
+ }
524
544
  function _verifyVcekChain(vcek, requirements) {
525
545
  return __awaiter(this, void 0, void 0, function* () {
526
546
  let cert;
@@ -541,19 +561,20 @@ function _verifyVcekChain(vcek, requirements) {
541
561
  }
542
562
  return new Uint8Array(ext.value);
543
563
  };
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}`);
564
+ const productExtension = find_extension("1.3.6.1.4.1.3704.1.2", "product name");
565
+ const { result: productIA5String } = fromBER(productExtension);
566
+ if (!(productIA5String instanceof IA5String)) {
567
+ throw new Error("Expected ASN.1 IA5String for product extension");
547
568
  }
548
- const product = String.fromCharCode(...productIA5String.slice(2)).split("-", 1)[0];
569
+ const product = productIA5String.valueBlock.value.split("-", 1)[0];
549
570
  if (!Object.prototype.hasOwnProperty.call(requirements, product)) {
550
571
  throw new Error(`Unexpected product: ${product}`);
551
572
  }
552
573
  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];
574
+ const bootloader = readExtensionInt(find_extension("1.3.6.1.4.1.3704.1.3.1", "bootloader version"));
575
+ const tee = readExtensionInt(find_extension("1.3.6.1.4.1.3704.1.3.2", "TEE"));
576
+ const snp = readExtensionInt(find_extension("1.3.6.1.4.1.3704.1.3.3", "SNP version"));
577
+ const microcode = readExtensionInt(find_extension("1.3.6.1.4.1.3704.1.3.8", "microcode"));
557
578
  const tcb = {
558
579
  bootloader: bootloader,
559
580
  tee: tee,
@@ -563,7 +584,7 @@ function _verifyVcekChain(vcek, requirements) {
563
584
  };
564
585
  const fmc = chain[0].extensions.find((ext) => ext.type == "1.3.6.1.4.1.3704.1.3.9");
565
586
  if (fmc) {
566
- tcb.fmc = new Uint8Array(fmc.value)[2];
587
+ tcb.fmc = readExtensionInt(new Uint8Array(fmc.value));
567
588
  }
568
589
  return {
569
590
  product: product,
@@ -594,24 +615,35 @@ function _getSealNonce(ephemeralPublicKey, recipientPublicKey) {
594
615
  .digest();
595
616
  }
596
617
  const REPORT_OFFSETS = {
618
+ version: 0,
619
+ policy: 8,
620
+ vmpl: 48,
621
+ sigAlgo: 52,
597
622
  platInfo: 64,
623
+ keyInfo: 72,
598
624
  reportData: 80,
599
625
  measurement: 144,
600
626
  reportedTcb: 384,
601
- cpuIdFamily: 392,
602
627
  cpuIdModel: 393,
603
628
  cpuIdStep: 394,
604
- chipId: 416,
605
- committedTcbSnp: 486,
629
+ committedTcb: 480,
606
630
  committedBuild: 492,
607
631
  committedMinor: 493,
608
632
  committedMajor: 494,
633
+ launchMitVector: 504,
634
+ currentMitVector: 512,
609
635
  signatureR: 672,
610
636
  signatureS: 744,
611
637
  };
612
638
  const PLATFORM_INFO_BITS = {
613
639
  aliasCheckComplete: 5,
614
640
  };
641
+ const GUEST_POLICY_BITS = {
642
+ debugAllowed: 19,
643
+ };
644
+ const KEY_INFO_BITS = {
645
+ signingKey: 2,
646
+ };
615
647
  function bytesToUtf8(bytes) {
616
648
  return __awaiter(this, void 0, void 0, function* () {
617
649
  return new TextDecoder().decode(bytes);
@@ -623,6 +655,7 @@ if (typeof process !== "undefined" && process.env.NODE_ENV === "test") {
623
655
  _testing$1.YAXI_SIGNING_KEYS = YAXI_SIGNING_KEYS;
624
656
  _testing$1.verifyReport = _verifyReport;
625
657
  _testing$1.verifyTcbVersion = _verifyTcbVersion;
658
+ _testing$1.readExtensionInt = readExtensionInt;
626
659
  }
627
660
 
628
661
  class KeySettlement {
@@ -654,17 +687,17 @@ class KeySettlement {
654
687
  throw yield this._onErrorResponse(response);
655
688
  }
656
689
  const responseData = yield response.json();
657
- const measurement = yield verifyAttestation(responseData, this._requirements, this._yaxiSigningKeys);
690
+ yield verifyAttestation(responseData, this._requirements, this._yaxiSigningKeys);
658
691
  const settlementBoxMessage = JSON.parse(yield bytesToUtf8(this.unseal(binaryStringToBytes(atob(responseData.chachaBox)))));
659
692
  this._serverKey = {
660
693
  publicKey: binaryStringToBytes(atob(settlementBoxMessage.publicKey)),
661
694
  base64SessionId: settlementBoxMessage.sessionId,
662
695
  };
663
- this._measurement = measurement;
696
+ this._systemVersion = JSON.stringify(responseData.systemVersion);
664
697
  });
665
698
  }
666
- measurement() {
667
- return this._measurement;
699
+ systemVersion() {
700
+ return this._systemVersion;
668
701
  }
669
702
  _getServerKey(settlementHeaders) {
670
703
  return __awaiter(this, void 0, void 0, function* () {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "routex-settlement",
3
- "version": "0.1.5",
3
+ "version": "0.2.1",
4
4
  "description": "Key settlement for the YAXI routex client",
5
5
  "homepage": "https://yaxi.tech",
6
6
  "author": "YAXI GmbH",
@@ -15,7 +15,7 @@
15
15
  "util.d.ts"
16
16
  ],
17
17
  "scripts": {
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
+ "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,asn1js",
19
19
  "clean": "tsc --build --clean",
20
20
  "fmt": "prettier --write .",
21
21
  "lint": "eslint",
@@ -27,7 +27,8 @@
27
27
  "@noble/hashes": "^2.0.0",
28
28
  "@peculiar/asn1-schema": "^2.3.15",
29
29
  "@peculiar/asn1-x509": "^2.3.15",
30
- "@peculiar/x509": "^1.12.3"
30
+ "@peculiar/x509": "^1.12.3",
31
+ "asn1js": "^3.0.6"
31
32
  },
32
33
  "devDependencies": {
33
34
  "@eslint/js": "^9.21.0",
package/util.d.ts CHANGED
@@ -30,9 +30,10 @@ type Requirements = {
30
30
  [id: string]: {
31
31
  minCommittedVersion: [number, number, number];
32
32
  minCommittedTcbSnp: number;
33
+ minMitVector?: number;
33
34
  };
34
35
  };
35
- export declare function verifyAttestation(response: SettlementResponse, requirements?: Requirements, signingKeys?: typeof YAXI_SIGNING_KEYS): Promise<Uint8Array>;
36
+ export declare function verifyAttestation(response: SettlementResponse, requirements?: Requirements, signingKeys?: typeof YAXI_SIGNING_KEYS): Promise<void>;
36
37
  export declare function bytesToUtf8(bytes: Uint8Array): Promise<string>;
37
38
  export declare const _testing: {
38
39
  [Key: string]: unknown;