@blamejs/pki 0.5.4 → 0.5.6

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/lib/cms-verify.js CHANGED
@@ -538,7 +538,10 @@ function _surfaceUnsignedAttrs(si) {
538
538
  // Verify every countersignature attached to `si` (RFC 5652 sec. 11.4): each id-countersignature
539
539
  // value is a SignerInfo over `si`'s signature octets. Returns per-countersignature verdicts; a
540
540
  // countersignature's OWN countersignatures verify over ITS signature octets (recursive). A
541
- // present-but-invalid countersignature is surfaced ok:false -- never silently dropped. The recursion
541
+ // countersignature that FAILS TO VERIFY is surfaced ok:false -- never silently dropped, and never
542
+ // allowed to change the primary verdict. A countersignature value that is not a well-formed
543
+ // SignerInfo does not reach here at all: the decoder validates every id-countersignature value by
544
+ // content rather than on the attribute type, so such a message is refused whole. The recursion
542
545
  // terminates because it only walks the FINITE parsed structure: each nested countersignature value
543
546
  // is a sub-encoding of its parent, and the strict decoder already bounds total nesting by
544
547
  // C.LIMITS.DER_MAX_DEPTH at parse (CWE-834/770), so a hostile deep chain fails closed before verify.
@@ -650,7 +653,14 @@ function _snapshotIfBytes(input, label) {
650
653
  // capability cannot arrive with its option silently ignored at this boundary.
651
654
  var _VERIFY_OPTS = { certs: 1, content: 1, trustAnchors: 1, time: 1, requiredEku: 1, checkPurpose: 1 };
652
655
 
656
+ // Documented `-> Promise`, so a fault leaves as a REJECTION (guard-async). The checks below stay
657
+ // synchronous -- they read a caller's mutable options and bytes, and resolving those before any turn
658
+ // passes is what stops a value being swapped between the check and the use.
653
659
  function verify(input, opts) {
660
+ return guard.async.deferred(function () { return _verify(input, opts); });
661
+ }
662
+
663
+ function _verify(input, opts) {
654
664
  opts = opts || {};
655
665
  if (typeof opts !== "object" || Buffer.isBuffer(opts)) throw _err("cms/bad-input", "pki.cms.verify options must be an object");
656
666
  // An unrecognized option is refused, not swallowed. This is what kept the missing trust seam
@@ -663,13 +673,17 @@ function verify(input, opts) {
663
673
  // the parse surfaced -- the signed content above all -- stays a view into the
664
674
  // caller's memory across that await, and a buffer rewritten in the gap would leave
665
675
  // the result describing one message while the signature was checked over another.
666
- // Only a Buffer / Uint8Array can change underneath us: a PEM string is immutable
667
- // and passes through untouched, and an already-parsed object is a lifetime the
668
- // caller chose. Narrow rather than blanket, so neither input form is refused here
669
- // that pki.cms.parse itself accepts.
670
- var parsed = (input && typeof input === "object" && !Buffer.isBuffer(input) && Array.isArray(input.signerInfos))
671
- ? input
672
- : cms.parse(_snapshotIfBytes(input, "pki.cms.verify"));
676
+ // Re-derived from the bytes the parser read, never trusted as the object it arrives as. A
677
+ // SignedData's meaning is a signature over a byte range, but a parsed one presents that range
678
+ // (`signedAttrsBytes`, or the encapsulated `eContent` when there are no signed attributes), the
679
+ // signature, the algorithms and the certificates as SEPARATE properties. Keep a genuine signer's
680
+ // signature and signed attributes and put different content beside them and every part of this
681
+ // check passes for a message that signer never signed -- the forgery this verb's own block claims
682
+ // to defend (CWE-347). A duck-type test on `signerInfos` cannot see that, because every field is
683
+ // individually well-formed; only re-deriving them all from one byte string can.
684
+ var parsed = guard.parsed.acceptDerived(input, "cms", function (bytes) {
685
+ return cms.parse(_snapshotIfBytes(bytes, "pki.cms.verify"));
686
+ }, _err, "cms/bad-input", "the SignedData");
673
687
  if (!Array.isArray(parsed.signerInfos)) throw _err("cms/bad-input", "input is not a CMS SignedData");
674
688
  var content = parsed.encapContentInfo.eContent;
675
689
  if (content == null) {
package/lib/crl-sign.js CHANGED
@@ -375,9 +375,10 @@ function _buildRevoked(entryList, isDelta) {
375
375
  // ---- the primitives --------------------------------------------------------
376
376
 
377
377
  function _parseIssuerCert(cert) {
378
- var parsed = (Buffer.isBuffer(cert) || typeof cert === "string") ? x509Schema.parse(cert) : cert;
379
- if (!parsed || !parsed.tbsBytes || !parsed.subjectPublicKeyInfo) throw _err("crl/bad-input", "issuer.cert must be a certificate DER/PEM or a parsed certificate");
380
- return parsed;
378
+ // The issuer certificate decides who may have signed this CRL: its key verifies the signature and
379
+ // its keyUsage says whether it may sign CRLs at all. A caller-assembled one could carry a real
380
+ // CA's name and cRLSign bit beside a substituted key, so it is re-derived like every other.
381
+ return guard.parsed.acceptDerived(cert, "certificate", x509Schema.parse, _err, "crl/bad-input", "issuer.cert");
381
382
  }
382
383
 
383
384
  // RFC 5280 sec. 4.2.1.3 -- a certificate whose key signs CRLs asserts the cRLSign keyUsage bit. When the
@@ -508,10 +509,15 @@ function _sign(spec, issuer, opts) {
508
509
  */
509
510
  function sign(spec, issuer, opts) { return Promise.resolve().then(function () { return _sign(spec, issuer, opts); }); }
510
511
 
512
+ // A CRL these verbs answer from is re-derived from the bytes its parser read. Completeness -- every
513
+ // field present with the right type -- is not enough for a verdict: the signature covers a byte
514
+ // range, while the revocation list and the scope extensions are separate properties of the parsed
515
+ // object. Keep a correctly signed CRL's `tbsBytes` and signature and empty `revokedCertificates`,
516
+ // and the signature still verifies while `isRevoked` answers from the edited list. An emptied
517
+ // `crlExtensions` does the same to scope: a shard that may only speak for some reasons, or a delta
518
+ // that may not answer alone, becomes one that answers for everything.
511
519
  function _coerceCrl(crl) {
512
- if (Buffer.isBuffer(crl) || typeof crl === "string") return crlSchema.parse(crl);
513
- if (crl && typeof crl === "object" && crl.tbsBytes && crl.signatureValue && crl.signatureAlgorithm) return crl;
514
- throw _err("crl/bad-input", "crl must be a CRL DER Buffer, a PEM string, or a parsed CRL (from pki.schema.crl.parse)");
520
+ return guard.parsed.acceptDerived(crl, "crl", crlSchema.parse, _err, "crl/bad-input", "the CRL");
515
521
  }
516
522
 
517
523
  // The issuer's key AND, when the caller supplied a certificate rather than a bare key, the
@@ -523,7 +529,13 @@ function _resolveIssuer(issuer) {
523
529
  if (Buffer.isBuffer(issuer)) { _assertValidSpki(issuer, "issuer SPKI"); return { spki: issuer, cert: null }; }
524
530
  if (issuer.cert != null) { var ic = _parseIssuerCert(issuer.cert); return { spki: ic.subjectPublicKeyInfo.bytes, cert: ic }; }
525
531
  if (issuer.publicKey != null) { var spki = _reqDer(issuer.publicKey, "issuer.publicKey"); _assertValidSpki(spki, "issuer.publicKey"); return { spki: spki, cert: null }; }
526
- if (issuer.subjectPublicKeyInfo && issuer.subjectPublicKeyInfo.bytes) return { spki: issuer.subjectPublicKeyInfo.bytes, cert: issuer }; // a parsed certificate
532
+ // A parsed certificate passed directly. It reaches the same identity and cRLSign checks the
533
+ // { cert } form does, so it goes through the same door: an object carrying an SPKI but missing
534
+ // the subject those checks compare would decide them on undefined.
535
+ if (issuer.subjectPublicKeyInfo && issuer.subjectPublicKeyInfo.bytes) {
536
+ var pc = _parseIssuerCert(issuer);
537
+ return { spki: pc.subjectPublicKeyInfo.bytes, cert: pc };
538
+ }
527
539
  throw _err("crl/bad-input", "issuer must be { cert }, { publicKey } (SPKI DER), or a raw SPKI Buffer");
528
540
  }
529
541
 
package/lib/est.js CHANGED
@@ -86,6 +86,59 @@ var OID_TEMPLATE = oid.byName("certificationRequestInfoTemplate");
86
86
 
87
87
  var OPERATIONS = ["cacerts", "simpleenroll", "simplereenroll", "fullcmc", "serverkeygen", "csrattrs"];
88
88
 
89
+ // ---- the option surface each verb accepts -----------------------------------
90
+ //
91
+ // A misspelled option reads as an omission rather than as a value: nothing is out of range and
92
+ // nothing fails to parse, so the caller who asked for something stricter gets the looser default
93
+ // and is told nothing. That is worst here, where the options carry the security posture of a
94
+ // network exchange -- a misspelled `tls` leaves the anchors unset (the no-anchors refusal names
95
+ // the missing pin, so this one is caught), a misspelled `strict` accepts the extra certificates it
96
+ // was set to reject, a misspelled `expectedRecipientKeyId` drops a recipient pin on a
97
+ // server-generated private key, and a misspelled `oldCert` fails the re-enrollment outright.
98
+ //
99
+ // Every network verb shares the client surface, because every one of them goes through _client and
100
+ // the redirect / authentication plumbing it drives. The per-verb tables extend it rather than
101
+ // restating it, so a key added to the client reaches every verb at once and none of them drifts.
102
+ var CLIENT_OPTS = {
103
+ transport: 1, tls: 1, label: 1, timeout: 1, maxResponseBytes: 1, maxRedirects: 1, now: 1,
104
+ auth: 1, username: 1, password: 1, allowCrossOriginRedirect: 1,
105
+ };
106
+ function _withClient(extra) {
107
+ var out = {};
108
+ Object.keys(CLIENT_OPTS).forEach(function (k) { out[k] = 1; });
109
+ Object.keys(extra || {}).forEach(function (k) { out[k] = 1; });
110
+ return out;
111
+ }
112
+ // `strict` is enroll-only: _certsResult reads it after the /cacerts branch has already returned, so
113
+ // accepting it on cacerts would advertise a check that cannot run there.
114
+ var CACERTS_OPTS = _withClient(null);
115
+ var SIMPLEENROLL_OPTS = _withClient({ strict: 1 });
116
+ var SIMPLEREENROLL_OPTS = _withClient({ strict: 1, oldCert: 1 });
117
+ // expectedRecipientKind is NOT here: it is derived from the CSR's own advertised attribute, never
118
+ // taken from the caller, so listing it would offer a pin that nothing reads.
119
+ var SERVERKEYGEN_OPTS = _withClient({
120
+ requestedEncryption: 1, expectedRecipientKeyId: 1, expectedRecipientIssuerSerial: 1,
121
+ });
122
+ var CSRATTRS_OPTS = _withClient(null);
123
+ var FULLCMC_OPTS = _withClient({
124
+ transactionId: 1, senderNonce: 1, dataReturn: 1,
125
+ responderCerts: 1, responseRecipient: 1, allowUnverifiedResponse: 1,
126
+ });
127
+ // The two verbs that take options without going near the network.
128
+ var CLASSIFY_OPTS = { op: 1, now: 1 };
129
+ var PATHS_OPTS = { label: 1 };
130
+ var PARSE_SERVERKEYGEN_OPTS = {
131
+ requestedEncryption: 1, expectedRecipientKeyId: 1, expectedRecipientKind: 1,
132
+ expectedRecipientIssuerSerial: 1,
133
+ };
134
+
135
+ function _knownOpts(opts, known, verb) {
136
+ guard.identifier.assertKnownKeys(opts, known, E, "est/bad-input", function (k) {
137
+ return "unknown option " + JSON.stringify(k) + " for pki.est." + verb + " -- accepted: " +
138
+ Object.keys(known).sort().join(", ");
139
+ });
140
+ }
141
+
89
142
  // ---- the RFC 8951 sec. 3/3.1 transfer codec (CTE-header-blind) -----------
90
143
 
91
144
  /**
@@ -397,6 +450,7 @@ function _partMediaType(contentType) {
397
450
 
398
451
  function parseServerKeygenResponse(body, contentType, opts) {
399
452
  opts = opts || {};
453
+ _knownOpts(opts, PARSE_SERVERKEYGEN_OPTS, "parseServerKeygenResponse");
400
454
  var parts = splitMultipartMixed(body, contentType);
401
455
  if (parts.length !== 2) throw E("est/bad-multipart", "a serverkeygen response must have exactly two parts (RFC 7030 sec. 4.4.2)");
402
456
  var keyPart = null, certPart = null, encrypted = false;
@@ -524,6 +578,7 @@ var NOT_IMPLEMENTED_OPS = { fullcmc: 1 };
524
578
  */
525
579
  function classifyResponse(status, headers, body, opts) {
526
580
  opts = opts || {};
581
+ _knownOpts(opts, CLASSIFY_OPTS, "classifyResponse");
527
582
  var op = opts.op;
528
583
  // Fail closed on an operation whose response this client cannot validate:
529
584
  // A named op this client cannot validate is a typo, not a pass. An
@@ -618,6 +673,7 @@ function classifyResponse(status, headers, body, opts) {
618
673
  */
619
674
  function paths(baseUrl, opts) {
620
675
  opts = opts || {};
676
+ _knownOpts(opts, PATHS_OPTS, "paths");
621
677
  var prefix = String(baseUrl).replace(/\/+$/, "") + "/.well-known/est";
622
678
  if (opts.label != null) {
623
679
  var label = String(opts.label);
@@ -1152,6 +1208,11 @@ function fullcmc(baseUrl, request, opts) {
1152
1208
  var der, wanted, sent;
1153
1209
  try {
1154
1210
  if (typeof opts !== "object" || Buffer.isBuffer(opts)) throw E("est/bad-input", "pki.est.fullcmc options must be an object");
1211
+ // Inside the try, so the refusal is a REJECTION like every other failure of this verb: it is
1212
+ // documented as Promise-returning, and a synchronous throw escapes the `.catch(...)` a caller
1213
+ // has already written. It stays in the synchronous capture rather than moving into a later turn,
1214
+ // because that is what closes the race described above.
1215
+ _knownOpts(opts, FULLCMC_OPTS, "fullcmc");
1155
1216
  der = _cmcRequestDer(request);
1156
1217
  // Confirm this IS a Full PKI Request before any of it goes over the wire. The
1157
1218
  // bytes are about to be labelled `smime-type=CMC-request`, so a PKIResponse or
@@ -1750,6 +1811,10 @@ function _enroll(op, baseUrl, csrInput, opts) {
1750
1811
  * - `tls` -- { anchors, useSystemStore, cert, key, minVersion, servername, checkServerIdentity }.
1751
1812
  * - `label` -- an OPTIONAL CA label path segment; `timeout` / `maxResponseBytes` / `maxRedirects` -- budgets.
1752
1813
  * - `now` -- receipt time (epoch ms) to render a 202 Retry-After HTTP-date as seconds.
1814
+ * - `auth` -- HTTP authentication: `{ scheme: "basic" | "digest", username, password, allowMD5, allowLegacyQop, maxStaleRetries }`.
1815
+ * There is no `"auto"`: the scheme is chosen here, not by whatever a server offers. `username` / `password`
1816
+ * at the top level are the older form and mean Basic. Answered only after the transport authenticated the server.
1817
+ * - `allowCrossOriginRedirect` -- opt in to following a cross-origin redirect on an unsafe method.
1753
1818
  * @example
1754
1819
  * // a live CA uses the default pki.transport.https; here an injected transport returns a canned bag
1755
1820
  * var r = await pki.est.cacerts("https://ca.example",
@@ -1759,6 +1824,11 @@ function _enroll(op, baseUrl, csrInput, opts) {
1759
1824
  function cacerts(baseUrl, opts) {
1760
1825
  opts = opts || {};
1761
1826
  return Promise.resolve().then(function () {
1827
+ // Inside the promise, like every other refusal these verbs make. They are documented as
1828
+ // Promise-returning, so a caller writes `.catch(...)` -- and a check that throws synchronously
1829
+ // escapes that catch entirely, turning a misspelled option into an uncaught exception rather
1830
+ // than the rejection the caller is already handling.
1831
+ _knownOpts(opts, CACERTS_OPTS, "cacerts");
1762
1832
  return _client("cacerts", "GET", baseUrl, null, { accept: "application/pkcs7-mime" }, opts);
1763
1833
  }).then(function (res) { return _certsResult("cacerts", res, opts, null); });
1764
1834
  }
@@ -1795,7 +1865,10 @@ function cacerts(baseUrl, opts) {
1795
1865
  */
1796
1866
  function simpleenroll(baseUrl, csrInput, opts) {
1797
1867
  opts = opts || {};
1798
- return Promise.resolve().then(function () { return _enroll("simpleenroll", baseUrl, csrInput, opts); });
1868
+ return Promise.resolve().then(function () {
1869
+ _knownOpts(opts, SIMPLEENROLL_OPTS, "simpleenroll");
1870
+ return _enroll("simpleenroll", baseUrl, csrInput, opts);
1871
+ });
1799
1872
  }
1800
1873
 
1801
1874
  /**
@@ -1826,6 +1899,7 @@ function simpleenroll(baseUrl, csrInput, opts) {
1826
1899
  function simplereenroll(baseUrl, csrInput, opts) {
1827
1900
  opts = opts || {};
1828
1901
  return Promise.resolve().then(function () {
1902
+ _knownOpts(opts, SIMPLEREENROLL_OPTS, "simplereenroll");
1829
1903
  if (!opts.oldCert) throw E("est/bad-input", "simplereenroll requires opts.oldCert (the certificate being renewed, RFC 7030 sec. 4.2.2)");
1830
1904
  reenrollGuard(opts.oldCert, _csrDer(csrInput)); // est/reenroll-* on mismatch, BEFORE the POST
1831
1905
  return _enroll("simplereenroll", baseUrl, csrInput, opts);
@@ -1989,6 +2063,7 @@ async function _serverkeygenResult(res, opts, derived) {
1989
2063
  function serverkeygen(baseUrl, csrInput, opts) {
1990
2064
  opts = opts || {};
1991
2065
  return Promise.resolve().then(function () {
2066
+ _knownOpts(opts, SERVERKEYGEN_OPTS, "serverkeygen");
1992
2067
  var csrDer = _csrDer(csrInput);
1993
2068
  var derived = _serverkeygenEncryptionFromCsr(csrDer);
1994
2069
  if (opts.requestedEncryption !== undefined && !!opts.requestedEncryption !== derived.requestedEncryption) throw E("est/bad-input", "opts.requestedEncryption (" + !!opts.requestedEncryption + ") contradicts the CSR's advertised key-encryption attribute (" + derived.requestedEncryption + ") (RFC 7030 sec. 4.4.1)");
@@ -2056,6 +2131,7 @@ function _csrattrsResult(res, opts) {
2056
2131
  function csrattrs(baseUrl, opts) {
2057
2132
  opts = opts || {};
2058
2133
  return Promise.resolve().then(function () {
2134
+ _knownOpts(opts, CSRATTRS_OPTS, "csrattrs");
2059
2135
  return _client("csrattrs", "GET", baseUrl, null, { accept: "application/csrattrs" }, opts);
2060
2136
  }).then(function (res) { return _csrattrsResult(res, opts); });
2061
2137
  }
package/lib/guard-all.js CHANGED
@@ -40,6 +40,10 @@
40
40
  // guard.header.assertField -- emitted MIME/RFC 5322 header field name +
41
41
  // value integrity (CR/LF/NUL header-injection
42
42
  // defence, CWE-93)
43
+ // guard.parsed.accept -- a CLAIMED-parsed structure carries every
44
+ // field the consuming code dereferences
45
+ // (type confusion / unverified provenance,
46
+ // CWE-843 / CWE-345)
43
47
  //
44
48
  // Each shape is enforced by a codebase-patterns detector: the characteristic
45
49
  // token of a guard (the Buffer.from(x.buffer, byteOffset) re-view, the
@@ -60,6 +64,8 @@ var identifier = require("./guard-identifier");
60
64
  var header = require("./guard-header");
61
65
  var compress = require("./guard-compress");
62
66
  var secret = require("./guard-secret");
67
+ var parsed = require("./guard-parsed");
68
+ var async_ = require("./guard-async");
63
69
 
64
70
  module.exports = {
65
71
  bytes: bytes,
@@ -75,4 +81,6 @@ module.exports = {
75
81
  header: header,
76
82
  compress: compress,
77
83
  secret: secret,
84
+ parsed: parsed,
85
+ async: async_,
78
86
  };
@@ -0,0 +1,37 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // Copyright (c) blamejs contributors
3
+ "use strict";
4
+ //
5
+ // @internal -- no operator-facing namespace. The documented surface is every verb whose
6
+ // @signature says `-> Promise<...>`; this is how each of them refuses.
7
+ //
8
+ // guard-async -- a verb documented as Promise-returning refuses by REJECTING, never by throwing.
9
+ //
10
+ // The failure is invisible at the call site, which is what makes it worth a choke point. An operator
11
+ // reads `-> Promise<...>` in the reference and writes the documented shape:
12
+ //
13
+ // pki.acme.newOrder(opts).catch(handleIt);
14
+ //
15
+ // A validation that runs BEFORE the promise is created throws straight past that `.catch`, so a
16
+ // misspelled option or a malformed input becomes an uncaught exception in code that already handles
17
+ // errors -- and nothing in the shape of the call tells the caller which verbs do that. Eleven verbs
18
+ // across five modules had it (pki.cms.verify / sign / countersign, pki.ocsp.sign, pki.tsp.sign and
19
+ // six pki.acme verbs), each having grown the same way: a cheap synchronous check added at the top of
20
+ // a function that returns a promise further down.
21
+ //
22
+ // What this does NOT change is WHEN the work happens. The body still runs synchronously, because
23
+ // several of these verbs must resolve a caller's mutable options object before any turn passes --
24
+ // reading a key, a nonce, or a request's bytes a turn later is a different value than the one that
25
+ // was checked. Only the exit changes: a fault leaves as a rejection instead of a throw.
26
+ //
27
+ // @enforced-by behavioral -- the rule has no rename-proof code shape (it is the ABSENCE of a wrapper
28
+ // around a synchronous prefix, which no lexical pattern can see). The guard is the derived test
29
+ // test/layer-0-primitives/promise-contract.test.js, which reads every `-> Promise<` @signature out
30
+ // of lib/ and calls each verb to prove the refusal arrives as a rejection -- so a new verb is in
31
+ // scope the day it is documented, and no list here can go stale.
32
+ function deferred(body) {
33
+ try { return Promise.resolve(body()); }
34
+ catch (e) { return Promise.reject(e); }
35
+ }
36
+
37
+ module.exports = { deferred: deferred };
@@ -25,9 +25,33 @@
25
25
  // domain/reason; `label` the field phrase; `maxBytes` an optional decoded-size
26
26
  // cap (null/undefined = uncapped, for a PEM cert body already bounded upstream).
27
27
 
28
- var B64URL_ALPHABET = /^[A-Za-z0-9_-]*$/;
29
- var B64_ALPHABET = /^[A-Za-z0-9+/]*={0,2}$/;
30
- var HEX_ALPHABET = /^[0-9A-Fa-f]*$/;
28
+ // The alphabets are TABLES, walked one character at a time, rather than regular
29
+ // expressions. A guard runs on the most hostile input the toolkit sees, and a
30
+ // pattern engine's cost on a non-matching string is a property of the pattern
31
+ // rather than of the length -- the one thing a bound cannot be placed on from the
32
+ // outside. A table lookup is one array index per character and its cost is the
33
+ // length, which the caller already caps. It is also the more honest statement of
34
+ // the rule: the set of permitted characters IS the rule, written out.
35
+ function _alphabet(chars) {
36
+ var t = new Uint8Array(128);
37
+ for (var i = 0; i < chars.length; i++) t[chars.charCodeAt(i)] = 1;
38
+ return t;
39
+ }
40
+ var UPPER = "ABCDEFGHIJKLMNOPQRSTUVWXYZ", LOWER = "abcdefghijklmnopqrstuvwxyz", DIGITS = "0123456789";
41
+ var B64URL_ALPHABET = _alphabet(UPPER + LOWER + DIGITS + "-_");
42
+ var B64_ALPHABET = _alphabet(UPPER + LOWER + DIGITS + "+/");
43
+ var HEX_ALPHABET = _alphabet(DIGITS + "abcdef" + "ABCDEF");
44
+
45
+ // Every character of `text` is in `table`. A code point outside Latin-1's low half
46
+ // (including every astral half) is outside all three alphabets, so the table bound
47
+ // is the reject rather than an index that reads undefined.
48
+ function _inAlphabet(text, table) {
49
+ for (var i = 0; i < text.length; i++) {
50
+ var c = text.charCodeAt(i);
51
+ if (c > 127 || table[c] !== 1) return false;
52
+ }
53
+ return true;
54
+ }
31
55
 
32
56
  // Reject before Buffer.from allocates: a base64 text of N chars decodes to at
33
57
  // most floor(N*3/4) bytes, a hex text to N/2.
@@ -49,7 +73,7 @@ function _capBefore(nChars, perByteChars, maxBytes, E, code, label) {
49
73
  // @enforced-by base64-decode-not-via-guard
50
74
  function base64url(text, maxBytes, E, code, label) {
51
75
  if (typeof text !== "string") throw E(code, label + " must be a string");
52
- if (!B64URL_ALPHABET.test(text)) throw E(code, label + " is not base64url (padding or a non-alphabet character)");
76
+ if (!_inAlphabet(text, B64URL_ALPHABET)) throw E(code, label + " is not base64url (padding or a non-alphabet character)");
53
77
  if (text.length % 4 === 1) throw E(code, label + " has an impossible base64url length");
54
78
  _capBefore(text.length * 3, 4, maxBytes, E, code, label);
55
79
  var buf = Buffer.from(text, "base64url");
@@ -62,7 +86,12 @@ function base64url(text, maxBytes, E, code, label) {
62
86
  // @enforced-by base64-decode-not-via-guard
63
87
  function base64(text, maxBytes, E, code, label) {
64
88
  if (typeof text !== "string") throw E(code, label + " must be a string");
65
- if (!B64_ALPHABET.test(text)) throw E(code, label + " is not base64 (a non-alphabet character)");
89
+ // Padding is positional, not alphabetic: at most two "=" and only at the end, so
90
+ // the body is measured first and the alphabet applies to what remains. An "="
91
+ // anywhere else leaves a non-alphabet character in the body and rejects there.
92
+ var pad = 0;
93
+ while (pad < 2 && text.length > pad && text.charCodeAt(text.length - 1 - pad) === 0x3d) pad++;
94
+ if (!_inAlphabet(text.slice(0, text.length - pad), B64_ALPHABET)) throw E(code, label + " is not base64 (a non-alphabet character)");
66
95
  if (text.length % 4 !== 0) throw E(code, label + " must be whole 4-character base64 groups (RFC 4648 sec. 3.5)");
67
96
  _capBefore(text.length * 3, 4, maxBytes, E, code, label);
68
97
  var buf = Buffer.from(text, "base64");
@@ -79,7 +108,7 @@ function base64(text, maxBytes, E, code, label) {
79
108
  // (a non-canonical / odd-length / non-hex #hex attribute value rejects) guard it.
80
109
  function hex(text, maxBytes, E, code, label) {
81
110
  if (typeof text !== "string") throw E(code, label + " must be a string");
82
- if (!HEX_ALPHABET.test(text)) throw E(code, label + " is not hexadecimal");
111
+ if (!_inAlphabet(text, HEX_ALPHABET)) throw E(code, label + " is not hexadecimal");
83
112
  if (text.length % 2 !== 0) throw E(code, label + " must have an even number of hex digits");
84
113
  _capBefore(text.length, 2, maxBytes, E, code, label);
85
114
  var buf = Buffer.from(text, "hex");
@@ -21,6 +21,32 @@
21
21
  // silent false-reject). Every string-form identifier check routes through here so
22
22
  // the string and DER forms cannot diverge.
23
23
 
24
+ // The dotted-decimal grammar, walked rather than matched. `(0|[1-9]\d*)(\.(0|[1-9]\d*))+`
25
+ // nests a quantified group inside a quantified group with an alternation in each,
26
+ // which is the shape whose cost on a REJECTING string is a property of the pattern
27
+ // rather than of the length -- and this guard's whole job is to be handed strings
28
+ // that reject. Walking the string is one pass, one comparison per character, and it
29
+ // states the two rules plainly: an arc is one or more digits, and an arc longer than
30
+ // one digit does not start with zero (the leading-zero form round-trips to a
31
+ // DIFFERENT OID, which is the divergence this guard exists to stop).
32
+ function _isDottedDecimal(str) {
33
+ if (str.length === 0) return false;
34
+ var arcs = 0, digits = 0, leadingZero = false;
35
+ for (var i = 0; i < str.length; i++) {
36
+ var c = str.charCodeAt(i);
37
+ if (c === 0x2e) { // "."
38
+ if (digits === 0 || leadingZero) return false; // empty arc, or "01"
39
+ arcs++; digits = 0; leadingZero = false;
40
+ continue;
41
+ }
42
+ if (c < 0x30 || c > 0x39) return false; // not a digit
43
+ if (digits === 1 && str.charCodeAt(i - 1) === 0x30) leadingZero = true;
44
+ digits++;
45
+ }
46
+ if (digits === 0 || leadingZero) return false; // trailing "." or a final "01"
47
+ return arcs >= 1; // two or more arcs
48
+ }
49
+
24
50
  // assertCanonicalOid(str, E, code, label, boundsCode) -> str | throws
25
51
  // A canonical dotted-decimal object identifier string: two or more arcs, each a
26
52
  // non-negative decimal integer with no leading zero (the SYNTAX), and -- unless
@@ -40,7 +66,7 @@
40
66
  // reject a non-canonical OID) driving the composing consumers are the guard.
41
67
  function assertCanonicalOid(str, E, code, label, boundsCode) {
42
68
  var who = label || "OID";
43
- if (typeof str !== "string" || !/^(0|[1-9]\d*)(\.(0|[1-9]\d*))+$/.test(str)) {
69
+ if (typeof str !== "string" || !_isDottedDecimal(str)) {
44
70
  throw E(code, who + " must be a canonical dotted-decimal OID string of two or more arcs with no leading-zero component");
45
71
  }
46
72
  if (boundsCode === null) return str;
package/lib/guard-json.js CHANGED
@@ -25,6 +25,14 @@
25
25
  var text = require("./guard-text");
26
26
  var limits = require("./guard-limits");
27
27
 
28
+ // One hex digit's value, or -1. Written out because the three ranges ARE the rule.
29
+ function _hexVal(c) {
30
+ if (c >= 0x30 && c <= 0x39) return c - 0x30; // 0-9
31
+ if (c >= 0x61 && c <= 0x66) return c - 0x61 + 10; // a-f
32
+ if (c >= 0x41 && c <= 0x46) return c - 0x41 + 10; // A-F
33
+ return -1;
34
+ }
35
+
28
36
  // parse(input, ErrorClass, spec) -> value. `input` is a Buffer or a string.
29
37
  // spec = { maxBytes, maxDepth, badJson, tooDeep, duplicateMember, tooLarge,
30
38
  // badInput, label } -- the caller's caps + frozen domain/reason codes.
@@ -118,9 +126,17 @@ function parse(input, ErrorClass, spec) {
118
126
  else if (e === "r") s += "\r";
119
127
  else if (e === "t") s += "\t";
120
128
  else if (e === "u") {
121
- var hex = str.substr(i, 4);
122
- if (!/^[0-9a-fA-F]{4}$/.test(hex)) fail("bad \\u escape");
123
- s += String.fromCharCode(parseInt(hex, 16));
129
+ // Four hex digits, read as digits rather than matched as a pattern: the
130
+ // scanner is already walking this string one character at a time, and a
131
+ // parser handed hostile input should not hand any of it to a second engine.
132
+ var cp = 0;
133
+ if (i + 4 > n) fail("bad \\u escape");
134
+ for (var h = 0; h < 4; h++) {
135
+ var d = _hexVal(str.charCodeAt(i + h));
136
+ if (d < 0) fail("bad \\u escape");
137
+ cp = (cp << 4) | d;
138
+ }
139
+ s += String.fromCharCode(cp);
124
140
  i += 4;
125
141
  } else fail("bad escape");
126
142
  } else if (c.charCodeAt(0) < 0x20) {
@@ -128,15 +144,35 @@ function parse(input, ErrorClass, spec) {
128
144
  } else s += c;
129
145
  }
130
146
  }
147
+ // RFC 8259 sec. 6, enforced BY the walk rather than by re-matching the token
148
+ // afterwards. The scan already knows where each part starts and ends, so the
149
+ // grammar's three rules -- an integer part that is "0" or has no leading zero, a
150
+ // fraction with at least one digit, an exponent with at least one digit -- are
151
+ // checked as it goes. Re-matching what the scanner just read meant maintaining
152
+ // the same grammar twice, in two notations, and the pattern was the copy whose
153
+ // cost on a rejecting token could not be bounded from outside.
131
154
  function number() {
132
155
  var start = i;
133
156
  if (str[i] === "-") i++;
157
+ var intStart = i;
134
158
  while (i < n && str[i] >= "0" && str[i] <= "9") i++;
135
- if (str[i] === ".") { i++; while (i < n && str[i] >= "0" && str[i] <= "9") i++; }
136
- if (str[i] === "e" || str[i] === "E") { i++; if (str[i] === "+" || str[i] === "-") i++; while (i < n && str[i] >= "0" && str[i] <= "9") i++; }
137
- var tok = str.slice(start, i);
138
- if (!/^-?(0|[1-9][0-9]*)(\.[0-9]+)?([eE][+-]?[0-9]+)?$/.test(tok)) fail("malformed number");
139
- var v = Number(tok);
159
+ var intLen = i - intStart;
160
+ if (intLen === 0) fail("malformed number");
161
+ if (intLen > 1 && str[intStart] === "0") fail("malformed number"); // no leading zero
162
+ if (str[i] === ".") {
163
+ i++;
164
+ var fracStart = i;
165
+ while (i < n && str[i] >= "0" && str[i] <= "9") i++;
166
+ if (i === fracStart) fail("malformed number"); // "1." has no fraction
167
+ }
168
+ if (str[i] === "e" || str[i] === "E") {
169
+ i++;
170
+ if (str[i] === "+" || str[i] === "-") i++;
171
+ var expStart = i;
172
+ while (i < n && str[i] >= "0" && str[i] <= "9") i++;
173
+ if (i === expStart) fail("malformed number"); // "1e" has no exponent
174
+ }
175
+ var v = Number(str.slice(start, i));
140
176
  if (!isFinite(v)) fail("bad number");
141
177
  return v;
142
178
  }
package/lib/guard-name.js CHANGED
@@ -65,10 +65,25 @@ function assertPrintableIa5(buf, E, code, label) {
65
65
  // matches OpenSSL's X509_NAME_cmp, so a chain OpenSSL accepts is not rejected. This
66
66
  // canonicalization is the shape the guard-shape-reinlined detector keys on
67
67
  // (declared on dnEqual): a boundary hand-rolling it is re-implementing DN identity.
68
+ // The collapse is a single walk rather than a pattern replace. This runs on every
69
+ // attribute value of every name the toolkit compares -- a certificate an attacker
70
+ // supplies included -- and one pass with a running "was the last character a space"
71
+ // flag costs exactly the length. It also spells out which characters count as
72
+ // whitespace: RFC 5280 sec. 7.1 defers to X.520's caseIgnoreMatch, whose SPACE is
73
+ // the ASCII space, and a pattern's \s silently also folds VT, FF, NBSP and every
74
+ // Unicode space separator, which would equate two names X.520 keeps distinct.
75
+ function _isSpace(c) { return c === 0x20 || c === 0x09 || c === 0x0a || c === 0x0d; }
68
76
  function _canonAttrValue(v, E, code, label) {
69
77
  if (typeof v !== "string") return v;
70
78
  assertNoControlBytes(v, E, code, label);
71
- return v.trim().replace(/\s+/g, " ").toLowerCase();
79
+ var out = "", lastWasSpace = true; // true so leading whitespace is dropped
80
+ for (var i = 0; i < v.length; i++) {
81
+ if (_isSpace(v.charCodeAt(i))) { lastWasSpace = true; continue; }
82
+ if (lastWasSpace && out.length) out += " ";
83
+ lastWasSpace = false;
84
+ out += v.charAt(i);
85
+ }
86
+ return out.toLowerCase();
72
87
  }
73
88
  // rdnEqual(a, b, E, code, label) -> boolean. Canonical comparison of a single
74
89
  // RelativeDistinguishedName (an unordered SET of type/value pairs, compared as a
@@ -151,17 +166,25 @@ function escapeControlBytes(str) {
151
166
  // with the hexstring form, so the report misstates a subject/issuer name (CWE-116).
152
167
  // The one place a DN attribute value is made display-safe; pki.schema.pkix's DN
153
168
  // rendering composes it, and pki.inspect reuses that parser output (name.dn).
154
- // @enforced-by behavioral -- the RFC 4514 separator class carries a quote inside a
155
- // regex literal, which the codebase-patterns literal-stripper mis-tokenizes, so no
156
- // rename-proof shape is detectable; the guard-name RED vectors + the schema-pkix DN
169
+ var DN_SPECIAL = { 0x2c: 1, 0x2b: 1, 0x22: 1, 0x5c: 1, 0x3c: 1, 0x3e: 1, 0x3b: 1 }; // , + " \ < > ;
170
+ // RFC 4514 sec. 2.4's special set is the TABLE above and the walk is a single pass.
171
+ // The separator escape used to be a pattern replace feeding a second loop, which
172
+ // meant two passes over an attacker-supplied value and a character class holding a
173
+ // quote and a backslash inside a regex literal -- the form most easily misread by a
174
+ // human and, as it happened, by the codebase-patterns literal-stripper.
175
+ //
176
+ // @enforced-by behavioral -- the escaping has no rename-proof code shape distinct
177
+ // from ordinary string building; the guard-name RED vectors + the schema-pkix DN
157
178
  // round-trip vectors (a comma / plus / leading '#' renders backslash-escaped) are the guard.
158
179
  function escapeDnValue(v) {
159
- var s = String(v).replace(/([,+"\\<>;])/g, "\\$1"), out = "";
160
- // RFC 4514 sec. 2.4: a NUL / control octet -> '\' + two hex digits, so an embedded
161
- // CR / LF / NUL in a decoded DN value can never forge a report line when displayed.
180
+ var s = String(v), out = "";
181
+ // A NUL / control octet becomes '\' + two hex digits, so an embedded CR / LF / NUL
182
+ // in a decoded DN value can never forge a report line when displayed.
162
183
  for (var i = 0; i < s.length; i++) {
163
184
  var c = s.charCodeAt(i);
164
- out += (c < 0x20 || c === 0x7f) ? "\\" + (c < 16 ? "0" : "") + c.toString(16).toUpperCase() : s.charAt(i);
185
+ if (c < 0x20 || c === 0x7f) out += "\\" + (c < 16 ? "0" : "") + c.toString(16).toUpperCase();
186
+ else if (DN_SPECIAL[c] === 1) out += "\\" + s.charAt(i);
187
+ else out += s.charAt(i);
165
188
  }
166
189
  if (out.length && out.charAt(out.length - 1) === " ") out = out.slice(0, -1) + "\\ ";
167
190
  if (out.charAt(0) === "#" || out.charAt(0) === " ") out = "\\" + out;