@blamejs/pki 0.1.31 → 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.
- package/CHANGELOG.md +33 -0
- package/README.md +2 -1
- package/index.js +7 -0
- package/lib/asn1-der.js +31 -22
- package/lib/constants.js +29 -0
- package/lib/ct.js +13 -12
- package/lib/est.js +10 -10
- package/lib/framework-error.js +11 -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-name.js +61 -3
- 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 +22 -9
- package/lib/path-validate.js +461 -56
- 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/trust.js +707 -0
- package/lib/webcrypto.js +18 -7
- package/package.json +2 -2
- package/sbom.cdx.json +6 -6
|
@@ -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-name.js
CHANGED
|
@@ -6,8 +6,9 @@
|
|
|
6
6
|
// consumers whose name-string integrity composes this guard (pki.path.validate
|
|
7
7
|
// DN chaining, pki.schema.x509 GeneralName / SAN decode).
|
|
8
8
|
//
|
|
9
|
-
// guard-name -- fail-closed
|
|
10
|
-
// distinguished-name / SAN string
|
|
9
|
+
// guard-name -- fail-closed name integrity: reject an embedded control byte in a
|
|
10
|
+
// distinguished-name / SAN string, and compare two distinguished names by their
|
|
11
|
+
// RFC 5280 sec. 7.1 canonical identity rather than their raw bytes.
|
|
11
12
|
//
|
|
12
13
|
// Defends the name-truncation / display-confusion class (CVE-2009-2408): a NUL
|
|
13
14
|
// or control byte embedded in a decoded name lets an attacker make two different
|
|
@@ -15,6 +16,14 @@
|
|
|
15
16
|
// "good.example.com\0.evil.com" is treated as "good.example.com". CWE-158
|
|
16
17
|
// (improper neutralization of null byte) / CWE-20. The reject is at decode, so a
|
|
17
18
|
// truncation name never reaches a comparison or a display.
|
|
19
|
+
//
|
|
20
|
+
// Defends the DN identity-vs-bytes class (CWE-706): a distinguished name has many
|
|
21
|
+
// RFC 5280 sec. 7.1-equal DER encodings (case, whitespace, PrintableString vs
|
|
22
|
+
// UTF8String). Binding identity to raw bytes -- a byte compare, or hashing
|
|
23
|
+
// name.bytes as a lookup key -- silently treats two equal names as different, so
|
|
24
|
+
// certificate chaining breaks, a revocation issuer / OCSP responder fails to
|
|
25
|
+
// match, or (the mirror risk) a name constraint is escaped. Every DN identity
|
|
26
|
+
// decision routes through the one canonical comparison here.
|
|
18
27
|
|
|
19
28
|
// assertNoControlBytes(str, E, code, label) -> str | throws E(code, ...)
|
|
20
29
|
// DirectoryString policy (a DN attribute value): reject NUL and C0 control bytes
|
|
@@ -50,4 +59,53 @@ function assertPrintableIa5(buf, E, code, label) {
|
|
|
50
59
|
return buf;
|
|
51
60
|
}
|
|
52
61
|
|
|
53
|
-
|
|
62
|
+
// Canonical form of a single DN attribute value (RFC 5280 sec. 7.1): reject an
|
|
63
|
+
// embedded control byte (CVE-2009-2408) then case-fold and collapse internal
|
|
64
|
+
// whitespace. Every standard X.520 attribute uses caseIgnoreMatch, and this form
|
|
65
|
+
// matches OpenSSL's X509_NAME_cmp, so a chain OpenSSL accepts is not rejected. This
|
|
66
|
+
// canonicalization is the shape the guard-shape-reinlined detector keys on
|
|
67
|
+
// (declared on dnEqual): a boundary hand-rolling it is re-implementing DN identity.
|
|
68
|
+
function _canonAttrValue(v, E, code, label) {
|
|
69
|
+
if (typeof v !== "string") return v;
|
|
70
|
+
assertNoControlBytes(v, E, code, label);
|
|
71
|
+
return v.trim().replace(/\s+/g, " ").toLowerCase();
|
|
72
|
+
}
|
|
73
|
+
// rdnEqual(a, b, E, code, label) -> boolean. Canonical comparison of a single
|
|
74
|
+
// RelativeDistinguishedName (an unordered SET of type/value pairs, compared as a
|
|
75
|
+
// multiset). The RDN-level primitive a name-constraint directoryName prefix match
|
|
76
|
+
// composes; dnEqual composes it over the RDN sequence. Comparing an RDN by raw DER
|
|
77
|
+
// would treat two RFC 5280-equal names as different (or let a truncation name
|
|
78
|
+
// compare equal).
|
|
79
|
+
// @enforced-by guard-shape-reinlined (shares the canonicalization shape declared on dnEqual)
|
|
80
|
+
function rdnEqual(a, b, E, code, label) {
|
|
81
|
+
if (a.length !== b.length) return false;
|
|
82
|
+
var used = [];
|
|
83
|
+
for (var i = 0; i < a.length; i++) {
|
|
84
|
+
var found = false;
|
|
85
|
+
for (var j = 0; j < b.length; j++) {
|
|
86
|
+
if (used[j]) continue;
|
|
87
|
+
if (a[i].type === b[j].type && _canonAttrValue(a[i].value, E, code, label) === _canonAttrValue(b[j].value, E, code, label)) {
|
|
88
|
+
used[j] = true; found = true; break;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
if (!found) return false;
|
|
92
|
+
}
|
|
93
|
+
return true;
|
|
94
|
+
}
|
|
95
|
+
// dnEqual(rdnsA, rdnsB, E, code, label) -> boolean. RFC 5280 sec. 7.1 canonical
|
|
96
|
+
// distinguished-name comparison over the RDN sequence -- the ONE place a DN identity
|
|
97
|
+
// is decided, so no caller binds identity to raw DER (a byte compare, or hashing
|
|
98
|
+
// name.bytes, would treat two RFC 5280-equal names as different, breaking a chain /
|
|
99
|
+
// a revocation match, or -- the mirror risk -- escaping a name constraint). The
|
|
100
|
+
// per-RDN canonicalization + control-byte reject is in rdnEqual / _canonAttrValue.
|
|
101
|
+
// @enforced-by guard-shape-reinlined
|
|
102
|
+
// @guard-shape replace\(/\\s\+/g,
|
|
103
|
+
function dnEqual(rdnsA, rdnsB, E, code, label) {
|
|
104
|
+
if (rdnsA.length !== rdnsB.length) return false;
|
|
105
|
+
for (var i = 0; i < rdnsA.length; i++) {
|
|
106
|
+
if (!rdnEqual(rdnsA[i], rdnsB[i], E, code, label)) return false;
|
|
107
|
+
}
|
|
108
|
+
return true;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
module.exports = { assertNoControlBytes: assertNoControlBytes, assertPrintableIa5: assertPrintableIa5, dnEqual: dnEqual, rdnEqual: rdnEqual };
|
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) {
|