@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/CHANGELOG.md +42 -0
- package/MIGRATING.md +72 -0
- package/lib/acme.js +47 -19
- 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-sign.js +11 -0
- package/lib/cms-verify.js +22 -8
- package/lib/crl-sign.js +19 -7
- package/lib/est.js +77 -1
- package/lib/guard-all.js +8 -0
- package/lib/guard-async.js +37 -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 +482 -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 +44 -8
- package/lib/path-validate.js +88 -44
- package/lib/pbes2.js +11 -1
- package/lib/pkcs12-build.js +105 -30
- package/lib/pki-build.js +12 -1
- package/lib/schema-cms.js +17 -2
- 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 +14 -0
- package/lib/schema-x509.js +14 -1
- package/lib/sign-scheme.js +22 -5
- package/lib/sigstore.js +10 -0
- package/lib/smime.js +47 -0
- package/lib/trust.js +121 -10
- package/lib/tsp-sign.js +112 -19
- package/lib/validator-cose.js +86 -1
- package/lib/validator-tpm.js +8 -3
- package/lib/webauthn-mds.js +10 -18
- 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/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
|
-
//
|
|
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
|
-
//
|
|
667
|
-
//
|
|
668
|
-
//
|
|
669
|
-
//
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
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
|
-
|
|
379
|
-
|
|
380
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 () {
|
|
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 };
|
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
|
}
|
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
|
-
|
|
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
|
-
|
|
155
|
-
//
|
|
156
|
-
//
|
|
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)
|
|
160
|
-
//
|
|
161
|
-
//
|
|
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
|
-
|
|
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;
|