@blamejs/pki 0.1.29 → 0.1.31

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,7 +4,29 @@ 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.29 — 2026-07-11
7
+ ## v0.1.31 — 2026-07-11
8
+
9
+ The DER format cohort and the JOSE surface graduate to stable.
10
+
11
+ ### Changed
12
+
13
+ - pki.schema.pkcs12 / attrcert / crmf / cmp / csrattrs (parse, pemDecode, pemEncode), pki.schema.all and pki.schema.parse, and the pki.jose signing / verification / thumbprint / base64url / JSON surface graduate from experimental to stable.
14
+ - The LTS-CALENDAR graduation criterion now states that a settled, well-tested format no mainstream tool implements graduates on the toolkit's own conformance-vector round-trip plus coverage-guided fuzzing, rather than on a harness oracle that does not exist.
15
+
16
+ ## v0.1.30 — 2026-07-11
17
+
18
+ Fail-closed hardening of the byte-input and text-decode boundaries.
19
+
20
+ ### Changed
21
+
22
+ - A detached-backed BufferSource (a transferred / structuredClone'd view) passed to any DER format parser -- pki.schema.x509 / crl / csr / pkcs8 / cms / pkcs12 -- fails closed with the format's typed bad-input error at the shared parse-input boundary rather than being decoded as an empty buffer.
23
+
24
+ ### Fixed
25
+
26
+ - The EST transfer and multipart-mixed decoders enforce their size cap on the raw byte length before decoding the payload to a string, and an HTTP error response body is decoded only up to the prefix shown in the message -- closing a single-input string-allocation amplification where an oversized body was materialized in full before the cap rejected it.
27
+ - pki.oid.fromDER rejects a non-Buffer or detached-backed input with a typed oid/bad-input error instead of a raw TypeError.
28
+
29
+ ## v0.1.29 — 2026-07-10
8
30
 
9
31
  A detached-backed BufferSource now fails closed with a typed error at every byte-input boundary.
10
32
 
package/lib/asn1-der.js CHANGED
@@ -32,6 +32,7 @@
32
32
 
33
33
  var constants = require("./constants");
34
34
  var frameworkError = require("./framework-error");
35
+ var guard = require("./guard-all");
35
36
 
36
37
  var Asn1Error = frameworkError.Asn1Error;
37
38
  var OidError = frameworkError.OidError;
@@ -105,19 +106,7 @@ function _className(bits) {
105
106
  }
106
107
 
107
108
  function _asBuffer(input, who) {
108
- // A Buffer is a Uint8Array, so it routes through the same guarded re-view --
109
- // NOT a `Buffer.isBuffer` fast-path that returns it as-is. A detached backing
110
- // ArrayBuffer (a transferred / structuredClone'd view) reads as zero-length;
111
- // re-viewing through Buffer.from surfaces it as a typed error here rather than
112
- // a misleading downstream truncated-DER verdict on an empty buffer.
113
- if (Buffer.isBuffer(input) || input instanceof Uint8Array) {
114
- try {
115
- return Buffer.from(input.buffer, input.byteOffset, input.byteLength);
116
- } catch (e) {
117
- throw new Asn1Error("asn1/not-buffer", who + ": input is not a usable byte view (detached backing buffer?)", e);
118
- }
119
- }
120
- throw new Asn1Error("asn1/not-buffer", who + ": expected a Buffer / Uint8Array");
109
+ return guard.bytes.view(input, Asn1Error, "asn1/not-buffer", who);
121
110
  }
122
111
 
123
112
  // ---- Decoder --------------------------------------------------------
@@ -160,27 +149,15 @@ function _asBuffer(input, who) {
160
149
  * var node = pki.asn1.decode(der);
161
150
  * node.tagNumber === pki.asn1.TAGS.SEQUENCE;
162
151
  */
163
- // A size/depth cap opt: absent means the constants.LIMITS default; a provided
164
- // value must be a non-negative finite integer. A non-finite cap (NaN, Infinity)
165
- // silently DISABLES the guard -- every `>` compare against it is false -- so a
166
- // deeply nested input would run to a bare RangeError instead of the typed
167
- // asn1/too-deep verdict. A bad cap is a config fault: throw at entry.
168
- function _capOpt(v, key, dflt) {
169
- if (v === undefined) return dflt;
170
- if (typeof v !== "number" || !isFinite(v) || v < 0 || Math.floor(v) !== v) {
171
- throw new TypeError("decode: " + key + " must be a non-negative integer");
172
- }
173
- return v;
174
- }
175
-
176
152
  function decode(input, opts) {
177
153
  opts = opts || {};
178
154
  var buf = _asBuffer(input, "decode");
179
- var maxBytes = _capOpt(opts.maxBytes, "maxBytes", constants.LIMITS.DER_MAX_BYTES);
180
- var maxDepth = _capOpt(opts.maxDepth, "maxDepth", constants.LIMITS.DER_MAX_DEPTH);
181
- if (maxDepth > constants.LIMITS.MAX_DECODE_DEPTH_CEILING) {
182
- throw new TypeError("decode: maxDepth " + maxDepth + " exceeds the stack-safe ceiling " + constants.LIMITS.MAX_DECODE_DEPTH_CEILING);
183
- }
155
+ // A size/depth cap opt: absent means the constants.LIMITS default; a provided
156
+ // value must be a non-negative integer (guard.limits.cap). depthCap also
157
+ // refuses a maxDepth above the stack-safe ceiling so a raised cap cannot drive
158
+ // recursive descent into a bare RangeError instead of the typed asn1/too-deep.
159
+ var maxBytes = guard.limits.cap(opts.maxBytes, "maxBytes", constants.LIMITS.DER_MAX_BYTES);
160
+ var maxDepth = guard.limits.depthCap(opts.maxDepth, "maxDepth", constants.LIMITS.DER_MAX_DEPTH);
184
161
  if (buf.length > maxBytes) {
185
162
  throw new Asn1Error("asn1/too-large", "input " + buf.length + " bytes exceeds cap " + maxBytes);
186
163
  }
package/lib/cbor-det.js CHANGED
@@ -37,6 +37,7 @@
37
37
  var constants = require("./constants");
38
38
  var frameworkError = require("./framework-error");
39
39
  var asn1 = require("./asn1-der");
40
+ var guard = require("./guard-all");
40
41
 
41
42
  var CborError = frameworkError.CborError;
42
43
 
@@ -57,31 +58,7 @@ var _utf8 = new TextDecoder("utf-8", { fatal: true });
57
58
  var _MAX_EPOCH_SECONDS = 8640000000000n;
58
59
 
59
60
  function _asBuffer(input, who) {
60
- // A Buffer is itself a Uint8Array, so it goes through the same guarded re-view
61
- // -- NOT a `Buffer.isBuffer` fast-path that would return it as-is. A detached
62
- // backing ArrayBuffer (a transferred / structuredClone'd view) has had its
63
- // bytes removed and reads as zero-length; the fast-path would hand the byte
64
- // walk an empty buffer. Always re-view through Buffer.from so a detached input
65
- // fails closed here as a typed error rather than a raw TypeError deeper in the
66
- // walk (or a misleading truncated-input verdict).
67
- if (Buffer.isBuffer(input) || input instanceof Uint8Array) {
68
- try {
69
- return Buffer.from(input.buffer, input.byteOffset, input.byteLength);
70
- } catch (e) {
71
- throw new CborError("cbor/not-buffer", who + ": input is not a usable byte view (detached backing buffer?)", e);
72
- }
73
- }
74
- throw new CborError("cbor/not-buffer", who + ": expected a Buffer / Uint8Array");
75
- }
76
-
77
- // Config-time cap validation: a bad cap is an authoring bug, so it throws a
78
- // TypeError (not a CborError) exactly like asn1-der's _capOpt.
79
- function _capOpt(v, key, dflt) {
80
- if (v === undefined) return dflt;
81
- if (typeof v !== "number" || !Number.isInteger(v) || v < 0) {
82
- throw new TypeError("decode: " + key + " must be a non-negative integer");
83
- }
84
- return v;
61
+ return guard.bytes.view(input, CborError, "cbor/not-buffer", who);
85
62
  }
86
63
 
87
64
  // Determinism profile -> a ruleset. v1 whitelists only "deterministic"
@@ -283,12 +260,9 @@ function _decodeItem(buf, start, limit, depth, maxD, rules, state) {
283
260
  function decode(input, opts) {
284
261
  opts = opts || {};
285
262
  var buf = _asBuffer(input, "decode");
286
- var maxBytes = _capOpt(opts.maxBytes, "maxBytes", constants.LIMITS.CBOR_MAX_BYTES);
287
- var maxDepth = _capOpt(opts.maxDepth, "maxDepth", constants.LIMITS.CBOR_MAX_DEPTH);
288
- if (maxDepth > constants.LIMITS.MAX_DECODE_DEPTH_CEILING) {
289
- throw new TypeError("decode: maxDepth " + maxDepth + " exceeds the stack-safe ceiling " + constants.LIMITS.MAX_DECODE_DEPTH_CEILING);
290
- }
291
- var maxItems = _capOpt(opts.maxItems, "maxItems", constants.LIMITS.CBOR_MAX_ITEMS);
263
+ var maxBytes = guard.limits.cap(opts.maxBytes, "maxBytes", constants.LIMITS.CBOR_MAX_BYTES);
264
+ var maxDepth = guard.limits.depthCap(opts.maxDepth, "maxDepth", constants.LIMITS.CBOR_MAX_DEPTH);
265
+ var maxItems = guard.limits.cap(opts.maxItems, "maxItems", constants.LIMITS.CBOR_MAX_ITEMS);
292
266
  var rules = _profile(opts.profile);
293
267
  if (buf.length > maxBytes) throw new CborError("cbor/too-large", "input " + buf.length + " bytes exceeds cap " + maxBytes);
294
268
  var r = _decodeItem(buf, 0, buf.length, 0, maxDepth, rules, { n: 0, max: maxItems });
package/lib/ct.js CHANGED
@@ -41,6 +41,7 @@
41
41
  var asn1 = require("./asn1-der.js");
42
42
  var constants = require("./constants.js");
43
43
  var frameworkError = require("./framework-error.js");
44
+ var guard = require("./guard-all.js");
44
45
 
45
46
  var CtError = frameworkError.CtError;
46
47
  var C = constants;
@@ -111,19 +112,7 @@ function _peelInner(extValue) {
111
112
  }
112
113
 
113
114
  function _toBuffer(v, field) {
114
- // A Buffer is a Uint8Array, so it routes through the same guarded re-view --
115
- // NOT a `Buffer.isBuffer` fast-path that returns it as-is. A detached backing
116
- // ArrayBuffer (a transferred / structuredClone'd view) reads as zero-length;
117
- // re-viewing through Buffer.from surfaces it as a typed error here rather than
118
- // a misleading downstream bad-DER verdict on an empty buffer.
119
- if (Buffer.isBuffer(v) || v instanceof Uint8Array) {
120
- try {
121
- return Buffer.from(v.buffer, v.byteOffset, v.byteLength);
122
- } catch (e) {
123
- throw new CtError("ct/bad-input", field + " is not a usable byte view (detached backing buffer?)", e);
124
- }
125
- }
126
- throw new CtError("ct/bad-input", field + " must be a Buffer or Uint8Array");
115
+ return guard.bytes.view(v, CtError, "ct/bad-input", field);
127
116
  }
128
117
 
129
118
  // Parse one SerializedSCT body inside a sub-reader bounded to the element (so a
package/lib/est.js CHANGED
@@ -50,6 +50,7 @@ var crl = require("./schema-crl");
50
50
  var pkcs8 = require("./schema-pkcs8");
51
51
  var csr = require("./schema-csr");
52
52
  var frameworkError = require("./framework-error");
53
+ var guard = require("./guard-all");
53
54
 
54
55
  var EstError = frameworkError.EstError;
55
56
  function E(code, message, cause) { return new EstError(code, message, cause); }
@@ -128,14 +129,16 @@ function _httpDateMs(s) {
128
129
  * var roundTripped = pki.est.transferDecode(pki.est.transferEncode(der));
129
130
  */
130
131
  function transferDecode(body) {
131
- var s = Buffer.isBuffer(body) ? body.toString("latin1") : String(body);
132
132
  // The pre-decode ceiling: the largest base64 that could yield a DER_MAX_BYTES
133
133
  // document, plus a generous line-wrapping allowance (CRLF at 64/76-char lines is
134
134
  // ~3%; 1/8 leaves ample margin) so a normally-wrapped near-limit body is not
135
135
  // rejected before the real DER_MAX_BYTES limit is enforced on the decode below.
136
+ // guard.text.decode caps the raw byte length BEFORE the latin1 copy.
136
137
  var b64Len = Math.ceil(constants.LIMITS.DER_MAX_BYTES * 4 / 3);
137
138
  var cap = b64Len + Math.ceil(b64Len / 8) + constants.BYTES.kib(64);
138
- if (s.length > cap) throw E("est/too-large", "the EST payload exceeds the transfer size cap");
139
+ var s = guard.text.decode(body, cap, EstError, {
140
+ charset: "latin1", tooLarge: "est/too-large", badInput: "est/bad-input", label: "the EST payload",
141
+ });
139
142
  var stripped = s.replace(/[\r\n \t]/g, "");
140
143
  if (!/^[A-Za-z0-9+/]*={0,2}$/.test(stripped)) throw E("est/bad-base64", "the EST payload is not RFC 4648 base64 (RFC 8951 sec. 3.1)");
141
144
  var der = Buffer.from(stripped, "base64");
@@ -186,8 +189,9 @@ function _multipartBoundary(contentType) {
186
189
  function splitMultipartMixed(body, contentType) {
187
190
  var boundary = _multipartBoundary(contentType);
188
191
  if (!boundary) throw E("est/bad-multipart", "a serverkeygen response must be multipart/mixed with a boundary (RFC 7030 sec. 4.4.2)");
189
- var text = Buffer.isBuffer(body) ? body.toString("latin1") : String(body);
190
- if (text.length > constants.LIMITS.DER_MAX_BYTES * 2) throw E("est/too-large", "the multipart body exceeds the size cap");
192
+ var text = guard.text.decode(body, constants.LIMITS.DER_MAX_BYTES * 2, EstError, {
193
+ charset: "latin1", tooLarge: "est/too-large", badInput: "est/bad-input", label: "the multipart body",
194
+ });
191
195
  var esc = boundary.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
192
196
  var delimRe = new RegExp("(?:^|\\r?\\n)--" + esc + "(--)?[ \\t]*(?:\\r?\\n|$)", "g");
193
197
  var marks = [], m;
@@ -526,8 +530,13 @@ function classifyResponse(status, headers, body, opts) {
526
530
  }
527
531
  if (status >= 300 && status < 400) return { status: "redirect", location: h["location"] || null };
528
532
  if (status >= 400) {
529
- var text = Buffer.isBuffer(body) ? body.toString("utf8") : String(body || "");
530
- throw E("est/http-error", "EST server returned HTTP " + status + (text ? ": " + text.slice(0, 512) : ""));
533
+ // Decode only a bounded prefix for the message -- a huge error body must not
534
+ // be materialized as a full string just to show its first 512 characters.
535
+ // toString(enc, start, end) bounds the decode WITHOUT constructing a subarray
536
+ // view, so a detached body reads as "" here rather than throwing a raw
537
+ // TypeError that would mask the est/http-error verdict.
538
+ var text = Buffer.isBuffer(body) ? body.toString("utf8", 0, 512) : String(body || "").slice(0, 512);
539
+ throw E("est/http-error", "EST server returned HTTP " + status + (text ? ": " + text : ""));
531
540
  }
532
541
  return { status: "unexpected", httpStatus: status };
533
542
  }
@@ -143,12 +143,13 @@ var CborError = defineClass("CborError", { withCause: true });
143
143
 
144
144
  // OidError -- a malformed object-identifier: fewer than two arcs, a first
145
145
  // arc outside 0..2, a second arc >= 40 under arcs 0/1, or a non-minimal
146
- // base-128 sub-identifier.
147
- var OidError = defineClass("OidError");
146
+ // base-128 sub-identifier. withCause threads a raw byte-view failure (a detached
147
+ // backing ArrayBuffer at fromDER) as the cause rather than discarding it.
148
+ var OidError = defineClass("OidError", { withCause: true });
148
149
 
149
150
  // PemError -- a malformed PEM envelope: missing / mismatched BEGIN/END
150
151
  // markers, a bad label, or non-base64 body.
151
- var PemError = defineClass("PemError");
152
+ var PemError = defineClass("PemError", { withCause: true });
152
153
 
153
154
  // CertificateError -- a byte sequence that is not a well-formed X.509
154
155
  // certificate (wrong outer structure, unparseable field, unsupported
@@ -0,0 +1,45 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // Copyright (c) blamejs contributors
3
+ "use strict";
4
+ //
5
+ // @internal -- no operator-facing namespace. This is lib composition only; the
6
+ // documented surface is the primitives whose boundaries compose these guards.
7
+ //
8
+ // guard-all -- the guard-family orchestrator (schema-all's role for the guard
9
+ // family). It assembles the per-shape fail-closed guards into one namespaced
10
+ // surface every input boundary composes, so a boundary depends on the family
11
+ // rather than re-implementing a guard inline:
12
+ //
13
+ // guard.bytes.view / .source -- untrusted byte-source -> Buffer re-view
14
+ // (detached-buffer fail-open defence)
15
+ // guard.text.decode -- byte-source -> string, cap BEFORE copy
16
+ // (parser-DoS string-allocation defence)
17
+ // guard.limits.cap / .depthCap -- config-time resource-cap validation
18
+ // (recursion / allocation DoS defence)
19
+ // guard.crypto.constantTimeEqual -- length-checked constant-time compare
20
+ // (timing side-channel defence)
21
+ // guard.range.int / .uint31 / .positiveInt31
22
+ // -- bound a decoded integer before narrowing
23
+ // to Number (silent-narrowing defence)
24
+ //
25
+ // Each shape is enforced by a codebase-patterns detector: the characteristic
26
+ // token of a guard (the Buffer.from(x.buffer, byteOffset) re-view, the
27
+ // timingSafeEqual call, the MAX_DECODE_DEPTH_CEILING check) must appear ONLY in
28
+ // its guard module, so a new boundary cannot re-inline the shape and forget the
29
+ // defence.
30
+
31
+ var bytes = require("./guard-bytes");
32
+ var text = require("./guard-text");
33
+ var limits = require("./guard-limits");
34
+ var crypto = require("./guard-crypto");
35
+ var range = require("./guard-range");
36
+ var name = require("./guard-name");
37
+
38
+ module.exports = {
39
+ bytes: bytes,
40
+ text: text,
41
+ limits: limits,
42
+ crypto: crypto,
43
+ range: range,
44
+ name: name,
45
+ };
@@ -0,0 +1,60 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // Copyright (c) blamejs contributors
3
+ "use strict";
4
+ //
5
+ // @internal -- no operator-facing namespace. The documented surface is the
6
+ // primitives whose input boundaries compose these guards (pki.asn1.decode,
7
+ // pki.cbor.decode, pki.ct.parseSctList, pki.webcrypto.*).
8
+ //
9
+ // guard-bytes -- fail-closed coercion of an untrusted byte-source input to a
10
+ // Buffer view. One of the enforced choke points of the guard family: a
11
+ // codebase-patterns detector requires every byte-input boundary to route
12
+ // through here, so the defence below cannot be forgotten at a new boundary.
13
+ //
14
+ // Defends the detached-buffer fail-OPEN: a transferred / structuredClone'd
15
+ // Buffer or view has a detached backing ArrayBuffer and reads as ZERO-LENGTH.
16
+ // An identity fast-path (`Buffer.isBuffer(x) return x`) that skips the re-view
17
+ // hands the caller an empty buffer, so a downstream digest / signature / parse
18
+ // silently processes EMPTY input instead of failing (CWE-20 improper input
19
+ // validation feeding a CWE-347-style verification-of-nothing). Always re-viewing
20
+ // through Buffer.from(x.buffer, x.byteOffset, x.byteLength) turns the detached
21
+ // read into a typed reject at the boundary. Size / length-field allocation
22
+ // bounds (CWE-770 / CWE-400, the parser-DoS class) are NOT enforced here -- they
23
+ // are per-format (a multi-MB CRL is legitimate, a Merkle proof is tiny), so they
24
+ // live in guard-params and each decoder's own cap.
25
+
26
+ // view(input, ErrorClass, code, label) -> Buffer view | throws ErrorClass(code, msg, cause)
27
+ // Accepts a Buffer / Uint8Array -- the DER / CBOR / CT / Merkle input contract.
28
+ // ErrorClass MUST be a withCause PkiError subclass (the raw detach failure is
29
+ // threaded as the cause).
30
+ // @enforced-by guard-shape-reinlined
31
+ // @guard-shape Buffer\.from\(\s*([A-Za-z_$][\w$]*)\.buffer\s*,\s*\1\.byteOffset
32
+ function view(input, ErrorClass, code, label) {
33
+ if (Buffer.isBuffer(input) || input instanceof Uint8Array) {
34
+ try {
35
+ return Buffer.from(input.buffer, input.byteOffset, input.byteLength);
36
+ } catch (e) {
37
+ throw new ErrorClass(code, label + ": input is not a usable byte view (detached backing buffer?)", e);
38
+ }
39
+ }
40
+ throw new ErrorClass(code, label + ": expected a Buffer / Uint8Array");
41
+ }
42
+
43
+ // source(input, ErrorClass, code, label) -> Buffer | throws ErrorClass
44
+ // Accepts the full W3C BufferSource (Buffer / TypedArray view / raw ArrayBuffer)
45
+ // -- the WebCrypto input contract. A raw ArrayBuffer is copied via Buffer.from,
46
+ // which also throws on a detached backing store.
47
+ // @enforced-by guard-shape-reinlined (the re-view shape is declared on view above)
48
+ function source(input, ErrorClass, code, label) {
49
+ var isAb = input instanceof ArrayBuffer;
50
+ if (isAb || ArrayBuffer.isView(input)) {
51
+ try {
52
+ return isAb ? Buffer.from(input) : Buffer.from(input.buffer, input.byteOffset, input.byteLength);
53
+ } catch (e) {
54
+ throw new ErrorClass(code, label + ": input is not a usable byte source (detached backing buffer?)", e);
55
+ }
56
+ }
57
+ throw new ErrorClass(code, label + ": expected a BufferSource (ArrayBuffer / TypedArray / Buffer)");
58
+ }
59
+
60
+ module.exports = { view: view, source: source };
@@ -0,0 +1,34 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // Copyright (c) blamejs contributors
3
+ "use strict";
4
+ //
5
+ // @internal -- no operator-facing namespace. The documented surface is the
6
+ // verify paths whose comparison composes this guard (pki.merkle,
7
+ // pki.webcrypto.subtle.verify).
8
+ //
9
+ // guard-crypto -- fail-closed cryptographic-invariant guards. Enforced choke
10
+ // point: a codebase-patterns detector requires every constant-time byte
11
+ // comparison to route through here, so no verify path re-implements it with a
12
+ // timing leak.
13
+ //
14
+ // Defends the timing side-channel class (CWE-208 observable timing discrepancy;
15
+ // the Lucky13 / MAC-forgery family). A secret-dependent byte comparison with an
16
+ // early exit (=== or a short-circuited &&) leaks the position of the first
17
+ // differing byte, letting an attacker recover a MAC / tag / tree root
18
+ // byte-by-byte. constantTimeEqual runs the whole comparison: a public length
19
+ // gate (lengths are not secret; crypto.timingSafeEqual RangeErrors on unequal
20
+ // lengths, so the gate must precede it) then a single timingSafeEqual over the
21
+ // equal-length buffers -- both operands are always read, no data-dependent
22
+ // branch. A length mismatch is an honest false, never a throw.
23
+
24
+ var nodeCrypto = require("node:crypto");
25
+
26
+ // constantTimeEqual(a, b) -> boolean. Length-checked, then constant-time over
27
+ // equal lengths. a and b are Buffers.
28
+ // @enforced-by guard-shape-reinlined
29
+ // @guard-shape \.timingSafeEqual\s*\(
30
+ function constantTimeEqual(a, b) {
31
+ return a.length === b.length && nodeCrypto.timingSafeEqual(a, b);
32
+ }
33
+
34
+ module.exports = { constantTimeEqual: constantTimeEqual };
@@ -0,0 +1,56 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // Copyright (c) blamejs contributors
3
+ "use strict";
4
+ //
5
+ // @internal -- no operator-facing namespace. The documented surface is the
6
+ // decoders whose option validation composes these guards (pki.asn1.decode,
7
+ // pki.cbor.decode).
8
+ //
9
+ // guard-limits -- fail-closed validation of operator-supplied decode limits at
10
+ // config time (the tier that throws on a bad argument so the operator catches
11
+ // the typo at boot). Enforced choke point: a codebase-patterns detector requires
12
+ // the stack-safe recursion ceiling to live only here, so a new decoder cannot be
13
+ // configured past it.
14
+ //
15
+ // Defends the parser-DoS / resource-exhaustion class (CWE-834 excessive
16
+ // recursion, CWE-770 allocation without limits; MITRE T1499.001; D3FEND
17
+ // Recursion-Bounding + Input-Size-Restriction). A recursive-descent decoder
18
+ // bounded only by a caller-tunable maxDepth overflows the native call stack (a
19
+ // raw RangeError, not a typed verdict) once the cap is raised past the frame
20
+ // limit; depthCap refuses a maxDepth above the stack-safe ceiling. A bad limit
21
+ // is an authoring bug, so this tier throws a TypeError (not a PkiError). NIST
22
+ // 800-53 SI-10 / SC-5 do not require these bounds (framework lag) -- the bound
23
+ // at the parser is the actual control.
24
+
25
+ var constants = require("./constants");
26
+
27
+ // cap(value, key, dflt) -> a validated non-negative integer, or dflt when value
28
+ // is undefined. A non-integer / non-finite / negative value is a config-time
29
+ // TypeError (Number.isInteger already rejects non-numbers, NaN, and Infinity).
30
+ // @enforced-by behavioral -- a config-time cap validator has no rename-proof code
31
+ // shape; the decode-rejects-a-bad-cap RED vectors (asn1/cbor) are the guard.
32
+ function cap(value, key, dflt) {
33
+ if (value === undefined) return dflt;
34
+ if (!Number.isInteger(value) || value < 0) {
35
+ throw new TypeError("decode: " + key + " must be a non-negative integer");
36
+ }
37
+ return value;
38
+ }
39
+
40
+ // depthCap(value, key, dflt) -> a validated recursion-depth cap: cap() plus the
41
+ // stack-safe ceiling, so a raised maxDepth cannot drive recursive descent into a
42
+ // raw RangeError past the native frame limit.
43
+ // @enforced-by guard-shape-reinlined
44
+ // @guard-scope file
45
+ // @guard-shape \bopts\.maxDepth\b
46
+ // @guard-shape \bdepth\s*\+\s*1\b
47
+ // @guard-via \.depthCap\s*\(
48
+ function depthCap(value, key, dflt) {
49
+ var n = cap(value, key, dflt);
50
+ if (n > constants.LIMITS.MAX_DECODE_DEPTH_CEILING) {
51
+ throw new TypeError("decode: " + key + " " + n + " exceeds the stack-safe ceiling " + constants.LIMITS.MAX_DECODE_DEPTH_CEILING);
52
+ }
53
+ return n;
54
+ }
55
+
56
+ module.exports = { cap: cap, depthCap: depthCap };
@@ -0,0 +1,53 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // Copyright (c) blamejs contributors
3
+ "use strict";
4
+ //
5
+ // @internal -- no operator-facing namespace. The documented surface is the
6
+ // consumers whose name-string integrity composes this guard (pki.path.validate
7
+ // DN chaining, pki.schema.x509 GeneralName / SAN decode).
8
+ //
9
+ // guard-name -- fail-closed rejection of an embedded control byte in a
10
+ // distinguished-name / SAN string. Two policies over one concern.
11
+ //
12
+ // Defends the name-truncation / display-confusion class (CVE-2009-2408): a NUL
13
+ // or control byte embedded in a decoded name lets an attacker make two different
14
+ // names compare equal (or a UI truncate at the NUL), so a cert issued for
15
+ // "good.example.com\0.evil.com" is treated as "good.example.com". CWE-158
16
+ // (improper neutralization of null byte) / CWE-20. The reject is at decode, so a
17
+ // truncation name never reaches a comparison or a display.
18
+
19
+ // assertNoControlBytes(str, E, code, label) -> str | throws E(code, ...)
20
+ // DirectoryString policy (a DN attribute value): reject NUL and C0 control bytes
21
+ // in a DECODED name string. TAB (0x09) is exempt; printable non-ASCII (a
22
+ // UTF8String CN carries accented / CJK characters) is allowed. `str` is assumed a
23
+ // string (the caller guards typeof). E is the (code, message) typed-error factory.
24
+ // @enforced-by behavioral -- the control-byte reject has no rename-proof code
25
+ // shape distinct from the ASN.1 charset readers; the CVE-2009-2408 RED vectors
26
+ // (a DN string with an embedded NUL / control byte rejects) are the guard.
27
+ function assertNoControlBytes(str, E, code, label) {
28
+ for (var i = 0; i < str.length; i++) {
29
+ var c = str.charCodeAt(i);
30
+ if (c === 0 || (c < 0x20 && c !== 0x09)) {
31
+ throw E(code, label + " contains an embedded control byte (CVE-2009-2408)");
32
+ }
33
+ }
34
+ return str;
35
+ }
36
+
37
+ // assertPrintableIa5(buf, E, code, label) -> buf | throws E(code, ...)
38
+ // IA5String policy (a dNSName / rfc822Name / URI GeneralName): every byte must be
39
+ // printable 7-bit ASCII [0x20, 0x7e] -- an embedded NUL / control byte enables the
40
+ // same name-truncation bypass downstream. `buf` is the raw GeneralName content.
41
+ // @enforced-by behavioral -- the printable-IA5 byte-range reject has no rename-proof
42
+ // code shape distinct from the ASN.1 IA5 reader; the CVE-2009-2408 RED vectors
43
+ // (a SAN with a control byte rejects) are the guard.
44
+ function assertPrintableIa5(buf, E, code, label) {
45
+ for (var i = 0; i < buf.length; i++) {
46
+ if (buf[i] < 0x20 || buf[i] > 0x7e) {
47
+ throw E(code, label + " must be a printable IA5String (no control bytes)");
48
+ }
49
+ }
50
+ return buf;
51
+ }
52
+
53
+ module.exports = { assertNoControlBytes: assertNoControlBytes, assertPrintableIa5: assertPrintableIa5 };
@@ -0,0 +1,69 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // Copyright (c) blamejs contributors
3
+ "use strict";
4
+ //
5
+ // @internal -- no operator-facing namespace. The documented surface is the
6
+ // parsers whose decoded-integer bounds compose this guard (pki.schema.x509 /
7
+ // crl / pkcs12 / cmp, pki.path.validate).
8
+ //
9
+ // guard-range -- fail-closed bound of an untrusted DECODED integer before it is
10
+ // narrowed to a JS Number. Parse-time (Tier-2): the value is malformed content,
11
+ // so it throws the caller's typed PkiError -- distinct from guard-limits, which
12
+ // validates an operator-supplied option at config time (Tier-1, TypeError).
13
+ //
14
+ // Defends the silent-narrowing class (a decoded ASN.1 INTEGER past 2^53 rounds
15
+ // when coerced to a Number, so a caller acting on the result -- a pathLen /
16
+ // skipCerts / saltLength / iteration counter -- acts on the WRONG number;
17
+ // CWE-190 integer overflow / CWE-681 incorrect conversion). The bound and the
18
+ // narrow are ATOMIC here: a caller cannot obtain the Number without the range
19
+ // having been enforced, so the value can never round silently and be acted on.
20
+
21
+ // 2^31 - 1 -- the shared skip-cert / path-length / salt / iteration ceiling.
22
+ // This literal lives ONLY here (a re-inline anywhere else is flagged: the
23
+ // guard-shape walk anchors on the literal). 2^53 - 1 is the safe-narrow ceiling.
24
+ var UINT31_MAX = 2147483647n;
25
+ var SAFE_MAX = 9007199254740991n;
26
+ var SAFE_MIN = -9007199254740991n;
27
+
28
+ // int(value, min, max, E, code, label) -> a Number in [min, max].
29
+ // value : the BigInt result of an ASN.1 INTEGER / ENUMERATED read.
30
+ // min, max : inclusive BigInt bounds.
31
+ // E : the (code, message, cause) typed-error factory in scope at the call
32
+ // site (ns.E / ctx.E / the module-local E) -- guard-range injects the
33
+ // caller's typed error through it, the tier-appropriate currency (a
34
+ // parse boundary carries a factory, not a bare ErrorClass).
35
+ // code : the frozen domain/reason code this field rejects under.
36
+ // label : field phrase (+ optional RFC cite) for the message.
37
+ // @enforced-by guard-shape-reinlined
38
+ // @guard-shape 2147483647n
39
+ function int(value, min, max, E, code, label) {
40
+ // Config-time authoring guard (Tier-1): narrowing to Number is lossless only
41
+ // when the WHOLE range sits inside the safe-integer band [-(2^53-1), 2^53-1].
42
+ // Both ends bind -- a min below the floor rounds a decoded value near it just
43
+ // as a max above the ceiling does. A wider range needs a BigInt-preserving
44
+ // guard, not this one (this is why the uint64 Merkle-coordinate domain must
45
+ // NOT route here).
46
+ if (max > SAFE_MAX) {
47
+ throw new TypeError("guard.range.int: max " + max + " exceeds the safe-integer ceiling; use a BigInt-preserving guard");
48
+ }
49
+ if (min < SAFE_MIN) {
50
+ throw new TypeError("guard.range.int: min " + min + " is below the safe-integer floor; use a BigInt-preserving guard");
51
+ }
52
+ // Parse-time reject (Tier-2): malformed / out-of-range decoded content.
53
+ if (typeof value !== "bigint" || value < min || value > max) {
54
+ throw E(code, label + " must be an integer within " + min + ".." + max);
55
+ }
56
+ return Number(value);
57
+ }
58
+
59
+ // uint31(value, E, code, label) -> Number in [0, 2^31-1]. The DER-INTEGER
60
+ // non-negative skip/path/salt shape (RFC 5280 / 4211 / 9810).
61
+ // @enforced-by guard-shape-reinlined (shares the 2147483647n shape declared on int)
62
+ function uint31(value, E, code, label) { return int(value, 0n, UINT31_MAX, E, code, label); }
63
+
64
+ // positiveInt31(value, E, code, label) -> Number in [1, 2^31-1]. The PKCS#12
65
+ // PBKDF2 / MAC iteration-count shape (a count of at least one).
66
+ // @enforced-by guard-shape-reinlined (shares the 2147483647n shape declared on int)
67
+ function positiveInt31(value, E, code, label) { return int(value, 1n, UINT31_MAX, E, code, label); }
68
+
69
+ module.exports = { int: int, uint31: uint31, positiveInt31: positiveInt31 };
@@ -0,0 +1,66 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // Copyright (c) blamejs contributors
3
+ "use strict";
4
+ //
5
+ // @internal -- no operator-facing namespace. The documented surface is the
6
+ // primitives whose text boundaries compose this guard (pki.jose.parseJson, the
7
+ // PEM decoders, the EST transfer decoders).
8
+ //
9
+ // guard-text -- fail-closed decode of an untrusted byte-source input to a
10
+ // string, capping the RAW byte length BEFORE the string is materialized.
11
+ // Enforced choke point for the byte->string boundary.
12
+ //
13
+ // Defends the parser-DoS string-allocation class (CWE-770 allocation without
14
+ // limits, CWE-400 uncontrolled resource consumption; MITRE T1499.001). The
15
+ // ORDERING is the invariant: cap the byte length, THEN decode. Decoding a Buffer
16
+ // to a string first allocates a full-size string for input the cap is about to
17
+ // reject; worse, a buffer above Node's max string length escapes the decode as
18
+ // an untyped ERR_STRING_TOO_LONG instead of the caller's typed <tooLarge>. This
19
+ // guard defines the cap-before-copy ordering and the fatal-UTF-8 decode once so
20
+ // no boundary re-derives it (and drifts to cap-after, as some had).
21
+
22
+ var bytes = require("./guard-bytes");
23
+
24
+ var LATIN1 = "latin1";
25
+
26
+ // decode(input, maxBytes, ErrorClass, spec) -> string.
27
+ // input : a Buffer (decoded per spec.charset) or a string (taken as-is).
28
+ // maxBytes : the raw byte-length ceiling, checked BEFORE any string copy.
29
+ // ErrorClass: a PkiError subclass (withCause where spec.fatal is set).
30
+ // spec : { charset: "latin1"|"utf-8" (default latin1),
31
+ // fatal: boolean -- strict UTF-8 (a lone continuation /
32
+ // truncated sequence throws, never substitutes U+FFFD),
33
+ // tooLarge: code for the over-cap reject,
34
+ // badDecode: code for a fatal-charset (bad-UTF-8) reject
35
+ // (required only when fatal is set),
36
+ // badInput: code for a non-Buffer/non-string reject,
37
+ // label: human phrase for the message }
38
+ // @enforced-by behavioral -- cap-before-copy has no rename-proof code shape; the
39
+ // over-cap RED vectors + the detached re-view through guard.bytes.view (itself
40
+ // enforced) are the guard.
41
+ function decode(input, maxBytes, ErrorClass, spec) {
42
+ var charset = spec.charset || LATIN1;
43
+ if (Buffer.isBuffer(input)) {
44
+ // Re-view through the byte guard FIRST so a detached backing ArrayBuffer (a
45
+ // transferred / structuredClone'd Buffer, which reads as zero-length) fails
46
+ // closed here -- the same detached-buffer defence the byte boundaries get --
47
+ // instead of being decoded as an empty string. Then cap, then decode.
48
+ input = bytes.view(input, ErrorClass, spec.badInput, spec.label);
49
+ if (input.length > maxBytes) throw new ErrorClass(spec.tooLarge, spec.label + " exceeds the size cap");
50
+ if (spec.fatal) {
51
+ try { return new TextDecoder(charset, { fatal: true }).decode(input); }
52
+ catch (e) { throw new ErrorClass(spec.badDecode, spec.label + " is not valid " + charset, e); }
53
+ }
54
+ return input.toString(charset);
55
+ }
56
+ if (typeof input === "string") {
57
+ // A latin1 string's char length equals its byte length; a UTF-8 string's
58
+ // does not, so bound the encoded byte length.
59
+ var byteLen = charset === LATIN1 ? input.length : Buffer.byteLength(input, "utf8");
60
+ if (byteLen > maxBytes) throw new ErrorClass(spec.tooLarge, spec.label + " exceeds the size cap");
61
+ return input;
62
+ }
63
+ throw new ErrorClass(spec.badInput, spec.label + " expects a string or Buffer");
64
+ }
65
+
66
+ module.exports = { decode: decode };