@blamejs/pki 0.1.32 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,149 @@
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 JSON integrity composes this guard (pki.jose message parsing,
7
+ // pki.acme resource objects, pki.webcrypto JWK unwrap).
8
+ //
9
+ // guard-json -- strict, bounded parse of an untrusted JSON document. JSON.parse
10
+ // silently takes the LAST value of a DUPLICATE member, so a signed / wrapped
11
+ // object carrying two members of the same name resolves differently for a
12
+ // verifier than for a consumer -- the JSON smuggling / parser-differential class
13
+ // (CWE-20 / CWE-436). It also caps neither size nor nesting (CWE-770 / CWE-400),
14
+ // and over a Buffer substitutes U+FFFD for invalid UTF-8 rather than failing.
15
+ //
16
+ // This is a single hand-written recursive-descent reader that composes
17
+ // guard.text.decode (the byte cap runs BEFORE the strict/fatal UTF-8 decode, so
18
+ // an oversized document is rejected before it is materialized), then rejects a
19
+ // duplicate member at EVERY nesting depth, caps nesting, assigns each member as
20
+ // an OWN data property (a "__proto__" key becomes a normal member -- it cannot
21
+ // mutate the prototype nor, being a non-own assignment for a primitive, silently
22
+ // defeat the duplicate-member gate), and enforces the RFC 8259 number/string
23
+ // grammar (no leading zero, a fraction/exponent needs a digit, no bare "-").
24
+ //
25
+ var text = require("./guard-text");
26
+ var limits = require("./guard-limits");
27
+
28
+ // parse(input, ErrorClass, spec) -> value. `input` is a Buffer or a string.
29
+ // spec = { maxBytes, maxDepth, badJson, tooDeep, duplicateMember, tooLarge,
30
+ // badInput, label } -- the caller's caps + frozen domain/reason codes.
31
+ // Both caps are REQUIRED authoring inputs: an omitted / NaN / fractional cap
32
+ // would silently disable the bound it configures (a depth-uncapped recursive
33
+ // descent escapes as a raw stack-overflow RangeError), so they are validated
34
+ // through the shared cap guards at entry -- maxDepth additionally against the
35
+ // stack-safe recursion ceiling, so no caller cap can exceed frame safety.
36
+ // @enforced-by json-parse-not-via-guard
37
+ function parse(input, ErrorClass, spec) {
38
+ function E(code, message, cause) { return new ErrorClass(code, message, cause); }
39
+ if (spec.maxBytes === undefined || spec.maxDepth === undefined) {
40
+ throw new TypeError("guard.json.parse: spec.maxBytes and spec.maxDepth are required");
41
+ }
42
+ var maxBytes = limits.cap(spec.maxBytes, "guard.json.parse spec.maxBytes", undefined);
43
+ var maxDepth = limits.depthCap(spec.maxDepth, "guard.json.parse spec.maxDepth", undefined);
44
+ // Byte cap BEFORE the fatal UTF-8 decode (an oversized/ill-encoded document is
45
+ // rejected before it is turned into a string).
46
+ var str = text.decode(input, maxBytes, ErrorClass, {
47
+ charset: "utf-8", fatal: true, tooLarge: spec.tooLarge, badDecode: spec.badJson, badInput: spec.badInput, label: spec.label,
48
+ });
49
+ var i = 0, n = str.length;
50
+ function ws() { while (i < n) { var c = str.charCodeAt(i); if (c === 0x20 || c === 0x09 || c === 0x0a || c === 0x0d) i++; else break; } }
51
+ function fail(msg) { throw E(spec.badJson, "invalid JSON at offset " + i + ": " + msg); }
52
+ function value(depth) {
53
+ if (depth > maxDepth) throw E(spec.tooDeep, "JSON nesting exceeds the depth cap");
54
+ ws();
55
+ if (i >= n) fail("unexpected end of input");
56
+ var c = str[i];
57
+ if (c === "{") return object(depth);
58
+ if (c === "[") return array(depth);
59
+ if (c === "\"") return string();
60
+ if (c === "-" || (c >= "0" && c <= "9")) return number();
61
+ if (str.substr(i, 4) === "true") { i += 4; return true; }
62
+ if (str.substr(i, 5) === "false") { i += 5; return false; }
63
+ if (str.substr(i, 4) === "null") { i += 4; return null; }
64
+ fail("unexpected token");
65
+ return undefined;
66
+ }
67
+ function object(depth) {
68
+ i++; // {
69
+ var out = {};
70
+ ws();
71
+ if (str[i] === "}") { i++; return out; }
72
+ for (;;) {
73
+ ws();
74
+ if (str[i] !== "\"") fail("expected a string key");
75
+ var key = string();
76
+ if (Object.prototype.hasOwnProperty.call(out, key)) throw E(spec.duplicateMember, "duplicate JSON member " + JSON.stringify(key));
77
+ ws();
78
+ if (str[i] !== ":") fail("expected ':'");
79
+ i++;
80
+ // Own data property (not out[key]=...): a "__proto__" key becomes a normal
81
+ // member, never a prototype mutation nor a silent duplicate-gate bypass.
82
+ Object.defineProperty(out, key, { value: value(depth + 1), writable: true, enumerable: true, configurable: true });
83
+ ws();
84
+ if (str[i] === ",") { i++; continue; }
85
+ if (str[i] === "}") { i++; return out; }
86
+ fail("expected ',' or '}'");
87
+ }
88
+ }
89
+ function array(depth) {
90
+ i++; // [
91
+ var out = [];
92
+ ws();
93
+ if (str[i] === "]") { i++; return out; }
94
+ for (;;) {
95
+ out.push(value(depth + 1));
96
+ ws();
97
+ if (str[i] === ",") { i++; continue; }
98
+ if (str[i] === "]") { i++; return out; }
99
+ fail("expected ',' or ']'");
100
+ }
101
+ }
102
+ function string() {
103
+ i++; // opening quote
104
+ var s = "";
105
+ for (;;) {
106
+ if (i >= n) fail("unterminated string");
107
+ var c = str[i++];
108
+ if (c === "\"") return s;
109
+ if (c === "\\") {
110
+ if (i >= n) fail("unterminated escape");
111
+ var e = str[i++];
112
+ if (e === "\"") s += "\"";
113
+ else if (e === "\\") s += "\\";
114
+ else if (e === "/") s += "/";
115
+ else if (e === "b") s += "\b";
116
+ else if (e === "f") s += "\f";
117
+ else if (e === "n") s += "\n";
118
+ else if (e === "r") s += "\r";
119
+ else if (e === "t") s += "\t";
120
+ else if (e === "u") {
121
+ var hex = str.substr(i, 4);
122
+ if (!/^[0-9a-fA-F]{4}$/.test(hex)) fail("bad \\u escape");
123
+ s += String.fromCharCode(parseInt(hex, 16));
124
+ i += 4;
125
+ } else fail("bad escape");
126
+ } else if (c.charCodeAt(0) < 0x20) {
127
+ fail("control character in string");
128
+ } else s += c;
129
+ }
130
+ }
131
+ function number() {
132
+ var start = i;
133
+ if (str[i] === "-") i++;
134
+ while (i < n && str[i] >= "0" && str[i] <= "9") i++;
135
+ if (str[i] === ".") { i++; while (i < n && str[i] >= "0" && str[i] <= "9") i++; }
136
+ if (str[i] === "e" || str[i] === "E") { i++; if (str[i] === "+" || str[i] === "-") i++; while (i < n && str[i] >= "0" && str[i] <= "9") i++; }
137
+ var tok = str.slice(start, i);
138
+ if (!/^-?(0|[1-9][0-9]*)(\.[0-9]+)?([eE][+-]?[0-9]+)?$/.test(tok)) fail("malformed number");
139
+ var v = Number(tok);
140
+ if (!isFinite(v)) fail("bad number");
141
+ return v;
142
+ }
143
+ var result = value(0);
144
+ ws();
145
+ if (i !== n) fail("trailing content after JSON value");
146
+ return result;
147
+ }
148
+
149
+ module.exports = { parse: parse };
@@ -24,15 +24,39 @@
24
24
 
25
25
  var constants = require("./constants");
26
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).
27
+ // cap(value, key, dflt, opts) -> a validated bounded integer, or dflt when value
28
+ // is undefined. A non-integer / non-finite / negative value fails closed
29
+ // (Number.isInteger already rejects non-numbers, NaN, and Infinity).
30
+ //
31
+ // With no opts it is the config-time codec default: a bare TypeError with the
32
+ // "decode:" prefix and a >= 0 floor (the asn1-der / cbor-det callers are
33
+ // untouched). opts lets a caller in another currency reuse the same integer
34
+ // floor with its OWN typed verdict and bounds -- so a fractional/negative/NaN
35
+ // bound cannot be spelled a fifth way with a dropped Number.isInteger:
36
+ // E - a (code, message) -> Error factory; when given, an out-of-range
37
+ // value throws E(code, ...) instead of the bare TypeError.
38
+ // code - the domain/reason code passed to E.
39
+ // label - names the argument in the message (defaults to key).
40
+ // min - inclusive lower bound (default 0).
41
+ // max - inclusive upper bound (optional).
30
42
  // @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) {
