routex-settlement 0.1.1 → 0.1.3

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.d.ts +4 -1
  2. package/index.js +242 -50
  3. package/package.json +7 -5
package/index.d.ts CHANGED
@@ -1,12 +1,14 @@
1
1
  export declare class KeySettlement {
2
2
  private _url;
3
3
  private _onErrorResponse;
4
+ private _onRequestError;
4
5
  private _secretKey;
5
6
  private _requirements;
6
7
  private _yaxiSigningKeys;
7
8
  private _serverKey?;
9
+ private _settlementPromise?;
8
10
  private _measurement?;
9
- constructor(url: URL, onErrorResponse: (response: Response) => void);
11
+ constructor(url: URL, onErrorResponse: (response: Response) => void, onRequestError?: (e: Error) => never);
10
12
  private _settle;
11
13
  measurement(): Uint8Array | undefined;
12
14
  private _getServerKey;
@@ -14,6 +16,7 @@ export declare class KeySettlement {
14
16
  seal(plaintext: Uint8Array, settlementHeaders?: HeadersInit): Promise<Uint8Array>;
15
17
  unseal(ciphertext: Uint8Array): Uint8Array;
16
18
  private _verifyAttestation;
19
+ private _verifyTcbVersion;
17
20
  private _verifyReport;
18
21
  private _verifyVcekChain;
19
22
  }
package/index.js CHANGED
@@ -9,12 +9,12 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
9
9
  };
10
10
  import { chacha20poly1305 } from "@noble/ciphers/chacha";
11
11
  import { bytesToUtf8 } from "@noble/ciphers/utils";
12
- import { bytesToNumberLE, equalBytes } from "@noble/curves/abstract/utils";
12
+ import { equalBytes } from "@noble/curves/utils";
13
13
  import { ed25519, x25519 } from "@noble/curves/ed25519";
14
- import { blake2b } from "@noble/hashes/blake2b";
15
- import { p384 } from "@noble/curves/p384";
14
+ import { blake2b } from "@noble/hashes/blake2";
15
+ import { p384 } from "@noble/curves/nist";
16
16
  import { hkdf } from "@noble/hashes/hkdf";
17
- import { wrapConstructor } from "@noble/hashes/utils";
17
+ import { createHasher } from "@noble/hashes/utils";
18
18
  import { sha256 } from "@noble/hashes/sha2";
19
19
  import * as x509 from "@peculiar/x509";
20
20
  import { AsnConvert } from "@peculiar/asn1-schema";
@@ -22,11 +22,16 @@ import { SubjectPublicKeyInfo } from "@peculiar/asn1-x509";
22
22
  const REQUIREMENTS = {
23
23
  Genoa: {
24
24
  minCommittedVersion: [0x1, 0x37, 0x26],
25
- minCommitedTcbSnp: 0x16,
25
+ minCommittedTcbSnp: 0x16,
26
26
  },
27
27
  Milan: {
28
28
  minCommittedVersion: [0x1, 0x37, 0x16],
29
- minCommitedTcbSnp: 0x17,
29
+ minCommittedTcbSnp: 0x17,
30
+ },
31
+ // for Turin: SB-3019 doesn't list a TCB[SNP] version in the mitigation, only firmware version
32
+ Turin: {
33
+ minCommittedVersion: [0x1, 0x37, 0x3b],
34
+ minCommittedTcbSnp: 0,
30
35
  },
31
36
  };
