@blamejs/pki 0.5.31 → 0.5.33
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 +25 -1
- package/README.md +3 -3
- 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 +214 -93
- package/lib/cms-verify.js +363 -237
- 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 +56 -4
- package/lib/guard-intrinsic.js +13 -0
- 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 +80 -1
- package/lib/x509-sign.js +1 -1
- package/package.json +2 -2
- package/sbom.cdx.json +6 -6
package/lib/crl-sign.js
CHANGED
|
@@ -180,7 +180,7 @@ function _resolveReason(reason, isDelta) {
|
|
|
180
180
|
// The AKI keyIdentifier from the issuer: an explicit Buffer, or true -> the issuer cert's subjectKeyIdentifier,
|
|
181
181
|
// else the SHA-1 of the issuer SPKI (RFC 5280 sec. 5.2.1 key-identifier method).
|
|
182
182
|
function _akiKeyId(val, ctx) {
|
|
183
|
-
if (
|
|
183
|
+
if (guard.bytes.isByteSource(val)) return guard.bytes.snapshotSource(val, CrlError, "crl/bad-input", "the authorityKeyIdentifier keyIdentifier");
|
|
184
184
|
if (val === true) {
|
|
185
185
|
if (ctx.issuerCert) {
|
|
186
186
|
var ski = (ctx.issuerCert.extensions || []).filter(function (e) { return e.oid === OID_SKI; })[0];
|
|
@@ -597,7 +597,7 @@ function _coerceCrl(crl) {
|
|
|
597
597
|
// names, are separate questions that only a certificate can answer.
|
|
598
598
|
function _resolveIssuer(issuer) {
|
|
599
599
|
if (issuer == null) throw _err("crl/bad-input", "an issuer is required to verify a CRL");
|
|
600
|
-
if (
|
|
600
|
+
if (guard.bytes.isByteSource(issuer)) { var _isp = guard.bytes.source(issuer, CrlError, "crl/bad-input", "issuer SPKI"); _assertValidSpki(_isp, "issuer SPKI"); return { spki: _isp, cert: null }; }
|
|
601
601
|
if (issuer.cert != null) { var ic = _parseIssuerCert(issuer.cert); return { spki: ic.subjectPublicKeyInfo.bytes, cert: ic }; }
|
|
602
602
|
if (issuer.publicKey != null) { var spki = _reqDer(issuer.publicKey, "issuer.publicKey"); _assertValidSpki(spki, "issuer.publicKey"); return { spki: spki, cert: null }; }
|
|
603
603
|
// A parsed certificate passed directly. It reaches the same identity and cRLSign checks the
|
|
@@ -685,8 +685,8 @@ function _serialHexOf(serial) {
|
|
|
685
685
|
if (typeof serial === "bigint") v = serial;
|
|
686
686
|
else if (typeof serial === "number") { if (!Number.isSafeInteger(serial)) throw _err("crl/bad-input", "serialNumber number must be a safe integer (pass a BigInt, hex string, or Buffer)"); v = BigInt(serial); }
|
|
687
687
|
else if (typeof serial === "string") { try { v = BigInt(serial); } catch (e) { throw _err("crl/bad-input", "serialNumber string must be a decimal or 0x-hex integer", e); } }
|
|
688
|
-
else if (
|
|
689
|
-
else throw _err("crl/bad-input", "serialNumber must be a BigInt, integer, hex string, or
|
|
688
|
+
else if (guard.bytes.isByteSource(serial)) { var _sb = guard.bytes.source(serial, CrlError, "crl/bad-input", "serialNumber"); v = _sb.length ? BigInt("0x" + _sb.toString("hex")) : 0n; }
|
|
689
|
+
else throw _err("crl/bad-input", "serialNumber must be a BigInt, integer, hex string, or BufferSource");
|
|
690
690
|
if (v <= 0n) throw _err("crl/bad-input", "serialNumber must be a positive integer");
|
|
691
691
|
// Match schema-crl's serialNumberHex: the DER INTEGER content octets (preserving sign padding).
|
|
692
692
|
return asn1.decode(b.integer(v)).content.toString("hex");
|
package/lib/crmf-sign.js
CHANGED
|
@@ -328,7 +328,8 @@ function _buildEncryptedKeyPop(pop, outerTag, template) {
|
|
|
328
328
|
// reject. CBC is the default here for that reason, not as a preference between the two.
|
|
329
329
|
var cea = pop.contentEncryptionAlgorithm || "aes-256-cbc";
|
|
330
330
|
if (typeof cea !== "string") throw _err("crmf/bad-input", "pop.contentEncryptionAlgorithm must be an algorithm name");
|
|
331
|
-
|
|
331
|
+
var _ceaLc = intrinsic.toLowerCase(cea); // /gcm|ccm|chacha/i (ASCII algorithm name)
|
|
332
|
+
if (intrinsic.stringIndexOf(_ceaLc, "gcm") !== -1 || intrinsic.stringIndexOf(_ceaLc, "ccm") !== -1 || intrinsic.stringIndexOf(_ceaLc, "chacha") !== -1) {
|
|
332
333
|
throw _err("crmf/bad-popo", "encryptedKey is typed EnvelopedData, and the AEAD algorithm " + cea +
|
|
333
334
|
" produces an AuthEnvelopedData instead (RFC 5083) -- choose a CBC content-encryption algorithm");
|
|
334
335
|
}
|
package/lib/csr-sign.js
CHANGED
|
@@ -47,9 +47,6 @@ var _b = pkiBuild.makeBuilder({
|
|
|
47
47
|
NAME_SCHEMA: pkix.name(NS), SPKI_SCHEMA: pkix.spki(NS), EXT_DECODERS: EXT_DECODERS,
|
|
48
48
|
});
|
|
49
49
|
|
|
50
|
-
// challengePassword DirectoryString charset (PrintableString set); anything else uses UTF8String.
|
|
51
|
-
var PRINTABLE_RE = /^[A-Za-z0-9 '()+,\-./:=?]*$/;
|
|
52
|
-
|
|
53
50
|
// attributes ::= [0] IMPLICIT SET OF Attribute (RFC 2986). The IMPLICIT tag replaces the universal SET,
|
|
54
51
|
// so it is a context-constructed [0] whose DER-sorted members ARE the SET-OF; empty -> A0 00.
|
|
55
52
|
function _implicitSetOf0(members) {
|
|
@@ -64,7 +61,7 @@ function _buildRequestedExtensions(extSpec, subjectSpki) { return _b.requestedEx
|
|
|
64
61
|
// value is in that charset, else UTF8String; never a Teletex/BMP/Universal string for a new value.
|
|
65
62
|
function _challengePassword(pw) {
|
|
66
63
|
if (typeof pw !== "string" || pw.length < 1 || pw.length > 255) throw _err("csr/bad-input", "challengePassword must be a 1..255 character string (RFC 2985 sec. 5.4.1)");
|
|
67
|
-
return
|
|
64
|
+
return asn1.isPrintableString(pw) ? b.printable(pw) : b.utf8(pw);
|
|
68
65
|
}
|
|
69
66
|
|
|
70
67
|
/**
|
package/lib/ct.js
CHANGED
|
@@ -49,6 +49,10 @@ var intrinsic = require("./guard-intrinsic.js");
|
|
|
49
49
|
// replaceable properties to answer one thing, and here it decides whether a log state is one this
|
|
50
50
|
// toolkit trusts -- answering true for a name the table never carried trusts an untrusted log.
|
|
51
51
|
var _hasOwn = intrinsic.hasOwn;
|
|
52
|
+
var _strSlice = intrinsic.uncurry(String.prototype.slice);
|
|
53
|
+
var _JSON_SUFFIX = ".json";
|
|
54
|
+
// A pathname ending with the literal ".json" suffix (matching /\.json$/), by suffix compare (no regex).
|
|
55
|
+
function _endsWithJson(s) { return s.length >= _JSON_SUFFIX.length && _strSlice(s, s.length - _JSON_SUFFIX.length) === _JSON_SUFFIX; }
|
|
52
56
|
var ByteReader = require("./byte-reader.js");
|
|
53
57
|
var ByteWriter = require("./byte-writer.js");
|
|
54
58
|
var oid = require("./oid.js");
|
|
@@ -973,9 +977,9 @@ function _parseHttpsUrl(u, label) {
|
|
|
973
977
|
// yield an unambiguous sig URL, so the caller must pass opts.sigUrl explicitly (guards never guess a value).
|
|
974
978
|
// The derived URL inherits the json URL's https origin.
|
|
975
979
|
function _sigUrlFor(parsed) {
|
|
976
|
-
if (
|
|
980
|
+
if (!_endsWithJson(parsed.pathname)) throw _ctErr("ct/bad-input", "cannot derive the detached-signature URL from a non-.json path (" + JSON.stringify(parsed.pathname) + "); pass opts.sigUrl explicitly");
|
|
977
981
|
var sig = new URL(parsed.href);
|
|
978
|
-
sig.pathname = parsed.pathname.
|
|
982
|
+
sig.pathname = _strSlice(parsed.pathname, 0, parsed.pathname.length - _JSON_SUFFIX.length) + ".sig";
|
|
979
983
|
return sig.href;
|
|
980
984
|
}
|
|
981
985
|
|
|
@@ -994,8 +998,8 @@ function _fetchBody(transport, url, req, label) {
|
|
|
994
998
|
res = res || {};
|
|
995
999
|
var h = {};
|
|
996
1000
|
Object.keys(res.headers || {}).forEach(function (k) { h[k.toLowerCase()] = res.headers[k]; });
|
|
997
|
-
var body =
|
|
998
|
-
? guard.bytes.
|
|
1001
|
+
var body = guard.bytes.isByteSource(res.body)
|
|
1002
|
+
? guard.bytes.source(res.body, CtError, "ct/bad-input", label)
|
|
999
1003
|
: Buffer.from(String(res.body == null ? "" : res.body), "utf8");
|
|
1000
1004
|
if (body.length > req.maxResponseBytes) throw _ctErr("ct/response-too-large", "the " + label + " (" + body.length + " bytes) exceeds the " + req.maxResponseBytes + "-byte cap");
|
|
1001
1005
|
if (res.status !== 200) throw _ctErr("ct/http-error", "the CT server returned HTTP " + JSON.stringify(res.status) + " for the " + label);
|
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
|
};
|