burnledger 0.4.0 → 0.6.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 (104) hide show
  1. package/README.md +11 -4
  2. package/dist/cjs/anchor.d.ts +71 -0
  3. package/dist/cjs/anchor.d.ts.map +1 -0
  4. package/dist/cjs/anchor.js +279 -0
  5. package/dist/cjs/anchor.js.map +1 -0
  6. package/dist/cjs/client.d.ts +26 -2
  7. package/dist/cjs/client.d.ts.map +1 -1
  8. package/dist/cjs/client.js +27 -5
  9. package/dist/cjs/client.js.map +1 -1
  10. package/dist/cjs/enclave-seal.d.ts +101 -0
  11. package/dist/cjs/enclave-seal.d.ts.map +1 -0
  12. package/dist/cjs/enclave-seal.js +479 -0
  13. package/dist/cjs/enclave-seal.js.map +1 -0
  14. package/dist/cjs/errors.d.ts +42 -1
  15. package/dist/cjs/errors.d.ts.map +1 -1
  16. package/dist/cjs/errors.js +44 -2
  17. package/dist/cjs/errors.js.map +1 -1
  18. package/dist/cjs/http.d.ts +21 -1
  19. package/dist/cjs/http.d.ts.map +1 -1
  20. package/dist/cjs/http.js +76 -12
  21. package/dist/cjs/http.js.map +1 -1
  22. package/dist/cjs/index.browser.d.ts +2 -4
  23. package/dist/cjs/index.browser.d.ts.map +1 -1
  24. package/dist/cjs/index.browser.js.map +1 -1
  25. package/dist/cjs/index.d.ts +16 -11
  26. package/dist/cjs/index.d.ts.map +1 -1
  27. package/dist/cjs/index.js +20 -1
  28. package/dist/cjs/index.js.map +1 -1
  29. package/dist/cjs/keys.d.ts +66 -0
  30. package/dist/cjs/keys.d.ts.map +1 -1
  31. package/dist/cjs/keys.js +128 -3
  32. package/dist/cjs/keys.js.map +1 -1
  33. package/dist/cjs/models.d.ts +41 -3
  34. package/dist/cjs/models.d.ts.map +1 -1
  35. package/dist/cjs/models.js +25 -4
  36. package/dist/cjs/models.js.map +1 -1
  37. package/dist/cjs/verify.d.ts +72 -6
  38. package/dist/cjs/verify.d.ts.map +1 -1
  39. package/dist/cjs/verify.js +392 -62
  40. package/dist/cjs/verify.js.map +1 -1
  41. package/dist/cjs/web-verifier.d.ts +54 -5
  42. package/dist/cjs/web-verifier.d.ts.map +1 -1
  43. package/dist/cjs/web-verifier.js +36 -3
  44. package/dist/cjs/web-verifier.js.map +1 -1
  45. package/dist/esm/anchor.d.ts +71 -0
  46. package/dist/esm/anchor.d.ts.map +1 -0
  47. package/dist/esm/anchor.js +275 -0
  48. package/dist/esm/anchor.js.map +1 -0
  49. package/dist/esm/cli.d.ts +45 -14
  50. package/dist/esm/cli.d.ts.map +1 -1
  51. package/dist/esm/cli.js +357 -50
  52. package/dist/esm/cli.js.map +1 -1
  53. package/dist/esm/client.d.ts +26 -2
  54. package/dist/esm/client.d.ts.map +1 -1
  55. package/dist/esm/client.js +27 -5
  56. package/dist/esm/client.js.map +1 -1
  57. package/dist/esm/enclave-seal.d.ts +101 -0
  58. package/dist/esm/enclave-seal.d.ts.map +1 -0
  59. package/dist/esm/enclave-seal.js +472 -0
  60. package/dist/esm/enclave-seal.js.map +1 -0
  61. package/dist/esm/errors.d.ts +42 -1
  62. package/dist/esm/errors.d.ts.map +1 -1
  63. package/dist/esm/errors.js +43 -1
  64. package/dist/esm/errors.js.map +1 -1
  65. package/dist/esm/http.d.ts +21 -1
  66. package/dist/esm/http.d.ts.map +1 -1
  67. package/dist/esm/http.js +75 -12
  68. package/dist/esm/http.js.map +1 -1
  69. package/dist/esm/index.browser.d.ts +2 -4
  70. package/dist/esm/index.browser.d.ts.map +1 -1
  71. package/dist/esm/index.browser.js.map +1 -1
  72. package/dist/esm/index.d.ts +16 -11
  73. package/dist/esm/index.d.ts.map +1 -1
  74. package/dist/esm/index.js +11 -2
  75. package/dist/esm/index.js.map +1 -1
  76. package/dist/esm/keys.d.ts +66 -0
  77. package/dist/esm/keys.d.ts.map +1 -1
  78. package/dist/esm/keys.js +125 -3
  79. package/dist/esm/keys.js.map +1 -1
  80. package/dist/esm/models.d.ts +41 -3
  81. package/dist/esm/models.d.ts.map +1 -1
  82. package/dist/esm/models.js +24 -4
  83. package/dist/esm/models.js.map +1 -1
  84. package/dist/esm/verify.d.ts +72 -6
  85. package/dist/esm/verify.d.ts.map +1 -1
  86. package/dist/esm/verify.js +390 -63
  87. package/dist/esm/verify.js.map +1 -1
  88. package/dist/esm/web-verifier.d.ts +54 -5
  89. package/dist/esm/web-verifier.d.ts.map +1 -1
  90. package/dist/esm/web-verifier.js +40 -3
  91. package/dist/esm/web-verifier.js.map +1 -1
  92. package/package.json +1 -1
  93. package/src/anchor.ts +330 -0
  94. package/src/cli.ts +389 -54
  95. package/src/client.ts +46 -4
  96. package/src/enclave-seal.ts +582 -0
  97. package/src/errors.ts +47 -2
  98. package/src/http.ts +79 -12
  99. package/src/index.browser.ts +2 -2
  100. package/src/index.ts +28 -9
  101. package/src/keys.ts +176 -3
  102. package/src/models.ts +72 -7
  103. package/src/verify.ts +467 -72
  104. package/src/web-verifier.ts +58 -5
