@blamejs/pki 0.5.4 → 0.5.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/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);
@@ -456,13 +473,26 @@ function verify(response, opts) {
456
473
  if (opts.cert == null || opts.issuer == null) return Promise.reject(_err("ocsp/bad-input", "verify requires opts.cert and opts.issuer"));
457
474
  var parsed, cert, issuerCert, time;
458
475
  try {
459
- parsed = (response && response.responseStatus) ? response : ocspSchema.parseResponse(_toDer(response, "the OCSP response"));
476
+ // The response is parsed from BYTES, always. A claimed-parsed response carries the signature,
477
+ // the algorithm that verifies it and the byte range it covers as three independent properties,
478
+ // so an object could pair one structure's tbsResponseDataBytes with another's signature -- and a
479
+ // signature the issuing CA made over a certificate it issued would verify as a ResponseData
480
+ // signature, returning status "good" for a certificate the responder never spoke about.
481
+ // Parsing here binds all three to one byte string. pki.schema.ocsp.parseResponse remains the
482
+ // parse-only route for a caller who wants the structure without a verdict.
483
+ parsed = _responseFromBytes(response);
460
484
  cert = _certOf(opts.cert, "the target certificate");
461
485
  issuerCert = _certOf(opts.issuer, "the issuer certificate");
462
486
  // The time drives the currency + responder-cert validity windows; an invalid Date fails closed
463
487
  // via _asDate (a NaN compares false against every bound, silently disabling both), never defaults.
464
488
  time = opts.time == null ? new Date() : _asDate(opts.time);
465
489
  } catch (e) { return Promise.reject(e); }
490
+ // The object parsed HERE goes to the verdict verb, not the caller's argument again. It carries the
491
+ // parser's record, so the verdict verb re-derives from the same recorded bytes -- one snapshot,
492
+ // read by both. Passing the caller's argument a second time would take a SECOND snapshot of it,
493
+ // and a shared-memory view can differ between the two: the nonce compared below would belong to
494
+ // one response and the signature verified to another, which is the split this whole mechanism
495
+ // exists to close.
466
496
  return pathValidate.verifyOcspResponse(parsed, cert, issuerCert, time, { historicalMode: opts.historicalMode === true }).then(function (verdict) {
467
497
  // A client that sent no nonce still gets the field, as null. Leaving it absent would make
468
498
  // "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,
@@ -296,6 +296,24 @@ function _buildBag(bag, opts, depth) {
296
296
  }
297
297
 
298
298
  // A pre-encoded DER value (one well-formed TLV, no trailing bytes) supplied verbatim (secretValue).
299
+ // The store, always parsed from the BYTES the caller handed over.
300
+ //
301
+ // Both verbs that use this decide integrity, and integrity is the one operation that must not read
302
+ // its inputs from two independently-chosen properties. Accepting a claimed-parsed store let
303
+ // `macedBytes` -- the range the MAC is verified over -- and `safeBags` / `encryptedSafes` -- the
304
+ // content returned as verified -- come from different sources: verify store A's MAC, hand back store
305
+ // B's bags. Parsing here binds them, because both are then derived from one byte string.
306
+ //
307
+ // `pki.schema.pkcs12.parse` remains the parse-only route for a caller who wants the structure
308
+ // without an integrity decision.
309
+ var _STORE_CLAIM = ["integrityMode", "mac", "macedBytes"];
310
+ function _storeFromBytes(pfx) {
311
+ return guard.parsed.fromTrustedSource(pfx, "pkcs12Store", _STORE_CLAIM, function (bytes) {
312
+ return schemaPkcs12.parse(_coerceDer(bytes, "pfx"));
313
+ }, _err, "pkcs12/bad-input",
314
+ "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");
315
+ }
316
+
299
317
  function _reqDer(input, label) {
300
318
  if (input == null) throw _err("pkcs12/bad-input", label + " is required");
301
319
  var der = _bytes(input, label);
@@ -522,8 +540,10 @@ async function build(spec, opts) {
522
540
  * @defends pkcs12-mac-forgery (CWE-347)
523
541
  * @related pki.pkcs12.build, pki.schema.pkcs12.parse
524
542
  *
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
543
+ * Verify a password-integrity PKCS#12 store's MAC. `pfx` is the store's DER `Buffer`, a PEM string, or an
544
+ * unmodified `pki.schema.pkcs12.parse` result. A REBUILT parsed store is refused: the MAC is verified over
545
+ * a byte range the object carries, and the parser's mark is what says that range and the store it describes
546
+ * 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
547
  * recomputed over the store's exact AuthenticatedSafe byte range (`macedBytes`) using the store's own MAC
528
548
  * parameters -- the classic Appendix B (ID=3) HMAC or the RFC 9579 PBMAC1 -- and constant-time-compared to
529
549
  * the stored MAC value. Returns `true` / `false` for the password match; throws `Pkcs12Error` on a MAC-less
@@ -538,8 +558,14 @@ async function build(spec, opts) {
538
558
  * var ok = await pki.pkcs12.verifyMac(p12, 'changeit');
539
559
  */
540
560
  async function verifyMac(pfx, password, opts) {
561
+ return _verifyMacOfStore(_storeFromBytes(pfx), password, opts);
562
+ }
563
+
564
+ // The MAC computation over a store this module has ALREADY parsed from bytes. Separate from the
565
+ // public verb so `open` can verify the store it just parsed without going back through the door --
566
+ // the door's job is to establish that the store came from the caller's bytes, and by here it has.
567
+ async function _verifyMacOfStore(m, password, opts) {
541
568
  opts = opts || {};
542
- var m = (pfx && pfx.integrityMode !== undefined && pfx.mac !== undefined) ? pfx : schemaPkcs12.parse(_coerceDer(pfx, "pfx"));
543
569
  if (m.integrityMode !== "password" || !m.mac) throw _err("pkcs12/bad-input", "the store carries no password MAC (integrityMode " + m.integrityMode + ")");
544
570
  var expected = m.mac.macValue;
545
571
  var computed;
@@ -600,8 +626,9 @@ function _capWork(iterations, salt, opts, keyLength, hardCap) {
600
626
  * (public-key privacy) safe with `opts.recipientKey` (RFC 7292 sec. 3.1, via `pki.cms.decrypt`) -- returning a
601
627
  * structured bundle `{ integrityMode, macVerified, signers, keys, certs, crls, secrets }` -- each private key as PKCS#8
602
628
  * `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.
629
+ * `friendlyName` / `localKeyId` for pairing. `pfx` is the store's DER `Buffer`, a PEM string, or an
630
+ * unmodified `pki.schema.pkcs12.parse` result; a REBUILT parsed store is refused, since the bytes whose
631
+ * integrity is checked and the bags returned as checked are separate properties of it.
605
632
  *
606
633
  * A MAC-less store is refused (`pkcs12/no-integrity`) unless `opts.allowUnauthenticated` is set. A public-key
607
634
  * integrity store is verified through `pki.cms.verify` before any bag is trusted; a signature failure is
@@ -635,7 +662,7 @@ async function open(pfx, password, opts) {
635
662
  if (opts.maxIterations != null && (typeof opts.maxIterations !== "number" || !isFinite(opts.maxIterations) || opts.maxIterations < 1 || Math.floor(opts.maxIterations) !== opts.maxIterations)) {
636
663
  throw _err("pkcs12/bad-input", "maxIterations must be a positive integer");
637
664
  }
638
- var m = (pfx && pfx.integrityMode !== undefined && pfx.mac !== undefined) ? pfx : schemaPkcs12.parse(_coerceDer(pfx, "pfx"));
665
+ var m = _storeFromBytes(pfx);
639
666
  var macVerified = false;
640
667
  var signers = null;
641
668
  if (m.integrityMode === "public-key") {
@@ -649,7 +676,7 @@ async function open(pfx, password, opts) {
649
676
  if (!res.valid) throw _err("pkcs12/signature-invalid", "the PKCS#12 SignedData signature did not verify (an untrusted or tampered store)");
650
677
  signers = res.signers;
651
678
  } else if (m.integrityMode === "password") {
652
- macVerified = await verifyMac(m, password, opts);
679
+ macVerified = await _verifyMacOfStore(m, password, opts);
653
680
  if (!macVerified) throw _err("pkcs12/mac-mismatch", "the PKCS#12 MAC did not verify (wrong password or a tampered store)");
654
681
  } else if (!opts.allowUnauthenticated) {
655
682
  throw _err("pkcs12/no-integrity", "the store carries no integrity MAC (integrityMode " + m.integrityMode + "); set opts.allowUnauthenticated to open it anyway");
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-crl.js CHANGED
@@ -217,7 +217,13 @@ var CERTIFICATE_LIST = pkix.signedEnvelope(NS, TBS_CERTLIST, {
217
217
  * var crl = pki.schema.crl.parse(der);
218
218
  * crl.revokedCertificates[0].serialNumberHex; // -> "0a3f"
219
219
  */
220
- var parse = pkix.makeParser({ pemLabel: "X509 CRL", PemError: PemError, ErrorClass: CrlError, prefix: "crl", what: "CRL", topSchema: CERTIFICATE_LIST, ns: NS });
220
+ // Recording, for the same reason the certificate parser is: a CRL is one signature over one byte
221
+ // range, and a parsed CRL presents that range and the revocation list it encodes as separate
222
+ // properties. Keep a correctly signed CRL's `tbsBytes` and signature and empty `revokedCertificates`
223
+ // and the signature still verifies while the revocation answer comes from the edited list -- a
224
+ // revoked certificate reported as good. The scope fields (`crlExtensions`, an IDP, a delta
225
+ // indicator) fail the same way. The verdict verbs re-parse from what is recorded here.
226
+ var parse = pkix.makeRecordingParser({ pemLabel: "X509 CRL", PemError: PemError, ErrorClass: CrlError, prefix: "crl", what: "CRL", topSchema: CERTIFICATE_LIST, ns: NS }, "crl");
221
227
 
222
228
  /**
223
229
  * @primitive pki.schema.crl.pemDecode
@@ -517,7 +517,12 @@ var parseRequest = pkix.makeParser({ pemLabel: "OCSP REQUEST", PemError: PemErro
517
517
  * res.responseStatus.name; // -> "successful"
518
518
  * res.basicResponse.responses[0].certStatus.type; // -> "good" | "revoked" | "unknown"
519
519
  */
520
- var parseResponse = pkix.makeParser({ pemLabel: "OCSP RESPONSE", PemError: PemError, ErrorClass: OcspError, prefix: "ocsp", what: "OCSP response", topSchema: OCSP_RESPONSE, ns: NS });
520
+ // The result records the input it was derived from. A verdict verb reads the
521
+ // signature, the algorithm that verifies it and the bytes it covers off this object as
522
+ // three separate properties, and re-derives all three from the recorded input rather
523
+ // than trusting the object it was handed -- so an object rebuilt or edited after
524
+ // parsing cannot make the three describe different responses.
525
+ var parseResponse = pkix.makeRecordingParser({ pemLabel: "OCSP RESPONSE", PemError: PemError, ErrorClass: OcspError, prefix: "ocsp", what: "OCSP response", topSchema: OCSP_RESPONSE, ns: NS }, "ocspResponse");
521
526
 
522
527
  /**
523
528
  * @primitive pki.schema.ocsp.pemDecode
@@ -523,10 +523,15 @@ function _buildBag(rec, state, ctx) {
523
523
  * var store = pki.schema.pkcs12.parse(der);
524
524
  * store.safeBags.map(function (b) { return b.type; });
525
525
  */
526
- var parse = pkix.makeParser({
526
+ // The result records the input it was derived from. pki.pkcs12.verifyMac and open read
527
+ // the MACed byte range and the bags returned as verified off this object as two
528
+ // separate properties, and re-derive both from the recorded input rather than trusting
529
+ // the object -- so "verify this" and "return that" cannot come to name different
530
+ // stores, whether by rebuilding the object or by editing it after parsing.
531
+ var parse = pkix.makeRecordingParser({
527
532
  pemLabel: "PKCS12", PemError: PemError, ErrorClass: Pkcs12Error,
528
533
  prefix: "pkcs12", what: "PFX", topSchema: PFX, ns: NS, ber: true,
529
- });
534
+ }, "pkcs12Store");
530
535
 
531
536
  /**
532
537
  * @primitive pki.schema.pkcs12.pemDecode
@@ -1556,6 +1556,19 @@ function makeParser(opts) {
1556
1556
  return function (input) { return runParse(input, opts); };
1557
1557
  }
1558
1558
 
1559
+ // makeRecordingParser(opts, kind) -> a parser that also records the bytes it read.
1560
+ //
1561
+ // The pairing is here rather than repeated per format because it is one decision, not four: a
1562
+ // structure whose parse feeds a VERDICT needs its fields bound to the byte string they came from,
1563
+ // since a parsed structure presents the signed range, the signature and the fields that range
1564
+ // encodes as separate properties -- and a caller can hand back a genuine range beside substituted
1565
+ // fields. `kind` is the tag the matching door re-derives under, so a certificate cannot be presented
1566
+ // where a CRL is expected. The record lives in guard-parsed, off the object, unreachable through it.
1567
+ function makeRecordingParser(opts, kind) {
1568
+ return guard.parsed.recordingParser(kind, makeParser(opts), opts.ErrorClass,
1569
+ opts.prefix + "/bad-input", "a " + opts.what);
1570
+ }
1571
+
1559
1572
  // The X.509 SIGNED{ToBeSigned} macro (RFC 5280 sec. 4.1.1.3): the outer
1560
1573
  // SEQUENCE { toBeSigned, signatureAlgorithm AlgorithmIdentifier,
1561
1574
  // signatureValue BIT STRING } shared by Certificate, CertificateList and
@@ -1633,6 +1646,7 @@ module.exports = {
1633
1646
  pbmac1Params: pbmac1Params,
1634
1647
  spki: spki,
1635
1648
  makeParser: makeParser,
1649
+ makeRecordingParser: makeRecordingParser,
1636
1650
  signedEnvelopeTbs: signedEnvelopeTbs,
1637
1651
  rootSequenceChildren: rootSequenceChildren,
1638
1652
  assertPolicyQualifiers: assertPolicyQualifiers,
@@ -231,7 +231,20 @@ var CERTIFICATE = pkix.signedEnvelope(NS, CERTIFICATE_TBS, {
231
231
  * cert.validity.notAfter; // Date
232
232
  * cert.signatureAlgorithm.name; // "Ed25519" (the algorithm the issuer signed with)
233
233
  */
234
- var parse = pkix.makeParser({ pemLabel: "CERTIFICATE", PemError: PemError, ErrorClass: CertificateError, prefix: "x509", what: "certificate", topSchema: CERTIFICATE, ns: NS });
234
+ // The parser RECORDS the bytes it read, off the returned object, so a verb that computes a verdict
235
+ // can re-derive from them instead of trusting the object it was handed.
236
+ //
237
+ // Completeness alone cannot carry a certificate's meaning. A certificate is one signature over one
238
+ // byte range, but a parsed certificate presents that range (`tbsBytes`), the signature, and every
239
+ // field the range encodes as separate properties. Keep a real CA certificate's signed bytes and
240
+ // signature and replace only `subjectPublicKeyInfo`, and the signature check still passes over the
241
+ // original range while the substituted key is what gets used to verify the next certificate in the
242
+ // chain -- a forged chain out of a genuine certificate. Emptying `extensions` is the same move
243
+ // against basicConstraints, keyUsage, name constraints, and the unknown-critical rule.
244
+ //
245
+ // Recording the source is what makes the object safe to accept: the verdict verbs parse it again
246
+ // from these bytes, so anything done to the object afterwards is discarded rather than believed.
247
+ var parse = pkix.makeRecordingParser({ pemLabel: "CERTIFICATE", PemError: PemError, ErrorClass: CertificateError, prefix: "x509", what: "certificate", topSchema: CERTIFICATE, ns: NS }, "certificate");
235
248
 
236
249
  // matches(root): does the decoded DER look like a Certificate? A CSR and a CRL
237
250
  // share the outer SEQUENCE-of-3 envelope, so the discriminator is inside the
@@ -22,6 +22,7 @@ var pkcs8 = require("./schema-pkcs8");
22
22
  var webcrypto = require("./webcrypto");
23
23
  var subtle = webcrypto.webcrypto.subtle;
24
24
  var validator = require("./validator-all");
25
+ var guard = require("./guard-all");
25
26
  var compositeSig = require("./composite-sig");
26
27
  var b = asn1.build;
27
28
  function O(name) { return oid.byName(name); }
@@ -213,12 +214,28 @@ function _importKey(key, imp, E) {
213
214
  // signs with. One whose material this process cannot reach is refused with that as the reason.
214
215
  return webcrypto.adoptKey(key, imp, ["sign"], E, "bad-input");
215
216
  }
216
- var der;
217
+ // A Uint8Array is copied here and a PEM string is decoded here, so both leave a SECOND copy of a
218
+ // private key that nothing else can reach -- wiped once the engine has imported it, which is the
219
+ // only thing that reads it. A caller's own Buffer is passed through untouched: they hold a live
220
+ // reference and will use it again, so clearing it would destroy their key.
221
+ var der, owned = false;
217
222
  if (Buffer.isBuffer(key)) der = key;
218
- else if (key instanceof Uint8Array) der = Buffer.from(key);
219
- else if (typeof key === "string") { try { der = pkcs8.pemDecode(key); } catch (e) { throw E("bad-input", "the signer PEM private key could not be decoded", e); } }
220
- else throw E("bad-input", "a signer key must be a CryptoKey, a PKCS#8 DER Buffer, or a PKCS#8 PEM string");
221
- return subtle.importKey("pkcs8", der, imp, false, ["sign"]);
223
+ else if (key instanceof Uint8Array) { der = Buffer.from(key); owned = true; }
224
+ else if (typeof key === "string") {
225
+ try { der = pkcs8.pemDecode(key); }
226
+ catch (e) { throw E("bad-input", "the signer PEM private key could not be decoded", e); }
227
+ owned = true;
228
+ } else throw E("bad-input", "a signer key must be a CryptoKey, a PKCS#8 DER Buffer, or a PKCS#8 PEM string");
229
+ var imported = subtle.importKey("pkcs8", der, imp, false, ["sign"]);
230
+ if (!owned) return imported;
231
+ // Wiped on the reject path too: a malformed key is not a way to leave the copy in memory.
232
+ return imported.then(function (k) {
233
+ guard.secret.zeroize(der, E, "bad-input", "the signer private-key copy");
234
+ return k;
235
+ }, function (e) {
236
+ guard.secret.zeroize(der, E, "bad-input", "the signer private-key copy");
237
+ throw e;
238
+ });
222
239
  }
223
240
 
224
241
  // signOverTbs(scheme, key, signedBytes, E) -> Promise<Buffer> the raw signature over signedBytes.
package/lib/smime.js CHANGED
@@ -44,6 +44,43 @@ var SmimeError = frameworkError.SmimeError;
44
44
 
45
45
  function _err(code, msg, cause) { return new SmimeError(code, msg, cause); }
46
46
 
47
+ // ---- the option surface each verb accepts -----------------------------------
48
+ //
49
+ // A misspelled option is the one input that reads as an omission rather than as a value: nothing is
50
+ // out of range, nothing fails to parse, and the caller who asked for something stricter silently
51
+ // gets the looser default. `protectHeaders` misspelled sends the headers a caller meant to protect
52
+ // as ordinary display copies; `strictMicalg` misspelled accepts the mismatch it was set to reject;
53
+ // `entity` misspelled wraps a caller's complete MIME entity inside another one.
54
+ //
55
+ // The tables are per VERB, not per module, because the surfaces genuinely differ -- `form` means
56
+ // something on sign and nothing on encrypt -- and a merged table would accept each verb's options at
57
+ // every other one, which is the same silence in a wider form. Each is the keys that verb's body and
58
+ // the helpers it hands `opts` to actually read: the value goes through _entityBytes, _cmsSignOpts or
59
+ // _cmsEncryptOpts as readily as it is read here, so a table built from the verb's own lines alone
60
+ // would refuse options that work.
61
+ var SIGN_OPTS = {
62
+ form: 1, entity: 1, contentType: 1, signingTime: 1, protectHeaders: 1, headers: 1, hcp: 1,
63
+ sid: 1, signedAttributes: 1, additionalSignedAttributes: 1,
64
+ };
65
+ var VERIFY_OPTS = {
66
+ certs: 1, trustAnchors: 1, time: 1, requiredEku: 1, checkPurpose: 1, strictMicalg: 1,
67
+ legacyHeaderProtection: 1,
68
+ };
69
+ var ENCRYPT_OPTS = {
70
+ entity: 1, contentType: 1, protectHeaders: 1, headers: 1, hcp: 1,
71
+ contentEncryptionAlgorithm: 1, oaepHash: 1, keyIdentifier: 1, ukm: 1,
72
+ };
73
+ var DECRYPT_OPTS = { recipientIndex: 1, maxIterations: 1, strictSmimeType: 1, legacyHeaderProtection: 1 };
74
+ var COMPRESS_OPTS = { entity: 1, contentType: 1, level: 1 };
75
+ var DECOMPRESS_OPTS = { maxOutputBytes: 1 };
76
+
77
+ function _knownOpts(opts, known, verb) {
78
+ guard.identifier.assertKnownKeys(opts, known, _err, "smime/bad-input", function (k) {
79
+ return "unknown option " + JSON.stringify(k) + " for pki.smime." + verb + " -- accepted: " +
80
+ Object.keys(known).sort().join(", ");
81
+ });
82
+ }
83
+
47
84
  // RFC 8551 uses application/pkcs7-<kind>; OpenSSL's legacy `smime` command emits the PKCS#7
48
85
  // application/x-pkcs7-<kind>. Accept both on the RECEIVE side (we always EMIT the RFC 8551 form).
49
86
  function _isPkcs7(type, kind) {
@@ -520,6 +557,10 @@ function _base64Body(der) {
520
557
  * @opts signingTime a `Date` for the CMS signing-time attribute, or false to omit it.
521
558
  * @opts protectHeaders enable RFC 9788 header protection (`hp="clear"`) -- inline `opts.headers` on the signed payload + the outer display headers.
522
559
  * @opts headers the Non-Structural fields to protect + display: an object `{ Name: value }` or an array `[{ name, value }]` (Subject / From / To / Date / ...); used with `protectHeaders`.
560
+ * @opts hcp the Header Confidentiality Policy applied to the OUTER display copies: `"hcp_baseline"` (default) or `"hcp_no_confidentiality"`. A signed message's payload is not encrypted, so this governs presentation, not secrecy; used with `protectHeaders`.
561
+ * @opts sid forwarded to cms.sign: the SignerIdentifier form, `"issuerAndSerial"` (default) or `"subjectKeyIdentifier"`.
562
+ * @opts signedAttributes forwarded: `false` omits the signed-attributes set entirely (a bare-signature SignedData).
563
+ * @opts additionalSignedAttributes forwarded: extra signed attributes to carry, as `[{ oid, values }]`.
523
564
  * @example
524
565
  * var pair = await pki.key.generate("Ed25519");
525
566
  * var signerKeyPkcs8 = await pki.key.export(pair.privateKey);
@@ -529,6 +570,7 @@ function _base64Body(der) {
529
570
  */
530
571
  async function sign(content, signers, opts) {
531
572
  opts = opts || {};
573
+ _knownOpts(opts, SIGN_OPTS, "sign");
532
574
  // RFC 9788 header protection (opts.protectHeaders): the inner Cryptographic Payload gains hp="clear" +
533
575
  // the inlined protected fields, and the outer frame carries the display copies. Off by default => the
534
576
  // shipped path, byte-for-byte.
@@ -624,6 +666,7 @@ function _capped(msg) {
624
666
  */
625
667
  async function verify(message, opts) {
626
668
  opts = opts || {};
669
+ _knownOpts(opts, VERIFY_OPTS, "verify");
627
670
  var ent = mime.parse(message, SmimeError, "smime/bad-mime");
628
671
  var ct = ent.contentType;
629
672
  // Forwarded, not re-decided here. This verb documents itself as pki.cms.verify's verdict plus the
@@ -762,6 +805,7 @@ function _cmsEncryptOpts(opts) {
762
805
  */
763
806
  async function encrypt(content, recipients, opts) {
764
807
  opts = opts || {};
808
+ _knownOpts(opts, ENCRYPT_OPTS, "encrypt");
765
809
  // RFC 9788 header protection (opts.protectHeaders): the inner payload carries hp="cipher" + the REAL
766
810
  // headers (inside the ciphertext); the outer frame carries only the Header-Confidentiality-Policy-processed
767
811
  // display copies (hcp_baseline obscures Subject to [...], removes Comments/Keywords). Off => shipped path.
@@ -824,6 +868,7 @@ async function encrypt(content, recipients, opts) {
824
868
  */
825
869
  async function decrypt(message, keyMaterial, opts) {
826
870
  opts = opts || {};
871
+ _knownOpts(opts, DECRYPT_OPTS, "decrypt");
827
872
  var ent = mime.parse(message, SmimeError, "smime/bad-mime");
828
873
  var ct = ent.contentType;
829
874
  if (!_isPkcs7(ct.type, "mime")) throw _err("smime/unsupported-type", "not an encrypted S/MIME message (Content-Type " + JSON.stringify(ct.type) + ")");
@@ -870,6 +915,7 @@ async function decrypt(message, keyMaterial, opts) {
870
915
  */
871
916
  async function compress(content, opts) {
872
917
  opts = opts || {};
918
+ _knownOpts(opts, COMPRESS_OPTS, "compress");
873
919
  var entity = _entityBytes(content, opts);
874
920
  var cOpts = {};
875
921
  if (opts.level !== undefined) cOpts.level = opts.level;
@@ -903,6 +949,7 @@ async function compress(content, opts) {
903
949
  */
904
950
  async function decompress(message, opts) {
905
951
  opts = opts || {};
952
+ _knownOpts(opts, DECOMPRESS_OPTS, "decompress");
906
953
  var ent = mime.parse(message, SmimeError, "smime/bad-mime");
907
954
  var ct = ent.contentType;
908
955
  if (!_isPkcs7(ct.type, "mime")) throw _err("smime/unsupported-type", "not a compressed S/MIME message (Content-Type " + JSON.stringify(ct.type) + ")");