burnledger 0.3.1 → 0.5.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 (89) hide show
  1. package/README.md +15 -3
  2. package/dist/cjs/client.d.ts +14 -0
  3. package/dist/cjs/client.d.ts.map +1 -1
  4. package/dist/cjs/client.js +4 -0
  5. package/dist/cjs/client.js.map +1 -1
  6. package/dist/cjs/errors.d.ts +28 -1
  7. package/dist/cjs/errors.d.ts.map +1 -1
  8. package/dist/cjs/errors.js +30 -2
  9. package/dist/cjs/errors.js.map +1 -1
  10. package/dist/cjs/http.d.ts +21 -1
  11. package/dist/cjs/http.d.ts.map +1 -1
  12. package/dist/cjs/http.js +76 -12
  13. package/dist/cjs/http.js.map +1 -1
  14. package/dist/cjs/index.browser.d.ts +2 -4
  15. package/dist/cjs/index.browser.d.ts.map +1 -1
  16. package/dist/cjs/index.browser.js.map +1 -1
  17. package/dist/cjs/index.d.ts +14 -7
  18. package/dist/cjs/index.d.ts.map +1 -1
  19. package/dist/cjs/index.js +13 -2
  20. package/dist/cjs/index.js.map +1 -1
  21. package/dist/cjs/keys.d.ts +6 -0
  22. package/dist/cjs/keys.d.ts.map +1 -1
  23. package/dist/cjs/keys.js.map +1 -1
  24. package/dist/cjs/models.d.ts +24 -4
  25. package/dist/cjs/models.d.ts.map +1 -1
  26. package/dist/cjs/models.js +10 -4
  27. package/dist/cjs/models.js.map +1 -1
  28. package/dist/cjs/pagination.d.ts.map +1 -1
  29. package/dist/cjs/pagination.js.map +1 -1
  30. package/dist/cjs/verify.d.ts +83 -5
  31. package/dist/cjs/verify.d.ts.map +1 -1
  32. package/dist/cjs/verify.js +359 -28
  33. package/dist/cjs/verify.js.map +1 -1
  34. package/dist/cjs/web-verifier.d.ts +10 -5
  35. package/dist/cjs/web-verifier.d.ts.map +1 -1
  36. package/dist/cjs/web-verifier.js +7 -3
  37. package/dist/cjs/web-verifier.js.map +1 -1
  38. package/dist/cjs/webhooks.js.map +1 -1
  39. package/dist/esm/cli.d.ts +6 -0
  40. package/dist/esm/cli.d.ts.map +1 -1
  41. package/dist/esm/cli.js +116 -15
  42. package/dist/esm/cli.js.map +1 -1
  43. package/dist/esm/client.d.ts +14 -0
  44. package/dist/esm/client.d.ts.map +1 -1
  45. package/dist/esm/client.js +4 -0
  46. package/dist/esm/client.js.map +1 -1
  47. package/dist/esm/errors.d.ts +28 -1
  48. package/dist/esm/errors.d.ts.map +1 -1
  49. package/dist/esm/errors.js +29 -1
  50. package/dist/esm/errors.js.map +1 -1
  51. package/dist/esm/http.d.ts +21 -1
  52. package/dist/esm/http.d.ts.map +1 -1
  53. package/dist/esm/http.js +75 -12
  54. package/dist/esm/http.js.map +1 -1
  55. package/dist/esm/index.browser.d.ts +2 -4
  56. package/dist/esm/index.browser.d.ts.map +1 -1
  57. package/dist/esm/index.browser.js.map +1 -1
  58. package/dist/esm/index.d.ts +14 -7
  59. package/dist/esm/index.d.ts.map +1 -1
  60. package/dist/esm/index.js +11 -3
  61. package/dist/esm/index.js.map +1 -1
  62. package/dist/esm/keys.d.ts +6 -0
  63. package/dist/esm/keys.d.ts.map +1 -1
  64. package/dist/esm/keys.js.map +1 -1
  65. package/dist/esm/models.d.ts +24 -4
  66. package/dist/esm/models.d.ts.map +1 -1
  67. package/dist/esm/models.js +10 -4
  68. package/dist/esm/models.js.map +1 -1
  69. package/dist/esm/pagination.d.ts.map +1 -1
  70. package/dist/esm/verify.d.ts +83 -5
  71. package/dist/esm/verify.d.ts.map +1 -1
  72. package/dist/esm/verify.js +357 -28
  73. package/dist/esm/verify.js.map +1 -1
  74. package/dist/esm/web-verifier.d.ts +10 -5
  75. package/dist/esm/web-verifier.d.ts.map +1 -1
  76. package/dist/esm/web-verifier.js +11 -3
  77. package/dist/esm/web-verifier.js.map +1 -1
  78. package/dist/esm/webhooks.js.map +1 -1
  79. package/package.json +5 -3
  80. package/src/cli.ts +123 -13
  81. package/src/client.ts +16 -0
  82. package/src/errors.ts +32 -2
  83. package/src/http.ts +79 -12
  84. package/src/index.browser.ts +2 -2
  85. package/src/index.ts +16 -4
  86. package/src/keys.ts +11 -1
  87. package/src/models.ts +34 -8
  88. package/src/verify.ts +428 -28
  89. package/src/web-verifier.ts +14 -5
@@ -17,16 +17,93 @@
17
17
  */
18
18
  import type { CryptoOps } from "./crypto.js";
19
19
  import type { TransparencyResult, VerificationResult } from "./models.js";