@@ -17,18 +17,21 @@
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 = void 0;
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;
21
21
  exports.hexToBytes = hexToBytes;
22
22
  exports.bytesToHex = bytesToHex;
23
23
  exports.publicKeyFromHex = publicKeyFromHex;
24
+ exports.keyIsUsable = keyIsUsable;
24
25
  exports.evaluateKey = evaluateKey;
25
26
  exports.certificateAnchor = certificateAnchor;
26
27
  exports.verifyCertificate = verifyCertificate;
27
28
  exports.verifyTransparency = verifyTransparency;
28
29
  exports.issuanceBytes = issuanceBytes;
29
30
  exports.formatTimestamp = formatTimestamp;
31
+ exports.buildTreeHeadPayload = buildTreeHeadPayload;
30
32
  exports.verifyConsistency = verifyConsistency;
31
33
  exports.buildCertificateStatusPayload = buildCertificateStatusPayload;
34
+ exports.buildKeyListPayload = buildKeyListPayload;
32
35
  exports.verifyCertificateWithStatus = verifyCertificateWithStatus;
33
36
  const errors_js_1 = require("./errors.js");
34
37
  // ---------------------------------------------------------------------------
@@ -49,11 +52,69 @@ const PAYLOAD_TYPE_ATTESTATION_V5 = "burnledger.attestation.v5";
49
52
  const PAYLOAD_TYPE_CERTIFICATE_V5 = "burnledger.certificate.v5";
50
53
  const FORMAT_VERSION_V5 = "5.0";
51
54
  const FORMAT_VERSION_V4 = "4.0";
55
+ // v6 renames the artifact and nothing else: the shape is byte-for-byte v5, and
56
+ // only the tag and the version string differ. It still takes its own tag, for
57
+ // the same reason v4 and v5 each took one -- a signature over a payload calling
58
+ // itself a verification record must not verify as one calling itself a
59
+ // certificate. Two names for one shape sharing a separator is exactly what a
60
+ // separator prevents.
61
+ const PAYLOAD_TYPE_ATTESTATION_V6 = "burnledger.attestation.v6";
62
+ const PAYLOAD_TYPE_VERIFICATION_RECORD_V6 = "burnledger.verification_record.v6";
63
+ const FORMAT_VERSION_V6 = "6.0";
64
+ // v7 changes the SHAPE, not just the name: every system entry gains
65
+ // read_only_enforcement and transport_security, and the issuer gains the
66
+ // algorithm identifier. Its tags move for the reason v4, v5 and v6 each moved
67
+ // theirs, and this time the bytes under them genuinely differ.
68
+ const PAYLOAD_TYPE_ATTESTATION_V7 = "burnledger.attestation.v7";
69
+ const PAYLOAD_TYPE_VERIFICATION_RECORD_V7 = "burnledger.verification_record.v7";
70
+ const FORMAT_VERSION_V7 = "7.0";
71
+ const FORMAT_VERSION_V3 = "3.0";
72
+ /**
73
+ * The only signature scheme this SDK can check. A v7 record names its own
74
+ * algorithm, so a record naming anything else must be refused with its own
75
+ * code rather than fed to ed25519Verify and reported as a bad signature.
76
+ */
77
+ const ALGORITHM_ED25519 = "ed25519";
78
+ /**
79
+ * Every format this build can read, oldest first. Mirrors core.FormatVersions()
80
+ * in the Go source; the cross-language conformance corpus carries a fixture per
81
+ * version, so a version present in one language and missing here fails there.
82
+ *
83
+ * Frozen because `readonly` is erased at build time: it stops a TypeScript
84
+ * caller from writing to the array, and does nothing at all to the JavaScript
85
+ * this compiles to — which is what the browser bundle actually runs, where any
86
+ * script on the page could otherwise push a version onto it.
87
+ */
88
+ exports.KNOWN_FORMAT_VERSIONS = Object.freeze([
89
+ FORMAT_VERSION_V3,
90
+ FORMAT_VERSION_V4,
91
+ FORMAT_VERSION_V5,
92
+ FORMAT_VERSION_V6,
93
+ FORMAT_VERSION_V7,
94
+ ]);
95
+ /**
96
+ * Returns the version, or throws if this SDK cannot read it.
97
+ *
98
+ * Rejects a non-string too: JSON carrying `6.0` as a NUMBER would compare
99
+ * unequal to every known version and silently take the oldest branch, which is
100
+ * the same trap as an unknown version wearing a different hat.
101
+ */
102
+ function checkFormatVersion(version) {
103
+ if (typeof version !== "string" || !exports.KNOWN_FORMAT_VERSIONS.includes(version)) {
104
+ throw new errors_js_1.VerificationError(`unsupported certificate_format_version ${JSON.stringify(version)}; ` +
105
+ `this version of the SDK reads ${exports.KNOWN_FORMAT_VERSIONS.join(", ")}. ` +
106
+ "The record may be genuine and simply newer than this library.", errors_js_1.UNSUPPORTED_FORMAT_VERSION);
107
+ }
108
+ return version;
109
+ }
52
110
  const PAYLOAD_TYPE_TREE_HEAD = "burnledger.sth.v3";