32
37
  const YAXI_SIGNING_KEYS = {
@@ -200,16 +205,100 @@ JZJ0+tuPMKmBnSH860llKk+VpVQsgqbzDIvOLvD6W1Umq25boxCYJ+TuBoa4s+HH
200
205
  CViAvgT9kf/rBq1d+ivj6skkHxuzcxbk1xv6ZGxrteJxVH7KlX7YRdZ6eARKwLe4
201
206
  AFZEAwoKCQ==
202
207
  -----END CERTIFICATE-----
208
+ `),
209
+ // ARK-Turin
210
+ new x509.X509Certificate(`
211
+ -----BEGIN CERTIFICATE-----
212
+ MIIGiTCCBDigAwIBAgIDAwABMEYGCSqGSIb3DQEBCjA5oA8wDQYJYIZIAWUDBAIC
213
+ BQChHDAaBgkqhkiG9w0BAQgwDQYJYIZIAWUDBAICBQCiAwIBMKMDAgEBMHsxFDAS
214
+ BgNVBAsMC0VuZ2luZWVyaW5nMQswCQYDVQQGEwJVUzEUMBIGA1UEBwwLU2FudGEg
215
+ Q2xhcmExCzAJBgNVBAgMAkNBMR8wHQYDVQQKDBZBZHZhbmNlZCBNaWNybyBEZXZp
216
+ Y2VzMRIwEAYDVQQDDAlBUkstVHVyaW4wHhcNMjMwNTE1MjAyNTIxWhcNNDgwNTE1
217
+ MjAyNTIxWjB7MRQwEgYDVQQLDAtFbmdpbmVlcmluZzELMAkGA1UEBhMCVVMxFDAS
218
+ BgNVBAcMC1NhbnRhIENsYXJhMQswCQYDVQQIDAJDQTEfMB0GA1UECgwWQWR2YW5j
219
+ ZWQgTWljcm8gRGV2aWNlczESMBAGA1UEAwwJU0VWLVR1cmluMIICIjANBgkqhkiG
220
+ 9w0BAQEFAAOCAg8AMIICCgKCAgEAnvg5Grv2Emd9lAhKdO64RXU3UESb6JTm0Hhz
221
+ evx1PyxinxYqJL329qTJM0XmdozLYb7rsHxgM5I2pU18M8gect2pN/YB2LQ1/bIq
222
+ 37TPDbg7ym0MN6KkZ6aERxAX0voYtdDyNxjDAUjpRpCe1FccAev/Es2n/Fz1G1Tm
223
+ C2XepTQqaKpmt6mnDWSCHCVsQoY0gSibeaG6doM6OiNUCbKXaC7KHH5b/96BD1DJ
224
+ 84M+JHqPClFhHqUJwzKF5Qxj4wgWAZzK8UPhiNGjrF6+TBdlFGdSzEqw1jOrCTHd
225
+ uYyLK+5OQ3OIw4S+vZeOVoxJajTIWdsqYP2DLc0HkL0qWOumEOrrc2/4DeETShB0
226
+ MyIpH05kSalyQN2eN5P6ptOB84hddCdbJPEepnD+FqQap1ukw3K8uBcgeBSAF23r
227
+ 6UtT8Uc5h7MsWX3MoZiEHcSkDQQ8IedTk7CLjsK6S7b/lfKqfYiRhKgGkRvsEd/M
228
+ DNcumHZKIgzasJwgagzSggiUo9jXp3EWm84fqyxNXzSutPB7qD5P/ULAB+q9Qgvr
229
+ zC8XneaLP0MNrHhM80UejmsBTIktMvFoWVIelYDLdcoi0eMD5DRccfsgrYaY6h/+
230
+ /qf9tgg+mX09UJpuSPRF38oyqnNNFMl5v/tWLgUsChPU6NCQC17Qaqr8mu2ynyyu
231
+ HEs5JVUCAwEAAaOBozCBoDAdBgNVHQ4EFgQUbYJXt6v2sMgUALjxD0WvG9aq628w
232
+ HwYDVR0jBBgwFoAUZKBfceMMCmTYO3XlAVmeK+4GA0QwEgYDVR0TAQH/BAgwBgEB
233
+ /wIBADAOBgNVHQ8BAf8EBAMCAQQwOgYDVR0fBDMwMTAvoC2gK4YpaHR0cHM6Ly9r
234
+ ZHNpbnRmLmFtZC5jb20vdmNlay92MS9UdXJpbi9jcmwwRgYJKoZIhvcNAQEKMDmg
235
+ DzANBglghkgBZQMEAgIFAKEcMBoGCSqGSIb3DQEBCDANBglghkgBZQMEAgIFAKID
236
+ AgEwowMCAQEDggIBAAXWJ3DPahralt5kXLPMm9oKlFRqeU3HcS7kA+VBlBA1lQRU
237
+ hXkbXnTvW1GZcgdZvNCB/VlET61KbCzoFIhPIESVjjb/xWX2kg3X0HHmh1EtCDbH
238
+ aUFM5rq6l+S1h7qOauRZebvrwApDzAANvW0LTHRumfGm/kqh9NDtVCIWPUZ1VQIg
239
+ Gx1T3dwmgOK8ncT1J3W5xIyS0Xu3KC6w7oBlq8G2pPgTcCBJ4JBCTXCEXiAAGaTR
240
+ /TJIaSzoZFLhxYhCMjP8WQGToPGDK2i/lZhkcGHnJOQ+lgrXfpLGqBtLlS3QODyV
241
+ P0MomczG4dqw3THP3Y8Aq9c2KE7SylAKsS/bBKCqkj4OrABkDSkMQEz3BBoFD63a
242
+ D5ZG/Qiz+tmhnptyPVcweC9uJlSWYm25KiV4lT52uBjxatDZKQcrpdgcU8+ozzKU
243
+ 8ICnZPOwfWeyuNMq/juyd/rzg5IePyyvt+13aJ5MlZBXZxJKoxCYIMKUwZigf0Xs
244
+ BteT8gw10/xk5smIFIB2ERtTQPMuTENgrPTUjOeiqmBg663c2dLVol+MDiT4ltqf
245
+ Em4Kl/cc4f+H6bEwhj1QKAN2ipRf+mP0NfzJb+6ZHNsOvyq/WByYpLXV9JJoiDW/
246
+ 8RZwPU/Mn7IuQBauCy78G7FS0ta3q1et74faYBBgeJ6awEasa25CvmsmlU0R
247
+ -----END CERTIFICATE-----
248
+ `),
249
+ // SEV-Turin
250
+ new x509.X509Certificate(`
251
+ -----BEGIN CERTIFICATE-----
252
+ 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
286
+ zpacMwFusA==
287
+ -----END CERTIFICATE-----
203
288
  `),
204
289
  ],
205
290
  });
206
291
  export class KeySettlement {
207
- constructor(url, onErrorResponse) {
292
+ constructor(url, onErrorResponse, onRequestError) {
208
293
  this._requirements = REQUIREMENTS;
209
294
  this._yaxiSigningKeys = YAXI_SIGNING_KEYS;
210
295
  this._url = url;
211
296
  this._onErrorResponse = onErrorResponse;
212
- this._secretKey = x25519.utils.randomPrivateKey();
297
+ this._onRequestError =
298
+ onRequestError !== null && onRequestError !== void 0 ? onRequestError : ((e) => {
299
+ throw e;
300
+ });
301
+ this._secretKey = x25519.utils.randomSecretKey();
213
302
  }
214
303
  _settle(headers) {
215
304
  return __awaiter(this, void 0, void 0, function* () {
@@ -219,6 +308,10 @@ export class KeySettlement {
219
308
  body: JSON.stringify({
220
309
  publicKey: btoa(bytesToBinaryString(x25519.getPublicKey(this._secretKey))),
221
310
  }),
311
+ })
312
+ .catch(this._onRequestError)
313
+ .finally(() => {
314
+ this._settlementPromise = undefined;
222
315
  });
223
316
  if (response.status >= 400) {
224
317
  throw yield this._onErrorResponse(response);
@@ -226,10 +319,10 @@ export class KeySettlement {
226
319
  const responseData = yield response.json();
227
320
  const measurement = yield this._verifyAttestation(responseData);
228
321
  const settlementBoxMessage = JSON.parse(bytesToUtf8(this.unseal(binaryStringToBytes(atob(responseData.chachaBox)))));
229
- this._serverKey = [
230
- binaryStringToBytes(atob(settlementBoxMessage.publicKey)),
231
- settlementBoxMessage.sessionId,
232
- ];
322
+ this._serverKey = {
323
+ publicKey: binaryStringToBytes(atob(settlementBoxMessage.publicKey)),
324
+ base64SessionId: settlementBoxMessage.sessionId,
325
+ };
233
326
  this._measurement = measurement;
234
327
  });
235
328
  }
@@ -239,21 +332,25 @@ export class KeySettlement {
239
332
  _getServerKey(settlementHeaders) {
240
333
  return __awaiter(this, void 0, void 0, function* () {
241
334
  if (this._serverKey == null) {
242
- yield this._settle(settlementHeaders);
335
+ if (this._settlementPromise == null) {
336
+ this._settlementPromise = this._settle(settlementHeaders);
337
+ }
338
+ yield this._settlementPromise;
243
339
  }
244
340
  return this._serverKey;
245
341
  });
246
342
  }
247
343
  getBase64SessionId() {
248
344
  return __awaiter(this, arguments, void 0, function* (settlementHeaders = {}) {
249
- return (yield this._getServerKey(settlementHeaders))[1];
345
+ return (yield this._getServerKey(settlementHeaders)).base64SessionId;
250
346
  });
251
347
  }
252
348
  seal(plaintext_1) {
253
349
  return __awaiter(this, arguments, void 0, function* (plaintext, settlementHeaders = {}) {
254
- const serverPublicKey = (yield this._getServerKey(settlementHeaders))[0];
350
+ const serverPublicKey = (yield this._getServerKey(settlementHeaders))
351
+ .publicKey;
255
352
  const tagLength = 16;
256
- const ephemeralSecretKey = x25519.utils.randomPrivateKey();
353
+ const ephemeralSecretKey = x25519.utils.randomSecretKey();
257
354
  const ephemeralPublicKey = x25519.getPublicKey(ephemeralSecretKey);
258
355
  const nonce = _getSealNonce(ephemeralPublicKey, serverPublicKey);
259
356
  const info = _getInfo(ephemeralPublicKey, serverPublicKey);
@@ -280,13 +377,15 @@ export class KeySettlement {
280
377
  throw new Error("Data in attestation report doesn't match chacha box");
281
378
  }
282
379
  const encoder = new TextEncoder();
283
- const launchMeasurement = binaryStringToBytes(atob(response.systemVersion.launchMeasurement));
380
+ const launchMeasurementInSystemVersion = binaryStringToBytes(atob(response.systemVersion.launchMeasurement));
284
381
  const input = new Uint8Array([
285
382
  ...encoder.encode(response.systemVersion.kind),
286
383
  ...encoder.encode(response.systemVersion.generation.toString()),
287
- ...encoder.encode(response.systemVersion.createdAt.replace("Z", "+00:00")),
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")),
288
387
  ...encoder.encode(response.systemVersion.ref),
289
- ...launchMeasurement,
388
+ ...launchMeasurementInSystemVersion,
290
389
  ]);
291
390
  const key = this._yaxiSigningKeys[response.systemVersion.signature.keyId];
292
391
  if (key == null) {
@@ -295,36 +394,108 @@ export class KeySettlement {
295
394
  if (!ed25519.verify(binaryStringToBytes(atob(response.systemVersion.signature.value)), input, key)) {
296
395
  throw new Error("Verification failed: Invalid system version signature");
297
396
  }
298
- if (!equalBytes(attestationReport.slice(REPORT_OFFSETS.measurement, REPORT_OFFSETS.measurement + 48), launchMeasurement)) {
397
+ if (!equalBytes(attestationReport.slice(REPORT_OFFSETS.measurement, REPORT_OFFSETS.measurement + 48), launchMeasurementInSystemVersion)) {
299
398
  throw new Error("Reported measurement doesn't match expected measurement");
300
399
  }
301
- return launchMeasurement;
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
+ }
302
450
  });
303
451
  }
304
452
  _verifyReport(attestationReport, vcekChain) {
305
453
  return __awaiter(this, void 0, void 0, function* () {
306
- const { requirements, publicKey } = yield this._verifyVcekChain(vcekChain);
307
- const sig = {
308
- r: bytesToNumberLE(attestationReport.slice(REPORT_OFFSETS.signatureR, REPORT_OFFSETS.signatureR + 48)),
309
- s: bytesToNumberLE(attestationReport.slice(REPORT_OFFSETS.signatureS, REPORT_OFFSETS.signatureS + 48)),
310
- };
311
- if (!p384.verify(sig, attestationReport.slice(0, 672), new Uint8Array(publicKey), { prehash: true })) {
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) {
312
479
  throw new Error("Verification failed: Invalid attestation report signature");
313
480
  }
314
- if ((attestationReport[REPORT_OFFSETS.platInfo] & (1 << 5)) == 0) {
481
+ if ((attestationReport[REPORT_OFFSETS.platInfo] &
482
+ (1 << PLATFORM_INFO_BITS.aliasCheckComplete)) ===
483
+ 0) {
315
484
  throw new Error("Verification failed: Alias check complete is false");
316
485
  }
317
- if ([
318
- attestationReport[REPORT_OFFSETS.committedMajor],
319
- attestationReport[REPORT_OFFSETS.committedMinor],
320
- attestationReport[REPORT_OFFSETS.committedBuild],
321
- ] < requirements.minCommittedVersion) {
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]) {
322
492
  throw new Error("Verification failed: Firmware version too small");
323
493
  }
324
494
  if (attestationReport[REPORT_OFFSETS.committedTcbSnp] <
325
- requirements.minCommitedTcbSnp) {
495
+ requirements.minCommittedTcbSnp) {
326
496
  throw new Error("Verification failed: SNP patch level too small");
327
497
  }
498
+ yield this._verifyTcbVersion(attestationReport, vcekTcb);
328
499
  });
329
500
  }
330
501
  _verifyVcekChain(vcek) {
@@ -340,39 +511,54 @@ export class KeySettlement {
340
511
  if (chain.length != 3) {
341
512
  throw new Error(`Certificate chain verification failed\n\nChain: ${chain}`);
342
513
  }
343
- const productExt = chain[0].extensions.find((ext) => ext.type == "1.3.6.1.4.1.3704.1.2");
344
- if (!productExt) {
345
- throw new Error(`Could not find product name\n\nExtensions: ${chain[0].extensions}`);
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}`);
346
524
  }
