attest-tpm 1.0.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/Readme.md +159 -0
  2. package/attest-tpm.mjs +1261 -0
  3. package/package.json +8 -0
package/attest-tpm.mjs ADDED
@@ -0,0 +1,1261 @@
1
+ import koffi from "koffi"
2
+ /**@import os from "os"*/
3
+
4
+ const authorityFields = {
5
+ /**The Trusted Root certificate's Issued by and Issued to and the Intermediate certificate's Issued by.*/ "Issued by": "",
6
+ /**O in the Trusted Root certificate's Issuer and Subject and in the Intermediate certificate's Issuer.*/ O: "",
7
+ /**C in the Trusted Root certificate's Issuer and Subject and in the Intermediate certificate's Issuer. Must be two characters.*/ C: "",
8
+ /**The Intermediate certificate's Issued to.*/ "Issued to": "",
9
+ /**The On-line Certificate Status Protocol URL in the Intermediate certificate's Authority Information Access.*/ "On-line Certificate Status Protocol": "",
10
+ /**The Intermediate certificate's CRL Distribution Points URL.*/ "CRL Distribution Points": ""
11
+ }
12
+
13
+ const C = (/**@type {string=}*/ C) => {
14
+ const subjectC = [11, 48, 9, 6, 3, 85, 4, 6, 12, 2]
15
+
16
+ if (C) {
17
+ subjectC.push(...Array.from(
18
+ C,
19
+ (character) => {
20
+ return character.charCodeAt(0)
21
+ }
22
+ ))
23
+ } else {
24
+ subjectC.push(85, 83)
25
+ }
26
+
27
+ return subjectC
28
+ }
29
+
30
+ const prependLength = (/**@type {number[]}*/ bytes) => {
31
+ const prependedBytes = []
32
+
33
+ if (bytes[255] === undefined) {
34
+ if (bytes[127] !== undefined) {
35
+ prependedBytes[0] = 129
36
+ }
37
+
38
+ prependedBytes[prependedBytes.length] = bytes.length
39
+ } else {
40
+ const bytesLength = Math.floor(bytes.length / 256)
41
+ prependedBytes.push(130, bytesLength, bytes.length - 256 * bytesLength)
42
+ }
43
+
44
+ prependedBytes.push(...bytes)
45
+ return prependedBytes
46
+ }
47
+
48
+ const O = (/**@type {string=}*/ O) => {
49
+ const subjectO = []
50
+
51
+ if (O) {
52
+ subjectO.push(...Array.from(
53
+ O,
54
+ (character) => {
55
+ return character.charCodeAt(0)
56
+ }
57
+ ))
58
+ }
59
+
60
+ return [49, 128, 48, 128, 6, 3, 85, 4, 10, 12, ...prependLength(subjectO), 0, 0, 0, 0, 49]
61
+ }
62
+
63
+ const Issued = (/**@type {string}*/ Issued) => {
64
+ return [
65
+ 128,
66
+ 48,
67
+ 128,
68
+ 6,
69
+ 3,
70
+ 85,
71
+ 4,
72
+ 3,
73
+ 12,
74
+ ...prependLength(Array.from(
75
+ Issued,
76
+ (character) => {
77
+ return character.charCodeAt(0)
78
+ }
79
+ )),
80
+ 0,
81
+ 0,
82
+ 0,
83
+ 0
84
+ ]
85
+ }
86
+
87
+ const createCer = async (/**@type {number[]}*/ Issuer, /**@type {number[]}*/ Valid, /**@type {number[]}*/ Subject, /**@type {string}*/ Publickey, /**@type {number[]}*/ Fields, /**@type {number}*/ KeyUsage, /**@type {(Details: ArrayBuffer) => Promise<ArrayBuffer>}*/ sign) => {
88
+ const Serialnumber = [0, ...crypto.getRandomValues(new Uint8Array(8))]
89
+
90
+ const Details = Uint8Array.from([
91
+ 48,
92
+ ...prependLength([
93
+ 160,
94
+ 3,
95
+ 2,
96
+ 1,
97
+ 2,
98
+ 2,
99
+ 9,
100
+ ...Serialnumber,
101
+ 48,
102
+ 11,
103
+ 6,
104
+ 9,
105
+ 42,
106
+ 134,
107
+ 72,
108
+ 134,
109
+ 247,
110
+ 13,
111
+ 1,
112
+ 1,
113
+ 11,
114
+ 48,
115
+ ...prependLength([49, ...Issuer]),
116
+ 48,
117
+ 24,
118
+ 23,
119
+ 10,
120
+ ...Valid,
121
+ 48,
122
+ ...prependLength([49, ...Subject]),
123
+ 48,
124
+ ...prependLength([
125
+ 48,
126
+ 11,
127
+ 6,
128
+ 9,
129
+ 42,
130
+ 134,
131
+ 72,
132
+ 134,
133
+ 247,
134
+ 13,
135
+ 1,
136
+ 1,
137
+ 1,
138
+ 3,
139
+ ...prependLength([
140
+ 0,
141
+ 48,
142
+ 128,
143
+ 2,
144
+ ...prependLength(Array.from(
145
+ Publickey,
146
+ (character) => {
147
+ return character.charCodeAt(0)
148
+ }
149
+ )),
150
+ 2,
151
+ 3,
152
+ 1,
153
+ 0,
154
+ 1,
155
+ 0,
156
+ 0
157
+ ])
158
+ ]),
159
+ 163,
160
+ 128,
161
+ 48,
162
+ 128,
163
+ 48,
164
+ ...Fields,
165
+ 48,
166
+ 14,
167
+ 6,
168
+ 3,
169
+ 85,
170
+ 29,
171
+ 15,
172
+ 1,
173
+ 1,
174
+ 255,
175
+ 4,
176
+ 4,
177
+ 3,
178
+ 2,
179
+ 0,
180
+ KeyUsage,
181
+ 0,
182
+ 0,
183
+ 0,
184
+ 0
185
+ ])
186
+ ])
187
+
188
+ return { cer: Uint8Array.from([48, 128, ...Details, 48, 11, 6, 9, 42, 134, 72, 134, 247, 13, 1, 1, 11, 3, 130, 2, 1, 0, ...new Uint8Array(await sign(Details.buffer)), 0, 0]), "Serial number": Serialnumber }
189
+ }
190
+
191
+ const OnlineCertificateStatusProtocol = (/**@type {string}*/ URL) => {
192
+ return [
193
+ 48,
194
+ 128,
195
+ 6,
196
+ 8,
197
+ 43,
198
+ 6,
199
+ 1,
200
+ 5,
201
+ 5,
202
+ 7,
203
+ 48,
204
+ 1,
205
+ 134,
206
+ ...prependLength(Array.from(
207
+ URL,
208
+ (character) => {
209
+ return character.charCodeAt(0)
210
+ }
211
+ )),
212
+ 0,
213
+ 0
214
+ ]
215
+ }
216
+
217
+ const signingFields = (/**@type {number[]}*/ AuthorityInformationAccess, /**@type {{ "CRL Distribution Points"?: string }=}*/ Fields) => {
218
+ let URL = Fields?.["CRL Distribution Points"]
219
+
220
+ if (!URL) {
221
+ URL = ""
222
+ }
223
+
224
+ return [
225
+ 128,
226
+ 6,
227
+ 8,
228
+ 43,
229
+ 6,
230
+ 1,
231
+ 5,
232
+ 5,
233
+ 7,
234
+ 1,
235
+ 1,
236
+ 4,
237
+ ...prependLength([48, 128, ...AuthorityInformationAccess, 0, 0]),
238
+ 0,
239
+ 0,
240
+ 48,
241
+ 128,
242
+ 6,
243
+ 3,
244
+ 85,
245
+ 29,
246
+ 31,
247
+ 4,
248
+ ...prependLength([
249
+ 48,
250
+ 128,
251
+ 48,
252
+ 128,
253
+ 160,
254
+ 128,
255
+ 160,
256
+ 128,
257
+ 134,
258
+ ...prependLength(Array.from(
259
+ URL,
260
+ (character) => {
261
+ return character.charCodeAt(0)
262
+ }
263
+ )),
264
+ 0,
265
+ 0,
266
+ 0,
267
+ 0,
268
+ 0,
269
+ 0,
270
+ 0,
271
+ 0
272
+ ]),
273
+ 0,
274
+ 0,
275
+ 48,
276
+ 19,
277
+ 6,
278
+ 3,
279
+ 85,
280
+ 29,
281
+ 37,
282
+ 4,
283
+ 12,
284
+ 48,
285
+ 10,
286
+ 6,
287
+ 8,
288
+ 43,
289
+ 6,
290
+ 1,
291
+ 5,
292
+ 5,
293
+ 7,
294
+ 3,
295
+ 3,
296
+ 48,
297
+ 19,
298
+ 6,
299
+ 3,
300
+ 85,
301
+ 29,
302
+ 32,
303
+ 4,
304
+ 12,
305
+ 48,
306
+ 10,
307
+ 48,
308
+ 8,
309
+ 6,
310
+ 6,
311
+ 103,
312
+ 129,
313
+ 12,
314
+ 1,
315
+ 4,
316
+ 1,
317
+ 48,
318
+ 31,
319
+ 6,
320
+ 3,
321
+ 85,
322
+ 29,
323
+ 35,
324
+ 4,
325
+ 24,
326
+ 48,
327
+ 22,
328
+ 128,
329
+ 20,
330
+ 0,
331
+ 0,
332
+ 0,
333
+ 0,
334
+ 0,
335
+ 0,
336
+ 0,
337
+ 0,
338
+ 0,
339
+ 0,
340
+ 0,
341
+ 0,
342
+ 0,
343
+ 0,
344
+ 0,
345
+ 0,
346
+ 0,
347
+ 0,
348
+ 0,
349
+ 0
350
+ ]
351
+ }
352
+
353
+ export const /**Create Trusted Root and Intermediate certificates.*/ authorityCers = async (
354
+ /**The Trusted Root certificate's Public key. Must have `algorithm: { name: "RSASSA-PKCS1-v1_5", modulusLength: 4096, publicExponent: Uint8Array.from([1, 0, 1]), hash: "SHA-256" }`. @type {CryptoKey}*/ TrustedRoot,
355
+ /**The Intermediate certificate's Public key. Must have `algorithm: { name: "RSASSA-PKCS1-v1_5", modulusLength: 4096, publicExponent: Uint8Array.from([1, 0, 1]), hash: "SHA-256" }`. @type {CryptoKey}*/ Intermediate,
356
+ /**A function to sign the certificates with the private key matching the Trusted Root certificate. @type {(Details: ArrayBuffer) => Promise<ArrayBuffer>}*/ sign,
357
+ /**The certificate fields. @type {Partial<authorityFields>}*/ Fields = {}
358
+ ) => {
359
+ if (TrustedRoot.extractable) {
360
+ if (TrustedRoot.algorithm.name === "RSASSA-PKCS1-v1_5") {
361
+ if (Intermediate.extractable) {
362
+ if (Intermediate.algorithm.name === "RSASSA-PKCS1-v1_5") {
363
+ let Issuedby = Fields["Issued by"]
364
+
365
+ if (!Issuedby) {
366
+ Issuedby = "\0"
367
+ }
368
+
369
+ let Issuedto = Fields["Issued to"]
370
+
371
+ if (!Issuedto) {
372
+ Issuedto = "\0"
373
+ }
374
+
375
+ if (Issuedby === Issuedto) {
376
+ Issuedby += "\0"
377
+ }
378
+
379
+ const Issuer = [...C(Fields.C), ...O(Fields.O), ...Issued(Issuedby)]
380
+ const Valid = [48, 48, 48, 49, 48, 49, 48, 48, 48, 48, 23, 10, 52, 57, 49, 50, 51, 49, 50, 51, 53, 57]
381
+ let URL = Fields["On-line Certificate Status Protocol"]
382
+
383
+ if (!URL) {
384
+ URL = ""
385
+ }
386
+
387
+ return {
388
+ /**A Trusted Root certificate.*/ "Trusted Root": (await createCer(Issuer, Valid, Issuer, atob(/**@type {string}*/((await crypto.subtle.exportKey("jwk", TrustedRoot)).n).replaceAll("_", "/").replaceAll("-", "+")), [15, 6, 3, 85, 29, 19, 1, 1, 255, 4, 5, 48, 3, 1, 1, 255], 6, sign)).cer,
389
+ /**An Intermediate certificate.*/ Intermediate: (await createCer(Issuer, Valid, Issued(Issuedto), atob(/**@type {string}*/((await crypto.subtle.exportKey("jwk", Intermediate)).n).replaceAll("_", "/").replaceAll("-", "+")), [...signingFields(OnlineCertificateStatusProtocol(URL), Fields), 48, 15, 6, 3, 85, 29, 19, 1, 1, 255, 4, 5, 48, 3, 1, 1, 255], 6, sign)).cer
390
+ }
391
+ }
392
+
393
+ throw new Error("`Intermediate` key isn't `RSASSA-PKCS1-v1_5`")
394
+ }
395
+
396
+ throw new Error("`Intermediate` key isn't `extractable`")
397
+ }
398
+
399
+ throw new Error("`Trusted Root` key isn't `RSASSA-PKCS1-v1_5`")
400
+ }
401
+
402
+ throw new Error("`Trusted Root` key isn't `extractable`")
403
+ }
404
+
405
+ const Tbsi_Context_Create = () => {
406
+ let tbs
407
+
408
+ try {
409
+ tbs = koffi.load("tbs").func
410
+ } catch {
411
+ throw new Error("`attestTPM.tpm` functions must run on Windows")
412
+ }
413
+
414
+ const Tbsip_Submit_Command = tbs("Tbsip_Submit_Command", "int", ["int", "void *", "void *", "uint8 *", "int", "uint8 *", "uint8 *"])
415
+ const contextResponse = new ArrayBuffer(4)
416
+ tbs("Tbsi_Context_Create", "void", ["uint8 *", "uint8 *"])([2, 0, 0, 0, 4, 0, 0, 0], contextResponse)
417
+ const tbsContext = new DataView(contextResponse).getUint32(0, true)
418
+
419
+ return (/**@type {number[]}*/ command) => {
420
+ const commandResponse = new Uint8Array(1045)
421
+
422
+ if (!(Tbsip_Submit_Command(tbsContext, undefined, undefined, command, command.length, commandResponse, [21, 4, 0, 0]) || new DataView(commandResponse.buffer).getUint32(6))) {
423
+ return commandResponse
424
+ }
425
+ }
426
+ }
427
+
428
+ export const /**Create TPM keys.*/ tpmCreate = async () => {
429
+ const Tbsip_Submit_Command = Tbsi_Context_Create()
430
+ const signingKey = Tbsip_Submit_Command([128, 2, 0, 0, 0, 63, 0, 0, 1, 83, 129, 0, 0, 1, 0, 0, 0, 9, 64, 0, 0, 9, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 0, 0, 22, 0, 1, 0, 11, 0, 4, 0, 114, 0, 0, 0, 16, 0, 16, 12, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0])
431
+
432
+ if (signingKey) {
433
+ const keys = signingKey.slice(14, new DataView(signingKey.buffer).getUint16(14) + 424)
434
+ const savedCer = Tbsip_Submit_Command([128, 1, 0, 0, 0, 14, 0, 0, 1, 105, 1, 192, 0, 2])
435
+ let cer
436
+
437
+ if (savedCer) {
438
+ let tpmCer = ""
439
+ const cerLength = new DataView(savedCer.buffer).getUint16(24)
440
+
441
+ while (tpmCer.length !== cerLength) {
442
+ const cerCommand = [128, 2, 0, 0, 0, 35, 0, 0, 1, 78, 1, 192, 0, 2, 1, 192, 0, 2, 0, 0, 0, 9, 64, 0, 0, 9, 0, 0, 0, 0, 0]
443
+ let /**@type {number}*/ responseLength = cerLength - tpmCer.length
444
+
445
+ if (responseLength > 1023) {
446
+ cerCommand.push(4, 0)
447
+ responseLength = 1024
448
+ } else {
449
+ const lengthBytes = new Uint8Array(2)
450
+ new DataView(lengthBytes.buffer).setUint16(0, responseLength)
451
+ cerCommand.push(...lengthBytes)
452
+ }
453
+
454
+ cerCommand.push(tpmCer.length / 256, 0)
455
+ tpmCer += String.fromCharCode(.../**@type {Uint8Array}*/(Tbsip_Submit_Command(cerCommand)).slice(16, responseLength + 16))
456
+ }
457
+
458
+ cer = btoa(tpmCer)
459
+ } else {
460
+ const cerKey = /**@type {Uint8Array}*/(Tbsip_Submit_Command([128, 1, 0, 0, 0, 14, 0, 0, 1, 115, 129, 1, 0, 1])).slice(70, 326)
461
+
462
+ if (String.fromCharCode(.../**@type {Uint8Array}*/(Tbsip_Submit_Command([128, 1, 0, 0, 0, 22, 0, 0, 1, 122, 0, 0, 0, 6, 0, 0, 1, 5, 0, 0, 0, 1])).slice(23, 27)) === "INTC") {
463
+ cer = decodeURIComponent((await (await fetch(`https://ekop.intel.com/ekcertservice/${encodeURIComponent(btoa(String.fromCharCode(...new Uint8Array(await crypto.subtle.digest("sha-256", Uint8Array.from([...cerKey, 1, 0, 1]))))))}`)).json()).certificate).replaceAll("-", "+").replaceAll("_", "/")
464
+ } else {
465
+ cer = btoa(String.fromCharCode(...await (await fetch(`https://ftpm.amd.com/pki/aia/${Array.from(
466
+ new Uint8Array(await crypto.subtle.digest("sha-256", Uint8Array.from([0, 0, 34, 34, 0, 1, 0, 1, ...cerKey]))),
467
+ (byte) => {
468
+ return byte.toString(16).padStart(2, "0")
469
+ }
470
+ ).join("")}`)).bytes()))
471
+ }
472
+ }
473
+
474
+ return {
475
+ /**A key pair that can be provided to `attestTPM.tpmDecrypt` and `attestTPM.tpmSign`. The private key is encrypted by the TPM.*/ keys,
476
+ /**JSON that can be provided to `attestTPM.authorityIssue`.*/ body: JSON.stringify({ cer, "Public key": btoa(String.fromCharCode(...keys.slice(-384))) })
477
+ }
478
+ }
479
+
480
+ throw new Error("computer's TPM doesn't support `modulusLength: 3072`")
481
+ }
482
+
483
+ const tpmFields = {
484
+ /**The Issued to.*/ "Issued to": "",
485
+ /**The Subject O.*/ O: "",
486
+ /**The Subject L.*/ L: "",
487
+ /**The Subject C. Must be two characters.*/ C: "",
488
+ /**The On-line Certificate Status Protocol URL in Authority Information Access.*/ "On-line Certificate Status Protocol": "",
489
+ /**The Certification Authority Issuer URL in Authority Information Access.*/ "Certification Authority Issuer": "",
490
+ /**The CRL Distribution Points URL.*/ "CRL Distribution Points": ""
491
+ }
492
+
493
+ /**@typedef {{ asn: Uint8Array<ArrayBuffer>, content: Uint8Array, children: parsedASN }[]} parsedASN*/
494
+
495
+ const parseASN = (/**@type {Uint8Array}*/ asn) => {
496
+ let asnByte = 0
497
+
498
+ const parseASN = (/**@type {number}*/ end) => {
499
+ const asnType = asnByte
500
+ let contentLength = asn[asnType + 1]
501
+ let contentStart = asnType + 2
502
+ const indefiniteLength = contentLength === 128
503
+ let contentEnd
504
+
505
+ if (indefiniteLength) {
506
+ contentEnd = end
507
+ } else {
508
+ if (contentLength === 129) {
509
+ contentLength = asn[contentStart++]
510
+ } else if (contentLength === 130) {
511
+ contentLength = 256 * asn[contentStart++] + asn[contentStart++]
512
+ }
513
+
514
+ contentEnd = contentLength + contentStart
515
+ }
516
+
517
+ const /**@type {parsedASN}*/ children = []
518
+
519
+ if (contentEnd <= end) {
520
+ asnByte = contentStart
521
+
522
+ while (asnByte !== contentEnd) {
523
+ const parsedASN = children[children.length] = parseASN(contentEnd)
524
+
525
+ if (indefiniteLength && !(parsedASN.asn[0] || parsedASN.asn[1])) {
526
+ break
527
+ }
528
+ }
529
+ } else {
530
+ asnByte = end
531
+ }
532
+
533
+ return { asn: asn.slice(asnType, asnByte), content: asn.slice(contentStart, asnByte), children }
534
+ }
535
+
536
+ return parseASN(asn.length)
537
+ }
538
+
539
+ const nuvotonKey = crypto.subtle.importKey("raw", Uint8Array.from([4, 218, 156, 220, 176, 62, 65, 63, 68, 128, 129, 103, 133, 99, 192, 44, 166, 44, 59, 108, 7, 181, 39, 191, 157, 142, 143, 65, 242, 18, 192, 31, 115, 191, 175, 140, 233, 118, 12, 255, 9, 110, 183, 40, 205, 141, 57, 179, 177, 133, 125, 10, 145, 222, 248, 111, 225, 151, 100, 36, 198, 165, 128, 234, 246]), { name: "ECDSA", namedCurve: "P-256" }, false, ["verify"])
540
+ const globalsignKey = crypto.subtle.importKey("jwk", { kty: "RSA", n: "68iNcEttUF8gLtIWNzPiGbpHQjK5EmknjmeljK2UAeahWiyhPeKyXnZPhQHhPWvVvzLtj69bcR3Sxk76b+ke1RzmH3TnTKcZ523S6+41nVxWdfaH9dNx0ohrCoQFVdFlDk4kyn9oOVTCxR4FP5KWEBqA2GbI3Da0LZpvl3uqvwVTTcOzqKjz3ZpIPbfPnXIsCLHXbW5Qcg+YcHJplGdopy1Nu7u6XAg2Wt1wH3ndnRZVCGKXnKNmzXjJ3jICcDdnVlb6as6Svx6FWxRu+tTHJ+EwicysRMjFHhH1flxMhWJrZJVO1+qtvpcwLX9PsrU8f3RBe2W3dYgsww0mEsPL6Q", e: "AQAB" }, { name: "RSASSA-PKCS1-v1_5", hash: "SHA-256" }, false, ["verify"])
541
+ const integrityBytes = Uint8Array.from([0, 0, 0, 1, 73, 78, 84, 69, 71, 82, 73, 84, 89, 0, 0, 0, 1, 0])
542
+ const aesCBC = { name: "aes-cbc", iv: new ArrayBuffer(16) }
543
+ const label = Uint8Array.from([73, 68, 69, 78, 84, 73, 84, 89, 0])
544
+
545
+ export const /**`verify` whether a certificate was issued by a TPM manufacturer and create a Code Signing certificate if so.*/ authorityIssue = async (
546
+ /**A `.cer` of an Intermediate certificate. @type {Uint8Array}*/ Intermediate,
547
+ /**A `body` from `attestTPM.tpmCreate`. @type {Uint8Array}*/ body,
548
+ /**A function to sign the Code Signing certificate with the private key matching the Intermediate certificate's Public key. @type {(Details: ArrayBuffer) => Promise<ArrayBuffer>}*/ sign,
549
+ /**The Code Signing certificate fields. @type {Partial<tpmFields>}*/ Fields = {}
550
+ ) => {
551
+ const Issuedby = parseASN(Intermediate).children[0]?.children[5]?.children
552
+
553
+ if (Issuedby?.[0]) {
554
+ const bodyJSON = String.fromCharCode(...body)
555
+ let tpmBody
556
+
557
+ try {
558
+ tpmBody = JSON.parse(bodyJSON)
559
+ } catch {
560
+ return { error: "body isn't in `JSON.stringify` format" }
561
+ }
562
+
563
+ let tpmCer
564
+
565
+ try {
566
+ tpmCer = atob(tpmBody.cer)
567
+ } catch {
568
+ return { error: "`cer` isn't in `btoa` format" }
569
+ }
570
+
571
+ let Publickey
572
+
573
+ try {
574
+ Publickey = atob(tpmBody["Public key"])
575
+ } catch {
576
+ return { error: "`Public key` isn't in `btoa` format" }
577
+ }
578
+
579
+ const parsedCer = parseASN(Uint8Array.from(
580
+ tpmCer,
581
+ (character) => {
582
+ return character.charCodeAt(0)
583
+ }
584
+ )).children
585
+
586
+ if (parsedCer[2] && parsedCer[0].children[6]?.children[1]) {
587
+ const /**@type {{ [key: string]: parsedASN }}*/ cerFields = {}
588
+
589
+ for (const asn in parsedCer[0].children[7]?.children[0]?.children) {
590
+ if (parsedCer[0].children[7].children[0].children[asn].children[0]) {
591
+ cerFields[String.fromCharCode(...parsedCer[0].children[7].children[0].children[asn].children[0].content)] = parsedCer[0].children[7].children[0].children[asn].children
592
+ }
593
+ }
594
+
595
+ if (cerFields["+\x06\x01\x05\x05\x07\x01\x01"]?.[1].children[0]?.children[0]?.children[1]) {
596
+ const URL = String.fromCharCode(...cerFields["+\x06\x01\x05\x05\x07\x01\x01"][1].children[0].children[0].children[1].content).replace("http://", "https://")
597
+ let TPMModel, TPMVersion, urlDomain
598
+ const signatureBytes = parsedCer[2].content.slice(1)
599
+ let cerSignature
600
+
601
+ if (URL === "https://www.nuvoton.com/security/NTC-TPM-EK-Cert/Nuvoton TPM Root CA 2111.cer") {
602
+ TPMModel = cerFields["U\x1d\x11"]?.[2]?.children[0]?.children[0]?.children[0]?.children[0]?.children[1]?.children[1]
603
+ TPMVersion = cerFields["U\x1d\x11"]?.[2]?.children[0]?.children[0]?.children[0]?.children[0]?.children[2]?.children[1]
604
+ urlDomain = "nuvoton"
605
+ } else {
606
+ TPMModel = cerFields["U\x1d\x11"]?.[2]?.children[0]?.children[0]?.children[0]?.children[1]?.children[0]?.children[1]
607
+ TPMVersion = cerFields["U\x1d\x11"]?.[2]?.children[0]?.children[0]?.children[0]?.children[2]?.children[0]?.children[1]
608
+
609
+ if (URL === "https://secure.globalsign.com/stmtpmekint06.crt") {
610
+ urlDomain = "globalsign"
611
+ cerSignature = signatureBytes
612
+ } else if (URL.startsWith("https://ftpm.amd.com/pki/aia/")) {
613
+ urlDomain = "amd"
614
+ cerSignature = signatureBytes
615
+ } else if (URL.startsWith("https://trustedservices.intel.com/content/CRL/ekcert/")) {
616
+ urlDomain = "intel"
617
+ }
618
+ }
619
+
620
+ if (TPMModel && TPMVersion && urlDomain) {
621
+ if (!cerSignature) {
622
+ const ecdsaSignature = parseASN(signatureBytes).children
623
+
624
+ if (ecdsaSignature[1]) {
625
+ cerSignature = Uint8Array.from([...ecdsaSignature[0].content, ...ecdsaSignature[1].content.slice(1)])
626
+ }
627
+ }
628
+
629
+ if (cerSignature) {
630
+ let verifyCer
631
+
632
+ if (urlDomain === "nuvoton") {
633
+ verifyCer = crypto.subtle.verify({ name: "ecdsa", hash: "sha-256" }, await nuvotonKey, cerSignature, parsedCer[0].asn)
634
+ } else if (urlDomain === "globalsign") {
635
+ verifyCer = crypto.subtle.verify("rsassa-pkcs1-v1_5", await globalsignKey, cerSignature, parsedCer[0].asn)
636
+ } else {
637
+ const Publickey = parseASN(await (await fetch(URL)).bytes()).children[0].children[6]?.children[1].content
638
+
639
+ if (Publickey) {
640
+ if (urlDomain === "amd") {
641
+ verifyCer = crypto.subtle.verify("rsassa-pkcs1-v1_5", await crypto.subtle.importKey("jwk", { kty: "RSA", n: btoa(String.fromCharCode(...Publickey.slice(10, 266))), e: "AQAB" }, { name: "RSASSA-PKCS1-v1_5", hash: "SHA-256" }, false, ["verify"]), cerSignature, parsedCer[0].asn)
642
+ } else {
643
+ verifyCer = crypto.subtle.verify({ name: "ecdsa", hash: "sha-256" }, await crypto.subtle.importKey("raw", Publickey.slice(1), { name: "ECDSA", namedCurve: "P-256" }, false, ["verify"]), cerSignature, parsedCer[0].asn)
644
+ }
645
+ }
646
+ }
647
+
648
+ if (await verifyCer) {
649
+ const identityRandom = crypto.getRandomValues(new Uint8Array(32))
650
+ const integrityKey = await crypto.subtle.importKey("raw", identityRandom, { name: "HMAC", hash: "SHA-256" }, false, ["sign"])
651
+ const /**@type {number[]}*/ key = []
652
+
653
+ const storageDigest = [
654
+ 0,
655
+ 11,
656
+ ...new Uint8Array(await crypto.subtle.digest(
657
+ "sha-256",
658
+ Uint8Array.from([
659
+ 0,
660
+ 1,
661
+ 0,
662
+ 11,
663
+ 0,
664
+ 4,
665
+ 0,
666
+ 114,
667
+ 0,
668
+ 0,
669
+ 0,
670
+ 16,
671
+ 0,
672
+ 16,
673
+ 12,
674
+ 0,
675
+ 0,
676
+ 0,
677
+ 0,
678
+ 0,
679
+ 1,
680
+ 128,
681
+ ...Array.from(
682
+ Publickey,
683
+ (character) => {
684
+ return character.charCodeAt(0)
685
+ }
686
+ )
687
+ ])
688
+ ))
689
+ ]
690
+
691
+ const storageKey = await crypto.subtle.importKey("raw", (await crypto.subtle.sign("hmac", integrityKey, Uint8Array.from([0, 0, 0, 1, 83, 84, 79, 82, 65, 71, 69, 0, ...storageDigest, 0, 0, 0, 128]))).slice(0, 16), "AES-CBC", false, ["encrypt"])
692
+ let keyBytes = new Uint8Array(16)
693
+ const prependedIdentity = [0, 32, ...identityRandom]
694
+
695
+ const encryptKey = async (/**@type {number}*/ bytes) => {
696
+ keyBytes = new Uint8Array((await crypto.subtle.encrypt(aesCBC, storageKey, keyBytes)).slice(0, bytes)).map((byte, index) => {
697
+ return prependedIdentity[index + key.length] ^ byte
698
+ })
699
+
700
+ key.push(...keyBytes)
701
+ }
702
+
703
+ await encryptKey(16)
704
+ await encryptKey(16)
705
+ await encryptKey(2)
706
+ const Validfrom = new Date
707
+
708
+ const validMonth = Array.from(
709
+ Validfrom.toISOString().slice(5, 7),
710
+ (character) => {
711
+ return character.charCodeAt(0)
712
+ }
713
+ )
714
+
715
+ const Valid = (/**@type {Date}*/ Valid) => {
716
+ return [
717
+ ...Array.from(
718
+ Valid.toISOString().slice(2, 4),
719
+ (character) => {
720
+ return character.charCodeAt(0)
721
+ }
722
+ ),
723
+ ...validMonth,
724
+ 48,
725
+ 49,
726
+ 48,
727
+ 48,
728
+ 48,
729
+ 48
730
+ ]
731
+ }
732
+
733
+ const Validto = new Date(Validfrom)
734
+ Validto.setFullYear(Validto.getFullYear() + 1)
735
+ const Subject = C(Fields.C)
736
+ const L = []
737
+
738
+ if (Fields.L) {
739
+ L.push(...Array.from(
740
+ Fields.L,
741
+ (character) => {
742
+ return character.charCodeAt(0)
743
+ }
744
+ ))
745
+ }
746
+
747
+ let Issuedto = Fields["Issued to"]
748
+
749
+ if (!Issuedto) {
750
+ Issuedto = ""
751
+ }
752
+
753
+ Subject.push(49, 128, 48, 128, 6, 3, 85, 4, 7, 12, ...prependLength(L), 0, 0, 0, 0, ...O(Fields.O), ...Issued(Issuedto))
754
+ let URL = Fields["On-line Certificate Status Protocol"]
755
+
756
+ if (!URL) {
757
+ URL = ""
758
+ }
759
+
760
+ let CertificationAuthorityIssuer = Fields["Certification Authority Issuer"]
761
+
762
+ if (!CertificationAuthorityIssuer) {
763
+ CertificationAuthorityIssuer = ""
764
+ }
765
+
766
+ const CodeSigning = await createCer(
767
+ Array.from(Issuedby[Issuedby.length - 1].asn.slice(1)),
768
+ [...Valid(Validfrom), 23, 10, ...Valid(Validto)],
769
+ Subject,
770
+ Publickey,
771
+ signingFields(
772
+ [
773
+ ...OnlineCertificateStatusProtocol(URL),
774
+ 48,
775
+ 128,
776
+ 6,
777
+ 8,
778
+ 43,
779
+ 6,
780
+ 1,
781
+ 5,
782
+ 5,
783
+ 7,
784
+ 48,
785
+ 2,
786
+ 134,
787
+ ...prependLength(Array.from(
788
+ CertificationAuthorityIssuer,
789
+ (character) => {
790
+ return character.charCodeAt(0)
791
+ }
792
+ )),
793
+ 0,
794
+ 0
795
+ ],
796
+ Fields
797
+ ),
798
+ 128,
799
+ sign
800
+ )
801
+
802
+ return {
803
+ /**The model.*/ TPMModel: String.fromCharCode(...TPMModel.content),
804
+ /**The firmware version.*/ TPMVersion: String.fromCharCode(...TPMVersion.content.slice(3)),
805
+ /**JSON that can be provided to `attestTPM.tpmDecrypt`.*/ response: JSON.stringify({
806
+ Intermediate: btoa(String.fromCharCode(...Intermediate)),
807
+ key: btoa(String.fromCharCode(...new Uint8Array(await crypto.subtle.sign("hmac", await crypto.subtle.importKey("raw", await crypto.subtle.sign("hmac", integrityKey, integrityBytes), { name: "HMAC", hash: "SHA-256" }, false, ["sign"]), Uint8Array.from([...key, ...storageDigest]))), ...key, 1, 0, ...new Uint8Array(await crypto.subtle.encrypt({ name: "rsa-oaep", label }, await crypto.subtle.importKey("jwk", { kty: "RSA", n: btoa(String.fromCharCode(...parsedCer[0].children[6].children[1].content.slice(10, 266))), e: "AQAB" }, { name: "RSA-OAEP", hash: "SHA-256" }, false, ["encrypt"]), identityRandom)))),
808
+ "Code Signing": btoa(String.fromCharCode(...new Uint8Array(await crypto.subtle.encrypt(aesCBC, await crypto.subtle.importKey("raw", identityRandom, "AES-CBC", false, ["encrypt"]), CodeSigning.cer))))
809
+ }),
810
+ /**`Code Signing`'s Serial number.*/ "Serial number": btoa(String.fromCharCode(...CodeSigning["Serial number"]))
811
+ }
812
+ }
813
+ }
814
+ }
815
+ }
816
+ }
817
+
818
+ return { error: "`cer` isn't a TPM certificate" }
819
+ }
820
+
821
+ throw new Error("`Intermediate` isn't a certificate")
822
+ }
823
+
824
+ const keyRequest = (/**@type {Uint8Array}*/ keys) => {
825
+ const keyLength = new Uint8Array(2)
826
+ new DataView(keyLength.buffer).setUint16(0, keys.length + 27)
827
+ return [128, 2, 0, 0, ...keyLength, 0, 0, 1, 87, 129, 0, 0, 1, 0, 0, 0, 9, 64, 0, 0, 9, 0, 0, 0, 0, 0, ...keys]
828
+ }
829
+
830
+ export const /**Use the TPM to decrypt a Code Signing certificate.*/ tpmDecrypt = async (
831
+ /**`keys` from `attestTPM.tpmCreate`. @type {Uint8Array}*/ keys,
832
+ /**The parsed `response` from `attestTPM.authorityIssue`. @type {{ Intermediate: string, key: string, "Code Signing": string }}*/ response
833
+ ) => {
834
+ let Intermediate
835
+
836
+ try {
837
+ Intermediate = atob(response.Intermediate)
838
+ } catch {
839
+ throw new Error("`Intermediate` isn't in `btoa` format")
840
+ }
841
+
842
+ let responseKey
843
+
844
+ try {
845
+ responseKey = atob(response.key)
846
+ } catch {
847
+ throw new Error("`key` isn't in `btoa` format")
848
+ }
849
+
850
+ let CodeSigning
851
+
852
+ try {
853
+ CodeSigning = atob(response["Code Signing"])
854
+ } catch {
855
+ throw new Error("`Code Signing` isn't in `btoa` format")
856
+ }
857
+
858
+ const Tbsip_Submit_Command = Tbsi_Context_Create()
859
+
860
+ if (Tbsip_Submit_Command(keyRequest(keys))) {
861
+ if (process.env.sessionname) {
862
+ throw new Error("`tpmDecrypt` must run as administrator")
863
+ }
864
+
865
+ Tbsip_Submit_Command([128, 1, 0, 0, 0, 43, 0, 0, 1, 118, 64, 0, 0, 7, 64, 0, 0, 7, 0, 16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 16, 0, 11])
866
+ Tbsip_Submit_Command([128, 2, 0, 0, 0, 41, 0, 0, 1, 81, 64, 0, 0, 11, 3, 0, 0, 0, 0, 0, 0, 9, 64, 0, 0, 9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0])
867
+
868
+ const decryptedKey = Tbsip_Submit_Command([
869
+ 128,
870
+ 2,
871
+ 0,
872
+ 0,
873
+ 1,
874
+ 112,
875
+ 0,
876
+ 0,
877
+ 1,
878
+ 71,
879
+ 128,
880
+ 255,
881
+ 255,
882
+ 255,
883
+ 129,
884
+ 1,
885
+ 0,
886
+ 1,
887
+ 0,
888
+ 0,
889
+ 0,
890
+ 18,
891
+ 64,
892
+ 0,
893
+ 0,
894
+ 9,
895
+ 0,
896
+ 0,
897
+ 0,
898
+ 0,
899
+ 0,
900
+ 3,
901
+ 0,
902
+ 0,
903
+ 0,
904
+ 0,
905
+ 0,
906
+ 0,
907
+ 0,
908
+ 0,
909
+ 0,
910
+ 68,
911
+ 0,
912
+ 32,
913
+ ...Array.from(
914
+ responseKey,
915
+ (character) => {
916
+ return character.charCodeAt(0)
917
+ }
918
+ )
919
+ ])?.slice(16, 48)
920
+
921
+ if (decryptedKey) {
922
+ const decryptCer = crypto.subtle.decrypt(
923
+ aesCBC,
924
+ await crypto.subtle.importKey("raw", decryptedKey, "AES-CBC", false, ["decrypt"]),
925
+ Uint8Array.from(
926
+ CodeSigning,
927
+ (character) => {
928
+ return character.charCodeAt(0)
929
+ }
930
+ )
931
+ )
932
+
933
+ let decryptedCer
934
+
935
+ try {
936
+ decryptedCer = await decryptCer
937
+ } catch {
938
+ throw new Error("`key` doesn't match `Code Signing`")
939
+ }
940
+
941
+ return Uint8Array.from([
942
+ 48,
943
+ 128,
944
+ 6,
945
+ 9,
946
+ 42,
947
+ 134,
948
+ 72,
949
+ 134,
950
+ 247,
951
+ 13,
952
+ 1,
953
+ 7,
954
+ 2,
955
+ 160,
956
+ 128,
957
+ 48,
958
+ 128,
959
+ 2,
960
+ 1,
961
+ 0,
962
+ 49,
963
+ 0,
964
+ 48,
965
+ 11,
966
+ 6,
967
+ 9,
968
+ 42,
969
+ 134,
970
+ 72,
971
+ 134,
972
+ 247,
973
+ 13,
974
+ 1,
975
+ 7,
976
+ 1,
977
+ 160,
978
+ 128,
979
+ ...new Uint8Array(decryptedCer),
980
+ ...Array.from(
981
+ Intermediate,
982
+ (character) => {
983
+ return character.charCodeAt(0)
984
+ }
985
+ ),
986
+ 0,
987
+ 0,
988
+ 49,
989
+ 0,
990
+ 0,
991
+ 0,
992
+ 0,
993
+ 0,
994
+ 0,
995
+ 0
996
+ ])
997
+ }
998
+
999
+ throw new Error("TPM keys don't match `key`")
1000
+ }
1001
+
1002
+ throw new Error("keys don't match TPM")
1003
+ }
1004
+
1005
+ export const /**Use the TPM to sign an `.exe`.*/ tpmSign = async (
1006
+ /**`keys` from `attestTPM.tpmCreate`. @type {Uint8Array}*/ keys,
1007
+ /**A `.p7b` of the Code Signing and Intermediate certificates. @type {Uint8Array}*/ p7b,
1008
+ /**An `.exe`. @type {Uint8Array}*/ exe
1009
+ ) => {
1010
+ const p7bCers = parseASN(p7b).children[1]?.children[0]?.children[3]
1011
+
1012
+ if (p7bCers?.children[0]?.children[0]?.children[3]) {
1013
+ if (exe[61] === undefined) {
1014
+ throw new Error("invalid `exe`")
1015
+ }
1016
+
1017
+ const Tbsip_Submit_Command = Tbsi_Context_Create()
1018
+
1019
+ if (Tbsip_Submit_Command(keyRequest(keys))) {
1020
+ const exeHeader = exe.slice(0, new DataView(exe.buffer).getUint16(60, true) + 168)
1021
+ const exeLength = 8 * Math.ceil(exe.length / 8)
1022
+ const exeContent = [...exe.slice(exeHeader.length + 8), ...Array(exeLength - exe.length)]
1023
+
1024
+ const signatureDigest = Uint8Array.from([
1025
+ 48,
1026
+ 14,
1027
+ 6,
1028
+ 10,
1029
+ 43,
1030
+ 6,
1031
+ 1,
1032
+ 4,
1033
+ 1,
1034
+ 130,
1035
+ 55,
1036
+ 2,
1037
+ 1,
1038
+ 15,
1039
+ 48,
1040
+ 0,
1041
+ 48,
1042
+ 47,
1043
+ 48,
1044
+ 11,
1045
+ 6,
1046
+ 9,
1047
+ 96,
1048
+ 134,
1049
+ 72,
1050
+ 1,
1051
+ 101,
1052
+ 3,
1053
+ 4,
1054
+ 2,
1055
+ 1,
1056
+ 4,
1057
+ 32,
1058
+ ...new Uint8Array(await crypto.subtle.digest("sha-256", Uint8Array.from([...exeHeader.slice(0, -80), ...exeHeader.slice(-76), ...exeContent])))
1059
+ ])
1060
+
1061
+ const Authenticatedattributes = [49, 48, 47, 6, 9, 42, 134, 72, 134, 247, 13, 1, 9, 4, 49, 34, 4, 32, ...new Uint8Array(await crypto.subtle.digest("sha-256", signatureDigest))]
1062
+
1063
+ const encryptedAttributes = /**@type {Uint8Array}*/(Tbsip_Submit_Command([
1064
+ 128,
1065
+ 2,
1066
+ 0,
1067
+ 0,
1068
+ 0,
1069
+ 73,
1070
+ 0,
1071
+ 0,
1072
+ 1,
1073
+ 93,
1074
+ 128,
1075
+ 255,
1076
+ 255,
1077
+ 255,
1078
+ 0,
1079
+ 0,
1080
+ 0,
1081
+ 9,
1082
+ 64,
1083
+ 0,
1084
+ 0,
1085
+ 9,
1086
+ 0,
1087
+ 0,
1088
+ 0,
1089
+ 0,
1090
+ 0,
1091
+ 0,
1092
+ 32,
1093
+ ...new Uint8Array(await crypto.subtle.digest("sha-256", Uint8Array.from([49, ...Authenticatedattributes]))),
1094
+ 0,
1095
+ 20,
1096
+ 0,
1097
+ 11,
1098
+ 128,
1099
+ 36,
1100
+ 64,
1101
+ 0,
1102
+ 0,
1103
+ 7,
1104
+ 0,
1105
+ 0
1106
+ ])).slice(20, 404)
1107
+
1108
+ const DigitalSignature = [
1109
+ 48,
1110
+ 128,
1111
+ 6,
1112
+ 9,
1113
+ 42,
1114
+ 134,
1115
+ 72,
1116
+ 134,
1117
+ 247,
1118
+ 13,
1119
+ 1,
1120
+ 7,
1121
+ 2,
1122
+ 160,
1123
+ 128,
1124
+ 48,
1125
+ 128,
1126
+ 2,
1127
+ 1,
1128
+ 0,
1129
+ 49,
1130
+ 13,
1131
+ 48,
1132
+ 11,
1133
+ 6,
1134
+ 9,
1135
+ 96,
1136
+ 134,
1137
+ 72,
1138
+ 1,
1139
+ 101,
1140
+ 3,
1141
+ 4,
1142
+ 2,
1143
+ 1,
1144
+ 48,
1145
+ 81,
1146
+ 6,
1147
+ 10,
1148
+ 43,
1149
+ 6,
1150
+ 1,
1151
+ 4,
1152
+ 1,
1153
+ 130,
1154
+ 55,
1155
+ 2,
1156
+ 1,
1157
+ 4,
1158
+ 160,
1159
+ 67,
1160
+ 48,
1161
+ 65,
1162
+ ...signatureDigest,
1163
+ ...p7bCers.asn,
1164
+ 49,
1165
+ 128,
1166
+ 48,
1167
+ 128,
1168
+ 2,
1169
+ 1,
1170
+ 0,
1171
+ 48,
1172
+ 128,
1173
+ ...p7bCers.children[0].children[0].children[3].asn,
1174
+ ...p7bCers.children[0].children[0].children[1].asn,
1175
+ 0,
1176
+ 0,
1177
+ 48,
1178
+ 11,
1179
+ 6,
1180
+ 9,
1181
+ 96,
1182
+ 134,
1183
+ 72,
1184
+ 1,
1185
+ 101,
1186
+ 3,
1187
+ 4,
1188
+ 2,
1189
+ 1,
1190
+ 160,
1191
+ ...Authenticatedattributes,
1192
+ 48,
1193
+ 11,
1194
+ 6,
1195
+ 9,
1196
+ 42,
1197
+ 134,
1198
+ 72,
1199
+ 134,
1200
+ 247,
1201
+ 13,
1202
+ 1,
1203
+ 1,
1204
+ 1,
1205
+ 4,
1206
+ 130,
1207
+ 1,
1208
+ 128,
1209
+ ...encryptedAttributes,
1210
+ 161,
1211
+ 128,
1212
+ 48,
1213
+ 128,
1214
+ 6,
1215
+ 10,
1216
+ 43,
1217
+ 6,
1218
+ 1,
1219
+ 4,
1220
+ 1,
1221
+ 130,
1222
+ 55,
1223
+ 3,
1224
+ 3,
1225
+ 1,
1226
+ 49,
1227
+ 128,
1228
+ ...(await (await fetch("https://timestamp.acs.microsoft.com", { method: "POST", body: Uint8Array.from([48, 55, 2, 1, 0, 48, 47, 48, 11, 6, 9, 96, 134, 72, 1, 101, 3, 4, 2, 1, 4, 32, ...new Uint8Array(await crypto.subtle.digest("sha-256", encryptedAttributes)), 1, 1, 255]) })).bytes()).slice(9),
1229
+ 0,
1230
+ 0,
1231
+ 0,
1232
+ 0,
1233
+ 0,
1234
+ 0,
1235
+ 0,
1236
+ 0,
1237
+ 0,
1238
+ 0,
1239
+ 0,
1240
+ 0,
1241
+ 0,
1242
+ 0,
1243
+ 0,
1244
+ 0
1245
+ ]
1246
+
1247
+ const lengthBytes = new Uint8Array(4)
1248
+ new DataView(lengthBytes.buffer).setUint32(0, exeLength, true)
1249
+ const signaturesLength = new Uint8Array(2)
1250
+ const signatureLength = new Uint8Array(2)
1251
+ new DataView(signatureLength.buffer).setUint16(0, DigitalSignature.length + 8, true)
1252
+ const DigitalSignatures = [...signatureLength, 0, 0, 0, 0, 2, 0, ...DigitalSignature, ...Array(8 * Math.ceil(DigitalSignature.length / 8) - DigitalSignature.length)]
1253
+ new DataView(signaturesLength.buffer).setUint16(0, DigitalSignatures.length, true)
1254
+ return Uint8Array.from([...exeHeader, ...lengthBytes, ...signaturesLength, 0, 0, ...exeContent, ...DigitalSignatures])
1255
+ }
1256
+
1257
+ throw new Error("keys don't match TPM")
1258
+ }
1259
+
1260
+ throw new Error("invalid `p7b`")
1261
+ }