@blamejs/pki 0.8.9 → 0.8.10

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,19 @@ 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.8.10 — 2026-09-23
8
+
9
+ A certificate's names are read the same way wherever the toolkit reads them.
10
+
11
+ ### Fixed
12
+
13
+ - `pki.tsp.verify` reports `tsp/bad-eku` for an empty extendedKeyUsage. RFC 5280 section 4.2.1.12 has at least one KeyPurposeId, and an empty one decodes as a SEQUENCE, so a reader checking only the tag called it well-formed and reported it as naming the wrong purpose, `tsp/eku-not-exclusive`. Every other consumer of that extension already answered with the malformed-extension code.
14
+ - The subjectAltName a timestamp token names its TSA through is read under the same rules every other consumer reads a name under, so a name this toolkit refuses elsewhere cannot be a source of TSA identity. The match itself is still a comparison of the encoded GeneralName bytes, which is what naming a TSA means: the name in the token and the name in the certificate have to be the same name.
15
+
16
+ ### Security
17
+
18
+ - `pki.sigstore.verifyBundle` refuses a Fulcio certificate whose subjectAltName carries more than one name, whatever forms those names take. It counted only rfc822Name, dNSName, uniformResourceIdentifier and otherName, and dropped every other form before counting, so a certificate pairing a URI with an iPAddress or a directoryName was accepted as binding one identity. A caller identity policy matched the form the verifier reported and never saw the other, which is the outcome the one-identity rule exists to prevent. The rule now counts every name the extension carries. A certificate carrying a single name in a form with no identity string still reports `identity.san` as `null`, unchanged, so a policy gates on the null rather than on a name nothing read.
19
+
7
20
  ## v0.8.9 — 2026-09-23
8
21
 
9
22
  The 0.8.9 tarball is the one to install for everything 0.8.8 added.
package/lib/acme.js CHANGED
@@ -578,7 +578,11 @@ var OID_AKI = oid.byName("authorityKeyIdentifier");
578
578
  var OID_CN = oid.byName("commonName");
579
579
  var _extNs = pkix.makeNS("acme", AcmeError, oid);
580
580
  var _extDecoders = pkix.certExtensionDecoders(_extNs);
581
- var _extCtx = intrinsic.assign(intrinsic.create(null), { E: function (c, m, cause) { return new AcmeError(c, m, cause); }, oid: oid });
581
+ /** @internal The decoders take the value and nothing else: the error class and the OID registry
582
+ * they raise and resolve through come from the namespace the table was built with, above. Passing
583
+ * a second argument here suggested a per-call context that the table has no parameter for and
584
+ * silently dropped; where a decode genuinely needs resources, the way to supply them is to build
585
+ * the table with `schema.withResources`, as `schema-attrcert.js` does. */
582
586
 
