@blamejs/pki 0.1.32 → 0.2.1
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 +38 -0
- package/README.md +3 -1
- package/index.js +13 -0
- package/lib/asn1-der.js +31 -22
- package/lib/constants.js +33 -0
- package/lib/ct.js +13 -12
- package/lib/est.js +10 -10
- package/lib/framework-error.js +13 -0
- package/lib/guard-all.js +15 -0
- package/lib/guard-crypto.js +31 -1
- package/lib/guard-encoding.js +90 -0
- package/lib/guard-identifier.js +56 -0
- package/lib/guard-json.js +149 -0
- package/lib/guard-limits.js +58 -8
- package/lib/guard-range.js +40 -7
- package/lib/guard-text.js +7 -0
- package/lib/jose.js +17 -128
- package/lib/merkle.js +7 -16
- package/lib/oid.js +33 -10
- package/lib/path-validate.js +173 -16
- package/lib/schema-attrcert.js +5 -0
- package/lib/schema-cms.js +22 -1
- package/lib/schema-crmf.js +3 -5
- package/lib/schema-ocsp.js +5 -1
- package/lib/schema-pkix.js +92 -12
- package/lib/shbs.js +348 -0
- package/lib/trust.js +707 -0
- package/lib/webcrypto.js +18 -7
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
|
@@ -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
|
+
// consumers whose identifier-string integrity composes this guard (pki.oid
|
|
7
|
+
// name/arc resolution, pki.asn1 OID encoding, pki.path.validate requiredEku /
|
|
8
|
+
// userInitialPolicySet key checking).
|
|
9
|
+
//
|
|
10
|
+
// guard-identifier -- fail-closed canonicalization of the structured-identifier
|
|
11
|
+
// STRINGS the toolkit compares and encodes. An identifier used as a lookup /
|
|
12
|
+
// comparison key must be in one canonical form, or the string form and the
|
|
13
|
+
// decoded form of the same identifier silently disagree.
|
|
14
|
+
//
|
|
15
|
+
// Defends the canonicalization-divergence class (CWE-20): a dotted-decimal OID
|
|
16
|
+
// with a leading-zero arc ("2.05.29.15") round-trips to a DIFFERENT OID
|
|
17
|
+
// ("2.5.29.15"), and an arc outside the X.660 bounds can never DER-encode at all.
|
|
18
|
+
// A hand-rolled string check that accepts either lets `build.oid` emit bytes that
|
|
19
|
+
// decode to a different OID than the string names, or lets a non-canonical policy /
|
|
20
|
+
// EKU key compare unequal to the canonical decoder output it is matched against (a
|
|
21
|
+
// silent false-reject). Every string-form identifier check routes through here so
|
|
22
|
+
// the string and DER forms cannot diverge.
|
|
23
|
+
|
|
24
|
+
// assertCanonicalOid(str, E, code, label, boundsCode) -> str | throws
|
|
25
|
+
// A canonical dotted-decimal object identifier string: two or more arcs, each a
|
|
26
|
+
// non-negative decimal integer with no leading zero (the SYNTAX), and -- unless
|
|
27
|
+
// bounds are waived -- the root arc 0..2 with the second arc 0..39 under roots 0
|
|
28
|
+
// and 1 (the X.660 ENCODABILITY bounds: the first two arcs pack into a single
|
|
29
|
+
// sub-identifier as 40*root+second). E is the (code, message) typed-error factory:
|
|
30
|
+
// - a syntax fault always throws E(code, ...).
|
|
31
|
+
// - boundsCode === null waives the arc-bound check -- for a LOOKUP key (oid.name
|
|
32
|
+
// / oid.has), where a well-formed but non-encodable OID is simply not
|
|
33
|
+
// registered (a miss), not an error.
|
|
34
|
+
// - otherwise an out-of-range arc throws E(boundsCode, ...) (boundsCode defaults
|
|
35
|
+
// to code), so a caller distinguishing the two reasons (oid.js: oid/bad-input
|
|
36
|
+
// syntax vs oid/bad-arc bounds) keeps both codes.
|
|
37
|
+
// @enforced-by behavioral -- string-form OID canonicalization has no rename-proof
|
|
38
|
+
// code shape distinct from the arc-based bounds check oid.js legitimately keeps
|
|
39
|
+
// for the arc path; the divergence RED vectors (build.oid / oid.toArcs / requiredEku
|
|
40
|
+
// reject a non-canonical OID) driving the composing consumers are the guard.
|
|
41
|
+
function assertCanonicalOid(str, E, code, label, boundsCode) {
|
|
42
|
+
var who = label || "OID";
|
|
43
|
+
if (typeof str !== "string" || !/^(0|[1-9]\d*)(\.(0|[1-9]\d*))+$/.test(str)) {
|
|
44
|
+
throw E(code, who + " must be a canonical dotted-decimal OID string of two or more arcs with no leading-zero component");
|
|
45
|
+
}
|
|
46
|
+
if (boundsCode === null) return str;
|
|
47
|
+
var bcode = boundsCode === undefined ? code : boundsCode;
|
|
48
|
+
var parts = str.split(".");
|
|
49
|
+
var root = BigInt(parts[0]);
|
|
50
|
+
var second = BigInt(parts[1]);
|
|
51
|
+
if (root > 2n) throw E(bcode, who + " root arc must be 0, 1, or 2 (X.660)");
|
|
52
|
+
if (root < 2n && second > 39n) throw E(bcode, who + " second arc must be 0..39 under roots 0 and 1 (X.660)");
|
|
53
|
+
return str;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
module.exports = { assertCanonicalOid: assertCanonicalOid };
|
|
@@ -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 };
|
package/lib/guard-limits.js
CHANGED
|
@@ -24,15 +24,39 @@
|
|
|
24
24
|
|
|
25
25
|
var constants = require("./constants");
|
|
26
26
|
|
|
27
|
-
// cap(value, key, dflt) -> a validated
|
|
28
|
-
// is undefined. A non-integer / non-finite / negative value
|
|
29
|
-
//
|
|
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
|
-
|
|
35
|
-
|
|
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
|
-
|
|
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 };
|
package/lib/guard-range.js
CHANGED
|
@@ -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):
|
|
41
|
-
//
|
|
42
|
-
//
|
|
43
|
-
//
|
|
44
|
-
//
|
|
45
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
89
|
-
|
|
90
|
-
//
|
|
91
|
-
|
|
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
|
-
//
|
|
233
|
-
//
|
|
234
|
-
|
|
235
|
-
|
|
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
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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) {
|