43
+ // shape; the decode-rejects-a-bad-cap RED vectors (asn1/cbor/path) are the guard.
44
+ function cap(value, key, dflt, opts) {
33
45
  if (value === undefined) return dflt;
34
- if (!Number.isInteger(value) || value < 0) {
35
- throw new TypeError("decode: " + key + " must be a non-negative integer");
46
+ opts = opts || {};
47
+ var min = opts.min === undefined ? 0 : opts.min;
48
+ // The bounds themselves are authoring inputs: a NaN / fractional min or max
49
+ // compares false against every value, silently disabling the check (a NaN min
50
+ // even REPLACES the default >= 0 floor). Always a config-time TypeError --
51
+ // the bound wiring is broken regardless of the value currency opts.E selects.
52
+ if (!Number.isInteger(min) || (opts.max !== undefined && !Number.isInteger(opts.max))) {
53
+ throw new TypeError("guard.limits.cap: the min/max bounds for " + key + " must be integers");
54
+ }
55
+ if (!Number.isInteger(value) || value < min || (opts.max !== undefined && value > opts.max)) {
56
+ var want = "an integer >= " + min + (opts.max !== undefined ? " and <= " + opts.max : "");
57
+ if (min === 0 && opts.max === undefined) want = "a non-negative integer";
58
+ if (opts.E) throw opts.E(opts.code, (opts.label || key) + " must be " + want);
59
+ throw new TypeError("decode: " + key + " must be " + want);
36
60
  }
37
61
  return value;
38
62
  }
@@ -53,4 +77,30 @@ function depthCap(value, key, dflt) {
53
77
  return n;
54
78
  }
55
79
 
56
- module.exports = { cap: cap, depthCap: depthCap };
80
+ // counter(max, E, code, label) -> { tick }. A parse-time item counter: the
81
+ // recursive decoder calls tick() once per decoded element so a small input
82
+ // cannot fan out into a massive eager node/element tree (CWE-770 / CWE-400 memory
83
+ // amplification -- the class pki.cbor closed with CBOR_MAX_ITEMS). Unlike cap
84
+ // (config-time TypeError), this is Tier-2 parse validation: an over-count throws
85
+ // the caller's typed PkiError via the E(code, message) factory. `max` is the
86
+ // caller's already-validated ceiling (an opts.maxItems run through cap()).
87
+ // @enforced-by behavioral -- a monotone counter has no rename-proof code shape
88
+ // distinct from any `n++ > max` loop bound; the memory-fanout RED vectors (a
89
+ // dense TLV run rejects at the cap) driving the composing decoders are the guard.
90
+ function counter(max, E, code, label) {
91
+ // The ceiling is an authoring input: an undefined / NaN / fractional max
92
+ // builds a counter whose `n > max` NEVER fires -- a silently dead fanout
93
+ // defence. Reject at construction (config-time TypeError).
94
+ if (!Number.isInteger(max) || max < 0) {
95
+ throw new TypeError("guard.limits.counter: max must be a non-negative integer");
96
+ }
97
+ var n = 0;
98
+ return {
99
+ tick: function () {
100
+ n += 1;
101
+ if (n > max) throw E(code, (label || "decoded item") + " count exceeds the cap " + max);
102
+ },
103
+ };
104
+ }
105
+
106
+ module.exports = { cap: cap, depthCap: depthCap, counter: counter };
@@ -24,6 +24,9 @@
24
24
  var UINT31_MAX = 2147483647n;
25
25
  var SAFE_MAX = 9007199254740991n;
26
26
  var SAFE_MIN = -9007199254740991n;
27
+ // 2^64 - 1 (also spelled 0xffffffffffffffffn). The uint64 ceiling lives ONLY here
28
+ // (both spellings are flagged as a re-inline; see uint64's @guard-shape lines).
29
+ var UINT64_MAX = 18446744073709551615n;
27
30
 
28
31
  // int(value, min, max, E, code, label) -> a Number in [min, max].
29
32
  // value : the BigInt result of an ASN.1 INTEGER / ENUMERATED read.
@@ -37,12 +40,17 @@ var SAFE_MIN = -9007199254740991n;
37
40
  // @enforced-by guard-shape-reinlined
38
41
  // @guard-shape 2147483647n
39
42
  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).