347
- const product = String.fromCharCode(...new Uint8Array(productExt.value.slice(2))).split("-", 1)[0];
348
- if (!(product in this._requirements)) {
525
+ const product = String.fromCharCode(...productIA5String.slice(2)).split("-", 1)[0];
526
+ if (!Object.prototype.hasOwnProperty.call(this._requirements, product)) {
349
527
  throw new Error(`Unexpected product: ${product}`);
350
528
  }
351
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
+ }
352
545
  return {
353
546
  requirements: this._requirements[product],
354
547
  publicKey: new Uint8Array(subjectPublicKeyInfo.subjectPublicKey),
548
+ vcekTcb: tcb,
355
549
  };
356
550
  });
357
551
  }
358
552
  }
359
553
  export function binaryStringToBytes(binaryString) {
360
- const byteArray = new Uint8Array(binaryString.length);
361
- for (let i = 0; i < binaryString.length; i++) {
362
- byteArray[i] = binaryString.charCodeAt(i);
363
- }
364
- return byteArray;
554
+ return Uint8Array.from(binaryString, (c) => c.charCodeAt(0));
365
555
  }
366
556
  export function bytesToBinaryString(bytes) {
367
- let binaryString = "";
368
- for (let i = 0; i < bytes.length; i++) {
369
- binaryString += String.fromCharCode(bytes[i]);
370
- }
371
- return binaryString;
557
+ return String.fromCharCode.apply(null, [...bytes]);
372
558
  }
