burnledger 0.8.1 → 0.9.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 (124) hide show
  1. package/README.md +139 -2
  2. package/dist/cjs/anchor.d.ts +5 -3
  3. package/dist/cjs/anchor.d.ts.map +1 -1
  4. package/dist/cjs/anchor.js +10 -4
  5. package/dist/cjs/anchor.js.map +1 -1
  6. package/dist/cjs/client.d.ts +95 -0
  7. package/dist/cjs/client.d.ts.map +1 -1
  8. package/dist/cjs/client.js +133 -2
  9. package/dist/cjs/client.js.map +1 -1
  10. package/dist/cjs/customer-keys.d.ts +73 -1
  11. package/dist/cjs/customer-keys.d.ts.map +1 -1
  12. package/dist/cjs/customer-keys.js +329 -3
  13. package/dist/cjs/customer-keys.js.map +1 -1
  14. package/dist/cjs/enclave-registration.d.ts +153 -0
  15. package/dist/cjs/enclave-registration.d.ts.map +1 -0
  16. package/dist/cjs/enclave-registration.js +275 -0
  17. package/dist/cjs/enclave-registration.js.map +1 -0
  18. package/dist/cjs/enclave-seal.d.ts +43 -0
  19. package/dist/cjs/enclave-seal.d.ts.map +1 -1
  20. package/dist/cjs/enclave-seal.js +62 -1
  21. package/dist/cjs/enclave-seal.js.map +1 -1
  22. package/dist/cjs/errors.d.ts +10 -0
  23. package/dist/cjs/errors.d.ts.map +1 -1
  24. package/dist/cjs/errors.js +11 -1
  25. package/dist/cjs/errors.js.map +1 -1
  26. package/dist/cjs/index.browser.d.ts +7 -3
  27. package/dist/cjs/index.browser.d.ts.map +1 -1
  28. package/dist/cjs/index.browser.js +6 -3
  29. package/dist/cjs/index.browser.js.map +1 -1
  30. package/dist/cjs/index.d.ts +22 -10
  31. package/dist/cjs/index.d.ts.map +1 -1
  32. package/dist/cjs/index.js +30 -6
  33. package/dist/cjs/index.js.map +1 -1
  34. package/dist/cjs/key-group.d.ts +80 -0
  35. package/dist/cjs/key-group.d.ts.map +1 -0
  36. package/dist/cjs/key-group.js +136 -0
  37. package/dist/cjs/key-group.js.map +1 -0
  38. package/dist/cjs/models.d.ts +60 -0
  39. package/dist/cjs/models.d.ts.map +1 -1
  40. package/dist/cjs/models.js +63 -0
  41. package/dist/cjs/models.js.map +1 -1
  42. package/dist/cjs/status-document.d.ts +25 -0
  43. package/dist/cjs/status-document.d.ts.map +1 -0
  44. package/dist/cjs/status-document.js +62 -0
  45. package/dist/cjs/status-document.js.map +1 -0
  46. package/dist/cjs/verify.d.ts +108 -3
  47. package/dist/cjs/verify.d.ts.map +1 -1
  48. package/dist/cjs/verify.js +389 -146
  49. package/dist/cjs/verify.js.map +1 -1
  50. package/dist/cjs/web-verifier.d.ts +23 -3
  51. package/dist/cjs/web-verifier.d.ts.map +1 -1
  52. package/dist/cjs/web-verifier.js +31 -5
  53. package/dist/cjs/web-verifier.js.map +1 -1
  54. package/dist/esm/anchor.d.ts +5 -3
  55. package/dist/esm/anchor.d.ts.map +1 -1
  56. package/dist/esm/anchor.js +10 -4
  57. package/dist/esm/anchor.js.map +1 -1
  58. package/dist/esm/cli.d.ts +13 -19
  59. package/dist/esm/cli.d.ts.map +1 -1
  60. package/dist/esm/cli.js +88 -81
  61. package/dist/esm/cli.js.map +1 -1
  62. package/dist/esm/client.d.ts +95 -0
  63. package/dist/esm/client.d.ts.map +1 -1
  64. package/dist/esm/client.js +133 -2
  65. package/dist/esm/client.js.map +1 -1
  66. package/dist/esm/customer-keys.d.ts +73 -1
  67. package/dist/esm/customer-keys.d.ts.map +1 -1
  68. package/dist/esm/customer-keys.js +323 -3
  69. package/dist/esm/customer-keys.js.map +1 -1
  70. package/dist/esm/enclave-registration.d.ts +153 -0
  71. package/dist/esm/enclave-registration.d.ts.map +1 -0
  72. package/dist/esm/enclave-registration.js +265 -0
  73. package/dist/esm/enclave-registration.js.map +1 -0
  74. package/dist/esm/enclave-seal.d.ts +43 -0
  75. package/dist/esm/enclave-seal.d.ts.map +1 -1
  76. package/dist/esm/enclave-seal.js +61 -1
  77. package/dist/esm/enclave-seal.js.map +1 -1
  78. package/dist/esm/errors.d.ts +10 -0
  79. package/dist/esm/errors.d.ts.map +1 -1
  80. package/dist/esm/errors.js +10 -0
  81. package/dist/esm/errors.js.map +1 -1
  82. package/dist/esm/index.browser.d.ts +7 -3
  83. package/dist/esm/index.browser.d.ts.map +1 -1
  84. package/dist/esm/index.browser.js +6 -3
  85. package/dist/esm/index.browser.js.map +1 -1
  86. package/dist/esm/index.d.ts +22 -10
  87. package/dist/esm/index.d.ts.map +1 -1
  88. package/dist/esm/index.js +18 -8
  89. package/dist/esm/index.js.map +1 -1
  90. package/dist/esm/key-group.d.ts +80 -0
  91. package/dist/esm/key-group.d.ts.map +1 -0
  92. package/dist/esm/key-group.js +130 -0
  93. package/dist/esm/key-group.js.map +1 -0
  94. package/dist/esm/models.d.ts +60 -0
  95. package/dist/esm/models.d.ts.map +1 -1
  96. package/dist/esm/models.js +59 -0
  97. package/dist/esm/models.js.map +1 -1
  98. package/dist/esm/status-document.d.ts +25 -0
  99. package/dist/esm/status-document.d.ts.map +1 -0
  100. package/dist/esm/status-document.js +59 -0
  101. package/dist/esm/status-document.js.map +1 -0
  102. package/dist/esm/verify.d.ts +108 -3
  103. package/dist/esm/verify.d.ts.map +1 -1
  104. package/dist/esm/verify.js +386 -146
  105. package/dist/esm/verify.js.map +1 -1
  106. package/dist/esm/web-verifier.d.ts +23 -3
  107. package/dist/esm/web-verifier.d.ts.map +1 -1
  108. package/dist/esm/web-verifier.js +27 -5
  109. package/dist/esm/web-verifier.js.map +1 -1
  110. package/package.json +1 -1
  111. package/src/anchor.ts +10 -4
  112. package/src/cli.ts +96 -79
  113. package/src/client.ts +198 -6
  114. package/src/customer-keys.ts +373 -3
  115. package/src/enclave-registration.ts +390 -0
  116. package/src/enclave-seal.ts +108 -1
  117. package/src/errors.ts +11 -0
  118. package/src/index.browser.ts +9 -3
  119. package/src/index.ts +35 -6
  120. package/src/key-group.ts +181 -0
  121. package/src/models.ts +131 -0
  122. package/src/status-document.ts +59 -0
  123. package/src/verify.ts +499 -151
  124. package/src/web-verifier.ts +36 -3
@@ -17,13 +17,15 @@
17
17
  * output at issuance time (transparency_status=PENDING, no transparency).
18
18
  */
19
19
  Object.defineProperty(exports, "__esModule", { value: true });
