routex-settlement 0.2.0 → 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.
Files changed (3) hide show
  1. package/index.js +105 -71
  2. package/package.json +4 -3
  3. package/util.d.ts +1 -0
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 = {
@@ -366,46 +370,28 @@ function verifyAttestation(response, requirements, signingKeys) {
366
370
  }
367
371
  });
368
372
  }
369
- 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) {
370
392
  return __awaiter(this, void 0, void 0, function* () {
371
- const versionBytes = attestationReport.slice(0, 4);
372
- const version = new DataView(versionBytes.buffer).getInt32(0, true);
373
- let turinLike;
374
- switch (version) {
375
- case 0:
376
- case 1:
377
- throw new Error("Unsupported Attestation Report Version");
378
- case 2: {
379
- const chipId = attestationReport.slice(REPORT_OFFSETS.chipId, REPORT_OFFSETS.chipId + 64);
380
- if (equalBytes(chipId, new Uint8Array(64))) {
381
- throw new Error("Could not derive CPU family: MASK_CHIP_ID enabled");
382
- }
383
- turinLike = equalBytes(chipId.slice(8), new Uint8Array(56));
384
- break;
385
- }
386
- default: // from version 3 onwards CPUID_FAM_ID field should exist
387
- turinLike = attestationReport[REPORT_OFFSETS.cpuIdFamily] == 0x1a;
388
- }
389
393
  const reportedTcb = attestationReport.slice(REPORT_OFFSETS.reportedTcb, REPORT_OFFSETS.reportedTcb + 8);
390
- let tcb;
391
- if (turinLike) {
392
- tcb = {
393
- microcode: reportedTcb[7],
394
- snp: reportedTcb[3],
395
- tee: reportedTcb[2],
396
- bootloader: reportedTcb[1],
397
- fmc: reportedTcb[0],
398
- };
399
- }
400
- else {
401
- tcb = {
402
- microcode: reportedTcb[7],
403
- snp: reportedTcb[6],
404
- tee: reportedTcb[1],
405
- bootloader: reportedTcb[0],
406
- fmc: 0,
407
- };
408
- }
394
+ const tcb = parseTcbVersion(reportedTcb, product);
409
395
  if (!(tcb.microcode === vcekTcb.microcode &&
410
396
  tcb.snp === vcekTcb.snp &&
411
397
  tcb.tee === vcekTcb.tee &&
@@ -422,6 +408,10 @@ function _verifyReport(attestationReport, vcekChain, requirementsMap) {
422
408
  }
423
409
  const { product, publicKey, vcekTcb } = yield _verifyVcekChain(vcekChain, requirementsMap);
424
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
+ }
425
415
  if (!equalBytes(attestationReport.slice(REPORT_OFFSETS.signatureR + 48, REPORT_OFFSETS.signatureR + 72), new Uint8Array(24)) ||
426
416
  !equalBytes(attestationReport.slice(REPORT_OFFSETS.signatureS + 48, REPORT_OFFSETS.signatureS + 72), new Uint8Array(24))) {
427
417
  throw new Error("Unexpected signature bits");
@@ -450,18 +440,49 @@ function _verifyReport(attestationReport, vcekChain, requirementsMap) {
450
440
  0) {
451
441
  throw new Error("Verification failed: Alias check complete is false");
452
442
  }
453
- if (attestationReport[REPORT_OFFSETS.committedMajor] <
454
- requirements.minCommittedVersion[0] ||
455
- attestationReport[REPORT_OFFSETS.committedMinor] <
456
- requirements.minCommittedVersion[1] ||
457
- attestationReport[REPORT_OFFSETS.committedBuild] <
458
- 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) {
459
464
  throw new Error("Verification failed: Firmware version too small");
460
465
  }
461
- if (attestationReport[REPORT_OFFSETS.committedTcbSnp] <
462
- requirements.minCommittedTcbSnp) {
466
+ const committedTcb = parseTcbVersion(attestationReport.slice(REPORT_OFFSETS.committedTcb, REPORT_OFFSETS.committedTcb + 8), product);
467
+ if (committedTcb.snp < requirements.minCommittedTcbSnp) {
463
468
  throw new Error("Verification failed: SNP patch level too small");
464
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
+ }
465
486
  const cpuModel = attestationReport[REPORT_OFFSETS.cpuIdModel];
466
487
  const cpuStep = attestationReport[REPORT_OFFSETS.cpuIdStep];
467
488
  let requiredMicrocode;
@@ -473,7 +494,7 @@ function _verifyReport(attestationReport, vcekChain, requirementsMap) {
473
494
  }
474
495
  else if (cpuModel === 1 && cpuStep === 2) {
475
496
  // Milan-X
476
- requiredMicrocode = 0x45;
497
+ requiredMicrocode = 0x47;
477
498
  }
478
499
  break;
479
500
  }
@@ -497,29 +518,29 @@ function _verifyReport(attestationReport, vcekChain, requirementsMap) {
497
518
  case "Turin":
498
519
  if (cpuModel === 2 && cpuStep === 1) {
499
520
  // Turin Classic
500
- requiredMicrocode = 0x50;
521
+ requiredMicrocode = 0x51;
501
522
  }
502
523
  else if (cpuModel === 0x11 && cpuStep === 0) {
503
524
  // Turin Dense
504
- requiredMicrocode = 0x4d;
505
- }
506
- else if (cpuModel > 2 ||
507
- (cpuModel === 2 && cpuStep > 1) ||
508
- (cpuModel === 0x11 && cpuStep > 0)) {
509
- // Accept any newer models/steppings for Turin
510
- requiredMicrocode = 0;
525
+ requiredMicrocode = 0x4e;
511
526
  }
512
527
  }
513
528
  if (requiredMicrocode === undefined) {
514
529
  throw new Error("Verification failed: Report doesn't match any known CPU family");
515
530
  }
516
- const reportedMicrocode = attestationReport.slice(REPORT_OFFSETS.reportedTcb, REPORT_OFFSETS.reportedTcb + 8)[7];
517
- if (reportedMicrocode < requiredMicrocode) {
518
- 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");
519
533
  }
520
- yield _verifyTcbVersion(attestationReport, vcekTcb);
534
+ yield _verifyTcbVersion(attestationReport, product, vcekTcb);
521
535
  });
522
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
+ }
523
544
  function _verifyVcekChain(vcek, requirements) {
524
545
  return __awaiter(this, void 0, void 0, function* () {
525
546
  let cert;
@@ -540,19 +561,20 @@ function _verifyVcekChain(vcek, requirements) {
540
561
  }
541
562
  return new Uint8Array(ext.value);
542
563
  };
543
- const productIA5String = find_extension("1.3.6.1.4.1.3704.1.2", "product name");
544
- if (productIA5String[0] !== 0x16) {
545
- 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");
546
568
  }
547
- const product = String.fromCharCode(...productIA5String.slice(2)).split("-", 1)[0];
569
+ const product = productIA5String.valueBlock.value.split("-", 1)[0];
548
570
  if (!Object.prototype.hasOwnProperty.call(requirements, product)) {
549
571
  throw new Error(`Unexpected product: ${product}`);
550
572
  }
551
573
  const subjectPublicKeyInfo = AsnConvert.parse(chain[0].publicKey.rawData, SubjectPublicKeyInfo);
552
- const bootloader = find_extension("1.3.6.1.4.1.3704.1.3.1", "bootloader version")[2];
553
- const tee = find_extension("1.3.6.1.4.1.3704.1.3.2", "TEE")[2];
554
- const snp = find_extension("1.3.6.1.4.1.3704.1.3.3", "SNP version")[2];
555
- 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"));
556
578
  const tcb = {
557
579
  bootloader: bootloader,
558
580
  tee: tee,
@@ -562,7 +584,7 @@ function _verifyVcekChain(vcek, requirements) {
562
584
  };
563
585
  const fmc = chain[0].extensions.find((ext) => ext.type == "1.3.6.1.4.1.3704.1.3.9");
564
586
  if (fmc) {
565
- tcb.fmc = new Uint8Array(fmc.value)[2];
587
+ tcb.fmc = readExtensionInt(new Uint8Array(fmc.value));
566
588
  }
567
589
  return {
568
590
  product: product,
@@ -593,24 +615,35 @@ function _getSealNonce(ephemeralPublicKey, recipientPublicKey) {
593
615
  .digest();
594
616
  }
595
617
  const REPORT_OFFSETS = {
618
+ version: 0,
619
+ policy: 8,
620
+ vmpl: 48,
621
+ sigAlgo: 52,
596
622
  platInfo: 64,
623
+ keyInfo: 72,
597
624
  reportData: 80,
598
625
  measurement: 144,
599
626
  reportedTcb: 384,
600
- cpuIdFamily: 392,
601
627
  cpuIdModel: 393,
602
628
  cpuIdStep: 394,
603
- chipId: 416,
604
- committedTcbSnp: 486,
629
+ committedTcb: 480,
605
630
  committedBuild: 492,
606
631
  committedMinor: 493,
607
632
  committedMajor: 494,
633
+ launchMitVector: 504,
634
+ currentMitVector: 512,
608
635
  signatureR: 672,
609
636
  signatureS: 744,
610
637
  };
611
638
  const PLATFORM_INFO_BITS = {
612
639
  aliasCheckComplete: 5,
613
640
  };
641
+ const GUEST_POLICY_BITS = {
642
+ debugAllowed: 19,
643
+ };
644
+ const KEY_INFO_BITS = {
645
+ signingKey: 2,
646
+ };
614
647
  function bytesToUtf8(bytes) {
615
648
  return __awaiter(this, void 0, void 0, function* () {
616
649
  return new TextDecoder().decode(bytes);
@@ -622,6 +655,7 @@ if (typeof process !== "undefined" && process.env.NODE_ENV === "test") {
622
655
  _testing$1.YAXI_SIGNING_KEYS = YAXI_SIGNING_KEYS;
623
656
  _testing$1.verifyReport = _verifyReport;
624
657
  _testing$1.verifyTcbVersion = _verifyTcbVersion;
658
+ _testing$1.readExtensionInt = readExtensionInt;
625
659
  }
626
660
 
627
661
  class KeySettlement {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "routex-settlement",
3
- "version": "0.2.0",
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,6 +30,7 @@ type Requirements = {
30
30
  [id: string]: {
31
31
  minCommittedVersion: [number, number, number];
32
32
  minCommittedTcbSnp: number;
33
+ minMitVector?: number;
33
34
  };
34
35
  };
35
36
  export declare function verifyAttestation(response: SettlementResponse, requirements?: Requirements, signingKeys?: typeof YAXI_SIGNING_KEYS): Promise<void>;