583
587
  /**
584
588
  * @primitive pki.acme.tlsAlpn01Extension
@@ -657,7 +661,7 @@ async function verifyTlsAlpn01(certDer, token, accountJwk, identifier) {
657
661
  if (!auth.equals(expected)) throw E("acme/bad-tlsalpn", "the acmeIdentifier digest does not match the key authorization");
658
662
  var sanExt = exts.filter(function (e) { return e.oid === OID_SAN; })[0];
659
663
  if (!sanExt) throw E("acme/bad-tlsalpn", "the validation certificate is missing the SubjectAltName");
660
- var san = _extDecoders.byOid[OID_SAN](sanExt.value, _extCtx);
664
+ var san = _extDecoders.byOid[OID_SAN](sanExt.value);
661
665
  if (!san.names || san.names.length !== 1) throw E("acme/bad-tlsalpn", "the SubjectAltName must carry EXACTLY ONE entry (RFC 8737 sec. 3)");
662
666
  var entry = san.names[0];
663
667
  if (!_isObject(identifier) || !_isString(identifier.type)) throw E("acme/bad-input", "an identifier { type, value } is required");
@@ -914,7 +918,7 @@ function _csrIdentifierSet(parsedCsr) {
914
918
  if (a.type !== oid.byName("extensionRequest") || !_isArrayI(a.extensions)) return;
915
919
  _forEach(a.extensions, function (e) {
916
920
  if (e.oid !== OID_SAN) return;
917
- var dec = _extDecoders.byOid[OID_SAN](e.value, _extCtx);
921
+ var dec = _extDecoders.byOid[OID_SAN](e.value);
918
922
  _forEach(dec.names || [], function (n) { _addSanName(set, n, "the finalize CSR"); });
919
923
  });
920
924
  });
@@ -981,7 +985,7 @@ function _certIdentifierSet(parsedCert) {
981
985
  var set = intrinsic.create(null);
982
986
  _forEach(parsedCert.extensions || [], function (e) {
983
987
  if (e.oid !== OID_SAN) return;
984
- var dec = _extDecoders.byOid[OID_SAN](e.value, _extCtx);
988
+ var dec = _extDecoders.byOid[OID_SAN](e.value);
985
989
  _forEach(dec.names || [], function (n) { _addSanName(set, n, "the downloaded certificate"); });
986
990
  });
987
991
  if (_objectKeys(set).length === 0) {
@@ -1249,7 +1253,7 @@ function ariCertId(certDer) {
1249
1253
  var cert = x509.parse(certDer);
1250
1254
  var akiExt = (cert.extensions || []).filter(function (e) { return e.oid === OID_AKI; })[0];
1251
1255
  if (!akiExt) throw E("acme/bad-certid", "the certificate has no authorityKeyIdentifier extension (RFC 9773 sec. 4.1)");
1252
- var aki = _extDecoders.byOid[OID_AKI](akiExt.value, _extCtx);
1256
+ var aki = _extDecoders.byOid[OID_AKI](akiExt.value);
1253
1257
  if (!aki || !Buffer.isBuffer(aki.keyIdentifier)) throw E("acme/bad-certid", "the authorityKeyIdentifier has no keyIdentifier field (RFC 9773 sec. 4.1)");
1254
1258
  var serialBytes = Buffer.from(cert.serialNumberHex, "hex");
1255
1259
  return jose.base64url.encode(aki.keyIdentifier) + "." + jose.base64url.encode(serialBytes);
package/lib/sigstore.js CHANGED
@@ -1055,6 +1055,13 @@ async function _verifyChain(leaf, chainDers, fulcioRoots, timeMs) {
1055
1055
 
1056
1056
 
1057
1057
  var SAN_OID = oid.byName("subjectAltName");
1058
+ /** @internal The shared table's entry, so the SAN this verb reads is read under the rules every
1059
+ * other consumer of a GeneralNames reads it under. It is built on the certificate namespace rather
1060
+ * than this module's, so a malformed name raises a CertificateError that `_identity` re-types to
1061
+ * `sigstore/bad-certificate` with that fault as its cause. That is the code this module already
1062
+ * reports for a certificate it cannot read, so the fix adds no new code to the public surface. */
1063
+ var _sanDecoder = pkix.certExtensionDecoders(
1064
+ pkix.makeNS("x509", frameworkError.CertificateError, oid)).byOid[SAN_OID];
1058
1065
  var FULCIO_PREFIX = _join(_arraySlice(_split(oid.byName("otherName"), "."), 0, -1), ".") + ".";
