@blamejs/pki 0.7.12 → 0.7.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,31 @@ All notable changes to `@blamejs/pki` are documented here. The format
4
4
  follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); this
5
5
  project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
+ ## v0.7.14 — 2026-09-11
8
+
9
+ Every option a verify verb decides with is taken once.
10
+
11
+ ### Changed
12
+
13
+ - The pki.smime verbs take their options once at entry. sign, verify, encrypt, decrypt, compress, decompress and buildCertsOnly share one option-checking helper, and it now returns the options it checked rather than leaving the caller's object to be read again by each later pass.
14
+
15
+ ### Security
16
+
17
+ - pki.jose.verify took opts.key seven times: a type gate, the RFC 7638 thumbprint comparison against the jwk the JWS embeds, and the key the signature is verified under. An accessor-backed opts.key could satisfy the comparison with the embedded key and hand a different one to the verification, so a JWS naming a signer the caller did not pin returned a verdict of keySource "opts.key" instead of the jose/key-mismatch refusal a plain object gets. The option is taken once, so the key compared is the key used.
18
+ - pki.webauthn.parseClientData took each expected* option twice: once to decide whether to compare, once for the value compared, and the returned checked field then recorded that the comparison ran. An accessor-backed expectedChallenge could enter the branch on the challenge a ceremony issued and be compared against the challenge the response carried, returning checked.challenge true for a comparison that never ran against the caller's value. expectedType and expectedOrigin behaved the same way. Each option is taken once, so checked answers for the value the caller supplied. pki.webauthn.verify and pki.webauthn.verifyAssertion already copied their inputs and were not affected.
19
+
20
+ ## v0.7.13 — 2026-09-10
21
+
22
+ The certificate signer refuses a pre-encoded extension whose criticality RFC 5280 fixes.
23
+
24
+ ### Changed
25
+
26
+ - A call that supplied one of the nine fixed-criticality extensions with the wrong flag now throws where it previously produced a certificate or a request. Set the flag the clause requires: omit the critical BOOLEAN for the non-critical set, and encode it TRUE for the critical set. The extensions and extensionRequest object forms already emitted the correct flag and are unaffected, as are the CRL and attribute-certificate signers, which enforce the criticality their own profiles fix.
27
+
28
+ ### Fixed
29
+
30
+ - pki.x509.sign and pki.csr.sign now refuse a pre-encoded extension carrying a criticality RFC 5280 does not permit, throwing x509/bad-input or csr/bad-input with the governing clause in the message. Nine extensions leave the issuer no choice. authorityKeyIdentifier (section 4.2.1.1), subjectKeyIdentifier (section 4.2.1.2), subjectDirectoryAttributes (section 4.2.1.8), freshestCRL (section 4.2.1.15), authorityInfoAccess (section 4.2.2.1) and subjectInfoAccess (section 4.2.2.2) must be non-critical. nameConstraints (section 4.2.1.10), policyConstraints (section 4.2.1.11) and inhibitAnyPolicy (section 4.2.1.14) must be critical. The check covers the pre-encoded array form on both verbs, which is the only way to supply these six extensions.
31
+
7
32
  ## v0.7.12 — 2026-09-10
8
33
 
9
34
  An issuer can draw a certificate serial number before signing with it.
