@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/ocsp.js
CHANGED
|
@@ -57,14 +57,31 @@ var OID_EXTENDED_REVOKE = O("ocspExtendedRevoke");
|
|
|
57
57
|
|
|
58
58
|
function _digest(wcHash, buf) { return subtle.digest(wcHash, buf).then(function (h) { return Buffer.from(h); }); }
|
|
59
59
|
function _certOf(arg, what) {
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
60
|
+
var label = what || "a certificate";
|
|
61
|
+
// Re-derived from the bytes its parser read, like every other certificate door. A CertID is an
|
|
62
|
+
// IDENTITY -- the issuer's name and key hashed, plus the serial -- so a certificate that names one
|
|
63
|
+
// identity while carrying another's signed bytes would have this verb ask about, or answer for,
|
|
64
|
+
// a certificate nobody issued.
|
|
65
|
+
return guard.parsed.acceptDerived(arg, "certificate", function (bytes) {
|
|
66
|
+
var der = bytes;
|
|
67
|
+
if (typeof bytes === "string") {
|
|
68
|
+
try { der = x509.pemDecode(bytes); } catch (e) { throw _err("ocsp/bad-input", label + " PEM could not be decoded", e); }
|
|
69
|
+
}
|
|
70
|
+
try { return x509.parse(der); } catch (e) { throw _err("ocsp/bad-input", label + " is not a well-formed X.509 certificate", e); }
|
|
71
|
+
}, _err, "ocsp/bad-input", label);
|
|
72
|
+
}
|
|
73
|
+
// The response, always parsed from the bytes the caller handed over. See verify's own comment for
|
|
74
|
+
// why an object cannot be accepted here: the three parts of a signature check would come from three
|
|
75
|
+
// independently-chosen properties. The claim is detected on any of the parsed-response fields, so a
|
|
76
|
+
// caller who passes one is told what happened rather than getting a byte-parse fault about its type.
|
|
77
|
+
var _RESPONSE_CLAIM = ["responseStatus", "basicResponse", "tbsResponseDataBytes"];
|
|
78
|
+
function _responseFromBytes(response) {
|
|
79
|
+
return guard.parsed.fromTrustedSource(response, "ocspResponse", _RESPONSE_CLAIM, function (bytes) {
|
|
80
|
+
return ocspSchema.parseResponse(_toDer(bytes, "the OCSP response"));
|
|
81
|
+
}, _err, "ocsp/bad-input",
|
|
82
|
+
"the OCSP response must be its DER bytes, a PEM string, or an unmodified pki.schema.ocsp.parseResponse result: the signature, the algorithm that verifies it and the bytes it covers are separate properties of a parsed object, so a REBUILT response (Object.assign, spread, a JSON round-trip) could have the three describe different responses and is refused");
|
|
67
83
|
}
|
|
84
|
+
|
|
68
85
|
function _toDer(input, what) {
|
|
69
86
|
if (Buffer.isBuffer(input)) return input;
|
|
70
87
|
if (input instanceof Uint8Array) return Buffer.from(input);
|
|
@@ -239,7 +256,13 @@ function _normCertDer(cert, what) {
|
|
|
239
256
|
* { responderID: "byName", responses: [{ cert: leafDer, issuer: caDer, status: "good" }] },
|
|
240
257
|
* { cert: responderCertDer, key: responderPkcs8 });
|
|
241
258
|
*/
|
|
259
|
+
// Documented `-> Promise`, so a fault leaves as a REJECTION (guard-async); the checks stay
|
|
260
|
+
// synchronous because they read the responder's mutable cert and key.
|
|
242
261
|
function sign(responseData, responder, opts) {
|
|
262
|
+
return guard.async.deferred(function () { return _sign(responseData, responder, opts); });
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function _sign(responseData, responder, opts) {
|
|
243
266
|
opts = opts || {};
|
|
244
267
|
responseData = responseData || {};
|
|
245
268
|
if (!responder || responder.cert == null || responder.key == null) throw _err("ocsp/bad-input", "a responder must be { cert, key }");
|
|
@@ -456,13 +479,26 @@ function verify(response, opts) {
|
|
|
456
479
|
if (opts.cert == null || opts.issuer == null) return Promise.reject(_err("ocsp/bad-input", "verify requires opts.cert and opts.issuer"));
|
|
457
480
|
var parsed, cert, issuerCert, time;
|
|
458
481
|
try {
|
|
459
|
-
|
|
482
|
+
// The response is parsed from BYTES, always. A claimed-parsed response carries the signature,
|
|
483
|
+
// the algorithm that verifies it and the byte range it covers as three independent properties,
|
|
484
|
+
// so an object could pair one structure's tbsResponseDataBytes with another's signature -- and a
|
|
485
|
+
// signature the issuing CA made over a certificate it issued would verify as a ResponseData
|
|
486
|
+
// signature, returning status "good" for a certificate the responder never spoke about.
|
|
487
|
+
// Parsing here binds all three to one byte string. pki.schema.ocsp.parseResponse remains the
|
|
488
|
+
// parse-only route for a caller who wants the structure without a verdict.
|
|
489
|
+
parsed = _responseFromBytes(response);
|
|
460
490
|
cert = _certOf(opts.cert, "the target certificate");
|
|
461
491
|
issuerCert = _certOf(opts.issuer, "the issuer certificate");
|
|
462
492
|
// The time drives the currency + responder-cert validity windows; an invalid Date fails closed
|
|
463
493
|
// via _asDate (a NaN compares false against every bound, silently disabling both), never defaults.
|
|
464
494
|
time = opts.time == null ? new Date() : _asDate(opts.time);
|
|
465
495
|
} catch (e) { return Promise.reject(e); }
|
|
496
|
+
// The object parsed HERE goes to the verdict verb, not the caller's argument again. It carries the
|
|
497
|
+
// parser's record, so the verdict verb re-derives from the same recorded bytes -- one snapshot,
|
|
498
|
+
// read by both. Passing the caller's argument a second time would take a SECOND snapshot of it,
|
|
499
|
+
// and a shared-memory view can differ between the two: the nonce compared below would belong to
|
|
500
|
+
// one response and the signature verified to another, which is the split this whole mechanism
|
|
501
|
+
// exists to close.
|
|
466
502
|
return pathValidate.verifyOcspResponse(parsed, cert, issuerCert, time, { historicalMode: opts.historicalMode === true }).then(function (verdict) {
|
|
467
503
|
// A client that sent no nonce still gets the field, as null. Leaving it absent would make
|
|
468
504
|
// "not requested" indistinguishable from "the field is not there yet" for a consumer reading
|
package/lib/path-validate.js
CHANGED
|
@@ -1178,7 +1178,10 @@ async function validate(path, opts) {
|
|
|
1178
1178
|
// crypto amplification from an oversized path). Entry-point tier: throw.
|
|
1179
1179
|
var maxCerts = guard.limits.cap(opts.maxPathCerts, "validate: opts.maxPathCerts", constants.LIMITS.PATH_MAX_CERTS, { E: E, code: "path/bad-input", min: 1 });
|
|
1180
1180
|
if (path.length > maxCerts) throw E("path/bad-input", "validate: the certification path has " + path.length + " certificates, exceeding the maxPathCerts limit (" + maxCerts + ")");
|
|
1181
|
-
|
|
1181
|
+
// Through the same door `build` uses, and for the same reason: every certificate this walk decides
|
|
1182
|
+
// over is re-derived from the bytes its parser read (see coerceCert), so a rebuilt object cannot
|
|
1183
|
+
// present a genuine signed range alongside substituted fields.
|
|
1184
|
+
var certs = path.map(function (c, ci) { return coerceCert(c, "validate: path[" + ci + "]"); });
|
|
1182
1185
|
var n = certs.length;
|
|
1183
1186
|
if (n < 1) throw E("path/empty-path", "validate: the certification path is empty");
|
|
1184
1187
|
if (!opts.trustAnchor) throw E("path/bad-input", "validate: a trustAnchor is required");
|
|
@@ -1856,7 +1859,16 @@ function selectDelta(candidates) {
|
|
|
1856
1859
|
* typeof checker.check; // "function"
|
|
1857
1860
|
*/
|
|
1858
1861
|
function crlChecker(crls, opts) {
|
|
1859
|
-
var parsed = (crls || []).map(function (c) {
|
|
1862
|
+
var parsed = (crls || []).map(function (c, ci) {
|
|
1863
|
+
// Re-derived from the bytes the parser read, exactly as a certificate is. A CRL's answer is a
|
|
1864
|
+
// verdict, and its signature covers a byte range while the revocation list and the scope
|
|
1865
|
+
// extensions are separate properties of the parsed object: keep a correctly signed CRL's
|
|
1866
|
+
// `tbsBytes` and signature, empty `revokedCertificates`, and the signature still verifies while
|
|
1867
|
+
// a revoked certificate reports good. The scope fields fail the same way -- an emptied
|
|
1868
|
+
// `crlExtensions` turns a scope-restricted CRL into one that answers for everything.
|
|
1869
|
+
return guard.parsed.acceptDerived(c, "crl", crl.parse, E, "path/bad-input",
|
|
1870
|
+
"crlChecker: crls[" + ci + "]");
|
|
1871
|
+
});
|
|
1860
1872
|
// RFC 5280 sec. 6.3.1(b): use-deltas is an INPUT to the algorithm. Default ON --
|
|
1861
1873
|
// a caller holding a delta wants it used -- and turning it off never makes a
|
|
1862
1874
|
// verdict weaker, only less determined.
|
|
@@ -2296,7 +2308,10 @@ cmsVerify.setEngine({ build: build, validate: validate, toAnchor: toAnchor,
|
|
|
2296
2308
|
* typeof checker.check; // "function"
|
|
2297
2309
|
*/
|
|
2298
2310
|
function ocspChecker(responses) {
|
|
2299
|
-
|
|
2311
|
+
// The same door verifyOcspResponse uses. This checker's responses reach the identical three-part
|
|
2312
|
+
// signature check, so accepting a parsed object here on a truthy responseStatus would leave the
|
|
2313
|
+
// door shut on one of the two ways into that check and open on the other.
|
|
2314
|
+
var parsed = (responses || []).map(function (r) { return _ocspFromBytes(r); });
|
|
2300
2315
|
return {
|
|
2301
2316
|
check: async function (cert, issuer, ctx) {
|
|
2302
2317
|
var time = ctx.time;
|
|
@@ -2346,14 +2361,14 @@ function ocspChecker(responses) {
|
|
|
2346
2361
|
|
|
2347
2362
|
/**
|
|
2348
2363
|
* @primitive pki.path.verifyOcspResponse
|
|
2349
|
-
* @signature pki.path.verifyOcspResponse(
|
|
2364
|
+
* @signature pki.path.verifyOcspResponse(response, cert, issuerCert, time, opts?) -> Promise<{ status, responderAuthorized, signatureValid, matched, thisUpdate, nextUpdate, revocationReason?, reason }>
|
|
2350
2365
|
* @since 0.2.22
|
|
2351
2366
|
* @status stable
|
|
2352
2367
|
* @spec RFC 6960
|
|
2353
2368
|
* @related pki.ocsp.verify, pki.path.ocspChecker
|
|
2354
2369
|
*
|
|
2355
|
-
* Verify a single
|
|
2356
|
-
* already-parsed issuer certificate at `time` -- the lower-level primitive
|
|
2370
|
+
* Verify a single OCSP response for one certificate
|
|
2371
|
+
* against its already-parsed issuer certificate at `time` -- the lower-level primitive
|
|
2357
2372
|
* `pki.ocsp.verify` composes after parsing its inputs (most callers want that
|
|
2358
2373
|
* ergonomic entry, which also handles DER/PEM decoding and request-nonce
|
|
2359
2374
|
* matching). It runs the EXACT SAME gates the path validator's `ocspChecker`
|
|
@@ -2374,6 +2389,16 @@ function ocspChecker(responses) {
|
|
|
2374
2389
|
* `time` either way. `time` must be a valid `Date`. A malformed response's parse
|
|
2375
2390
|
* fault surfaces as the parser's typed `ocsp/*` / `asn1/*` error.
|
|
2376
2391
|
*
|
|
2392
|
+
* The response is its DER bytes, a PEM string, or an unmodified
|
|
2393
|
+
* `pki.schema.ocsp.parseResponse` result. A REBUILT parsed response is refused. A
|
|
2394
|
+
* signature check has three parts -- the signature, the algorithm that verifies it,
|
|
2395
|
+
* and the bytes it covers -- and on a parsed object all three are separate properties:
|
|
2396
|
+
* a genuine CA signature over a certificate that CA issued, relabelled, verifies as a
|
|
2397
|
+
* ResponseData signature for a response that never existed. The parser marks what it
|
|
2398
|
+
* returns, so those three are known to have been derived together from one byte
|
|
2399
|
+
* string; `Object.assign`, spread and a JSON round-trip all drop the mark, which is
|
|
2400
|
+
* exactly how such an object is assembled.
|
|
2401
|
+
*
|
|
2377
2402
|
* @example
|
|
2378
2403
|
* var ca = await pki.key.generate("Ed25519");
|
|
2379
2404
|
* var caKey = await pki.key.export(ca.privateKey);
|
|
@@ -2389,11 +2414,49 @@ function ocspChecker(responses) {
|
|
|
2389
2414
|
* { responderID: "byName", responses: [{ cert: leafDer, issuer: caDer, status: "good" }] },
|
|
2390
2415
|
* { cert: caDer, key: caKey });
|
|
2391
2416
|
* var cert = pki.schema.x509.parse(leafDer), issuerCert = pki.schema.x509.parse(caDer);
|
|
2392
|
-
* var
|
|
2393
|
-
* var v = await pki.path.verifyOcspResponse(resp, cert, issuerCert, new Date());
|
|
2417
|
+
* var v = await pki.path.verifyOcspResponse(der, cert, issuerCert, new Date());
|
|
2394
2418
|
* v.status; // "good" | "revoked" | "unknown"
|
|
2395
2419
|
*/
|
|
2396
|
-
function verifyOcspResponse(
|
|
2420
|
+
function verifyOcspResponse(response, cert, issuerCert, time, opts) {
|
|
2421
|
+
var parsedResponse, subject, issuer;
|
|
2422
|
+
try {
|
|
2423
|
+
parsedResponse = _ocspFromBytes(response);
|
|
2424
|
+
// The two certificates go through the same door as the response, for the same reason: this
|
|
2425
|
+
// verdict is about a certificate's IDENTITY -- its serial and its issuer's name and key are
|
|
2426
|
+
// what the CertID is matched against, and the issuer's key is what authorizes a delegated
|
|
2427
|
+
// responder. A caller-assembled certificate could name one identity while carrying another's
|
|
2428
|
+
// signed bytes, so both are re-derived from the bytes their parser read.
|
|
2429
|
+
subject = coerceCert(cert, "the certificate");
|
|
2430
|
+
issuer = coerceCert(issuerCert, "the issuer certificate");
|
|
2431
|
+
} catch (e) { return Promise.reject(e); }
|
|
2432
|
+
return _verifyOcspParsed(parsedResponse, subject, issuer, time, opts);
|
|
2433
|
+
}
|
|
2434
|
+
|
|
2435
|
+
// The response, always parsed from the bytes the caller handed over.
|
|
2436
|
+
//
|
|
2437
|
+
// A signature check has three parts -- the signature, the algorithm that verifies it, and the byte
|
|
2438
|
+
// range it covers -- and on a claimed-parsed response all three are separate properties of one
|
|
2439
|
+
// caller-supplied object. Pair a real CA's signature over a certificate IT issued with that
|
|
2440
|
+
// certificate's own tbsBytes and algorithm, label them `signature` / `tbsResponseDataBytes` /
|
|
2441
|
+
// `signatureAlgorithm`, and the check verifies: a genuine signature, over the bytes it was made
|
|
2442
|
+
// over, under the right key -- for a structure the responder never produced. Parsing binds the three
|
|
2443
|
+
// to one byte string, which is the only thing that makes the verdict about a response at all.
|
|
2444
|
+
var _OCSP_CLAIM = ["responseStatus", "basicResponse", "tbsResponseDataBytes"];
|
|
2445
|
+
function _ocspFromBytes(response) {
|
|
2446
|
+
return guard.parsed.fromTrustedSource(response, "ocspResponse", _OCSP_CLAIM, function (bytes) {
|
|
2447
|
+
if (!Buffer.isBuffer(bytes) && !(bytes instanceof Uint8Array) && typeof bytes !== "string") {
|
|
2448
|
+
throw E("path/bad-input", "verifyOcspResponse: the response must be an OCSP response DER Buffer, a PEM string, or a pki.schema.ocsp.parseResponse result");
|
|
2449
|
+
}
|
|
2450
|
+
return ocsp.parseResponse(bytes);
|
|
2451
|
+
}, E, "path/bad-input",
|
|
2452
|
+
"verifyOcspResponse: the response must be its DER bytes, a PEM string, or an unmodified pki.schema.ocsp.parseResponse result: the signature, the algorithm that verifies it and the bytes it covers are separate properties of a parsed object, so a REBUILT response (Object.assign, spread, a JSON round-trip) could have the three describe different responses and is refused");
|
|
2453
|
+
}
|
|
2454
|
+
|
|
2455
|
+
// The verification over a response this module has ALREADY derived from bytes -- reached only
|
|
2456
|
+
// through the door above and through ocspChecker, which parses the responder's own reply. Not
|
|
2457
|
+
// exported: this module's export object IS pki.path, so anything on it is public surface and frozen
|
|
2458
|
+
// by the API snapshot, whatever a comment beside it claims.
|
|
2459
|
+
function _verifyOcspParsed(parsedResponse, cert, issuerCert, time, opts) {
|
|
2397
2460
|
opts = opts || {};
|
|
2398
2461
|
// The currency + responder-cert validity windows compare against `time`; a missing or invalid
|
|
2399
2462
|
// check date must fail closed (a NaN compares false against every bound), never silently pass.
|
|
@@ -2451,41 +2514,22 @@ function nameMatchSoft(rdnsA, rdnsB) {
|
|
|
2451
2514
|
catch (_e) { return false; }
|
|
2452
2515
|
}
|
|
2453
2516
|
|
|
2454
|
-
// A
|
|
2455
|
-
// the
|
|
2456
|
-
|
|
2457
|
-
|
|
2458
|
-
//
|
|
2459
|
-
// every
|
|
2460
|
-
//
|
|
2461
|
-
//
|
|
2462
|
-
|
|
2463
|
-
|
|
2464
|
-
|
|
2465
|
-
|
|
2466
|
-
|
|
2467
|
-
|
|
2468
|
-
|
|
2469
|
-
|
|
2470
|
-
!!o.subjectPublicKeyInfo && Buffer.isBuffer(o.subjectPublicKeyInfo.bytes) &&
|
|
2471
|
-
!!o.subjectPublicKeyInfo.algorithm && typeof o.subjectPublicKeyInfo.algorithm.oid === "string" &&
|
|
2472
|
-
!!o.subjectPublicKeyInfo.publicKey && Buffer.isBuffer(o.subjectPublicKeyInfo.publicKey.bytes) &&
|
|
2473
|
-
typeof o.subjectPublicKeyInfo.publicKey.unusedBits === "number" &&
|
|
2474
|
-
Array.isArray(o.extensions) && o.extensions.every(_isExtensionEntry);
|
|
2475
|
-
}
|
|
2476
|
-
|
|
2477
|
-
function coerceCert(input) {
|
|
2478
|
-
// An already-parsed certificate is passed through; a DER Buffer / PEM string is
|
|
2479
|
-
// parsed (and its typed error normalized by the caller). A claimed-parsed object
|
|
2480
|
-
// (a truthy tbsBytes) must carry the COMPLETE parsed-certificate shape build and
|
|
2481
|
-
// the validate hand-off dereference -- a bare { tbsBytes } object, or any partial
|
|
2482
|
-
// shape, fails closed here as a typed PathError rather than a raw TypeError deeper
|
|
2483
|
-
// in the walk or inside validate.
|
|
2484
|
-
if (input && typeof input === "object" && !Buffer.isBuffer(input) && input.tbsBytes !== undefined) {
|
|
2485
|
-
if (!_isParsedCert(input)) throw E("path/bad-input", "build: an input has tbsBytes but is not a well-formed parsed certificate");
|
|
2486
|
-
return input;
|
|
2487
|
-
}
|
|
2488
|
-
return x509.parse(input);
|
|
2517
|
+
// A certificate reaching a VERDICT is re-derived from the bytes the parser read, never trusted as
|
|
2518
|
+
// the object it arrives as. Completeness -- every field present with the right type -- is not enough
|
|
2519
|
+
// here, because a certificate's meaning is one signature over one byte range while a parsed
|
|
2520
|
+
// certificate presents that range, the signature, and the fields the range encodes as separate
|
|
2521
|
+
// properties. Keep a real CA certificate's `tbsBytes` and signature and substitute only its
|
|
2522
|
+
// `subjectPublicKeyInfo`, and every completeness rule passes, this walk verifies the ORIGINAL signed
|
|
2523
|
+
// range, and then uses the substituted key to check the next certificate -- a forged chain built out
|
|
2524
|
+
// of a genuine certificate. Emptying `extensions` is the same move against basicConstraints,
|
|
2525
|
+
// keyUsage, the name constraints and the unknown-critical rule.
|
|
2526
|
+
//
|
|
2527
|
+
// So the door takes the parser's record instead: a certificate from pki.schema.x509.parse re-parses
|
|
2528
|
+
// from the bytes it was read from, and anything done to that object since is discarded. One a caller
|
|
2529
|
+
// assembled has no record and is refused rather than silently believed.
|
|
2530
|
+
function coerceCert(input, label) {
|
|
2531
|
+
return guard.parsed.acceptDerived(input, "certificate", x509.parse, E, "path/bad-input",
|
|
2532
|
+
label || "a certificate");
|
|
2489
2533
|
}
|
|
2490
2534
|
|
|
2491
2535
|
// A trust-store entry is either a ready anchor tuple { name, publicKey,
|
package/lib/pbes2.js
CHANGED
|
@@ -177,7 +177,12 @@ function pbes2Encrypt(pwBytes, plaintext, opts, E, prefix) {
|
|
|
177
177
|
// <prefix>/bad-input a param guard raises is normalized to the structural <prefix>/bad-algorithm-parameters.
|
|
178
178
|
// A wrong key / bad PKCS#7 pad collapses to the UNIFORM <prefix>/decrypt-failed (RFC 8018 sec. 8). The
|
|
179
179
|
// plaintext integrity re-check (re-parse as a PrivateKeyInfo / SafeContents) is the CALLER's step.
|
|
180
|
-
|
|
180
|
+
// `budget` (optional) is a shared { rounds } tally a caller decrypting MANY structures under one
|
|
181
|
+
// call charges against, so the per-structure iteration cap cannot simply reset each time: PBKDF2
|
|
182
|
+
// runs on the event loop, and a store that repeats a costly bag up to the parser's element limit
|
|
183
|
+
// otherwise multiplies the cap by that limit. A caller decrypting exactly one structure passes
|
|
184
|
+
// nothing and is bounded by the cap alone.
|
|
185
|
+
function pbes2Decrypt(pwBytes, params, ciphertext, opts, E, prefix, budget) {
|
|
181
186
|
var keyBits, iv, pb;
|
|
182
187
|
try {
|
|
183
188
|
var p = seqChildren(params, 2, "PBES2 parameters", E, prefix);
|
|
@@ -197,6 +202,11 @@ function pbes2Decrypt(pwBytes, params, ciphertext, opts, E, prefix) {
|
|
|
197
202
|
}
|
|
198
203
|
throw E(prefix + "/bad-algorithm-parameters", "malformed PBES2 parameters", e);
|
|
199
204
|
}
|
|
205
|
+
// Charge the shared budget BEFORE deriving, so the work is refused rather than performed.
|
|
206
|
+
if (budget) {
|
|
207
|
+
budget.rounds -= pb.iterations;
|
|
208
|
+
if (budget.rounds < 0) throw E(prefix + "/iteration-limit", "the aggregate PBKDF2 key-derivation work exceeds the budget (a hostile many-element input)");
|
|
209
|
+
}
|
|
200
210
|
var dk = nodeCrypto.pbkdf2Sync(pwBytes, pb.salt, pb.iterations, keyBits / 8, pb.prfNode);
|
|
201
211
|
try { return cbcDecrypt(dk, iv, ciphertext, keyBits); }
|
|
202
212
|
catch (_e) { throw E(prefix + "/decrypt-failed", "decryption failed"); }
|
package/lib/pkcs12-build.js
CHANGED
|
@@ -69,12 +69,14 @@ var MAX_PBMAC1_KEYLEN = 1024; // an HMAC key beyond a hash block is pointless
|
|
|
69
69
|
// pkcs12-local (its KDF is bespoke here) while PBMAC1 reuses the toolkit-wide PBKDF2 ceiling. 1e6 is ~500x
|
|
70
70
|
// the OpenSSL default (2048) yet still bounds the loop to ~1 second.
|
|
71
71
|
var CLASSIC_MAC_MAX_ITERATIONS = 1000000;
|
|
72
|
-
// The AGGREGATE cap on synchronous
|
|
73
|
-
//
|
|
74
|
-
// hostile store that duplicates a costly
|
|
75
|
-
// the event loop for minutes; this bounds the total to
|
|
76
|
-
//
|
|
77
|
-
|
|
72
|
+
// The AGGREGATE cap on synchronous KDF work across a single open() -- rounds summed over every encrypted
|
|
73
|
+
// bag and safe, whichever scheme it uses: the legacy App. B KDF (its block count x iterations) and PBKDF2
|
|
74
|
+
// alike. The per-bag iteration cap resets per bag, so a hostile store that duplicates a costly bag up to the
|
|
75
|
+
// parser's 1024-element limit would otherwise block the event loop for minutes; this bounds the total to
|
|
76
|
+
// ~the classic MAC ceiling. It covers BOTH schemes because covering only the legacy one left the modern
|
|
77
|
+
// path -- the one every current producer emits -- free to multiply its cap by the element limit.
|
|
78
|
+
// A conforming store runs only a few thousand rounds (its handful of bags at ~2048 iterations).
|
|
79
|
+
var KDF_MAX_ROUNDS = CLASSIC_MAC_MAX_ITERATIONS;
|
|
78
80
|
|
|
79
81
|
// The classic App. B.2 KDF (u = hash output bytes, v = compression block bytes) per RFC 7292 App. B.2.
|
|
80
82
|
var P12_KDF_UV = {
|
|
@@ -102,6 +104,18 @@ var DIGEST_NAME = { sha1: "sha1", sha256: "sha256", sha384: "sha384", sha512: "s
|
|
|
102
104
|
// is taken verbatim as already-formatted bytes (an escape hatch for a caller that pre-encodes).
|
|
103
105
|
function _p12Password(pw) { return _p12PasswordOwned(pw).bytes; }
|
|
104
106
|
|
|
107
|
+
// An ABSENT password is refused rather than encoded as the empty one. The two are not the same
|
|
108
|
+
// credential, and the difference is invisible at the call site: a caller who misspells the option,
|
|
109
|
+
// or threads it through a layer that drops it, otherwise gets a store whose private key is
|
|
110
|
+
// protected by nothing and no error anywhere saying so. The empty password remains available --
|
|
111
|
+
// it just has to be asked for, as "".
|
|
112
|
+
var _MISSING_PASSWORD = "a password must be a string, Buffer, or Uint8Array -- an omitted password " +
|
|
113
|
+
"is not the empty password; pass \"\" to use the empty one deliberately";
|
|
114
|
+
|
|
115
|
+
// Every field opts.integrity reads. Adding one here is the only way to make it accepted, so a
|
|
116
|
+
// capability cannot arrive with its option silently ignored at this boundary.
|
|
117
|
+
var _INTEGRITY_OPTS = { mode: 1, signer: 1, signers: 1, certificates: 1, sid: 1, signingTime: 1 };
|
|
118
|
+
|
|
105
119
|
// The same encoding, reporting OWNERSHIP -- mirroring pbes2.passwordBytesOwned. A caller-supplied
|
|
106
120
|
// Buffer is returned AS-IS and is BORROWED: clearing it would destroy the caller's own credential,
|
|
107
121
|
// which is a worse defect than leaving a copy readable. Every other input is re-encoded into a
|
|
@@ -111,10 +125,9 @@ function _p12PasswordOwned(pw) {
|
|
|
111
125
|
return { bytes: _p12Encode(pw), owned: true };
|
|
112
126
|
}
|
|
113
127
|
function _p12Encode(pw) {
|
|
114
|
-
if (pw == null) pw = "";
|
|
115
128
|
if (Buffer.isBuffer(pw)) return pw;
|
|
116
129
|
if (pw instanceof Uint8Array) return Buffer.from(pw);
|
|
117
|
-
if (typeof pw !== "string") throw _err("pkcs12/bad-input",
|
|
130
|
+
if (typeof pw !== "string") throw _err("pkcs12/bad-input", _MISSING_PASSWORD);
|
|
118
131
|
var out = Buffer.alloc(pw.length * 2 + 2); // + the 2-byte NULL terminator
|
|
119
132
|
for (var i = 0; i < pw.length; i++) {
|
|
120
133
|
var u = pw.charCodeAt(i);
|
|
@@ -130,12 +143,19 @@ function _p12Encode(pw) {
|
|
|
130
143
|
// PBKDF2 the raw UTF-8 password for these modern schemes (confirmed byte-for-byte against `openssl pkcs12`),
|
|
131
144
|
// reserving the BMPString+NULL form for the bespoke Appendix B KDF only. A file we emit must open in OpenSSL,
|
|
132
145
|
// so the modern schemes use UTF-8 here; only the classic Appendix B MAC uses `_p12Password`.
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
146
|
+
// The UTF-8 encoding, reporting OWNERSHIP -- the sibling of _p12PasswordOwned, and it exists
|
|
147
|
+
// for the same reason. A caller-supplied Buffer is BORROWED and left alone; every other input is
|
|
148
|
+
// re-encoded into a buffer this module allocated, and a plaintext password copy this module made
|
|
149
|
+
// is cleared once the derivation has consumed it. Without it the App. B.1 copy taken from the same
|
|
150
|
+
// argument in the same call was wiped while this one was not.
|
|
151
|
+
function _pbePasswordOwned(pw) {
|
|
152
|
+
if (Buffer.isBuffer(pw)) return { bytes: pw, owned: false };
|
|
153
|
+
if (pw instanceof Uint8Array) return { bytes: Buffer.from(pw), owned: true };
|
|
154
|
+
if (typeof pw !== "string") throw _err("pkcs12/bad-input", _MISSING_PASSWORD);
|
|
155
|
+
return { bytes: Buffer.from(pw, "utf8"), owned: true };
|
|
156
|
+
}
|
|
157
|
+
function _wipePw(owned) {
|
|
158
|
+
if (owned.owned) guard.secret.zeroize(owned.bytes, Pkcs12Error, "pkcs12/bad-input", "the password encoding");
|
|
139
159
|
}
|
|
140
160
|
|
|
141
161
|
// Concatenate copies of `src` to the smallest positive multiple of `blockSize` (>= src length), truncating
|
|
@@ -267,8 +287,10 @@ function _buildBag(bag, opts, depth) {
|
|
|
267
287
|
var kDer = _coerceDer(bag.key, "shroudedKey key");
|
|
268
288
|
try { pkcs8.parse(kDer); } catch (e2) { throw _err("pkcs12/bad-input", "shroudedKey key is not a well-formed PKCS#8 PrivateKeyInfo", e2); }
|
|
269
289
|
var enc = bag.encrypt || {};
|
|
270
|
-
var pw =
|
|
271
|
-
var r
|
|
290
|
+
var pw = _pbePasswordOwned(enc.password != null ? enc.password : opts.password);
|
|
291
|
+
var r;
|
|
292
|
+
try { r = pbes2.pbes2Encrypt(pw.bytes, kDer, _pbeOpts(enc), _err, "pkcs12"); }
|
|
293
|
+
finally { _wipePw(pw); }
|
|
272
294
|
return _safeBag("pkcs8ShroudedKeyBag", b.sequence([r.algId, b.octetString(r.ct)]), bag); // EncryptedPrivateKeyInfo
|
|
273
295
|
}
|
|
274
296
|
case "cert": {
|
|
@@ -296,6 +318,24 @@ function _buildBag(bag, opts, depth) {
|
|
|
296
318
|
}
|
|
297
319
|
|
|
298
320
|
// A pre-encoded DER value (one well-formed TLV, no trailing bytes) supplied verbatim (secretValue).
|
|
321
|
+
// The store, always parsed from the BYTES the caller handed over.
|
|
322
|
+
//
|
|
323
|
+
// Both verbs that use this decide integrity, and integrity is the one operation that must not read
|
|
324
|
+
// its inputs from two independently-chosen properties. Accepting a claimed-parsed store let
|
|
325
|
+
// `macedBytes` -- the range the MAC is verified over -- and `safeBags` / `encryptedSafes` -- the
|
|
326
|
+
// content returned as verified -- come from different sources: verify store A's MAC, hand back store
|
|
327
|
+
// B's bags. Parsing here binds them, because both are then derived from one byte string.
|
|
328
|
+
//
|
|
329
|
+
// `pki.schema.pkcs12.parse` remains the parse-only route for a caller who wants the structure
|
|
330
|
+
// without an integrity decision.
|
|
331
|
+
var _STORE_CLAIM = ["integrityMode", "mac", "macedBytes"];
|
|
332
|
+
function _storeFromBytes(pfx) {
|
|
333
|
+
return guard.parsed.fromTrustedSource(pfx, "pkcs12Store", _STORE_CLAIM, function (bytes) {
|
|
334
|
+
return schemaPkcs12.parse(_coerceDer(bytes, "pfx"));
|
|
335
|
+
}, _err, "pkcs12/bad-input",
|
|
336
|
+
"pfx must be the store's DER bytes, a PEM string, or an unmodified pki.schema.pkcs12.parse result: the MAC is verified over a byte range carried on the object and the bags returned as verified are a separate property of it, so a REBUILT store (Object.assign, spread, a JSON round-trip) could have the two describe different stores and is refused");
|
|
337
|
+
}
|
|
338
|
+
|
|
299
339
|
function _reqDer(input, label) {
|
|
300
340
|
if (input == null) throw _err("pkcs12/bad-input", label + " is required");
|
|
301
341
|
var der = _bytes(input, label);
|
|
@@ -358,8 +398,10 @@ async function _buildAuthSafeElement(sc, opts) {
|
|
|
358
398
|
return b.sequence([b.oid(O("data")), b.explicit(0, b.octetString(safeContentsDer))]);
|
|
359
399
|
}
|
|
360
400
|
if (!sc.encrypt || typeof sc.encrypt !== "object") throw _err("pkcs12/bad-input", "safeContents.encrypt must be an object { password? } (RFC 7292 sec. 5.1) -- omit it entirely for a plaintext safe");
|
|
361
|
-
var pw =
|
|
362
|
-
var r
|
|
401
|
+
var pw = _pbePasswordOwned(sc.encrypt.password != null ? sc.encrypt.password : opts.password);
|
|
402
|
+
var r;
|
|
403
|
+
try { r = pbes2.pbes2Encrypt(pw.bytes, safeContentsDer, _pbeOpts(sc.encrypt), _err, "pkcs12"); }
|
|
404
|
+
finally { _wipePw(pw); }
|
|
363
405
|
var eci = b.sequence([b.oid(O("data")), r.algId, b.contextPrimitive(0, r.ct)]); // EncryptedContentInfo, [0] IMPLICIT ct
|
|
364
406
|
var encData = b.sequence([b.integer(0n), eci]); // EncryptedData { version 0, eci }
|
|
365
407
|
return b.sequence([b.oid(O("encryptedData")), b.explicit(0, encData)]);
|
|
@@ -402,7 +444,10 @@ async function _buildMacData(macOpts, sharedPassword, authSafeDer) {
|
|
|
402
444
|
var iter2 = _assertMacIter(macOpts.iterations == null ? DEFAULT_PBMAC1_ITER : macOpts.iterations, C.LIMITS.PBKDF2_MAX_ITERATIONS);
|
|
403
445
|
var keyLen = macOpts.keyLength != null ? macOpts.keyLength : prf.keyLen;
|
|
404
446
|
if (typeof keyLen !== "number" || !Number.isInteger(keyLen) || keyLen < 20 || keyLen > MAX_PBMAC1_KEYLEN) throw _err("pkcs12/bad-input", "PBMAC1 keyLength must be an integer in [20, " + MAX_PBMAC1_KEYLEN + "] (RFC 9579 sec. 9)");
|
|
405
|
-
var
|
|
447
|
+
var macPw2 = _pbePasswordOwned(password);
|
|
448
|
+
var mac;
|
|
449
|
+
try { mac = await pbes2.pbmac1(macPw2.bytes, salt, iter2, keyLen, prf.wc, prf.wc, authSafeDer); } // PBKDF2 -> UTF-8; prf == messageAuthScheme on build
|
|
450
|
+
finally { _wipePw(macPw2); }
|
|
406
451
|
var desc = { salt: salt, iterationCount: iter2, keyLength: keyLen, prfName: prf.prfName, macName: prf.prfName };
|
|
407
452
|
var digestInfo2 = b.sequence([pbes2.pbmac1AlgId(desc), b.octetString(mac)]);
|
|
408
453
|
// MacData.macSalt + iterations are ignored on a PBMAC1 verify but MUST be present + non-1 (RFC 9579 4c/4d).
|
|
@@ -480,7 +525,18 @@ function _normalizeSpec(spec, opts) {
|
|
|
480
525
|
*/
|
|
481
526
|
async function build(spec, opts) {
|
|
482
527
|
opts = opts || {};
|
|
483
|
-
|
|
528
|
+
// The integrity mode is checked against the permitted set rather than compared to one literal.
|
|
529
|
+
// Compared, any other spelling reads as "not public-key" and silently selects password integrity,
|
|
530
|
+
// dropping the signer with it: a caller who wrote "publicKey" gets a MAC where they asked for a
|
|
531
|
+
// signature, and nothing in the store or the call says which they got.
|
|
532
|
+
if (opts.integrity != null) {
|
|
533
|
+
if (typeof opts.integrity !== "object" || Array.isArray(opts.integrity)) throw _err("pkcs12/bad-input", "opts.integrity must be an object { mode, signer|signers, ... }");
|
|
534
|
+
guard.identifier.assertKnownKeys(opts.integrity, _INTEGRITY_OPTS, _err, "pkcs12/bad-input", "opts.integrity has an unknown option ");
|
|
535
|
+
if (opts.integrity.mode !== "public-key") {
|
|
536
|
+
throw _err("pkcs12/bad-integrity-mode", "opts.integrity.mode must be \"public-key\" (the only mode it selects); omit opts.integrity for password integrity, got " + JSON.stringify(opts.integrity.mode));
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
var pubKey = opts.integrity != null;
|
|
484
540
|
// RFC 7292 sec. 4: public-key integrity OMITS MacData entirely -- a caller combining opts.mac with it is a
|
|
485
541
|
// config-time reject (the self-check re-parse would otherwise fail the coherence rule anyway).
|
|
486
542
|
if (pubKey && opts.mac != null && opts.mac !== false) throw _err("pkcs12/bad-integrity-mode", "public-key integrity has no MacData -- do not combine opts.mac with opts.integrity.mode 'public-key' (RFC 7292 sec. 4)");
|
|
@@ -522,8 +578,10 @@ async function build(spec, opts) {
|
|
|
522
578
|
* @defends pkcs12-mac-forgery (CWE-347)
|
|
523
579
|
* @related pki.pkcs12.build, pki.schema.pkcs12.parse
|
|
524
580
|
*
|
|
525
|
-
* Verify a password-integrity PKCS#12 store's MAC. `pfx` is
|
|
526
|
-
* `
|
|
581
|
+
* Verify a password-integrity PKCS#12 store's MAC. `pfx` is the store's DER `Buffer`, a PEM string, or an
|
|
582
|
+
* unmodified `pki.schema.pkcs12.parse` result. A REBUILT parsed store is refused: the MAC is verified over
|
|
583
|
+
* a byte range the object carries, and the parser's mark is what says that range and the store it describes
|
|
584
|
+
* came from one place. `Object.assign`, spread and a JSON round-trip drop the mark. The password is BMPString+NULL encoded (RFC 7292 App. B.1), the MAC is
|
|
527
585
|
* recomputed over the store's exact AuthenticatedSafe byte range (`macedBytes`) using the store's own MAC
|
|
528
586
|
* parameters -- the classic Appendix B (ID=3) HMAC or the RFC 9579 PBMAC1 -- and constant-time-compared to
|
|
529
587
|
* the stored MAC value. Returns `true` / `false` for the password match; throws `Pkcs12Error` on a MAC-less
|
|
@@ -538,8 +596,14 @@ async function build(spec, opts) {
|
|
|
538
596
|
* var ok = await pki.pkcs12.verifyMac(p12, 'changeit');
|
|
539
597
|
*/
|
|
540
598
|
async function verifyMac(pfx, password, opts) {
|
|
599
|
+
return _verifyMacOfStore(_storeFromBytes(pfx), password, opts);
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
// The MAC computation over a store this module has ALREADY parsed from bytes. Separate from the
|
|
603
|
+
// public verb so `open` can verify the store it just parsed without going back through the door --
|
|
604
|
+
// the door's job is to establish that the store came from the caller's bytes, and by here it has.
|
|
605
|
+
async function _verifyMacOfStore(m, password, opts) {
|
|
541
606
|
opts = opts || {};
|
|
542
|
-
var m = (pfx && pfx.integrityMode !== undefined && pfx.mac !== undefined) ? pfx : schemaPkcs12.parse(_coerceDer(pfx, "pfx"));
|
|
543
607
|
if (m.integrityMode !== "password" || !m.mac) throw _err("pkcs12/bad-input", "the store carries no password MAC (integrityMode " + m.integrityMode + ")");
|
|
544
608
|
var expected = m.mac.macValue;
|
|
545
609
|
var computed;
|
|
@@ -564,7 +628,9 @@ async function verifyMac(pfx, password, opts) {
|
|
|
564
628
|
// downgraded store cannot pass under a weak MAC even though the algorithm identifiers parse.
|
|
565
629
|
if (prfWc === "SHA-1" || macWc === "SHA-1") throw _err("pkcs12/unsupported-algorithm", "PBMAC1 with a <= 160-bit digest (SHA-1) is refused (RFC 9579 sec. 5/7)");
|
|
566
630
|
_capWork(kdf.iterationCount, kdf.salt, opts, kdf.keyLength, C.LIMITS.PBKDF2_MAX_ITERATIONS);
|
|
567
|
-
|
|
631
|
+
var vPw = _pbePasswordOwned(password);
|
|
632
|
+
try { computed = await pbes2.pbmac1(vPw.bytes, kdf.salt, kdf.iterationCount, kdf.keyLength, prfWc, macWc, m.macedBytes); } // PBKDF2 -> UTF-8
|
|
633
|
+
finally { _wipePw(vPw); }
|
|
568
634
|
}
|
|
569
635
|
return computed.length === expected.length && guard.crypto.constantTimeEqual(computed, expected);
|
|
570
636
|
}
|
|
@@ -600,8 +666,9 @@ function _capWork(iterations, salt, opts, keyLength, hardCap) {
|
|
|
600
666
|
* (public-key privacy) safe with `opts.recipientKey` (RFC 7292 sec. 3.1, via `pki.cms.decrypt`) -- returning a
|
|
601
667
|
* structured bundle `{ integrityMode, macVerified, signers, keys, certs, crls, secrets }` -- each private key as PKCS#8
|
|
602
668
|
* `PrivateKeyInfo` DER (re-validated), each certificate / CRL / secret as raw DER, all carrying their
|
|
603
|
-
* `friendlyName` / `localKeyId` for pairing. `pfx` is
|
|
604
|
-
* `pki.schema.pkcs12.parse` result
|
|
669
|
+
* `friendlyName` / `localKeyId` for pairing. `pfx` is the store's DER `Buffer`, a PEM string, or an
|
|
670
|
+
* unmodified `pki.schema.pkcs12.parse` result; a REBUILT parsed store is refused, since the bytes whose
|
|
671
|
+
* integrity is checked and the bags returned as checked are separate properties of it.
|
|
605
672
|
*
|
|
606
673
|
* A MAC-less store is refused (`pkcs12/no-integrity`) unless `opts.allowUnauthenticated` is set. A public-key
|
|
607
674
|
* integrity store is verified through `pki.cms.verify` before any bag is trusted; a signature failure is
|
|
@@ -635,7 +702,7 @@ async function open(pfx, password, opts) {
|
|
|
635
702
|
if (opts.maxIterations != null && (typeof opts.maxIterations !== "number" || !isFinite(opts.maxIterations) || opts.maxIterations < 1 || Math.floor(opts.maxIterations) !== opts.maxIterations)) {
|
|
636
703
|
throw _err("pkcs12/bad-input", "maxIterations must be a positive integer");
|
|
637
704
|
}
|
|
638
|
-
var m = (pfx
|
|
705
|
+
var m = _storeFromBytes(pfx);
|
|
639
706
|
var macVerified = false;
|
|
640
707
|
var signers = null;
|
|
641
708
|
if (m.integrityMode === "public-key") {
|
|
@@ -649,7 +716,7 @@ async function open(pfx, password, opts) {
|
|
|
649
716
|
if (!res.valid) throw _err("pkcs12/signature-invalid", "the PKCS#12 SignedData signature did not verify (an untrusted or tampered store)");
|
|
650
717
|
signers = res.signers;
|
|
651
718
|
} else if (m.integrityMode === "password") {
|
|
652
|
-
macVerified = await
|
|
719
|
+
macVerified = await _verifyMacOfStore(m, password, opts);
|
|
653
720
|
if (!macVerified) throw _err("pkcs12/mac-mismatch", "the PKCS#12 MAC did not verify (wrong password or a tampered store)");
|
|
654
721
|
} else if (!opts.allowUnauthenticated) {
|
|
655
722
|
throw _err("pkcs12/no-integrity", "the store carries no integrity MAC (integrityMode " + m.integrityMode + "); set opts.allowUnauthenticated to open it anyway");
|
|
@@ -660,7 +727,7 @@ async function open(pfx, password, opts) {
|
|
|
660
727
|
// wrong bag password fails at the first encrypted bag as the uniform pkcs12/decrypt-failed.
|
|
661
728
|
var out = { integrityMode: m.integrityMode, macVerified: macVerified, signers: signers, keys: [], certs: [], crls: [], secrets: [] };
|
|
662
729
|
var i;
|
|
663
|
-
var kdfBudget = { rounds:
|
|
730
|
+
var kdfBudget = { rounds: KDF_MAX_ROUNDS }; // aggregate KDF work budget for this whole open(), both schemes
|
|
664
731
|
for (i = 0; i < m.safeBags.length; i++) _openBag(m.safeBags[i], password, opts, out, 0, kdfBudget);
|
|
665
732
|
for (i = 0; i < m.encryptedSafes.length; i++) await _openEncryptedSafe(m.encryptedSafes[i], password, opts, out, 0, kdfBudget);
|
|
666
733
|
if (opts.keys === "crypto") {
|
|
@@ -774,7 +841,15 @@ function _decryptLegacyPbe(ea, ct, password, opts, budget) {
|
|
|
774
841
|
// Decrypt a PBES2 (RFC 8018) or legacy-PBE (RFC 7292 App. C) bag/safe -- dispatch on the encryptionAlgorithm
|
|
775
842
|
// OID. PBES2 uses the UTF-8 password (the pinned interop convention); legacy PBE uses the App. B.1 BMPString.
|
|
776
843
|
function _decryptBag(ea, ct, password, opts, budget) {
|
|
777
|
-
|
|
844
|
+
// BOTH arms charge the one shared budget. Charging only the legacy arm left the modern one --
|
|
845
|
+
// the arm every current producer emits -- able to reset its per-bag cap on every bag, so a store
|
|
846
|
+
// that repeats a costly PBES2 bag up to the parser's element limit multiplied the cap by that
|
|
847
|
+
// limit in blocking pbkdf2Sync work.
|
|
848
|
+
if (ea.oid === O("pbes2")) {
|
|
849
|
+
var pw = _pbePasswordOwned(password);
|
|
850
|
+
try { return pbes2.pbes2Decrypt(pw.bytes, ea.parameters, ct, opts, _err, "pkcs12", budget); }
|
|
851
|
+
finally { _wipePw(pw); }
|
|
852
|
+
}
|
|
778
853
|
return _decryptLegacyPbe(ea, ct, password, opts, budget);
|
|
779
854
|
}
|
|
780
855
|
|
package/lib/pki-build.js
CHANGED
|
@@ -395,4 +395,15 @@ function tbsNameField(cert, which) {
|
|
|
395
395
|
return tbs.children[(hasVersion ? 1 : 0) + (which === "subject" ? 4 : 2)].bytes;
|
|
396
396
|
}
|
|
397
397
|
|
|
398
|
-
|
|
398
|
+
// The serialNumber of a parsed X.509 certificate, read from the SIGNED bytes rather than off the
|
|
399
|
+
// object. Same tbs layout as above, and the same reason: where a producer binds an identity to an
|
|
400
|
+
// existing certificate, that identity is issuer AND serial together. Deriving one from the bytes
|
|
401
|
+
// and reading the other from the object lets the halves name different certificates -- a Holder
|
|
402
|
+
// whose issuer is genuine and whose serial is whatever the caller wrote.
|
|
403
|
+
function tbsSerialNumber(cert) {
|
|
404
|
+
var tbs = asn1.decode(cert.tbsBytes);
|
|
405
|
+
var hasVersion = tbs.children[0].tagClass === "context" && tbs.children[0].tagNumber === 0;
|
|
406
|
+
return asn1.read.integer(tbs.children[hasVersion ? 1 : 0]);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
module.exports = { makeBuilder: makeBuilder, KU_BIT: KU_BIT, tbsNameField: tbsNameField, tbsSerialNumber: tbsSerialNumber };
|
package/lib/schema-cms.js
CHANGED
|
@@ -47,6 +47,7 @@
|
|
|
47
47
|
var asn1 = require("./asn1-der");
|
|
48
48
|
var schema = require("./schema-engine");
|
|
49
49
|
var pkix = require("./schema-pkix");
|
|
50
|
+
var guard = require("./guard-all");
|
|
50
51
|
var oid = require("./oid");
|
|
51
52
|
var frameworkError = require("./framework-error");
|
|
52
53
|
var schemaX509 = require("./schema-x509");
|
|
@@ -1150,7 +1151,15 @@ var CONTENT_INFO = schema.seq([
|
|
|
1150
1151
|
* cms.signerInfos[0].sid.serialNumberHex; // -> "0a1b"
|
|
1151
1152
|
* cms.encapContentInfo.eContent; // -> Buffer | null (detached)
|
|
1152
1153
|
*/
|
|
1153
|
-
|
|
1154
|
+
// Recording, for the reason the certificate and CRL parsers are: a SignedData is one or more
|
|
1155
|
+
// signatures over byte ranges, and a parsed one presents those ranges (`signedAttrsBytes`, the
|
|
1156
|
+
// encapsulated `eContent`), the signatures, and the certificates that verify them as separate
|
|
1157
|
+
// properties of one object. Keep a genuine signer's `signature` and `signedAttrsBytes` and replace
|
|
1158
|
+
// the `eContent` beside them, and every part of the check still passes for content that signer never
|
|
1159
|
+
// signed -- which is exactly the forgery pki.cms.verify's own block claims to defend. The verdict
|
|
1160
|
+
// verbs re-derive from what is recorded here, so the object a caller passes names bytes rather than
|
|
1161
|
+
// asserting facts.
|
|
1162
|
+
var parse = pkix.makeRecordingParser({ pemLabel: "CMS", PemError: PemError, ErrorClass: CmsError, prefix: "cms", what: "CMS ContentInfo", topSchema: CONTENT_INFO, ns: NS }, "cms");
|
|
1154
1163
|
|
|
1155
1164
|
/**
|
|
1156
1165
|
* @primitive pki.schema.cms.pemDecode
|
|
@@ -1220,7 +1229,13 @@ function walkEnvelopedData(node) { return schema.walk(ENVELOPED_DATA, node, NS).
|
|
|
1220
1229
|
// (an RFC 7292 PFX authSafe or encrypted safe, whose wire encoding may be BER
|
|
1221
1230
|
// that the strict `parse` entry would refuse). Same contract as
|
|
1222
1231
|
// walkEnvelopedData: the node is the bare structure, typed cms/* on rejection.
|
|
1223
|
-
|
|
1232
|
+
// Records provenance, like parse: the structure a consumer hands to pki.cms.verify must be
|
|
1233
|
+
// re-derivable from the bytes it was walked from, and a PFX authSafe reaches verify this way. The
|
|
1234
|
+
// walker's own BER-tolerant decode is what replays it -- the strict `parse` entry would refuse the
|
|
1235
|
+
// indefinite-length encoding real stores carry.
|
|
1236
|
+
var walkSignedData = guard.parsed.recordingWalker("cms", function (node) {
|
|
1237
|
+
return schema.walk(SIGNED_DATA, node, NS).result;
|
|
1238
|
+
}, function (der) { return asn1.decode(der, { ber: true }); });
|
|
1224
1239
|
function walkEncryptedData(node) { return schema.walk(ENCRYPTED_DATA, node, NS).result; }
|
|
1225
1240
|
// Validate + surface one countersignature value (RFC 5652 sec. 11.4, Countersignature ::=
|
|
1226
1241
|
// SignerInfo) into the same parsed shape parse() gives a top-level SignerInfo -- so pki.cms.verify
|