@blamejs/pki 0.5.31 → 0.5.32
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 +16 -1
- package/README.md +2 -2
- package/lib/acme.js +347 -90
- package/lib/asn1-der.js +93 -33
- package/lib/cmc-build.js +19 -8
- package/lib/cmc-verify.js +4 -4
- package/lib/cmp-build.js +36 -6
- package/lib/cmp-session.js +5 -5
- package/lib/cmp-verify.js +86 -16
- package/lib/cms-compress.js +2 -2
- package/lib/cms-decrypt.js +3 -3
- package/lib/cms-encrypt.js +1 -1
- package/lib/cms-sign.js +7 -7
- package/lib/cms-verify.js +9 -4
- package/lib/crl-sign.js +4 -4
- package/lib/crmf-sign.js +2 -1
- package/lib/csr-sign.js +1 -4
- package/lib/ct.js +8 -4
- package/lib/est.js +234 -34
- package/lib/framework-error.js +20 -6
- package/lib/guard-bytes.js +24 -3
- package/lib/http-digest.js +69 -16
- package/lib/http-retry-after.js +68 -24
- package/lib/http-transport.js +60 -6
- package/lib/inspect.js +13 -2
- package/lib/key.js +2 -2
- package/lib/lint.js +1 -1
- package/lib/mime.js +27 -4
- package/lib/ocsp.js +7 -7
- package/lib/oid.js +16 -0
- package/lib/path-validate.js +76 -18
- package/lib/pbes2.js +2 -2
- package/lib/pkcs12-build.js +2 -2
- package/lib/pki-build.js +6 -6
- package/lib/rfc3339.js +75 -17
- package/lib/schema-c509.js +36 -9
- package/lib/schema-cmc.js +3 -3
- package/lib/schema-pkix.js +174 -25
- package/lib/schema-tsp.js +10 -1
- package/lib/sign-scheme.js +1 -1
- package/lib/sigstore.js +52 -9
- package/lib/smime.js +86 -6
- package/lib/trust.js +114 -14
- package/lib/tsp-sign.js +39 -27
- package/lib/validator-tpm.js +2 -2
- package/lib/webauthn-mds.js +24 -6
- package/lib/webauthn.js +3 -3
- package/lib/webcrypto.js +1 -1
- package/lib/x509-sign.js +1 -1
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
package/lib/framework-error.js
CHANGED
|
@@ -28,9 +28,23 @@
|
|
|
28
28
|
* classes the toolkit throws.
|
|
29
29
|
*/
|
|
30
30
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
var
|
|
31
|
+
var intrinsic = require("./guard-intrinsic");
|
|
32
|
+
var _charCodeAt = intrinsic.uncurry(String.prototype.charCodeAt);
|
|
33
|
+
var _String = intrinsic.String;
|
|
34
|
+
var _defineProperty = intrinsic.defineProperty;
|
|
35
|
+
|
|
36
|
+
// The frozen shape of every error code: a `domain/reason` string -- one or more lowercase alphanumerics or
|
|
37
|
+
// dashes, a single "/", then one or more of the same. Decided by character scan (no regex).
|
|
38
|
+
function _isCodeShape(c) {
|
|
39
|
+
var n = c.length, i, ch, slash = -1;
|
|
40
|
+
if (n < 3) return false; // shortest is "a/b"
|
|
41
|
+
for (i = 0; i < n; i += 1) {
|
|
42
|
+
ch = _charCodeAt(c, i);
|
|
43
|
+
if (ch === 47) { if (slash !== -1) return false; slash = i; } // "/" -- at most one
|
|
44
|
+
else if (!((ch >= 97 && ch <= 122) || (ch >= 48 && ch <= 57) || ch === 45)) return false; // a-z 0-9 "-"
|
|
45
|
+
}
|
|
46
|
+
return slash > 0 && slash < n - 1; // one slash, both sides non-empty
|
|
47
|
+
}
|
|
34
48
|
|
|
35
49
|
/**
|
|
36
50
|
* @primitive pki.errors.PkiError
|
|
@@ -59,9 +73,9 @@ class PkiError extends Error {
|
|
|
59
73
|
super(message);
|
|
60
74
|
this.name = "PkiError";
|
|
61
75
|
var c = code || "pki/invalid";
|
|
62
|
-
if (typeof c !== "string" || !
|
|
76
|
+
if (typeof c !== "string" || !_isCodeShape(c)) {
|
|
63
77
|
throw new TypeError(
|
|
64
|
-
"PkiError code must be a domain/reason string, got " +
|
|
78
|
+
"PkiError code must be a domain/reason string, got " + _String(c) +
|
|
65
79
|
" (PkiError takes (message, code); defineClass subclasses take (code, message))"
|
|
66
80
|
);
|
|
67
81
|
}
|
|
@@ -114,7 +128,7 @@ function defineClass(name, opts) {
|
|
|
114
128
|
}
|
|
115
129
|
}
|
|
116
130
|
};
|
|
117
|
-
|
|
131
|
+
_defineProperty(GeneratedError, "name", { value: name, configurable: true });
|
|
118
132
|
GeneratedError.factory = function (code, message, arg3) {
|
|
119
133
|
return new GeneratedError(code, message, arg3);
|
|
120
134
|
};
|
package/lib/guard-bytes.js
CHANGED
|
@@ -41,7 +41,7 @@ var _stringify = intrinsic.stringify;
|
|
|
41
41
|
var _forEach = intrinsic.forEach;
|
|
42
42
|
var _map = intrinsic.map;
|
|
43
43
|
var _filter = intrinsic.filter;
|
|
44
|
-
var
|
|
44
|
+
var _charCodeAt = intrinsic.uncurry(String.prototype.charCodeAt);
|
|
45
45
|
var _setForEach = intrinsic.uncurry(Set.prototype.forEach);
|
|
46
46
|
var _mapForEach = intrinsic.uncurry(Map.prototype.forEach);
|
|
47
47
|
var _String = intrinsic.String;
|
|
@@ -151,10 +151,26 @@ function _reView(v) {
|
|
|
151
151
|
// The two string operations in this table run AT LOAD, in the same statement that reads
|
|
152
152
|
// `util.types` once and for the same reason. Nothing a caller can replace has run yet, so there is
|
|
153
153
|
// no live read to remove; capturing them would only move the question one line earlier.
|
|
154
|
+
// A util.types predicate that names a concrete typed-array kind: "is" + one-or-more alphanumerics +
|
|
155
|
+
// "Array" (e.g. "isUint8Array"), decided character by character so no regex match protocol -- which
|
|
156
|
+
// re-dispatches through a replaceable `exec` -- stands between this table and the runtime it reads.
|
|
157
|
+
function _isTypedArrayPredName(name) {
|
|
158
|
+
var n = name.length;
|
|
159
|
+
if (n < 8) return false; // "is" + >=1 middle char + "Array"
|
|
160
|
+
if (_charCodeAt(name, 0) !== 105 || _charCodeAt(name, 1) !== 115) return false; // "is"
|
|
161
|
+
var tail = "Array";
|
|
162
|
+
for (var k = 0; k < 5; k++) { if (_charCodeAt(name, n - 5 + k) !== _charCodeAt(tail, k)) return false; }
|
|
163
|
+
for (var i = 2; i < n - 5; i++) {
|
|
164
|
+
var c = _charCodeAt(name, i);
|
|
165
|
+
if (!((c >= 48 && c <= 57) || (c >= 65 && c <= 90) || (c >= 97 && c <= 122))) return false;
|
|
166
|
+
}
|
|
167
|
+
return true;
|
|
168
|
+
}
|
|
169
|
+
|
|
154
170
|
var _CONCRETE_KINDS = _filter(
|
|
155
171
|
_map(
|
|
156
172
|
// allow:guard-reads-runtime-live -- module-init, see above
|
|
157
|
-
_filter(_objectKeys(util.types), function (name) { return
|
|
173
|
+
_filter(_objectKeys(util.types), function (name) { return _isTypedArrayPredName(name); }),
|
|
158
174
|
// allow:guard-reads-runtime-live -- module-init, see above
|
|
159
175
|
function (name) { return [util.types[name], globalThis[name.slice(2)]]; }
|
|
160
176
|
),
|
|
@@ -188,7 +204,12 @@ function _kindName(v) {
|
|
|
188
204
|
return typeof v === "function" ? "function" : "value";
|
|
189
205
|
}
|
|
190
206
|
|
|
191
|
-
function _article(name) {
|
|
207
|
+
function _article(name) {
|
|
208
|
+
var c = name.length > 0 ? _charCodeAt(name, 0) : 0;
|
|
209
|
+
var vowel = c === 65 || c === 69 || c === 73 || c === 79 || c === 85 ||
|
|
210
|
+
c === 97 || c === 101 || c === 105 || c === 111 || c === 117; // A E I O U, either case
|
|
211
|
+
return vowel ? "an" : "a";
|
|
212
|
+
}
|
|
192
213
|
|
|
193
214
|
// outputView(input, ErrorClass, code, label) -> a Buffer aliasing a caller's view, for WRITING.
|
|
194
215
|
//
|
package/lib/http-digest.js
CHANGED
|
@@ -19,6 +19,10 @@ var intrinsic = require("./guard-intrinsic");
|
|
|
19
19
|
// replaceable properties to answer one thing, and here it decides whether an auth-param repeats --
|
|
20
20
|
// answering false lets the second copy of a nonce overwrite the first.
|
|
21
21
|
var _hasOwn = intrinsic.hasOwn;
|
|
22
|
+
var _charCodeAt = intrinsic.uncurry(String.prototype.charCodeAt);
|
|
23
|
+
var _strSlice = intrinsic.uncurry(String.prototype.slice);
|
|
24
|
+
var _fromCharCode = intrinsic.fromCharCode;
|
|
25
|
+
var pkix = require("./schema-pkix"); // the shared JS-\s code-point predicate (pkix.isJsWhitespace)
|
|
22
26
|
|
|
23
27
|
// Algorithm registry, a data row and not a switch: rank orders the RFC 7616 sec. 3.7
|
|
24
28
|
// "most secure the client can use" selection (SHA-512-256 > SHA-256 > MD5); an unknown algorithm ranks 0.
|
|
@@ -41,7 +45,12 @@ var DEFAULT_CODES = {
|
|
|
41
45
|
// is hashed over its exact bytes. KD(secret, data) = H(secret ":" data).
|
|
42
46
|
function H(hash, s) { return crypto.createHash(hash).update(s, "latin1").digest("hex"); }
|
|
43
47
|
function KD(hash, secret, data) { return H(hash, secret + ":" + data); }
|
|
44
|
-
|
|
48
|
+
// Quote a value: wrap in double quotes, backslash-escaping any " or \ (matching /(["\\])/g -> "\\$1").
|
|
49
|
+
function _qstr(v) {
|
|
50
|
+
var s = String(v), out = "";
|
|
51
|
+
for (var i = 0; i < s.length; i++) { var c = _charCodeAt(s, i); if (c === 34 || c === 92) out += "\\"; out += _fromCharCode(c); }
|
|
52
|
+
return "\"" + out + "\"";
|
|
53
|
+
}
|
|
45
54
|
|
|
46
55
|
// Split a header at commas outside a quoted-string (a quoted comma is a literal, not a delimiter).
|
|
47
56
|
function _commaSplitOutsideQuotes(s) {
|
|
@@ -69,7 +78,13 @@ function _firstEqOutsideQuotes(s) {
|
|
|
69
78
|
}
|
|
70
79
|
return -1;
|
|
71
80
|
}
|
|
72
|
-
|
|
81
|
+
// Unquote a well-formed quoted-string (only ever called after _closedQuote): drop the surrounding
|
|
82
|
+
// quotes and each escaping backslash (matching s.slice(1,-1).replace(/\\(.)/g, "$1")).
|
|
83
|
+
function _unq(s) {
|
|
84
|
+
var out = "", last = 1, n = s.length; // skip the opening quote at 0
|
|
85
|
+
for (var i = 1; i < n - 1; i++) { if (_charCodeAt(s, i) === 92) { out += _strSlice(s, last, i); last = i + 1; i += 1; } }
|
|
86
|
+
return out + _strSlice(s, last, n - 1); // trailing content, excluding the closing quote
|
|
87
|
+
}
|
|
73
88
|
// A well-formed quoted-string: opens with ", closes with an unescaped " as the last character (nothing trails
|
|
74
89
|
// it). An unterminated / trailing-garbage quoted-string is malformed (RFC 7230 sec. 3.2.6) and rejected.
|
|
75
90
|
function _closedQuote(s) {
|
|
@@ -86,24 +101,62 @@ function _closedQuote(s) {
|
|
|
86
101
|
// A control octet (RFC 7230 sec. 3.2.6 quoted-string / qdtext forbids CTL except HTAB). Scanned with charCodeAt
|
|
87
102
|
// instead of a control-char regex literal (which eslint no-control-regex correctly refuses).
|
|
88
103
|
function _hasCtl(s) {
|
|
89
|
-
for (var i = 0; i < s.length; i++) { var c = s
|
|
104
|
+
for (var i = 0; i < s.length; i++) { var c = _charCodeAt(s, i); if ((c < 0x20 && c !== 0x09) || c === 0x7f) return true; }
|
|
90
105
|
return false;
|
|
91
106
|
}
|
|
92
107
|
|
|
93
108
|
// Split a WWW-Authenticate header into [{ scheme, paramText }]. A comma-separated segment whose pre-'='
|
|
94
109
|
// text is "token WS token" (a scheme name before the first auth-param key), or a bare token with no '=',
|
|
95
110
|
// STARTS a new challenge; a "key=value" segment is a param continuation of the current challenge.
|
|
111
|
+
// The leading token when `pre` is "token WS more" (/^(\S+)\s+(\S[\s\S]*)$/) -- a scheme name before the
|
|
112
|
+
// first auth-param -- else null. \s is the shared JS-\s predicate (RFC 7230 OWS is a subset).
|
|
113
|
+
function _matchTokenRest(pre) {
|
|
114
|
+
var n = pre.length, p = 0;
|
|
115
|
+
while (p < n && !pkix.isJsWhitespace(_charCodeAt(pre, p))) p += 1; // ^(\S+)
|
|
116
|
+
if (p === 0) return null;
|
|
117
|
+
var tokEnd = p;
|
|
118
|
+
if (p >= n || !pkix.isJsWhitespace(_charCodeAt(pre, p))) return null; // \s+
|
|
119
|
+
while (p < n && pkix.isJsWhitespace(_charCodeAt(pre, p))) p += 1;
|
|
120
|
+
if (p >= n || pkix.isJsWhitespace(_charCodeAt(pre, p))) return null; // (\S[\s\S]*)$ -- at least one non-\s
|
|
121
|
+
return _strSlice(pre, 0, tokEnd);
|
|
122
|
+
}
|
|
123
|
+
// The index past a leading "OWS token OWS" (/^\s*\S+\s+/), or 0 when the string does not begin that way.
|
|
124
|
+
// The caller slices from this index (the slice that replaces the removed .replace).
|
|
125
|
+
function _stripTokenIndex(seg) {
|
|
126
|
+
var n = seg.length, p = 0;
|
|
127
|
+
while (p < n && pkix.isJsWhitespace(_charCodeAt(seg, p))) p += 1; // ^\s*
|
|
128
|
+
var ts = p;
|
|
129
|
+
while (p < n && !pkix.isJsWhitespace(_charCodeAt(seg, p))) p += 1; // \S+
|
|
130
|
+
if (p === ts) return 0;
|
|
131
|
+
var ws = p;
|
|
132
|
+
while (p < n && pkix.isJsWhitespace(_charCodeAt(seg, p))) p += 1; // \s+ (required)
|
|
133
|
+
return p === ws ? 0 : p;
|
|
134
|
+
}
|
|
135
|
+
// String.prototype.split(/\s+/): the pieces between \s runs (a leading/trailing run yields a "" element).
|
|
136
|
+
function _splitWhitespace(s) {
|
|
137
|
+
var out = [], i = 0, n = s.length, ps = 0;
|
|
138
|
+
while (i < n) {
|
|
139
|
+
if (pkix.isJsWhitespace(_charCodeAt(s, i))) {
|
|
140
|
+
out[out.length] = _strSlice(s, ps, i);
|
|
141
|
+
while (i < n && pkix.isJsWhitespace(_charCodeAt(s, i))) i += 1;
|
|
142
|
+
ps = i;
|
|
143
|
+
} else i += 1;
|
|
144
|
+
}
|
|
145
|
+
out[out.length] = _strSlice(s, ps);
|
|
146
|
+
return out;
|
|
147
|
+
}
|
|
148
|
+
|
|
96
149
|
function _splitChallenges(s) {
|
|
97
150
|
var segs = _commaSplitOutsideQuotes(s);
|
|
98
151
|
var out = [], cur = null;
|
|
99
152
|
for (var i = 0; i < segs.length; i++) {
|
|
100
|
-
var seg = segs[i].
|
|
153
|
+
var seg = segs[i].trim();
|
|
101
154
|
if (seg === "") continue;
|
|
102
155
|
var eq = _firstEqOutsideQuotes(seg);
|
|
103
|
-
var pre = (eq < 0 ? seg : seg
|
|
104
|
-
var
|
|
156
|
+
var pre = (eq < 0 ? seg : _strSlice(seg, 0, eq)).trim();
|
|
157
|
+
var scheme = _matchTokenRest(pre);
|
|
105
158
|
if (eq < 0) { cur = { scheme: seg, paramText: "" }; out.push(cur); } // a bare scheme token (e.g. "Basic")
|
|
106
|
-
else if (
|
|
159
|
+
else if (scheme !== null) { cur = { scheme: scheme, paramText: _strSlice(seg, _stripTokenIndex(seg)) }; out.push(cur); } // "scheme firstKey=..."
|
|
107
160
|
else if (cur) { cur.paramText = cur.paramText ? cur.paramText + "," + seg : seg; } // "key=value" continuation
|
|
108
161
|
}
|
|
109
162
|
return out;
|
|
@@ -112,12 +165,12 @@ function _splitChallenges(s) {
|
|
|
112
165
|
function _parseParams(paramText, E, code) {
|
|
113
166
|
var segs = _commaSplitOutsideQuotes(paramText), map = Object.create(null);
|
|
114
167
|
for (var i = 0; i < segs.length; i++) {
|
|
115
|
-
var seg = segs[i].
|
|
168
|
+
var seg = segs[i].trim();
|
|
116
169
|
if (seg === "") continue;
|
|
117
170
|
var eq = _firstEqOutsideQuotes(seg);
|
|
118
171
|
if (eq < 0) throw E(code, "malformed Digest auth-param (no '='): " + JSON.stringify(seg));
|
|
119
|
-
var key = seg
|
|
120
|
-
var rawVal = seg
|
|
172
|
+
var key = _strSlice(seg, 0, eq).trim().toLowerCase();
|
|
173
|
+
var rawVal = _strSlice(seg, eq + 1).trim();
|
|
121
174
|
var quoted = rawVal.charAt(0) === "\"";
|
|
122
175
|
if (quoted && !_closedQuote(rawVal)) throw E(code, "an unterminated or trailing-garbage Digest quoted-string (RFC 7230 sec. 3.2.6)");
|
|
123
176
|
if (_hasOwn(map, key)) throw E(code, "repeated Digest auth-param " + JSON.stringify(key));
|
|
@@ -138,7 +191,7 @@ function _validateDigest(paramText, E, code) {
|
|
|
138
191
|
var qop = [];
|
|
139
192
|
if (p.qop) {
|
|
140
193
|
if (!p.qop.quoted) throw E(code, "the Digest qop must be a quoted list (RFC 7616 sec. 3.3)");
|
|
141
|
-
qop = p.qop.value.split(",").map(function (x) { return x.
|
|
194
|
+
qop = p.qop.value.split(",").map(function (x) { return x.trim().toLowerCase(); }).filter(Boolean);
|
|
142
195
|
// A qop directive that is present but lists no value (qop="" / qop=", ,") is malformed, and is not the
|
|
143
196
|
// absent-qop (RFC 2069) case. Reject it instead of silently collapsing to a no-qop response the server
|
|
144
197
|
// cannot accept (RFC 7616 sec. 3.3). Only an omitted qop directive selects the legacy no-qop path.
|
|
@@ -150,8 +203,8 @@ function _validateDigest(paramText, E, code) {
|
|
|
150
203
|
// server intended. Surfaced as an array of the listed URIs, or null when omitted or quoted-empty, which per
|
|
151
204
|
// the RFC means "all URIs on the responding server" (the answer may be reused for any same-origin URI, sec. 3.5).
|
|
152
205
|
if (p.domain && !p.domain.quoted) throw E(code, "the Digest domain must be a quoted-string (RFC 7616 sec. 3.3)");
|
|
153
|
-
var domain = (p.domain && p.domain.value.
|
|
154
|
-
? p.domain.value.
|
|
206
|
+
var domain = (p.domain && p.domain.value.trim() !== "")
|
|
207
|
+
? _splitWhitespace(p.domain.value.trim()) : null;
|
|
155
208
|
// charset (RFC 7616 sec. 3.3): the one permitted value is the unquoted token "UTF-8". A quoted charset, or any
|
|
156
209
|
// other value, is malformed, since answering it would hash the credentials in the wrong encoding, so it is
|
|
157
210
|
// rejected (and thus skipped during multi-offer selection in favor of a conforming offer).
|
|
@@ -250,7 +303,7 @@ function _octets(s, charset) {
|
|
|
250
303
|
// Does the string carry any non-ASCII code unit (> U+007F)? Such a UTF-8 username is sent via `username*`.
|
|
251
304
|
function _hasNonAscii(s) {
|
|
252
305
|
s = String(s == null ? "" : s);
|
|
253
|
-
for (var i = 0; i < s.length; i++) { if (s
|
|
306
|
+
for (var i = 0; i < s.length; i++) { if (_charCodeAt(s, i) > 0x7F) return true; }
|
|
254
307
|
return false;
|
|
255
308
|
}
|
|
256
309
|
|
|
@@ -264,7 +317,7 @@ function _pctEncodeUtf8(s) {
|
|
|
264
317
|
if ((b >= 0x30 && b <= 0x39) || (b >= 0x41 && b <= 0x5A) || (b >= 0x61 && b <= 0x7A) ||
|
|
265
318
|
b === 0x21 || b === 0x23 || b === 0x24 || b === 0x26 || b === 0x2B || b === 0x2D ||
|
|
266
319
|
b === 0x2E || b === 0x5E || b === 0x5F || b === 0x60 || b === 0x7C || b === 0x7E) {
|
|
267
|
-
out +=
|
|
320
|
+
out += _fromCharCode(b);
|
|
268
321
|
} else {
|
|
269
322
|
out += "%" + (b < 0x10 ? "0" : "") + b.toString(16).toUpperCase();
|
|
270
323
|
}
|
|
@@ -323,7 +376,7 @@ function answer(challenge, params, E) {
|
|
|
323
376
|
// (a same-origin redirect, an auth-int retry) increments nc; a fresh nonce restarts at 1. The caller supplies
|
|
324
377
|
// the count for this nonce (default 1); formatted as 8 lowercase hex.
|
|
325
378
|
var ncNum = (typeof params.nc === "number" && params.nc >= 1) ? Math.floor(params.nc) : 1;
|
|
326
|
-
var nc = ("0000000" + ncNum.toString(16)
|
|
379
|
+
var nc = _strSlice("0000000" + ncNum.toString(16), -8);
|
|
327
380
|
var response = useQop
|
|
328
381
|
? KD(alg.hash, HA1, nonce + ":" + nc + ":" + cnonce + ":" + useQop + ":" + HA2)
|
|
329
382
|
: KD(alg.hash, HA1, nonce + ":" + HA2);
|
package/lib/http-retry-after.js
CHANGED
|
@@ -19,12 +19,32 @@ var MAX_RETRY_AFTER_SECONDS = constants.TIME.days(365) / constants.TIME.seconds(
|
|
|
19
19
|
// rfc850-date / asctime-date a recipient must still accept. Gating Date.parse on this grammar keeps its
|
|
20
20
|
// permissiveness (an ISO string like "2026-07-10") from passing as a valid Retry-After header.
|
|
21
21
|
var MONTHS = "Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec".split(" ");
|
|
22
|
-
var
|
|
23
|
-
var
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
22
|
+
var DOW_SHORT = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"];
|
|
23
|
+
var DOW_LONG = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"];
|
|
24
|
+
|
|
25
|
+
// The grammar is scanned character by character (no regex): each form is a fixed layout of a
|
|
26
|
+
// day-of-week name, delimiters, an all-digit field, a three-letter month name, and an HH:MM:SS time.
|
|
27
|
+
function _isDigit(ch) { return ch >= "0" && ch <= "9"; }
|
|
28
|
+
// The two-digit / four-digit integer at `i`, or -1 when those positions are not all digits.
|
|
29
|
+
function _digits2(s, i) { return _isDigit(s.charAt(i)) && _isDigit(s.charAt(i + 1)) ? (s.charCodeAt(i) - 48) * 10 + (s.charCodeAt(i + 1) - 48) : -1; }
|
|
30
|
+
function _digits4(s, i) { for (var k = 0; k < 4; k++) if (!_isDigit(s.charAt(i + k))) return -1; return parseInt(s.slice(i, i + 4), 10); }
|
|
31
|
+
// The length of a name from `list` matching `s` at `i` (the day-of-week alternation), or -1.
|
|
32
|
+
function _matchName(s, i, list) { for (var j = 0; j < list.length; j++) { if (s.slice(i, i + list[j].length) === list[j]) return list[j].length; } return -1; }
|
|
33
|
+
// The three-letter month at `i` when it is a real month name, else null.
|
|
34
|
+
function _monthAt(s, i) { return _matchName(s, i, MONTHS) === -1 ? null : s.slice(i, i + 3); }
|
|
35
|
+
// An "HH:MM:SS" time at `i` -> { hh, mi, ss, next } (next = i + 8), or null.
|
|
36
|
+
function _timeAt(s, i) {
|
|
37
|
+
var hh = _digits2(s, i); if (hh < 0 || s.charAt(i + 2) !== ":") return null;
|
|
38
|
+
var mi = _digits2(s, i + 3); if (mi < 0 || s.charAt(i + 5) !== ":") return null;
|
|
39
|
+
var ss = _digits2(s, i + 6); if (ss < 0) return null;
|
|
40
|
+
return { hh: hh, mi: mi, ss: ss, next: i + 8 };
|
|
41
|
+
}
|
|
42
|
+
// A non-empty run of ASCII digits (matching /^\d+$/, the delay-seconds form).
|
|
43
|
+
function _isAllDigits(s) {
|
|
44
|
+
if (s.length === 0) return false;
|
|
45
|
+
for (var i = 0; i < s.length; i++) if (!_isDigit(s.charAt(i))) return false;
|
|
46
|
+
return true;
|
|
47
|
+
}
|
|
28
48
|
|
|
29
49
|
// Expand an obsolete RFC 850 two-digit year: HTTP interprets it relative to the receipt year, as the full
|
|
30
50
|
// year with these two digits within [refYear-50, refYear+49] (moved to the past only when it would
|
|
@@ -40,29 +60,53 @@ function _rfc850Year(yy, refMs) {
|
|
|
40
60
|
return full;
|
|
41
61
|
}
|
|
42
62
|
|
|
63
|
+
// IMF-fixdate (the required form): "Sun, 06 Nov 1994 08:49:37 GMT".
|
|
64
|
+
function _scanImf(s) {
|
|
65
|
+
var p = _matchName(s, 0, DOW_SHORT);
|
|
66
|
+
if (p === -1 || s.slice(p, p + 2) !== ", ") return null; p += 2;
|
|
67
|
+
var dd = _digits2(s, p); if (dd < 0 || s.charAt(p + 2) !== " ") return null; p += 3;
|
|
68
|
+
var mo = _monthAt(s, p); if (!mo || s.charAt(p + 3) !== " ") return null; p += 4;
|
|
69
|
+
var yy = _digits4(s, p); if (yy < 0 || s.charAt(p + 4) !== " ") return null; p += 5;
|
|
70
|
+
var t = _timeAt(s, p); if (!t || s.slice(t.next, t.next + 4) !== " GMT" || t.next + 4 !== s.length) return null;
|
|
71
|
+
return { day: dd, monName: mo, year: yy, hh: t.hh, mi: t.mi, ss: t.ss };
|
|
72
|
+
}
|
|
73
|
+
// rfc850-date (obsolete, still accepted): "Sunday, 06-Nov-94 08:49:37 GMT" -- a two-digit year expanded
|
|
74
|
+
// relative to the receipt year.
|
|
75
|
+
function _scanRfc850(s, refMs) {
|
|
76
|
+
var p = _matchName(s, 0, DOW_LONG);
|
|
77
|
+
if (p === -1 || s.slice(p, p + 2) !== ", ") return null; p += 2;
|
|
78
|
+
var dd = _digits2(s, p); if (dd < 0 || s.charAt(p + 2) !== "-") return null; p += 3;
|
|
79
|
+
var mo = _monthAt(s, p); if (!mo || s.charAt(p + 3) !== "-") return null; p += 4;
|
|
80
|
+
var yy = _digits2(s, p); if (yy < 0 || s.charAt(p + 2) !== " ") return null; p += 3;
|
|
81
|
+
var t = _timeAt(s, p); if (!t || s.slice(t.next, t.next + 4) !== " GMT" || t.next + 4 !== s.length) return null;
|
|
82
|
+
return { day: dd, monName: mo, year: _rfc850Year(yy, refMs), hh: t.hh, mi: t.mi, ss: t.ss };
|
|
83
|
+
}
|
|
84
|
+
// asctime-date (obsolete, still accepted): "Sun Nov 6 08:49:37 1994" -- a space-padded one/two-digit day.
|
|
85
|
+
function _scanAsctime(s) {
|
|
86
|
+
var p = _matchName(s, 0, DOW_SHORT);
|
|
87
|
+
if (p === -1 || s.charAt(p) !== " ") return null; p += 1;
|
|
88
|
+
var mo = _monthAt(s, p); if (!mo || s.charAt(p + 3) !== " ") return null; p += 4;
|
|
89
|
+
var c0 = s.charAt(p);
|
|
90
|
+
if (!(c0 === " " || _isDigit(c0)) || !_isDigit(s.charAt(p + 1))) return null;
|
|
91
|
+
var dd = parseInt(s.slice(p, p + 2), 10); if (s.charAt(p + 2) !== " ") return null; p += 3;
|
|
92
|
+
var t = _timeAt(s, p); if (!t || s.charAt(t.next) !== " ") return null;
|
|
93
|
+
var yy = _digits4(s, t.next + 1); if (yy < 0 || t.next + 1 + 4 !== s.length) return null;
|
|
94
|
+
return { day: dd, monName: mo, year: yy, hh: t.hh, mi: t.mi, ss: t.ss };
|
|
95
|
+
}
|
|
96
|
+
|
|
43
97
|
// Parse an HTTP-date to epoch ms, or NaN when its grammar, calendar, or time is invalid. Built from the
|
|
44
98
|
// extracted fields via Date.UTC (never Date.parse): every HTTP-date is UTC, the asctime form carries
|
|
45
99
|
// no GMT token so delegating would parse it in local time, and the round-trip check rejects an
|
|
46
100
|
// impossible date / time (Date.UTC normalizes Feb 31 -> Mar 2 just as Date.parse would).
|
|
47
101
|
function httpDateMs(s, refMs) {
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
var
|
|
51
|
-
if (
|
|
52
|
-
|
|
53
|
-
var dmy = /(\d{2})[ -](\w{3})[ -](\d{2,4})/.exec(s); // IMF / rfc850: day month year
|
|
54
|
-
if (!dmy) return NaN;
|
|
55
|
-
day = parseInt(dmy[1], 10); monName = dmy[2]; year = parseInt(dmy[3], 10);
|
|
56
|
-
if (year < 100) year = _rfc850Year(year, refMs); // rfc850 two-digit year
|
|
57
|
-
}
|
|
58
|
-
var t = /(\d{2}):(\d{2}):(\d{2})/.exec(s);
|
|
59
|
-
var mon = MONTHS.indexOf(monName);
|
|
60
|
-
if (!t || mon < 0) return NaN;
|
|
61
|
-
var hh = parseInt(t[1], 10), mi = parseInt(t[2], 10), ss = parseInt(t[3], 10);
|
|
62
|
-
var when = Date.UTC(year, mon, day, hh, mi, ss);
|
|
102
|
+
var f = _scanImf(s) || _scanRfc850(s, refMs) || _scanAsctime(s);
|
|
103
|
+
if (!f) return NaN;
|
|
104
|
+
var mon = MONTHS.indexOf(f.monName);
|
|
105
|
+
if (mon < 0) return NaN;
|
|
106
|
+
var when = Date.UTC(f.year, mon, f.day, f.hh, f.mi, f.ss);
|
|
63
107
|
var d = new Date(when);
|
|
64
|
-
if (d.getUTCDate() !== day || d.getUTCMonth() !== mon || d.getUTCFullYear() !== year ||
|
|
65
|
-
d.getUTCHours() !== hh || d.getUTCMinutes() !== mi || d.getUTCSeconds() !== ss) return NaN;
|
|
108
|
+
if (d.getUTCDate() !== f.day || d.getUTCMonth() !== mon || d.getUTCFullYear() !== f.year ||
|
|
109
|
+
d.getUTCHours() !== f.hh || d.getUTCMinutes() !== f.mi || d.getUTCSeconds() !== f.ss) return NaN;
|
|
66
110
|
return when;
|
|
67
111
|
}
|
|
68
112
|
|
|
@@ -78,7 +122,7 @@ function parse(value, opts) {
|
|
|
78
122
|
opts = opts || {};
|
|
79
123
|
var raStr = String(value).trim();
|
|
80
124
|
var out = { retryAfterSeconds: null, retryAfterDate: null };
|
|
81
|
-
if (
|
|
125
|
+
if (_isAllDigits(raStr)) {
|
|
82
126
|
var n = parseInt(raStr, 10);
|
|
83
127
|
// opts.cap (seconds): a caller that will clamp anyway (e.g. an ARI poll cadence) wants a delay above its
|
|
84
128
|
// ceiling reduced to the ceiling, not rejected, so a valid-but-huge value never discards the response.
|
package/lib/http-transport.js
CHANGED
|
@@ -69,14 +69,38 @@ function _budget(value, key, dflt, max, E, code) {
|
|
|
69
69
|
// form (e.g. what pki.est.cacerts / parseCertsOnly return), is silently ignored there, so the
|
|
70
70
|
// connection would fail server authentication despite a valid anchor. Wrap a DER anchor as PEM
|
|
71
71
|
// (a Buffer already carrying PEM text, or a PEM string, passes through unchanged).
|
|
72
|
+
// The JS regex \s code points representable in a latin1 string (0..255): TAB LF VT FF CR SP NBSP.
|
|
73
|
+
// Built at load so the source stays pure ASCII. The armor scan runs over a latin1 prefix, where these
|
|
74
|
+
// are the only \s characters that can appear, so this is exactly /^\s*/ for that input.
|
|
75
|
+
var _LATIN1_WS = String.fromCharCode(0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x20, 0xa0);
|
|
76
|
+
|
|
77
|
+
// PEM armor at the START after optional whitespace, decided by character scan (no regex): skip the
|
|
78
|
+
// leading \s run, then require "-----BEGIN ".
|
|
79
|
+
function _looksLikePemArmor(s) {
|
|
80
|
+
var i = 0;
|
|
81
|
+
while (i < s.length && _LATIN1_WS.indexOf(s.charAt(i)) !== -1) i += 1;
|
|
82
|
+
return s.slice(i, i + 11) === "-----BEGIN ";
|
|
83
|
+
}
|
|
84
|
+
// Base64 wrapped to <=64-column lines, each terminated by LF (matching /.{1,64}/g -> "$&\n", which
|
|
85
|
+
// keeps a trailing LF on the final line).
|
|
86
|
+
function _wrap64Lines(s) {
|
|
87
|
+
var out = "";
|
|
88
|
+
for (var i = 0; i < s.length; i += 64) out += s.slice(i, i + 64) + "\n";
|
|
89
|
+
return out;
|
|
90
|
+
}
|
|
91
|
+
// The surrounding "[" "]" of an IPv6 literal removed, else the string unchanged (matching /^\[(.*)\]$/).
|
|
92
|
+
function _stripIpv6Brackets(s) {
|
|
93
|
+
return (s.length >= 2 && s.charAt(0) === "[" && s.charAt(s.length - 1) === "]") ? s.slice(1, s.length - 1) : s;
|
|
94
|
+
}
|
|
95
|
+
|
|
72
96
|
function _pemifyAnchor(a) {
|
|
73
97
|
if (typeof a === "string") return a;
|
|
74
98
|
if (Buffer.isBuffer(a)) {
|
|
75
99
|
// Detect PEM by its armor at the START (after optional whitespace) only: a DER certificate begins
|
|
76
100
|
// with the SEQUENCE tag 0x30 and may itself contain the bytes "-----BEGIN" in a subject / extension
|
|
77
101
|
// value, so an anywhere-search would leave such a DER anchor unwrapped and unusable.
|
|
78
|
-
if (
|
|
79
|
-
var b64 = a.toString("base64")
|
|
102
|
+
if (_looksLikePemArmor(a.toString("latin1", 0, 64))) return a;
|
|
103
|
+
var b64 = _wrap64Lines(a.toString("base64"));
|
|
80
104
|
return "-----BEGIN CERTIFICATE-----\n" + b64 + "-----END CERTIFICATE-----\n";
|
|
81
105
|
}
|
|
82
106
|
return a;
|
|
@@ -179,11 +203,41 @@ var _guardedLookup = _makeGuardedLookup(nodeDns.lookup);
|
|
|
179
203
|
// certificate / identity / handshake failure is a server-authentication failure;
|
|
180
204
|
// anything else is a generic transport error. Every arm threads the raw fault as
|
|
181
205
|
// `.cause`, so the diagnostic survives.
|
|
206
|
+
// SELF.?SIGNED : "self", an optional single non-line-terminator character, then "signed" (in a lowercased string).
|
|
207
|
+
function _hasSelfSigned(l) {
|
|
208
|
+
for (var i = 0; i + 4 <= l.length; i++) {
|
|
209
|
+
if (l.charCodeAt(i) === 0x73 && l.charCodeAt(i + 1) === 0x65 && l.charCodeAt(i + 2) === 0x6c && l.charCodeAt(i + 3) === 0x66) {
|
|
210
|
+
if (l.indexOf("signed", i + 4) === i + 4) return true;
|
|
211
|
+
var mid = l.charCodeAt(i + 4);
|
|
212
|
+
if (i + 4 < l.length && mid !== 0x0a && mid !== 0x0d && mid !== 0x2028 && mid !== 0x2029 && l.indexOf("signed", i + 5) === i + 5) return true;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
return false;
|
|
216
|
+
}
|
|
217
|
+
// \bWORD\b : `word` (lowercase) bounded on both sides by a non-word character ([a-z0-9_]) or a string edge.
|
|
218
|
+
function _hasWordBounded(l, word) {
|
|
219
|
+
var at = l.indexOf(word);
|
|
220
|
+
while (at !== -1) {
|
|
221
|
+
var before = at === 0 || !_isLowerWordChar(l.charCodeAt(at - 1));
|
|
222
|
+
var afterI = at + word.length;
|
|
223
|
+
var after = afterI === l.length || !_isLowerWordChar(l.charCodeAt(afterI));
|
|
224
|
+
if (before && after) return true;
|
|
225
|
+
at = l.indexOf(word, at + 1);
|
|
226
|
+
}
|
|
227
|
+
return false;
|
|
228
|
+
}
|
|
229
|
+
function _isLowerWordChar(c) { return (c >= 97 && c <= 122) || (c >= 48 && c <= 57) || c === 95; }
|
|
182
230
|
function _classifyError(e, C) {
|
|
183
231
|
if (e && e.pkiBlockedAddress) return C("blocked-address");
|
|
184
232
|
var s = String((e && e.code) || "") + " " + String((e && e.message) || "");
|
|
185
|
-
|
|
186
|
-
|
|
233
|
+
var l = s.toLowerCase();
|
|
234
|
+
// /PROTOCOL_VERSION|UNSUPPORTED_PROTOCOL|VERSION_TOO_LOW|WRONG_VERSION|NO_PROTOCOLS_AVAILABLE|INAPPROPRIATE_FALLBACK/i
|
|
235
|
+
if (l.indexOf("protocol_version") !== -1 || l.indexOf("unsupported_protocol") !== -1 || l.indexOf("version_too_low") !== -1 ||
|
|
236
|
+
l.indexOf("wrong_version") !== -1 || l.indexOf("no_protocols_available") !== -1 || l.indexOf("inappropriate_fallback") !== -1) return C("tls-floor");
|
|
237
|
+
// /CERT|SELF.?SIGNED|VERIFY|ALTNAME|HOSTNAME|DEPTH_ZERO|LOCAL_ISSUER|HANDSHAKE|\bSSL\b|\bTLS\b/i
|
|
238
|
+
if (l.indexOf("cert") !== -1 || l.indexOf("verify") !== -1 || l.indexOf("altname") !== -1 || l.indexOf("hostname") !== -1 ||
|
|
239
|
+
l.indexOf("depth_zero") !== -1 || l.indexOf("local_issuer") !== -1 || l.indexOf("handshake") !== -1 ||
|
|
240
|
+
_hasSelfSigned(l) || _hasWordBounded(l, "ssl") || _hasWordBounded(l, "tls")) return C("server-auth-failed");
|
|
187
241
|
return C("transport-error");
|
|
188
242
|
}
|
|
189
243
|
|
|
@@ -256,7 +310,7 @@ function httpsTransport(defaults) {
|
|
|
256
310
|
if (minVersion !== "TLSv1.2" && minVersion !== "TLSv1.3") throw E(C("bad-input"), "tls.minVersion must be 'TLSv1.2' or 'TLSv1.3' (never below the floor), got " + minVersion);
|
|
257
311
|
|
|
258
312
|
var body = request.body;
|
|
259
|
-
if (
|
|
313
|
+
if (guard.bytes.isByteSource(body)) body = guard.bytes.source(body, E, C("bad-input"), "the request body");
|
|
260
314
|
|
|
261
315
|
// Frame a body-bearing request with a fixed Content-Length computed from the EXACT body, so it is sent
|
|
262
316
|
// length-delimited rather than Transfer-Encoding: chunked (node's default when a body is written with no
|
|
@@ -273,7 +327,7 @@ function httpsTransport(defaults) {
|
|
|
273
327
|
|
|
274
328
|
// URL.hostname keeps the square brackets on an IPv6 literal ([2001:db8::1]); node's `hostname`
|
|
275
329
|
// option and its IP-detection expect the bare address, so strip the brackets once here.
|
|
276
|
-
var host = url.hostname
|
|
330
|
+
var host = _stripIpv6Brackets(url.hostname);
|
|
277
331
|
var options = {
|
|
278
332
|
method: request.method || "GET",
|
|
279
333
|
hostname: host,
|
package/lib/inspect.js
CHANGED
|
@@ -121,7 +121,7 @@ function _serial(cert, indent) {
|
|
|
121
121
|
// anything larger renders as a colon-hex block.
|
|
122
122
|
if (buf.length <= 6) {
|
|
123
123
|
var n = parseInt(buf.toString("hex") || "0", 16);
|
|
124
|
-
return "Serial Number: " + n + " (0x" + (buf.toString("hex")
|
|
124
|
+
return "Serial Number: " + n + " (0x" + (_stripLeadingZeros(buf.toString("hex")) || "0") + ")";
|
|
125
125
|
}
|
|
126
126
|
return "Serial Number:\n" + " ".repeat(indent) + _hexColon(buf, {});
|
|
127
127
|
}
|
|
@@ -905,11 +905,22 @@ _CMS_SHAPE[oid.byName("compressedData")] = function (o) { return _isObj(o.compre
|
|
|
905
905
|
// SEQUENCE is a real Certificate/CertificateList -> delegate to the full sub-report
|
|
906
906
|
// (guarded; a one-line summary on any failure); a CONTEXT-tagged CHOICE alternative
|
|
907
907
|
// (attribute certificate / other) renders a one-line tag+size summary, never parsed.
|
|
908
|
+
// Leading "0" characters removed, and a single trailing "\n" removed -- both by character scan
|
|
909
|
+
// (no regex): display helpers, matching the /^0+/ and /\n$/ replacements they succeed.
|
|
910
|
+
function _stripLeadingZeros(s) {
|
|
911
|
+
var i = 0;
|
|
912
|
+
while (i < s.length && s.charAt(i) === "0") i += 1;
|
|
913
|
+
return s.slice(i);
|
|
914
|
+
}
|
|
915
|
+
function _stripTrailingNewline(s) {
|
|
916
|
+
return (s.length > 0 && s.charAt(s.length - 1) === "\n") ? s.slice(0, s.length - 1) : s;
|
|
917
|
+
}
|
|
918
|
+
|
|
908
919
|
function _cmsEmbedded(kind, el, pad) {
|
|
909
920
|
if (el.tagClass === "universal") {
|
|
910
921
|
try {
|
|
911
922
|
var sub = (kind === "CRL") ? crlReport(el.bytes) : certificate(el.bytes);
|
|
912
|
-
return pad + kind + ":\n" + sub
|
|
923
|
+
return pad + kind + ":\n" + _stripTrailingNewline(sub).split("\n").map(function (l) { return pad + " " + l; }).join("\n");
|
|
913
924
|
} catch (_e) { /* fall through to the summary */ }
|
|
914
925
|
}
|
|
915
926
|
return pad + kind + " [" + el.tagClass + " " + el.tagNumber + "] (" + el.bytes.length + " bytes)";
|
package/lib/key.js
CHANGED
|
@@ -441,7 +441,7 @@ function _inferAlgorithm(detected) {
|
|
|
441
441
|
function _importUsages(name, isPublic) {
|
|
442
442
|
var n = String(name == null ? "" : name).toUpperCase();
|
|
443
443
|
if (n === "X25519" || n === "X448" || n === "ECDH") return isPublic ? [] : ["deriveBits", "deriveKey"];
|
|
444
|
-
if (
|
|
444
|
+
if (n.indexOf("ML-KEM") === 0) return isPublic ? ["encapsulateBits"] : ["decapsulateBits"];
|
|
445
445
|
if (n === "RSA-OAEP") return isPublic ? ["encrypt"] : ["decrypt"];
|
|
446
446
|
return isPublic ? ["verify"] : ["sign"]; // signing default (Ed / ECDSA / RSA-* sign / ML-DSA / SLH-DSA)
|
|
447
447
|
}
|
|
@@ -449,7 +449,7 @@ function _importUsages(name, isPublic) {
|
|
|
449
449
|
function _generateUsages(name) {
|
|
450
450
|
var n = String(name == null ? "" : name).toUpperCase();
|
|
451
451
|
if (n === "X25519" || n === "X448" || n === "ECDH") return ["deriveBits", "deriveKey"];
|
|
452
|
-
if (
|
|
452
|
+
if (n.indexOf("ML-KEM") === 0) return ["encapsulateBits", "decapsulateBits"];
|
|
453
453
|
if (n === "RSA-OAEP") return ["encrypt", "decrypt"];
|
|
454
454
|
return ["sign", "verify"];
|
|
455
455
|
}
|
package/lib/lint.js
CHANGED
|
@@ -97,7 +97,7 @@ function _finding(rule, detail) {
|
|
|
97
97
|
// would be the check computed and thrown away. Never throws: _ingest's contract is to return a
|
|
98
98
|
// finding for bad DATA and reserve throws for a wrong-TYPE argument.
|
|
99
99
|
function _derivedCert(o) {
|
|
100
|
-
if (!o || typeof o !== "object" ||
|
|
100
|
+
if (!o || typeof o !== "object" || guard.bytes.isByteSource(o)) return null;
|
|
101
101
|
try {
|
|
102
102
|
var p = guard.parsed.acceptDerived(o, "certificate", x509.parse, _cfg, "lint/bad-input", "the certificate");
|
|
103
103
|
return guard.parsed.isCert(p) ? p : null;
|
package/lib/mime.js
CHANGED
|
@@ -36,15 +36,38 @@ function _splitPoint(bytes) {
|
|
|
36
36
|
return null;
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
+
// Line-ending + whitespace helpers, decided character by character (no regex): _toLf collapses any
|
|
40
|
+
// CRLF / bare CR / bare LF to a single LF; _toCrlf then expands every LF to CRLF; _stripLeadingHtab
|
|
41
|
+
// drops a leading run of SP/HTAB. They match the /\r\n/g, /\r/g, /\n/g and /^[ \t]+/ replacements.
|
|
42
|
+
function _toLf(s) {
|
|
43
|
+
var out = "";
|
|
44
|
+
for (var i = 0; i < s.length; i++) {
|
|
45
|
+
var c = s.charAt(i);
|
|
46
|
+
if (c === "\r") { if (s.charAt(i + 1) === "\n") continue; out += "\n"; } // CRLF -> its LF next; bare CR -> LF
|
|
47
|
+
else out += c;
|
|
48
|
+
}
|
|
49
|
+
return out;
|
|
50
|
+
}
|
|
51
|
+
function _toCrlf(s) {
|
|
52
|
+
var lf = _toLf(s), out = "";
|
|
53
|
+
for (var i = 0; i < lf.length; i++) { var c = lf.charAt(i); out += (c === "\n") ? "\r\n" : c; }
|
|
54
|
+
return out;
|
|
55
|
+
}
|
|
56
|
+
function _stripLeadingHtab(s) {
|
|
57
|
+
var i = 0;
|
|
58
|
+
while (i < s.length && (s.charAt(i) === " " || s.charAt(i) === "\t")) i += 1;
|
|
59
|
+
return s.slice(i);
|
|
60
|
+
}
|
|
61
|
+
|
|
39
62
|
// Split a header block into unfolded logical lines (RFC 5322 folding: a continuation line begins with
|
|
40
63
|
// SP/HTAB and joins the previous). Returns the raw header strings (each "Name: value").
|
|
41
64
|
function _unfoldHeaders(headerText) {
|
|
42
|
-
var lines = headerText
|
|
65
|
+
var lines = _toLf(headerText).split("\n");
|
|
43
66
|
var out = [];
|
|
44
67
|
for (var i = 0; i < lines.length; i++) {
|
|
45
68
|
var ln = lines[i];
|
|
46
69
|
if (ln === "") continue;
|
|
47
|
-
if ((ln[0] === " " || ln[0] === "\t") && out.length) out[out.length - 1] += " " + ln
|
|
70
|
+
if ((ln[0] === " " || ln[0] === "\t") && out.length) out[out.length - 1] += " " + _stripLeadingHtab(ln);
|
|
48
71
|
else out.push(ln);
|
|
49
72
|
}
|
|
50
73
|
return out;
|
|
@@ -130,7 +153,7 @@ function parse(input, E, code) {
|
|
|
130
153
|
// with only the single conventional ": " delimiter space removed. Leading/trailing whitespace inside
|
|
131
154
|
// the body is preserved, so a consumer that must reproduce the exact authenticated field body (S/MIME
|
|
132
155
|
// header protection) is not handed a lossily-trimmed value.
|
|
133
|
-
headers.push({ name: nm, lname: nm.toLowerCase(), value: body.trim(), rawValue: body.
|
|
156
|
+
headers.push({ name: nm, lname: nm.toLowerCase(), value: body.trim(), rawValue: body.charAt(0) === " " ? body.slice(1) : body });
|
|
134
157
|
}
|
|
135
158
|
function header(name) { var l = name.toLowerCase(); for (var j = 0; j < headers.length; j++) if (headers[j].lname === l) return headers[j].value; return null; }
|
|
136
159
|
var ctv = header("content-type");
|
|
@@ -146,7 +169,7 @@ function parse(input, E, code) {
|
|
|
146
169
|
// Normalize a text body's line endings to the RFC 8551 sec. 3.1.1 canonical CRLF pair (any bare CR or
|
|
147
170
|
// LF becomes CRLF; an existing CRLF is preserved).
|
|
148
171
|
function canonicalizeText(bodyBytes) {
|
|
149
|
-
var s = bodyBytes.toString("latin1")
|
|
172
|
+
var s = _toCrlf(bodyBytes.toString("latin1"));
|
|
150
173
|
return Buffer.from(s, "latin1");
|
|
151
174
|
}
|
|
152
175
|
|