@mikeargento/bitgraph-audit 0.1.0

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 (108) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +11 -0
  3. package/dist/anchors.d.ts +31 -0
  4. package/dist/anchors.d.ts.map +1 -0
  5. package/dist/anchors.js +116 -0
  6. package/dist/anchors.js.map +1 -0
  7. package/dist/anomalies.d.ts +42 -0
  8. package/dist/anomalies.d.ts.map +1 -0
  9. package/dist/anomalies.js +642 -0
  10. package/dist/anomalies.js.map +1 -0
  11. package/dist/attestation.d.ts +20 -0
  12. package/dist/attestation.d.ts.map +1 -0
  13. package/dist/attestation.js +795 -0
  14. package/dist/attestation.js.map +1 -0
  15. package/dist/audit.d.ts +19 -0
  16. package/dist/audit.d.ts.map +1 -0
  17. package/dist/audit.js +116 -0
  18. package/dist/audit.js.map +1 -0
  19. package/dist/authority.d.ts +37 -0
  20. package/dist/authority.d.ts.map +1 -0
  21. package/dist/authority.js +126 -0
  22. package/dist/authority.js.map +1 -0
  23. package/dist/aws-nitro-root-ca.d.ts +24 -0
  24. package/dist/aws-nitro-root-ca.d.ts.map +1 -0
  25. package/dist/aws-nitro-root-ca.js +44 -0
  26. package/dist/aws-nitro-root-ca.js.map +1 -0
  27. package/dist/cli.d.ts +3 -0
  28. package/dist/cli.d.ts.map +1 -0
  29. package/dist/cli.js +257 -0
  30. package/dist/cli.js.map +1 -0
  31. package/dist/contents-hash.d.ts +30 -0
  32. package/dist/contents-hash.d.ts.map +1 -0
  33. package/dist/contents-hash.js +73 -0
  34. package/dist/contents-hash.js.map +1 -0
  35. package/dist/export.d.ts +64 -0
  36. package/dist/export.d.ts.map +1 -0
  37. package/dist/export.js +192 -0
  38. package/dist/export.js.map +1 -0
  39. package/dist/index.d.ts +31 -0
  40. package/dist/index.d.ts.map +1 -0
  41. package/dist/index.js +17 -0
  42. package/dist/index.js.map +1 -0
  43. package/dist/ingest.d.ts +36 -0
  44. package/dist/ingest.d.ts.map +1 -0
  45. package/dist/ingest.js +828 -0
  46. package/dist/ingest.js.map +1 -0
  47. package/dist/reconstruct.d.ts +40 -0
  48. package/dist/reconstruct.d.ts.map +1 -0
  49. package/dist/reconstruct.js +400 -0
  50. package/dist/reconstruct.js.map +1 -0
  51. package/dist/report-json.d.ts +3 -0
  52. package/dist/report-json.d.ts.map +1 -0
  53. package/dist/report-json.js +290 -0
  54. package/dist/report-json.js.map +1 -0
  55. package/dist/report-md.d.ts +3 -0
  56. package/dist/report-md.d.ts.map +1 -0
  57. package/dist/report-md.js +853 -0
  58. package/dist/report-md.js.map +1 -0
  59. package/dist/rlp.d.ts +37 -0
  60. package/dist/rlp.d.ts.map +1 -0
  61. package/dist/rlp.js +120 -0
  62. package/dist/rlp.js.map +1 -0
  63. package/dist/tar.d.ts +47 -0
  64. package/dist/tar.d.ts.map +1 -0
  65. package/dist/tar.js +483 -0
  66. package/dist/tar.js.map +1 -0
  67. package/dist/temporal.d.ts +55 -0
  68. package/dist/temporal.d.ts.map +1 -0
  69. package/dist/temporal.js +461 -0
  70. package/dist/temporal.js.map +1 -0
  71. package/dist/types.d.ts +1389 -0
  72. package/dist/types.d.ts.map +1 -0
  73. package/dist/types.js +3 -0
  74. package/dist/types.js.map +1 -0
  75. package/dist/validity.d.ts +21 -0
  76. package/dist/validity.d.ts.map +1 -0
  77. package/dist/validity.js +86 -0
  78. package/dist/validity.js.map +1 -0
  79. package/dist/verify-tiers.d.ts +11 -0
  80. package/dist/verify-tiers.d.ts.map +1 -0
  81. package/dist/verify-tiers.js +120 -0
  82. package/dist/verify-tiers.js.map +1 -0
  83. package/dist/witness.d.ts +25 -0
  84. package/dist/witness.d.ts.map +1 -0
  85. package/dist/witness.js +308 -0
  86. package/dist/witness.js.map +1 -0
  87. package/package.json +42 -0
  88. package/src/anchors.ts +168 -0
  89. package/src/anomalies.ts +773 -0
  90. package/src/attestation.ts +878 -0
  91. package/src/audit.ts +136 -0
  92. package/src/authority.ts +165 -0
  93. package/src/aws-nitro-root-ca.ts +47 -0
  94. package/src/cli.ts +284 -0
  95. package/src/contents-hash.ts +95 -0
  96. package/src/export.ts +278 -0
  97. package/src/index.ts +122 -0
  98. package/src/ingest.ts +967 -0
  99. package/src/reconstruct.ts +491 -0
  100. package/src/report-json.ts +331 -0
  101. package/src/report-md.ts +1067 -0
  102. package/src/rlp.ts +142 -0
  103. package/src/tar.ts +587 -0
  104. package/src/temporal.ts +629 -0
  105. package/src/types.ts +1528 -0
  106. package/src/validity.ts +90 -0
  107. package/src/verify-tiers.ts +137 -0
  108. package/src/witness.ts +407 -0