1059
1066
  function _identity(leaf) {
1060
1067
  var out = { san: null, extensions: _create(null) };
@@ -1072,29 +1079,36 @@ function _identity(leaf) {
1072
1079
  return out;
1073
1080
  }
1074
1081
  var GN_TYPE = _assign(_create(null), { 1: "rfc822Name", 2: "dNSName", 6: "uri" });
1082
+ /** @internal The GeneralNames come from the shared extension decoder, which is where the RFC 5280
1083
+ * sec. 4.2.1.6 rules live: at least one name, a non-empty IA5String for the text forms, and no
1084
+ * control byte inside one (CVE-2009-2408). Reading the SEQUENCE here instead left this verb
1085
+ * accepting a dNSName carrying a NUL or a CR and handing it on as the signer's identity.
1086
+ *
1087
+ * The one-identity rule counts every name the SAN carries, not the subset this verb turns into an
1088
+ * identity string. Counting only the recognized forms let a certificate pair a URI with an
1089
+ * iPAddress and pass as single-identity, with the second binding invisible to the caller's policy
1090
+ * -- which is the thing the rule exists to stop. A SAN of one name this verb has no string form
1091
+ * for still yields null, so a policy gates on the null rather than on a name nothing read. */
1075
1092
  function _sanValue(ext) {
1076
- var seq = asn1.decode(ext.value);
1077
- var names = [];
1078
- for (var i = 0; i < (seq.children || []).length; i++) {
1079
- var n = seq.children[i];
1080
- if (n.tagClass !== "context") continue;
1081
- var val = null;
1082
- if (!n.constructed && GN_TYPE[n.tagNumber]) {
1083
- val = { type: GN_TYPE[n.tagNumber], value: _bufToString(asn1.read.octetStringImplicit(n, n.tagNumber), "utf8") };
1084
- } else if (n.constructed && n.tagNumber === 0 && (n.children || []).length >= 2) {
1085
- var inner = (n.children[1].children || [])[0] || n.children[1];
1086
- var v;
1087
- try {
1088
- v = asn1.read.string(inner);
1089
- } catch (_e) {
1090
- v = null;
1091
- }
1092
- val = { type: "otherName", oid: asn1.read.oid(n.children[0]), value: v };
1093
- }
1094
- if (val) guard.list.append(names, val);
1093
+ /** @internal The shared decoder is the gate: it applies the sec. 4.2.1.6 rules and throws on a
1094
+ * name that breaks one. What it returns is a GeneralNames view rather than the codec nodes the
1095
+ * mapping below reads, so the count comes from it and the one name is then taken from the raw
1096
+ * decode, which carries the node shape `octetStringImplicit` and the otherName walk need. */
1097
+ var validated = _sanDecoder(ext.value).names || [];
1098
+ if (validated.length > 1) throw _err("sigstore/bad-certificate", "the Fulcio certificate carries multiple SAN identities");
1099
+ var n = (asn1.decode(ext.value).children || [])[0];
1100
+ if (!n || n.tagClass !== "context") return null;
1101
+ if (!n.constructed && GN_TYPE[n.tagNumber]) {
1102
+ return { type: GN_TYPE[n.tagNumber], value: _bufToString(asn1.read.octetStringImplicit(n, n.tagNumber), "utf8") };
1103
+ }
1104
+ if (n.constructed && n.tagNumber === 0 && (n.children || []).length >= 2) {
1105
+ var inner = (n.children[1].children || [])[0] || n.children[1];
1106
+ var v;
1107
+ try { v = asn1.read.string(inner); }
1108
+ catch (_e) { v = null; } // allow:swallow-unverified an otherName body with no string form has no identity string, which the caller's policy then gates on
1109
+ return { type: "otherName", oid: asn1.read.oid(n.children[0]), value: v };
1095
1110
  }
1096
- if (names.length > 1) throw _err("sigstore/bad-certificate", "the Fulcio certificate carries multiple SAN identities");
1097
- return names[0] || null;
1111
+ return null;
1098
1112
  }
1099
1113
  function _fulcioExtValue(ext, leafArc) {
1100
1114
  if (!_isBuffer(ext.value)) throw _err("sigstore/bad-certificate", "Fulcio extension " + ext.oid + " has no value");
package/lib/tsp-sign.js CHANGED
@@ -34,6 +34,9 @@ var pkiBuild = require("./pki-build");
34
34
  var constants = require("./constants");
35
35
  var TspError = frameworkError.TspError;
36
36
  var _NS = pkix.makeNS("tsp", TspError, oid);
37
+ /** @internal The shared certificate-extension decoders, so an extension this module reads is read
38
+ * under the rules every other consumer reads it under rather than under a second set written here. */
39
+ var _EXT_DECODERS = pkix.certExtensionDecoders(_NS).byOid;
37
40
  var b = asn1.build;
38
41
  function _err(code, message, cause) { return new TspError(code, message, cause); }
39
42
 
@@ -487,12 +490,13 @@ function _checkTsaCertUsage(tsaCertDer) {
487
490
  var exts = (cert.extensions || []).filter(function (e) { return e.oid === O("extKeyUsage"); });
488
491
  if (exts.length !== 1) return "tsp/bad-eku";
489
492
  if (!exts[0].critical) return "tsp/eku-not-critical";
490
- var purposes, ekuNode;
491
- try { ekuNode = asn1.decode(exts[0].value); }
492
- catch (_e) { return "tsp/bad-eku"; }
493
- if (ekuNode.tagClass !== "universal" || ekuNode.tagNumber !== asn1.TAGS.SEQUENCE || !ekuNode.children) return "tsp/bad-eku";
494
- try { purposes = ekuNode.children.map(function (c) { return asn1.read.oid(c); }); }
495
- catch (_e) { return "tsp/bad-eku"; }
493
+ /** @internal Read through the shared decoder, which is where the sec. 4.2.1.12 rules live: an
494
+ * ExtKeyUsage carries at least one KeyPurposeId. Reading the SEQUENCE here instead called an
495
+ * empty one well-formed and then reported it as the wrong PURPOSE, so the same class of fault
496
+ * answered with a different code here than at every other consumer of this extension. */
497
+ var purposes;
498
+ try { purposes = _EXT_DECODERS[O("extKeyUsage")](exts[0].value); }
499
+ catch (_e) { return "tsp/bad-eku"; } // allow:swallow-unverified this verb answers with a code rather than throwing, and every malformed shape is the one code
496
500
  if (purposes.length !== 1 || purposes[0] !== O("timeStamping")) return "tsp/eku-not-exclusive";
497
501
  var ku;
498
502
  try { ku = pkix.keyUsageOf(_NS, cert, _err, "tsp/bad-key-usage", "TSA certificate"); }
@@ -508,13 +512,16 @@ function _tsaMatchesCert(tsaBytes, signerCert) {
508
512
  if (gn.tagNumber === 4 && gn.value && guard.name.dnEqual(gn.value.rdns, signerCert.subject.rdns, _err, "tsp/tsa-mismatch", "the TSA subject")) return true;
509
513
  var sanExt = (signerCert.extensions || []).filter(function (e) { return e.oid === O("subjectAltName"); })[0];
510
514
  if (sanExt) {
511
- var san;
512
- try { san = asn1.decode(sanExt.value); }
513
- catch (_e) { return false; }
514
- if (san.tagClass === "universal" && san.tagNumber === asn1.TAGS.SEQUENCE && san.children) {
515
- for (var i = 0; i < san.children.length; i++) {
516
- if (san.children[i].bytes.equals(tsaBytes)) return true;
517
- }
515
+ /** @internal Gated by the shared decoder before the comparison, so a SAN this toolkit refuses
516
+ * everywhere else cannot be a source of identity here. The match itself stays a comparison of
517
+ * the encoded GeneralName bytes, which is what "the token names this TSA" means: the name in
518
+ * the token and the name in the certificate have to be the same name, not merely names that
519
+ * decode alike. */
520
+ var names;
521
+ try { names = _EXT_DECODERS[O("subjectAltName")](sanExt.value).names || []; }
522
+ catch (_e) { return false; } // allow:swallow-unverified a SAN that does not decode names no TSA, which is what this verb answers
523
+ for (var i = 0; i < names.length; i++) {
524
+ if (names[i].bytes && names[i].bytes.equals(tsaBytes)) return true;
518
525
  }
519
526
  }
520
527
  return false;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blamejs/pki",
3
- "version": "0.8.9",
3
+ "version": "0.8.10",
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:1d0c3725-eae5-4743-8430-5684e3e52ec7",
5
+ "serialNumber": "urn:uuid:4cdebd00-c3c0-4ba6-9d9c-daacb902163c",
6
6
  "version": 1,
7
7
  "metadata": {
8
- "timestamp": "2026-09-23T19:11:33.586Z",
8
+ "timestamp": "2026-09-23T19:58:00.767Z",
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.8.9",
22
+ "bom-ref": "@blamejs/pki@0.8.10",
23
23
  "type": "application",
24
24
  "name": "pki",
25
- "version": "0.8.9",
25
+ "version": "0.8.10",
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.8.9",
29
+ "purl": "pkg:npm/%40blamejs/pki@0.8.10",
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.8.9",
57
+ "ref": "@blamejs/pki@0.8.10",
58
58
  "dependsOn": []
59
59
  }
60
60
  ]