package/lib/jose.js CHANGED
@@ -319,6 +319,11 @@ var _VERIFY_KEYS = intrinsic.assign(intrinsic.create(null), { key: 1, profile: 1
319
319
  async function verify(jws, opts) {
320
320
  opts = opts || {};
321
321
  guard.identifier.assertKnownKeys(opts, _VERIFY_KEYS, E, "jose/bad-input", "unknown pki.jose.verify option ");
322
+ /** @internal opts.key is examined several times: the type gate, the RFC 7638 thumbprint comparison
323
+ * against the embedded jwk, and the key the signature is verified under. An option reached through
324
+ * an accessor answers each read separately, so the key the comparison ran on need not be the key
325
+ * the verification used. Take each option once. */
326
+ opts = guard.identifier.snapshotOptions(opts, _VERIFY_KEYS);
322
327
  if (!jws || typeof jws !== "object" || _isArray(jws)) throw E("jose/bad-jws", "a flattened JWS object is required");
323
328
  if (_hasOwn(jws, "signatures")) throw E("jose/bad-jws", "the multi-signature signatures member is forbidden");
324
329
  if (_hasOwn(jws, "header")) throw E("jose/bad-jws", "the unprotected header member is forbidden");
package/lib/pki-build.js CHANGED
@@ -273,6 +273,17 @@ function reqDenseArrayImpl(list, what, E, code) {
273
273
  return out;
274
274
  }
275
275
 
276
+ /** @internal RFC 5280 sec. 4.2 fixes the criticality of these certificate extensions. Each entry maps
277
+ * the extension OID to the flag the RFC requires and the clause that sets it. */
278
+ var CERT_FIXED_CRITICALITY = intrinsic.create(null);
279
+ [["authorityKeyIdentifier", false, "RFC 5280 sec. 4.2.1.1"], ["subjectKeyIdentifier", false, "RFC 5280 sec. 4.2.1.2"],
280
+ ["subjectDirectoryAttributes", false, "RFC 5280 sec. 4.2.1.8"], ["nameConstraints", true, "RFC 5280 sec. 4.2.1.10"],
281
+ ["policyConstraints", true, "RFC 5280 sec. 4.2.1.11"], ["inhibitAnyPolicy", true, "RFC 5280 sec. 4.2.1.14"],
282
+ ["freshestCRL", false, "RFC 5280 sec. 4.2.1.15"], ["authorityInfoAccess", false, "RFC 5280 sec. 4.2.2.1"],
283
+ ["subjectInfoAccess", false, "RFC 5280 sec. 4.2.2.2"]].forEach(function (r) {
284
+ CERT_FIXED_CRITICALITY[oid.byName(r[0])] = { critical: r[1], citation: r[2] };
285
+ });
286
+
276
287
  function makeBuilder(ctx) {
277
288
  var ErrorClass = ctx.ErrorClass, O = ctx.O, NS = ctx.NS;
278
289
  var NAME_SCHEMA = ctx.NAME_SCHEMA, SPKI_SCHEMA = ctx.SPKI_SCHEMA;
@@ -498,6 +509,15 @@ function makeBuilder(ctx) {
498
509
  var last = n.children[n.children.length - 1];
499
510
  if (last.tagNumber !== asn1.TAGS.OCTET_STRING || last.tagClass !== "universal") throw E("bad-input", "pre-encoded extension [" + idx + "] extnValue must be an OCTET STRING");
500
511
  }
512
+ /** @internal Applies the certificate profile's fixed-criticality table. The CRL, CRL-entry and
513
+ * attribute-certificate profiles set their own criticality in their own signers. */
514
+ function assertCertCriticality(extnId, isCritical, label) {
515
+ if (!intrinsic.hasOwn(CERT_FIXED_CRITICALITY, extnId)) return;
516
+ var req = CERT_FIXED_CRITICALITY[extnId];
517
+ if (isCritical === req.critical) return;
518
+ throw E("bad-input", label + " " + (oid.name(extnId) || extnId) + " extension must be marked " +
519
+ (req.critical ? "critical" : "non-critical") + " (" + req.citation + ")");
520
+ }
501
521
  var BC_KEYS = { cA: 1, pathLen: 1, critical: 1 };
502
522
  var REQ_EXT_KEYS = {
503
523
  subjectAltName: 1, keyUsage: 1, keyUsageCritical: 1, extendedKeyUsage: 1, extendedKeyUsageCritical: 1,
@@ -515,6 +535,7 @@ function makeBuilder(ctx) {
515
535
  var extnId = asn1.read.oid(n.children[0]);
516
536
  if (seen[extnId]) throw E("bad-input", "duplicate requested extension " + extnId + " (RFC 5280 sec. 4.2)");
517
537
  seen[extnId] = true;
538
+ assertCertCriticality(extnId, n.children.length === 3, "requested");
518
539
  var dec = EXT_DECODERS && EXT_DECODERS[extnId];
519
540
  if (dec) {
520
541
  try { dec(asn1.read.octetString(n.children[n.children.length - 1])); }
@@ -607,6 +628,7 @@ function makeBuilder(ctx) {
607
628
  extSan: extSan, extCertPolicies: extCertPolicies, ext: ext,
608
629
  spkiKeyId: spkiKeyId, skiKeyId: skiKeyId,
609
630
  reqDer: reqDer, reqDenseArray: reqDenseArray, reqDerSequence: reqDerSequence, assertValidSpki: assertValidSpki, assertValidExtension: assertValidExtension,
631
+ assertCertCriticality: assertCertCriticality,
610
632
  certLikeFromSpki: certLikeFromSpki, assertSignatureVerifies: assertSignatureVerifies,
611
633
  samePublicKey: samePublicKey,
612
634
  };
package/lib/smime.js CHANGED
@@ -64,11 +64,15 @@ var DECRYPT_OPTS = Object.assign(Object.create(null), { recipientIndex: 1, maxIt
64
64
  var COMPRESS_OPTS = Object.assign(Object.create(null), { entity: 1, contentType: 1, level: 1 });
65
65
  var DECOMPRESS_OPTS = Object.assign(Object.create(null), { maxOutputBytes: 1 });
66
66
 
67
+ /** @internal Checks the option names and RETURNS the options taken once. A verb reads one option to
68
+ * decide whether a rule applies and reads it again for the value the rule uses, and an option
69
+ * reached through an accessor answers each read separately. Callers assign the return value. */
67
70
  function _knownOpts(opts, known, verb) {
68
71
  guard.identifier.assertKnownKeys(opts, known, _err, "smime/bad-input", function (k) {
69
72
  return "unknown option " + JSON.stringify(k) + " for pki.smime." + verb + " -- accepted: " +
70
73
  Object.keys(known).sort().join(", ");
71
74
  });
75
+ return guard.identifier.snapshotOptions(opts, known);
72
76
  }
73
77
 
74
78
  function _isPkcs7(type, kind) {
@@ -500,7 +504,7 @@ function _base64Body(der) {
500
504
  */
501
505
  async function sign(content, signers, opts) {
502
506
  opts = opts || {};
503
- _knownOpts(opts, SIGN_OPTS, "sign");
507
+ opts = _knownOpts(opts, SIGN_OPTS, "sign");
504
508
  var hp = opts.protectHeaders === true;
505
509
  var hpList = hp ? _hpHeaderList(opts.headers) : null;
506
510
  var outerList = hp ? _outerHeaderList(hpList, "clear", opts.hcp) : null;
@@ -612,7 +616,7 @@ function _capped(msg) {
612
616
  */
613
617
  async function verify(message, opts) {
614
618
  opts = opts || {};
615
- _knownOpts(opts, VERIFY_OPTS, "verify");
619
+ opts = _knownOpts(opts, VERIFY_OPTS, "verify");
616
620
  var ent = mime.parse(message, SmimeError, "smime/bad-mime");
617
621
  var ct = ent.contentType;
618
622
  var vOpts = {};
@@ -720,7 +724,7 @@ function _cmsEncryptOpts(opts) {
720
724
  */
721
725
  async function encrypt(content, recipients, opts) {
722
726
  opts = opts || {};
723
- _knownOpts(opts, ENCRYPT_OPTS, "encrypt");
727
+ opts = _knownOpts(opts, ENCRYPT_OPTS, "encrypt");
724
728
  var hp = opts.protectHeaders === true;
725
729
  var hpList = hp ? _hpHeaderList(opts.headers) : null;
726
730
  var outerList = hp ? _outerHeaderList(hpList, "cipher", opts.hcp) : null;
@@ -776,7 +780,7 @@ async function encrypt(content, recipients, opts) {
776
780
  */
777
781
  async function decrypt(message, keyMaterial, opts) {
778
782
  opts = opts || {};
779
- _knownOpts(opts, DECRYPT_OPTS, "decrypt");
783
+ opts = _knownOpts(opts, DECRYPT_OPTS, "decrypt");
780
784
  var ent = mime.parse(message, SmimeError, "smime/bad-mime");
781
785
  var ct = ent.contentType;
782
786
  if (!_isPkcs7(ct.type, "mime")) throw _err("smime/unsupported-type", "not an encrypted S/MIME message (Content-Type " + JSON.stringify(ct.type) + ")");
@@ -821,7 +825,7 @@ async function decrypt(message, keyMaterial, opts) {
821
825
  */
822
826
  async function compress(content, opts) {
823
827
  opts = opts || {};
824
- _knownOpts(opts, COMPRESS_OPTS, "compress");
828
+ opts = _knownOpts(opts, COMPRESS_OPTS, "compress");
825
829
  var entity = _entityBytes(content, opts);
826
830
  var cOpts = {};
827
831
  if (opts.level !== undefined) cOpts.level = opts.level;
@@ -855,7 +859,7 @@ async function compress(content, opts) {
855
859
  */
856
860
  async function decompress(message, opts) {
857
861
  opts = opts || {};
858
- _knownOpts(opts, DECOMPRESS_OPTS, "decompress");
862
+ opts = _knownOpts(opts, DECOMPRESS_OPTS, "decompress");
859
863
  var ent = mime.parse(message, SmimeError, "smime/bad-mime");
860
864
  var ct = ent.contentType;
861
865
  if (!_isPkcs7(ct.type, "mime")) throw _err("smime/unsupported-type", "not a compressed S/MIME message (Content-Type " + JSON.stringify(ct.type) + ")");
@@ -901,7 +905,7 @@ var CERTS_ONLY_OPTS = Object.assign(Object.create(null), { crls: 1 });
901
905
  */
902
906
  function buildCertsOnly(certs, opts) {
903
907
  opts = opts || {};
904
- _knownOpts(opts, CERTS_ONLY_OPTS, "buildCertsOnly");
908
+ opts = _knownOpts(opts, CERTS_ONLY_OPTS, "buildCertsOnly");
905
909
  var der = _toBuf(cms.certsOnly(certs, { crls: opts.crls }));
906
910
  return _capped(Buffer.concat([_pkcs7MimeHead("certs-only", "smime.p7c"), _base64Body(der), mime.CRLF]));
907
911
  }
package/lib/webauthn.js CHANGED
@@ -1275,6 +1275,11 @@ function parseClientData(bytes, opts) {
1275
1275
  opts = opts || {};
1276
1276
  if (!_isPlainObject(opts)) throw _err("webauthn/bad-input", "opts must be an object");
1277
1277
  guard.identifier.assertKnownKeys(opts, _CLIENT_DATA_OPTS, _err, "webauthn/bad-input", "opts has an unknown key ");
1278
+ /** @internal Each expected* option is read to decide whether to compare and read again for the
1279
+ * value compared, and `checked` then records that the comparison ran. An option reached through an
1280
+ * accessor answers each read separately, so `checked` could describe a comparison against a value
1281
+ * the caller never supplied. Take each option once. */
1282
+ opts = guard.identifier.snapshotOptions(opts, _CLIENT_DATA_OPTS);
1278
1283
  bytes = _bytesArg(bytes, "clientDataJSON");
1279
1284
  var doc = guard.json.parse(Buffer.from(bytes), _err, {
1280
1285
  maxBytes: constants.LIMITS.JSON_MAX_BYTES, maxDepth: constants.LIMITS.JSON_MAX_DEPTH,
package/lib/x509-sign.js CHANGED
@@ -56,6 +56,7 @@ var KNOWN_EXT_KEYS = intrinsic.assign(intrinsic.create(null), {
56
56
  var _b = pkiBuild.makeBuilder({ ErrorClass: CertificateError, prefix: "x509", O: O, NS: NS, NAME_SCHEMA: NAME_SCHEMA, SPKI_SCHEMA: SPKI_SCHEMA, EXT_DECODERS: EXT_DECODERS });
57
57
  var _encodeName = _b.encodeName, _isEmptyName = _b.isEmptyName, _reqDer = _b.reqDer,
58
58
  _assertValidSpki = _b.assertValidSpki, _assertValidExtension = _b.assertValidExtension,
59
+ _assertCertCriticality = _b.assertCertCriticality,
59
60
  _certLikeFromSpki = _b.certLikeFromSpki, _assertCertVerifies = _b.assertSignatureVerifies,
60
61
  _ext = _b.ext, _extBasicConstraints = _b.extBasicConstraints, _validateBcSpec = _b.validateBcSpec,
61
62
  _extKeyUsage = _b.extKeyUsage, _extExtKeyUsage = _b.extExtKeyUsage, _extSki = _b.extSki,
@@ -89,6 +90,7 @@ function _buildExtensions(extSpec, ctx) {
89
90
  var extnId = asn1.read.oid(n.children[0]);
90
91
  if (seenExt[extnId]) throw _err("x509/bad-input", "duplicate extension " + extnId + " in the extensions array (RFC 5280 sec. 4.2 -- at most one instance of an extension)");
91
92
  seenExt[extnId] = true;
93
+ _assertCertCriticality(extnId, n.children.length === 3, "pre-encoded");
92
94
  var dec = EXT_DECODERS[extnId];
93
95
  if (dec) {
94
96
  var decoded;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blamejs/pki",
3
- "version": "0.7.12",
3
+ "version": "0.7.14",
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:4cd44552-2dd6-48c5-b89d-d0f03d957e45",
5
+ "serialNumber": "urn:uuid:bf9440b0-2e9b-45e1-8551-36b17188bd96",
6
6
  "version": 1,
7
7
  "metadata": {
8
- "timestamp": "2026-09-11T00:21:46.779Z",
8
+ "timestamp": "2026-09-11T02:22:59.455Z",
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.7.12",
22
+ "bom-ref": "@blamejs/pki@0.7.14",
23
23
  "type": "application",
24
24
  "name": "pki",
25
- "version": "0.7.12",
25
+ "version": "0.7.14",
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.7.12",
29
+ "purl": "pkg:npm/%40blamejs/pki@0.7.14",
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.7.12",
57
+ "ref": "@blamejs/pki@0.7.14",
58
58
  "dependsOn": []
59
59
  }
60
60
  ]