@blamejs/pki 0.5.4 → 0.5.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/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
- if (arg && arg.subjectPublicKeyInfo && arg.tbsBytes) return arg; // already parsed
61
- var der;
62
- if (Buffer.isBuffer(arg)) der = arg;
63
- else if (arg instanceof Uint8Array) der = Buffer.from(arg);
64
- else if (typeof arg === "string") { try { der = x509.pemDecode(arg); } catch (e) { throw _err("ocsp/bad-input", (what || "a certificate") + " PEM could not be decoded", e); } }
65
- else throw _err("ocsp/bad-input", (what || "a certificate") + " must be a parsed certificate, a DER Buffer, or a PEM string");
66
- try { return x509.parse(der); } catch (e) { throw _err("ocsp/bad-input", (what || "a certificate") + " is not a well-formed X.509 certificate", e); }
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
- parsed = (response && response.responseStatus) ? response : ocspSchema.parseResponse(_toDer(response, "the OCSP response"));
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
@@ -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
- var certs = path.map(function (c) { return (c && c.tbsBytes) ? c : x509.parse(c); });
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) { return (c && c.tbsBytes) ? c : crl.parse(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
- var parsed = (responses || []).map(function (r) { return (r && r.responseStatus) ? r : ocsp.parseResponse(r); });
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(parsedResponse, cert, issuerCert, time, opts?) -> Promise<{ status, responderAuthorized, signatureValid, matched, thisUpdate, nextUpdate, revocationReason?, reason }>
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 already-parsed OCSP response for one certificate against its
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 resp = pki.schema.ocsp.parseResponse(der);
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(parsedResponse, cert, issuerCert, time, opts) {
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 parsed extension entry the search's findExt dereferences by .oid and, for
2455
- // the subjectAltName, by .value (a Buffer in the identity key).
2456
- function _isExtensionEntry(e) { return !!e && typeof e.oid === "string" && Buffer.isBuffer(e.value); }
2457
-
2458
- // The complete parsed-certificate shape build produces AND hands to validate --
2459
- // every top-level field this module dereferences (grep-verified), each with the
2460
- // type the code assumes. A claimed-parsed object satisfying this cannot throw a
2461
- // raw TypeError anywhere in the search or the validate hand-off.
2462
- function _isParsedCert(o) {
2463
- return Buffer.isBuffer(o.tbsBytes) &&
2464
- typeof o.serialNumberHex === "string" &&
2465
- !!o.signatureAlgorithm && typeof o.signatureAlgorithm.oid === "string" &&
2466
- !!o.signatureValue && Buffer.isBuffer(o.signatureValue.bytes) &&
2467
- !!o.validity && o.validity.notBefore instanceof Date && o.validity.notAfter instanceof Date &&
2468
- !!o.issuer && Array.isArray(o.issuer.rdns) &&
2469
- !!o.subject && Array.isArray(o.subject.rdns) && Buffer.isBuffer(o.subject.bytes) &&
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
- function pbes2Decrypt(pwBytes, params, ciphertext, opts, E, prefix) {
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"); }
@@ -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 legacy-PBE App. B KDF work across a single open() -- SHA-1 rounds summed
73
- // over every legacy bag (its KDF block count x iterations). The per-bag iteration cap resets per bag, so a
74
- // hostile store that duplicates a costly legacy bag up to the parser's 1024-bag limit would otherwise block
75
- // the event loop for minutes; this bounds the total to ~the classic MAC ceiling. A conforming store runs only
76
- // a few thousand rounds (its handful of bags at ~2048 iterations).
77
- var LEGACY_KDF_MAX_ROUNDS = CLASSIC_MAC_MAX_ITERATIONS;
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", "a password must be a string, Buffer, or Uint8Array");
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
- function _pbePassword(pw) {
134
- if (pw == null) pw = "";
135
- if (Buffer.isBuffer(pw)) return pw;
136
- if (pw instanceof Uint8Array) return Buffer.from(pw);
137
- if (typeof pw !== "string") throw _err("pkcs12/bad-input", "a password must be a string, Buffer, or Uint8Array");
138
- return Buffer.from(pw, "utf8");
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 = _pbePassword(enc.password != null ? enc.password : opts.password);
271
- var r = pbes2.pbes2Encrypt(pw, kDer, _pbeOpts(enc), _err, "pkcs12");
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 = _pbePassword(sc.encrypt.password != null ? sc.encrypt.password : opts.password);
362
- var r = pbes2.pbes2Encrypt(pw, safeContentsDer, _pbeOpts(sc.encrypt), _err, "pkcs12");
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 mac = await pbes2.pbmac1(_pbePassword(password), salt, iter2, keyLen, prf.wc, prf.wc, authSafeDer); // PBKDF2 -> UTF-8; prf == messageAuthScheme on build
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
- var pubKey = opts.integrity != null && opts.integrity.mode === "public-key";
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 a `pki.schema.pkcs12.parse` result, a DER
526
- * `Buffer`, or a PEM string. The password is BMPString+NULL encoded (RFC 7292 App. B.1), the MAC is
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
- computed = await pbes2.pbmac1(_pbePassword(password), kdf.salt, kdf.iterationCount, kdf.keyLength, prfWc, macWc, m.macedBytes); // PBKDF2 -> UTF-8
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 a DER `Buffer`, PEM string, or a
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 && pfx.integrityMode !== undefined && pfx.mac !== undefined) ? pfx : schemaPkcs12.parse(_coerceDer(pfx, "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 verifyMac(m, password, opts);
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: LEGACY_KDF_MAX_ROUNDS }; // aggregate legacy-PBE KDF work budget for this whole open()
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
- if (ea.oid === O("pbes2")) return pbes2.pbes2Decrypt(_pbePassword(password), ea.parameters, ct, opts, _err, "pkcs12");
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
- module.exports = { makeBuilder: makeBuilder, KU_BIT: KU_BIT, tbsNameField: tbsNameField };
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
- var parse = pkix.makeParser({ pemLabel: "CMS", PemError: PemError, ErrorClass: CmsError, prefix: "cms", what: "CMS ContentInfo", topSchema: CONTENT_INFO, ns: NS });
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
- function walkSignedData(node) { return schema.walk(SIGNED_DATA, node, NS).result; }
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