20
+ /**
21
+ * Every format this build can read, oldest first. Mirrors core.FormatVersions()
22
+ * in the Go source; the cross-language conformance corpus carries a fixture per
23
+ * version, so a version present in one language and missing here fails there.
24
+ *
25
+ * Frozen because `readonly` is erased at build time: it stops a TypeScript
26
+ * caller from writing to the array, and does nothing at all to the JavaScript
27
+ * this compiles to — which is what the browser bundle actually runs, where any
28
+ * script on the page could otherwise push a version onto it.
29
+ */
30
+ export declare const KNOWN_FORMAT_VERSIONS: readonly string[];
20
31
  export declare function hexToBytes(hex: string): Uint8Array;
21
32
  export declare function bytesToHex(bytes: Uint8Array): string;
33
+ /** A signing key's disposition (ADR-017 §1).
34
+ *
35
+ * There is deliberately no "revoked": that word belongs to certificate
36
+ * revocation, and one word meaning two things is the confusion ADR-017 ends.
37
+ * Retirement is routine and a retired key's signatures stay exactly as good as
38
+ * the day they were made; compromise is an incident with a date.
39
+ */
40
+ export type KeyStatus = "active" | "retired" | "compromised";
22
41
  export interface PublicKeyInfo {
23
42
  readonly keyBytes: Uint8Array;
24
43
  readonly keyId: string;
44
+ /** Raw `key_status` from the published entry. Kept as a string rather than
45
+ * KeyStatus so a word this verifier does not understand can fail closed
46
+ * instead of being narrowed away at the type level. */
47
+ readonly keyStatus: string;
48
+ /** RFC 3339, whole seconds, exactly as published. */
49
+ readonly notBefore?: string;
50
+ readonly notAfter?: string;
51
+ readonly compromisedFrom?: string;
52
+ /** @deprecated Superseded by keyStatus. Retained because it is published
53
+ * surface: a caller still passing `{ revoked: true }` gets the same
54
+ * fail-closed behaviour it always did. */
25
55
  readonly revoked: boolean;
26
56
  }
27
- export declare function publicKeyFromHex(crypto: CryptoOps, hexKey: string, opts?: {
57
+ /**
58
+ * The options publicKeyFromHex accepts, mirroring one entry of the published key
59
+ * set at `/.well-known/burnledger-keys`.
60
+ *
61
+ * Named, and exported, because it is restated by every entry point that wraps
62
+ * publicKeyFromHex — index.ts, index.browser.ts, web-verifier.ts. When the
63
+ * ADR-017 fields were added, two of the three were widened and the browser entry
64
+ * was not, so the dashboard could not express a compromised key through the API
65
+ * it imports and every key it built looked unconditionally active. A shared type
66
+ * is what stops the next field from being added to two places out of three.
67
+ */
68
+ export interface PublicKeyOptions {
69
+ /** @deprecated Use keyStatus. Retained: true still means "unusable". */
28
70
  revoked?: boolean;
29
- }): Promise<PublicKeyInfo>;
71
+ keyStatus?: string;
72
+ notBefore?: string;
73
+ notAfter?: string;
74
+ compromisedFrom?: string;
75
+ }
76
+ export declare function publicKeyFromHex(crypto: CryptoOps, hexKey: string, opts?: PublicKeyOptions): Promise<PublicKeyInfo>;
77
+ /** Verdicts for the key check (ADR-017 §2). */
78
+ export declare const KEY_OUTSIDE_VALIDITY = "KEY_OUTSIDE_VALIDITY";
79
+ export declare const KEY_COMPROMISED = "KEY_COMPROMISED";
80
+ export declare const VALID_KEY_COMPROMISED_LATER = "VALID_KEY_COMPROMISED_LATER";
81
+ export declare const VALID_KEY_WINDOW_UNKNOWN = "VALID_KEY_WINDOW_UNKNOWN";
82
+ /** Whether a key was authorized to sign at anchor time `t`.
83
+ *
84
+ * Returns `"VALID"` when the key clears. The anchor is seconds since the epoch
85
+ * from a time the verifier observed independently — the signed tree head's
86
+ * timestamp — never the certificate's own `issued_at`, which is signed by the
87
+ * very key whose validity is in question.
88
+ *
89
+ * Mirrors core.EvaluateKey in Go, including both of its settled readings: an
90
+ * entry stating no interval has nothing to check, so a missing anchor is not
91
+ * "unknown"; and a compromised key with no anchor does not soften, because
92
+ * without an anchor we cannot show the signature predates the compromise.
93
+ */
94
+ export declare function evaluateKey(pki: PublicKeyInfo, anchor: number | null): string;
95
+ /**
96
+ * The anchor a certificate supplies, or null when it supplies none.
97
+ *
98
+ * READS AN UNSIGNED FIELD. `transparency` is stripped before signing, so on a
99
+ * certificate handed to a verifier the timestamp is holder-editable. Use
100
+ * {@link verifiedCertificateAnchor} anywhere the anchor decides whether a key
101
+ * may be relied on; this is safe only where the same timestamp is inside a
102
+ * payload the caller goes on to verify, which is what verifyTransparency does.
103
+ *
104
+ * Exported and unchanged because it is published API.
105
+ */
106
+ export declare function certificateAnchor(certificate: Record<string, unknown>): number | null;
30
107
  type Cert = Record<string, unknown>;
31
108
  /** Verify all signatures on a deletion certificate offline. */
32
109
  export declare function verifyCertificate(crypto: CryptoOps, certificate: Cert, publicKeys: Map<string, PublicKeyInfo>): Promise<VerificationResult>;
@@ -93,10 +170,11 @@ export declare const VALID_REVOCATION_UNKNOWN = "VALID_REVOCATION_UNKNOWN";
93
170
  * | input | result |
94
171
  * |---|---|
95
172
  * | fresh statement, REVOKED | throws VerificationError |