43
+ // Config-time authoring guard (Tier-1): the bounds must be BigInt -- a Number
44
+ // bound (worst: NaN, which compares false against every BigInt) silently
45
+ // disables the range so ANY decoded value narrows. Then: narrowing to Number
46
+ // is lossless only when the WHOLE range sits inside the safe-integer band
47
+ // [-(2^53-1), 2^53-1]. Both ends bind -- a min below the floor rounds a
48
+ // decoded value near it just as a max above the ceiling does. A wider range
49
+ // needs a BigInt-preserving guard, not this one (this is why the uint64
50
+ // Merkle-coordinate domain must NOT route here).
51
+ if (typeof min !== "bigint" || typeof max !== "bigint") {
52
+ throw new TypeError("guard.range.int: the min/max bounds must be BigInt");
53
+ }
46
54
  if (max > SAFE_MAX) {
47
55
  throw new TypeError("guard.range.int: max " + max + " exceeds the safe-integer ceiling; use a BigInt-preserving guard");
48
56
  }
@@ -66,4 +74,29 @@ function uint31(value, E, code, label) { return int(value, 0n, UINT31_MAX, E, co
66
74
  // @enforced-by guard-shape-reinlined (shares the 2147483647n shape declared on int)
67
75
  function positiveInt31(value, E, code, label) { return int(value, 1n, UINT31_MAX, E, code, label); }
68
76
 
69
- module.exports = { int: int, uint31: uint31, positiveInt31: positiveInt31 };
77
+ // uint64(value, E, code, label) -> a BigInt in [0, 2^64-1], WITHOUT narrowing to
78
+ // Number. Accepts a non-negative safe-integer Number (widened to BigInt) or a
79
+ // BigInt; a Number past 2^53 (precision already lost), a negative, a non-integer,
80
+ // or a non-number-non-bigint throws the caller's typed verdict. The uint64 domain
81
+ // (a Merkle tree coordinate RFC 9162, an SCT timestamp RFC 6962) must NOT route
82
+ // through int() -- narrowing to Number would corrupt the very value it guards, so
83
+ // this is the BigInt-preserving sibling int()'s authoring guard points callers to.
84
+ // @enforced-by guard-shape-reinlined
85
+ // @guard-shape 18446744073709551615n
86
+ // @guard-shape 0xffffffffffffffffn
87
+ function uint64(value, E, code, label) {
88
+ var v;
89
+ if (typeof value === "bigint") {
90
+ v = value;
91
+ } else if (typeof value === "number" && Number.isSafeInteger(value) && value >= 0) {
92
+ v = BigInt(value);
93
+ } else {
94
+ throw E(code, label + " must be a non-negative safe-integer Number or a BigInt (a uint64)");
95
+ }
96
+ if (v < 0n || v > UINT64_MAX) {
97
+ throw E(code, label + " must be a uint64 in [0, 2^64)");
98
+ }
99
+ return v;
100
+ }
101
+
102
+ module.exports = { int: int, uint31: uint31, positiveInt31: positiveInt31, uint64: uint64 };
package/lib/guard-text.js CHANGED
@@ -39,6 +39,13 @@ var LATIN1 = "latin1";
39
39
  // over-cap RED vectors + the detached re-view through guard.bytes.view (itself
40
40
  // enforced) are the guard.
41
41
  function decode(input, maxBytes, ErrorClass, spec) {
42
+ // maxBytes is an authoring input: an undefined / NaN / fractional cap makes
43
+ // the `length > maxBytes` comparison always false -- the size cap silently
44
+ // disabled on the guard whose contract is cap-BEFORE-copy. Config-time
45
+ // TypeError (every composing boundary passes a C.LIMITS constant).
46
+ if (!Number.isInteger(maxBytes) || maxBytes < 0) {
47
+ throw new TypeError("guard.text.decode: maxBytes must be a non-negative integer");
48
+ }
42
49
  var charset = spec.charset || LATIN1;
43
50
  if (Buffer.isBuffer(input)) {
44
51
  // Re-view through the byte guard FIRST so a detached backing ArrayBuffer (a
package/lib/jose.js CHANGED
@@ -47,8 +47,6 @@ var LIMITS = constants.LIMITS;
47
47
 
48
48
  // ---- strict base64url codec (RFC 7515 sec. 2 / RFC 4648 sec. 5) ----------
49
49
 
50
- var B64U_ALPHABET = /^[A-Za-z0-9_-]*$/;
51
-
52
50
  /**
53
51
  * @primitive pki.jose.base64url.encode
54
52
  * @signature pki.jose.base64url.encode(bytes) -> string
@@ -65,6 +63,9 @@ var B64U_ALPHABET = /^[A-Za-z0-9_-]*$/;
65
63
  */
66
64
  function b64uEncode(bytes) {
67
65
  if (!Buffer.isBuffer(bytes)) throw E("jose/bad-input", "base64url.encode requires a Buffer");
66
+ // Re-view through the byte guard: a detached-backed Buffer reads as zero-length,
67
+ // so a bare .toString would silently encode "" -- fail closed instead.
68
+ bytes = guard.bytes.view(bytes, JoseError, "jose/bad-input", "base64url.encode input");
68
69
  return bytes.toString("base64url");
69
70
  }
70
71
 
@@ -85,131 +86,14 @@ function b64uEncode(bytes) {
85
86
  * pki.jose.base64url.decode("AQID"); // -> <Buffer 01 02 03>
86
87
  */
87
88
  function b64uDecode(text) {
88
- if (typeof text !== "string") throw E("jose/bad-base64url", "base64url.decode requires a string");
89
- if (!B64U_ALPHABET.test(text)) throw E("jose/bad-base64url", "value is not base64url (padding or a non-alphabet character)");
90
- // length % 4 === 1 is not a possible base64 encoding length.
91
- if (text.length % 4 === 1) throw E("jose/bad-base64url", "base64url value has an impossible length");
92
- var buf = Buffer.from(text, "base64url");
93
- // Re-encode: a non-canonical final character (non-zero discarded bits) yields
94
- // a different canonical form, so the round-trip fails closed.
95
- if (buf.toString("base64url") !== text) throw E("jose/bad-base64url", "base64url value is not canonical");
96
- return buf;
89
+ // The strict alphabet + impossible-length + canonical-round-trip discipline
90
+ // lives once in the shared encoding guard (RFC 4648 sec. 5 / RFC 8555 sec. 6.1);
91
+ // the frozen jose/bad-base64url code is threaded through.
92
+ return guard.encoding.base64url(text, null, E, "jose/bad-base64url", "base64url value");
97
93
  }
98
94
 
99
95
  // ---- strict bounded JSON reader ------------------------------------------
100
96
 
101
- // A single hand-written recursive-descent reader (never JSON.parse, which
102
- // silently takes the LAST of a duplicate member -- the smuggling class). It
103
- // enforces the byte-size cap, the depth cap, and duplicate-member rejection at
104
- // EVERY nesting level, and reads UTF-8 strictly. Returns the parsed value.
105
- function _jsonReader(str) {
106
- var i = 0, n = str.length;
107
- function ws() { while (i < n) { var c = str.charCodeAt(i); if (c === 0x20 || c === 0x09 || c === 0x0a || c === 0x0d) i++; else break; } }
108
- function fail(msg) { throw E("jose/bad-json", "invalid JSON at offset " + i + ": " + msg); }
109
- function value(depth) {
110
- if (depth > LIMITS.JSON_MAX_DEPTH) throw E("jose/too-deep", "JSON nesting exceeds the depth cap");
111
- ws();
112
- if (i >= n) fail("unexpected end of input");
113
- var c = str[i];
114
- if (c === "{") return object(depth);
115
- if (c === "[") return array(depth);
116
- if (c === "\"") return string();
117
- if (c === "-" || (c >= "0" && c <= "9")) return number();
118
- if (str.substr(i, 4) === "true") { i += 4; return true; }
119
- if (str.substr(i, 5) === "false") { i += 5; return false; }
120
- if (str.substr(i, 4) === "null") { i += 4; return null; }
121
- fail("unexpected token");
122
- return undefined;
123
- }
124
- function object(depth) {
125
- i++; // {
126
- var out = {};
127
- ws();
128
- if (str[i] === "}") { i++; return out; }
129
- for (;;) {
130
- ws();
131
- if (str[i] !== "\"") fail("expected a string key");
132
- var key = string();
133
- if (Object.prototype.hasOwnProperty.call(out, key)) throw E("jose/duplicate-member", "duplicate JSON member " + JSON.stringify(key));
134
- ws();
135
- if (str[i] !== ":") fail("expected ':'");
136
- i++;
137
- // Assign as an OWN data property via defineProperty (not out[key] = ...): a
138
- // "__proto__" key must become a normal own member, not mutate the object's
139
- // prototype. A bare `out["__proto__"] = v` would pollute the returned object
140
- // AND (for a primitive v) create no own property, silently defeating the
141
- // duplicate-member gate above on a repeated __proto__.
142
- Object.defineProperty(out, key, { value: value(depth + 1), writable: true, enumerable: true, configurable: true });
143
- ws();
144
- if (str[i] === ",") { i++; continue; }
145
- if (str[i] === "}") { i++; return out; }
146
- fail("expected ',' or '}'");
147
- }
148
- }
149
- function array(depth) {
150
- i++; // [
151
- var out = [];
152
- ws();
153
- if (str[i] === "]") { i++; return out; }
154
- for (;;) {
155
- out.push(value(depth + 1));
156
- ws();
157
- if (str[i] === ",") { i++; continue; }
158
- if (str[i] === "]") { i++; return out; }
159
- fail("expected ',' or ']'");
160
- }
161
- }
162
- function string() {
163
- i++; // opening quote
164
- var s = "";
165
- for (;;) {
166
- if (i >= n) fail("unterminated string");
167
- var c = str[i++];
168
- if (c === "\"") return s;
169
- if (c === "\\") {
170
- if (i >= n) fail("unterminated escape");
171
- var e = str[i++];
172
- if (e === "\"") s += "\"";
173
- else if (e === "\\") s += "\\";
174
- else if (e === "/") s += "/";
175
- else if (e === "b") s += "\b";
176
- else if (e === "f") s += "\f";
177
- else if (e === "n") s += "\n";
178
- else if (e === "r") s += "\r";
179
- else if (e === "t") s += "\t";
180
- else if (e === "u") {
181
- var hex = str.substr(i, 4);
182
- if (!/^[0-9a-fA-F]{4}$/.test(hex)) fail("bad \\u escape");
183
- s += String.fromCharCode(parseInt(hex, 16));
184
- i += 4;
185
- } else fail("bad escape");
186
- } else if (c.charCodeAt(0) < 0x20) {
187
- fail("control character in string");
188
- } else s += c;
189
- }
190
- }
191
- function number() {
192
- var start = i;
193
- if (str[i] === "-") i++;
194
- while (i < n && str[i] >= "0" && str[i] <= "9") i++;
195
- if (str[i] === ".") { i++; while (i < n && str[i] >= "0" && str[i] <= "9") i++; }
196
- if (str[i] === "e" || str[i] === "E") { i++; if (str[i] === "+" || str[i] === "-") i++; while (i < n && str[i] >= "0" && str[i] <= "9") i++; }
197
- var tok = str.slice(start, i);
198
- // Enforce the RFC 8259 number grammar the greedy scan does not: no leading zero
199
- // ("01"), a fraction and an exponent each need at least one digit ("1.", "1e",
200
- // "1.e1"), and a bare "-" is not a number. A lenient Number() would accept these
201
- // and reintroduce a parser differential against a strict JSON reader.
202
- if (!/^-?(0|[1-9][0-9]*)(\.[0-9]+)?([eE][+-]?[0-9]+)?$/.test(tok)) fail("malformed number");
203
- var v = Number(tok);
204
- if (!isFinite(v)) fail("bad number");
205
- return v;
206
- }
207
- var result = value(0);
208
- ws();
209
- if (i !== n) fail("trailing content after JSON value");
210
- return result;
211
- }
212
-
213
97
  /**
214
98
  * @primitive pki.jose.parseJson
215
99
  * @signature pki.jose.parseJson(input) -> value
@@ -229,12 +113,14 @@ function _jsonReader(str) {
229
113
  * pki.jose.parseJson('{"a":1}'); // -> { a: 1 }
230
114
  */
231
115
  function parseJson(input) {
232
- // guard.text.decode caps the raw byte length BEFORE the (strict, fatal) UTF-8
233
- // decode, so an oversized document is rejected before it is materialized.
234
- var str = guard.text.decode(input, LIMITS.JSON_MAX_BYTES, JoseError, {
235
- charset: "utf-8", fatal: true, tooLarge: "jose/too-large", badDecode: "jose/bad-json", badInput: "jose/bad-input", label: "the JSON document",
116
+ // The strict bounded reader (byte + depth caps, duplicate-member reject at every
117
+ // depth, __proto__-safe own-property assignment, fatal UTF-8, RFC 8259 grammar)
118
+ // lives once in the shared JSON guard; the frozen jose/* codes are threaded through.
119
+ return guard.json.parse(input, JoseError, {
120
+ maxBytes: LIMITS.JSON_MAX_BYTES, maxDepth: LIMITS.JSON_MAX_DEPTH,
121
+ badJson: "jose/bad-json", tooDeep: "jose/too-deep", duplicateMember: "jose/duplicate-member",
122
+ tooLarge: "jose/too-large", badInput: "jose/bad-input", label: "the JSON document",
236
123
  });
237
- return _jsonReader(str);
238
124
  }
239
125
 
240
126
  // ---- JOSE algorithm registry (RFC 7518 / 8037 / 9964) --------------------
@@ -513,6 +399,9 @@ async function sign(opts) {
513
399
  var header = opts.protected;
514
400
  var payload = opts.payload;
515
401
  if (!Buffer.isBuffer(payload)) throw E("jose/bad-input", "payload must be a Buffer (empty Buffer for POST-as-GET)");
402
+ // Re-view before the length===0 fast-path: a DETACHED payload reads as zero-length
403
+ // and would be silently signed as an empty POST-as-GET body -- fail closed instead.
404
+ payload = guard.bytes.view(payload, JoseError, "jose/bad-input", "payload");
516
405
  // A non-CryptoKey (a raw Buffer, a string, a JWK object) would reach subtle.sign
517
406
  // and throw a bare TypeError; require a CryptoKey (it carries an `algorithm`) so
518
407
  // every caller -- and every acme builder that composes sign -- fails closed.
package/lib/merkle.js CHANGED
@@ -48,8 +48,11 @@ var guard = require("./guard-all");
48
48
 
49
49
  var MerkleError = frameworkError.MerkleError;
50
50
 
51
+ // (code, message) -> MerkleError, the factory the composed guards throw through
52
+ // so a malformed coordinate keeps the merkle/* typed verdict.
53
+ function _merkleErr(c, m) { return new MerkleError(c, m); }
54
+
51
55
  var SHA256_BYTES = 32;
52
- var UINT64_MAX = 18446744073709551615n; // 2n ** 64n - 1n
53
56
  var LEAF_PREFIX = Buffer.from([0x00]);
54
57
  var NODE_PREFIX = Buffer.from([0x01]);
55
58
  var EMPTY = Buffer.alloc(0);
@@ -75,21 +78,9 @@ function _node32(v, field) {
75
78
  // throws -- the number-narrows-unbounded-integer discipline for uint64 tree
76
79
  // coordinates.
77
80
  function _coerceCoord(v, field) {
78
- var b;
79
- if (typeof v === "bigint") {
80
- b = v;
81
- } else if (typeof v === "number") {
82
- if (!Number.isSafeInteger(v) || v < 0) {
83
- throw new MerkleError("merkle/bad-input", field + " must be a non-negative safe integer or a BigInt (a Number >= 2^53 loses precision)");
84
- }
85
- b = BigInt(v);
86
- } else {
87
- throw new MerkleError("merkle/bad-input", field + " must be a number or a BigInt");
88
- }
89
- if (b < 0n || b > UINT64_MAX) {
90
- throw new MerkleError("merkle/bad-input", field + " must be in [0, 2^64)");
91
- }
92
- return b;
81
+ // A tree coordinate is a uint64 (RFC 9162): the BigInt-preserving range guard
82
+ // owns the 0..2^64-1 bound and the safe-integer widening in one place.
83
+ return guard.range.uint64(v, _merkleErr, "merkle/bad-input", field);
93
84
  }
94
85
 
95
86
  function _proofNodes(proof) {
package/lib/oid.js CHANGED
@@ -39,6 +39,10 @@ var guard = require("./guard-all");
39
39
 
40
40
  var OidError = frameworkError.OidError;
41
41
 
42
+ // (code, message) -> OidError, the factory shape the composed guards throw
43
+ // through so a malformed identifier keeps the oid/* typed verdict.
44
+ function _oidError(c, m) { return new OidError(c, m); }
45
+
42
46
  // FAMILIES -- OIDs grouped by their shared base arc (the "similar starting
43
47
  // variable" that defines a class). A member is `name: leaf`, where leaf is a
44
48
  // trailing arc (number) or a short arc array for a multi-level leaf; the full
@@ -302,11 +306,18 @@ Object.keys(FAMILIES).forEach(function (fam) {
302
306
  });
303
307
 
304
308
  function _assertDotted(dotted, who) {
305
- // No leading-zero components: "01.2.840" is a key no decoded OID can match,
306
- // and the arc converters round-trip it to a DIFFERENT OID ("1.2.840").
307
- if (typeof dotted !== "string" || !/^(0|[1-9]\d*)(\.(0|[1-9]\d*))+$/.test(dotted)) {
308
- throw new OidError("oid/bad-input", who + ": expected a dotted-decimal OID string (no leading-zero components)");
309
- }
309
+ // SYNTAX only (canonical dotted form, no leading-zero arc that would round-trip
310
+ // to a DIFFERENT OID). For the LOOKUP entry points (name / has): a well-formed
311
+ // but non-encodable OID is simply not registered (a miss), not an error, so the
312
+ // X.660 arc bounds are waived (boundsCode null).
313
+ guard.identifier.assertCanonicalOid(dotted, _oidError, "oid/bad-input", who, null);
314
+ }
315
+
316
+ function _assertEncodable(dotted, who) {
317
+ // SYNTAX and the X.660 arc bounds -- for the paths that assert / convert a real
318
+ // encodable OID (toArcs / toDER / register), where an out-of-bounds arc is a
319
+ // hard reject (oid/bad-arc), so the string form agrees with the DER form.
320
+ guard.identifier.assertCanonicalOid(dotted, _oidError, "oid/bad-input", who, "oid/bad-arc");
310
321
  }
311
322
 
312
323
  // X.660 encodability: the root arc is 0..2 and, under roots 0 and 1, the
@@ -370,9 +381,11 @@ function has(dotted) {
370
381
  * pki.oid.register("1.3.6.1.4.1.99999.1", "acmeWidgetPolicy");
371
382
  */
372
383
  function register(dotted, n) {
373
- _assertDotted(dotted, "register");
384
+ // _assertDotted now enforces the X.660 arc bounds on the string form too, so
385
+ // the separate arc-based check register used to run is redundant here (it is
386
+ // kept for registerFamily, which validates arcs it assembles, not a string).
387
+ _assertEncodable(dotted, "register");
374
388
  if (typeof n !== "string" || n.length === 0) throw new OidError("oid/bad-input", "register: name must be a non-empty string");
375
- _assertEncodableArcs(dotted.split(".").map(BigInt), "register");
376
389
  _index(dotted, n);
377
390
  }
378
391
 
@@ -432,7 +445,7 @@ function all() {
432
445
  // Number where safe and BigInt where an arc exceeds 2^53 (rare, but a
433
446
  // UUID-based OID arc can), so the round-trip never loses precision.
434
447
  function toArcs(dotted) {
435
- _assertDotted(dotted, "toArcs");
448
+ _assertEncodable(dotted, "toArcs");
436
449
  return dotted.split(".").map(function (p) {
437
450
  var b = BigInt(p);
438
451
  return b <= 9007199254740991n ? Number(b) : b;
@@ -453,7 +466,7 @@ function fromArcs(arcs) {
453
466
 
454
467
  // DER convenience -- thin pass-throughs to the codec so callers reach for
455
468
  // one namespace when they have a dotted string in hand.
456
- function toDER(dotted) { _assertDotted(dotted, "toDER"); return asn1.build.oid(dotted); }
469
+ function toDER(dotted) { _assertEncodable(dotted, "toDER"); return asn1.build.oid(dotted); }
457
470
  function fromDER(input) {
458
471
  var buf = guard.bytes.view(input, OidError, "oid/bad-input", "fromDER");
459
472
  var node = asn1.decode(buf);