@blamejs/pki 0.5.3 → 0.5.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/crl-sign.js CHANGED
@@ -30,6 +30,7 @@ var signScheme = require("./sign-scheme");
30
30
  var guard = require("./guard-all");
31
31
  var frameworkError = require("./framework-error");
32
32
  var pkix = require("./schema-pkix");
33
+ var schema = require("./schema-engine");
33
34
  var pkiBuild = require("./pki-build");
34
35
  var constants = require("./constants");
35
36
  require("./path-validate"); // side-effect: path-validate injects its signature engine into crl-verify at load
@@ -183,17 +184,22 @@ function _idpValue(idp) {
183
184
  // Validate a pre-encoded IssuingDistributionPoint value against the same RFC 5280 sec. 5.2.5 profile the
184
185
  // object form enforces: MUST NOT be empty; at most one of onlyContainsUserCerts [1] / onlyContainsCACerts [2]
185
186
  // TRUE; onlyContainsAttributeCerts [5] MUST be FALSE; indirectCRL [4] deferred (crlChecker skips indirect CRLs).
187
+ // Read through the shared sec. 5.2.5 grammar, not a walk over the raw children: each scope flag is
188
+ // an IMPLICIT BOOLEAN whose meaning is fixed by the encoding rules, and a caller-supplied encoding
189
+ // is exactly where a flag that is not one content octet of 0x00 or 0xFF arrives. Deciding TRUE from
190
+ // a content byte would emit a CRL whose scope this toolkit and the relying party read differently.
191
+ var _PRE_ENCODED_IDP_SCHEMA = pkix.issuingDistributionPoint("crl/bad-idp");
186
192
  function _validatePreEncodedIdp(inner) {
187
193
  if (!inner.children || !inner.children.length) throw _err("crl/bad-idp", "pre-encoded issuingDistributionPoint MUST NOT be empty (RFC 5280 sec. 5.2.5)");
188
- var scopeTrue = 0;
189
- inner.children.forEach(function (c) {
190
- if (c.tagClass !== "context") throw _err("crl/bad-idp", "pre-encoded issuingDistributionPoint members must be context-tagged (RFC 5280 sec. 5.2.5)");
191
- var isTrue = c.content != null && c.content.length > 0 && c.content[c.content.length - 1] !== 0x00; // a present DEFAULT-FALSE boolean is TRUE
192
- if (c.tagNumber === 4) throw _err("crl/bad-idp", _INDIRECT_IDP_DEFERRED); // indirectCRL
193
- if (c.tagNumber === 5 && isTrue) throw _err("crl/bad-idp", "onlyContainsAttributeCerts=TRUE is not permitted for a conforming CRL issuer (RFC 5280 sec. 5.2.5)");
194
- if ((c.tagNumber === 1 || c.tagNumber === 2) && isTrue) scopeTrue++;
194
+ var f = schema.walk(_PRE_ENCODED_IDP_SCHEMA, inner, NS).fields;
195
+ // A present DEFAULT-FALSE flag encodes a default DER omits (X.690 sec. 11.5), so it is rejected
196
+ // whichever value it carries rather than counted as the FALSE it spells.
197
+ ["onlyContainsUserCerts", "onlyContainsCACerts", "onlyContainsAttributeCerts"].forEach(function (name) {
198
+ if (f[name].present && f[name].value !== true) throw _err("crl/bad-idp", "pre-encoded issuingDistributionPoint " + name + " encodes its DEFAULT FALSE, which DER omits (X.690 sec. 11.5)");
195
199
  });
196
- if (scopeTrue > 1) throw _err("crl/bad-idp", "at most one of onlyContainsUserCerts / onlyContainsCACerts may be TRUE (RFC 5280 sec. 5.2.5)");
200
+ if (f.indirectCRL.present) throw _err("crl/bad-idp", _INDIRECT_IDP_DEFERRED);
201
+ if (f.onlyContainsAttributeCerts.present) throw _err("crl/bad-idp", "onlyContainsAttributeCerts=TRUE is not permitted for a conforming CRL issuer (RFC 5280 sec. 5.2.5)");
202
+ if (f.onlyContainsUserCerts.present && f.onlyContainsCACerts.present) throw _err("crl/bad-idp", "at most one of onlyContainsUserCerts / onlyContainsCACerts may be TRUE (RFC 5280 sec. 5.2.5)");
197
203
  }
198
204
 
199
205
  // freshestCRL / CRLDistributionPoints (sec. 5.2.6): SEQUENCE OF DistributionPoint carrying only
@@ -369,9 +375,10 @@ function _buildRevoked(entryList, isDelta) {
369
375
  // ---- the primitives --------------------------------------------------------
370
376
 
371
377
  function _parseIssuerCert(cert) {
372
- var parsed = (Buffer.isBuffer(cert) || typeof cert === "string") ? x509Schema.parse(cert) : cert;
373
- if (!parsed || !parsed.tbsBytes || !parsed.subjectPublicKeyInfo) throw _err("crl/bad-input", "issuer.cert must be a certificate DER/PEM or a parsed certificate");
374
- 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");
375
382
  }
376
383
 
377
384
  // RFC 5280 sec. 4.2.1.3 -- a certificate whose key signs CRLs asserts the cRLSign keyUsage bit. When the
@@ -502,21 +509,59 @@ function _sign(spec, issuer, opts) {
502
509
  */
503
510
  function sign(spec, issuer, opts) { return Promise.resolve().then(function () { return _sign(spec, issuer, opts); }); }
504
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.
505
519
  function _coerceCrl(crl) {
506
- if (Buffer.isBuffer(crl) || typeof crl === "string") return crlSchema.parse(crl);
507
- if (crl && typeof crl === "object" && crl.tbsBytes && crl.signatureValue && crl.signatureAlgorithm) return crl;
508
- 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");
509
521
  }
510
522
 
511
- function _resolveIssuerSpki(issuer) {
523
+ // The issuer's key AND, when the caller supplied a certificate rather than a bare key, the
524
+ // certificate itself -- because a CRL signature that verifies says only that SOME key signed these
525
+ // bytes. Whether that key was allowed to sign a CRL, and whether it belongs to the issuer this CRL
526
+ // names, are separate questions that only a certificate can answer.
527
+ function _resolveIssuer(issuer) {
512
528
  if (issuer == null) throw _err("crl/bad-input", "an issuer is required to verify a CRL");
513
- if (Buffer.isBuffer(issuer)) { _assertValidSpki(issuer, "issuer SPKI"); return issuer; }
514
- if (issuer.cert != null) return _parseIssuerCert(issuer.cert).subjectPublicKeyInfo.bytes;
515
- if (issuer.publicKey != null) { var spki = _reqDer(issuer.publicKey, "issuer.publicKey"); _assertValidSpki(spki, "issuer.publicKey"); return spki; }
516
- if (issuer.subjectPublicKeyInfo && issuer.subjectPublicKeyInfo.bytes) return issuer.subjectPublicKeyInfo.bytes; // a parsed certificate
529
+ if (Buffer.isBuffer(issuer)) { _assertValidSpki(issuer, "issuer SPKI"); return { spki: issuer, cert: null }; }
530
+ if (issuer.cert != null) { var ic = _parseIssuerCert(issuer.cert); return { spki: ic.subjectPublicKeyInfo.bytes, cert: ic }; }
531
+ if (issuer.publicKey != null) { var spki = _reqDer(issuer.publicKey, "issuer.publicKey"); _assertValidSpki(spki, "issuer.publicKey"); return { spki: spki, cert: null }; }
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
+ }
517
539
  throw _err("crl/bad-input", "issuer must be { cert }, { publicKey } (SPKI DER), or a raw SPKI Buffer");
518
540
  }
519
541
 
542
+ // RFC 5280 sec. 5.1.1.2 / 6.3.3: the certificate that signed a CRL must BE the CRL's issuer, and if
543
+ // it carries a keyUsage extension that extension must assert cRLSign. Neither is implied by the
544
+ // signature verifying: any key can sign any bytes, and a certificate restricted to digitalSignature
545
+ // chains to its root perfectly well while having no authority to revoke anything. The producing
546
+ // side of this same file already refuses to SIGN a CRL with a certificate lacking cRLSign; the
547
+ // verifying side asked neither question, so a CRL minted under an end-entity certificate of the
548
+ // same CA verified as that CA's CRL.
549
+ // Returns false rather than throwing, because both answers are statements about the CRL, not about
550
+ // the caller's input: "this CRL was not validly issued by the certificate you named". That keeps
551
+ // the verb's boolean contract, and keeps working the ordinary pattern of trying each candidate
552
+ // issuer to find which one issued a CRL. A MALFORMED keyUsage is different -- it is a defect in the
553
+ // caller's own certificate rather than a verdict about the CRL -- and throws.
554
+ function _issuerMaySign(parsed, cert) {
555
+ if (!guard.name.dnEqual(parsed.issuer.rdns, cert.subject.rdns, _err, "crl/bad-issuer", "the CRL issuer")) return false;
556
+ // Through the shared reader, not a local BIT STRING read: keyUsage is a NamedBitList, so DER drops
557
+ // its trailing zero bits and sec. 4.2.1.3 requires at least one bit set, and a boundary reading
558
+ // the bits itself applies neither -- authorizing here a certificate the signing side and the path
559
+ // validator both refuse as malformed.
560
+ var ku = pkix.keyUsageOf(NS, cert, _err, "crl/bad-issuer", "issuer certificate");
561
+ if (!ku) return true; // absent keyUsage places no restriction (sec. 4.2.1.3)
562
+ return ku.cRLSign === true;
563
+ }
564
+
520
565
  /**
521
566
  * @primitive pki.crl.verify
522
567
  * @signature pki.crl.verify(crl, issuer) -> Promise<boolean>
@@ -531,8 +576,16 @@ function _resolveIssuerSpki(issuer) {
531
576
  * (SPKI DER), or a raw SPKI `Buffer`. Verification composes the one path-validation signature engine
532
577
  * `pki.path.crlChecker` uses -- the same algorithm-confusion (RFC 9814 sec. 4 key-OID == sig-OID) and
533
578
  * EdDSA low-order-point gates -- so there is no second, weaker CRL verifier. It fails closed to `false` on
534
- * any resolution, import, or verification fault; malformed input throws a typed `CrlError`. This checks the
535
- * signature only -- issuer authorization, currency, and distribution-point scope are `pki.path.crlChecker`.
579
+ * any resolution, import, or verification fault; malformed input throws a typed `CrlError`.
580
+ *
581
+ * Given a CERTIFICATE rather than a bare key, it also asks what only a certificate can answer: that
582
+ * the certificate is the issuer this CRL names, and that its keyUsage -- when it carries one -- asserts
583
+ * `cRLSign` (RFC 5280 sec. 4.2.1.3, the same rule this module's signing side already enforces). Either
584
+ * failing is `false`: a statement about the CRL, not about the caller's input, so trying each candidate
585
+ * issuer in turn still works. A signature verifying says only that SOME key signed these bytes; without
586
+ * those two questions a CRL minted under an end-entity certificate of the same CA verified as that CA's.
587
+ * Handed a bare SPKI there is no certificate to carry either restriction, and the signature is all that
588
+ * is checked. Currency and distribution-point scope remain `pki.path.crlChecker`.
536
589
  *
537
590
  * @example
538
591
  * var pair = await pki.key.generate("Ed25519");
@@ -548,8 +601,12 @@ function _resolveIssuerSpki(issuer) {
548
601
  function verify(crl, issuer) { return Promise.resolve().then(function () { return _verify(crl, issuer); }); }
549
602
  function _verify(crl, issuer) {
550
603
  var parsed = _coerceCrl(crl);
551
- var spki = _resolveIssuerSpki(issuer);
552
- return crlVerify.verifyCrlSignature(parsed, spki);
604
+ var resolved = _resolveIssuer(issuer);
605
+ // Asked BEFORE the signature: a certificate that may not sign CRLs, or that is not this CRL's
606
+ // issuer, is a caller-configuration answer that does not depend on the math, and spending a
607
+ // verification on it would let the cheaper question be answered by the more expensive one.
608
+ if (resolved.cert && !_issuerMaySign(parsed, resolved.cert)) return false;
609
+ return crlVerify.verifyCrlSignature(parsed, resolved.spki);
553
610
  }
554
611
 
555
612
  function _serialHexOf(serial) {
@@ -578,6 +635,22 @@ function _serialHexOf(serial) {
578
635
  * revocationDate, crlEntryExtensions }`) or `null` when the serial is not listed. A structural lookup only --
579
636
  * it does NOT verify the CRL signature or its currency; call `pki.crl.verify` / `pki.path.crlChecker` for that.
580
637
  *
638
+ * It does check SCOPE first, because a serial number means something only within the set of
639
+ * certificates a CRL speaks for, and this verb is given a serial and nothing else -- so a CRL that
640
+ * speaks for part of its issuer's certificates is refused rather than answered from:
641
+ *
642
+ * - A DELTA CRL lists changes since a base, so a serial in it may be there to say the certificate
643
+ * was RELEASED; read alone, the entry meaning "no longer revoked" reads as "revoked"
644
+ * (`crl/delta-not-authoritative`). Merge it with its base through `pki.path.crlChecker`.
645
+ * - An INDIRECT CRL carries entries for other issuers, whose serials are unrelated to yours
646
+ * (`crl/indirect-not-supported`) -- as does any CRL carrying `certificateIssuer` on an entry
647
+ * while not declaring itself indirect, a contradiction about whose certificates it lists.
648
+ * - Any other `issuingDistributionPoint` narrows the CRL to one distribution point, one kind of
649
+ * certificate, or a subset of revocation reasons (`crl/scope-not-authoritative`). Which part
650
+ * applies is decided against fields of the CERTIFICATE, which this verb never sees, so an absent
651
+ * serial is not an unrevoked certificate. `pki.path.crlChecker` is handed the certificate and
652
+ * performs the RFC 5280 sec. 6.3.3 correspondence.
653
+ *
581
654
  * @example
582
655
  * var pair = await pki.key.generate("Ed25519");
583
656
  * var signerKeyPkcs8 = await pki.key.export(pair.privateKey);
@@ -592,10 +665,83 @@ function _serialHexOf(serial) {
592
665
  function isRevoked(crl, serialNumber) {
593
666
  var parsed = _coerceCrl(crl);
594
667
  var hex = _serialHexOf(serialNumber);
668
+ // SCOPE, before the serial is looked for at all. A serial number is only meaningful within the
669
+ // set of certificates a CRL actually speaks for, and two shapes of CRL speak for a different set
670
+ // than a caller matching on serial alone assumes. pki.path.crlChecker already refuses both; this
671
+ // verb answered from any CRL it was handed, so the toolkit knew the rule and applied it in one
672
+ // consumer -- which is how the standalone verb came to give the opposite answer.
673
+ //
674
+ // A DELTA CRL lists CHANGES since its base, so a serial present in it may be there to say the
675
+ // certificate was RELEASED (removeFromCRL); read alone, the extension that means "no longer
676
+ // revoked" reads as "revoked". The answer is only derivable from the delta MERGED with its base,
677
+ // which pki.path.crlChecker does.
678
+ if (_findExtOid(parsed.crlExtensions, "deltaCRLIndicator")) {
679
+ throw _err("crl/delta-not-authoritative", "this is a delta CRL: it lists changes since a base CRL, so a serial appearing in it may be RELEASED rather than revoked. Merge it with its base -- pki.path.crlChecker does -- rather than reading a revocation status out of the delta alone");
680
+ }
681
+ // An INDIRECT CRL carries entries for OTHER issuers' certificates, identified by a per-entry
682
+ // certificateIssuer attribute. Matching on serial alone attributes another issuer's revocation to
683
+ // this one's certificate of the same serial number -- serials are unique per issuer, not globally.
684
+ var idp = _findExtOid(parsed.crlExtensions, "issuingDistributionPoint");
685
+ if (idp) _assertAuthoritativeScope(idp);
686
+ // certificateIssuer (sec. 5.3.3) names the issuer an entry belongs to, and is meaningful ONLY on
687
+ // an indirect CRL -- which was refused above. One here means the CRL declares itself direct while
688
+ // carrying an entry that says otherwise, and the two readings disagree about whose certificates
689
+ // this list covers. That is a property of the CRL, so it is settled BEFORE the lookup and over
690
+ // EVERY entry: checking only the entry that matches would leave the not-listed answer -- the one
691
+ // that says the certificate is fine -- coming from a list whose own entries dispute what it lists.
595
692
  for (var i = 0; i < parsed.revokedCertificates.length; i++) {
596
- if (parsed.revokedCertificates[i].serialNumberHex === hex) return parsed.revokedCertificates[i];
693
+ if (_findExtOid(parsed.revokedCertificates[i].crlEntryExtensions, "certificateIssuer")) {
694
+ throw _err("crl/indirect-not-supported", "a revoked-certificate entry carries certificateIssuer, which names another issuer, on a CRL that does not declare itself indirect -- the entry and the CRL disagree about whose certificates this list covers, so no revocation status follows from it");
695
+ }
696
+ }
697
+ for (var j = 0; j < parsed.revokedCertificates.length; j++) {
698
+ if (parsed.revokedCertificates[j].serialNumberHex === hex) return parsed.revokedCertificates[j];
597
699
  }
598
700
  return null;
599
701
  }
600
702
 
703
+ // One extension out of a parsed list, by registered OID name. Null-safe: an absent list is no
704
+ // extension rather than a fault, which is what every caller here means by it.
705
+ function _findExtOid(list, name) {
706
+ var want = O(name);
707
+ return (list || []).filter(function (e) { return e.oid === want; })[0] || null;
708
+ }
709
+
710
+ // The issuingDistributionPoint states which certificates a CRL speaks for (sec. 5.2.5), and every
711
+ // form of it narrows that set below what a bare serial lookup assumes. `isRevoked` is handed a
712
+ // serial and nothing else, so it cannot tell whether the certificate behind that serial falls in
713
+ // the scope -- that decision needs the certificate, which is what pki.path.crlChecker has and this
714
+ // verb does not. So the presence of the extension is the refusal, whichever field carries it:
715
+ //
716
+ // indirectCRL entries name their own issuers, and serials are unique per issuer: a match
717
+ // would attribute another issuer's revocation to your certificate.
718
+ // distributionPoint the CRL covers the certificates whose own distribution point corresponds to
719
+ // this one (sec. 6.3.3(b)(2)(i)) -- a byte-exact comparison against a field of
720
+ // the certificate.
721
+ // onlyContainsUserCerts / onlyContainsCACerts / onlyContainsAttributeCerts
722
+ // the CRL covers one KIND of certificate; a serial does not say which kind it
723
+ // names, so an absent entry says nothing about a certificate of another kind.
724
+ // onlySomeReasons the CRL covers some revocation REASONS; a certificate revoked for a reason
725
+ // outside the set is absent from it while being revoked.
726
+ //
727
+ // Written as a throwing assertion rather than a predicate so the malformed case cannot be mistaken
728
+ // for a verdict about the certificate. The extension is read through the shared sec. 5.2.5 grammar
729
+ // rather than by walking its children, because the flags are IMPLICIT BOOLEANs and only the encoding
730
+ // rules say what their bytes mean: a walk that tests a content byte reads an EMPTY [4] as an absent
731
+ // flag and a multi-octet one by whichever byte it indexes, and "absent, so unscoped" is precisely
732
+ // the reading that lets an unreadable scope license an answer.
733
+ var _IDP_SCHEMA = pkix.issuingDistributionPoint("crl/scope-not-authoritative");
734
+ var _INDIRECT_REFUSAL = "this CRL is marked indirect (issuingDistributionPoint indirectCRL): its entries name their own issuers, so a serial alone does not identify a certificate. Reading a revocation status from it by serial would attribute another issuer's revocation to yours";
735
+ var _SCOPED_REFUSAL = "this CRL carries an issuingDistributionPoint, so it speaks for part of its issuer's certificates rather than all of them, and which part is decided against fields of the certificate -- which this verb is not given. A serial absent from it is not a certificate that is unrevoked. Use pki.path.crlChecker, which is handed the certificate and performs the RFC 5280 sec. 6.3.3 scope correspondence";
736
+ function _assertAuthoritativeScope(ext) {
737
+ var m;
738
+ try { m = schema.walk(_IDP_SCHEMA, asn1.decode(ext.value), NS); }
739
+ catch (e) { throw _err("crl/scope-not-authoritative", "the issuingDistributionPoint cannot be read, so this CRL's scope cannot be established", e); }
740
+ var f = m.fields;
741
+ // Named first so the sharper reason wins: indirect is not merely a narrower scope, it is a list
742
+ // whose serials belong to other issuers.
743
+ if (f.indirectCRL.present && f.indirectCRL.value === true) throw _err("crl/indirect-not-supported", _INDIRECT_REFUSAL);
744
+ throw _err("crl/scope-not-authoritative", _SCOPED_REFUSAL);
745
+ }
746
+
601
747
  module.exports = { sign: sign, verify: verify, isRevoked: isRevoked };
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,7 @@ 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");
63
68
 
64
69
  module.exports = {
65
70
  bytes: bytes,
@@ -75,4 +80,5 @@ module.exports = {
75
80
  header: header,
76
81
  compress: compress,
77
82
  secret: secret,
83
+ parsed: parsed,
78
84
  };
@@ -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
  }