@blamejs/pki 0.1.26 → 0.1.27

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,21 @@ 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.27 — 2026-07-11
8
+
9
+ A strict deterministic-CBOR codec joins the toolkit as pki.cbor.
10
+
11
+ ### Added
12
+
13
+ - 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.
14
+ - 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).
15
+ - 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.
16
+ - 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.
17
+
18
+ ### Changed
19
+
20
+ - 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.
21
+
7
22
  ## v0.1.26 — 2026-07-10
8
23
 
9
24
  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 |
package/index.js CHANGED
@@ -30,6 +30,7 @@
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");
@@ -47,6 +48,12 @@ module.exports = {
47
48
  errors: errors,
48
49
  // `asn1` is the strict DER codec (decode/encode/build/read/TAGS).
49
50
  asn1: asn1,
51
+ // `cbor` is the strict, fail-closed RFC 8949 deterministic CBOR codec
52
+ // (decode + read.* leaf readers), sibling to `asn1`. It rejects every
53
+ // non-deterministic shape -- indefinite length, a non-minimal argument,
54
+ // unsorted / duplicate map keys, a non-shortest float, trailing bytes --
55
+ // before it walks a byte, and surfaces zero-copy bytes / content views.
56
+ cbor: cbor,
50
57
  oid: oid,
51
58
  // `schema` is the family: the L2 structure-schema engine (schema.engine) and
52
59
  // the per-format parsers (schema.x509, ...) with detect-and-route schema.parse.
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.
@@ -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.
@@ -255,6 +264,7 @@ module.exports = {
255
264
  defineClass: defineClass,
256
265
  ConstantsError: ConstantsError,
257
266
  Asn1Error: Asn1Error,
267
+ CborError: CborError,
258
268
  OidError: OidError,
259
269
  PemError: PemError,
260
270
  CertificateError: CertificateError,
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.27",
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:cdf3408c-61a7-4b5e-ac8a-dfd37f45e9e6",
6
6
  "version": 1,
7
7
  "metadata": {
8
- "timestamp": "2026-07-10T23:32:35.928Z",
8
+ "timestamp": "2026-07-11T01:50:55.133Z",
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.27",
23
23
  "type": "application",
24
24
  "name": "pki",
25
- "version": "0.1.26",
25
+ "version": "0.1.27",
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.27",
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.27",
58
58
  "dependsOn": []
59
59
  }
60
60
  ]