@blamejs/pki 0.1.26 → 0.1.28

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.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,33 @@ All notable changes to `@blamejs/pki` are documented here. The format
4
4
  follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); this
5
5
  project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
+ ## v0.1.28 — 2026-07-11
8
+
9
+ Merkle transparency proof verification joins the toolkit as pki.merkle.
10
+
11
+ ### Added
12
+
13
+ - pki.merkle.leafHash / nodeHash / emptyRootHash -- the RFC 6962 / RFC 9162 tree hashes: a leaf is SHA-256(0x00 || entry), an interior node is SHA-256(0x01 || left || right), the empty tree is SHA-256(""). The domain-separation prefixes are applied unconditionally.
14
+ - pki.merkle.verifyInclusion({ leafIndex, treeSize, leafHash, proof, rootHash }) -- verify an RFC 6962 / RFC 9162 audit proof by folding the leaf up the audit path and constant-time-comparing the reconstructed root to a trusted root. Returns true only when the proof binds the leaf to the root.
15
+ - pki.merkle.verifyConsistency({ oldSize, newSize, oldRoot, newRoot, proof }) -- verify an append-only consistency proof by reconstructing BOTH the old and the new root and constant-time-comparing each; the append-only guarantee lives in the old-root leg.
16
+ - The error taxonomy gains MerkleError (merkle/*). A node-count ceiling (C.LIMITS.MERKLE_MAX_PROOF_NODES) rejects a pathologically long proof before any hashing; the precise per-proof guard is the geometry check in each verifier.
17
+ - Fuzz target merkle-verify (both fold algorithms and the hash producers over adversarial coordinates, hashes, and proofs) joins the per-PR and nightly fuzz matrices with a seed corpus.
18
+
19
+ ## v0.1.27 — 2026-07-10
20
+
21
+ A strict deterministic-CBOR codec joins the toolkit as pki.cbor.
22
+
23
+ ### Added
24
+
25
+ - pki.cbor.decode -- the RFC 8949 core-deterministic CBOR decoder. It returns a node carrying the major type, the argument (a lossless BigInt), zero-copy content / bytes views (the raw ranges an external verifier hashes), and children (array elements, ordered map key/value pairs, or a tag's one inner item). Every non-canonical shape fails closed with a stable cbor/* code; maxBytes, maxDepth, maxItems, and a per-bignum byte cap bound the work before allocation (so a container declaring millions of tiny elements fails closed rather than exhausting memory); allowTrailing decodes the first item of a CBOR Sequence.
26
+ - The read.* leaf readers over a decoded node: read.uint / read.nint / read.int (uniform BigInt), read.byteString (zero-copy Buffer), read.textString (strict UTF-8), read.array, read.map (ordered key/value node pairs), read.boolean / read.nullValue / read.undefinedValue, read.float (half / single / double), and the tagged forms read.biguint (RFC 8949 tag 2 unsigned bignum, minimality and byte cap enforced), read.time (RFC 8949 tag 1 epoch time, bounded to the valid Date range), and read.oid (RFC 9090 tag 111, decoded through the shared OID-content codec so a malformed body surfaces the existing oid/* codes).
27
+ - The error taxonomy gains CborError (cbor/*). The decoder is profile-parameterized, so a future CTAP2 canonical profile is a data addition rather than a new code path.
28
+ - Fuzz target cbor-det-parse (the decode head-well-formedness and minimal-argument checks, the map ordering / uniqueness verify, the shortest-float rule, the strict-UTF-8 gate, and the size / depth / bignum caps, in both whole-buffer and CBOR-Sequence modes) joins the per-PR and nightly fuzz matrices with a seed corpus.
29
+
30
+ ### Changed
31
+
32
+ - pki.oid.paramsMustBeAbsent graduated from experimental to stable: its dotted-OID-to-boolean surface has been unchanged since 0.1.21 and is exercised end-to-end by the algorithm-identifier decoder every format shares.
33
+
7
34
  ## v0.1.26 — 2026-07-10
8
35
 
9
36
  Test-coverage measurement and the OpenSSF Best Practices badge.
package/README.md CHANGED
@@ -199,6 +199,7 @@ is callable today; nothing below is a stub.
199
199
  | Namespace | What it does |
200
200
  |---|---|
201
201
  | `pki.asn1` | Strict, bounded DER codec — `decode` (zero-copy node tree), `encode`, `build.*` canonical-DER value builders, `read.*` typed readers, `TAGS`, OID-content encode/decode |
202
+ | `pki.cbor` | Strict, bounded RFC 8949 deterministic CBOR codec — `decode` (zero-copy node tree) + `read.*` typed leaf readers, fail-closed on every non-canonical shape (indefinite length, non-minimal argument, unsorted / duplicate map keys, non-shortest float, trailing bytes) |
202
203
  | `pki.oid` | Two-way OID ↔ name registry — `name`, `byName`, `register`, `toArcs`/`fromArcs`, `toDER`/`fromDER`; seeded with RFC 5280 + NIST PQC arcs |
203
204
  | `pki.webcrypto` | A W3C WebCrypto (`SubtleCrypto`) engine over `node:crypto` — `sign`/`verify`/`encrypt`/`decrypt`/`deriveBits`/`digest`/`generateKey`/`importKey`/`exportKey` across RSA, ECDSA, ECDH, Ed25519/Ed448, AES, HMAC, HKDF, PBKDF2, SHA — **and** post-quantum ML-DSA-44/65/87 and SLH-DSA signatures, plus ML-KEM-512/768/1024 key generation (KEM encapsulation on the roadmap). Zero-dependency, OpenSSL-interoperable |
204
205
  | `pki.schema` | The schema family — `parse` detects which PKI format DER / PEM encodes and routes to the right parser, `all` enumerates the registered formats, and the engine + per-format members are grouped here |
@@ -221,6 +222,7 @@ is callable today; nothing below is a stub.
221
222
  | `pki.schema.engine` | The declarative ASN.1 structure-schema engine every format parser composes — `walk` / `encode` / `embeddedDer` plus the schema combinators |
222
223
  | `pki.path` | RFC 5280 §6 certification-path validation — `validate` runs the §6.1 state machine (signature chaining, validity windows, name chaining, basic constraints and path length, key usage, name constraints, the certificate-policy tree) over an ordered path and a trust anchor, returning a structured verdict with per-check reason codes; `crlChecker` supplies CRL-based revocation. Pure and re-entrant, fail-closed — `validate`, `crlChecker` |
223
224
  | `pki.ct` | Parse RFC 6962 Certificate Transparency SCT lists — `parseSctList` decodes the `SignedCertificateTimestampList` a certificate or OCSP response carries in the SCT extension (a TLS-presentation-language payload inside the §3.3 double DER wrap) into per-SCT log id, exact `timestamp` (BigInt), named signature algorithm, and raw signature; `reconstructSignedData` rebuilds the exact `digitally-signed` preimage for external verification. Structure decoded, crypto surfaced raw, bounded decode, fail-closed — `parseSctList`, `reconstructSignedData` |
225
+ | `pki.merkle` | RFC 6962 / RFC 9162 Merkle-tree proof verification — `leafHash` / `nodeHash` / `emptyRootHash` build the domain-separated (0x00 leaf / 0x01 node) SHA-256 tree hashes; `verifyInclusion` folds an audit proof back to a root and `verifyConsistency` reconstructs both the old and new root (the append-only guarantee), each constant-time-compared to a trusted checkpoint root. Fail-closed on bad geometry, sync hashing, transport-free — `leafHash`, `nodeHash`, `emptyRootHash`, `verifyInclusion`, `verifyConsistency` |
224
226
  | `pki.C` / `pki.constants` | Version-stable constants — functional scale helpers (`C.TIME.*`, `C.BYTES.*`), codec `LIMITS`, `version` |
225
227
  | `pki.errors` | The `PkiError` taxonomy — `defineClass` plus `ConstantsError` / `Asn1Error` / `OidError` / `PemError` / `CertificateError` / `CrlError` / `CsrError` / `Pkcs8Error` / `CmsError` / `OcspError` / `TspError` / `AttrCertError` / `CrmfError` / `Pkcs12Error` / `CmpError` / `PathError` / `CtError` / `JoseError` / `AcmeError`, each carrying a stable `code` in `domain/reason` form |
226
228
  | `pki` CLI | `pki version`, `pki oid <dotted\|name>`, `pki parse <cert>` |
package/index.js CHANGED
@@ -30,11 +30,13 @@
30
30
  var constants = require("./lib/constants");
31
31
  var errors = require("./lib/framework-error");
32
32
  var asn1 = require("./lib/asn1-der");
33
+ var cbor = require("./lib/cbor-det");
33
34
  var oid = require("./lib/oid");
34
35
  var webcrypto = require("./lib/webcrypto");
35
36
  var schema = require("./lib/schema-all");
36
37
  var path = require("./lib/path-validate");
37
38
  var ct = require("./lib/ct");
39
+ var merkle = require("./lib/merkle");
38
40
  var est = require("./lib/est");
39
41
  var jose = require("./lib/jose");
40
42
  var acme = require("./lib/acme");
@@ -47,6 +49,12 @@ module.exports = {
47
49
  errors: errors,
48
50
  // `asn1` is the strict DER codec (decode/encode/build/read/TAGS).
49
51
  asn1: asn1,
52
+ // `cbor` is the strict, fail-closed RFC 8949 deterministic CBOR codec
53
+ // (decode + read.* leaf readers), sibling to `asn1`. It rejects every
54
+ // non-deterministic shape -- indefinite length, a non-minimal argument,
55
+ // unsorted / duplicate map keys, a non-shortest float, trailing bytes --
56
+ // before it walks a byte, and surfaces zero-copy bytes / content views.
57
+ cbor: cbor,
50
58
  oid: oid,
51
59
  // `schema` is the family: the L2 structure-schema engine (schema.engine) and
52
60
  // the per-format parsers (schema.x509, ...) with detect-and-route schema.parse.
@@ -58,6 +66,12 @@ module.exports = {
58
66
  // SCT-list extension a certificate / OCSP response carries; the signature is
59
67
  // surfaced raw for external verification (pki.ct.reconstructSignedData).
60
68
  ct: ct,
69
+ // `merkle` is the RFC 6962 / RFC 9162 Merkle-tree proof-verification core --
70
+ // pki.merkle.leafHash / nodeHash / emptyRootHash build the domain-separated
71
+ // (0x00 leaf / 0x01 node) SHA-256 tree hashes; pki.merkle.verifyInclusion and
72
+ // verifyConsistency fold an audit / consistency proof and constant-time-
73
+ // compare to a checkpoint root. Pure sync hashing, fail-closed, transport-free.
74
+ merkle: merkle,
61
75
  // `est` is RFC 7030 / 8951 / 9908 Enrollment over Secure Transport -- the
62
76
  // transport-agnostic client codecs (base64 transfer, multipart splitter),
63
77
  // certs-only + serverkeygen validators over CMS, the enroll-attribute builders,
package/lib/asn1-der.js CHANGED
@@ -168,6 +168,9 @@ function decode(input, opts) {
168
168
  var buf = _asBuffer(input, "decode");
169
169
  var maxBytes = _capOpt(opts.maxBytes, "maxBytes", constants.LIMITS.DER_MAX_BYTES);
170
170
  var maxDepth = _capOpt(opts.maxDepth, "maxDepth", constants.LIMITS.DER_MAX_DEPTH);
171
+ if (maxDepth > constants.LIMITS.MAX_DECODE_DEPTH_CEILING) {
172
+ throw new TypeError("decode: maxDepth " + maxDepth + " exceeds the stack-safe ceiling " + constants.LIMITS.MAX_DECODE_DEPTH_CEILING);
173
+ }
171
174
  if (buf.length > maxBytes) {
172
175
  throw new Asn1Error("asn1/too-large", "input " + buf.length + " bytes exceeds cap " + maxBytes);
173
176
  }
@@ -0,0 +1,611 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // Copyright (c) blamejs contributors
3
+ "use strict";
4
+ /**
5
+ * @module pki.cbor
6
+ * @nav Core
7
+ * @title CBOR (deterministic)
8
+ * @order 35
9
+ * @slug cbor
10
+ *
11
+ * @intro
12
+ * A strict, fail-closed decoder for RFC 8949 Concise Binary Object
13
+ * Representation, restricted to the Core Deterministic Encoding profile
14
+ * (RFC 8949 sec. 4.2). It is the binary sibling of `pki.asn1`: an in-house
15
+ * codec that owns its stack, rejects every non-canonical shape before it
16
+ * returns a value, and bounds size and depth before it walks a byte.
17
+ *
18
+ * Anything a lenient CBOR reader would tolerate -- an indefinite-length
19
+ * item, a non-minimal ("preferred") integer / length / tag argument, a
20
+ * non-shortest or non-canonical-NaN float, out-of-order or duplicate map
21
+ * keys, ill-formed UTF-8, or trailing bytes -- is a permanent `CborError`
22
+ * here, because deterministic CBOR is a canonical encoding and a producer
23
+ * that violates it produced invalid bytes. There is no lenient mode.
24
+ *
25
+ * `decode` returns a navigable node tree with zero-copy `bytes` / `content`
26
+ * views (the raw ranges an external verifier hashes); the `read.*` leaf
27
+ * readers turn a node into a JS value (a `BigInt` for every integer, a
28
+ * `Buffer` for a byte string, a `Date` for an epoch time, a dotted string
29
+ * for a tagged OID). It is the primitive the CBOR-encoded PKI surfaces
30
+ * (C509 certificates, COSE / CWT) will compose.
31
+ *
32
+ * @card
33
+ * `decode` (bounded, fail-closed, deterministic-only) + the `read.*` typed
34
+ * leaf readers over its node tree.
35
+ */
36
+
37
+ var constants = require("./constants");
38
+ var frameworkError = require("./framework-error");
39
+ var asn1 = require("./asn1-der");
40
+
41
+ var CborError = frameworkError.CborError;
42
+
43
+ // Reusable little-work scratch for IEEE-754 float decode + the shortest-form
44
+ // round-trip checks. Single-threaded synchronous use, reset on every write.
45
+ var _fbuf = new ArrayBuffer(8);
46
+ var _fdv = new DataView(_fbuf);
47
+
48
+ // Strict UTF-8 validator (fatal); a lone continuation / truncated sequence
49
+ // throws instead of substituting U+FFFD -- the fail-open class the toolkit
50
+ // refuses, mirroring asn1-der's strict text decode.
51
+ var _utf8 = new TextDecoder("utf-8", { fatal: true });
52
+
53
+ // The ECMAScript Date-valid window is +/- 8,640,000,000,000,000 ms; in seconds
54
+ // that is +/- 8.64e12. read.time bounds an epoch value to this window BEFORE
55
+ // narrowing the BigInt to a Number, so the narrowing is lossless (well under
56
+ // 2^53) and the millisecond result stays a safe integer.
57
+ var _MAX_EPOCH_SECONDS = 8640000000000n;
58
+
59
+ function _asBuffer(input, who) {
60
+ if (Buffer.isBuffer(input)) return input;
61
+ if (input instanceof Uint8Array) return Buffer.from(input.buffer, input.byteOffset, input.byteLength);
62
+ throw new CborError("cbor/not-buffer", who + ": expected a Buffer / Uint8Array");
63
+ }
64
+
65
+ // Config-time cap validation: a bad cap is an authoring bug, so it throws a
66
+ // TypeError (not a CborError) exactly like asn1-der's _capOpt.
67
+ function _capOpt(v, key, dflt) {
68
+ if (v === undefined) return dflt;
69
+ if (typeof v !== "number" || !Number.isInteger(v) || v < 0) {
70
+ throw new TypeError("decode: " + key + " must be a non-negative integer");
71
+ }
72
+ return v;
73
+ }
74
+
75
+ // Determinism profile -> a ruleset. v1 whitelists only "deterministic"
76
+ // (RFC 8949 sec. 4.2 core); an unknown profile is a config-time TypeError, so
77
+ // there is no path to a lenient decode. A future "ctap2" (length-first map
78
+ // ordering) is a data row here, not a switch through the decoder.
79
+ function _profile(name) {
80
+ var p = name === undefined ? "deterministic" : name;
81
+ if (p === "deterministic") return { mapKeyCompare: Buffer.compare };
82
+ throw new TypeError("decode: unknown profile " + JSON.stringify(p) + " (only \"deterministic\" is supported)");
83
+ }
84
+
85
+ function _assertUtf8(buf) {
86
+ try {
87
+ _utf8.decode(buf);
88
+ } catch (_e) {
89
+ throw new CborError("cbor/bad-utf8", "text string is not well-formed UTF-8");
90
+ }
91
+ }
92
+
93
+ // Decode the raw IEEE-754 bits (carried in the argument) to a JS double, per
94
+ // the additional-info width: 25 half, 26 single, 27 double.
95
+ function _readFloatBits(argument, ai) {
96
+ if (ai === 25) { _fdv.setUint16(0, Number(argument)); return _fdv.getFloat16(0); }
97
+ if (ai === 26) { _fdv.setUint32(0, Number(argument)); return _fdv.getFloat32(0); }
98
+ _fdv.setBigUint64(0, argument); return _fdv.getFloat64(0);
99
+ }
100
+
101
+ // Is d exactly representable as a half float? (round-trip through float16;
102
+ // Object.is keeps -0 and the NaN/Inf cases honest).
103
+ function _halfRoundtrips(d) {
104
+ _fdv.setFloat16(0, d);
105
+ return Object.is(_fdv.getFloat16(0), d);
106
+ }
107
+
108
+ // Is d exactly representable as a single float?
109
+ function _singleRoundtrips(d) {
110
+ return Object.is(Math.fround(d), d);
111
+ }
112
+
113
+ // Recursive-descent decode of one CBOR item over [start, limit): fail-closed,
114
+ // depth-budgeted, zero-copy. Mirrors asn1-der's _decodeTLV.
115
+ function _decodeItem(buf, start, limit, depth, maxD, rules, state) {
116
+ if (depth > maxD) throw new CborError("cbor/too-deep", "nesting exceeds depth cap " + maxD);
117
+ state.n += 1;
118
+ if (state.n > state.max) throw new CborError("cbor/too-many-items", "decoded item count exceeds cap " + state.max);
119
+ var p = start;
120
+ if (p >= limit) throw new CborError("cbor/truncated", "expected an initial byte");
121
+ var ib = buf[p]; p += 1;
122
+ var mt = ib >> 5;
123
+ var ai = ib & 0x1f;
124
+
125
+ // Head well-formedness, before reading the argument.
126
+ if (ib === 0xff) throw new CborError("cbor/unexpected-break", "stray break byte (0xff) at an item head");
127
+ if (ai === 28 || ai === 29 || ai === 30) throw new CborError("cbor/reserved-ai", "reserved additional-info " + ai);
128
+ if (ai === 31) {
129
+ if (mt === 2 || mt === 3 || mt === 4 || mt === 5) {
130
+ throw new CborError("cbor/indefinite-length", "indefinite-length items are not valid deterministic CBOR");
131
+ }
132
+ throw new CborError("cbor/reserved-ai", "additional-info 31 is undefined for major type " + mt);
133
+ }
134
+
135
+ // The argument, minimal-checked, as a lossless BigInt.
136
+ var argument;
137
+ var contentStart;
138
+ if (ai <= 23) {
139
+ argument = BigInt(ai);
140
+ contentStart = p;
141
+ } else {
142
+ var nBytes = ai === 24 ? 1 : ai === 25 ? 2 : ai === 26 ? 4 : 8;
143
+ if (p + nBytes > limit) throw new CborError("cbor/truncated", "truncated " + nBytes + "-byte argument");
144
+ if (nBytes === 1) argument = BigInt(buf[p]);
145
+ else if (nBytes === 2) argument = BigInt(buf.readUInt16BE(p));
146
+ else if (nBytes === 4) argument = BigInt(buf.readUInt32BE(p));
147
+ else argument = buf.readBigUInt64BE(p);
148
+ p += nBytes;
149
+ // Preferred (shortest) argument -- for every major type but 7, where the
150
+ // "argument" is raw float bits governed by the shortest-float rule instead.
151
+ if (mt !== 7) {
152
+ var minForWidth = nBytes === 1 ? 24n : nBytes === 2 ? 256n : nBytes === 4 ? 65536n : 4294967296n;
153
+ if (argument < minForWidth) {
154
+ throw new CborError("cbor/non-minimal-argument", "argument " + argument + " is not in the shortest head form");
155
+ }
156
+ }
157
+ contentStart = p;
158
+ }
159
+
160
+ var content = null;
161
+ var children = null;
162
+ var end;
163
+
164
+ if (mt === 0 || mt === 1) {
165
+ end = contentStart;
166
+ } else if (mt === 2 || mt === 3) {
167
+ var len = Number(argument);
168
+ if (contentStart + len > limit) throw new CborError("cbor/truncated", "string content overruns the buffer");
169
+ content = buf.subarray(contentStart, contentStart + len);
170
+ if (mt === 3) _assertUtf8(content);
171
+ end = contentStart + len;
172
+ } else if (mt === 4) {
173
+ children = [];
174
+ var cp = contentStart;
175
+ for (var i = 0n; i < argument; i++) {
176
+ var el = _decodeItem(buf, cp, limit, depth + 1, maxD, rules, state);
177
+ children.push(el.node);
178
+ cp = el.end;
179
+ }
180
+ end = cp;
181
+ } else if (mt === 5) {
182
+ children = [];
183
+ var mp = contentStart;
184
+ for (var j = 0n; j < argument; j++) {
185
+ var k = _decodeItem(buf, mp, limit, depth + 1, maxD, rules, state);
186
+ mp = k.end;
187
+ var v = _decodeItem(buf, mp, limit, depth + 1, maxD, rules, state);
188
+ mp = v.end;
189
+ if (children.length > 0) {
190
+ var cmp = rules.mapKeyCompare(children[children.length - 1][0].bytes, k.node.bytes);
191
+ if (cmp > 0) throw new CborError("cbor/unsorted-map-keys", "map keys are not in bytewise-ascending order");
192
+ if (cmp === 0) throw new CborError("cbor/duplicate-map-key", "duplicate map key");
193
+ }
194
+ children.push([k.node, v.node]);
195
+ }
196
+ end = mp;
197
+ } else if (mt === 6) {
198
+ var inner = _decodeItem(buf, contentStart, limit, depth + 1, maxD, rules, state);
199
+ children = [inner.node];
200
+ end = inner.end;
201
+ } else {
202
+ // mt === 7: simple value or float.
203
+ if (ai <= 23) {
204
+ end = contentStart;
205
+ } else if (ai === 24) {
206
+ if (argument < 32n) {
207
+ throw new CborError("cbor/reserved-simple", "simple value " + argument + " must use the single-byte immediate form");
208
+ }
209
+ end = contentStart;
210
+ } else {
211
+ var d = _readFloatBits(argument, ai);
212
+ if (Number.isNaN(d)) {
213
+ if (!(ai === 25 && argument === 0x7e00n)) {
214
+ throw new CborError("cbor/non-canonical-nan", "a NaN must be the canonical half float 0xf97e00");
215
+ }
216
+ } else if (ai === 26) {
217
+ if (_halfRoundtrips(d)) throw new CborError("cbor/non-minimal-float", "a half-representable value must use the half form");
218
+ } else if (ai === 27) {
219
+ if (_halfRoundtrips(d) || _singleRoundtrips(d)) {
220
+ throw new CborError("cbor/non-minimal-float", "a value representable in a narrower float must use it");
221
+ }
222
+ }
223
+ end = contentStart;
224
+ }
225
+ }
226
+
227
+ var node = {
228
+ majorType: mt,
229
+ ai: ai,
230
+ argument: argument,
231
+ content: content,
232
+ children: children,
233
+ header: { start: start, end: contentStart },
234
+ contentStart: contentStart,
235
+ contentEnd: end,
236
+ length: end - contentStart,
237
+ bytes: buf.subarray(start, end),
238
+ };
239
+ return { node: node, end: end };
240
+ }
241
+
242
+ /**
243
+ * @primitive pki.cbor.decode
244
+ * @signature pki.cbor.decode(bytes, opts?) -> node
245
+ * @since 0.1.27
246
+ * @status experimental
247
+ * @spec RFC 8949 sec. 4.2 (core deterministic encoding)
248
+ *
249
+ * Decode one Deterministically Encoded CBOR item into a navigable node tree.
250
+ * Every non-canonical shape is refused: an indefinite length, a non-minimal
251
+ * argument, unsorted or duplicate map keys, a non-shortest or non-canonical-
252
+ * NaN float, ill-formed UTF-8, a reserved additional-info value, or (unless
253
+ * `allowTrailing`) leftover bytes after the top-level item. Size, depth, and
254
+ * total item count are bounded, so a high-fanout container fails closed rather
255
+ * than exhausting memory. A node carries `majorType`, the `argument`
256
+ * (a lossless BigInt), a zero-copy `content` / `bytes` view, and `children`
257
+ * (array elements, map key/value pairs, or a tag's one inner item).
258
+ *
259
+ * @opts
260
+ * maxBytes: number, // default: C.LIMITS.CBOR_MAX_BYTES (16 MiB)
261
+ * maxDepth: number, // default: C.LIMITS.CBOR_MAX_DEPTH (64)
262
+ * maxItems: number, // default: C.LIMITS.CBOR_MAX_ITEMS (1,000,000 total decoded items)
263
+ * allowTrailing: boolean, // default: false -- true returns the first item and permits bytes after it (CBOR Sequence)
264
+ * profile: string, // default: "deterministic" (the only value v1 accepts)
265
+ *
266
+ * @example
267
+ * var node = pki.cbor.decode(Buffer.from("83010203", "hex"));
268
+ * node.majorType; // 4 (array)
269
+ * pki.cbor.read.uint(node.children[0]); // 1n
270
+ */
271
+ function decode(input, opts) {
272
+ opts = opts || {};
273
+ var buf = _asBuffer(input, "decode");
274
+ var maxBytes = _capOpt(opts.maxBytes, "maxBytes", constants.LIMITS.CBOR_MAX_BYTES);
275
+ var maxDepth = _capOpt(opts.maxDepth, "maxDepth", constants.LIMITS.CBOR_MAX_DEPTH);
276
+ if (maxDepth > constants.LIMITS.MAX_DECODE_DEPTH_CEILING) {
277
+ throw new TypeError("decode: maxDepth " + maxDepth + " exceeds the stack-safe ceiling " + constants.LIMITS.MAX_DECODE_DEPTH_CEILING);
278
+ }
279
+ var maxItems = _capOpt(opts.maxItems, "maxItems", constants.LIMITS.CBOR_MAX_ITEMS);
280
+ var rules = _profile(opts.profile);
281
+ if (buf.length > maxBytes) throw new CborError("cbor/too-large", "input " + buf.length + " bytes exceeds cap " + maxBytes);
282
+ var r = _decodeItem(buf, 0, buf.length, 0, maxDepth, rules, { n: 0, max: maxItems });
283
+ if (!opts.allowTrailing && r.end !== buf.length) {
284
+ throw new CborError("cbor/trailing-bytes", (buf.length - r.end) + " trailing byte(s) after the top-level item");
285
+ }
286
+ return r.node;
287
+ }
288
+
289
+ function _expectMajor(node, mt, who) {
290
+ if (!node || node.majorType !== mt) {
291
+ throw new CborError("cbor/unexpected-major", who + ": expected major type " + mt);
292
+ }
293
+ }
294
+
295
+ // Assert a tag node with a specific tag number, and return its one inner item.
296
+ function _tagInner(node, tagNum, who) {
297
+ if (!node || node.majorType !== 6 || node.argument !== BigInt(tagNum)) {
298
+ throw new CborError("cbor/unexpected-tag", who + ": expected tag " + tagNum);
299
+ }
300
+ return node.children[0];
301
+ }
302
+
303
+ /**
304
+ * @primitive pki.cbor.read.uint
305
+ * @signature pki.cbor.read.uint(node) -> 0n
306
+ * @since 0.1.27
307
+ * @status experimental
308
+ * @spec RFC 8949 sec. 3.1 (major type 0)
309
+ *
310
+ * The unsigned integer value of a major-type-0 node, as a BigInt (uniform
311
+ * for every magnitude, so the type never varies with the value). Throws
312
+ * `cbor/unexpected-major` on any other major type.
313
+ *
314
+ * @example
315
+ * pki.cbor.read.uint(node); // -> 0n
316
+ */
317
+ function readUint(node) {
318
+ _expectMajor(node, 0, "read.uint");
319
+ return node.argument;
320
+ }
321
+
322
+ /**
323
+ * @primitive pki.cbor.read.nint
324
+ * @signature pki.cbor.read.nint(node) -> -1n
325
+ * @since 0.1.27
326
+ * @status experimental
327
+ * @spec RFC 8949 sec. 3.1 (major type 1)
328
+ *
329
+ * The negative integer value of a major-type-1 node, as a BigInt
330
+ * (value = -1 - argument). Throws `cbor/unexpected-major` otherwise.
331
+ *
332
+ * @example
333
+ * pki.cbor.read.nint(node); // -> -1n
334
+ */
335
+ function readNint(node) {
336
+ _expectMajor(node, 1, "read.nint");
337
+ return -1n - node.argument;
338
+ }
339
+
340
+ /**
341
+ * @primitive pki.cbor.read.int
342
+ * @signature pki.cbor.read.int(node) -> -1n
343
+ * @since 0.1.27
344
+ * @status experimental
345
+ * @spec RFC 8949 sec. 3.1 (major types 0 and 1)
346
+ *
347
+ * The signed integer value of a major-type-0 or -1 node, as a BigInt. Throws
348
+ * `cbor/unexpected-major` on any other major type.
349
+ *
350
+ * @example
351
+ * pki.cbor.read.int(node); // -> -1n
352
+ */
353
+ function readInt(node) {
354
+ if (node && node.majorType === 0) return node.argument;
355
+ if (node && node.majorType === 1) return -1n - node.argument;
356
+ throw new CborError("cbor/unexpected-major", "read.int: expected major type 0 or 1");
357
+ }
358
+
359
+ /**
360
+ * @primitive pki.cbor.read.byteString
361
+ * @signature pki.cbor.read.byteString(node) -> Buffer
362
+ * @since 0.1.27
363
+ * @status experimental
364
+ * @spec RFC 8949 sec. 3.1 (major type 2)
365
+ *
366
+ * The zero-copy `Buffer` content of a major-type-2 byte string. Throws
367
+ * `cbor/unexpected-major` otherwise.
368
+ *
369
+ * @example
370
+ * pki.cbor.read.byteString(node); // -> <Buffer 01 02 03 04>
371
+ */
372
+ function readByteString(node) {
373
+ _expectMajor(node, 2, "read.byteString");
374
+ return node.content;
375
+ }
376
+
377
+ /**
378
+ * @primitive pki.cbor.read.textString
379
+ * @signature pki.cbor.read.textString(node) -> "text"
380
+ * @since 0.1.27
381
+ * @status experimental
382
+ * @spec RFC 8949 sec. 3.1 (major type 3)
383
+ *
384
+ * The string value of a major-type-3 text string (already validated as
385
+ * well-formed UTF-8 at decode). Throws `cbor/unexpected-major` otherwise.
386
+ *
387
+ * @example
388
+ * pki.cbor.read.textString(node); // -> "a"
389
+ */
390
+ function readTextString(node) {
391
+ _expectMajor(node, 3, "read.textString");
392
+ return node.content.toString("utf8");
393
+ }
394
+
395
+ /**
396
+ * @primitive pki.cbor.read.array
397
+ * @signature pki.cbor.read.array(node) -> [node, ...]
398
+ * @since 0.1.27
399
+ * @status experimental
400
+ * @spec RFC 8949 sec. 3.1 (major type 4)
401
+ *
402
+ * The element nodes of a major-type-4 array. Throws `cbor/unexpected-major`
403
+ * otherwise.
404
+ *
405
+ * @example
406
+ * pki.cbor.read.array(node); // -> [node, node, node]
407
+ */
408
+ function readArray(node) {
409
+ _expectMajor(node, 4, "read.array");
410
+ return node.children;
411
+ }
412
+
413
+ /**
414
+ * @primitive pki.cbor.read.map
415
+ * @signature pki.cbor.read.map(node) -> [[keyNode, valueNode], ...]
416
+ * @since 0.1.27
417
+ * @status experimental
418
+ * @spec RFC 8949 sec. 3.1 (major type 5)
419
+ *
420
+ * The ordered key/value node pairs of a major-type-5 map (ordering and
421
+ * uniqueness already enforced at decode). Throws `cbor/unexpected-major`
422
+ * otherwise.
423
+ *
424
+ * @example
425
+ * pki.cbor.read.map(node); // -> [[keyNode, valueNode]]
426
+ */
427
+ function readMap(node) {
428
+ _expectMajor(node, 5, "read.map");
429
+ return node.children;
430
+ }
431
+
432
+ /**
433
+ * @primitive pki.cbor.read.boolean
434
+ * @signature pki.cbor.read.boolean(node) -> false
435
+ * @since 0.1.27
436
+ * @status experimental
437
+ * @spec RFC 8949 sec. 3.3 (simple values 20 / 21)
438
+ *
439
+ * The boolean value of a simple-value node (false=20, true=21). Throws
440
+ * `cbor/unexpected-major` on a non-simple node, `cbor/bad-simple` on any
441
+ * other simple value.
442
+ *
443
+ * @example
444
+ * pki.cbor.read.boolean(node); // -> true
445
+ */
446
+ function readBoolean(node) {
447
+ _expectMajor(node, 7, "read.boolean");
448
+ if (node.ai === 20) return false;
449
+ if (node.ai === 21) return true;
450
+ throw new CborError("cbor/bad-simple", "read.boolean: simple value " + node.ai + " is not a boolean");
451
+ }
452
+
453
+ /**
454
+ * @primitive pki.cbor.read.nullValue
455
+ * @signature pki.cbor.read.nullValue(node) -> null
456
+ * @since 0.1.27
457
+ * @status experimental
458
+ * @spec RFC 8949 sec. 3.3 (simple value 22)
459
+ *
460
+ * `null` for a simple-value-22 node. Throws `cbor/unexpected-major` on a
461
+ * non-simple node, `cbor/bad-simple` on any other simple value.
462
+ *
463
+ * @example
464
+ * pki.cbor.read.nullValue(node); // -> null
465
+ */
466
+ function readNull(node) {
467
+ _expectMajor(node, 7, "read.nullValue");
468
+ if (node.ai !== 22) throw new CborError("cbor/bad-simple", "read.nullValue: simple value " + node.ai + " is not null");
469
+ return null;
470
+ }
471
+
472
+ /**
473
+ * @primitive pki.cbor.read.undefinedValue
474
+ * @signature pki.cbor.read.undefinedValue(node) -> undefined
475
+ * @since 0.1.27
476
+ * @status experimental
477
+ * @spec RFC 8949 sec. 3.3 (simple value 23)
478
+ *
479
+ * `undefined` for a simple-value-23 node. Throws `cbor/unexpected-major` on a
480
+ * non-simple node, `cbor/bad-simple` on any other simple value.
481
+ *
482
+ * @example
483
+ * pki.cbor.read.undefinedValue(node); // -> undefined
484
+ */
485
+ function readUndefined(node) {
486
+ _expectMajor(node, 7, "read.undefinedValue");
487
+ if (node.ai !== 23) throw new CborError("cbor/bad-simple", "read.undefinedValue: simple value " + node.ai + " is not undefined");
488
+ return undefined;
489
+ }
490
+
491
+ /**
492
+ * @primitive pki.cbor.read.float
493
+ * @signature pki.cbor.read.float(node) -> 1.5
494
+ * @since 0.1.27
495
+ * @status experimental
496
+ * @spec RFC 8949 sec. 3.1 (major type 7 floats)
497
+ *
498
+ * The JS number (double) of a half / single / double float node (the
499
+ * shortest-form and canonical-NaN rules already enforced at decode). Throws
500
+ * `cbor/unexpected-major` on a non-major-7 node, `cbor/bad-simple` on a
501
+ * simple value that is not a float.
502
+ *
503
+ * @example
504
+ * pki.cbor.read.float(node); // -> 1.5
505
+ */
506
+ function readFloat(node) {
507
+ _expectMajor(node, 7, "read.float");
508
+ if (node.ai !== 25 && node.ai !== 26 && node.ai !== 27) {
509
+ throw new CborError("cbor/bad-simple", "read.float: node is a simple value, not a float");
510
+ }
511
+ return _readFloatBits(node.argument, node.ai);
512
+ }
513
+
514
+ /**
515
+ * @primitive pki.cbor.read.biguint
516
+ * @signature pki.cbor.read.biguint(node) -> 18446744073709551616n
517
+ * @since 0.1.27
518
+ * @status experimental
519
+ * @spec RFC 8949 sec. 3.4.3 (tag 2 unsigned bignum)
520
+ *
521
+ * The BigInt value of a tag-2 unsigned bignum (a big-endian magnitude byte
522
+ * string, no sign octet). Enforces the byte cap
523
+ * (`cbor/biguint-too-large`), the no-leading-zero and prefer-basic-int
524
+ * minimality rules (`cbor/non-minimal-biguint`), and the wrapped content type
525
+ * (`cbor/bad-tag-content`); a wrong / absent tag throws `cbor/unexpected-tag`.
526
+ *
527
+ * @example
528
+ * pki.cbor.read.biguint(node); // -> 18446744073709551616n
529
+ */
530
+ function readBiguint(node) {
531
+ var inner = _tagInner(node, 2, "read.biguint");
532
+ if (inner.majorType !== 2) throw new CborError("cbor/bad-tag-content", "read.biguint: tag 2 must wrap a byte string");
533
+ var c = inner.content;
534
+ if (c.length > constants.LIMITS.CBOR_MAX_BIGUINT_BYTES) {
535
+ throw new CborError("cbor/biguint-too-large", "bignum " + c.length + " bytes exceeds cap " + constants.LIMITS.CBOR_MAX_BIGUINT_BYTES);
536
+ }
537
+ if (c.length > 0 && c[0] === 0x00) throw new CborError("cbor/non-minimal-biguint", "bignum has a leading zero byte");
538
+ if (c.length <= 8) throw new CborError("cbor/non-minimal-biguint", "a value that fits a basic integer must not use a bignum");
539
+ return BigInt("0x" + c.toString("hex"));
540
+ }
541
+
542
+ /**
543
+ * @primitive pki.cbor.read.time
544
+ * @signature pki.cbor.read.time(node) -> Date
545
+ * @since 0.1.27
546
+ * @status experimental
547
+ * @spec RFC 8949 sec. 3.4.2 (tag 1 epoch date/time)
548
+ *
549
+ * The `Date` of a tag-1 epoch time (seconds since 1970-01-01T00:00Z, integer).
550
+ * Throws `cbor/unexpected-tag` on a wrong / absent tag, `cbor/bad-tag-content`
551
+ * when tag 1 does not wrap an integer, `cbor/bad-time` on an out-of-range value.
552
+ *
553
+ * @example
554
+ * pki.cbor.read.time(node); // -> Date 2013-03-21T20:04:00.000Z
555
+ */
556
+ function readTime(node) {
557
+ var inner = _tagInner(node, 1, "read.time");
558
+ if (inner.majorType !== 0 && inner.majorType !== 1) {
559
+ throw new CborError("cbor/bad-tag-content", "read.time: tag 1 must wrap an integer");
560
+ }
561
+ var secs = inner.majorType === 0 ? inner.argument : (-1n - inner.argument);
562
+ if (secs < -_MAX_EPOCH_SECONDS || secs > _MAX_EPOCH_SECONDS) {
563
+ throw new CborError("cbor/bad-time", "epoch time out of range");
564
+ }
565
+ var ns = Number(secs);
566
+ var d = new Date(ns < 0 ? -constants.TIME.seconds(-ns) : constants.TIME.seconds(ns));
567
+ if (isNaN(d.getTime())) throw new CborError("cbor/bad-time", "epoch time out of range");
568
+ return d;
569
+ }
570
+
571
+ /**
572
+ * @primitive pki.cbor.read.oid
573
+ * @signature pki.cbor.read.oid(node) -> "2.5.4.3"
574
+ * @since 0.1.27
575
+ * @status experimental
576
+ * @spec RFC 9090 (tag 111 CBOR OID)
577
+ *
578
+ * The dotted OID string of a tag-111 CBOR OID (a byte string carrying the BER
579
+ * object-identifier content octets, decoded through the shared
580
+ * `asn1.decodeOidContent`, so a malformed body surfaces the existing `oid/*`
581
+ * codes). Throws `cbor/unexpected-tag` on a wrong / absent tag,
582
+ * `cbor/bad-tag-content` when tag 111 does not wrap a byte string.
583
+ *
584
+ * @example
585
+ * pki.cbor.read.oid(node); // -> "2.5.4.3"
586
+ */
587
+ function readOid(node) {
588
+ var inner = _tagInner(node, 111, "read.oid");
589
+ if (inner.majorType !== 2) throw new CborError("cbor/bad-tag-content", "read.oid: tag 111 must wrap a byte string");
590
+ return asn1.decodeOidContent(inner.content);
591
+ }
592
+
593
+ module.exports = {
594
+ decode: decode,
595
+ read: {
596
+ uint: readUint,
597
+ nint: readNint,
598
+ int: readInt,
599
+ byteString: readByteString,
600
+ textString: readTextString,
601
+ array: readArray,
602
+ map: readMap,
603
+ boolean: readBoolean,
604
+ nullValue: readNull,
605
+ undefinedValue: readUndefined,
606
+ float: readFloat,
607
+ biguint: readBiguint,
608
+ time: readTime,
609
+ oid: readOid,
610
+ },
611
+ };
package/lib/constants.js CHANGED
@@ -132,9 +132,37 @@ var BYTES = {
132
132
  var LIMITS = {
133
133
  DER_MAX_BYTES: BYTES.mib(16),
134
134
  DER_MAX_DEPTH: 64,
135
+ // Hard stack-safe ceiling on the recursion depth of the DER / CBOR
136
+ // decoders, independent of the (operator-tunable) maxDepth. Both decoders
137
+ // are recursive descent; a maxDepth raised above the engine's native call-
138
+ // stack limit would let deeply nested input overflow the C stack with a raw
139
+ // RangeError instead of the typed too-deep verdict, defeating the fail-closed
140
+ // contract. A maxDepth above this ceiling is refused at config time. 256 is
141
+ // four times the default cap and far deeper than any real PKI / CBOR nesting
142
+ // (a certificate is ~6 levels; the deepest bounded re-decode is 16), while
143
+ // staying well below the overflow threshold on any supported platform.
144
+ MAX_DECODE_DEPTH_CEILING: 256,
135
145
  PEM_MAX_BYTES: BYTES.mib(16),
136
146
  DER_MAX_INTEGER_BYTES: BYTES.kib(16),
137
147
  OID_MAX_SUBIDENTIFIER_BYTES: 32,
148
+ // Deterministic-CBOR codec ceilings (RFC 8949), the DER neighbours' siblings:
149
+ // a whole-document cap refused before the walk, a nesting cap, and a per-value
150
+ // bignum ceiling the document cap can't provide. Unlike DER_MAX_INTEGER_BYTES,
151
+ // the bignum cap carries NO +1 sign octet -- a CBOR tag-2/3 bignum body is pure
152
+ // unsigned big-endian magnitude (RFC 8949 sec. 3.4.3), so 16 KiB is the exact
153
+ // byte ceiling (covers an RSA-131072 modulus).
154
+ CBOR_MAX_BYTES: BYTES.mib(16),
155
+ CBOR_MAX_DEPTH: 64,
156
+ CBOR_MAX_BIGUINT_BYTES: BYTES.kib(16),
157
+ // Total decoded-item ceiling. A definite-length array or map can declare a
158
+ // huge element count that stays under the byte cap yet allocates one node per
159
+ // element -- e.g. a 16 MiB input of `9a00fffffb` followed by ~16 million
160
+ // one-byte items would build ~16 million nodes and exhaust memory before a
161
+ // typed verdict. The decoder counts every item it builds and refuses past this
162
+ // cap, so a high-fanout bomb fails closed with cbor/too-many-items instead of
163
+ // OOM-ing. 1,000,000 is far above any real CBOR-encoded PKI structure (a C509
164
+ // certificate or COSE message is hundreds of items) while bounding the tree.
165
+ CBOR_MAX_ITEMS: 1000000,
138
166
  // Certificate Transparency SCT-list bounds (RFC 6962 sec. 3.3). The outer sct_list
139
167
  // vector carries a 2-byte length prefix, so a well-formed list body is at most
140
168
  // 2^16-1 = 65535 bytes and the full TLS blob (prefix + body) is at most 65537.
@@ -148,6 +176,17 @@ var LIMITS = {
148
176
  // 2-5 SCTs; 256 is far above policy.
149
177
  SCT_MAX_BYTES: BYTES.kib(64) + 1,
150
178
  SCT_MAX_COUNT: 256,
179
+ // Merkle-proof node-count ceiling (RFC 6962 / RFC 9162). A DoS backstop that
180
+ // rejects a pathologically long proof array BEFORE the O(log n) fold does any
181
+ // digest work. 65 is the exact structural maximum: a tree size is a uint64, so
182
+ // an audit (inclusion) path is at most ceil(log2(2^64)) = 64 nodes, but a
183
+ // consistency proof is at most ceil(log2(newSize)) + 1 = 65 nodes (the extra
184
+ // node is the SUBPROOF terminal for a non-power-of-two old size near 2^64). Real
185
+ // CT logs are 2^30-2^40 (30-40-node proofs), so 65 is generous headroom while
186
+ // still bounding hostile work. The PRECISE per-proof guard is the geometry
187
+ // check in each verify (the node count must equal what the coordinates require);
188
+ // this is the coarse cap. A node count, not a byte size.
189
+ MERKLE_MAX_PROOF_NODES: 65,
151
190
  // Certification-path length ceiling: bounds the per-cert asymmetric verify
152
191
  // work on an untrusted certificate bundle (a real chain is well under this;
153
192
  // the operator may override via opts.maxPathCerts).
@@ -130,6 +130,15 @@ var ConstantsError = defineClass("ConstantsError");
130
130
  // decoder rejects is permanently invalid.
131
131
  var Asn1Error = defineClass("Asn1Error");
132
132
 
133
+ // CborError -- malformed / non-deterministic CBOR: a reserved or indefinite
134
+ // additional-info value, a stray break, a non-minimal ("preferred") argument,
135
+ // a non-shortest or non-canonical-NaN float, an out-of-order or duplicate map
136
+ // key, a non-minimal / oversized bignum, bad UTF-8, a wrong-typed tag body,
137
+ // leftover trailing bytes, or a depth / size cap exceeded. RFC 8949 core-
138
+ // deterministic encoding (sec. 4.2.1) is canonical, so anything the decoder
139
+ // rejects is permanently invalid.
140
+ var CborError = defineClass("CborError");
141
+
133
142
  // OidError -- a malformed object-identifier: fewer than two arcs, a first
134
143
  // arc outside 0..2, a second arc >= 40 under arcs 0/1, or a non-minimal
135
144
  // base-128 sub-identifier.
@@ -219,6 +228,16 @@ var SmimeError = defineClass("SmimeError", { withCause: true });
219
228
  // inner `asn1/*` decode error) as `.cause`.
220
229
  var CtError = defineClass("CtError", { withCause: true });
221
230
 
231
+ // MerkleError -- a malformed input to the RFC 6962 / RFC 9162 Merkle-tree
232
+ // proof-verification core: a tree coordinate that is not a non-negative integer
233
+ // (or a Number above 2^53 where an exact BigInt is required), a leafIndex
234
+ // outside its tree, an inverted consistency window, a hash chunk that is not 32
235
+ // bytes, or a proof whose node count does not match the tree geometry. The final
236
+ // root comparison is a constant-time boolean (root matched / did not), NOT an
237
+ // error -- only a structurally malformed input throws. Carries an underlying
238
+ // leaf fault as `.cause`.
239
+ var MerkleError = defineClass("MerkleError", { withCause: true });
240
+
222
241
  // CsrattrsError -- a byte sequence that is not a well-formed RFC 8951 sec. 3.5
223
242
  // CsrAttrs (SEQUENCE OF AttrOrOID), or one violating an RFC 9908 semantic rule
224
243
  // (a repeated id-ExtensionReq, an extension-request values SET that is not
@@ -255,6 +274,7 @@ module.exports = {
255
274
  defineClass: defineClass,
256
275
  ConstantsError: ConstantsError,
257
276
  Asn1Error: Asn1Error,
277
+ CborError: CborError,
258
278
  OidError: OidError,
259
279
  PemError: PemError,
260
280
  CertificateError: CertificateError,
@@ -271,6 +291,7 @@ module.exports = {
271
291
  CmpError: CmpError,
272
292
  PathError: PathError,
273
293
  CtError: CtError,
294
+ MerkleError: MerkleError,
274
295
  SmimeError: SmimeError,
275
296
  CsrattrsError: CsrattrsError,
276
297
  EstError: EstError,
package/lib/merkle.js ADDED
@@ -0,0 +1,345 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // Copyright (c) blamejs contributors
3
+ "use strict";
4
+ /**
5
+ * @module pki.merkle
6
+ * @nav Transparency
7
+ * @title Merkle
8
+ * @order 220
9
+ * @slug merkle
10
+ *
11
+ * @intro
12
+ * RFC 6962 (Certificate Transparency) / RFC 9162 (CT 2.0) Merkle-tree hash
13
+ * and proof-verification core -- the load-bearing primitive a static-CT
14
+ * client, a Merkle-Tree-Certificates relying party, and a sigstore / Rekor
15
+ * inclusion check all compose. It is ALL strict verification over SHA-256:
16
+ * zero new crypto.
17
+ *
18
+ * `leafHash` / `nodeHash` / `emptyRootHash` build the tree hashes with the
19
+ * two domain-separation prefixes fixed by the spec -- a leaf is
20
+ * `SHA-256(0x00 || entry)`, an interior node is `SHA-256(0x01 || left ||
21
+ * right)`, the empty tree is `SHA-256("")`. Those `0x00` / `0x01` prefixes
22
+ * are the second-preimage defense: without them a leaf whose bytes equal a
23
+ * valid interior node's preimage could be smuggled in as present.
24
+ *
25
+ * `verifyInclusion` folds an audit path back to a root and constant-time-
26
+ * compares it to a trusted checkpoint root; `verifyConsistency` reconstructs
27
+ * BOTH the old and the new root from a consistency proof (the append-only
28
+ * guarantee lives in the old-root leg). Both are fail-closed: a malformed
29
+ * coordinate, an out-of-range index, an inverted window, a wrong hash length,
30
+ * or a proof whose node count does not match the tree geometry throws a typed
31
+ * `merkle/*` error; the ONLY boolean-`false` result is the final root
32
+ * comparison ("root matched" vs "did not"). A `false` from `verifyInclusion`
33
+ * means "not proven present against this root", never "validly absent" -- an
34
+ * inclusion proof cannot express absence. Tree coordinates are uint64, carried
35
+ * as `BigInt` so a large index is never `Number`-narrowed. This is NOT a DER
36
+ * format: like `pki.ct` it is a companion module reached explicitly, never
37
+ * routed by the detect-and-parse orchestrator.
38
+ *
39
+ * @card
40
+ * `leafHash` / `nodeHash` / `emptyRootHash` + `verifyInclusion` /
41
+ * `verifyConsistency` over sync SHA-256, fail-closed, transport-free.
42
+ */
43
+
44
+ var nodeCrypto = require("node:crypto");
45
+ var constants = require("./constants");
46
+ var frameworkError = require("./framework-error");
47
+
48
+ var MerkleError = frameworkError.MerkleError;
49
+
50
+ var SHA256_BYTES = 32;
51
+ var UINT64_MAX = 18446744073709551615n; // 2n ** 64n - 1n
52
+ var LEAF_PREFIX = Buffer.from([0x00]);
53
+ var NODE_PREFIX = Buffer.from([0x01]);
54
+ var EMPTY = Buffer.alloc(0);
55
+
56
+ function _sha256(buf) {
57
+ return nodeCrypto.createHash("sha256").update(buf).digest();
58
+ }
59
+
60
+ function _toBuffer(v, field) {
61
+ if (!Buffer.isBuffer(v) && !(v instanceof Uint8Array)) {
62
+ throw new MerkleError("merkle/bad-input", field + " must be a Buffer or Uint8Array");
63
+ }
64
+ try {
65
+ // A zero-copy view over the same bytes; this also surfaces a detached
66
+ // backing ArrayBuffer (a transferred / structuredClone'd view) as a typed
67
+ // error here rather than a raw TypeError from a later concat / digest.
68
+ return Buffer.from(v.buffer, v.byteOffset, v.byteLength);
69
+ } catch (e) {
70
+ throw new MerkleError("merkle/bad-input", field + " is not a usable byte view", e);
71
+ }
72
+ }
73
+
74
+ function _node32(v, field) {
75
+ var buf = _toBuffer(v, field);
76
+ if (buf.length !== SHA256_BYTES) {
77
+ throw new MerkleError("merkle/bad-hash-length", field + " must be exactly 32 bytes, got " + buf.length);
78
+ }
79
+ return buf;
80
+ }
81
+
82
+ // number | bigint -> a validated BigInt in [0, 2^64). A Number >= 2^53 (where
83
+ // precision is lost), a negative / non-integer, or a non-number-non-bigint
84
+ // throws -- the number-narrows-unbounded-integer discipline for uint64 tree
85
+ // coordinates.
86
+ function _coerceCoord(v, field) {
87
+ var b;
88
+ if (typeof v === "bigint") {
89
+ b = v;
90
+ } else if (typeof v === "number") {
91
+ if (!Number.isSafeInteger(v) || v < 0) {
92
+ throw new MerkleError("merkle/bad-input", field + " must be a non-negative safe integer or a BigInt (a Number >= 2^53 loses precision)");
93
+ }
94
+ b = BigInt(v);
95
+ } else {
96
+ throw new MerkleError("merkle/bad-input", field + " must be a number or a BigInt");
97
+ }
98
+ if (b < 0n || b > UINT64_MAX) {
99
+ throw new MerkleError("merkle/bad-input", field + " must be in [0, 2^64)");
100
+ }
101
+ return b;
102
+ }
103
+
104
+ function _proofNodes(proof) {
105
+ if (!Array.isArray(proof)) throw new MerkleError("merkle/bad-proof", "proof must be an array");
106
+ if (proof.length > constants.LIMITS.MERKLE_MAX_PROOF_NODES) {
107
+ throw new MerkleError("merkle/proof-too-large", "proof has " + proof.length + " nodes, exceeds " + constants.LIMITS.MERKLE_MAX_PROOF_NODES);
108
+ }
109
+ var out = [];
110
+ for (var i = 0; i < proof.length; i++) out.push(_node32(proof[i], "proof[" + i + "]"));
111
+ return out;
112
+ }
113
+
114
+ // Constant-time equality of two already-validated equal-length 32-byte buffers.
115
+ function _ctEq(a, b) {
116
+ return a.length === b.length && nodeCrypto.timingSafeEqual(a, b);
117
+ }
118
+
119
+ /**
120
+ * @primitive pki.merkle.leafHash
121
+ * @signature pki.merkle.leafHash(entry) -> Buffer
122
+ * @since 0.1.28
123
+ * @status experimental
124
+ * @spec RFC 6962, RFC 9162
125
+ * @related pki.merkle.nodeHash, pki.merkle.verifyInclusion
126
+ *
127
+ * The Merkle leaf hash `MTH({d}) = SHA-256(0x00 || entry)`. The `0x00` prefix
128
+ * is the leaf-domain second-preimage separation and is applied unconditionally.
129
+ * Throws `merkle/bad-input` if `entry` is not a Buffer / Uint8Array.
130
+ *
131
+ * @example
132
+ * pki.merkle.leafHash(Buffer.from("leaf data")); // -> <Buffer 32-byte leaf hash>
133
+ */
134
+ function leafHash(entry) {
135
+ var e = _toBuffer(entry, "entry");
136
+ return _sha256(Buffer.concat([LEAF_PREFIX, e]));
137
+ }
138
+
139
+ /**
140
+ * @primitive pki.merkle.nodeHash
141
+ * @signature pki.merkle.nodeHash(left, right) -> Buffer
142
+ * @since 0.1.28
143
+ * @status experimental
144
+ * @spec RFC 6962, RFC 9162
145
+ * @related pki.merkle.leafHash
146
+ *
147
+ * The Merkle interior-node hash `SHA-256(0x01 || left || right)`. Both operands
148
+ * must be 32-byte hashes; the `0x01` prefix is applied unconditionally. Throws
149
+ * `merkle/bad-input` on a non-buffer operand, `merkle/bad-hash-length` on an
150
+ * operand that is not exactly 32 bytes.
151
+ *
152
+ * @example
153
+ * var l = pki.merkle.leafHash(Buffer.from([0]));
154
+ * var r = pki.merkle.leafHash(Buffer.from([1]));
155
+ * pki.merkle.nodeHash(l, r); // -> <Buffer 32-byte node hash>
156
+ */
157
+ function nodeHash(left, right) {
158
+ var l = _node32(left, "left");
159
+ var r = _node32(right, "right");
160
+ return _sha256(Buffer.concat([NODE_PREFIX, l, r]));
161
+ }
162
+
163
+ /**
164
+ * @primitive pki.merkle.emptyRootHash
165
+ * @signature pki.merkle.emptyRootHash() -> Buffer
166
+ * @since 0.1.28
167
+ * @status experimental
168
+ * @spec RFC 6962, RFC 9162
169
+ * @related pki.merkle.verifyConsistency
170
+ *
171
+ * The Merkle tree head of the empty tree, `MTH({}) = SHA-256("")`
172
+ * (`e3b0c442...b855`). A fresh Buffer each call.
173
+ *
174
+ * @example
175
+ * pki.merkle.emptyRootHash(); // -> <Buffer e3 b0 c4 42 ...>
176
+ */
177
+ function emptyRootHash() {
178
+ return _sha256(EMPTY);
179
+ }
180
+
181
+ /**
182
+ * @primitive pki.merkle.verifyInclusion
183
+ * @signature pki.merkle.verifyInclusion(opts) -> boolean
184
+ * @since 0.1.28
185
+ * @status experimental
186
+ * @spec RFC 6962, RFC 9162
187
+ * @related pki.merkle.leafHash, pki.merkle.verifyConsistency
188
+ *
189
+ * Verify an RFC 6962 / RFC 9162 audit (inclusion) proof: fold `leafHash` up the
190
+ * audit path and constant-time-compare the reconstructed root to `rootHash`.
191
+ * Returns `true` iff the proof binds the leaf to the root; a well-formed proof
192
+ * that does not match returns `false` ("not proven present against this root",
193
+ * NEVER "validly absent"). A malformed input -- a coordinate that is not a
194
+ * non-negative integer (or a Number >= 2^53), `treeSize` 0, `leafIndex >=
195
+ * treeSize`, a non-32-byte hash, or a proof whose node count does not match the
196
+ * tree geometry -- throws a typed `merkle/*` error.
197
+ *
198
+ * @opts
199
+ * leafIndex: number | bigint, // 0-based leaf position (uint64; pass BigInt above 2^53)
200
+ * treeSize: number | bigint, // total leaf count of the tree the root commits to
201
+ * leafHash: Buffer, // 32-byte leaf hash (e.g. from pki.merkle.leafHash)
202
+ * proof: Buffer[], // the audit path, each node a 32-byte hash
203
+ * rootHash: Buffer, // 32-byte trusted checkpoint root
204
+ *
205
+ * @example
206
+ * var lh = pki.merkle.leafHash(Buffer.from([0]));
207
+ * pki.merkle.verifyInclusion({ leafIndex: 0, treeSize: 1, leafHash: lh, proof: [], rootHash: lh }); // -> true
208
+ */
209
+ function verifyInclusion(opts) {
210
+ opts = opts || {};
211
+ var leafIndex = _coerceCoord(opts.leafIndex, "leafIndex");
212
+ var treeSize = _coerceCoord(opts.treeSize, "treeSize");
213
+ if (treeSize === 0n) throw new MerkleError("merkle/empty-tree", "an empty tree has no leaves to include");
214
+ if (leafIndex >= treeSize) throw new MerkleError("merkle/index-out-of-range", "leafIndex " + leafIndex + " is not less than treeSize " + treeSize);
215
+ var lh = _node32(opts.leafHash, "leafHash");
216
+ var rootHash = _node32(opts.rootHash, "rootHash");
217
+ var proof = _proofNodes(opts.proof);
218
+
219
+ var fn = leafIndex;
220
+ var sn = treeSize - 1n;
221
+ var r = lh;
222
+ for (var i = 0; i < proof.length; i++) {
223
+ var p = proof[i];
224
+ if (sn === 0n) throw new MerkleError("merkle/bad-proof-length", "proof is longer than the tree geometry allows");
225
+ if ((fn & 1n) === 1n || fn === sn) {
226
+ r = nodeHash(p, r);
227
+ if ((fn & 1n) === 0n) {
228
+ do { fn >>= 1n; sn >>= 1n; } while ((fn & 1n) === 0n && fn !== 0n);
229
+ }
230
+ } else {
231
+ r = nodeHash(r, p);
232
+ }
233
+ fn >>= 1n;
234
+ sn >>= 1n;
235
+ }
236
+ if (sn !== 0n) throw new MerkleError("merkle/bad-proof-length", "proof is shorter than the tree geometry requires");
237
+ return _ctEq(r, rootHash);
238
+ }
239
+
240
+ /**
241
+ * @primitive pki.merkle.verifyConsistency
242
+ * @signature pki.merkle.verifyConsistency(opts) -> boolean
243
+ * @since 0.1.28
244
+ * @status experimental
245
+ * @spec RFC 6962, RFC 9162
246
+ * @related pki.merkle.verifyInclusion, pki.merkle.emptyRootHash
247
+ *
248
+ * Verify an RFC 6962 / RFC 9162 consistency proof between an older tree of
249
+ * `oldSize` leaves (root `oldRoot`) and a newer tree of `newSize` leaves (root
250
+ * `newRoot`). Reconstructs BOTH roots from the proof and constant-time-compares
251
+ * each; returns `true` iff both match (the append-only guarantee lives in the
252
+ * old-root leg -- a proof that yields a valid `newRoot` but the wrong `oldRoot`
253
+ * is a rewritten history and returns `false`). The empty tree is a prefix of
254
+ * every tree, so `oldSize` 0 checks `oldRoot == emptyRootHash()` -- and, when
255
+ * `newSize` is also 0, `newRoot == emptyRootHash()` too (both trees empty); for
256
+ * `newSize > 0` an empty-old proof places no binding on `newRoot` (an empty prior
257
+ * tree carries no append-only guarantee -- authenticate `newRoot` separately,
258
+ * e.g. via its checkpoint signature). Equal non-zero sizes require an empty proof
259
+ * and `oldRoot == newRoot`. A malformed input --
260
+ * `oldSize > newSize`, a non-empty proof where
261
+ * the geometry requires none (or empty where it requires one), a non-32-byte
262
+ * hash, or a wrong node count -- throws a typed `merkle/*` error.
263
+ *
264
+ * @opts
265
+ * oldSize: number | bigint, // leaf count of the older tree (uint64)
266
+ * newSize: number | bigint, // leaf count of the newer tree (>= oldSize)
267
+ * oldRoot: Buffer, // 32-byte root of the older tree
268
+ * newRoot: Buffer, // 32-byte root of the newer tree
269
+ * proof: Buffer[], // the consistency proof, each node a 32-byte hash
270
+ *
271
+ * @example
272
+ * var r = pki.merkle.leafHash(Buffer.from([0]));
273
+ * pki.merkle.verifyConsistency({ oldSize: 1, newSize: 1, oldRoot: r, newRoot: r, proof: [] }); // -> true
274
+ */
275
+ function verifyConsistency(opts) {
276
+ opts = opts || {};
277
+ var oldSize = _coerceCoord(opts.oldSize, "oldSize");
278
+ var newSize = _coerceCoord(opts.newSize, "newSize");
279
+ var oldRoot = _node32(opts.oldRoot, "oldRoot");
280
+ var newRoot = _node32(opts.newRoot, "newRoot");
281
+ var proof = _proofNodes(opts.proof);
282
+
283
+ if (oldSize > newSize) throw new MerkleError("merkle/old-size-exceeds-new", "oldSize " + oldSize + " exceeds newSize " + newSize);
284
+ if (oldSize === 0n) {
285
+ if (proof.length !== 0) throw new MerkleError("merkle/bad-proof-length", "an empty older tree admits only the empty consistency proof");
286
+ var er = emptyRootHash();
287
+ var oldEmpty = _ctEq(oldRoot, er);
288
+ // The empty tree is a prefix of every tree, so its own root must be the
289
+ // empty root. When the NEW tree is also empty, its root must be the empty
290
+ // root too (a bogus newRoot must not pass); when newSize > 0 an empty-old
291
+ // proof places no binding on newRoot.
292
+ if (newSize === 0n) {
293
+ var newEmpty = _ctEq(newRoot, er);
294
+ return oldEmpty && newEmpty;
295
+ }
296
+ return oldEmpty;
297
+ }
298
+ if (oldSize === newSize) {
299
+ if (proof.length !== 0) throw new MerkleError("merkle/sizes-equal-nonempty-proof", "equal tree sizes require an empty consistency proof");
300
+ return _ctEq(oldRoot, newRoot);
301
+ }
302
+ if (proof.length === 0) throw new MerkleError("merkle/empty-consistency-proof", "a non-trivial consistency proof must not be empty");
303
+
304
+ var path = proof;
305
+ if ((oldSize & (oldSize - 1n)) === 0n) {
306
+ // oldSize is an exact power of two: the old root is the first proof node
307
+ // (it is not otherwise carried in the proof).
308
+ path = [oldRoot].concat(proof);
309
+ }
310
+ var fn = oldSize - 1n;
311
+ var sn = newSize - 1n;
312
+ while ((fn & 1n) === 1n) { fn >>= 1n; sn >>= 1n; }
313
+ var fr = path[0];
314
+ var sr = path[0];
315
+ for (var i = 1; i < path.length; i++) {
316
+ var c = path[i];
317
+ if (sn === 0n) throw new MerkleError("merkle/bad-proof-length", "consistency proof is longer than the geometry allows");
318
+ if ((fn & 1n) === 1n || fn === sn) {
319
+ fr = nodeHash(c, fr);
320
+ sr = nodeHash(c, sr);
321
+ if ((fn & 1n) === 0n) {
322
+ do { fn >>= 1n; sn >>= 1n; } while ((fn & 1n) === 0n && fn !== 0n);
323
+ }
324
+ } else {
325
+ sr = nodeHash(sr, c);
326
+ }
327
+ fn >>= 1n;
328
+ sn >>= 1n;
329
+ }
330
+ if (sn !== 0n) throw new MerkleError("merkle/bad-proof-length", "consistency proof is shorter than the geometry requires");
331
+ // Evaluate BOTH constant-time compares unconditionally before combining, so a
332
+ // `&&` never short-circuits the second `timingSafeEqual` (which would leak,
333
+ // via timing, whether the old-root leg matched).
334
+ var okOld = _ctEq(fr, oldRoot);
335
+ var okNew = _ctEq(sr, newRoot);
336
+ return okOld && okNew;
337
+ }
338
+
339
+ module.exports = {
340
+ leafHash: leafHash,
341
+ nodeHash: nodeHash,
342
+ emptyRootHash: emptyRootHash,
343
+ verifyInclusion: verifyInclusion,
344
+ verifyConsistency: verifyConsistency,
345
+ };
package/lib/oid.js CHANGED
@@ -500,7 +500,7 @@ var _PARAMS_ABSENT = new Set();
500
500
  * @primitive pki.oid.paramsMustBeAbsent
501
501
  * @signature pki.oid.paramsMustBeAbsent(dotted) -> boolean
502
502
  * @since 0.1.21
503
- * @status experimental
503
+ * @status stable
504
504
  * @spec RFC 9909, RFC 9814, RFC 9881, RFC 8410
505
505
  * @related pki.oid.name, pki.oid.byName
506
506
  *
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blamejs/pki",
3
- "version": "0.1.26",
3
+ "version": "0.1.28",
4
4
  "description": "Pure-JavaScript PKI toolkit that owns its stack — X.509, ASN.1/DER, CMS, PQC-first.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "blamejs contributors",
package/sbom.cdx.json CHANGED
@@ -2,10 +2,10 @@
2
2
  "$schema": "http://cyclonedx.org/schema/bom-1.5.schema.json",
3
3
  "bomFormat": "CycloneDX",
4
4
  "specVersion": "1.5",
5
- "serialNumber": "urn:uuid:51669f50-bf8f-4b65-8c89-e8fd503b0399",
5
+ "serialNumber": "urn:uuid:e10c86e3-914c-4324-abeb-585344830973",
6
6
  "version": 1,
7
7
  "metadata": {
8
- "timestamp": "2026-07-10T23:32:35.928Z",
8
+ "timestamp": "2026-07-11T04:47:27.941Z",
9
9
  "lifecycles": [
10
10
  {
11
11
  "phase": "build"
@@ -19,14 +19,14 @@
19
19
  }
20
20
  ],
21
21
  "component": {
22
- "bom-ref": "@blamejs/pki@0.1.26",
22
+ "bom-ref": "@blamejs/pki@0.1.28",
23
23
  "type": "application",
24
24
  "name": "pki",
25
- "version": "0.1.26",
25
+ "version": "0.1.28",
26
26
  "scope": "required",
27
27
  "author": "blamejs contributors",
28
28
  "description": "Pure-JavaScript PKI toolkit that owns its stack — X.509, ASN.1/DER, CMS, PQC-first.",
29
- "purl": "pkg:npm/%40blamejs/pki@0.1.26",
29
+ "purl": "pkg:npm/%40blamejs/pki@0.1.28",
30
30
  "properties": [],
31
31
  "externalReferences": [
32
32
  {
@@ -54,7 +54,7 @@
54
54
  "components": [],
55
55
  "dependencies": [
56
56
  {
57
- "ref": "@blamejs/pki@0.1.26",
57
+ "ref": "@blamejs/pki@0.1.28",
58
58
  "dependsOn": []
59
59
  }
60
60
  ]