20
- exports.VALID_REVOCATION_UNKNOWN = exports.VALID_KEY_WINDOW_UNKNOWN = exports.VALID_KEY_COMPROMISED_LATER = exports.KEY_COMPROMISED = exports.KEY_OUTSIDE_VALIDITY = exports.KNOWN_FORMAT_VERSIONS = void 0;
20
+ exports.VALID_REVOCATION_UNKNOWN = exports.PRE_PROTOCOL_12_IMAGES = exports.VALID_KEY_WINDOW_UNKNOWN = exports.VALID_KEY_COMPROMISED_LATER = exports.KEY_COMPROMISED = exports.KEY_OUTSIDE_VALIDITY = exports.KNOWN_FORMAT_VERSIONS = void 0;
21
21
  exports.hexToBytes = hexToBytes;
22
22
  exports.bytesToHex = bytesToHex;
23
23
  exports.publicKeyFromHex = publicKeyFromHex;
24
24
  exports.keyIsUsable = keyIsUsable;
25
25
  exports.evaluateKey = evaluateKey;
26
26
  exports.certificateAnchor = certificateAnchor;
27
+ exports.issuingImage = issuingImage;
28
+ exports.registrationQualifier = registrationQualifier;
27
29
  exports.verifyCertificate = verifyCertificate;
28
30
  exports.verifyTransparency = verifyTransparency;
29
31
  exports.issuanceBytes = issuanceBytes;
@@ -34,6 +36,7 @@ exports.verifyConsistency = verifyConsistency;
34
36
  exports.buildCertificateStatusPayload = buildCertificateStatusPayload;
35
37
  exports.buildKeyListPayload = buildKeyListPayload;
36
38
  exports.verifyCertificateWithStatus = verifyCertificateWithStatus;
39
+ exports.verifyStatusStatement = verifyStatusStatement;
37
40
  const errors_js_1 = require("./errors.js");
38
41
  // ---------------------------------------------------------------------------
39
42
  // Pure byte helpers (no Buffer, no node:crypto)
@@ -82,8 +85,9 @@ const PAYLOAD_TYPE_ATTESTATION_V8 = "burnledger.attestation.v8";
82
85
  const PAYLOAD_TYPE_VERIFICATION_RECORD_V8 = "burnledger.verification_record.v8";
83
86
  const FORMAT_VERSION_V8 = "8.0";
84
87
  // v9 adds two per-system fields, authorization and customer_key_id: whether the
85
- // enclave checked this system against a customer-signed registration
86
- // certificate (ADR-025 §4), and under which customer key group if it did.
88
+ // enclave checked this system against a registration certificate (ADR-025 §4),
89
+ // and under which customer key group if it did. That the key group SIGNED the
90
+ // registration holds only from enclave protocol 12: see PRE_PROTOCOL_12_IMAGES.
87
91
  //
88
92
  // The attestation gains NO field at v9. Its tag still moves, because the tag
89
93
  // follows the record's version and the record's bytes changed — a v9
@@ -227,17 +231,22 @@ const PAYLOAD_TYPE_LOG_LEAF = "burnledger.log_leaf.v3";
227
231
  // in the same enclave call that signs the certificate (ADR-016 §2).
228
232
  const PAYLOAD_TYPE_CERTIFICATE_STATUS = "burnledger.certificate_status.v3";
229
233
  const PAYLOAD_TYPE_KEY_LIST = "burnledger.key_list.v3";