@@ -0,0 +1,795 @@
1
+ // Copyright (c) 2024-2026 Mike Argento. Licensed under the MIT License. See LICENSE.
2
+ /**
3
+ * bitgraph-audit offline attestation validation
4
+ *
5
+ * A self-contained Node port of the website's AWS Nitro attestation
6
+ * validator (website/src/lib/nitro-verify.ts), fully offline:
7
+ *
8
+ * 1. Decode the base64 COSE_Sign1 envelope (minimal CBOR reader).
9
+ * 2. Parse the attestation payload (module_id, timestamp, pcrs,
10
+ * certificate, cabundle, user_data).
11
+ * 3. Verify the ECDSA P-384 signature over the RFC 9052 Sig_structure
12
+ * against the leaf certificate's public key (node:crypto webcrypto).
13
+ * 4. Walk the certificate bundle: each certificate signed by its
14
+ * predecessor, leaf last.
15
+ * 5. Verify the top of the bundle against the trust root. The default
16
+ * trust root is the bundled AWS Nitro Enclaves Root CA G1 constant
17
+ * (aws-nitro-root-ca.ts, same string the website embeds); tests and
18
+ * non-AWS deployments may supply other trust material explicitly.
19
+ * 6. Evaluate every chain certificate's validity window at the
20
+ * attestation document's OWN timestamp. The website validator does
21
+ * not perform this check; it is added here because an audit runs
22
+ * long after the short-lived leaf certificates expire, and the
23
+ * document's own timestamp is the only offline-evaluable instant.
24
+ *
25
+ * Separately tracked facts, never conflated (G9): declared measurement
26
+ * present; attestation document present; document cryptographically
27
+ * validated; attested PCR0 matches the declared measurement; user_data
28
+ * bound to the signed body. PCR0 and user_data comparisons run only on a
29
+ * VALIDATED document: values parsed from an unvalidated document prove
30
+ * nothing.
31
+ *
32
+ * user_data binding mirrors the website and the enclave exactly: the
33
+ * enclave requests attestation with user_data = SHA-256 of the canonical
34
+ * signed body (the raw bytes whose base64 IS the canonical proof hash),
35
+ * so base64(user_data) must equal the proof hash the audit tool computes
36
+ * itself.
37
+ *
38
+ * No network calls anywhere. Never outputs "hardware verified" from
39
+ * presence alone.
40
+ */
41
+ import { webcrypto } from "node:crypto";
42
+ import { rootCaDerBytes } from "./aws-nitro-root-ca.js";
43
+ // ---------------------------------------------------------------------------
44
+ // Low-level document validation
45
+ // ---------------------------------------------------------------------------
46
+ /**
47
+ * Validate one base64 COSE_Sign1 attestation document offline. See the
48
+ * module doc for the check sequence. Document checks short-circuit; the
49
+ * PCR0 and user_data comparisons run only when the document validated and
50
+ * the corresponding expectation was supplied.
51
+ */
52
+ export async function validateNitroAttestationDocument(reportB64, options) {
53
+ const checks = [];
54
+ let pcrs = {};
55
+ let pcr0;
56
+ let moduleId;
57
+ let timestamp;
58
+ let certChainLength;
59
+ let userDataB64;
60
+ const failed = () => ({
61
+ documentValid: false,
62
+ checks,
63
+ failure: [...checks].reverse().find((c) => !c.pass)?.detail,
64
+ ...(pcr0 !== undefined ? { pcr0 } : {}),
65
+ pcrs,
66
+ ...(moduleId !== undefined ? { moduleId } : {}),
67
+ ...(timestamp !== undefined ? { timestamp } : {}),
68
+ ...(certChainLength !== undefined ? { certChainLength } : {}),
69
+ ...(userDataB64 !== undefined ? { userDataB64 } : {}),
70
+ });
71
+ try {
72
+ // Step 1: decode the CBOR envelope.
73
+ const reportBytes = b64ToBytes(reportB64);
74
+ if (reportBytes === null) {
75
+ checks.push({
76
+ name: "CBOR Decode",
77
+ pass: false,
78
+ detail: "reportB64 is not valid base64",
79
+ });
80
+ return failed();
81
+ }
82
+ const cose = decodeCbor(reportBytes, 0).value;
83
+ if (!Array.isArray(cose) || cose.length < 4) {
84
+ checks.push({ name: "CBOR Decode", pass: false, detail: "Not a valid COSE_Sign1 array" });
85
+ return failed();
86
+ }
87
+ checks.push({ name: "CBOR Decode", pass: true, detail: "COSE_Sign1 envelope parsed" });
88
+ const protectedHeaderBytes = cose[0];
89
+ const payloadBytes = cose[2];
90
+ const signatureBytes = cose[3];
91
+ if (!(protectedHeaderBytes instanceof Uint8Array) ||
92
+ !(payloadBytes instanceof Uint8Array) ||
93
+ !(signatureBytes instanceof Uint8Array)) {
94
+ checks.push({
95
+ name: "COSE Structure",
96
+ pass: false,
97
+ detail: "Missing protected/payload/signature",
98
+ });
99
+ return failed();
100
+ }
101
+ // Step 2: parse the attestation payload.
102
+ const attDoc = decodeCbor(payloadBytes, 0).value;
103
+ if (!attDoc || typeof attDoc !== "object" || Array.isArray(attDoc)) {
104
+ checks.push({
105
+ name: "Payload Decode",
106
+ pass: false,
107
+ detail: "Could not decode attestation payload",
108
+ });
109
+ return failed();
110
+ }
111
+ pcrs = extractPcrs(attDoc);
112
+ pcr0 = pcrs[0];
113
+ moduleId = typeof attDoc["module_id"] === "string" ? attDoc["module_id"] : undefined;
114
+ timestamp = typeof attDoc["timestamp"] === "number" ? attDoc["timestamp"] : undefined;
115
+ const ud = attDoc["user_data"];
116
+ userDataB64 =
117
+ ud instanceof Uint8Array && ud.length > 0 ? Buffer.from(ud).toString("base64") : undefined;
118
+ const cabundle = attDoc["cabundle"];
119
+ const leafCertBytes = attDoc["certificate"];
120
+ if (!(leafCertBytes instanceof Uint8Array)) {
121
+ checks.push({
122
+ name: "Leaf Certificate",
123
+ pass: false,
124
+ detail: "No leaf certificate in attestation",
125
+ });
126
+ return failed();
127
+ }
128
+ certChainLength = (Array.isArray(cabundle) ? cabundle.length : 0) + 1;
129
+ // Step 3: leaf certificate signature over the Sig_structure.
130
+ const leafCert = parseCertificate(leafCertBytes);
131
+ const sigStructure = encodeSigStructure(protectedHeaderBytes, payloadBytes);
132
+ const sigValid = await verifyP384Raw(signatureBytes, sigStructure, leafCert.publicKey);
133
+ checks.push({
134
+ name: "ECDSA P-384 Signature",
135
+ pass: sigValid,
136
+ detail: sigValid
137
+ ? "Attestation signed by leaf certificate"
138
+ : "Signature verification failed against leaf certificate public key",
139
+ });
140
+ if (!sigValid)
141
+ return failed();
142
+ // Step 4: walk the certificate chain (bundle root first, leaf last).
143
+ if (!Array.isArray(cabundle) || cabundle.length === 0 || !cabundle.every((c) => c instanceof Uint8Array)) {
144
+ checks.push({ name: "Certificate Chain", pass: false, detail: "Empty cabundle" });
145
+ return failed();
146
+ }
147
+ const chain = [...cabundle, leafCertBytes].map(parseCertificate);
148
+ let chainValid = true;
149
+ let chainFailReason = "";
150
+ for (let i = 1; i < chain.length; i++) {
151
+ const child = chain[i];
152
+ const parent = chain[i - 1];
153
+ const ok = await verifyP384Raw(child.signature, child.tbsCertificate, parent.publicKey);
154
+ if (!ok) {
155
+ chainValid = false;
156
+ chainFailReason = `Certificate ${i} signature invalid (parent: ${i - 1})`;
157
+ break;
158
+ }
159
+ }
160
+ checks.push({
161
+ name: "Certificate Chain",
162
+ pass: chainValid,
163
+ detail: chainValid ? `${chain.length} certificates, each signed by parent` : chainFailReason,
164
+ });
165
+ if (!chainValid)
166
+ return failed();
167
+ // Step 5: anchor the top of the bundle to the trust root.
168
+ const usingBundledRoot = options?.trustedRootCaDer === undefined;
169
+ const rootCert = parseCertificate(options?.trustedRootCaDer ?? rootCaDerBytes());
170
+ const topCert = chain[0];
171
+ const rootMatch = await verifyP384Raw(topCert.signature, topCert.tbsCertificate, rootCert.publicKey);
172
+ checks.push({
173
+ name: "AWS Nitro Root CA",
174
+ pass: rootMatch,
175
+ detail: rootMatch
176
+ ? usingBundledRoot
177
+ ? "Chain anchored to AWS Nitro Root G1 (CN=aws.nitro-enclaves)"
178
+ : "Chain anchored to the supplied trust root (non-default trust material)"
179
+ : usingBundledRoot
180
+ ? "Top of chain not signed by AWS Nitro Root CA"
181
+ : "Top of chain not signed by the supplied trust root",
182
+ });
183
+ if (!rootMatch)
184
+ return failed();
185
+ // Step 6: certificate validity windows at the document's own
186
+ // timestamp. This check does not exist in the website validator; see
187
+ // the module doc for why it is added and why the document timestamp
188
+ // is the evaluation instant.
189
+ let windowValid = true;
190
+ let windowDetail = `All ${chain.length} certificate validity windows contain the document timestamp`;
191
+ if (timestamp === undefined) {
192
+ windowValid = false;
193
+ windowDetail =
194
+ "Attestation document carries no timestamp; certificate validity cannot be evaluated offline";
195
+ }
196
+ else {
197
+ for (let i = 0; i < chain.length; i++) {
198
+ const cert = chain[i];
199
+ if (timestamp < cert.notBeforeMs || timestamp > cert.notAfterMs) {
200
+ windowValid = false;
201
+ windowDetail =
202
+ `Certificate ${i} validity window ` +
203
+ `[${new Date(cert.notBeforeMs).toISOString()}, ${new Date(cert.notAfterMs).toISOString()}] ` +
204
+ `does not contain the document timestamp ${new Date(timestamp).toISOString()}`;
205
+ break;
206
+ }
207
+ }
208
+ }
209
+ checks.push({ name: "Certificate Validity Window", pass: windowValid, detail: windowDetail });
210
+ if (!windowValid)
211
+ return failed();
212
+ // Document validated. Now, and only now, the separate comparisons.
213
+ let pcr0Matches;
214
+ if (options?.expectedPcr0 !== undefined) {
215
+ pcr0Matches = pcr0 === options.expectedPcr0;
216
+ checks.push({
217
+ name: "PCR0 Match",
218
+ pass: pcr0Matches,
219
+ detail: pcr0Matches
220
+ ? "PCR0 inside attestation matches declared measurement"
221
+ : `PCR0 mismatch: expected ${options.expectedPcr0.slice(0, 16)}..., got ${(pcr0 ?? "").slice(0, 16)}...`,
222
+ });
223
+ }
224
+ let userDataMatches;
225
+ if (options?.expectedUserDataB64 !== undefined) {
226
+ userDataMatches = userDataB64 !== undefined && userDataB64 === options.expectedUserDataB64;
227
+ checks.push({
228
+ name: "Bound to this proof",
229
+ pass: userDataMatches,
230
+ detail: userDataMatches
231
+ ? "Attestation user_data matches this proof's canonical hash"
232
+ : userDataB64 !== undefined
233
+ ? "Attestation user_data does not match this proof"
234
+ : "Attestation carries no user_data to bind",
235
+ });
236
+ }
237
+ return {
238
+ documentValid: true,
239
+ checks,
240
+ ...(pcr0 !== undefined ? { pcr0 } : {}),
241
+ pcrs,
242
+ ...(moduleId !== undefined ? { moduleId } : {}),
243
+ ...(timestamp !== undefined ? { timestamp } : {}),
244
+ certChainLength,
245
+ ...(userDataB64 !== undefined ? { userDataB64 } : {}),
246
+ ...(pcr0Matches !== undefined ? { pcr0Matches } : {}),
247
+ ...(userDataMatches !== undefined ? { userDataMatches } : {}),
248
+ };
249
+ }
250
+ catch (error) {
251
+ checks.push({
252
+ name: "Verification Error",
253
+ pass: false,
254
+ detail: error instanceof Error ? error.message : String(error),
255
+ });
256
+ return failed();
257
+ }
258
+ }
259
+ // ---------------------------------------------------------------------------
260
+ // Pipeline
261
+ // ---------------------------------------------------------------------------
262
+ /**
263
+ * Validate the attestation of every observed proof and populate
264
+ * AuthorityGroup.attested on the given authority analysis (the typed
265
+ * Phase 4c extension point). Groups without an attestation document keep
266
+ * attested undefined; a declared measurement is never treated as
267
+ * attested.
268
+ *
269
+ * The trust root defaults to the bundled AWS Nitro root; the override
270
+ * exists for tests and explicitly non-AWS trust material.
271
+ */
272
+ export async function validateAttestations(ingest, authority, options) {
273
+ const records = [];
274
+ const findings = [];
275
+ const counts = {
276
+ proofsWithDeclaredMeasurement: 0,
277
+ proofsWithDocument: 0,
278
+ documentsValidated: 0,
279
+ documentsFailed: 0,
280
+ pcr0Matches: 0,
281
+ pcr0Mismatches: 0,
282
+ userDataBound: 0,
283
+ userDataUnbound: 0,
284
+ };
285
+ for (const proof of ingest.proofs) {
286
+ const declaredMeasurement = proof.measurement !== undefined && proof.measurement.length > 0
287
+ ? proof.measurement
288
+ : undefined;
289
+ if (declaredMeasurement !== undefined)
290
+ counts.proofsWithDeclaredMeasurement++;
291
+ const attestation = readAttestation(proof);
292
+ const record = {
293
+ proofHash: proof.proofHash,
294
+ declaredMeasurementPresent: declaredMeasurement !== undefined,
295
+ ...(declaredMeasurement !== undefined ? { declaredMeasurement } : {}),
296
+ documentPresent: attestation !== undefined,
297
+ ...(attestation?.format !== undefined ? { attestationFormat: attestation.format } : {}),
298
+ documentValidated: false,
299
+ checks: [],
300
+ };
301
+ if (attestation !== undefined) {
302
+ counts.proofsWithDocument++;
303
+ const result = await validateNitroAttestationDocument(attestation.reportB64, {
304
+ ...(declaredMeasurement !== undefined ? { expectedPcr0: declaredMeasurement } : {}),
305
+ expectedUserDataB64: proof.proofHash,
306
+ ...(options?.trustedRootCaDer !== undefined
307
+ ? { trustedRootCaDer: options.trustedRootCaDer }
308
+ : {}),
309
+ });
310
+ record.documentValidated = result.documentValid;
311
+ record.checks = result.checks;
312
+ if (result.pcr0 !== undefined)
313
+ record.attestedPcr0 = result.pcr0;
314
+ record.pcrs = result.pcrs;
315
+ if (result.moduleId !== undefined)
316
+ record.moduleId = result.moduleId;
317
+ if (result.timestamp !== undefined)
318
+ record.timestamp = result.timestamp;
319
+ if (result.certChainLength !== undefined)
320
+ record.certChainLength = result.certChainLength;
321
+ if (result.userDataB64 !== undefined)
322
+ record.userDataB64 = result.userDataB64;
323
+ if (!result.documentValid) {
324
+ counts.documentsFailed++;
325
+ if (result.failure !== undefined)
326
+ record.validationFailure = result.failure;
327
+ findings.push({
328
+ code: "attestation-invalid",
329
+ ...pathOf(proof),
330
+ message: `attestation document failed offline validation: ${result.failure ?? "unknown"}. Presence of a document is reported separately and proves nothing by itself.`,
331
+ details: { proofHash: proof.proofHash },
332
+ });
333
+ }
334
+ else {
335
+ counts.documentsValidated++;
336
+ if (result.pcr0Matches !== undefined) {
337
+ record.pcr0MatchesDeclared = result.pcr0Matches;
338
+ if (result.pcr0Matches)
339
+ counts.pcr0Matches++;
340
+ else {
341
+ counts.pcr0Mismatches++;
342
+ findings.push({
343
+ code: "attestation-measurement-mismatch",
344
+ ...pathOf(proof),
345
+ message: "the validated attestation document's PCR0 does not equal the proof's declared " +
346
+ "environment.measurement. Declared and attested measurements are reported " +
347
+ "separately and disagree here.",
348
+ details: {
349
+ proofHash: proof.proofHash,
350
+ declared: declaredMeasurement,
351
+ ...(result.pcr0 !== undefined ? { attested: result.pcr0 } : {}),
352
+ },
353
+ });
354
+ }
355
+ }
356
+ if (result.userDataMatches !== undefined) {
357
+ record.userDataBoundToProof = result.userDataMatches;
358
+ if (result.userDataMatches)
359
+ counts.userDataBound++;
360
+ else {
361
+ counts.userDataUnbound++;
362
+ findings.push({
363
+ code: "attestation-user-data-mismatch",
364
+ ...pathOf(proof),
365
+ message: "the validated attestation document's user_data is not bound to this proof's " +
366
+ "canonical hash. A genuine attestation lifted from another proof would look " +
367
+ "exactly like this.",
368
+ details: {
369
+ proofHash: proof.proofHash,
370
+ ...(result.userDataB64 !== undefined ? { userDataB64: result.userDataB64 } : {}),
371
+ },
372
+ });
373
+ }
374
+ }
375
+ }
376
+ }
377
+ records.push(record);
378
+ }
379
+ if (authority !== undefined)
380
+ populateAuthorityGroups(authority, records);
381
+ return { records, findings, counts };
382
+ }
383
+ function populateAuthorityGroups(authority, records) {
384
+ const byHash = new Map(records.map((r) => [r.proofHash, r]));
385
+ for (const group of authority.groups) {
386
+ if (!group.attestationPresent)
387
+ continue;
388
+ const memberRecords = group.proofHashes
389
+ .map((h) => byHash.get(h))
390
+ .filter((r) => r !== undefined && r.documentPresent);
391
+ if (memberRecords.length === 0)
392
+ continue;
393
+ const validated = memberRecords.filter((r) => r.documentValidated);
394
+ const failedCount = memberRecords.length - validated.length;
395
+ const attestedValues = [...new Set(validated.map((r) => r.attestedPcr0).filter((v) => v !== undefined))].sort();
396
+ const attestedMeasurement = attestedValues.length === 1 ? attestedValues[0] : undefined;
397
+ group.attested = {
398
+ status: failedCount === 0 && validated.length > 0 ? "validated" : "validation-failed",
399
+ ...(attestedMeasurement !== undefined ? { attestedMeasurement } : {}),
400
+ ...(attestedMeasurement !== undefined && group.measurement !== undefined
401
+ ? { matchesDeclared: attestedMeasurement === group.measurement }
402
+ : {}),
403
+ validatedProofCount: validated.length,
404
+ failedProofCount: failedCount,
405
+ ...(attestedValues.length > 1 ? { attestedMeasurements: attestedValues } : {}),
406
+ };
407
+ }
408
+ }
409
+ function readAttestation(proof) {
410
+ const environment = proof.proof["environment"];
411
+ if (environment === null || typeof environment !== "object" || Array.isArray(environment)) {
412
+ return undefined;
413
+ }
414
+ const attestation = environment["attestation"];
415
+ if (attestation === null || typeof attestation !== "object" || Array.isArray(attestation)) {
416
+ return undefined;
417
+ }
418
+ const reportB64 = attestation["reportB64"];
419
+ if (typeof reportB64 !== "string" || reportB64.length === 0)
420
+ return undefined;
421
+ const format = attestation["format"];
422
+ return { reportB64, ...(typeof format === "string" ? { format } : {}) };
423
+ }
424
+ function pathOf(proof) {
425
+ const path = proof.sources[0]?.path;
426
+ return path !== undefined ? { path } : {};
427
+ }
428
+ // ---------------------------------------------------------------------------
429
+ // Base64 (mirrors the website's atob-based decoder: base64url tolerated,
430
+ // padding restored, anything else rejected via the round-trip property)
431
+ // ---------------------------------------------------------------------------
432
+ function b64ToBytes(b64) {
433
+ let s = b64.replace(/-/g, "+").replace(/_/g, "/");
434
+ while (s.length % 4 !== 0)
435
+ s += "=";
436
+ const decoded = Buffer.from(s, "base64");
437
+ if (decoded.toString("base64") !== s)
438
+ return null;
439
+ return new Uint8Array(decoded);
440
+ }
441
+ // ---------------------------------------------------------------------------
442
+ // CBOR encoder subset (Sig_structure only), ported from the website
443
+ // ---------------------------------------------------------------------------
444
+ function encodeCborInt(n) {
445
+ if (n < 24)
446
+ return new Uint8Array([n]);
447
+ if (n < 256)
448
+ return new Uint8Array([0x18, n]);
449
+ if (n < 65536)
450
+ return new Uint8Array([0x19, n >> 8, n & 0xff]);
451
+ return new Uint8Array([0x1a, (n >>> 24) & 0xff, (n >>> 16) & 0xff, (n >>> 8) & 0xff, n & 0xff]);
452
+ }
453
+ function encodeCborBytes(bytes) {
454
+ const lenPrefix = encodeCborInt(bytes.length);
455
+ lenPrefix[0] = (lenPrefix[0] & 0x1f) | 0x40;
456
+ const out = new Uint8Array(lenPrefix.length + bytes.length);
457
+ out.set(lenPrefix, 0);
458
+ out.set(bytes, lenPrefix.length);
459
+ return out;
460
+ }
461
+ function encodeCborText(s) {
462
+ const utf8 = new TextEncoder().encode(s);
463
+ const lenPrefix = encodeCborInt(utf8.length);
464
+ lenPrefix[0] = (lenPrefix[0] & 0x1f) | 0x60;
465
+ const out = new Uint8Array(lenPrefix.length + utf8.length);
466
+ out.set(lenPrefix, 0);
467
+ out.set(utf8, lenPrefix.length);
468
+ return out;
469
+ }
470
+ function encodeCborArray(items) {
471
+ const lenPrefix = encodeCborInt(items.length);
472
+ lenPrefix[0] = (lenPrefix[0] & 0x1f) | 0x80;
473
+ const totalLen = lenPrefix.length + items.reduce((sum, x) => sum + x.length, 0);
474
+ const out = new Uint8Array(totalLen);
475
+ let off = 0;
476
+ out.set(lenPrefix, off);
477
+ off += lenPrefix.length;
478
+ for (const item of items) {
479
+ out.set(item, off);
480
+ off += item.length;
481
+ }
482
+ return out;
483
+ }
484
+ /**
485
+ * COSE_Sign1 Sig_structure per RFC 9052 section 4.4:
486
+ * ["Signature1", body_protected, external_aad (empty), payload].
487
+ */
488
+ function encodeSigStructure(protectedHeader, payload) {
489
+ return encodeCborArray([
490
+ encodeCborText("Signature1"),
491
+ encodeCborBytes(protectedHeader),
492
+ encodeCborBytes(new Uint8Array(0)),
493
+ encodeCborBytes(payload),
494
+ ]);
495
+ }
496
+ // ---------------------------------------------------------------------------
497
+ // CBOR decoder subset, ported from the website with explicit bounds checks
498
+ // (the website indexes past the end silently; here truncation throws a
499
+ // precise error and the document fails closed)
500
+ // ---------------------------------------------------------------------------
501
+ function decodeCbor(data, offset = 0) {
502
+ const first = data[offset];
503
+ if (first === undefined)
504
+ throw new Error("CBOR: unexpected end of input");
505
+ const major = first >> 5;
506
+ const info = first & 0x1f;
507
+ offset++;
508
+ const byteAt = (position) => {
509
+ const value = data[position];
510
+ if (value === undefined)
511
+ throw new Error("CBOR: unexpected end of input");
512
+ return value;
513
+ };
514
+ function readLength() {
515
+ if (info < 24)
516
+ return info;
517
+ if (info === 24) {
518
+ const v = byteAt(offset);
519
+ offset += 1;
520
+ return v;
521
+ }
522
+ if (info === 25) {
523
+ const v = (byteAt(offset) << 8) | byteAt(offset + 1);
524
+ offset += 2;
525
+ return v;
526
+ }
527
+ if (info === 26) {
528
+ const v = ((byteAt(offset) << 24) |
529
+ (byteAt(offset + 1) << 16) |
530
+ (byteAt(offset + 2) << 8) |
531
+ byteAt(offset + 3)) >>>
532
+ 0;
533
+ offset += 4;
534
+ return v;
535
+ }
536
+ if (info === 27) {
537
+ let v = 0;
538
+ for (let i = 0; i < 8; i++)
539
+ v = v * 256 + byteAt(offset + i);
540
+ offset += 8;
541
+ return v;
542
+ }
543
+ throw new Error(`Unsupported CBOR length info: ${info}`);
544
+ }
545
+ switch (major) {
546
+ case 0:
547
+ return { value: readLength(), offset };
548
+ case 1:
549
+ return { value: -1 - readLength(), offset };
550
+ case 2: {
551
+ const len = readLength();
552
+ if (offset + len > data.length)
553
+ throw new Error("CBOR: byte string is truncated");
554
+ return { value: data.slice(offset, offset + len), offset: offset + len };
555
+ }
556
+ case 3: {
557
+ const len = readLength();
558
+ if (offset + len > data.length)
559
+ throw new Error("CBOR: text string is truncated");
560
+ const v = new TextDecoder().decode(data.slice(offset, offset + len));
561
+ return { value: v, offset: offset + len };
562
+ }
563
+ case 4: {
564
+ if (info === 31) {
565
+ const arr = [];
566
+ while (byteAt(offset) !== 0xff) {
567
+ const item = decodeCbor(data, offset);
568
+ arr.push(item.value);
569
+ offset = item.offset;
570
+ }
571
+ return { value: arr, offset: offset + 1 };
572
+ }
573
+ const len = readLength();
574
+ const arr = [];
575
+ for (let i = 0; i < len; i++) {
576
+ const item = decodeCbor(data, offset);
577
+ arr.push(item.value);
578
+ offset = item.offset;
579
+ }
580
+ return { value: arr, offset };
581
+ }
582
+ case 5: {
583
+ if (info === 31) {
584
+ const map = {};
585
+ while (byteAt(offset) !== 0xff) {
586
+ const k = decodeCbor(data, offset);
587
+ offset = k.offset;
588
+ const v = decodeCbor(data, offset);
589
+ offset = v.offset;
590
+ map[String(k.value)] = v.value;
591
+ }
592
+ return { value: map, offset: offset + 1 };
593
+ }
594
+ const len = readLength();
595
+ const map = {};
596
+ for (let i = 0; i < len; i++) {
597
+ const k = decodeCbor(data, offset);
598
+ offset = k.offset;
599
+ const v = decodeCbor(data, offset);
600
+ offset = v.offset;
601
+ map[String(k.value)] = v.value;
602
+ }
603
+ return { value: map, offset };
604
+ }
605
+ case 6: {
606
+ // Tag: read the tag number, then the tagged value.
607
+ readLength();
608
+ return decodeCbor(data, offset);
609
+ }
610
+ case 7: {
611
+ if (info === 20)
612
+ return { value: false, offset };
613
+ if (info === 21)
614
+ return { value: true, offset };
615
+ if (info === 22)
616
+ return { value: null, offset };
617
+ if (info === 23)
618
+ return { value: undefined, offset };
619
+ throw new Error(`Unsupported CBOR simple value: ${info}`);
620
+ }
621
+ default:
622
+ throw new Error(`Unsupported CBOR major type: ${major}`);
623
+ }
624
+ }
625
+ function parseCertificate(der) {
626
+ const r = readSequence(der, 0);
627
+ let off = r.contentStart;
628
+ // TBSCertificate (SEQUENCE)
629
+ const tbsHeader = readTLV(der, off);
630
+ const tbsCertificate = der.slice(off, tbsHeader.end);
631
+ off = tbsHeader.end;
632
+ // signatureAlgorithm (SEQUENCE): skipped, as in the website port.
633
+ const sigAlg = readTLV(der, off);
634
+ off = sigAlg.end;
635
+ // signatureValue (BIT STRING): first content byte is the unused-bit
636
+ // count (0 for signatures), the rest is a DER ECDSA-Sig-Value.
637
+ const sigBitString = readTLV(der, off);
638
+ const sigContent = der.slice(sigBitString.contentStart + 1, sigBitString.end);
639
+ const signature = derEcdsaSigToRaw(sigContent, 48);
640
+ const { publicKey, notBeforeMs, notAfterMs } = walkTbs(tbsCertificate);
641
+ return { tbsCertificate, signature, publicKey, notBeforeMs, notAfterMs };
642
+ }
643
+ function readTLV(data, offset) {
644
+ const tag = data[offset];
645
+ if (tag === undefined)
646
+ throw new Error("DER: unexpected end of input");
647
+ let off = offset + 1;
648
+ let length = data[off++];
649
+ if (length === undefined)
650
+ throw new Error("DER: unexpected end of input");
651
+ if (length & 0x80) {
652
+ const numBytes = length & 0x7f;
653
+ length = 0;
654
+ for (let i = 0; i < numBytes; i++) {
655
+ const b = data[off++];
656
+ if (b === undefined)
657
+ throw new Error("DER: unexpected end of input");
658
+ length = length * 256 + b;
659
+ }
660
+ }
661
+ const end = off + length;
662
+ if (end > data.length)
663
+ throw new Error("DER: element is truncated");
664
+ return { tag, contentStart: off, contentEnd: end, end };
665
+ }
666
+ function readSequence(data, offset) {
667
+ const tlv = readTLV(data, offset);
668
+ if (tlv.tag !== 0x30) {
669
+ throw new Error(`Expected SEQUENCE at offset ${offset}, got tag 0x${tlv.tag.toString(16)}`);
670
+ }
671
+ return tlv;
672
+ }
673
+ /**
674
+ * Walk a TBSCertificate: [0] version (optional), serialNumber, signature
675
+ * AlgorithmIdentifier, issuer, validity, subject, subjectPublicKeyInfo.
676
+ * Returns the SPKI public key point and the validity window.
677
+ */
678
+ function walkTbs(tbs) {
679
+ const seq = readSequence(tbs, 0);
680
+ let off = seq.contentStart;
681
+ if (tbs[off] === 0xa0) {
682
+ off = readTLV(tbs, off).end; // [0] version
683
+ }
684
+ off = readTLV(tbs, off).end; // serialNumber
685
+ off = readTLV(tbs, off).end; // signature AlgorithmIdentifier
686
+ off = readTLV(tbs, off).end; // issuer
687
+ // validity ::= SEQUENCE { notBefore Time, notAfter Time }
688
+ const validity = readSequence(tbs, off);
689
+ let vOff = validity.contentStart;
690
+ const notBeforeTlv = readTLV(tbs, vOff);
691
+ vOff = notBeforeTlv.end;
692
+ const notAfterTlv = readTLV(tbs, vOff);
693
+ const notBeforeMs = parseDerTime(tbs, notBeforeTlv);
694
+ const notAfterMs = parseDerTime(tbs, notAfterTlv);
695
+ off = validity.end;
696
+ off = readTLV(tbs, off).end; // subject
697
+ // SubjectPublicKeyInfo ::= SEQUENCE { AlgorithmIdentifier, BIT STRING }
698
+ const spki = readSequence(tbs, off);
699
+ let spkiOff = spki.contentStart;
700
+ const algId = readTLV(tbs, spkiOff);
701
+ spkiOff = algId.end;
702
+ const bitString = readTLV(tbs, spkiOff);
703
+ const publicKey = tbs.slice(bitString.contentStart + 1, bitString.end);
704
+ return { publicKey, notBeforeMs, notAfterMs };
705
+ }
706
+ /** UTCTime (0x17, YYMMDDHHMMSSZ) or GeneralizedTime (0x18, YYYYMMDDHHMMSSZ) to epoch ms. */
707
+ function parseDerTime(data, tlv) {
708
+ const text = String.fromCharCode(...data.slice(tlv.contentStart, tlv.contentEnd));
709
+ let year;
710
+ let rest;
711
+ if (tlv.tag === 0x17) {
712
+ if (!/^\d{12}Z$/.test(text))
713
+ throw new Error(`DER: malformed UTCTime "${text}"`);
714
+ const yy = Number.parseInt(text.slice(0, 2), 10);
715
+ year = yy < 50 ? 2000 + yy : 1900 + yy;
716
+ rest = text.slice(2);
717
+ }
718
+ else if (tlv.tag === 0x18) {
719
+ if (!/^\d{14}Z$/.test(text))
720
+ throw new Error(`DER: malformed GeneralizedTime "${text}"`);
721
+ year = Number.parseInt(text.slice(0, 4), 10);
722
+ rest = text.slice(4);
723
+ }
724
+ else {
725
+ throw new Error(`DER: expected a Time value, got tag 0x${tlv.tag.toString(16)}`);
726
+ }
727
+ const month = Number.parseInt(rest.slice(0, 2), 10);
728
+ const day = Number.parseInt(rest.slice(2, 4), 10);
729
+ const hour = Number.parseInt(rest.slice(4, 6), 10);
730
+ const minute = Number.parseInt(rest.slice(6, 8), 10);
731
+ const second = Number.parseInt(rest.slice(8, 10), 10);
732
+ return Date.UTC(year, month - 1, day, hour, minute, second);
733
+ }
734
+ /** DER ECDSA-Sig-Value (SEQUENCE { r INTEGER, s INTEGER }) to raw r||s. */
735
+ function derEcdsaSigToRaw(der, coordSize) {
736
+ const seq = readSequence(der, 0);
737
+ let off = seq.contentStart;
738
+ const rTlv = readTLV(der, off);
739
+ if (rTlv.tag !== 0x02)
740
+ throw new Error("Expected INTEGER for r");
741
+ let r = der.slice(rTlv.contentStart, rTlv.end);
742
+ off = rTlv.end;
743
+ const sTlv = readTLV(der, off);
744
+ if (sTlv.tag !== 0x02)
745
+ throw new Error("Expected INTEGER for s");
746
+ let s = der.slice(sTlv.contentStart, sTlv.end);
747
+ if (r[0] === 0x00 && r.length > coordSize)
748
+ r = r.slice(1);
749
+ if (s[0] === 0x00 && s.length > coordSize)
750
+ s = s.slice(1);
751
+ if (r.length > coordSize || s.length > coordSize) {
752
+ throw new Error("ECDSA signature component longer than the curve coordinate size");
753
+ }
754
+ const out = new Uint8Array(coordSize * 2);
755
+ out.set(r, coordSize - r.length);
756
+ out.set(s, coordSize * 2 - s.length);
757
+ return out;
758
+ }
759
+ // ---------------------------------------------------------------------------
760
+ // ECDSA P-384 via node:crypto webcrypto (no dependency). WebCrypto hashes
761
+ // the message itself (SHA-384) and does not enforce low-S form, matching
762
+ // the website's noble configuration (prehash + lowS: false).
763
+ // ---------------------------------------------------------------------------
764
+ async function verifyP384Raw(rawSig, message, publicKeyPoint) {
765
+ if (publicKeyPoint.length !== 97 || publicKeyPoint[0] !== 0x04)
766
+ return false;
767
+ if (rawSig.length !== 96)
768
+ return false;
769
+ try {
770
+ const key = await webcrypto.subtle.importKey("raw", publicKeyPoint, { name: "ECDSA", namedCurve: "P-384" }, false, ["verify"]);
771
+ return await webcrypto.subtle.verify({ name: "ECDSA", hash: "SHA-384" }, key, rawSig, message);
772
+ }
773
+ catch {
774
+ return false;
775
+ }
776
+ }
777
+ // ---------------------------------------------------------------------------
778
+ // PCR extraction, ported from the website (all-zero PCRs treated as absent)
779
+ // ---------------------------------------------------------------------------
780
+ function extractPcrs(attDoc) {
781
+ const pcrs = {};
782
+ const map = attDoc["pcrs"];
783
+ if (!map || typeof map !== "object")
784
+ return pcrs;
785
+ for (const [idx, val] of Object.entries(map)) {
786
+ if (val instanceof Uint8Array) {
787
+ const hex = Buffer.from(val).toString("hex");
788
+ if (hex.replace(/0/g, "").length > 0) {
789
+ pcrs[Number(idx)] = hex;
790
+ }
791
+ }
792
+ }
793
+ return pcrs;
794
+ }
795
+ //# sourceMappingURL=attestation.js.map