96
- * | fresh statement, ACTIVE | `"VALID"` |
97
- * | absent or expired statement | `"VALID_REVOCATION_UNKNOWN"` |
173
+ * | fresh statement, ACTIVE | `"VALID"`, or a soft verdict on either key |
174
+ * | absent or expired statement | `"VALID_REVOCATION_UNKNOWN"`, or the certificate key's soft verdict |
175
+ * | statement key unusable (ADR-017) | throws VerificationError |
98
176
  *
99
- * The last row is the point: `verifyCertificate` answers VALID there, which
177
+ * The third row is the point: `verifyCertificate` answers VALID there, which
100
178
  * reads as "not revoked" and is not something it checked.
101
179
  */
102
180
  export declare function verifyCertificateWithStatus(crypto: CryptoOps, certificate: Cert, publicKeys: Map<string, PublicKeyInfo>, status?: Record<string, unknown> | null, now?: Date): Promise<string>;
@@ -1 +1 @@
1
- {"version":3,"file":"verify.d.ts","sourceRoot":"","sources":["../../src/verify.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAE7C,OAAO,KAAK,EAAE,kBAAkB,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AAkB1E,wBAAgB,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,UAAU,CASlD;AAYD,wBAAgB,UAAU,CAAC,KAAK,EAAE,UAAU,GAAG,MAAM,CAQpD;AAoCD,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,QAAQ,EAAE,UAAU,CAAC;IAC9B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;CAC3B;AAED,wBAAsB,gBAAgB,CACpC,MAAM,EAAE,SAAS,EACjB,MAAM,EAAE,MAAM,EACd,IAAI,CAAC,EAAE;IAAE,OAAO,CAAC,EAAE,OAAO,CAAA;CAAE,GAC3B,OAAO,CAAC,aAAa,CAAC,CAQxB;AAMD,KAAK,IAAI,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AAEpC,+DAA+D;AAC/D,wBAAsB,iBAAiB,CACrC,MAAM,EAAE,SAAS,EACjB,WAAW,EAAE,IAAI,EACjB,UAAU,EAAE,GAAG,CAAC,MAAM,EAAE,aAAa,CAAC,GACrC,OAAO,CAAC,kBAAkB,CAAC,CA2F7B;AAKD,+DAA+D;AAC/D,wBAAsB,kBAAkB,CACtC,MAAM,EAAE,SAAS,EACjB,WAAW,EAAE,IAAI,EACjB,UAAU,EAAE,GAAG,CAAC,MAAM,EAAE,aAAa,CAAC,GACrC,OAAO,CAAC,kBAAkB,CAAC,CAyE7B;AAMD;;;;;;;;;GASG;AACH,wBAAgB,aAAa,CAAC,WAAW,EAAE,IAAI,GAAG,UAAU,CAK3D;AAoFD;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,eAAe,CAAC,EAAE,EAAE,OAAO,GAAG,MAAM,CAMnD;AAgVD;;;GAGG;AACH,wBAAsB,iBAAiB,CACrC,MAAM,EAAE,SAAS,EACjB,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,UAAU,EACnB,OAAO,EAAE,UAAU,EACnB,KAAK,EAAE,UAAU,EAAE,GAClB,OAAO,CAAC,OAAO,CAAC,CAwDlB;AAGD;;;;;;GAMG;AACH,wBAAgB,6BAA6B,CAC3C,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC5B,UAAU,CAoBZ;AAED,uFAAuF;AACvF,eAAO,MAAM,wBAAwB,6BAA6B,CAAC;AAEnE;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAsB,2BAA2B,CAC/C,MAAM,EAAE,SAAS,EACjB,WAAW,EAAE,IAAI,EACjB,UAAU,EAAE,GAAG,CAAC,MAAM,EAAE,aAAa,CAAC,EACtC,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,EACvC,GAAG,CAAC,EAAE,IAAI,GACT,OAAO,CAAC,MAAM,CAAC,CAkDjB"}
1
+ {"version":3,"file":"verify.d.ts","sourceRoot":"","sources":["../../src/verify.ts"],"names":[],"mappings":"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"}
@@ -15,7 +15,7 @@
15
15
  * certificate signing payload. issuanceBytes reconstructs json.Marshal
16
16
  * output at issuance time (transparency_status=PENDING, no transparency).
17
17
  */
18
- import { VerificationError } from "./errors.js";
18
+ import { CERTIFICATE_REVOKED, UNSUPPORTED_FORMAT_VERSION, VerificationError, } from "./errors.js";
19
19
  // ---------------------------------------------------------------------------
20
20
  // Pure byte helpers (no Buffer, no node:crypto)
21
21
  // ---------------------------------------------------------------------------
@@ -24,6 +24,57 @@ const HEX_CHARS = "0123456789abcdef";
24
24
  // source-of-truth const block in Go's core/payload.go byte-for-byte.
25
25
  const PAYLOAD_TYPE_ATTESTATION = "burnledger.attestation.v3";
26
26
  const PAYLOAD_TYPE_CERTIFICATE = "burnledger.certificate.v3";
27
+ // v4 signs the legal entity behind the trading name and a commitment to the
28
+ // limitations wording. v3 certificates are verified forever -- they are evidence
29
+ // designed to outlive the company -- so this is a branch, never a replacement.
30
+ const PAYLOAD_TYPE_CERTIFICATE_V4 = "burnledger.certificate.v4";
31
+ // v5 moves BOTH tags. v4 moved only the certificate's, because the attestation
32
+ // shape did not change; in v5 it does, by way of the certificate carrying it.
33
+ const PAYLOAD_TYPE_ATTESTATION_V5 = "burnledger.attestation.v5";
34
+ const PAYLOAD_TYPE_CERTIFICATE_V5 = "burnledger.certificate.v5";
35
+ const FORMAT_VERSION_V5 = "5.0";
36
+ const FORMAT_VERSION_V4 = "4.0";
37
+ // v6 renames the artifact and nothing else: the shape is byte-for-byte v5, and
38
+ // only the tag and the version string differ. It still takes its own tag, for
39
+ // the same reason v4 and v5 each took one -- a signature over a payload calling
40
+ // itself a verification record must not verify as one calling itself a
41
+ // certificate. Two names for one shape sharing a separator is exactly what a
42
+ // separator prevents.
43
+ const PAYLOAD_TYPE_ATTESTATION_V6 = "burnledger.attestation.v6";
44
+ const PAYLOAD_TYPE_VERIFICATION_RECORD_V6 = "burnledger.verification_record.v6";
45
+ const FORMAT_VERSION_V6 = "6.0";
46
+ const FORMAT_VERSION_V3 = "3.0";
47
+ /**
48
+ * Every format this build can read, oldest first. Mirrors core.FormatVersions()
49
+ * in the Go source; the cross-language conformance corpus carries a fixture per
50
+ * version, so a version present in one language and missing here fails there.
51
+ *
52
+ * Frozen because `readonly` is erased at build time: it stops a TypeScript
53
+ * caller from writing to the array, and does nothing at all to the JavaScript
54
+ * this compiles to — which is what the browser bundle actually runs, where any
55
+ * script on the page could otherwise push a version onto it.
56
+ */
57
+ export const KNOWN_FORMAT_VERSIONS = Object.freeze([
58
+ FORMAT_VERSION_V3,
59
+ FORMAT_VERSION_V4,
60
+ FORMAT_VERSION_V5,
61
+ FORMAT_VERSION_V6,
62
+ ]);
63
+ /**
64
+ * Returns the version, or throws if this SDK cannot read it.
65
+ *
66
+ * Rejects a non-string too: JSON carrying `6.0` as a NUMBER would compare
67
+ * unequal to every known version and silently take the oldest branch, which is
68
+ * the same trap as an unknown version wearing a different hat.
69
+ */
70
+ function checkFormatVersion(version) {
71
+ if (typeof version !== "string" || !KNOWN_FORMAT_VERSIONS.includes(version)) {
72
+ throw new VerificationError(`unsupported certificate_format_version ${JSON.stringify(version)}; ` +
73
+ `this version of the SDK reads ${KNOWN_FORMAT_VERSIONS.join(", ")}. ` +
74
+ "The record may be genuine and simply newer than this library.", UNSUPPORTED_FORMAT_VERSION);
75
+ }
76
+ return version;
77
+ }
27
78
  const PAYLOAD_TYPE_TREE_HEAD = "burnledger.sth.v3";
28
79
  const PAYLOAD_TYPE_LOG_LEAF = "burnledger.log_leaf.v3";
29
80
  // There is deliberately no verification payload type: v3 produces those facts
@@ -95,17 +146,189 @@ export async function publicKeyFromHex(crypto, hexKey, opts) {
95
146
  }
96
147
  const hash = await crypto.sha256(raw);
97
148
  const keyId = "dp_k_" + bytesToHex(hash);
98
- return { keyBytes: raw, keyId, revoked: opts?.revoked ?? false };
149
+ const revoked = opts?.revoked ?? false;
150
+ // An old caller passing revoked:true meant "this key is unusable" and must
151
+ // keep getting exactly that. Mapping it to a status this verifier cannot
152
+ // interpret reaches the same fail-closed branch as before.
153
+ const keyStatus = opts?.keyStatus ?? (revoked ? "revoked" : "active");
154
+ return {
155
+ keyBytes: raw,
156
+ keyId,
157
+ keyStatus,
158
+ notBefore: opts?.notBefore,
159
+ notAfter: opts?.notAfter,
160
+ compromisedFrom: opts?.compromisedFrom,
161
+ revoked,
162
+ };
163
+ }
164
+ /** Verdicts for the key check (ADR-017 §2). */
165
+ export const KEY_OUTSIDE_VALIDITY = "KEY_OUTSIDE_VALIDITY";
166
+ export const KEY_COMPROMISED = "KEY_COMPROMISED";
167
+ export const VALID_KEY_COMPROMISED_LATER = "VALID_KEY_COMPROMISED_LATER";
168
+ export const VALID_KEY_WINDOW_UNKNOWN = "VALID_KEY_WINDOW_UNKNOWN";
169
+ /**
170
+ * The revocation states a statement may assert. Mirrors
171
+ * core.ValidCertificateStatusValue: anything else means the statement was read
172
+ * but not understood, which is not the same as "not revoked".
173
+ */
174
+ const KNOWN_STATUS_VALUES = new Set(["ACTIVE", "REVOKED"]);
175
+ /**
176
+ * The key verdicts that permit relying on a signature: VALID plus the two soft
177
+ * verdicts ADR-017 §2 says a reader must be SHOWN rather than refused. Anything
178
+ * not named here is refused, including a verdict added in a later release.
179
+ */
180
+ const USABLE_KEY_VERDICTS = new Set([
181
+ "VALID",
182
+ VALID_KEY_WINDOW_UNKNOWN,
183
+ VALID_KEY_COMPROMISED_LATER,
184
+ ]);
185
+ /** Whether a key was authorized to sign at anchor time `t`.
186
+ *
187
+ * Returns `"VALID"` when the key clears. The anchor is seconds since the epoch
188
+ * from a time the verifier observed independently — the signed tree head's
189
+ * timestamp — never the certificate's own `issued_at`, which is signed by the
190
+ * very key whose validity is in question.
191
+ *
192
+ * Mirrors core.EvaluateKey in Go, including both of its settled readings: an
193
+ * entry stating no interval has nothing to check, so a missing anchor is not
194
+ * "unknown"; and a compromised key with no anchor does not soften, because
195
+ * without an anchor we cannot show the signature predates the compromise.
196
+ */
197
+ export function evaluateKey(pki, anchor) {
198
+ // A caller can still build a PublicKeyInfo by hand with revoked:true rather
199
+ // than going through publicKeyFromHex. That flag meant "this key is
200
+ // unusable" and must keep meaning exactly that.
201
+ if (pki.revoked)
202
+ return "UNKNOWN_KEY";
203
+ if (pki.keyStatus === "compromised") {
204
+ if (anchor === null || pki.compromisedFrom === undefined)
205
+ return KEY_COMPROMISED;
206
+ return anchor < parseRfc3339(pki.compromisedFrom)
207
+ ? VALID_KEY_COMPROMISED_LATER
208
+ : KEY_COMPROMISED;
209
+ }
210
+ if (pki.keyStatus === "active" || pki.keyStatus === "retired" || pki.keyStatus === "") {
211
+ if (pki.notBefore === undefined && pki.notAfter === undefined)
212
+ return "VALID";
213
+ if (anchor === null)
214
+ return VALID_KEY_WINDOW_UNKNOWN;
215
+ if (pki.notBefore !== undefined && anchor < parseRfc3339(pki.notBefore)) {
216
+ return KEY_OUTSIDE_VALIDITY;
217
+ }
218
+ if (pki.notAfter !== undefined && anchor >= parseRfc3339(pki.notAfter)) {
219
+ return KEY_OUTSIDE_VALIDITY;
220
+ }
221
+ return "VALID";
222
+ }
223
+ // A disposition this verifier does not understand is not one it can vouch
224
+ // for. Fail closed, as the boolean it replaces did.
225
+ return "UNKNOWN_KEY";
226
+ }
227
+ /**
228
+ * The anchor a certificate supplies, or null when it supplies none.
229
+ *
230
+ * READS AN UNSIGNED FIELD. `transparency` is stripped before signing, so on a
231
+ * certificate handed to a verifier the timestamp is holder-editable. Use
232
+ * {@link verifiedCertificateAnchor} anywhere the anchor decides whether a key
233
+ * may be relied on; this is safe only where the same timestamp is inside a
234
+ * payload the caller goes on to verify, which is what verifyTransparency does.
235
+ *
236
+ * Exported and unchanged because it is published API.
237
+ */
238
+ export function certificateAnchor(certificate) {
239
+ const transparency = certificate.transparency;
240
+ if (transparency == null)
241
+ return null;
242
+ const sth = transparency.signed_tree_head;
243
+ if (sth === undefined || typeof sth.timestamp !== "string")
244
+ return null;
245
+ try {
246
+ return parseRfc3339(sth.timestamp);
247
+ }
248
+ catch {
249
+ return null;
250
+ }
251
+ }
252
+ /**
253
+ * The anchor, but only once the tree head carrying it has been checked against
254
+ * the key being evaluated.
255
+ *
256
+ * The bypass this closes: for a key declared compromised, evaluateKey answers
257
+ * KEY_COMPROMISED with no anchor and VALID_KEY_COMPROMISED_LATER when the
258
+ * anchor predates the compromise — and the second is a verdict requireUsableKey
259
+ * passes through as the RESULT of verifyCertificate. Since the timestamp is
260
+ * covered by no signature, a holder could backdate it and have a certificate
261
+ * signed with a compromised key reported as verified-with-a-caveat. Deleting
262
+ * `transparency` failed closed; keeping a doctored one did not.
263
+ *
264
+ * A head that does not verify yields no anchor, which is the same conservative
265
+ * answer as a certificate carrying no transparency block at all.
266
+ */
267
+ async function verifiedCertificateAnchor(crypto, certificate, pki) {
268
+ const anchor = certificateAnchor(certificate);
269
+ if (anchor === null)
270
+ return null;
271
+ const transparency = certificate.transparency;
272
+ const sth = transparency.signed_tree_head;
273
+ try {
274
+ const headPayload = buildTreeHeadPayload(sth);
275
+ const headSig = decodeSignature(sth.signature);
276
+ if (!(await crypto.ed25519Verify(pki.keyBytes, headPayload, headSig)))
277
+ return null;
278
+ }
279
+ catch {
280
+ return null;
281
+ }
282
+ return anchor;
283
+ }
284
+ /** Throws for a key that cannot be used at all; returns the verdict otherwise.
285
+ *
286
+ * VALID_KEY_COMPROMISED_LATER and VALID_KEY_WINDOW_UNKNOWN are returned rather
287
+ * than thrown because they are not refusals — they are results a caller must
288
+ * show the reader instead of collapsing into green.
289
+ */
290
+ function requireUsableKey(pki, anchor, keyId,
291
+ // Named, because this is also the key that signs a certificate-status
292
+ // statement, and "issuer key is compromised" would send a reader looking at
293
+ // the wrong key.
294
+ role = "issuer key") {
295
+ const verdict = evaluateKey(pki, anchor);
296
+ if (verdict === KEY_COMPROMISED) {
297
+ throw new VerificationError(`${role} is compromised: ${keyId}`);
298
+ }
299
+ if (verdict === KEY_OUTSIDE_VALIDITY) {
300
+ throw new VerificationError(`${role} was not valid when it signed: ${keyId}`);
301
+ }
302
+ if (verdict === "UNKNOWN_KEY") {
303
+ throw new VerificationError(`${role} has an unusable status "${pki.keyStatus}": ${keyId}`);
304
+ }
305
+ // An ALLOW-list, matching core.KeyIsUsable, which was inverted away from
306
+ // exactly the shape above. Naming the refusals and returning everything else
307
+ // means a verdict added to evaluateKey later becomes a PASS by default — and
308
+ // the value returned here is propagated out of verifyCertificate as its
309
+ // result, so "everything else" is rendered to a reader as "verified, with a
310
+ // note". Go would refuse the same document. That is #684's three-verifiers-
311
+ // two-answers divergence, in the direction that calls a bad document good.
312
+ if (!USABLE_KEY_VERDICTS.has(verdict)) {
313
+ throw new VerificationError(`${role} returned a verdict this SDK does not recognize (${verdict}): ${keyId}`);
314
+ }
315
+ return verdict;
99
316
  }
