@blamejs/pki 0.6.50 → 0.6.52

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.
@@ -193,6 +193,22 @@ function isRecordedAsProduced(obj) {
193
193
  return true;
194
194
  }
195
195
 
196
+ /**
197
+ * The bytes a recorded parse result came from, for a consumer that has to hand DER on to a verb taking
198
+ * bytes rather than a parsed object. A copy, so the caller cannot reach the recorded source.
199
+ *
200
+ * Null for anything not recorded, and for a result whose fields no longer match what the parse
201
+ * produced: the bytes are the truth, so handing them on for an object a caller has since edited would
202
+ * silently discard the edit and forward something the caller did not mean.
203
+ *
204
+ * @enforced-by behavioral -- an accessor has no rename-proof shape to detect; its contract is pinned by
205
+ * the vectors that drive it on an unrecorded object, a recorded one, and an edited one.
206
+ */
207
+ function sourceOf(obj) {
208
+ if (!isRecordedAsProduced(obj)) return null;
209
+ return _bufferFrom(_weakGet(PROVENANCE, obj).source);
210
+ }
211
+
196
212
  function _shapeOf(obj) {
197
213
  var shape = _create(null);
198
214
  var keys = _allNames(obj);
@@ -278,5 +294,5 @@ module.exports = _freezeExports({
278
294
  fromTrustedSource: fromTrustedSource, recordingParser: recordingParser,
279
295
  recordingWalker: recordingWalker,
280
296
  isCert: certShape, isCrl: crlShape, isRecorded: isRecorded,
281
- isRecordedAsProduced: isRecordedAsProduced,
297
+ isRecordedAsProduced: isRecordedAsProduced, sourceOf: sourceOf,
282
298
  });
package/lib/key.js CHANGED
@@ -340,6 +340,164 @@ async function publicFromPrivate(privateKey, opts) {
340
340
  return opts.pem ? pkix.pemEncode(spki, "PUBLIC KEY", PemError) : spki;
341
341
  }
342
342
 
343
+ /**
344
+ * @internal Whether a private key and a SubjectPublicKeyInfo are the two halves of one key pair, for a
345
+ * caller deciding whether a private key it was handed goes with a certificate it was handed.
346
+ *
347
+ * Deriving the public key and comparing does NOT answer this. A key structure states its own public
348
+ * half and the key engine reads what it is told: RFC 5958 sec. 2 gives a OneAsymmetricKey an optional
349
+ * `publicKey`, an EC key's RFC 5915 `ECPrivateKey` carries its own public point, and an RSA private
350
+ * key carries the modulus and public exponent outright. A structure whose private components were
351
+ * replaced while its stored public ones were left alone therefore derives to exactly the public key
352
+ * that was planted in it, and the comparison accepts a private key that cannot use the certificate.
353
+ *
354
+ * So the pair is proven by USING it, on whichever operation the key type can perform: a signature the
355
+ * public half verifies, a Diffie-Hellman agreement that reaches the same secret from both sides, or a
356
+ * key encapsulation the private half decapsulates to the same secret. Each fails for a key that merely
357
+ * claims the public half.
358
+ *
359
+ * What a true answer means is exactly that: the private key WORKS with this public key, on the
360
+ * operation its type performs. It is not a statement that every field of the private structure is
361
+ * internally consistent, which no operation reveals. A component the algorithm ignores, such as a bit
362
+ * an X25519 scalar clamps away or an ML-KEM implicit-rejection secret, is not exercised by a
363
+ * correspondence that succeeds, and does not stop the key from being usable with the certificate.
364
+ * A caller needing structural consistency has a different question, and this is not it.
365
+ */
366
+ var _PAIR_DIGEST = Object.assign(Object.create(null), { rsa: "sha256", ec: "sha256" });
367
+ var _PAIR_AGREE = Object.assign(Object.create(null), { x25519: 1, x448: 1 });
368
+ var _PAIR_KEM = Object.assign(Object.create(null),
369
+ { "ml-kem-512": 1, "ml-kem-768": 1, "ml-kem-1024": 1 });
370
+ /** @internal RFC 4055 sec. 1.2: ONE RSA key pair may be identified either by `rsaEncryption` or by
371
+ * `id-RSASSA-PSS`, and a delivered private key and the certificate that certifies it need not have
372
+ * chosen the same one. The key engine reports those two encodings as different types, so comparing
373
+ * types alone would call a usable pair no pair at all. They name one algorithm and are exercised
374
+ * together, under PSS, which is the padding the restricted encoding permits. */
375
+ var _PAIR_RSA = Object.assign(Object.create(null), { rsa: 1, "rsa-pss": 1 });
376
+ var _PAIR_PROBE = Buffer.from("pki.js key pair correspondence", "utf8");
377
+
378
+ function _sameKeyFamily(priv, pub) {
379
+ if (priv.asymmetricKeyType === pub.asymmetricKeyType) return true;
380
+ return !!(_PAIR_RSA[guard.text.keyOf(priv.asymmetricKeyType)] &&
381
+ _PAIR_RSA[guard.text.keyOf(pub.asymmetricKeyType)]);
382
+ }
383
+
384
+ /** @internal How to exercise a signing pair: the digest, and any padding the encoding requires. An
385
+ * `id-RSASSA-PSS` key may pin the hash and salt length it is usable with, and the engine refuses any
386
+ * other, so a pinned value from either half is what proves the pair. */
387
+ function _pairSignOptions(priv, pub) {
388
+ var privPss = priv.asymmetricKeyType === "rsa-pss", pubPss = pub.asymmetricKeyType === "rsa-pss";
389
+ if (!privPss && !pubPss) {
390
+ return { digest: _PAIR_DIGEST[guard.text.keyOf(priv.asymmetricKeyType)] || null, options: null };
391
+ }
392
+ var privDetails = _pssRestrictionOf(priv, privPss);
393
+ var pubDetails = _pssRestrictionOf(pub, pubPss);
394
+ /** @internal A half that pins nothing constrains nothing, so only two halves that BOTH pin can
395
+ * disagree. Both encodings pinning incompatible parameters describes no operation this pair can
396
+ * perform together, which is not the same as being different keys: it is reported as unexercisable
397
+ * rather than as a false "not a pair". Where one pins and the other does not, the pinned half
398
+ * supplies the parameters below and the engine enforces it against its own key. */
399
+ if (_pssConflict(privDetails, pubDetails)) return null;
400
+ var digest = privDetails.hashAlgorithm || pubDetails.hashAlgorithm || "sha256";
401
+ /** @internal The engine derives MGF1 from the digest and takes no separate option for it, so a half
402
+ * that pins a mask generator other than its own hash names an operation this pair cannot perform
403
+ * together. That is the same "cannot be decided" verdict two conflicting halves get, not a false
404
+ * report that the halves are different keys. */
405
+ var mgf1 = privDetails.mgf1HashAlgorithm || pubDetails.mgf1HashAlgorithm;
406
+ if (mgf1 != null && mgf1 !== digest) return null;
407
+ var salt = _pssSalt(privDetails.saltLength, pubDetails.saltLength);
408
+ /** @internal Null-prototype, because the engine READS this object by name. A plain object would
409
+ * let an inherited `saltLength` reach the signature as a restriction neither half stated. */
410
+ var options = Object.create(null);
411
+ options.padding = nodeCrypto.constants.RSA_PKCS1_PSS_PADDING;
412
+ if (salt != null) options.saltLength = salt;
413
+ return { digest: digest, options: options };
414
+ }
415
+
416
+ var _PSS_PINS = ["hashAlgorithm", "mgf1HashAlgorithm", "saltLength"];
417
+
418
+ /** @internal What a half pins, as a null-prototype record carrying only what the key itself states.
419
+ * A half pins nothing when it is not an id-RSASSA-PSS key at all, and equally when it is one whose
420
+ * parameters are absent, which RFC 4055 sec. 3.1 leaves optional. Reading either through a plain
421
+ * object would let an inherited `saltLength` or `hashAlgorithm` answer as a restriction the key never
422
+ * carried, and refuse a genuine pair. */
423
+ function _pssRestrictionOf(key, isPss) {
424
+ var out = Object.create(null);
425
+ var details = isPss ? key.asymmetricKeyDetails : null;
426
+ if (details == null) return out;
427
+ for (var i = 0; i < _PSS_PINS.length; i++) {
428
+ var name = _PSS_PINS[i];
429
+ if (Object.prototype.hasOwnProperty.call(details, name) && details[name] != null) out[name] = details[name];
430
+ }
431
+ return out;
432
+ }
433
+
434
+ /** @internal Whether two halves state a restriction that cannot be satisfied at once. RFC 4055
435
+ * sec. 3.3 requires the signature's hash and mask generator to match the key's, so two halves naming
436
+ * different ones describe no shared operation. It treats the salt length differently: "The saltLength
437
+ * field in the signature parameters MUST be greater or equal to that in the key parameters field",
438
+ * and sec. 3.1 says the field "does not need to be fixed for a given RSA key pair". Two halves naming
439
+ * different salt lengths are therefore both satisfied by the larger, and do not conflict. A field
440
+ * only one half states constrains only that half, so it cannot disagree with anything. */
441
+ function _pssConflict(a, b) {
442
+ if (a.hashAlgorithm != null && b.hashAlgorithm != null && a.hashAlgorithm !== b.hashAlgorithm) return true;
443
+ if (a.mgf1HashAlgorithm != null && b.mgf1HashAlgorithm != null && a.mgf1HashAlgorithm !== b.mgf1HashAlgorithm) return true;
444
+ return false;
445
+ }
446
+
447
+ /** @internal The smallest salt length both halves accept, which is the larger of the two floors. */
448
+ function _pssSalt(a, b) {
449
+ if (a == null) return b;
450
+ if (b == null) return a;
451
+ return a > b ? a : b;
452
+ }
453
+
454
+ async function correspondsTo(privateKey, spkiDer) {
455
+ var privDer = await _toPrivateKeyDer(privateKey);
456
+ var priv, pub;
457
+ try { priv = nodeCrypto.createPrivateKey({ key: privDer, format: "der", type: "pkcs8" }); }
458
+ catch (e) { throw _err("key/bad-input", "the private key could not be read", e); }
459
+ try { pub = nodeCrypto.createPublicKey({ key: Buffer.from(spkiDer), format: "der", type: "spki" }); }
460
+ catch (e) { throw _err("key/bad-input", "the public key could not be read", e); }
461
+ if (!_sameKeyFamily(priv, pub)) return false;
462
+ var kind = guard.text.keyOf(priv.asymmetricKeyType);
463
+ try {
464
+ if (_PAIR_KEM[kind]) {
465
+ var sealed = nodeCrypto.encapsulate(pub);
466
+ return guard.crypto.constantTimeEqual(nodeCrypto.decapsulate(priv, sealed.ciphertext), sealed.sharedKey);
467
+ }
468
+ if (_PAIR_AGREE[kind]) {
469
+ var ephemeral = nodeCrypto.generateKeyPairSync(priv.asymmetricKeyType);
470
+ if (!guard.crypto.constantTimeEqual(
471
+ nodeCrypto.diffieHellman({ privateKey: priv, publicKey: ephemeral.publicKey }),
472
+ nodeCrypto.diffieHellman({ privateKey: ephemeral.privateKey, publicKey: pub }))) return false;
473
+ /** @internal The agreement proves the SCALAR, not the encoding it was handed. RFC 7748 masks the
474
+ * top bit of an X25519 u-coordinate before the ladder, so a public value differing from the real
475
+ * one only in a masked bit reaches the same secret from both sides. The certificate would then
476
+ * carry bytes this key does not produce, and its subjectKeyIdentifier and any pinning of it
477
+ * would name a key the holder does not have. These types store no public copy in the private
478
+ * structure for an attacker to plant, so what the scalar generates IS the canonical answer and
479
+ * comparing against it adds a bound the agreement alone does not give. */
480
+ return guard.crypto.constantTimeEqual(
481
+ nodeCrypto.createPublicKey(priv).export({ format: "der", type: "spki" }),
482
+ pub.export({ format: "der", type: "spki" }));
483
+ }
484
+ var how = _pairSignOptions(priv, pub);
485
+ if (how === null) {
486
+ throw _err("key/unsupported-algorithm", "the two halves pin incompatible RSASSA-PSS parameters, so no operation exercises them together and whether they are a pair cannot be decided");
487
+ }
488
+ var signKey = how.options ? Object.assign(Object.create(null), { key: priv }, how.options) : priv;
489
+ var verifyKey = how.options ? Object.assign(Object.create(null), { key: pub }, how.options) : pub;
490
+ return nodeCrypto.verify(how.digest, _PAIR_PROBE, verifyKey,
491
+ nodeCrypto.sign(how.digest, _PAIR_PROBE, signKey)) === true;
492
+ } catch (e) {
493
+ /** @internal The incompatible-parameters verdict is decided rather than caught, so it is passed
494
+ * through with the reason it was raised for. Rewrapping it would tell an operator the pair could
495
+ * not be exercised without saying that the two encodings pin conflicting parameters. */
496
+ if (e && e.code === "key/unsupported-algorithm" && e.isPkiError === true) throw e;
497
+ throw _err("key/unsupported-algorithm", "the key pair could not be exercised to decide whether its two halves correspond", e);
498
+ }
499
+ }
500
+
343
501
 
344
502
  function _detectKeyInput(input) {
345
503
  var der = pkix.coerceToDer(input, { pemLabel: null, PemError: PemError, ErrorClass: KeyError, prefix: "key" });
@@ -397,4 +555,5 @@ module.exports = {
397
555
  import: import_,
398
556
  generate: generate,
399
557
  publicFromPrivate: publicFromPrivate,
558
+ correspondsTo: correspondsTo, // @internal
400
559
  };
package/lib/oid.js CHANGED
@@ -174,6 +174,8 @@ var FAMILIES = Object.assign(Object.create(null), {
174
174
  countersignature: 6, challengePassword: 7, extensionRequest: 14,
175
175
  smimeCapabilities: 15, friendlyName: 20, localKeyId: 21 } },
176
176
 
177
+ keyPackageContentTypes: { base: [2, 16, 840, 1, 101, 2, 1, 2, 78], of: { aKeyPackage: 5 } },
178
+
177
179
  pkcs9CertTypes: { base: [1, 2, 840, 113549, 1, 9, 22], of: { x509Certificate: 1, sdsiCertificate: 2 } },
178
180
  pkcs9CrlTypes: { base: [1, 2, 840, 113549, 1, 9, 23], of: { x509CRL: 1 } },
179
181
 
@@ -486,8 +486,16 @@ function ipMatch(constraint, addr) {
486
486
  return true;
487
487
  }
488
488
 
489
+ /** @internal The one place a constraint base meets a name, so the base's form is read here and
490
+ * nowhere else. A caller's base met `guard.name.constraintBaseRefusal` at the door; a base a
491
+ * certificate carries in its own nameConstraints extension met no door, and the same rule decides it
492
+ * here. Answering a base outside its tag's form with no match rather than no verdict would make an
493
+ * excluded subtree that covers nothing exclude nothing. */
489
494
  function nameMatchesConstraint(gnTag, gnValue, base) {
490
495
  if (base.tagNumber !== gnTag) return null;
496
+ if (gnTag === 1 || gnTag === 2 || gnTag === 6) {
497
+ if (guard.name.constraintBaseUnreadable(gnTag, base.value)) return "unsupported";
498
+ }
491
499
  switch (gnTag) {
492
500
  case 1: return emailMatch(base.value, gnValue);
493
501
  case 2: return hostConstraintMatch(base.value, gnValue);
package/lib/schema-cmp.js CHANGED
@@ -359,7 +359,7 @@ var ENCRYPTED_KEY = schema.choice([
359
359
  if (ct === null || ct.length === 0) {
360
360
  throw ctx.E("cmp/bad-cert-response", "an EncryptedKey envelopedData must carry non-empty attached ciphertext -- its encryptedContent is the certificate/key payload (RFC 9810 sec. 5.2.2)");
361
361
  }
362
- return { envelopedData: env };
362
+ return { envelopedData: env, bytes: asn1.sequenceTlv(n) };
363
363
  }) },
364
364
  ], { code: "cmp/bad-cert-response", what: "EncryptedKey" });
365
365
 
@@ -539,6 +539,44 @@ function keyUsageOf(ns, cert, E, code, label) {
539
539
  return null;
540
540
  }
541
541
 
542
+ /** @internal The extendedKeyUsage purposes a certificate asserts, or null when it carries no such
543
+ * extension. A malformed extension throws through the caller's ErrorClass; it never reads as an
544
+ * empty set. */
545
+ var _EKU_DECODER = new _WeakMapI();
546
+ function extKeyUsageOf(ns, cert, E, code, label) {
547
+ var exts = (cert && cert.extensions) || [];
548
+ var want = ns.oid.byName("extKeyUsage");
549
+ for (var i = 0; i < exts.length; i++) {
550
+ if (exts[i].oid !== want || exts[i].value == null) continue;
551
+ var dec = _weakGet(_EKU_DECODER, ns);
552
+ if (!dec) { dec = certExtensionDecoders(ns).byOid[want]; _weakSet(_EKU_DECODER, ns, dec); }
553
+ try { return dec(exts[i].value); }
554
+ catch (e) { throw E(code, "the " + label + " extendedKeyUsage extension is malformed", e); }
555
+ }
556
+ return null;
557
+ }
558
+
559
+ /**
560
+ * @internal Whether a certificate ASSERTS a key purpose, for a rule that requires the assertion to be
561
+ * present. RFC 5280 sec. 4.2.1.12 constrains a certificate only when the extension is there, so an
562
+ * absent extendedKeyUsage permits every purpose and satisfies `pki.path.validate`'s `requiredEku`. A
563
+ * profile that reads the purpose as an authorization statement (RFC 6960's id-kp-OCSPSigning
564
+ * responder, RFC 9483 sec. 4.1.6's id-kp-cmKGA key generation authority) needs the opposite reading:
565
+ * the certificate must say so. This returns false for an absent, empty, malformed, or
566
+ * differently-purposed extension, and true only when the named purpose is listed.
567
+ *
568
+ * @enforced-by behavioral -- an absent extension answering an authorization question has no
569
+ * rename-proof code shape; each consumer's RED conformance vector drives the shipped verb on a
570
+ * certificate with no extendedKeyUsage and asserts the refusal.
571
+ */
572
+ function assertsPurpose(ns, cert, purposeOid) {
573
+ var purposes;
574
+ try { purposes = extKeyUsageOf(ns, cert, ns.E, ns.prefix + "/bad-extension-value", "certificate"); }
575
+ catch (_e) { return false; }
576
+ if (!purposes) return false;
577
+ return guard.list.contains(purposes, purposeOid);
578
+ }
579
+
542
580
  function issuingDistributionPoint(code) {
543
581
  return schema.seq([
544
582
  schema.trailing([
@@ -676,7 +714,7 @@ function certExtensionDecoders(ns) {
676
714
  for (var z = 0; z < bs.bytes.length; z++) { if (bs.bytes[z] !== 0) { anyBit = true; break; } }
677
715
  if (!anyBit) throw ns.E(C, "KeyUsage must assert at least one bit (RFC 5280 sec. 4.2.1.3)");
678
716
  schema.assertMinimalNamedBits(bs.unusedBits, bs.bytes, function (m) { throw ns.E(C, m); });
679
- // allow:registry-table-inherits-object-prototype -- the decoded KeyUsage record, whose keys are the fixed KU_BITS names the schema declares rather than anything the certificate supplies
717
+ // allow:registry-table-inherits-object-prototype -- the decoded KeyUsage record, whose keys are the fixed KU_BITS names the schema declares; the certificate supplies none of them
680
718
  var out = {};
681
719
  _forEach(KU_BITS, function (nm, bit) {
682
720
  var byte = bit >> 3, mask = 0x80 >> (bit & 7);
@@ -822,7 +860,7 @@ function certExtensionDecoders(ns) {
822
860
  function authorityKeyIdentifier(buf) {
823
861
  var C = ns.prefix + "/bad-extension-value";
824
862
  var kids = seqChildren(buf, C, "AuthorityKeyIdentifier");
825
- // allow:registry-table-inherits-object-prototype -- the decoded AuthorityKeyIdentifier record, read by the three field names the schema declares rather than by anything the certificate supplies
863
+ // allow:registry-table-inherits-object-prototype -- the decoded AuthorityKeyIdentifier record, read by the three field names the schema declares; the certificate supplies none of them
826
864
  var out = { keyIdentifier: null, authorityCertIssuer: null, authorityCertSerialNumber: null };
827
865
  var lastTag = -1;
828
866
  _forEach(kids, function (k) {
@@ -869,7 +907,7 @@ function certExtensionDecoders(ns) {
869
907
  if (dp.tagClass !== "universal" || dp.tagNumber !== _T.SEQUENCE || !dp.children) {
870
908
  throw ns.E(C, "DistributionPoint must be a SEQUENCE { distributionPoint?, reasons?, cRLIssuer? }");
871
909
  }
872
- // allow:registry-table-inherits-object-prototype -- the decoded DistributionPoint record, read by the three field names the schema declares rather than by anything the certificate supplies
910
+ // allow:registry-table-inherits-object-prototype -- the decoded DistributionPoint record, read by the three field names the schema declares; the certificate supplies none of them
873
911
  var out = { distributionPoint: null, reasons: null, cRLIssuer: null };
874
912
  var lastTag = -1;
875
913
  _forEach(dp.children, function (f) {
@@ -1247,6 +1285,8 @@ module.exports = {
1247
1285
  distributionPointName: distributionPointName,
1248
1286
  issuingDistributionPoint: issuingDistributionPoint,
1249
1287
  keyUsageOf: keyUsageOf,
1288
+ extKeyUsageOf: extKeyUsageOf,
1289
+ assertsPurpose: assertsPurpose,
1250
1290
  generalizedTime: generalizedTime,
1251
1291
  utf8Text: utf8Text,
1252
1292
  rawNonEmptySequence: rawNonEmptySequence,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blamejs/pki",
3
- "version": "0.6.50",
3
+ "version": "0.6.52",
4
4
  "description": "Pure-JavaScript PKI toolkit that owns its stack — X.509, ASN.1/DER, CMS, PQC-first.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "blamejs contributors",
package/sbom.cdx.json CHANGED
@@ -2,10 +2,10 @@
2
2
  "$schema": "http://cyclonedx.org/schema/bom-1.5.schema.json",
3
3
  "bomFormat": "CycloneDX",
4
4
  "specVersion": "1.5",
5
- "serialNumber": "urn:uuid:e7a3f132-aaea-4bc5-ae0f-387a72dc5e4f",
5
+ "serialNumber": "urn:uuid:5de9cc32-97b6-4f34-a868-7ad628091d11",
6
6
  "version": 1,
7
7
  "metadata": {
8
- "timestamp": "2026-09-07T05:02:17.771Z",
8
+ "timestamp": "2026-09-07T15:01:14.383Z",
9
9
  "lifecycles": [
10
10
  {
11
11
  "phase": "build"
@@ -19,14 +19,14 @@
19
19
  }
20
20
  ],
21
21
  "component": {
22
- "bom-ref": "@blamejs/pki@0.6.50",
22
+ "bom-ref": "@blamejs/pki@0.6.52",
23
23
  "type": "application",
24
24
  "name": "pki",
25
- "version": "0.6.50",
25
+ "version": "0.6.52",
26
26
  "scope": "required",
27
27
  "author": "blamejs contributors",
28
28
  "description": "Pure-JavaScript PKI toolkit that owns its stack — X.509, ASN.1/DER, CMS, PQC-first.",
29
- "purl": "pkg:npm/%40blamejs/pki@0.6.50",
29
+ "purl": "pkg:npm/%40blamejs/pki@0.6.52",
30
30
  "properties": [],
31
31
  "externalReferences": [
32
32
  {
@@ -54,7 +54,7 @@
54
54
  "components": [],
55
55
  "dependencies": [
56
56
  {
57
- "ref": "@blamejs/pki@0.6.50",
57
+ "ref": "@blamejs/pki@0.6.52",
58
58
  "dependsOn": []
59
59
  }
60
60
  ]