111
+ // The domain for a tree head that names its log. See buildTreeHeadPayload.
112
+ const PAYLOAD_TYPE_TREE_HEAD_V7 = "burnledger.sth.v7";
53
113
  const PAYLOAD_TYPE_LOG_LEAF = "burnledger.log_leaf.v3";
54
114
  // There is deliberately no verification payload type: v3 produces those facts
55
115
  // in the same enclave call that signs the certificate (ADR-016 §2).
56
116
  const PAYLOAD_TYPE_CERTIFICATE_STATUS = "burnledger.certificate_status.v3";
117
+ const PAYLOAD_TYPE_KEY_LIST = "burnledger.key_list.v3";
57
118
  function hexToBytes(hex) {
58
119
  const len = hex.length >>> 1;
59
120
  const out = new Uint8Array(len);
@@ -140,6 +201,29 @@ exports.KEY_OUTSIDE_VALIDITY = "KEY_OUTSIDE_VALIDITY";
140
201
  exports.KEY_COMPROMISED = "KEY_COMPROMISED";
141
202
  exports.VALID_KEY_COMPROMISED_LATER = "VALID_KEY_COMPROMISED_LATER";
142
203
  exports.VALID_KEY_WINDOW_UNKNOWN = "VALID_KEY_WINDOW_UNKNOWN";
204
+ /**
205
+ * The revocation states a statement may assert. Mirrors
206
+ * core.ValidCertificateStatusValue: anything else means the statement was read
207
+ * but not understood, which is not the same as "not revoked".
208
+ */
209
+ const KNOWN_STATUS_VALUES = new Set(["ACTIVE", "REVOKED"]);
210
+ /**
211
+ * The key verdicts that permit relying on a signature: VALID plus the two soft
212
+ * verdicts ADR-017 §2 says a reader must be SHOWN rather than refused. Anything
213
+ * not named here is refused, including a verdict added in a later release.
214
+ */
215
+ const USABLE_KEY_VERDICTS = new Set([
216
+ "VALID",
217
+ exports.VALID_KEY_WINDOW_UNKNOWN,
218
+ exports.VALID_KEY_COMPROMISED_LATER,
219
+ ]);
220
+ /** Whether a key verdict permits relying on a signature. Mirrors
221
+ * core.KeyIsUsable; exported so the key-list verifier (keys.ts) refuses the
222
+ * same set requireUsableKey refuses rather than keeping a second copy of it —
223
+ * a second copy is exactly how #684's fourth answer came to exist. */
224
+ function keyIsUsable(verdict) {
225
+ return USABLE_KEY_VERDICTS.has(verdict);
226
+ }
143
227
  /** Whether a key was authorized to sign at anchor time `t`.
144
228
  *
145
229
  * Returns `"VALID"` when the key clears. The anchor is seconds since the epoch
@@ -182,7 +266,17 @@ function evaluateKey(pki, anchor) {
182
266
  // for. Fail closed, as the boolean it replaces did.
183
267
  return "UNKNOWN_KEY";
184
268
  }
185
- /** The anchor a certificate supplies, or null when it supplies none. */
269
+ /**
270
+ * The anchor a certificate supplies, or null when it supplies none.
271
+ *
272
+ * READS AN UNSIGNED FIELD. `transparency` is stripped before signing, so on a
273
+ * certificate handed to a verifier the timestamp is holder-editable. Use
274
+ * {@link verifiedCertificateAnchor} anywhere the anchor decides whether a key
275
+ * may be relied on; this is safe only where the same timestamp is inside a
276
+ * payload the caller goes on to verify, which is what verifyTransparency does.
277
+ *
278
+ * Exported and unchanged because it is published API.
279
+ */
186
280
  function certificateAnchor(certificate) {
187
281
  const transparency = certificate.transparency;
188
282
  if (transparency == null)
@@ -197,35 +291,95 @@ function certificateAnchor(certificate) {
197
291
  return null;
198
292
  }
199
293
  }
294
+ /**
295
+ * The anchor, but only once the tree head carrying it has been checked against
296
+ * the key being evaluated.
297
+ *
298
+ * The bypass this closes: for a key declared compromised, evaluateKey answers
299
+ * KEY_COMPROMISED with no anchor and VALID_KEY_COMPROMISED_LATER when the
300
+ * anchor predates the compromise — and the second is a verdict requireUsableKey
301
+ * passes through as the RESULT of verifyCertificate. Since the timestamp is
302
+ * covered by no signature, a holder could backdate it and have a certificate
303
+ * signed with a compromised key reported as verified-with-a-caveat. Deleting
304
+ * `transparency` failed closed; keeping a doctored one did not.
305
+ *
306
+ * A head that does not verify yields no anchor, which is the same conservative
307
+ * answer as a certificate carrying no transparency block at all.
308
+ */
309
+ async function verifiedCertificateAnchor(crypto, certificate, pki) {
310
+ const anchor = certificateAnchor(certificate);
311
+ if (anchor === null)
312
+ return null;
313
+ const transparency = certificate.transparency;
314
+ const sth = transparency.signed_tree_head;
315
+ try {
316
+ const headPayload = buildTreeHeadPayload(sth);
317
+ const headSig = decodeSignature(sth.signature);
318
+ if (!(await crypto.ed25519Verify(pki.keyBytes, headPayload, headSig)))
319
+ return null;
320
+ }
321
+ catch {
322
+ return null;
323
+ }
324
+ return anchor;
325
+ }
200
326
  /** Throws for a key that cannot be used at all; returns the verdict otherwise.
201
327
  *
202
328
  * VALID_KEY_COMPROMISED_LATER and VALID_KEY_WINDOW_UNKNOWN are returned rather
203
329
  * than thrown because they are not refusals — they are results a caller must
204
330
  * show the reader instead of collapsing into green.
205
331
  */
206
- function requireUsableKey(pki, anchor, keyId) {
332
+ function requireUsableKey(pki, anchor, keyId,
333
+ // Named, because this is also the key that signs a certificate-status
334
+ // statement, and "issuer key is compromised" would send a reader looking at
335
+ // the wrong key.
336
+ role = "issuer key") {
207
337
  const verdict = evaluateKey(pki, anchor);
208
338
  if (verdict === exports.KEY_COMPROMISED) {
209
- throw new errors_js_1.VerificationError(`issuer key is compromised: ${keyId}`);
339
+ throw new errors_js_1.VerificationError(`${role} is compromised: ${keyId}`);
210
340
  }
211
341
  if (verdict === exports.KEY_OUTSIDE_VALIDITY) {
212
- throw new errors_js_1.VerificationError(`issuer key was not valid when it signed: ${keyId}`);
342
+ throw new errors_js_1.VerificationError(`${role} was not valid when it signed: ${keyId}`);
213
343
  }
214
344
  if (verdict === "UNKNOWN_KEY") {
215
- throw new errors_js_1.VerificationError(`issuer key has an unusable status "${pki.keyStatus}": ${keyId}`);
345
+ throw new errors_js_1.VerificationError(`${role} has an unusable status "${pki.keyStatus}": ${keyId}`);
346
+ }
347
+ // An ALLOW-list, matching core.KeyIsUsable, which was inverted away from
348
+ // exactly the shape above. Naming the refusals and returning everything else
349
+ // means a verdict added to evaluateKey later becomes a PASS by default — and
350
+ // the value returned here is propagated out of verifyCertificate as its
351
+ // result, so "everything else" is rendered to a reader as "verified, with a
352
+ // note". Go would refuse the same document. That is #684's three-verifiers-
353
+ // two-answers divergence, in the direction that calls a bad document good.
354
+ if (!USABLE_KEY_VERDICTS.has(verdict)) {
355
+ throw new errors_js_1.VerificationError(`${role} returned a verdict this SDK does not recognize (${verdict}): ${keyId}`);
216
356
  }
217
357
  return verdict;
218
358
  }
219
359
  /** Verify all signatures on a deletion certificate offline. */
220
360
  async function verifyCertificate(crypto, certificate, publicKeys) {
361
+ // Step 0: can this SDK read the format at all? Every check below rebuilds
362
+ // signed bytes from the version, so an unreadable version makes all of them
363
+ // meaningless - and "unknown issuer key" would be the wrong thing to tell
364
+ // someone holding a record that is merely newer than this library.
365
+ const formatVersion = checkFormatVersion(certificate.certificate_format_version);
221
366
  const issuer = certificate.issuer;
367
+ // Step 0b: can this SDK check the algorithm the record names? Asked before
368
+ // any signature, because verifying an Ed25519 signature over a record that
369
+ // says it was signed with something else answers a question nobody asked.
370
+ // Below v7 no record names one, so there is nothing to check.
371
+ if (formatVersion === FORMAT_VERSION_V7 && issuer.algorithm !== ALGORITHM_ED25519) {
372
+ throw new errors_js_1.VerificationError(`record names signature algorithm ${JSON.stringify(issuer.algorithm)}; ` +
373
+ `this version of the SDK verifies ${ALGORITHM_ED25519}. ` +
374
+ "The record may be genuine and simply signed with a scheme this library does not implement.", errors_js_1.UNSUPPORTED_ALGORITHM);
375
+ }
222
376
  const keyId = issuer.key_id;
223
377
  const pki = publicKeys.get(keyId);
224
378
  if (pki === undefined)
225
379
  throw new errors_js_1.VerificationError(`unknown issuer key: ${keyId}`);
226
380
  // The key check runs before any signature check: a signature verified
227
381
  // against a key nobody vouches for proves nothing about the issuer.
228
- const keyVerdict = requireUsableKey(pki, certificateAnchor(certificate), keyId);
382
+ const keyVerdict = requireUsableKey(pki, await verifiedCertificateAnchor(crypto, certificate, pki), keyId);
229
383
  // 1. Certificate signature FIRST — nothing below may trust a field until the
230
384
  // bytes carrying it are covered by a verified signature.
231
385
  const certPayload = buildCertificatePayload(certificate);
@@ -348,6 +502,17 @@ async function verifyTransparency(crypto, certificate, publicKeys) {
348
502
  throw new errors_js_1.VerificationError(`transparency.tree_size (${unsignedSize}) does not match the signed tree head ` +
349
503
  `(${treeSize}); it is not covered by any signature`);
350
504
  }
505
+ // Same rule for the log id (Q31). transparency.log_id sits beside log_url and
506
+ // is the field a reader looks at to answer "which log is this?", and nothing
507
+ // signs it — so a holder could relabel a genuine proof as belonging to a
508
+ // different log while every signature still checked out. A head predating log
509
+ // ids has neither side set and passes.
510
+ const signedLogId = sth.log_id ?? "";
511
+ const unsignedLogId = transparency.log_id ?? "";
512
+ if (unsignedLogId !== signedLogId) {
513
+ throw new errors_js_1.VerificationError(`transparency.log_id (${JSON.stringify(unsignedLogId)}) does not match the signed ` +
514
+ `tree head (${JSON.stringify(signedLogId)}); it is not covered by any signature`);
515
+ }
351
516
  if (!(await verifyInclusion(crypto, leaf, index, treeSize, proofHashes, root))) {
352
517
  throw new errors_js_1.VerificationError("merkle inclusion proof is invalid");
353
518
  }
@@ -582,31 +747,85 @@ function civilFromDays(days) {
582
747
  // ---------------------------------------------------------------------------
583
748
  function buildAttestationPayload(subject, att, certFormatVersion) {
584
749
  const attestedAt = formatTimestamp(att.attested_at);
585
- const systems = att.systems.map((s) => ({
586
- canonical_version: s.canonical_version ?? null,
587
- connector_type: s.connector_type,
588
- hash_scope: s.hash_scope,
589
- merkle_root: s.merkle_root
590
- ? toHex(s.merkle_root)
591
- : null,
592
- // The system's own observation time, not the envelope's.
593
- observed_at: formatTimestamp(s.observed_at),
594
- query_hash: toHex(s.query_hash),
595
- record_count: s.record_count,
596
- system_id: s.system_id,
597
- system_name: s.system_name,
598
- }));
750
+ // v7 signs two per-system measurements v6 leaves on a mutable row. Gated on
751
+ // the record's OWN version: emitting them for an older format would rebuild
752
+ // bytes no signer ever produced and reject every record already issued.
753
+ const isV7 = certFormatVersion === FORMAT_VERSION_V7;
754
+ const systems = att.systems.map((s) => {
755
+ const sys = {
756
+ canonical_version: s.canonical_version ?? null,
757
+ connector_type: s.connector_type,
758
+ hash_scope: s.hash_scope,
759
+ merkle_root: s.merkle_root
760
+ ? toHex(s.merkle_root)
761
+ : null,
762
+ // The system's own observation time, not the envelope's.
763
+ observed_at: formatTimestamp(s.observed_at),
764
+ query_hash: toHex(s.query_hash),
765
+ record_count: s.record_count,
766
+ system_id: s.system_id,
767
+ system_name: s.system_name,
768
+ };
769
+ if (isV7) {
770
+ sys.read_only_enforcement = requireMeasured(s.read_only_enforcement, "read_only_enforcement", s.system_name);
771
+ sys.transport_security = requireMeasured(s.transport_security, "transport_security", s.system_name);
772
+ }
773
+ return sys;
774
+ });
599
775
  const payload = {
600
776
  attested_at: attestedAt,
601
- payload_type: certFormatVersion === FORMAT_VERSION_V5
602
- ? PAYLOAD_TYPE_ATTESTATION_V5
603
- : PAYLOAD_TYPE_ATTESTATION,
777
+ payload_type: attestationPayloadType(certFormatVersion),
604
778
  proof_mode: att.proof_mode,
605
779
  subject_hash: toHex(subject.identifier_hash),
606
780
  systems,
607
781
  };
608
782
  return canonicalJson(payload);
609
783
  }
784
+ /**
785
+ * Read a v7 measured field that MUST be a non-empty string.
786
+ *
787
+ * A missing or non-string value cannot be turned into `""` and canonicalized:
788
+ * the signer never emits an empty measurement, so an empty one here would
789
+ * rebuild bytes no signature covers and be reported as forgery. Refusing with a
790
+ * document-shape message says the true thing — this record is malformed, not
791
+ * this record is fake.
792
+ */
793
+ function requireMeasured(value, field, systemName) {
794
+ if (typeof value !== "string" || value === "") {
795
+ throw new errors_js_1.VerificationError(`system ${JSON.stringify(systemName)}: ${field} is missing from a 7.0 record, ` +
796
+ "which signs it; the document is incomplete rather than unverifiable");
797
+ }
798
+ return value;
799
+ }
800
+ /** Domain separator for the record itself, by certificate format version. */
801
+ function certificatePayloadType(version) {
802
+ checkFormatVersion(version);
803
+ if (version === FORMAT_VERSION_V7)
804
+ return PAYLOAD_TYPE_VERIFICATION_RECORD_V7;
805
+ if (version === FORMAT_VERSION_V6)
806
+ return PAYLOAD_TYPE_VERIFICATION_RECORD_V6;
807
+ if (version === FORMAT_VERSION_V5)
808
+ return PAYLOAD_TYPE_CERTIFICATE_V5;
809
+ if (version === FORMAT_VERSION_V4)
810
+ return PAYLOAD_TYPE_CERTIFICATE_V4;
811
+ return PAYLOAD_TYPE_CERTIFICATE;
812
+ }
813
+ /**
814
+ * Domain separator for the attestation carried by a record of that format. It
815
+ * follows the record's version because a verifier rebuilds the attestation
816
+ * payload from the record it is checking, and must reproduce the exact bytes
817
+ * the enclave signed.
818
+ */
819
+ function attestationPayloadType(version) {
820
+ checkFormatVersion(version);
821
+ if (version === FORMAT_VERSION_V7)
822
+ return PAYLOAD_TYPE_ATTESTATION_V7;
823
+ if (version === FORMAT_VERSION_V6)
824
+ return PAYLOAD_TYPE_ATTESTATION_V6;
825
+ if (version === FORMAT_VERSION_V5)
826
+ return PAYLOAD_TYPE_ATTESTATION_V5;
827
+ return PAYLOAD_TYPE_ATTESTATION;
828
+ }
610
829
  function buildCertificatePayload(cert) {
611
830
  const att = cert.attestation;
612
831
  const issuer = cert.issuer;
@@ -614,19 +833,28 @@ function buildCertificatePayload(cert) {
614
833
  // One list, keyed by system_id (ADR-016 §2). v2 signed an attestation list
615
834
  // and a verification list joined only on the human-editable system_name,
616
835
  // which made a partial deletion indistinguishable from a complete one.
617
- const systems = (cert.systems ?? []).map((s) => ({
618
- attested_at: formatTimestamp(s.attested_at),
619
- attested_count: s.attested_count,
620
- canonical_version: s.canonical_version ?? null,
621
- connector_type: s.connector_type,
622
- hash_scope: s.hash_scope,
623
- merkle_root: s.merkle_root ? toHex(s.merkle_root) : null,
624
- query_hash: toHex(s.query_hash),
625
- system_id: s.system_id,
626
- system_name: s.system_name,
627
- verified_at: formatTimestamp(s.verified_at),
628
- verified_count: s.verified_count,
629
- }));
836
+ const version = cert.certificate_format_version;
837
+ const isV7 = version === FORMAT_VERSION_V7;
838
+ const systems = (cert.systems ?? []).map((s) => {
839
+ const sys = {
840
+ attested_at: formatTimestamp(s.attested_at),
841
+ attested_count: s.attested_count,
842
+ canonical_version: s.canonical_version ?? null,
843
+ connector_type: s.connector_type,
844
+ hash_scope: s.hash_scope,
845
+ merkle_root: s.merkle_root ? toHex(s.merkle_root) : null,
846
+ query_hash: toHex(s.query_hash),
847
+ system_id: s.system_id,
848
+ system_name: s.system_name,
849
+ verified_at: formatTimestamp(s.verified_at),
850
+ verified_count: s.verified_count,
851
+ };
852
+ if (isV7) {
853
+ sys.read_only_enforcement = requireMeasured(s.read_only_enforcement, "read_only_enforcement", s.system_name);
854
+ sys.transport_security = requireMeasured(s.transport_security, "transport_security", s.system_name);
855
+ }
856
+ return sys;
857
+ });
630
858
  // The attestation block carries no system list of its own: the merged list
631
859
  // above is a superset of it. verification_signature is gone entirely.
632
860
  const attObj = {
@@ -637,20 +865,28 @@ function buildCertificatePayload(cert) {
637
865
  // Fields added after v3 are gated on the certificate's OWN version. A v3
638
866
  // certificate must reconstruct to the same bytes forever; reading these
639
867
  // unconditionally would break every certificate already issued.
640
- const version = cert.certificate_format_version;
641
- const isV5 = version === FORMAT_VERSION_V5;
642
- const isV4Plus = version === FORMAT_VERSION_V4 || isV5;
868
+ // v6 carries v5's shape exactly, so it takes every gate v5 takes. Naming
869
+ // these "Plus" rather than testing equality at each use is what stops a new
870
+ // version from silently missing one.
871
+ const isV5Plus = version === FORMAT_VERSION_V5 || version === FORMAT_VERSION_V6 || isV7;
872
+ const isV4Plus = version === FORMAT_VERSION_V4 || isV5Plus;
643
873
  const issuerObj = {
644
874
  key_id: issuer.key_id,
645
875
  name: issuer.name,
646
876
  public_key: toHex(issuer.public_key),
647
877
  };
878
+ // v7 signs the algorithm, and does so unconditionally: unlike legal_entity
879
+ // and enclave_pcr0, "which scheme signed this" is never unknown to a signer,
880
+ // so a v7 record missing it is malformed rather than merely sparse.
881
+ if (isV7) {
882
+ issuerObj.algorithm = requireMeasured(issuer.algorithm, "issuer.algorithm", "issuer");
883
+ }
648
884
  if (isV4Plus && typeof issuer.legal_entity === "string" && issuer.legal_entity !== "") {
649
885
  issuerObj.legal_entity = issuer.legal_entity;
650
886
  }
651
887
  // Signed from v5, and omitted when absent or empty: a build with no
652
888
  // measurement signs none, and "" is a value a reader could mistake for one.
653
- if (isV5 && typeof issuer.enclave_pcr0 === "string" && issuer.enclave_pcr0 !== "") {
889
+ if (isV5Plus && typeof issuer.enclave_pcr0 === "string" && issuer.enclave_pcr0 !== "") {
654
890
  issuerObj.enclave_pcr0 = issuer.enclave_pcr0;
655
891
  }
656
892
  // v5 drops identifier_type_hint: signed, but always the constant "custom",
@@ -658,7 +894,7 @@ function buildCertificatePayload(cert) {
658
894
  const subjectObj = {
659
895
  identifier_hash: toHex(subject.identifier_hash),
660
896
  };
661
- if (!isV5) {
897
+ if (!isV5Plus) {
662
898
  subjectObj.identifier_type_hint = subject.identifier_type_hint;
663
899
  }
664
900
  // status and revocation are deliberately absent (ADR-016 §3): a signature
@@ -671,11 +907,7 @@ function buildCertificatePayload(cert) {
671
907
  certificate_id: cert.certificate_id,
672
908
  issued_at: formatTimestamp(cert.issued_at),
673
909
  issuer: issuerObj,
674
- payload_type: isV5
675
- ? PAYLOAD_TYPE_CERTIFICATE_V5
676
- : isV4Plus
677
- ? PAYLOAD_TYPE_CERTIFICATE_V4
678
- : PAYLOAD_TYPE_CERTIFICATE,
910
+ payload_type: certificatePayloadType(version),
679
911
  subject: subjectObj,
680
912
  systems,
681
913
  };
@@ -718,15 +950,37 @@ function attestationSystems(cert) {
718
950
  observed_at: s.attested_at,
719
951
  merkle_root: s.merkle_root,
720
952
  canonical_version: s.canonical_version,
953
+ // Carried through so the v7 attestation payload can be rebuilt from the
954
+ // record. Undefined on older formats, where buildAttestationPayload never
955
+ // reads them.
956
+ read_only_enforcement: s.read_only_enforcement,
957
+ transport_security: s.transport_security,
721
958
  }));
722
959
  }
960
+ /**
961
+ * Port of Go's BuildTreeHeadPayload.
962
+ *
963
+ * A head that names its log is signed in a DIFFERENT DOMAIN from one that does
964
+ * not (Q31). That is what makes log_id unstrippable: removing it moves the
965
+ * rebuild into burnledger.sth.v3 and adding one moves it into
966
+ * burnledger.sth.v7, and the signature fails either way. Heads signed before
967
+ * log ids existed carry none and reproduce exactly the bytes they always did.
968
+ *
969
+ * Exported for anchor.ts, which rebuilds the preimage of a head fetched from
970
+ * GET /v1/log/head. Not re-exported from index.ts: it is an internal seam
971
+ * between two modules of this package, not published surface.
972
+ */
723
973
  function buildTreeHeadPayload(head) {
974
+ const logId = head.log_id;
975
+ const named = typeof logId === "string" && logId !== "";
724
976
  const payload = {
725
- payload_type: PAYLOAD_TYPE_TREE_HEAD,
977
+ payload_type: named ? PAYLOAD_TYPE_TREE_HEAD_V7 : PAYLOAD_TYPE_TREE_HEAD,
726
978
  root_hash: toHex(head.root_hash),
727
979
  timestamp: formatTimestamp(head.timestamp),
728
980
  tree_size: head.tree_size,
729
981
  };
982
+ if (named)
983
+ payload.log_id = logId;
730
984
  return canonicalJson(payload);
731
985
  }
732
986
  // ---------------------------------------------------------------------------
@@ -881,6 +1135,42 @@ function buildCertificateStatusPayload(stmt) {
881
1135
  }
882
1136
  return canonicalJson(payload);
883
1137
  }
1138
+ /**
1139
+ * Port of Go's BuildKeyListPayload (core/key_list.go).
1140
+ *
1141
+ * The key array is sorted by key_id before signing: canonical JSON sorts
1142
+ * object keys but does nothing to array order, and the server has no reason to
1143
+ * preserve any particular order across a rotation. Optional entry fields are
1144
+ * omitted, never emitted as null, for the reason buildCertificateStatusPayload
1145
+ * omits them. `keys` is the array exactly as HANDED to the verifier — the
1146
+ * signature covers the published entries, not a parsed reinterpretation of
1147
+ * them, which is what makes an edited key_status detectable.
1148
+ */
1149
+ function buildKeyListPayload(doc) {
1150
+ const entries = doc.keys.map((e) => {
1151
+ const k = {};
1152
+ if (e.compromised_from != null)
1153
+ k.compromised_from = e.compromised_from;
1154
+ k.key_id = e.key_id;
1155
+ k.key_status = e.key_status;
1156
+ if (e.not_after != null)
1157
+ k.not_after = e.not_after;
1158
+ if (e.not_before != null && e.not_before !== "")
1159
+ k.not_before = e.not_before;
1160
+ k.public_key = e.public_key;
1161
+ return k;
1162
+ });
1163
+ entries.sort((a, b) => (a.key_id < b.key_id ? -1 : a.key_id === b.key_id ? 0 : 1));
1164
+ const payload = {
1165
+ payload_type: PAYLOAD_TYPE_KEY_LIST,
1166
+ keys: entries,
1167
+ statement_expires_at: formatTimestamp(doc.statement_expires_at),
1168
+ statement_issued_at: formatTimestamp(doc.statement_issued_at),
1169
+ sth_root_hash: toHex(doc.sth_root_hash),
1170
+ sth_tree_size: doc.sth_tree_size,
1171
+ };
1172
+ return canonicalJson(payload);
1173
+ }
884
1174
  /** The verdict when a certificate verified but nothing said whether it was revoked. */
885
1175
  exports.VALID_REVOCATION_UNKNOWN = "VALID_REVOCATION_UNKNOWN";
886
1176
  /**
@@ -896,28 +1186,43 @@ exports.VALID_REVOCATION_UNKNOWN = "VALID_REVOCATION_UNKNOWN";
896
1186
  * | input | result |
897
1187
  * |---|---|
898
1188
  * | fresh statement, REVOKED | throws VerificationError |
899
- * | fresh statement, ACTIVE | `"VALID"` |
900
- * | absent or expired statement | `"VALID_REVOCATION_UNKNOWN"` |
1189
+ * | fresh statement, ACTIVE | `"VALID"`, or a soft verdict on either key |
1190
+ * | absent or expired statement | `"VALID_REVOCATION_UNKNOWN"`, or the certificate key's soft verdict |
1191
+ * | statement key unusable (ADR-017) | throws VerificationError |
901
1192
  *
902
- * The last row is the point: `verifyCertificate` answers VALID there, which
1193
+ * The third row is the point: `verifyCertificate` answers VALID there, which
903
1194
  * reads as "not revoked" and is not something it checked.
904
1195
  */
905
1196
  async function verifyCertificateWithStatus(crypto, certificate, publicKeys, status, now) {
1197
+ // A refusal throws out of verifyCertificate, so `base` is VALID or one of the
1198
+ // two soft verdicts. Returning a soft verdict here skipped every check below
1199
+ // it: a revoked certificate signed by a compromised-later key answered
1200
+ // VALID_KEY_COMPROMISED_LATER and its statement was never authenticated. The
1201
+ // verdict is held instead, and resolved against what the statement says.
906
1202
  const base = await verifyCertificate(crypto, certificate, publicKeys);
907
- if (base !== "VALID") {
908
- return base;
909
- }
910
1203
  if (status == null) {
911
- return exports.VALID_REVOCATION_UNKNOWN;
1204
+ return resolveWithBase(base, exports.VALID_REVOCATION_UNKNOWN);
912
1205
  }
913
1206
  const keyId = status.key_id;
914
1207
  const info = keyId ? publicKeys.get(keyId) : undefined;
915
1208
  if (!info) {
916
1209
  throw new errors_js_1.VerificationError(`status statement signed by unknown key: ${keyId}`);
917
1210
  }
918
- if (info.revoked) {
919
- throw new errors_js_1.VerificationError("status statement signed by a revoked key");
920
- }
1211
+ // The whole ADR-017 table, not just the deprecated boolean. `revoked` is
1212
+ // never set by a key loaded from /.well-known/burnledger-keys — that endpoint
1213
+ // publishes key_status — so this check read `false` for a key the issuer had
1214
+ // declared COMPROMISED, and whoever held that key could mint an ACTIVE
1215
+ // statement for a revoked certificate that every SDK and the browser bundle
1216
+ // rendered as a signed "not revoked". core.VerifyCertificateWithStatus has
1217
+ // always evaluated it.
1218
+ //
1219
+ // The anchor is the verifier's own clock, not a field inside the statement: a
1220
+ // statement must already be inside its own validity window to be served at
1221
+ // all, and `now` is a time this verifier observed rather than one the signing
1222
+ // key asserted about itself.
1223
+ const when = now ?? new Date();
1224
+ const at = formatTimestamp(when.toISOString());
1225
+ const statusKeyVerdict = requireUsableKey(info, parseRfc3339(at), keyId, "status statement key");
921
1226
  const payload = buildCertificateStatusPayload(status);
922
1227
  // decodeSignature, not hexToBytes: signatures arrive as hex, base64 or a byte
923
1228
  // array depending on the producer, and every other signature on this path
@@ -945,17 +1250,42 @@ async function verifyCertificateWithStatus(crypto, certificate, publicKeys, stat
945
1250
  }
946
1251
  // Compared in the normalized form the payload signs, so the freshness check
947
1252
  // cannot disagree with what was signed about the same two instants.
948
- const at = formatTimestamp((now ?? new Date()).toISOString());
949
1253
  const issued = formatTimestamp(status.statement_issued_at);
950
1254
  const expires = formatTimestamp(status.statement_expires_at);
951
1255
  if (at < issued || at >= expires) {
952
1256
  // Stale is not a weaker answer, it is no answer — including for a REVOKED
953
1257
  // statement, which must never decay into VALID.
954
- return exports.VALID_REVOCATION_UNKNOWN;
1258
+ return resolveWithBase(base, exports.VALID_REVOCATION_UNKNOWN);
955
1259
  }
956
1260
  if (status.status === "REVOKED") {
957
- throw new errors_js_1.VerificationError("certificate has been revoked");
1261
+ // Coded, because this is the only status-path rejection that is a fact
1262
+ // about the certificate rather than a failure to authenticate the
1263
+ // statement. A caller that cannot tell the two apart will render an
1264
+ // unsigned statement as a revocation.
1265
+ throw new errors_js_1.VerificationError("certificate has been revoked", errors_js_1.CERTIFICATE_REVOKED);
1266
+ }
1267
+ // A state this SDK does not recognize is NOT active. This tested only for
1268
+ // REVOKED and treated everything else as a pass, so a signed, fresh,
1269
+ // correctly-bound statement saying "SUSPENDED" — or "revoked" in the wrong
1270
+ // case — was reported as revocation CHECKED AND PASSED. Mirrors
1271
+ // core.ValidCertificateStatusValue.
1272
+ if (!KNOWN_STATUS_VALUES.has(status.status)) {
1273
+ return resolveWithBase(base, exports.VALID_REVOCATION_UNKNOWN);
958
1274
  }
959
- return "VALID";
1275
+ // A soft verdict on the status key is the result, not a footnote — but it is
1276
+ // reported only once the statement has authenticated and been read, so it can
1277
+ // neither speak for an unverified statement nor suppress a revocation.
1278
+ return resolveWithBase(base, statusKeyVerdict);
1279
+ }
1280
+ /**
1281
+ * Picks what to report when the certificate's own key returned a soft verdict
1282
+ * and the status path also has something to say. A doubt about the key that
1283
+ * signed the certificate questions the whole document, so it outranks a doubt
1284
+ * about revocation or about the statement's own key; only one verdict string
1285
+ * comes back, and this is the one a reader most needs. Mirrors
1286
+ * core.resolveWithBase.
1287
+ */
1288
+ function resolveWithBase(base, verdict) {
1289
+ return base !== "VALID" ? base : verdict;
960
1290
  }
961
1291
  //# sourceMappingURL=verify.js.map