373
559
  function _createCipher(publicKey, secretKey, info, nonce) {
374
560
  const sharedSecret = x25519.getSharedSecret(secretKey, publicKey);
375
- const key = hkdf(wrapConstructor(() => blake2b.create({ dkLen: 64 })), sharedSecret, Uint8Array.from([]), info, 32);
561
+ const key = hkdf(createHasher(() => blake2b.create({ dkLen: 64 })), sharedSecret, Uint8Array.from([]), info, 32);
376
562
  return chacha20poly1305(key, nonce);
377
563
  }
378
564
  function _getInfo(ephemeralPublicKey, recipientPublicKey) {
@@ -393,6 +579,9 @@ const REPORT_OFFSETS = {
393
579
  platInfo: 64,
394
580
  reportData: 80,
395
581
  measurement: 144,
582
+ reportedTcb: 384,
583
+ cpuIdFamily: 392,
584
+ chipId: 416,
396
585
  committedTcbSnp: 486,
397
586
  committedBuild: 492,
398
587
  committedMinor: 493,
@@ -400,3 +589,6 @@ const REPORT_OFFSETS = {
400
589
  signatureR: 672,
401
590
  signatureS: 744,
402
591
  };
592
+ const PLATFORM_INFO_BITS = {
593
+ aliasCheckComplete: 5,
594
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "routex-settlement",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "description": "Key settlement for the YAXI routex client",
5
5
  "homepage": "https://yaxi.tech",
6
6
  "author": "YAXI GmbH",
@@ -16,13 +16,14 @@
16
16
  "scripts": {
17
17
  "build": "tsc --build",
18
18
  "clean": "tsc --build --clean",
19
+ "fmt": "prettier --write .",
19
20
  "lint": "eslint",
20
21
  "test": "tsc --build && NODE_OPTIONS=\"$NODE_OPTIONS --experimental-vm-modules\" jest"
21
22
  },
22
23
  "dependencies": {
23
- "@noble/ciphers": "^1.2.1",
24
- "@noble/curves": "^1.8.1",
25
- "@noble/hashes": "^1.7.1",
24
+ "@noble/ciphers": "^2.0.0",
25
+ "@noble/curves": "^2.0.0",
26
+ "@noble/hashes": "^2.0.0",
26
27
  "@peculiar/asn1-schema": "^2.3.15",
27
28
  "@peculiar/asn1-x509": "^2.3.15",
28
29
  "@peculiar/x509": "^1.12.3"
@@ -31,7 +32,8 @@
31
32
  "@eslint/js": "^9.21.0",
32
33
  "eslint": "^9.21.0",
33
34
  "globals": "^16.0.0",
34
- "jest": "^29.7.0",
35
+ "jest": "^30.1.3",
36
+ "prettier": "^3.6.2",
35
37
  "typescript": "^5.7.3",
36
38
  "typescript-eslint": "^8.24.1"
37
39
  }