@blamejs/pki 0.5.30 → 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 +28 -0
- package/README.md +2 -2
- package/lib/acme.js +347 -90
- package/lib/asn1-der.js +97 -33
- package/lib/attrcert-sign.js +1 -1
- 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 +5 -5
- 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/ip-utils.js +102 -25
- 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 +320 -21
- 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 +27 -7
- 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 +9 -5
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
package/lib/est.js
CHANGED
|
@@ -73,6 +73,10 @@ var crmfFmt = require("./schema-crmf");
|
|
|
73
73
|
var frameworkError = require("./framework-error");
|
|
74
74
|
var guard = require("./guard-all");
|
|
75
75
|
var intrinsic = require("./guard-intrinsic");
|
|
76
|
+
var _charCodeAt = intrinsic.uncurry(String.prototype.charCodeAt);
|
|
77
|
+
var _strSlice = intrinsic.uncurry(String.prototype.slice);
|
|
78
|
+
var _toLowerCase = intrinsic.uncurry(String.prototype.toLowerCase);
|
|
79
|
+
var pkix = require("./schema-pkix"); // shared base64-whitespace strip + JS-\s predicate
|
|
76
80
|
// Own-key membership through an operation taken at load: written out it reads two replaceable
|
|
77
81
|
// properties to answer one question, and here it decides which fields of a response are copied.
|
|
78
82
|
var _hasOwn = intrinsic.hasOwn;
|
|
@@ -85,6 +89,14 @@ function E(code, message, cause) { return new EstError(code, message, cause); }
|
|
|
85
89
|
var ID_SIGNED_DATA = oid.byName("signedData");
|
|
86
90
|
var OID_CHALLENGE_PASSWORD = oid.byName("challengePassword");
|
|
87
91
|
var OID_DECRYPT_KEY_ID = oid.byName("decryptKeyID");
|
|
92
|
+
|
|
93
|
+
// Charset tables built once from the shared pkix.charTable / pkix.allCharsIn char-scan home.
|
|
94
|
+
var _DIGITS_TABLE = pkix.charTable("0123456789"); // /^[0-9]+$/
|
|
95
|
+
var _LABEL_TABLE = pkix.charTable("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789._~-"); // /^[A-Za-z0-9._~-]+$/
|
|
96
|
+
|
|
97
|
+
// Trailing-slash strip (/\/+$/ -> ""): the length after dropping a run of trailing "/" (0x2f); the
|
|
98
|
+
// caller slices to it, so the removed regex read is traded for the slice that replaces it.
|
|
99
|
+
function _stripTrailingSlashEnd(s) { var end = s.length; while (end > 0 && _charCodeAt(s, end - 1) === 0x2f) end -= 1; return end; }
|
|
88
100
|
var OID_ASYMM_DECRYPT_KEY_ID = oid.byName("asymmDecryptKeyID");
|
|
89
101
|
var OID_SMIME_CAPABILITIES = oid.byName("smimeCapabilities");
|
|
90
102
|
var OID_TEMPLATE = oid.byName("certificationRequestInfoTemplate");
|
|
@@ -175,7 +187,7 @@ function transferDecode(body) {
|
|
|
175
187
|
var s = guard.text.decode(body, cap, EstError, {
|
|
176
188
|
charset: "latin1", tooLarge: "est/too-large", badInput: "est/bad-input", label: "the EST payload",
|
|
177
189
|
});
|
|
178
|
-
var stripped =
|
|
190
|
+
var stripped = pkix.stripBase64Whitespace(s);
|
|
179
191
|
// Strict canonical RFC 4648 base64 (RFC 8951 sec. 3.1) via the shared encoding
|
|
180
192
|
// guard: the alphabet + whole-4-group + canonical round-trip fail a malformed
|
|
181
193
|
// body closed instead of silently truncating to a shorter, different DER.
|
|
@@ -208,11 +220,31 @@ function transferEncode(der) {
|
|
|
208
220
|
|
|
209
221
|
// ---- the multipart/mixed splitter (/serverkeygen) -----------------------
|
|
210
222
|
|
|
223
|
+
// Case-insensitive ASCII prefix test: does `s` begin with the uppercase literal `litUpper`?
|
|
224
|
+
function _ciStartsWith(s, litUpper) {
|
|
225
|
+
if (s.length < litUpper.length) return false;
|
|
226
|
+
for (var k = 0; k < litUpper.length; k++) {
|
|
227
|
+
var c = _charCodeAt(s, k);
|
|
228
|
+
if (c >= 97 && c <= 122) c -= 32; // fold lowercase -> uppercase
|
|
229
|
+
if (c !== _charCodeAt(litUpper, k)) return false;
|
|
230
|
+
}
|
|
231
|
+
return true;
|
|
232
|
+
}
|
|
233
|
+
// /^multipart\/mixed\s*(;|$)/i by character scan: the media type "multipart/mixed" (case-insensitive),
|
|
234
|
+
// then JS \s* whitespace, then a `;` parameter separator or end of the header value.
|
|
235
|
+
var _MULTIPART_MIXED = "MULTIPART/MIXED";
|
|
236
|
+
function _isMultipartMixedPrefix(ct) {
|
|
237
|
+
if (!_ciStartsWith(ct, _MULTIPART_MIXED)) return false;
|
|
238
|
+
var p = _MULTIPART_MIXED.length;
|
|
239
|
+
while (p < ct.length && pkix.isJsWhitespace(_charCodeAt(ct, p))) p++;
|
|
240
|
+
return p === ct.length || _charCodeAt(ct, p) === 0x3b;
|
|
241
|
+
}
|
|
242
|
+
|
|
211
243
|
// Extract the boundary from a `multipart/mixed; boundary=...` content-type,
|
|
212
244
|
// tolerating whitespace before the semicolon (erratum 5779 REJECTED the ban).
|
|
213
245
|
function _multipartBoundary(contentType) {
|
|
214
246
|
var ct = String(contentType || "");
|
|
215
|
-
if (
|
|
247
|
+
if (!_isMultipartMixedPrefix(ct)) return null;
|
|
216
248
|
// Two boundaries name two different splits of the same body; there is no reading
|
|
217
249
|
// of that header, so it is refused rather than resolved by taking the first.
|
|
218
250
|
var bp = _ctParam(ct, "boundary");
|
|
@@ -223,6 +255,94 @@ function _multipartBoundary(contentType) {
|
|
|
223
255
|
return bp.value;
|
|
224
256
|
}
|
|
225
257
|
|
|
258
|
+
// A boundary delimiter at a candidate line start `ls`: `--boundary`, an optional `--` close marker, optional
|
|
259
|
+
// SP/HTAB transport-padding, then a CRLF/LF or end-of-text. Returns { end, close } (end = the index past the
|
|
260
|
+
// trailing line terminator) or null. `(--)?` is taken greedily without backtracking loss: a `-` can be neither
|
|
261
|
+
// padding nor a terminator, so if the close marker is present it is the only possible parse.
|
|
262
|
+
function _matchDelimAt(text, ls, boundary) {
|
|
263
|
+
var n = text.length, q = ls, blen = boundary.length, k;
|
|
264
|
+
if (q + 2 > n || _charCodeAt(text, q) !== 0x2d || _charCodeAt(text, q + 1) !== 0x2d) return null;
|
|
265
|
+
q += 2;
|
|
266
|
+
if (q + blen > n) return null;
|
|
267
|
+
for (k = 0; k < blen; k++) { if (_charCodeAt(text, q + k) !== _charCodeAt(boundary, k)) return null; }
|
|
268
|
+
q += blen;
|
|
269
|
+
var close = false;
|
|
270
|
+
if (q + 2 <= n && _charCodeAt(text, q) === 0x2d && _charCodeAt(text, q + 1) === 0x2d) { close = true; q += 2; }
|
|
271
|
+
while (q < n && (_charCodeAt(text, q) === 0x20 || _charCodeAt(text, q) === 0x09)) q += 1;
|
|
272
|
+
if (q === n) return { end: q, close: close };
|
|
273
|
+
if (_charCodeAt(text, q) === 0x0a) return { end: q + 1, close: close };
|
|
274
|
+
if (_charCodeAt(text, q) === 0x0d && q + 1 < n && _charCodeAt(text, q + 1) === 0x0a) return { end: q + 2, close: close };
|
|
275
|
+
return null;
|
|
276
|
+
}
|
|
277
|
+
// Every delimiter in `text`, replacing the global /(?:^|\r?\n)--boundary(--)?[ \t]*(?:\r?\n|$)/g scan: a
|
|
278
|
+
// delimiter begins the text or follows a CRLF/LF (which the match CONSUMES), so `at` is the index of that
|
|
279
|
+
// leading terminator (0 at the start) and `bodyStart` is the index past the trailing one. The leftmost match
|
|
280
|
+
// at or after the previous `bodyStart` is taken, so two adjacent delimiters share one terminator and only the
|
|
281
|
+
// first is found -- the same non-overlapping behavior the regex has.
|
|
282
|
+
function _findMultipartMarks(text, boundary) {
|
|
283
|
+
var marks = [], n = text.length, searchStart = 0, p;
|
|
284
|
+
while (searchStart <= n) {
|
|
285
|
+
var found = null, foundAt = -1;
|
|
286
|
+
for (p = searchStart; p <= n; p++) {
|
|
287
|
+
if (p === 0) { var r0 = _matchDelimAt(text, 0, boundary); if (r0) { found = r0; foundAt = 0; break; } }
|
|
288
|
+
var lsN = -1;
|
|
289
|
+
if (_charCodeAt(text, p) === 0x0a) lsN = p + 1;
|
|
290
|
+
else if (_charCodeAt(text, p) === 0x0d && p + 1 < n && _charCodeAt(text, p + 1) === 0x0a) lsN = p + 2;
|
|
291
|
+
if (lsN !== -1) { var r1 = _matchDelimAt(text, lsN, boundary); if (r1) { found = r1; foundAt = p; break; } }
|
|
292
|
+
}
|
|
293
|
+
if (!found) break;
|
|
294
|
+
marks[marks.length] = { at: foundAt, bodyStart: found.end, close: found.close };
|
|
295
|
+
searchStart = found.end === foundAt ? found.end + 1 : found.end;
|
|
296
|
+
}
|
|
297
|
+
return marks;
|
|
298
|
+
}
|
|
299
|
+
// Strip the part-body framing the regexes /^(\r?\n){2}/ and /\r?\n$/ removed: the two leading line terminators
|
|
300
|
+
// (all-or-nothing) that follow the header/body separator, and one trailing terminator.
|
|
301
|
+
function _stripTwoLeadingEol(seg, from) {
|
|
302
|
+
var q = from;
|
|
303
|
+
for (var t = 0; t < 2; t++) {
|
|
304
|
+
if (_charCodeAt(seg, q) === 0x0d && _charCodeAt(seg, q + 1) === 0x0a) q += 2;
|
|
305
|
+
else if (_charCodeAt(seg, q) === 0x0a) q += 1;
|
|
306
|
+
else return from;
|
|
307
|
+
}
|
|
308
|
+
return q;
|
|
309
|
+
}
|
|
310
|
+
function _stripOneTrailingEol(seg, end) {
|
|
311
|
+
if (end >= 1 && _charCodeAt(seg, end - 1) === 0x0a) {
|
|
312
|
+
if (end >= 2 && _charCodeAt(seg, end - 2) === 0x0d) return end - 2;
|
|
313
|
+
return end - 1;
|
|
314
|
+
}
|
|
315
|
+
return end;
|
|
316
|
+
}
|
|
317
|
+
// Unfold folded MIME header lines (/\r?\n(?=[ \t])/g -> ""): a CRLF/LF immediately followed by SP/HTAB is a
|
|
318
|
+
// FOLD, not a header break, so the terminator is dropped while the whitespace stays.
|
|
319
|
+
function _unfoldHeaders(raw) {
|
|
320
|
+
var out = "", last = 0, i = 0, n = raw.length;
|
|
321
|
+
while (i < n) {
|
|
322
|
+
var nlLen = 0;
|
|
323
|
+
if (_charCodeAt(raw, i) === 0x0d && i + 1 < n && _charCodeAt(raw, i + 1) === 0x0a) nlLen = 2;
|
|
324
|
+
else if (_charCodeAt(raw, i) === 0x0a) nlLen = 1;
|
|
325
|
+
if (nlLen > 0) {
|
|
326
|
+
var after = i + nlLen;
|
|
327
|
+
if (after < n && (_charCodeAt(raw, after) === 0x20 || _charCodeAt(raw, after) === 0x09)) { out += _strSlice(raw, last, i); last = after; i = after; continue; }
|
|
328
|
+
}
|
|
329
|
+
i += 1;
|
|
330
|
+
}
|
|
331
|
+
return out + _strSlice(raw, last, n);
|
|
332
|
+
}
|
|
333
|
+
// Split on CRLF/LF (.split(/\r?\n/)) by character scan, preserving JS split semantics (leading/trailing "").
|
|
334
|
+
function _splitCrlf(s) {
|
|
335
|
+
var out = [], last = 0, i = 0, n = s.length;
|
|
336
|
+
while (i < n) {
|
|
337
|
+
var nlLen = 0;
|
|
338
|
+
if (_charCodeAt(s, i) === 0x0d && i + 1 < n && _charCodeAt(s, i + 1) === 0x0a) nlLen = 2;
|
|
339
|
+
else if (_charCodeAt(s, i) === 0x0a) nlLen = 1;
|
|
340
|
+
if (nlLen > 0) { out[out.length] = _strSlice(s, last, i); i += nlLen; last = i; } else i += 1;
|
|
341
|
+
}
|
|
342
|
+
out[out.length] = _strSlice(s, last, n);
|
|
343
|
+
return out;
|
|
344
|
+
}
|
|
345
|
+
|
|
226
346
|
// Split a multipart/mixed body into its parts, each { headers, contentType,
|
|
227
347
|
// body }. A boundary delimiter is `--boundary` only at the START OF A LINE (body
|
|
228
348
|
// start or after CRLF), optional transport-padding, then CRLF (a part) or `--`
|
|
@@ -234,34 +354,30 @@ function splitMultipartMixed(body, contentType) {
|
|
|
234
354
|
var text = guard.text.decode(body, constants.LIMITS.DER_MAX_BYTES * 2, EstError, {
|
|
235
355
|
charset: "latin1", tooLarge: "est/too-large", badInput: "est/bad-input", label: "the multipart body",
|
|
236
356
|
});
|
|
237
|
-
var
|
|
238
|
-
var delimRe = new RegExp("(?:^|\\r?\\n)--" + esc + "(--)?[ \\t]*(?:\\r?\\n|$)", "g");
|
|
239
|
-
var marks = [], m;
|
|
240
|
-
while ((m = delimRe.exec(text)) !== null) {
|
|
241
|
-
marks.push({ at: m.index, bodyStart: delimRe.lastIndex, close: m[1] === "--" });
|
|
242
|
-
if (delimRe.lastIndex === m.index) delimRe.lastIndex += 1; // guard a zero-length match
|
|
243
|
-
}
|
|
357
|
+
var marks = _findMultipartMarks(text, boundary);
|
|
244
358
|
var closeAt = -1;
|
|
245
359
|
for (var c = 0; c < marks.length; c++) { if (marks[c].close) { closeAt = c; break; } }
|
|
246
360
|
if (closeAt === -1) throw E("est/bad-multipart", "the multipart body is missing its terminal boundary (RFC 2046)");
|
|
247
361
|
var parts = [];
|
|
248
362
|
for (var i = 0; i < closeAt; i++) {
|
|
249
|
-
var seg = text
|
|
363
|
+
var seg = _strSlice(text, marks[i].bodyStart, marks[i + 1].at); // between this delimiter and the next
|
|
250
364
|
var sep = seg.indexOf("\r\n\r\n");
|
|
251
365
|
if (sep === -1) sep = seg.indexOf("\n\n");
|
|
252
366
|
if (sep === -1) throw E("est/bad-multipart", "a multipart part is missing its header/body separator");
|
|
253
|
-
var rawHeaders = seg
|
|
254
|
-
var partBody = seg
|
|
367
|
+
var rawHeaders = _strSlice(seg, 0, sep);
|
|
368
|
+
var partBody = _strSlice(seg, _stripTwoLeadingEol(seg, sep), _stripOneTrailingEol(seg, seg.length));
|
|
255
369
|
var headers = {};
|
|
256
370
|
// Unfold MIME part headers (RFC 5322 sec. 2.2.3 / RFC 2046): a CRLF immediately followed by whitespace is a
|
|
257
371
|
// FOLD, not a header break, so it is removed before the split -- a Content-Type whose smime-type parameter
|
|
258
372
|
// continues on the next line must be read whole, not truncated.
|
|
259
|
-
|
|
373
|
+
var lines = _splitCrlf(_unfoldHeaders(rawHeaders));
|
|
374
|
+
for (var li = 0; li < lines.length; li++) {
|
|
375
|
+
var line = lines[li];
|
|
260
376
|
var col = line.indexOf(":");
|
|
261
377
|
if (col > 0) headers[line.slice(0, col).trim().toLowerCase()] = line.slice(col + 1).trim();
|
|
262
|
-
}
|
|
378
|
+
}
|
|
263
379
|
var partCt = headers["content-type"] || "";
|
|
264
|
-
if (
|
|
380
|
+
if (_ciStartsWith(partCt, "MULTIPART/")) throw E("est/bad-multipart", "a nested multipart part is not permitted");
|
|
265
381
|
parts.push({ headers: headers, contentType: partCt, body: partBody });
|
|
266
382
|
}
|
|
267
383
|
return parts;
|
|
@@ -376,7 +492,7 @@ function _recipientMatches(recipientInfos, opts) {
|
|
|
376
492
|
var sn = ias.serialNumber, want;
|
|
377
493
|
if (typeof sn === "bigint" && sn >= 0n) want = sn;
|
|
378
494
|
else if (typeof sn === "number" && Number.isSafeInteger(sn) && sn >= 0) want = BigInt(sn);
|
|
379
|
-
else if (typeof sn === "string" &&
|
|
495
|
+
else if (typeof sn === "string" && pkix.allCharsIn(sn, _DIGITS_TABLE)) want = BigInt(sn);
|
|
380
496
|
else throw E("est/bad-input", "expectedRecipientIssuerSerial.serialNumber must be a NON-NEGATIVE bigint, a safe non-negative integer, or a decimal digit string (a certificate serial is non-negative, RFC 5280 sec. 4.1.2.2)");
|
|
381
497
|
// An issuerAndSerialNumber names an ASYMMETRIC recipient -- a certificate identity the server derived from
|
|
382
498
|
// an AsymmetricDecryptKeyIdentifier (RFC 7030 sec. 4.4.2); a symmetric KEK recipient has none. So a SYMMETRIC
|
|
@@ -442,12 +558,31 @@ function _ctParam(contentType, name) {
|
|
|
442
558
|
return { value: value, duplicated: count > 1 };
|
|
443
559
|
}
|
|
444
560
|
|
|
561
|
+
// A media type `type/subtype` by character scan (RFC 2045 sec. 5.1 restricted token): the first character
|
|
562
|
+
// of each part is alphanumeric, the rest add !#$&^_.+- (case-insensitive), exactly one `/` separates them.
|
|
563
|
+
var _MT_FIRST = pkix.charTable("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789");
|
|
564
|
+
var _MT_REST = pkix.charTable("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!#$&^_.+-");
|
|
565
|
+
function _isMediaToken(s, from, to) {
|
|
566
|
+
if (to <= from || !_MT_FIRST[_charCodeAt(s, from)]) return false;
|
|
567
|
+
for (var i = from + 1; i < to; i++) { if (!_MT_REST[_charCodeAt(s, i)]) return false; }
|
|
568
|
+
return true;
|
|
569
|
+
}
|
|
570
|
+
// /^\s*([a-z0-9][a-z0-9!#$&^_.+-]*\/[a-z0-9][a-z0-9!#$&^_.+-]*)\s*$/i -> the lowercased media type, or null:
|
|
571
|
+
// trim JS whitespace, require a single `/` splitting two restricted tokens.
|
|
572
|
+
function _mediaType(seg) {
|
|
573
|
+
var a = 0, b = seg.length, slash = -1, i;
|
|
574
|
+
while (a < b && pkix.isJsWhitespace(_charCodeAt(seg, a))) a += 1;
|
|
575
|
+
while (b > a && pkix.isJsWhitespace(_charCodeAt(seg, b - 1))) b -= 1;
|
|
576
|
+
for (i = a; i < b; i++) { if (_charCodeAt(seg, i) === 0x2f) { if (slash !== -1) return null; slash = i; } }
|
|
577
|
+
if (slash === -1 || !_isMediaToken(seg, a, slash) || !_isMediaToken(seg, slash + 1, b)) return null;
|
|
578
|
+
return _toLowerCase(_strSlice(seg, a, b));
|
|
579
|
+
}
|
|
580
|
+
|
|
445
581
|
function _partMediaType(contentType) {
|
|
446
582
|
var segs = _splitContentTypeParams(String(contentType || ""));
|
|
447
|
-
var mediaMatch = /^\s*([a-z0-9][a-z0-9!#$&^_.+-]*\/[a-z0-9][a-z0-9!#$&^_.+-]*)\s*$/i.exec(segs[0]);
|
|
448
583
|
var st = _ctParam(contentType, "smime-type");
|
|
449
584
|
return {
|
|
450
|
-
media:
|
|
585
|
+
media: _mediaType(segs[0]),
|
|
451
586
|
smimeType: st.value === null ? null : st.value.toLowerCase(),
|
|
452
587
|
ambiguous: st.duplicated,
|
|
453
588
|
};
|
|
@@ -679,14 +814,15 @@ function classifyResponse(status, headers, body, opts) {
|
|
|
679
814
|
function paths(baseUrl, opts) {
|
|
680
815
|
opts = opts || {};
|
|
681
816
|
_knownOpts(opts, PATHS_OPTS, "paths");
|
|
682
|
-
var
|
|
817
|
+
var base = String(baseUrl);
|
|
818
|
+
var prefix = base.slice(0, _stripTrailingSlashEnd(base)) + "/.well-known/est";
|
|
683
819
|
if (opts.label != null) {
|
|
684
820
|
var label = String(opts.label);
|
|
685
821
|
// A label is ONE URL path segment: unreserved characters only (RFC 3986),
|
|
686
822
|
// never a dot-segment or an operation name. Rejecting rather than
|
|
687
823
|
// percent-encoding keeps a reserved char (`/` `?` `#` `%`) or `..` from
|
|
688
824
|
// silently retargeting the request to a different resource.
|
|
689
|
-
if (label === "" || label === "." || label === ".." ||
|
|
825
|
+
if (label === "" || label === "." || label === ".." || !pkix.allCharsIn(label, _LABEL_TABLE) || OPERATIONS.indexOf(label) !== -1) {
|
|
690
826
|
throw E("est/bad-label", "an EST CA label must be a single path segment of unreserved characters, not '.' / '..' or an operation name (RFC 7030 sec. 3.2.2)");
|
|
691
827
|
}
|
|
692
828
|
prefix += "/" + label;
|
|
@@ -707,8 +843,10 @@ function challengePasswordFromTlsUnique(channelBinding) {
|
|
|
707
843
|
// The count comes from the guard, so an own `length` on the caller's Buffer cannot report a
|
|
708
844
|
// full one as empty. `length` is shadowable on any instance, and `Object.defineProperty(buf,
|
|
709
845
|
// "length", { get: function () { return 0; } })` had this refuse bytes that were there.
|
|
710
|
-
if (!
|
|
711
|
-
var
|
|
846
|
+
if (!guard.bytes.isByteSource(channelBinding)) throw E("est/bad-input", "challengePasswordFromTlsUnique requires the channel-binding bytes");
|
|
847
|
+
var _cb = guard.bytes.source(channelBinding, EstError, "est/bad-input", "the channel-binding bytes");
|
|
848
|
+
if (_cb.length === 0) throw E("est/bad-input", "challengePasswordFromTlsUnique requires the channel-binding bytes");
|
|
849
|
+
var b64 = _cb.toString("base64");
|
|
712
850
|
if (b64.length > 255) throw E("est/tls-unique-too-long", "the base64 tls-unique value exceeds 255 octets (RFC 7030 sec. 3.5)");
|
|
713
851
|
return _attr(OID_CHALLENGE_PASSWORD, [asn1.build.printable(b64)]);
|
|
714
852
|
}
|
|
@@ -834,9 +972,9 @@ var MAX_TIMEOUT = constants.TIME.seconds(600);
|
|
|
834
972
|
// The DER of a caller-supplied CSR: a DER Buffer as-is, or a PEM "CERTIFICATE REQUEST"
|
|
835
973
|
// decoded. Any other input is a config-time est/bad-input.
|
|
836
974
|
function _csrDer(input) {
|
|
837
|
-
if (
|
|
975
|
+
if (guard.bytes.isByteSource(input)) return guard.bytes.snapshotSource(input, EstError, "est/bad-input", "a CSR");
|
|
838
976
|
if (typeof input === "string") return csr.pemDecode(input);
|
|
839
|
-
throw E("est/bad-input", "a CSR must be a DER
|
|
977
|
+
throw E("est/bad-input", "a CSR must be a DER BufferSource or a PEM CERTIFICATE REQUEST string");
|
|
840
978
|
}
|
|
841
979
|
|
|
842
980
|
// Parse + validate the request URL BEFORE any transport is called: a URL that will not
|
|
@@ -880,9 +1018,47 @@ function _redirectTarget(current, location, method, opts) {
|
|
|
880
1018
|
// which must NOT be read as a Basic challenge), then the scheme is matched at the start or after a
|
|
881
1019
|
// comma-separated challenge, followed by whitespace / a comma / end -- never a substring, so `Digest
|
|
882
1020
|
// realm="basic"` is not taken as Basic. `scheme` is always our own literal ("Basic" / "Digest").
|
|
1021
|
+
// Blank the CONTENTS of every complete quoted-string (/"(?:[^"\\]|\\.)*"/g -> `""`) by character scan: a
|
|
1022
|
+
// `\` escapes the next character, so `\"` does not close; an unterminated final quote is left as written
|
|
1023
|
+
// (the regex only matches a closed quoted-string).
|
|
1024
|
+
function _blankQuotedStrings(s) {
|
|
1025
|
+
var out = "", last = 0, i = 0, n = s.length;
|
|
1026
|
+
while (i < n) {
|
|
1027
|
+
if (_charCodeAt(s, i) === 0x22) {
|
|
1028
|
+
var j = i + 1, closed = -1;
|
|
1029
|
+
while (j < n) { var c = _charCodeAt(s, j); if (c === 0x5c) { j += 2; continue; } if (c === 0x22) { closed = j; break; } j += 1; }
|
|
1030
|
+
if (closed === -1) break;
|
|
1031
|
+
out += _strSlice(s, last, i) + "\"\"";
|
|
1032
|
+
last = closed + 1; i = closed + 1;
|
|
1033
|
+
} else i += 1;
|
|
1034
|
+
}
|
|
1035
|
+
return out + _strSlice(s, last, n);
|
|
1036
|
+
}
|
|
1037
|
+
// Is `scheme` (our own literal, case-insensitive) offered as a challenge TOKEN in the blanked header?
|
|
1038
|
+
// (?:^|,)\s*SCHEME(?=[\s,]|$): at the start or after a comma, optional whitespace, the scheme, then a
|
|
1039
|
+
// whitespace / comma / end boundary -- never a substring, so `Digest realm="basic"` is not read as Basic.
|
|
1040
|
+
function _schemeOffered(s, scheme) {
|
|
1041
|
+
var n = s.length, slen = scheme.length, p, k;
|
|
1042
|
+
for (p = 0; p <= n; p++) {
|
|
1043
|
+
if (p !== 0 && _charCodeAt(s, p - 1) !== 0x2c) continue;
|
|
1044
|
+
var q = p;
|
|
1045
|
+
while (q < n && pkix.isJsWhitespace(_charCodeAt(s, q))) q += 1;
|
|
1046
|
+
if (q + slen > n) continue;
|
|
1047
|
+
var ok = true;
|
|
1048
|
+
for (k = 0; k < slen; k++) {
|
|
1049
|
+
var x = _charCodeAt(s, q + k), y = _charCodeAt(scheme, k);
|
|
1050
|
+
if (x >= 65 && x <= 90) x += 32;
|
|
1051
|
+
if (y >= 65 && y <= 90) y += 32;
|
|
1052
|
+
if (x !== y) { ok = false; break; }
|
|
1053
|
+
}
|
|
1054
|
+
if (!ok) continue;
|
|
1055
|
+
var after = q + slen;
|
|
1056
|
+
if (after === n || pkix.isJsWhitespace(_charCodeAt(s, after)) || _charCodeAt(s, after) === 0x2c) return true;
|
|
1057
|
+
}
|
|
1058
|
+
return false;
|
|
1059
|
+
}
|
|
883
1060
|
function _offersScheme(www, scheme) {
|
|
884
|
-
|
|
885
|
-
return new RegExp("(?:^|,)\\s*" + scheme + "(?=[\\s,]|$)", "i").test(s);
|
|
1061
|
+
return _schemeOffered(_blankQuotedStrings(String(www || "")), scheme);
|
|
886
1062
|
}
|
|
887
1063
|
// The chosen HTTP auth scheme: an explicit opts.auth.scheme wins; a legacy opts.username / opts.password
|
|
888
1064
|
// WITHOUT opts.auth means Basic (backwards compatible); otherwise none. There is NO scheme:"auto" -- a
|
|
@@ -1573,8 +1749,8 @@ function _collectNestedPaths(list, prefix, out, depth) {
|
|
|
1573
1749
|
function _assertAgrees(name, supplied, carried) {
|
|
1574
1750
|
if (supplied == null) return;
|
|
1575
1751
|
var same;
|
|
1576
|
-
if (
|
|
1577
|
-
same = Buffer.isBuffer(carried) &&
|
|
1752
|
+
if (guard.bytes.isByteSource(supplied)) {
|
|
1753
|
+
same = Buffer.isBuffer(carried) && guard.bytes.source(supplied, EstError, "est/bad-input", "opts.senderNonce").equals(carried);
|
|
1578
1754
|
} else {
|
|
1579
1755
|
// Through the shared authoring guard, not a bare BigInt(): a `number` above
|
|
1580
1756
|
// Number.MAX_SAFE_INTEGER has already lost the digits that distinguish it from
|
|
@@ -1624,8 +1800,8 @@ function _shallowCopy(o) {
|
|
|
1624
1800
|
function _cmcRequestDer(request) {
|
|
1625
1801
|
// COPIED, not aliased: these bytes are parsed now (for the requested keys) and
|
|
1626
1802
|
// transmitted later, so sharing the caller's buffer would let the two disagree.
|
|
1627
|
-
if (
|
|
1628
|
-
return guard.bytes.
|
|
1803
|
+
if (guard.bytes.isByteSource(request)) {
|
|
1804
|
+
return guard.bytes.snapshotSource(request, EstError, "est/bad-input", "the Full PKI Request");
|
|
1629
1805
|
}
|
|
1630
1806
|
if (typeof request === "string") return cms.pemDecode(request);
|
|
1631
1807
|
throw E("est/bad-input", "pki.est.fullcmc requires the Full PKI Request as DER bytes or a PEM CMS block");
|
|
@@ -1991,13 +2167,37 @@ function _serverkeygenEncryptionFromCsr(csrDer) {
|
|
|
1991
2167
|
// The delivered private key rides TLS confidentiality alone: a NULL / anonymous / EXPORT suite MUST NOT be
|
|
1992
2168
|
// used to carry it (RFC 7030 sec. 4.4, sec. 6). Assert (never reconfigure) the negotiated cipher when the
|
|
1993
2169
|
// transport surfaces it; an injected transport that reports no cipher is trusted (the test/loopback channel).
|
|
2170
|
+
// The weak-cipher token scan (RFC 3986/2119-free ASCII, by character code): a "word" character is
|
|
2171
|
+
// [A-Za-z0-9_] (regex \w), and a boundary (\b) sits where a word character meets a non-word one or a
|
|
2172
|
+
// string edge. `_subAt`/`_containsSub` test a literal token at / anywhere in an already-uppercased name.
|
|
2173
|
+
function _isWordChar(c) { return (c >= 48 && c <= 57) || (c >= 65 && c <= 90) || (c >= 97 && c <= 122) || c === 95; }
|
|
2174
|
+
function _subAt(s, sub, i) { var m = sub.length; if (i + m > s.length) return false; for (var k = 0; k < m; k++) { if (_charCodeAt(s, i + k) !== _charCodeAt(sub, k)) return false; } return true; }
|
|
2175
|
+
function _containsSub(s, sub) { for (var i = 0; i + sub.length <= s.length; i++) { if (_subAt(s, sub, i)) return true; } return false; }
|
|
2176
|
+
function _wbBefore(s, i) { return i === 0 || !_isWordChar(_charCodeAt(s, i - 1)); }
|
|
2177
|
+
function _wbAfter(s, j) { return j === s.length || !_isWordChar(_charCodeAt(s, j)); }
|
|
2178
|
+
// /NULL|ANON|EXPORT|\bEXP[-_0-9]|\bA(EC)?DH\b/ over the uppercased name: the three plain substrings, then
|
|
2179
|
+
// EXP at a word boundary followed by a separator OR a digit run (EXP-RC4-MD5, EXP1024-RC4-SHA -- a bare
|
|
2180
|
+
// \bEXP\b misses EXP1024), then ADH / AECDH each bounded on both sides.
|
|
2181
|
+
function _hasWeakCipherToken(name) {
|
|
2182
|
+
if (_containsSub(name, "NULL") || _containsSub(name, "ANON") || _containsSub(name, "EXPORT")) return true;
|
|
2183
|
+
var i, n = name.length;
|
|
2184
|
+
for (i = 0; i + 3 <= n; i++) {
|
|
2185
|
+
if (_subAt(name, "EXP", i) && _wbBefore(name, i)) {
|
|
2186
|
+
var c = _charCodeAt(name, i + 3);
|
|
2187
|
+
if (c === 45 || c === 95 || (c >= 48 && c <= 57)) return true;
|
|
2188
|
+
}
|
|
2189
|
+
}
|
|
2190
|
+
for (i = 0; i < n; i++) {
|
|
2191
|
+
if (_subAt(name, "ADH", i) && _wbBefore(name, i) && _wbAfter(name, i + 3)) return true;
|
|
2192
|
+
if (_subAt(name, "AECDH", i) && _wbBefore(name, i) && _wbAfter(name, i + 5)) return true;
|
|
2193
|
+
}
|
|
2194
|
+
return false;
|
|
2195
|
+
}
|
|
1994
2196
|
function _assertConfidentialCipher(res) {
|
|
1995
2197
|
if (!res || !res.tls || !res.tls.cipher) return;
|
|
1996
2198
|
var c = res.tls.cipher;
|
|
1997
2199
|
var name = (String(c.name || "") + " " + String(c.standardName || "")).toUpperCase();
|
|
1998
|
-
|
|
1999
|
-
// digit run (EXP-RC4-MD5, EXP1024-RC4-SHA), so match EXP before either -- a bare `\bEXP\b` misses EXP1024.
|
|
2000
|
-
if (/NULL|ANON|EXPORT|\bEXP[-_0-9]|\bA(EC)?DH\b/.test(name)) throw E("est/weak-cipher", "the serverkeygen channel negotiated a NULL / anonymous / EXPORT cipher (" + (c.standardName || c.name) + "), which cannot protect the delivered private key (RFC 7030 sec. 4.4)");
|
|
2200
|
+
if (_hasWeakCipherToken(name)) throw E("est/weak-cipher", "the serverkeygen channel negotiated a NULL / anonymous / EXPORT cipher (" + (c.standardName || c.name) + "), which cannot protect the delivered private key (RFC 7030 sec. 4.4)");
|
|
2001
2201
|
}
|
|
2002
2202
|
|
|
2003
2203
|
// A case-insensitive header lookup: the injectable transport seam only promises { status, headers, body }, so a
|
|
@@ -2130,7 +2330,7 @@ function serverkeygen(baseUrl, csrInput, opts) {
|
|
|
2130
2330
|
var eis = opts.expectedRecipientIssuerSerial;
|
|
2131
2331
|
if (typeof eis !== "object" || Buffer.isBuffer(eis) || !Buffer.isBuffer(eis.issuer)) throw E("est/bad-input", "opts.expectedRecipientIssuerSerial must be { issuer: Buffer, serialNumber }");
|
|
2132
2332
|
var s = eis.serialNumber;
|
|
2133
|
-
if (!((typeof s === "bigint" && s >= 0n) || (typeof s === "number" && Number.isSafeInteger(s) && s >= 0) || (typeof s === "string" &&
|
|
2333
|
+
if (!((typeof s === "bigint" && s >= 0n) || (typeof s === "number" && Number.isSafeInteger(s) && s >= 0) || (typeof s === "string" && pkix.allCharsIn(s, _DIGITS_TABLE)))) throw E("est/bad-input", "opts.expectedRecipientIssuerSerial.serialNumber must be a NON-NEGATIVE bigint, a safe non-negative integer, or a decimal digit string (a certificate serial is non-negative, RFC 5280 sec. 4.1.2.2)");
|
|
2134
2334
|
}
|
|
2135
2335
|
// A recipient expectation implies the key part MUST be encrypted to that recipient; if the CSR advertised no
|
|
2136
2336
|
// key-encryption attribute the coherence check below never runs, so a compromised CA could deliver the key
|
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
|
//
|