@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/CHANGELOG.md +43 -1
- package/MIGRATING.md +31 -0
- package/README.md +3 -3
- package/lib/attrcert-sign.js +11 -7
- package/lib/cmp-session.js +23 -10
- package/lib/cmp-verify.js +12 -4
- package/lib/cms-decrypt.js +71 -30
- package/lib/cms-encrypt.js +7 -11
- package/lib/crl-sign.js +170 -24
- package/lib/est.js +77 -1
- package/lib/guard-all.js +6 -0
- package/lib/guard-encoding.js +35 -6
- package/lib/guard-identifier.js +27 -1
- package/lib/guard-json.js +44 -8
- package/lib/guard-name.js +31 -8
- package/lib/guard-parsed.js +443 -0
- package/lib/hpke.js +36 -3
- package/lib/jose.js +8 -0
- package/lib/lint.js +20 -4
- package/lib/merkle.js +9 -0
- package/lib/ocsp.js +38 -8
- package/lib/path-validate.js +201 -72
- package/lib/pkcs12-build.js +34 -7
- package/lib/pki-build.js +12 -1
- package/lib/schema-crl.js +7 -1
- package/lib/schema-ocsp.js +6 -1
- package/lib/schema-pkcs12.js +7 -2
- package/lib/schema-pkix.js +76 -0
- package/lib/schema-x509.js +14 -1
- package/lib/sign-scheme.js +22 -5
- package/lib/smime.js +47 -0
- package/lib/trust.js +121 -10
- package/lib/tsp-sign.js +40 -19
- package/lib/validator-cose.js +86 -1
- package/lib/validator-tpm.js +8 -3
- package/lib/webauthn-mds.js +19 -29
- package/lib/webauthn.js +40 -18
- package/lib/x509-sign.js +6 -2
- package/package.json +5 -1
- package/sbom.cdx.json +6 -6
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
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
if (
|
|
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 (
|
|
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
|
-
|
|
373
|
-
|
|
374
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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)
|
|
515
|
-
if (issuer.publicKey != null) { var spki = _reqDer(issuer.publicKey, "issuer.publicKey"); _assertValidSpki(spki, "issuer.publicKey"); return spki; }
|
|
516
|
-
|
|
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`.
|
|
535
|
-
*
|
|
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
|
|
552
|
-
|
|
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].
|
|
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 () {
|
|
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
|
};
|
package/lib/guard-encoding.js
CHANGED
|
@@ -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
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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 (!
|
|
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
|
-
|
|
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 (!
|
|
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");
|
package/lib/guard-identifier.js
CHANGED
|
@@ -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" ||
|
|
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
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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
|
-
|
|
136
|
-
if (
|
|
137
|
-
|
|
138
|
-
if (
|
|
139
|
-
|
|
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
|
}
|