234
+ /** Decode hex: whole bytes, digits in either case, nothing else. Anything else
235
+ * throws TypeError. It is exported, so its input is a boundary: reading a stray
236
+ * character as 0, as it once did, turned a typo into different bytes. */
230
237
  function hexToBytes(hex) {
231
- const len = hex.length >>> 1;
232
- const out = new Uint8Array(len);
233
- for (let i = 0; i < len; i++) {
234
- const hi = hex.charCodeAt(i * 2);
235
- const lo = hex.charCodeAt(i * 2 + 1);
236
- out[i] = (unhex(hi) << 4) | unhex(lo);
238
+ if (typeof hex !== "string")
239
+ throw new TypeError(`hex is not a string: ${typeof hex}`);
240
+ if (hex.length % 2 !== 0)
241
+ throw new TypeError(`hex has an odd number of digits: ${hex.length}`);
242
+ const out = new Uint8Array(hex.length / 2);
243
+ for (let i = 0; i < out.length; i++) {
244
+ out[i] = (unhex(hex, i * 2) << 4) | unhex(hex, i * 2 + 1);
237
245
  }
238
246
  return out;
239
247
  }
240
- function unhex(c) {
248
+ function unhex(hex, at) {
249
+ const c = hex.charCodeAt(at);
241
250
  // 0-9
242
251
  if (c >= 48 && c <= 57)
243
252
  return c - 48;
@@ -247,7 +256,7 @@ function unhex(c) {
247
256
  // A-F
248
257
  if (c >= 65 && c <= 70)
249
258
  return c - 55;
250
- return 0;
259
+ throw new TypeError(`hex has a non-hex character ${JSON.stringify(hex[at])} at ${at}`);
251
260
  }
252
261
  function bytesToHex(bytes) {
253
262
  let out = "";
@@ -287,6 +296,13 @@ function textToBytes(s) {
287
296
  return new TextEncoder().encode(s);
288
297
  }
289
298
  async function publicKeyFromHex(crypto, hexKey, opts) {
299
+ // Refused here, as the VerificationError callers are told to catch, rather
300
+ // than left to hexToBytes's TypeError. The alphabet is the one
301
+ // core.KeyEntry.PublicKeyInfo takes through hex.DecodeString: either case,
302
+ // whole bytes, nothing else.
303
+ if (typeof hexKey !== "string" || !HEX_TEXT_RE.test(hexKey)) {
304
+ throw new errors_js_1.VerificationError(`public key is not hex: ${JSON.stringify(hexKey)}`);
305
+ }
290
306
  const raw = hexToBytes(hexKey);
291
307
  if (raw.length !== 32) {
292
308
  throw new errors_js_1.VerificationError(`public key must be 32 bytes, got ${raw.length}`);
@@ -394,7 +410,7 @@ function certificateAnchor(certificate) {
394
410
  if (transparency == null)
395
411
  return null;
396
412
  const sth = transparency.signed_tree_head;
397
- if (sth === undefined || typeof sth.timestamp !== "string")
413
+ if (sth == null || typeof sth.timestamp !== "string")
398
414
  return null;
399
415
  try {
400
416
  return parseRfc3339(sth.timestamp);
@@ -424,15 +440,22 @@ async function verifiedCertificateAnchor(crypto, certificate, pki) {
424
440
  return null;
425
441
  const transparency = certificate.transparency;
426
442
  const sth = transparency.signed_tree_head;
443
+ // Only a refusal of the head's own fields means "no anchor". A bare catch
444
+ // here gave a defect in this library — or a crypto provider that threw — the
445
+ // same quiet answer as a malformed document.
446
+ let headPayload;
447
+ let headSig;
427
448
  try {
428
- const headPayload = buildTreeHeadPayload(sth);
429
- const headSig = decodeSignature(sth.signature);
430
- if (!(await crypto.ed25519Verify(pki.keyBytes, headPayload, headSig)))
449
+ headPayload = buildTreeHeadPayload(sth);
450
+ headSig = decodeFixed(sth.signature, "signed_tree_head.signature", 64);
451
+ }
452
+ catch (e) {
453
+ if (e instanceof errors_js_1.VerificationError)
431
454
  return null;
455
+ throw e;
432
456
  }
433
- catch {
457
+ if (!(await crypto.ed25519Verify(pki.keyBytes, headPayload, headSig)))
434
458
  return null;
435
- }
436
459
  return anchor;
437
460
  }
438
461
  /** Throws for a key that cannot be used at all; returns the verdict otherwise.
@@ -468,14 +491,155 @@ role = "issuer key") {
468
491
  }
469
492
  return verdict;
470
493
  }
494
+ function policySet(values, field, lower) {
495
+ if (values === undefined)
496
+ return undefined;
497
+ if (!Array.isArray(values) || !values.every((v) => typeof v === "string" && v !== "")) {
498
+ throw new TypeError(`requireAuthorization.${field} must be an array of non-empty strings`);
499
+ }
500
+ if (values.length === 0) {
501
+ // An empty requirement requires nothing, which is never what a caller who
502
+ // built a policy meant.
503
+ throw new Error(`an empty requireAuthorization.${field} requires nothing; omit it instead`);
504
+ }
505
+ return new Set(values.map((v) => (lower ? v.toLowerCase() : v)));
506
+ }
507
+ /** Check a policy before anything is verified, so a caller's mistake surfaces as
508
+ * one whatever the record turns out to be. */
509
+ function parsePolicy(policy) {
510
+ const keys = policy.customerKeyIds;
511
+ if (keys === undefined || (Array.isArray(keys) && keys.length === 0)) {
512
+ throw new TypeError("requireAuthorization.customerKeyIds is required, with at least one key id: `certified` under " +
513
+ "a key id you did not name proves nothing to you, because the relay can enroll a key group of its own");
514
+ }
515
+ return {
516
+ systems: policySet(policy.systems, "systems", true),
517
+ customerKeyIds: policySet(policy.customerKeyIds, "customerKeyIds", false),
518
+ };
519
+ }
520
+ /**
521
+ * The enclave images that issued format 9.0 while running wire protocol 11 or
522
+ * earlier, by PCR0, as the published measurement history records them
523
+ * (website/docs/enclave-measurements/, docs/CONNECTOR-STATUS.md).
524
+ *
525
+ * Their `certified` is not evidence of a customer signature. Before protocol 12
526
+ * the enclave enrolled a key group with no proof that anyone held its keys, and
527
+ * registered a system for whoever relayed the authorization — so the host could
528
+ * make a genuine 9.0 record say `certified`, under a key id it chose, with no
529
+ * customer signature anywhere (reproduced by an independent re-review,
530
+ * 2026-09-13).
531
+ *
532
+ * THE SET IS CLOSED BY POLICY, NOT BY ENFORCEMENT: the release runbook forbids
533
+ * admitting or rolling back to an image of protocol 11 or earlier once any
534
+ * customer key is enrolled, and admitting one takes a KMS re-pin, which ADR-027
535
+ * leaves to the owner. The protocol-12 cutover's KMS tighten drops every
536
+ * measurement here from the signing key's policy, after which none of them can
537
+ * sign. v42 ran for seven minutes and is recorded as issuing nothing; it is here
538
+ * because it held the signing key while it ran.
539
+ *
540
+ * The Python SDK carries the same list, in _verify.py.
541
+ */
542
+ exports.PRE_PROTOCOL_12_IMAGES = new Map([
543
+ ["0e79f985c287fbad3ed9aa1d5443189c45428929086dc4d4312ba71c4039742a8bca02f4ea15ab318d82e1d86ff883d3", "EIF v39"],
544
+ ["665864551aeb57705c3b3721018112cbd0e2be98de2e0ff9142a6c0abf366c72c6ba6cdcd31cd6ed8471862e1b9187ce", "EIF v40"],
545
+ ["7a61327a0ee11508764a8ad03d68e81e3b374e4e605a284e9e815ec9cf57de8497e59d57048977441f9c5c98bee1d14b", "EIF v41"],
546
+ ["f1e02ace0011bfc0d60d6dd76da0078b7706f3165eff6904a36e97e5d2029246099ef20bf15f87d0263dd843f878731c", "EIF v42"],
547
+ ["10ed72207ffd6a14f5913130eca69e9f4f9de0447b360e9cfd8aa8e349681c6fe98c515499a38a347d67932a6201a9b4", "EIF v43"],
548
+ ["0bfbd2251cb9e138491c6ce424ad132b28be152e1486aa7100acabce0201f3f5d500eb47dc00184de4eebc93fe4dc695", "EIF v44"],
549
+ ["4d68b3ef6b77418f2827964400241ee9db7e101ce53666b75cc6cf7aa17d7c9f3a1b9055bafec4f799beee6e00227546", "EIF v45 and v46"],
550
+ ]);
551
+ function issuingImage(certificate) {
552
+ const issuer = certificate.issuer;
553
+ const pcr0 = String(issuer?.enclave_pcr0 ?? "").toLowerCase();
554
+ if (pcr0 === "")
555
+ return { kind: "unnamed" };
556
+ const name = exports.PRE_PROTOCOL_12_IMAGES.get(pcr0);
557
+ return name === undefined
558
+ ? { kind: "protocol-12-or-later", pcr0 }
559
+ : { kind: "before-protocol-12", name, pcr0 };
560
+ }
561
+ /**
562
+ * What `certified` is evidence of, given the image that signed the record: the
563
+ * words every verifier prints after "certified under <key id> —". The Go CLI
564
+ * (cmd/cli/registration_image.go), both SDK CLIs and the /verify page state it
565
+ * identically, and testdata/registration_line_cases.json holds them to it.
566
+ */
567
+ function registrationQualifier(image) {
568
+ switch (image.kind) {
569
+ case "unnamed":
570
+ return ("not evidence here: this record names no enclave image, so it may come from one " +
571
+ "that could mark a system certified without the customer's signature.");
572
+ case "before-protocol-12":
573
+ return (`not evidence here: this record comes from ${image.name}, an enclave image of ` +
574
+ "protocol 11 or earlier, which could mark a system certified without the customer's signature.");
575
+ case "protocol-12-or-later":
576
+ return ("the key group with that id signed this system's registration, if that key id is yours: " +
577
+ "this record comes from an enclave image of protocol 12 or later. If that key id is not yours, " +
578
+ "someone else authorized this verification.");
579
+ }
580
+ }
581
+ /**
582
+ * Throw unless a record that has already verified meets `policy`.
583
+ *
584
+ * From 9.0 every field read here is under the certificate signature, the
585
+ * issuer's enclave_pcr0 included. Below 9.0
586
+ * nothing signs `authorization` — those records carry the field outside the
587
+ * signed bytes — so one that says `certified` says only what its last holder
588
+ * typed. It fails on the format, before the field is read.
589
+ */
590
+ function enforceAuthorization(certificate, formatVersion, policy) {
591
+ const systemsWanted = policy.systems;
592
+ if (!signatureCoversAuthorization(formatVersion)) {
593
+ throw new errors_js_1.VerificationError(`authorization required, but this record is format ${formatVersion}, which predates ` +
594
+ `customer-signed registration (${FORMAT_VERSION_V9}); it cannot show that any system was certified`, errors_js_1.AUTHORIZATION_POLICY_FAILED);
595
+ }
596
+ const systems = certificate.systems ?? [];
597
+ let required = systems;
598
+ if (systemsWanted !== undefined) {
599
+ const byId = new Map(systems.map((s) => [String(s.system_id).toLowerCase(), s]));
600
+ const missing = [...systemsWanted].filter((id) => !byId.has(id)).sort();
601
+ if (missing.length > 0) {
602
+ throw new errors_js_1.VerificationError(`authorization required for system ${missing[0]}, which this record does not cover`, errors_js_1.AUTHORIZATION_POLICY_FAILED);
603
+ }
604
+ required = [...systemsWanted].sort().map((id) => byId.get(id));
605
+ }
606
+ const labelOf = (s) => `system ${JSON.stringify(s.system_name)} (${String(s.system_id)})`;
607
+ for (const s of required) {
608
+ if (s.authorization !== AUTHORIZATION_CERTIFIED) {
609
+ throw new errors_js_1.VerificationError(`authorization required, but ${labelOf(s)} is ${JSON.stringify(s.authorization ?? null)}: ` +
610
+ "the enclave did not check it against a registration", errors_js_1.AUTHORIZATION_POLICY_FAILED);
611
+ }
612
+ }
613
+ // Asked once, for the record, after every required system has said certified:
614
+ // which image said it decides whether `certified` means a customer signed.
615
+ const image = issuingImage(certificate);
616
+ if (image.kind === "unnamed") {
617
+ throw new errors_js_1.VerificationError("authorization required, but this record names no enclave image (issuer.enclave_pcr0 is " +
618
+ "absent), so nothing shows it came from one that registers a system only with the " +
619
+ "customer's signature", errors_js_1.AUTHORIZATION_POLICY_FAILED);
620
+ }
621
+ if (image.kind === "before-protocol-12") {
622
+ throw new errors_js_1.VerificationError(`authorization required, but this record was signed by ${image.name} (PCR0 ${image.pcr0.slice(0, 16)}…), ` +
623
+ "an enclave image of wire protocol 11 or earlier, which could register a system without " +
624
+ "the customer's signature; its `certified` does not show that any key group signed anything", errors_js_1.AUTHORIZATION_POLICY_FAILED);
625
+ }
626
+ for (const s of required) {
627
+ if (!policy.customerKeyIds.has(s.customer_key_id)) {
628
+ throw new errors_js_1.VerificationError(`${labelOf(s)} is certified under ${String(s.customer_key_id)}, which is not a key you ` +
629
+ "accept; if it is not yours, someone else authorized this verification", errors_js_1.AUTHORIZATION_POLICY_FAILED);
630
+ }
631
+ }
632
+ }
471
633
  /** Verify all signatures on a deletion certificate offline. */
472
- async function verifyCertificate(crypto, certificate, publicKeys) {
634
+ async function verifyCertificate(crypto, certificate, publicKeys, options) {
473
635
  // Step 0: can this SDK read the format at all? Every check below rebuilds
474
636
  // signed bytes from the version, so an unreadable version makes all of them
475
637
  // meaningless - and "unknown issuer key" would be the wrong thing to tell
476
638
  // someone holding a record that is merely newer than this library.
639
+ requireObject(certificate, "certificate");
640
+ const policy = options?.requireAuthorization === undefined ? undefined : parsePolicy(options.requireAuthorization);
477
641
  const formatVersion = checkFormatVersion(certificate.certificate_format_version);
478
- const issuer = certificate.issuer;
642
+ const issuer = objectAt(certificate, "issuer", "issuer");
479
643
  // Step 0b: can this SDK check the algorithm the record names? Asked before
480
644
  // any signature, because verifying an Ed25519 signature over a record that
481
645
  // says it was signed with something else answers a question nobody asked.
@@ -486,7 +650,7 @@ async function verifyCertificate(crypto, certificate, publicKeys) {
486
650
  `this version of the SDK verifies ${ALGORITHM_ED25519}. ` +
487
651
  "The record may be genuine and simply signed with a scheme this library does not implement.", errors_js_1.UNSUPPORTED_ALGORITHM);
488
652
  }
489
- const keyId = issuer.key_id;
653
+ const keyId = stringAt(issuer, "key_id", "issuer.key_id");
490
654
  const pki = publicKeys.get(keyId);
491
655
  if (pki === undefined)
492
656
  throw new errors_js_1.VerificationError(`unknown issuer key: ${keyId}`);
@@ -496,7 +660,7 @@ async function verifyCertificate(crypto, certificate, publicKeys) {
496
660
  // 1. Certificate signature FIRST — nothing below may trust a field until the
497
661
  // bytes carrying it are covered by a verified signature.
498
662
  const certPayload = buildCertificatePayload(certificate);
499
- const certSig = decodeSignature(certificate.certificate_signature);
663
+ const certSig = decodeFixed(certificate.certificate_signature, "certificate_signature", 64);
500
664
  if (!(await crypto.ed25519Verify(pki.keyBytes, certPayload, certSig))) {
501
665
  throw new errors_js_1.VerificationError("certificate signature is invalid");
502
666
  }
@@ -510,7 +674,7 @@ async function verifyCertificate(crypto, certificate, publicKeys) {
510
674
  attested_at: att.attested_at,
511
675
  systems: attestationSystems(certificate),
512
676
  }, certificate.certificate_format_version);
513
- const attSig = decodeSignature(att.attestation_signature);
677
+ const attSig = decodeFixed(att.attestation_signature, "attestation.attestation_signature", 64);
514
678
  if (!(await crypto.ed25519Verify(pki.keyBytes, attPayload, attSig))) {
515
679
  throw new errors_js_1.VerificationError("attestation signature is invalid");
516
680
  }
@@ -559,6 +723,9 @@ async function verifyCertificate(crypto, certificate, publicKeys) {
559
723
  if (!anyAttested) {
560
724
  throw new errors_js_1.VerificationError("incomplete verification: no system held any records at attestation time");
561
725
  }
726
+ if (policy !== undefined) {
727
+ enforceAuthorization(certificate, formatVersion, policy);
728
+ }
562
729
  // A soft key verdict is the result, not a footnote: VALID_KEY_WINDOW_UNKNOWN
563
730
  // and VALID_KEY_COMPROMISED_LATER mean this document still needs a human, and
564
731
  // reporting VALID here would be the lying by omission ADR-017 exists to stop.
@@ -567,13 +734,14 @@ async function verifyCertificate(crypto, certificate, publicKeys) {
567
734
  const NIL_UUID = "00000000-0000-0000-0000-000000000000";
568
735
  /** Verify the transparency proof embedded in a certificate. */
569
736
  async function verifyTransparency(crypto, certificate, publicKeys) {
570
- const transparency = certificate.transparency;
571
- if (transparency == null) {
737
+ requireObject(certificate, "certificate");
738
+ if (certificate.transparency == null) {
572
739
  return "NOT_AVAILABLE";
573
740
  }
574
- const sth = transparency.signed_tree_head;
575
- const issuer = certificate.issuer;
576
- const keyId = issuer.key_id;
741
+ const transparency = objectAt(certificate, "transparency", "transparency");
742
+ const sth = objectAt(transparency, "signed_tree_head", "transparency.signed_tree_head");
743
+ const issuer = objectAt(certificate, "issuer", "issuer");
744
+ const keyId = stringAt(issuer, "key_id", "issuer.key_id");
577
745
  const pki = publicKeys.get(keyId);
578
746
  if (pki === undefined)
579
747
  throw new errors_js_1.VerificationError(`unknown issuer key: ${keyId}`);
@@ -581,7 +749,7 @@ async function verifyTransparency(crypto, certificate, publicKeys) {
581
749
  requireUsableKey(pki, certificateAnchor(certificate), keyId);
582
750
  // 1. Tree head signature
583
751
  const headPayload = buildTreeHeadPayload(sth);
584
- const headSig = decodeSignature(sth.signature);
752
+ const headSig = decodeFixed(sth.signature, "signed_tree_head.signature", 64);
585
753
  if (!(await crypto.ed25519Verify(pki.keyBytes, headPayload, headSig))) {
586
754
  throw new errors_js_1.VerificationError("tree head signature is invalid");
587
755
  }
@@ -594,10 +762,10 @@ async function verifyTransparency(crypto, certificate, publicKeys) {
594
762
  // revocation of the same certificate produced identical leaves and the tree
595
763
  // committed to neither the entry type nor when it happened.
596
764
  const issuanceData = issuanceBytes(certificate);
597
- const leafPayload = buildLogLeafPayload(transparency.entry_type, certificate.certificate_id, await crypto.sha256(issuanceData), transparency.appended_at);
765
+ const leafPayload = buildLogLeafPayload(transparency.entry_type, stringAt(certificate, "certificate_id", "certificate_id"), await crypto.sha256(issuanceData), transparency.appended_at);
598
766
  const leaf = await hashLeaf(crypto, leafPayload);
599
- const proofHashes = (transparency.inclusion_proof ?? []).map((h) => decodeBytes(h));
600
- const root = decodeBytes(sth.root_hash);
767
+ const proofHashes = arrayAt(transparency, "inclusion_proof", "transparency.inclusion_proof").map((h, i) => decodeFixed(h, `transparency.inclusion_proof[${i}]`, 32));
768
+ const root = decodeFixed(sth.root_hash, "signed_tree_head.root_hash", 32);
601
769
  // `?? 0` is not a convenience: encoding/json leaves 0 in Go's uint64 fields
602
770
  // for an explicit null and for an absent key rather than failing, so refusing
603
771
  // either would reject documents the reference accepts — the same class of
@@ -679,47 +847,89 @@ function canonicalStringify(val) {
679
847
  }
680
848
  return JSON.stringify(val);
681
849
  }
682
- // ---------------------------------------------------------------------------
683
- // Byte encoding helpers
684
- // ---------------------------------------------------------------------------
685
- /** Convert a byte field to lowercase hex. Handles:
686
- * - hex string (from hand-crafted test data)
687
- * - base64 string (from Go's json.Marshal of []byte slices)
688
- * - number[] (from Go's json.Marshal of [N]byte fixed arrays)
689
- */
690
- function toHex(value) {
691
- if (Array.isArray(value)) {
692
- return bytesToHex(new Uint8Array(value));
850
+ function requireObject(value, path) {
851
+ if (value === undefined || value === null)
852
+ throw new errors_js_1.VerificationError(`${path} is missing`);
853
+ if (typeof value !== "object" || Array.isArray(value)) {
854
+ throw new errors_js_1.VerificationError(`${path} is not an object`);
693
855
  }
694
- const str = value;
695
- if (/^[0-9a-fA-F]+$/.test(str) && str.length % 2 === 0) {
696
- return str.toLowerCase();
697
- }
698
- return bytesToHex(base64ToBytes(str));
856
+ return value;
857
+ }
858
+ function objectAt(parent, key, path) {
859
+ return requireObject(parent[key], path);
860
+ }
861
+ function stringAt(parent, key, path) {
862
+ const value = parent[key];
863
+ if (value === undefined || value === null)
864
+ throw new errors_js_1.VerificationError(`${path} is missing`);
865
+ if (typeof value !== "string")
866
+ throw new errors_js_1.VerificationError(`${path} is not a string`);
867
+ return value;
868
+ }
869
+ function optionalStringAt(parent, key, path) {
870
+ const value = parent[key];
871
+ if (value === undefined || value === null)
872
+ return undefined;
873
+ if (typeof value !== "string")
874
+ throw new errors_js_1.VerificationError(`${path} is not a string`);
875
+ return value;
699
876
  }
700
- /** Decode a signature field to raw bytes. Same format handling as toHex. */
701
- function decodeSignature(value) {
877
+ /**
878
+ * An array field. Absent and null read as empty: Go leaves a nil slice.
879
+ *
880
+ * Returned dense. JSON cannot express a hole, but a caller building the object
881
+ * in JavaScript can, and `.map` skips holes — so one reached the Merkle code as
882
+ * `undefined` and threw a TypeError. Array.from reads a hole as undefined,
883
+ * which the element's own reader then refuses as missing.
884
+ */
885
+ function arrayAt(parent, key, path) {
886
+ const value = parent[key];
887
+ if (value === undefined || value === null)
888
+ return [];
889
+ if (!Array.isArray(value))
890
+ throw new errors_js_1.VerificationError(`${path} is not an array`);
891
+ return Array.from(value);
892
+ }
893
+ const HEX_TEXT_RE = /^(?:[0-9a-fA-F]{2})*$/;
894
+ const BASE64_TEXT_RE = /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/;
895
+ /**
896
+ * Decode a fixed-size byte field — a hash, a key, a signature.
897
+ *
898
+ * Go marshals `[N]byte` as an array of numbers; hand-written documents carry
899
+ * hex or standard base64. A well-formed value of the right size is never
900
+ * ambiguous between the two text forms, because base64 of 32 or 64 bytes ends
901
+ * in padding hex cannot contain.
902
+ *
903
+ * Everything else is refused here, by name: a number, an array element that is
904
+ * not a byte, text that is neither encoding, the wrong length. Decoding it
905
+ * leniently instead wrapped 300 to 44 inside a Uint8Array, produced bytes no
906
+ * signature covers and blamed the signature — or threw from inside atob.
907
+ */
908
+ function decodeFixed(value, path, length) {
909
+ if (value === undefined || value === null)
910
+ throw new errors_js_1.VerificationError(`${path} is missing`);
911
+ let raw;
702
912
  if (Array.isArray(value)) {
703
- return new Uint8Array(value);
913
+ if (!value.every((b) => Number.isInteger(b) && b >= 0 && b <= 255)) {
914
+ throw new errors_js_1.VerificationError(`${path} is not an array of bytes`);
915
+ }
916
+ raw = new Uint8Array(value);
704
917
  }
705
- const str = value;
706
- if (/^[0-9a-fA-F]+$/.test(str)) {
707
- const raw = hexToBytes(str);
708
- if (raw.length === 64)
709
- return raw;
918
+ else if (typeof value === "string") {
919
+ if (HEX_TEXT_RE.test(value))
920
+ raw = hexToBytes(value);
921
+ else if (BASE64_TEXT_RE.test(value))
922
+ raw = base64ToBytes(value);
923
+ else
924
+ throw new errors_js_1.VerificationError(`${path} is neither hex nor base64`);
710
925
  }
711
- return base64ToBytes(str);
712
- }
713
- /** Decode a hash/bytes field to raw bytes. Same format handling as toHex. */
714
- function decodeBytes(value) {
715
- if (Array.isArray(value)) {
716
- return new Uint8Array(value);
926
+ else {
927
+ throw new errors_js_1.VerificationError(`${path} is not a byte string`);
717
928
  }
718
- const str = value;
719
- if (/^[0-9a-fA-F]+$/.test(str) && str.length % 2 === 0) {
720
- return hexToBytes(str);
929
+ if (raw.length !== length) {
930
+ throw new errors_js_1.VerificationError(`${path} is ${raw.length} bytes, expected ${length}`);
721
931
  }
722
- return base64ToBytes(str);
932
+ return raw;
723
933
  }
724
934
  /** Go's zero time.Time — what encoding/json leaves in a non-pointer time.Time
725
935
  * field for an explicit JSON null or an absent key. */
@@ -748,14 +958,17 @@ const RFC3339_RE = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(?:\.\d+)?(?
748
958
  * Exported from this module so the timestamp_vectors.json suite can diff it
749
959
  * against Go directly. It is deliberately not re-exported from index.ts, so it
750
960
  * is not part of the published package surface.
961
+ *
962
+ * `field` names the value in a refusal. It defaults to the word these messages
963
+ * always used, for callers normalizing a time that is not a field of a document.
751
964
  */
752
- function formatTimestamp(ts) {
965
+ function formatTimestamp(ts, field = "timestamp") {
753
966
  if (ts === null || ts === undefined)
754
967
  return ZERO_INSTANT;
755
968
  if (typeof ts !== "string") {
756
- throw new errors_js_1.VerificationError(`timestamp is not a string: ${JSON.stringify(ts)}`);
969
+ throw new errors_js_1.VerificationError(`${field} is not a string: ${JSON.stringify(ts)}`);
757
970
  }
758
- return formatEpochSeconds(parseRfc3339(ts));
971
+ return formatEpochSeconds(parseRfc3339(ts, field));
759
972
  }
760
973
  /** Parse a strict RFC 3339 timestamp to seconds since 1970-01-01T00:00:00Z.
761
974
  *
@@ -766,20 +979,20 @@ function formatTimestamp(ts) {
766
979
  * `+24:00` and `+00:60` parse, while `+25:00` and `+00:61` do not. Sub-second
767
980
  * digits are dropped, matching Format's truncation.
768
981
  */
769
- function parseRfc3339(ts) {
982
+ function parseRfc3339(ts, field = "timestamp") {
770
983
  const m = RFC3339_RE.exec(ts);
771
984
  if (m === null)
772
- throw new errors_js_1.VerificationError(`timestamp is not RFC 3339: ${ts}`);
985
+ throw new errors_js_1.VerificationError(`${field} is not RFC 3339: ${ts}`);
773
986
  const [year, month, day, hour, minute, second] = m
774
987
  .slice(1, 7)
775
988
  .map((v) => Number.parseInt(v, 10));
776
989
  if (month < 1 || month > 12)
777
- throw new errors_js_1.VerificationError(`timestamp month out of range: ${ts}`);
990
+ throw new errors_js_1.VerificationError(`${field} month out of range: ${ts}`);
778
991
  if (day < 1 || day > daysInMonth(year, month)) {
779
- throw new errors_js_1.VerificationError(`timestamp day out of range: ${ts}`);
992
+ throw new errors_js_1.VerificationError(`${field} day out of range: ${ts}`);
780
993
  }
781
994
  if (hour > 23 || minute > 59 || second > 59) {
782
- throw new errors_js_1.VerificationError(`timestamp time of day out of range: ${ts}`);
995
+ throw new errors_js_1.VerificationError(`${field} time of day out of range: ${ts}`);
783
996
  }
784
997
  let offset = 0;
785
998
  const sign = m[7];
@@ -787,9 +1000,9 @@ function parseRfc3339(ts) {
787
1000
  const offHour = Number.parseInt(m[8], 10);
788
1001
  const offMin = Number.parseInt(m[9], 10);
789
1002
  if (offHour > 24)
790
- throw new errors_js_1.VerificationError(`timestamp zone offset hour out of range: ${ts}`);
1003
+ throw new errors_js_1.VerificationError(`${field} zone offset hour out of range: ${ts}`);
791
1004
  if (offMin > 60)
792
- throw new errors_js_1.VerificationError(`timestamp zone offset minute out of range: ${ts}`);
1005
+ throw new errors_js_1.VerificationError(`${field} zone offset minute out of range: ${ts}`);
793
1006
  offset = (offHour * 3600 + offMin * 60) * (sign === "-" ? -1 : 1);
794
1007
  }
795
1008
  return daysFromCivil(year, month, day) * 86400 + hour * 3600 + minute * 60 + second - offset;
@@ -873,17 +1086,17 @@ function buildAttestationPayload(subject, att, certFormatVersion) {
873
1086
  // payload and call every genuine v8 record a forgery.
874
1087
  const coversMeasured = signatureCoversMeasuredTransport(certFormatVersion);
875
1088
  const coversRecoverable = signatureCoversRecoverableState(certFormatVersion);
876
- const systems = att.systems.map((s) => {
1089
+ const systems = att.systems.map((s, i) => {
877
1090
  const sys = {
878
1091
  canonical_version: s.canonical_version ?? null,
879
1092
  connector_type: s.connector_type,
880
1093
  hash_scope: s.hash_scope,
881
- merkle_root: s.merkle_root
882
- ? toHex(s.merkle_root)
883
- : null,
1094
+ merkle_root: s.merkle_root == null
1095
+ ? null
1096
+ : bytesToHex(decodeFixed(s.merkle_root, `systems[${i}].merkle_root`, 32)),
884
1097
  // The system's own observation time, not the envelope's.
885
- observed_at: formatTimestamp(s.observed_at),
886
- query_hash: toHex(s.query_hash),
1098
+ observed_at: formatTimestamp(s.observed_at, `systems[${i}].attested_at`),
1099
+ query_hash: bytesToHex(decodeFixed(s.query_hash, `systems[${i}].query_hash`, 32)),
887
1100
  record_count: s.record_count,
888
1101
  system_id: s.system_id,
889
1102
  system_name: s.system_name,
@@ -901,7 +1114,7 @@ function buildAttestationPayload(subject, att, certFormatVersion) {
901
1114
  attested_at: attestedAt,
902
1115
  payload_type: attestationPayloadType(certFormatVersion),
903
1116
  proof_mode: att.proof_mode,
904
- subject_hash: toHex(subject.identifier_hash),
1117
+ subject_hash: bytesToHex(decodeFixed(subject.identifier_hash, "subject.identifier_hash", 32)),
905
1118
  systems,
906
1119
  };
907
1120
  return canonicalJson(payload);
@@ -967,9 +1180,9 @@ function attestationPayloadType(version) {
967
1180
  return PAYLOAD_TYPE_ATTESTATION;
968
1181
  }
969
1182
  function buildCertificatePayload(cert) {
970
- const att = cert.attestation;
971
- const issuer = cert.issuer;
972
- const subject = cert.subject;
1183
+ const att = objectAt(cert, "attestation", "attestation");
1184
+ const issuer = objectAt(cert, "issuer", "issuer");
1185
+ const subject = objectAt(cert, "subject", "subject");
973
1186
  // One list, keyed by system_id (ADR-016 §2). v2 signed an attestation list
974
1187
  // and a verification list joined only on the human-editable system_name,
975
1188
  // which made a partial deletion indistinguishable from a complete one.
@@ -977,19 +1190,25 @@ function buildCertificatePayload(cert) {
977
1190
  const coversMeasured = signatureCoversMeasuredTransport(version);
978
1191
  const coversRecoverable = signatureCoversRecoverableState(version);
979
1192
  const coversAuthorization = signatureCoversAuthorization(version);
980
- const systems = (cert.systems ?? []).map((s) => {
1193
+ const systems = arrayAt(cert, "systems", "systems").map((entry, i) => {
1194
+ const where = `systems[${i}]`;
1195
+ const s = requireObject(entry, where);
981
1196
  const sys = {
982
- attested_at: formatTimestamp(s.attested_at),
983
- attested_count: s.attested_count,
984
- canonical_version: s.canonical_version ?? null,
985
- connector_type: s.connector_type,
986
- hash_scope: s.hash_scope,
987
- merkle_root: s.merkle_root ? toHex(s.merkle_root) : null,
988
- query_hash: toHex(s.query_hash),
989
- system_id: s.system_id,
990
- system_name: s.system_name,
991
- verified_at: formatTimestamp(s.verified_at),
992
- verified_count: s.verified_count,
1197
+ attested_at: formatTimestamp(s.attested_at, `${where}.attested_at`),
1198
+ attested_count: requireUint(s.attested_count, `${where}.attested_count`),
1199
+ canonical_version: optionalStringAt(s, "canonical_version", `${where}.canonical_version`) ?? null,
1200
+ connector_type: stringAt(s, "connector_type", `${where}.connector_type`),
1201
+ hash_scope: stringAt(s, "hash_scope", `${where}.hash_scope`),
1202
+ // Absent is a system with no Merkle root; anything present must be one.
1203
+ // Go's *[32]byte is nil or 32 bytes, never "" or [].
1204
+ merkle_root: s.merkle_root == null
1205
+ ? null
1206
+ : bytesToHex(decodeFixed(s.merkle_root, `${where}.merkle_root`, 32)),
1207
+ query_hash: bytesToHex(decodeFixed(s.query_hash, `${where}.query_hash`, 32)),
1208
+ system_id: stringAt(s, "system_id", `${where}.system_id`),
1209
+ system_name: stringAt(s, "system_name", `${where}.system_name`),
1210
+ verified_at: formatTimestamp(s.verified_at, `${where}.verified_at`),
1211
+ verified_count: requireUint(s.verified_count, `${where}.verified_count`),
993
1212
  };
994
1213
  if (coversMeasured) {
995
1214
  sys.read_only_enforcement = requireMeasured(s.read_only_enforcement, "read_only_enforcement", s.system_name, version);
@@ -1009,9 +1228,9 @@ function buildCertificatePayload(cert) {
1009
1228
  // The attestation block carries no system list of its own: the merged list
1010
1229
  // above is a superset of it. verification_signature is gone entirely.
1011
1230
  const attObj = {
1012
- attestation_signature: toHex(att.attestation_signature),
1013
- attested_at: formatTimestamp(att.attested_at),
1014
- proof_mode: att.proof_mode,
1231
+ attestation_signature: bytesToHex(decodeFixed(att.attestation_signature, "attestation.attestation_signature", 64)),
1232
+ attested_at: formatTimestamp(att.attested_at, "attestation.attested_at"),
1233
+ proof_mode: stringAt(att, "proof_mode", "attestation.proof_mode"),
1015
1234
  };
1016
1235
  // Fields added after v3 are gated on the certificate's OWN version. A v3
1017
1236
  // certificate must reconstruct to the same bytes forever; reading these
@@ -1023,9 +1242,9 @@ function buildCertificatePayload(cert) {
1023
1242
  const isV5Plus = formatAtLeast(version, FORMAT_VERSION_V5);
1024
1243
  const isV4Plus = formatAtLeast(version, FORMAT_VERSION_V4);
1025
1244
  const issuerObj = {
1026
- key_id: issuer.key_id,
1027
- name: issuer.name,
1028
- public_key: toHex(issuer.public_key),
1245
+ key_id: stringAt(issuer, "key_id", "issuer.key_id"),
1246
+ name: stringAt(issuer, "name", "issuer.name"),
1247
+ public_key: bytesToHex(decodeFixed(issuer.public_key, "issuer.public_key", 32)),
1029
1248
  };
1030
1249
  // v7 signs the algorithm, and does so unconditionally: unlike legal_entity
1031
1250
  // and enclave_pcr0, "which scheme signed this" is never unknown to a signer,
@@ -1033,43 +1252,48 @@ function buildCertificatePayload(cert) {
1033
1252
  if (signatureCoversAlgorithm(version)) {
1034
1253
  issuerObj.algorithm = requireMeasured(issuer.algorithm, "issuer.algorithm", "issuer", version);
1035
1254
  }
1036
- if (isV4Plus && typeof issuer.legal_entity === "string" && issuer.legal_entity !== "") {
1037
- issuerObj.legal_entity = issuer.legal_entity;
1255
+ // A non-string here used to be dropped silently, so a junk value added to a
1256
+ // record that never carried one still verified. Python refused it; so does
1257
+ // Go, at parse. Now this does too.
1258
+ if (isV4Plus) {
1259
+ const legalEntity = optionalStringAt(issuer, "legal_entity", "issuer.legal_entity");
1260
+ if (legalEntity)
1261
+ issuerObj.legal_entity = legalEntity;
1038
1262
  }
1039
1263
  // Signed from v5, and omitted when absent or empty: a build with no
1040
1264
  // measurement signs none, and "" is a value a reader could mistake for one.
1041
- if (signatureCoversEnclavePcr0(version) &&
1042
- typeof issuer.enclave_pcr0 === "string" &&
1043
- issuer.enclave_pcr0 !== "") {
1044
- issuerObj.enclave_pcr0 = issuer.enclave_pcr0;
1265
+ if (signatureCoversEnclavePcr0(version)) {
1266
+ const pcr0 = optionalStringAt(issuer, "enclave_pcr0", "issuer.enclave_pcr0");
1267
+ if (pcr0)
1268
+ issuerObj.enclave_pcr0 = pcr0;
1045
1269
  }
1046
1270
  // v5 drops identifier_type_hint: signed, but always the constant "custom",
1047
1271
  // so it was never evidence. v3 and v4 keep it or they stop verifying.
1048
1272
  const subjectObj = {
1049
- identifier_hash: toHex(subject.identifier_hash),
1273
+ identifier_hash: bytesToHex(decodeFixed(subject.identifier_hash, "subject.identifier_hash", 32)),
1050
1274
  };
1051
1275
  if (!isV5Plus) {
1052
- subjectObj.identifier_type_hint = subject.identifier_type_hint;
1276
+ subjectObj.identifier_type_hint = stringAt(subject, "identifier_type_hint", "subject.identifier_type_hint");
1053
1277
  }
1054
1278
  // status and revocation are deliberately absent (ADR-016 §3): a signature
1055
1279
  // commits to bytes at an instant, revocation is discovered later, so it
1056
1280
  // travels as a separate short-lived signed status statement.
1057
1281
  const payload = {
1058
1282
  attestation: attObj,
1059
- attestation_id: cert.attestation_id,
1060
- certificate_format_version: cert.certificate_format_version,
1061
- certificate_id: cert.certificate_id,
1062
- issued_at: formatTimestamp(cert.issued_at),
1283
+ attestation_id: stringAt(cert, "attestation_id", "attestation_id"),
1284
+ certificate_format_version: version,
1285
+ certificate_id: stringAt(cert, "certificate_id", "certificate_id"),
1286
+ issued_at: formatTimestamp(cert.issued_at, "issued_at"),
1063
1287
  issuer: issuerObj,
1064
1288
  payload_type: certificatePayloadType(version),
1065
1289
  subject: subjectObj,
1066
1290
  systems,
1067
1291
  };
1068
1292
  if (isV4Plus && cert.scope != null) {
1069
- const scope = cert.scope;
1293
+ const scope = objectAt(cert, "scope", "scope");
1070
1294
  payload.scope = {
1071
- text_sha256: toHex(scope.text_sha256),
1072
- version: scope.version,
1295
+ text_sha256: bytesToHex(decodeFixed(scope.text_sha256, "scope.text_sha256", 32)),
1296
+ version: stringAt(scope, "version", "scope.version"),
1073
1297
  };
1074
1298
  }
1075
1299
  return canonicalJson(payload);
@@ -1080,7 +1304,7 @@ function buildLogLeafPayload(entryType, certificateId, certificateHash, appended
1080
1304
  throw new errors_js_1.VerificationError(`invalid log entry_type: ${String(entryType)}`);
1081
1305
  }
1082
1306
  const payload = {
1083
- appended_at: formatTimestamp(appendedAt),
1307
+ appended_at: formatTimestamp(appendedAt, "transparency.appended_at"),
1084
1308
  certificate_hash: bytesToHex(certificateHash),
1085
1309
  certificate_id: certificateId,
1086
1310
  entry_type: entryType,
@@ -1129,13 +1353,13 @@ function attestationSystems(cert) {
1129
1353
  * between two modules of this package, not published surface.
1130
1354
  */
1131
1355
  function buildTreeHeadPayload(head) {
1132
- const logId = head.log_id;
1133
- const named = typeof logId === "string" && logId !== "";
1356
+ const logId = optionalStringAt(head, "log_id", "signed_tree_head.log_id");
1357
+ const named = logId !== undefined && logId !== "";
1134
1358
  const payload = {
1135
1359
  payload_type: named ? PAYLOAD_TYPE_TREE_HEAD_V7 : PAYLOAD_TYPE_TREE_HEAD,
1136
- root_hash: toHex(head.root_hash),
1137
- timestamp: formatTimestamp(head.timestamp),
1138
- tree_size: head.tree_size,
1360
+ root_hash: bytesToHex(decodeFixed(head.root_hash, "signed_tree_head.root_hash", 32)),
1361
+ timestamp: formatTimestamp(head.timestamp, "signed_tree_head.timestamp"),
1362
+ tree_size: requireUint(head.tree_size ?? 0, "signed_tree_head.tree_size"),
1139
1363
  };
1140
1364
  if (named)
1141
1365
  payload.log_id = logId;
@@ -1275,21 +1499,21 @@ async function verifyConsistency(crypto, oldSize, newSize, oldRoot, newRoot, pro
1275
1499
  function buildCertificateStatusPayload(stmt) {
1276
1500
  const payload = {
1277
1501
  payload_type: PAYLOAD_TYPE_CERTIFICATE_STATUS,
1278
- certificate_id: stmt.certificate_id,
1279
- statement_expires_at: formatTimestamp(stmt.statement_expires_at),
1280
- statement_issued_at: formatTimestamp(stmt.statement_issued_at),
1281
- status: stmt.status,
1282
- sth_root_hash: toHex(stmt.sth_root_hash),
1283
- sth_tree_size: stmt.sth_tree_size,
1502
+ certificate_id: stringAt(stmt, "certificate_id", "status.certificate_id"),
1503
+ statement_expires_at: formatTimestamp(stmt.statement_expires_at, "status.statement_expires_at"),
1504
+ statement_issued_at: formatTimestamp(stmt.statement_issued_at, "status.statement_issued_at"),
1505
+ status: stringAt(stmt, "status", "status.status"),
1506
+ sth_root_hash: bytesToHex(decodeFixed(stmt.sth_root_hash, "status.sth_root_hash", 32)),
1507
+ sth_tree_size: requireUint(stmt.sth_tree_size ?? 0, "status.sth_tree_size"),
1284
1508
  };
1285
- if (stmt.replacement_certificate_id != null) {
1286
- payload.replacement_certificate_id = stmt.replacement_certificate_id;
1287
- }
1509
+ const replacement = optionalStringAt(stmt, "replacement_certificate_id", "status.replacement_certificate_id");
1510
+ if (replacement !== undefined)
1511
+ payload.replacement_certificate_id = replacement;
1288
1512
  if (stmt.revocation_log_index != null) {
1289
- payload.revocation_log_index = stmt.revocation_log_index;
1513
+ payload.revocation_log_index = requireUint(stmt.revocation_log_index, "status.revocation_log_index");
1290
1514
  }
1291
1515
  if (stmt.revoked_at != null) {
1292
- payload.revoked_at = formatTimestamp(stmt.revoked_at);
1516
+ payload.revoked_at = formatTimestamp(stmt.revoked_at, "status.revoked_at");
1293
1517
  }
1294
1518
  return canonicalJson(payload);
1295
1519
  }
@@ -1324,7 +1548,7 @@ function buildKeyListPayload(doc) {
1324
1548
  keys: entries,
1325
1549
  statement_expires_at: formatTimestamp(doc.statement_expires_at),
1326
1550
  statement_issued_at: formatTimestamp(doc.statement_issued_at),
1327
- sth_root_hash: toHex(doc.sth_root_hash),
1551
+ sth_root_hash: bytesToHex(decodeFixed(doc.sth_root_hash, "sth_root_hash", 32)),
1328
1552
  sth_tree_size: doc.sth_tree_size,
1329
1553
  };
1330
1554
  return canonicalJson(payload);
@@ -1350,18 +1574,38 @@ exports.VALID_REVOCATION_UNKNOWN = "VALID_REVOCATION_UNKNOWN";
1350
1574
  *
1351
1575
  * The third row is the point: `verifyCertificate` answers VALID there, which
1352
1576
  * reads as "not revoked" and is not something it checked.
1577
+ *
1578
+ * `options` is passed to {@link verifyCertificate} unchanged.
1353
1579
  */
1354
- async function verifyCertificateWithStatus(crypto, certificate, publicKeys, status, now) {
1580
+ async function verifyCertificateWithStatus(crypto, certificate, publicKeys, status, now, options) {
1355
1581
  // A refusal throws out of verifyCertificate, so `base` is VALID or one of the
1356
1582
  // two soft verdicts. Returning a soft verdict here skipped every check below
1357
1583
  // it: a revoked certificate signed by a compromised-later key answered
1358
1584
  // VALID_KEY_COMPROMISED_LATER and its statement was never authenticated. The
1359
1585
  // verdict is held instead, and resolved against what the statement says.
1360
- const base = await verifyCertificate(crypto, certificate, publicKeys);
1586
+ const base = await verifyCertificate(crypto, certificate, publicKeys, options);
1361
1587
  if (status == null) {
1362
1588
  return resolveWithBase(base, exports.VALID_REVOCATION_UNKNOWN);
1363
1589
  }
1364
- const keyId = status.key_id;
1590
+ return resolveWithBase(base, await verifyStatusStatement(crypto, status, publicKeys, certificate.certificate_id, now));
1591
+ }
1592
+ /**
1593
+ * The statement half of {@link verifyCertificateWithStatus}: authenticate a
1594
+ * status statement about `certificateId` against the published keys, and read
1595
+ * it at `now`. The /verify page's QR lookup has a record id and no record, and
1596
+ * uses this so it shows what a signed, fresh statement says, never what the
1597
+ * status endpoint claims.
1598
+ *
1599
+ * | input | result |
1600
+ * |---|---|
1601
+ * | fresh, REVOKED | throws VerificationError, code CERTIFICATE_REVOKED |
1602
+ * | fresh, ACTIVE | the statement key's verdict: `"VALID"`, or a soft one |
1603
+ * | stale, or a status this SDK does not know | `"VALID_REVOCATION_UNKNOWN"` |
1604
+ * | unknown or unusable key, bad signature, another certificate | throws VerificationError |
1605
+ */
1606
+ async function verifyStatusStatement(crypto, status, publicKeys, certificateId, now) {
1607
+ requireObject(status, "status");
1608
+ const keyId = optionalStringAt(status, "key_id", "status.key_id");
1365
1609
  const info = keyId ? publicKeys.get(keyId) : undefined;
1366
1610
  if (!info) {
1367
1611
  throw new errors_js_1.VerificationError(`status statement signed by unknown key: ${keyId}`);
@@ -1382,10 +1626,10 @@ async function verifyCertificateWithStatus(crypto, certificate, publicKeys, stat
1382
1626
  const at = formatTimestamp(when.toISOString());
1383
1627
  const statusKeyVerdict = requireUsableKey(info, parseRfc3339(at), keyId, "status statement key");
1384
1628
  const payload = buildCertificateStatusPayload(status);
1385
- // decodeSignature, not hexToBytes: signatures arrive as hex, base64 or a byte
1629
+ // decodeFixed, not hexToBytes: signatures arrive as hex, base64 or a byte
1386
1630
  // array depending on the producer, and every other signature on this path
1387
1631
  // goes through the same decoder.
1388
- const sig = decodeSignature(status.signature);
1632
+ const sig = decodeFixed(status.signature, "status.signature", 64);
1389
1633
  if (!(await crypto.ed25519Verify(info.keyBytes, payload, sig))) {
1390
1634
  throw new errors_js_1.VerificationError("status statement signature is invalid");
1391
1635
  }
@@ -1402,8 +1646,7 @@ async function verifyCertificateWithStatus(crypto, certificate, publicKeys, stat
1402
1646
  // Every signature still verified; the binding was the only forged part.
1403
1647
  // core/verify.go never had the fallback, so Go said STATUS_STATEMENT_MISMATCH
1404
1648
  // while both SDKs and the browser bundle said VALID.
1405
- const certId = certificate.certificate_id;
1406
- if (status.certificate_id !== certId) {
1649
+ if (status.certificate_id !== certificateId) {
1407
1650
  throw new errors_js_1.VerificationError("status statement is about a different certificate");
1408
1651
  }
1409
1652
  // Compared in the normalized form the payload signs, so the freshness check
@@ -1413,7 +1656,7 @@ async function verifyCertificateWithStatus(crypto, certificate, publicKeys, stat
1413
1656
  if (at < issued || at >= expires) {
1414
1657
  // Stale is not a weaker answer, it is no answer — including for a REVOKED
1415
1658
  // statement, which must never decay into VALID.
1416
- return resolveWithBase(base, exports.VALID_REVOCATION_UNKNOWN);
1659
+ return exports.VALID_REVOCATION_UNKNOWN;
1417
1660
  }
1418
1661
  if (status.status === "REVOKED") {
1419
1662
  // Coded, because this is the only status-path rejection that is a fact
@@ -1428,12 +1671,12 @@ async function verifyCertificateWithStatus(crypto, certificate, publicKeys, stat
1428
1671
  // case — was reported as revocation CHECKED AND PASSED. Mirrors
1429
1672
  // core.ValidCertificateStatusValue.
1430
1673
  if (!KNOWN_STATUS_VALUES.has(status.status)) {
1431
- return resolveWithBase(base, exports.VALID_REVOCATION_UNKNOWN);
1674
+ return exports.VALID_REVOCATION_UNKNOWN;
1432
1675
  }
1433
1676
  // A soft verdict on the status key is the result, not a footnote — but it is
1434
1677
  // reported only once the statement has authenticated and been read, so it can
1435
1678
  // neither speak for an unverified statement nor suppress a revocation.
1436
- return resolveWithBase(base, statusKeyVerdict);
1679
+ return statusKeyVerdict;
1437
1680
  }
1438
1681
  /**
1439
1682
  * Picks what to report when the certificate's own key returned a soft verdict