100
317
  /** Verify all signatures on a deletion certificate offline. */
101
318
  export async function verifyCertificate(crypto, certificate, publicKeys) {
319
+ // Step 0: can this SDK read the format at all? Every check below rebuilds
320
+ // signed bytes from the version, so an unreadable version makes all of them
321
+ // meaningless - and "unknown issuer key" would be the wrong thing to tell
322
+ // someone holding a record that is merely newer than this library.
323
+ checkFormatVersion(certificate.certificate_format_version);
102
324
  const issuer = certificate.issuer;
103
325
  const keyId = issuer.key_id;
104
326
  const pki = publicKeys.get(keyId);
105
327
  if (pki === undefined)
106
328
  throw new VerificationError(`unknown issuer key: ${keyId}`);
107
- if (pki.revoked)
108
- throw new VerificationError(`issuer key is revoked: ${keyId}`);
329
+ // The key check runs before any signature check: a signature verified
330
+ // against a key nobody vouches for proves nothing about the issuer.
331
+ const keyVerdict = requireUsableKey(pki, await verifiedCertificateAnchor(crypto, certificate, pki), keyId);
109
332
  // 1. Certificate signature FIRST — nothing below may trust a field until the
110
333
  // bytes carrying it are covered by a verified signature.
111
334
  const certPayload = buildCertificatePayload(certificate);
@@ -122,7 +345,7 @@ export async function verifyCertificate(crypto, certificate, publicKeys) {
122
345
  proof_mode: att.proof_mode,
123
346
  attested_at: att.attested_at,
124
347
  systems: attestationSystems(certificate),
125
- });
348
+ }, certificate.certificate_format_version);
126
349
  const attSig = decodeSignature(att.attestation_signature);
