@blamejs/pki 0.5.4 → 0.5.5
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 +22 -0
- package/MIGRATING.md +31 -0
- package/lib/attrcert-sign.js +11 -7
- package/lib/cmp-session.js +23 -10
- package/lib/cmp-verify.js +12 -4
- package/lib/cms-decrypt.js +71 -30
- package/lib/crl-sign.js +19 -7
- package/lib/est.js +77 -1
- package/lib/guard-all.js +6 -0
- package/lib/guard-encoding.js +35 -6
- package/lib/guard-identifier.js +27 -1
- package/lib/guard-json.js +44 -8
- package/lib/guard-name.js +31 -8
- package/lib/guard-parsed.js +443 -0
- package/lib/hpke.js +36 -3
- package/lib/jose.js +8 -0
- package/lib/lint.js +20 -4
- package/lib/merkle.js +9 -0
- package/lib/ocsp.js +38 -8
- package/lib/path-validate.js +88 -44
- package/lib/pkcs12-build.js +34 -7
- package/lib/pki-build.js +12 -1
- package/lib/schema-crl.js +7 -1
- package/lib/schema-ocsp.js +6 -1
- package/lib/schema-pkcs12.js +7 -2
- package/lib/schema-pkix.js +14 -0
- package/lib/schema-x509.js +14 -1
- package/lib/sign-scheme.js +22 -5
- package/lib/smime.js +47 -0
- package/lib/trust.js +121 -10
- package/lib/validator-cose.js +86 -1
- package/lib/validator-tpm.js +8 -3
- package/lib/webauthn-mds.js +10 -18
- package/lib/webauthn.js +40 -18
- package/lib/x509-sign.js +6 -2
- package/package.json +5 -1
- package/sbom.cdx.json +6 -6
package/lib/guard-encoding.js
CHANGED
|
@@ -25,9 +25,33 @@
|
|
|
25
25
|
// domain/reason; `label` the field phrase; `maxBytes` an optional decoded-size
|
|
26
26
|
// cap (null/undefined = uncapped, for a PEM cert body already bounded upstream).
|
|
27
27
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
28
|
+
// The alphabets are TABLES, walked one character at a time, rather than regular
|
|
29
|
+
// expressions. A guard runs on the most hostile input the toolkit sees, and a
|
|
30
|
+
// pattern engine's cost on a non-matching string is a property of the pattern
|
|
31
|
+
// rather than of the length -- the one thing a bound cannot be placed on from the
|
|
32
|
+
// outside. A table lookup is one array index per character and its cost is the
|
|
33
|
+
// length, which the caller already caps. It is also the more honest statement of
|
|
34
|
+
// the rule: the set of permitted characters IS the rule, written out.
|
|
35
|
+
function _alphabet(chars) {
|
|
36
|
+
var t = new Uint8Array(128);
|
|
37
|
+
for (var i = 0; i < chars.length; i++) t[chars.charCodeAt(i)] = 1;
|
|
38
|
+
return t;
|
|
39
|
+
}
|
|
40
|
+
var UPPER = "ABCDEFGHIJKLMNOPQRSTUVWXYZ", LOWER = "abcdefghijklmnopqrstuvwxyz", DIGITS = "0123456789";
|
|
41
|
+
var B64URL_ALPHABET = _alphabet(UPPER + LOWER + DIGITS + "-_");
|
|
42
|
+
var B64_ALPHABET = _alphabet(UPPER + LOWER + DIGITS + "+/");
|
|
43
|
+
var HEX_ALPHABET = _alphabet(DIGITS + "abcdef" + "ABCDEF");
|
|
44
|
+
|
|
45
|
+
// Every character of `text` is in `table`. A code point outside Latin-1's low half
|
|
46
|
+
// (including every astral half) is outside all three alphabets, so the table bound
|
|
47
|
+
// is the reject rather than an index that reads undefined.
|
|
48
|
+
function _inAlphabet(text, table) {
|
|
49
|
+
for (var i = 0; i < text.length; i++) {
|
|
50
|
+
var c = text.charCodeAt(i);
|
|
51
|
+
if (c > 127 || table[c] !== 1) return false;
|
|
52
|
+
}
|
|
53
|
+
return true;
|
|
54
|
+
}
|
|
31
55
|
|
|
32
56
|
// Reject before Buffer.from allocates: a base64 text of N chars decodes to at
|
|
33
57
|
// most floor(N*3/4) bytes, a hex text to N/2.
|
|
@@ -49,7 +73,7 @@ function _capBefore(nChars, perByteChars, maxBytes, E, code, label) {
|
|
|
49
73
|
// @enforced-by base64-decode-not-via-guard
|
|
50
74
|
function base64url(text, maxBytes, E, code, label) {
|
|
51
75
|
if (typeof text !== "string") throw E(code, label + " must be a string");
|
|
52
|
-
if (!
|
|
76
|
+
if (!_inAlphabet(text, B64URL_ALPHABET)) throw E(code, label + " is not base64url (padding or a non-alphabet character)");
|
|
53
77
|
if (text.length % 4 === 1) throw E(code, label + " has an impossible base64url length");
|
|
54
78
|
_capBefore(text.length * 3, 4, maxBytes, E, code, label);
|
|
55
79
|
var buf = Buffer.from(text, "base64url");
|
|
@@ -62,7 +86,12 @@ function base64url(text, maxBytes, E, code, label) {
|
|
|
62
86
|
// @enforced-by base64-decode-not-via-guard
|
|
63
87
|
function base64(text, maxBytes, E, code, label) {
|
|
64
88
|
if (typeof text !== "string") throw E(code, label + " must be a string");
|
|
65
|
-
|
|
89
|
+
// Padding is positional, not alphabetic: at most two "=" and only at the end, so
|
|
90
|
+
// the body is measured first and the alphabet applies to what remains. An "="
|
|
91
|
+
// anywhere else leaves a non-alphabet character in the body and rejects there.
|
|
92
|
+
var pad = 0;
|
|
93
|
+
while (pad < 2 && text.length > pad && text.charCodeAt(text.length - 1 - pad) === 0x3d) pad++;
|
|
94
|
+
if (!_inAlphabet(text.slice(0, text.length - pad), B64_ALPHABET)) throw E(code, label + " is not base64 (a non-alphabet character)");
|
|
66
95
|
if (text.length % 4 !== 0) throw E(code, label + " must be whole 4-character base64 groups (RFC 4648 sec. 3.5)");
|
|
67
96
|
_capBefore(text.length * 3, 4, maxBytes, E, code, label);
|
|
68
97
|
var buf = Buffer.from(text, "base64");
|
|
@@ -79,7 +108,7 @@ function base64(text, maxBytes, E, code, label) {
|
|
|
79
108
|
// (a non-canonical / odd-length / non-hex #hex attribute value rejects) guard it.
|
|
80
109
|
function hex(text, maxBytes, E, code, label) {
|
|
81
110
|
if (typeof text !== "string") throw E(code, label + " must be a string");
|
|
82
|
-
if (!
|
|
111
|
+
if (!_inAlphabet(text, HEX_ALPHABET)) throw E(code, label + " is not hexadecimal");
|
|
83
112
|
if (text.length % 2 !== 0) throw E(code, label + " must have an even number of hex digits");
|
|
84
113
|
_capBefore(text.length, 2, maxBytes, E, code, label);
|
|
85
114
|
var buf = Buffer.from(text, "hex");
|
package/lib/guard-identifier.js
CHANGED
|
@@ -21,6 +21,32 @@
|
|
|
21
21
|
// silent false-reject). Every string-form identifier check routes through here so
|
|
22
22
|
// the string and DER forms cannot diverge.
|
|
23
23
|
|
|
24
|
+
// The dotted-decimal grammar, walked rather than matched. `(0|[1-9]\d*)(\.(0|[1-9]\d*))+`
|
|
25
|
+
// nests a quantified group inside a quantified group with an alternation in each,
|
|
26
|
+
// which is the shape whose cost on a REJECTING string is a property of the pattern
|
|
27
|
+
// rather than of the length -- and this guard's whole job is to be handed strings
|
|
28
|
+
// that reject. Walking the string is one pass, one comparison per character, and it
|
|
29
|
+
// states the two rules plainly: an arc is one or more digits, and an arc longer than
|
|
30
|
+
// one digit does not start with zero (the leading-zero form round-trips to a
|
|
31
|
+
// DIFFERENT OID, which is the divergence this guard exists to stop).
|
|
32
|
+
function _isDottedDecimal(str) {
|
|
33
|
+
if (str.length === 0) return false;
|
|
34
|
+
var arcs = 0, digits = 0, leadingZero = false;
|
|
35
|
+
for (var i = 0; i < str.length; i++) {
|
|
36
|
+
var c = str.charCodeAt(i);
|
|
37
|
+
if (c === 0x2e) { // "."
|
|
38
|
+
if (digits === 0 || leadingZero) return false; // empty arc, or "01"
|
|
39
|
+
arcs++; digits = 0; leadingZero = false;
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
if (c < 0x30 || c > 0x39) return false; // not a digit
|
|
43
|
+
if (digits === 1 && str.charCodeAt(i - 1) === 0x30) leadingZero = true;
|
|
44
|
+
digits++;
|
|
45
|
+
}
|
|
46
|
+
if (digits === 0 || leadingZero) return false; // trailing "." or a final "01"
|
|
47
|
+
return arcs >= 1; // two or more arcs
|
|
48
|
+
}
|
|
49
|
+
|
|
24
50
|
// assertCanonicalOid(str, E, code, label, boundsCode) -> str | throws
|
|
25
51
|
// A canonical dotted-decimal object identifier string: two or more arcs, each a
|
|
26
52
|
// non-negative decimal integer with no leading zero (the SYNTAX), and -- unless
|
|
@@ -40,7 +66,7 @@
|
|
|
40
66
|
// reject a non-canonical OID) driving the composing consumers are the guard.
|
|
41
67
|
function assertCanonicalOid(str, E, code, label, boundsCode) {
|
|
42
68
|
var who = label || "OID";
|
|
43
|
-
if (typeof str !== "string" ||
|
|
69
|
+
if (typeof str !== "string" || !_isDottedDecimal(str)) {
|
|
44
70
|
throw E(code, who + " must be a canonical dotted-decimal OID string of two or more arcs with no leading-zero component");
|
|
45
71
|
}
|
|
46
72
|
if (boundsCode === null) return str;
|
package/lib/guard-json.js
CHANGED
|
@@ -25,6 +25,14 @@
|
|
|
25
25
|
var text = require("./guard-text");
|
|
26
26
|
var limits = require("./guard-limits");
|
|
27
27
|
|
|
28
|
+
// One hex digit's value, or -1. Written out because the three ranges ARE the rule.
|
|
29
|
+
function _hexVal(c) {
|
|
30
|
+
if (c >= 0x30 && c <= 0x39) return c - 0x30; // 0-9
|
|
31
|
+
if (c >= 0x61 && c <= 0x66) return c - 0x61 + 10; // a-f
|
|
32
|
+
if (c >= 0x41 && c <= 0x46) return c - 0x41 + 10; // A-F
|
|
33
|
+
return -1;
|
|
34
|
+
}
|
|
35
|
+
|
|
28
36
|
// parse(input, ErrorClass, spec) -> value. `input` is a Buffer or a string.
|
|
29
37
|
// spec = { maxBytes, maxDepth, badJson, tooDeep, duplicateMember, tooLarge,
|
|
30
38
|
// badInput, label } -- the caller's caps + frozen domain/reason codes.
|
|
@@ -118,9 +126,17 @@ function parse(input, ErrorClass, spec) {
|
|
|
118
126
|
else if (e === "r") s += "\r";
|
|
119
127
|
else if (e === "t") s += "\t";
|
|
120
128
|
else if (e === "u") {
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
129
|
+
// Four hex digits, read as digits rather than matched as a pattern: the
|
|
130
|
+
// scanner is already walking this string one character at a time, and a
|
|
131
|
+
// parser handed hostile input should not hand any of it to a second engine.
|
|
132
|
+
var cp = 0;
|
|
133
|
+
if (i + 4 > n) fail("bad \\u escape");
|
|
134
|
+
for (var h = 0; h < 4; h++) {
|
|
135
|
+
var d = _hexVal(str.charCodeAt(i + h));
|
|
136
|
+
if (d < 0) fail("bad \\u escape");
|
|
137
|
+
cp = (cp << 4) | d;
|
|
138
|
+
}
|
|
139
|
+
s += String.fromCharCode(cp);
|
|
124
140
|
i += 4;
|
|
125
141
|
} else fail("bad escape");
|
|
126
142
|
} else if (c.charCodeAt(0) < 0x20) {
|
|
@@ -128,15 +144,35 @@ function parse(input, ErrorClass, spec) {
|
|
|
128
144
|
} else s += c;
|
|
129
145
|
}
|
|
130
146
|
}
|
|
147
|
+
// RFC 8259 sec. 6, enforced BY the walk rather than by re-matching the token
|
|
148
|
+
// afterwards. The scan already knows where each part starts and ends, so the
|
|
149
|
+
// grammar's three rules -- an integer part that is "0" or has no leading zero, a
|
|
150
|
+
// fraction with at least one digit, an exponent with at least one digit -- are
|
|
151
|
+
// checked as it goes. Re-matching what the scanner just read meant maintaining
|
|
152
|
+
// the same grammar twice, in two notations, and the pattern was the copy whose
|
|
153
|
+
// cost on a rejecting token could not be bounded from outside.
|
|
131
154
|
function number() {
|
|
132
155
|
var start = i;
|
|
133
156
|
if (str[i] === "-") i++;
|
|
157
|
+
var intStart = i;
|
|
134
158
|
while (i < n && str[i] >= "0" && str[i] <= "9") i++;
|
|
135
|
-
|
|
136
|
-
if (
|
|
137
|
-
|
|
138
|
-
if (
|
|
139
|
-
|
|
159
|
+
var intLen = i - intStart;
|
|
160
|
+
if (intLen === 0) fail("malformed number");
|
|
161
|
+
if (intLen > 1 && str[intStart] === "0") fail("malformed number"); // no leading zero
|
|
162
|
+
if (str[i] === ".") {
|
|
163
|
+
i++;
|
|
164
|
+
var fracStart = i;
|
|
165
|
+
while (i < n && str[i] >= "0" && str[i] <= "9") i++;
|
|
166
|
+
if (i === fracStart) fail("malformed number"); // "1." has no fraction
|
|
167
|
+
}
|
|
168
|
+
if (str[i] === "e" || str[i] === "E") {
|
|
169
|
+
i++;
|
|
170
|
+
if (str[i] === "+" || str[i] === "-") i++;
|
|
171
|
+
var expStart = i;
|
|
172
|
+
while (i < n && str[i] >= "0" && str[i] <= "9") i++;
|
|
173
|
+
if (i === expStart) fail("malformed number"); // "1e" has no exponent
|
|
174
|
+
}
|
|
175
|
+
var v = Number(str.slice(start, i));
|
|
140
176
|
if (!isFinite(v)) fail("bad number");
|
|
141
177
|
return v;
|
|
142
178
|
}
|
package/lib/guard-name.js
CHANGED
|
@@ -65,10 +65,25 @@ function assertPrintableIa5(buf, E, code, label) {
|
|
|
65
65
|
// matches OpenSSL's X509_NAME_cmp, so a chain OpenSSL accepts is not rejected. This
|
|
66
66
|
// canonicalization is the shape the guard-shape-reinlined detector keys on
|
|
67
67
|
// (declared on dnEqual): a boundary hand-rolling it is re-implementing DN identity.
|
|
68
|
+
// The collapse is a single walk rather than a pattern replace. This runs on every
|
|
69
|
+
// attribute value of every name the toolkit compares -- a certificate an attacker
|
|
70
|
+
// supplies included -- and one pass with a running "was the last character a space"
|
|
71
|
+
// flag costs exactly the length. It also spells out which characters count as
|
|
72
|
+
// whitespace: RFC 5280 sec. 7.1 defers to X.520's caseIgnoreMatch, whose SPACE is
|
|
73
|
+
// the ASCII space, and a pattern's \s silently also folds VT, FF, NBSP and every
|
|
74
|
+
// Unicode space separator, which would equate two names X.520 keeps distinct.
|
|
75
|
+
function _isSpace(c) { return c === 0x20 || c === 0x09 || c === 0x0a || c === 0x0d; }
|
|
68
76
|
function _canonAttrValue(v, E, code, label) {
|
|
69
77
|
if (typeof v !== "string") return v;
|
|
70
78
|
assertNoControlBytes(v, E, code, label);
|
|
71
|
-
|
|
79
|
+
var out = "", lastWasSpace = true; // true so leading whitespace is dropped
|
|
80
|
+
for (var i = 0; i < v.length; i++) {
|
|
81
|
+
if (_isSpace(v.charCodeAt(i))) { lastWasSpace = true; continue; }
|
|
82
|
+
if (lastWasSpace && out.length) out += " ";
|
|
83
|
+
lastWasSpace = false;
|
|
84
|
+
out += v.charAt(i);
|
|
85
|
+
}
|
|
86
|
+
return out.toLowerCase();
|
|
72
87
|
}
|
|
73
88
|
// rdnEqual(a, b, E, code, label) -> boolean. Canonical comparison of a single
|
|
74
89
|
// RelativeDistinguishedName (an unordered SET of type/value pairs, compared as a
|
|
@@ -151,17 +166,25 @@ function escapeControlBytes(str) {
|
|
|
151
166
|
// with the hexstring form, so the report misstates a subject/issuer name (CWE-116).
|
|
152
167
|
// The one place a DN attribute value is made display-safe; pki.schema.pkix's DN
|
|
153
168
|
// rendering composes it, and pki.inspect reuses that parser output (name.dn).
|
|
154
|
-
|
|
155
|
-
//
|
|
156
|
-
//
|
|
169
|
+
var DN_SPECIAL = { 0x2c: 1, 0x2b: 1, 0x22: 1, 0x5c: 1, 0x3c: 1, 0x3e: 1, 0x3b: 1 }; // , + " \ < > ;
|
|
170
|
+
// RFC 4514 sec. 2.4's special set is the TABLE above and the walk is a single pass.
|
|
171
|
+
// The separator escape used to be a pattern replace feeding a second loop, which
|
|
172
|
+
// meant two passes over an attacker-supplied value and a character class holding a
|
|
173
|
+
// quote and a backslash inside a regex literal -- the form most easily misread by a
|
|
174
|
+
// human and, as it happened, by the codebase-patterns literal-stripper.
|
|
175
|
+
//
|
|
176
|
+
// @enforced-by behavioral -- the escaping has no rename-proof code shape distinct
|
|
177
|
+
// from ordinary string building; the guard-name RED vectors + the schema-pkix DN
|
|
157
178
|
// round-trip vectors (a comma / plus / leading '#' renders backslash-escaped) are the guard.
|
|
158
179
|
function escapeDnValue(v) {
|
|
159
|
-
var s = String(v)
|
|
160
|
-
//
|
|
161
|
-
//
|
|
180
|
+
var s = String(v), out = "";
|
|
181
|
+
// A NUL / control octet becomes '\' + two hex digits, so an embedded CR / LF / NUL
|
|
182
|
+
// in a decoded DN value can never forge a report line when displayed.
|
|
162
183
|
for (var i = 0; i < s.length; i++) {
|
|
163
184
|
var c = s.charCodeAt(i);
|
|
164
|
-
|
|
185
|
+
if (c < 0x20 || c === 0x7f) out += "\\" + (c < 16 ? "0" : "") + c.toString(16).toUpperCase();
|
|
186
|
+
else if (DN_SPECIAL[c] === 1) out += "\\" + s.charAt(i);
|
|
187
|
+
else out += s.charAt(i);
|
|
165
188
|
}
|
|
166
189
|
if (out.length && out.charAt(out.length - 1) === " ") out = out.slice(0, -1) + "\\ ";
|
|
167
190
|
if (out.charAt(0) === "#" || out.charAt(0) === " ") out = "\\" + out;
|