127
350
  if (!(await crypto.ed25519Verify(pki.keyBytes, attPayload, attSig))) {
128
351
  throw new VerificationError("attestation signature is invalid");
@@ -172,7 +395,10 @@ export async function verifyCertificate(crypto, certificate, publicKeys) {
172
395
  if (!anyAttested) {
173
396
  throw new VerificationError("incomplete verification: no system held any records at attestation time");
174
397
  }
175
- return "VALID";
398
+ // A soft key verdict is the result, not a footnote: VALID_KEY_WINDOW_UNKNOWN
399
+ // and VALID_KEY_COMPROMISED_LATER mean this document still needs a human, and
400
+ // reporting VALID here would be the lying by omission ADR-017 exists to stop.
401
+ return keyVerdict;
176
402
  }
177
403
  const NIL_UUID = "00000000-0000-0000-0000-000000000000";
178
404
  /** Verify the transparency proof embedded in a certificate. */
@@ -187,8 +413,8 @@ export async function verifyTransparency(crypto, certificate, publicKeys) {
187
413
  const pki = publicKeys.get(keyId);
188
414
  if (pki === undefined)
189
415
  throw new VerificationError(`unknown issuer key: ${keyId}`);
190
- if (pki.revoked)
191
- throw new VerificationError(`issuer key is revoked: ${keyId}`);
416
+ // The tree head carries its own timestamp, so this path always has an anchor.
417
+ requireUsableKey(pki, certificateAnchor(certificate), keyId);
192
418
  // 1. Tree head signature
193
419
  const headPayload = buildTreeHeadPayload(sth);
194
420
  const headSig = decodeSignature(sth.signature);
@@ -457,7 +683,7 @@ function civilFromDays(days) {
457
683
  // ---------------------------------------------------------------------------
458
684
  // Payload builders — exact ports of core/payload.go
459
685
  // ---------------------------------------------------------------------------
460
- function buildAttestationPayload(subject, att) {
686
+ function buildAttestationPayload(subject, att, certFormatVersion) {
461
687
  const attestedAt = formatTimestamp(att.attested_at);
462
688
  const systems = att.systems.map((s) => ({
463
689
  canonical_version: s.canonical_version ?? null,
@@ -475,13 +701,38 @@ function buildAttestationPayload(subject, att) {
475
701
  }));
476
702
  const payload = {
477
703
  attested_at: attestedAt,
478
- payload_type: PAYLOAD_TYPE_ATTESTATION,
704
+ payload_type: attestationPayloadType(certFormatVersion),
479
705
  proof_mode: att.proof_mode,
480
706
  subject_hash: toHex(subject.identifier_hash),
481
707
  systems,
482
708
  };
483
709
  return canonicalJson(payload);
484
710
  }
711
+ /** Domain separator for the record itself, by certificate format version. */
712
+ function certificatePayloadType(version) {
713
+ checkFormatVersion(version);
714
+ if (version === FORMAT_VERSION_V6)
715
+ return PAYLOAD_TYPE_VERIFICATION_RECORD_V6;
716
+ if (version === FORMAT_VERSION_V5)
717
+ return PAYLOAD_TYPE_CERTIFICATE_V5;
718
+ if (version === FORMAT_VERSION_V4)
719
+ return PAYLOAD_TYPE_CERTIFICATE_V4;
720
+ return PAYLOAD_TYPE_CERTIFICATE;
721
+ }
722
+ /**
723
+ * Domain separator for the attestation carried by a record of that format. It
724
+ * follows the record's version because a verifier rebuilds the attestation
725
+ * payload from the record it is checking, and must reproduce the exact bytes
726
+ * the enclave signed.
727
+ */
728
+ function attestationPayloadType(version) {
729
+ checkFormatVersion(version);
730
+ if (version === FORMAT_VERSION_V6)
731
+ return PAYLOAD_TYPE_ATTESTATION_V6;
732
+ if (version === FORMAT_VERSION_V5)
733
+ return PAYLOAD_TYPE_ATTESTATION_V5;
734
+ return PAYLOAD_TYPE_ATTESTATION;
735
+ }
485
736
  function buildCertificatePayload(cert) {
486
737
  const att = cert.attestation;
487
738
  const issuer = cert.issuer;
@@ -509,15 +760,36 @@ function buildCertificatePayload(cert) {
509
760
  attested_at: formatTimestamp(att.attested_at),
510
761
  proof_mode: att.proof_mode,
511
762
  };
763
+ // Fields added after v3 are gated on the certificate's OWN version. A v3
764
+ // certificate must reconstruct to the same bytes forever; reading these
765
+ // unconditionally would break every certificate already issued.
766
+ const version = cert.certificate_format_version;
767
+ // v6 carries v5's shape exactly, so it takes every gate v5 takes. Naming
768
+ // these "Plus" rather than testing equality at each use is what stops a new
769
+ // version from silently missing one.
770
+ const isV5Plus = version === FORMAT_VERSION_V5 || version === FORMAT_VERSION_V6;
771
+ const isV4Plus = version === FORMAT_VERSION_V4 || isV5Plus;
512
772
  const issuerObj = {
513
773
  key_id: issuer.key_id,
514
774
  name: issuer.name,
515
775
  public_key: toHex(issuer.public_key),
516
776
  };
777
+ if (isV4Plus && typeof issuer.legal_entity === "string" && issuer.legal_entity !== "") {
778
+ issuerObj.legal_entity = issuer.legal_entity;
779
+ }
780
+ // Signed from v5, and omitted when absent or empty: a build with no
781
+ // measurement signs none, and "" is a value a reader could mistake for one.
782
+ if (isV5Plus && typeof issuer.enclave_pcr0 === "string" && issuer.enclave_pcr0 !== "") {
783
+ issuerObj.enclave_pcr0 = issuer.enclave_pcr0;
784
+ }
785
+ // v5 drops identifier_type_hint: signed, but always the constant "custom",
786
+ // so it was never evidence. v3 and v4 keep it or they stop verifying.
517
787
  const subjectObj = {
518
788
  identifier_hash: toHex(subject.identifier_hash),
519
- identifier_type_hint: subject.identifier_type_hint,
520
789
  };
790
+ if (!isV5Plus) {
791
+ subjectObj.identifier_type_hint = subject.identifier_type_hint;
792
+ }
521
793
  // status and revocation are deliberately absent (ADR-016 §3): a signature
522
794
  // commits to bytes at an instant, revocation is discovered later, so it
523
795
  // travels as a separate short-lived signed status statement.
@@ -528,10 +800,17 @@ function buildCertificatePayload(cert) {
528
800
  certificate_id: cert.certificate_id,
529
801
  issued_at: formatTimestamp(cert.issued_at),
530
802
  issuer: issuerObj,
531
- payload_type: PAYLOAD_TYPE_CERTIFICATE,
803
+ payload_type: certificatePayloadType(version),
532
804
  subject: subjectObj,
533
805
  systems,
534
806
  };
807
+ if (isV4Plus && cert.scope != null) {
808
+ const scope = cert.scope;
809
+ payload.scope = {
810
+ text_sha256: toHex(scope.text_sha256),
811
+ version: scope.version,
812
+ };
813
+ }
535
814
  return canonicalJson(payload);
536
815
  }
537
816
  /** Port of Go's BuildLogLeafPayload (ADR-016 §4). */
@@ -742,28 +1021,43 @@ export const VALID_REVOCATION_UNKNOWN = "VALID_REVOCATION_UNKNOWN";
742
1021
  * | input | result |
743
1022
  * |---|---|
744
1023
  * | fresh statement, REVOKED | throws VerificationError |
745
- * | fresh statement, ACTIVE | `"VALID"` |
746
- * | absent or expired statement | `"VALID_REVOCATION_UNKNOWN"` |
1024
+ * | fresh statement, ACTIVE | `"VALID"`, or a soft verdict on either key |
1025
+ * | absent or expired statement | `"VALID_REVOCATION_UNKNOWN"`, or the certificate key's soft verdict |
1026
+ * | statement key unusable (ADR-017) | throws VerificationError |
747
1027
  *
748
- * The last row is the point: `verifyCertificate` answers VALID there, which
1028
+ * The third row is the point: `verifyCertificate` answers VALID there, which
749
1029
  * reads as "not revoked" and is not something it checked.
750
1030
  */
751
1031
  export async function verifyCertificateWithStatus(crypto, certificate, publicKeys, status, now) {
1032
+ // A refusal throws out of verifyCertificate, so `base` is VALID or one of the
1033
+ // two soft verdicts. Returning a soft verdict here skipped every check below
1034
+ // it: a revoked certificate signed by a compromised-later key answered
1035
+ // VALID_KEY_COMPROMISED_LATER and its statement was never authenticated. The
1036
+ // verdict is held instead, and resolved against what the statement says.
752
1037
  const base = await verifyCertificate(crypto, certificate, publicKeys);
753
- if (base !== "VALID") {
754
- return base;
755
- }
756
1038
  if (status == null) {
757
- return VALID_REVOCATION_UNKNOWN;
1039
+ return resolveWithBase(base, VALID_REVOCATION_UNKNOWN);
758
1040
  }
759
1041
  const keyId = status.key_id;
760
1042
  const info = keyId ? publicKeys.get(keyId) : undefined;
761
1043
  if (!info) {
762
1044
  throw new VerificationError(`status statement signed by unknown key: ${keyId}`);
763
1045
  }
764
- if (info.revoked) {
765
- throw new VerificationError("status statement signed by a revoked key");
766
- }
1046
+ // The whole ADR-017 table, not just the deprecated boolean. `revoked` is
1047
+ // never set by a key loaded from /.well-known/burnledger-keys — that endpoint
1048
+ // publishes key_status — so this check read `false` for a key the issuer had
1049
+ // declared COMPROMISED, and whoever held that key could mint an ACTIVE
1050
+ // statement for a revoked certificate that every SDK and the browser bundle
1051
+ // rendered as a signed "not revoked". core.VerifyCertificateWithStatus has
1052
+ // always evaluated it.
1053
+ //
1054
+ // The anchor is the verifier's own clock, not a field inside the statement: a
1055
+ // statement must already be inside its own validity window to be served at
1056
+ // all, and `now` is a time this verifier observed rather than one the signing
1057
+ // key asserted about itself.
1058
+ const when = now ?? new Date();
1059
+ const at = formatTimestamp(when.toISOString());
1060
+ const statusKeyVerdict = requireUsableKey(info, parseRfc3339(at), keyId, "status statement key");
767
1061
  const payload = buildCertificateStatusPayload(status);
768
1062
  // decodeSignature, not hexToBytes: signatures arrive as hex, base64 or a byte
769
1063
  // array depending on the producer, and every other signature on this path
@@ -774,24 +1068,59 @@ export async function verifyCertificateWithStatus(crypto, certificate, publicKey
774
1068
  }
775
1069
  // A validly signed statement about a different certificate must not be
776
1070
  // allowed to speak for this one.
777
- const certId = certificate.id ??
778
- certificate.certificate_id;
1071
+ //
1072
+ // Compared against `certificate_id` and nothing else. This used to fall back
1073
+ // through `certificate.id`, which the signature does not cover:
1074
+ // buildCertificatePayload (core/payload.go) enumerates nine keys and `id` is
1075
+ // not among them, and no verifier rejects unknown top-level keys. So a holder
1076
+ // of a REVOKED certificate could add `"id": "<some other certificate>"`,
1077
+ // staple that certificate's ACTIVE status statement — served unauthenticated
1078
+ // by design — and this check would compare the two things the attacker chose.
1079
+ // Every signature still verified; the binding was the only forged part.
1080
+ // core/verify.go never had the fallback, so Go said STATUS_STATEMENT_MISMATCH
1081
+ // while both SDKs and the browser bundle said VALID.
1082
+ const certId = certificate.certificate_id;
779
1083
  if (status.certificate_id !== certId) {
780
1084
  throw new VerificationError("status statement is about a different certificate");
781
1085
  }
782
1086
  // Compared in the normalized form the payload signs, so the freshness check
783
1087
  // cannot disagree with what was signed about the same two instants.
784
- const at = formatTimestamp((now ?? new Date()).toISOString());
785
1088
  const issued = formatTimestamp(status.statement_issued_at);
786
1089
  const expires = formatTimestamp(status.statement_expires_at);
787
1090
  if (at < issued || at >= expires) {
788
1091
  // Stale is not a weaker answer, it is no answer — including for a REVOKED
789
1092
  // statement, which must never decay into VALID.
790
- return VALID_REVOCATION_UNKNOWN;
1093
+ return resolveWithBase(base, VALID_REVOCATION_UNKNOWN);
791
1094
  }
792
1095
  if (status.status === "REVOKED") {
793
- throw new VerificationError("certificate has been revoked");
1096
+ // Coded, because this is the only status-path rejection that is a fact
1097
+ // about the certificate rather than a failure to authenticate the
1098
+ // statement. A caller that cannot tell the two apart will render an
1099
+ // unsigned statement as a revocation.
1100
+ throw new VerificationError("certificate has been revoked", CERTIFICATE_REVOKED);
794
1101
  }
795
- return "VALID";
1102
+ // A state this SDK does not recognize is NOT active. This tested only for
1103
+ // REVOKED and treated everything else as a pass, so a signed, fresh,
1104
+ // correctly-bound statement saying "SUSPENDED" — or "revoked" in the wrong
1105
+ // case — was reported as revocation CHECKED AND PASSED. Mirrors
1106
+ // core.ValidCertificateStatusValue.
1107
+ if (!KNOWN_STATUS_VALUES.has(status.status)) {
1108
+ return resolveWithBase(base, VALID_REVOCATION_UNKNOWN);
1109
+ }
1110
+ // A soft verdict on the status key is the result, not a footnote — but it is
1111
+ // reported only once the statement has authenticated and been read, so it can
1112
+ // neither speak for an unverified statement nor suppress a revocation.
1113
+ return resolveWithBase(base, statusKeyVerdict);
1114
+ }
1115
+ /**
1116
+ * Picks what to report when the certificate's own key returned a soft verdict
1117
+ * and the status path also has something to say. A doubt about the key that
1118
+ * signed the certificate questions the whole document, so it outranks a doubt
1119
+ * about revocation or about the statement's own key; only one verdict string
1120
+ * comes back, and this is the one a reader most needs. Mirrors
1121
+ * core.resolveWithBase.
1122
+ */
1123
+ function resolveWithBase(base, verdict) {
1124
+ return base !== "VALID" ? base : verdict;
796
1125
  }
797
1126
  //# sourceMappingURL=verify.js.map