@blamejs/pki 0.7.5 → 0.7.7

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.7 — 2026-09-10
8
+
9
+ The two GeneralName arms that were taken on their tag alone are now read.
10
+
11
+ ### Changed
12
+
13
+ - Every verb that walks a name is affected, since they share one reader: certification-path validation, certificate and attribute-certificate parsing, CRL and CMP name handling, certificate linting, and the name-carrying extension decoders. A certificate carrying one of these two arms malformed was accepted before and is refused now, with the typed code that verb already raises for a bad name. A conforming arm is unaffected, and no error code was added or changed.
14
+
15
+ ### Fixed
16
+
17
+ - GeneralName ediPartyName [5] is read as the EDIPartyName it holds: an optional nameAssigner [0] before a required partyName [1], each wrapping a DirectoryString of at least one character, in that order and with nothing else alongside. It was previously accepted as any non-empty constructed value, so a [5] over a NULL, over an INTEGER, or carrying nameAssigner without partyName all decoded as names while encoding none. GeneralName is defined in the RFC 5280 Appendix A.2 implicitly tagged module, so the [5] tag replaces the EDIPartyName SEQUENCE tag and its children are the two arms directly.
18
+ - GeneralName x400Address [3] must open with the BuiltInStandardAttributes SEQUENCE that begins an ORAddress, so an arm carrying that tag over some other value is refused. What the components hold is an X.400 structure this does not read, and every one of them is optional, so an address whose components are all empty is conforming and is not refused.
19
+
20
+ ## v0.7.6 — 2026-09-10
21
+
22
+ A Sigstore bundle is held to the shape its specification states, in signature count and in entry count.
23
+
24
+ ### Changed
25
+
26
+ - A bundle offering more than 32 transparency-log entries is refused with sigstore/bad-bundle, naming the count it offered. Each entry a bundle offers can cost a certification-path validation, since an entry is tried until one passes every check that depends on which entry it is, and the validation instant comes from the entry itself unless the caller pins opts.time. Nothing bounded the count: the 1 MiB bundle budget leaves room for roughly 155 entries, and every bundle in the Sigstore conformance suite carries exactly one. The ceiling is a local resource bound rather than a rule the specification states, and it is checked before any entry is read. It is available as pki.C.LIMITS.TLOG_MAX_COUNT.
27
+
28
+ ### Fixed
29
+
30
+ - A DSSE envelope carrying more than one signature is refused with sigstore/bad-dsse, naming the count it carried. The Sigstore bundle specification states the rule on the producer, that an envelope in a bundle has exactly one signature, and again on the verifier, that a client rejects an envelope whose signature count is not one. Only the first signature was ever read, so an envelope with a second was accepted and returned a verdict that said nothing about it. An envelope with exactly one is unaffected, and an empty signature list keeps the refusal it already had.
31
+
7
32
  ## v0.7.5 — 2026-09-10
8
33
 
9
34
  Certificate linting now covers the ML-DSA and SLH-DSA certificate profiles.
package/lib/constants.js CHANGED
@@ -128,6 +128,7 @@ var LIMITS = Object.assign(Object.create(null), {
128
128
  CBOR_MAX_ITEMS: 1000000,
129
129
  SCT_MAX_BYTES: BYTES.kib(64) + 1,
130
130
  SCT_MAX_COUNT: 256,
131
+ TLOG_MAX_COUNT: 32,
131
132
  CMS_MAX_KARI_ENCRYPTED_KEYS: 64,
132
133
  CMP_KEM_MAX_KEY_BYTES: 1024,
133
134
  CMP_KEM_MIN_KEY_BYTES: 20,
package/lib/crmf-sign.js CHANGED
@@ -825,24 +825,16 @@ function _namesSomebody(node) {
825
825
  if (!_isUniversal(node, 16) || !node.children || node.children.length === 0) return false;
826
826
  try { schema.walk(pkix.generalNames(NS, { code: "crmf/bad-popo" }), node, NS); }
827
827
  catch (_e) { return false; } // allow:swallow-unverified a list the shared reader refuses names nobody, which the caller's refusal states
828
- /** @internal The shared reader takes ediPartyName [5] as any non-empty constructed value, so a [5]
829
- * holding a NULL passes it while encoding no name at all. EDIPartyName is SEQUENCE { nameAssigner
830
- * [0] DirectoryString OPTIONAL, partyName [1] DirectoryString } (RFC 5280 sec. 4.2.1.6), so the
831
- * arms it holds are context-tagged and a universal value is not one of them. x400Address [3] is an
832
- * ORAddress, which this does not read, and is left to the shared reader: the derivation hashes
833
- * these bytes rather than interpreting them, so an arm it cannot check is still an arm both sides
834
- * hash alike. */
835
- /** @internal Walked arm by arm, because the reader answers for most of them and not for all. It
836
- * requires the IA5 arms [1] [2] [6] to carry characters, [7] to be an address of a real width and
837
- * [8] to be an OBJECT IDENTIFIER, and it walks [0] and [4] into their own shapes. What it does not
838
- * do is refuse the two empty forms below, and x400Address [3] it takes on its tag alone, which is
839
- * left as it is: the derivation hashes these bytes rather than reading them, so an arm neither
840
- * side reads is still an arm both sides hash alike. */
828
+ /** @internal The shared reader answers for the SHAPE of every arm, including ediPartyName [5] and
829
+ * the ORAddress opening of x400Address [3]. What remains here is the separate question this
830
+ * derivation asks, which is whether the arm names SOMEBODY rather than whether it is well formed:
831
+ * a Name with no RDNs and an ORAddress whose components are all empty are both conforming and
832
+ * neither identifies a subject. Splitting it this way keeps a naming-strength rule out of a
833
+ * structural reader every other consumer shares. */
841
834
  for (var i = 0; i < node.children.length; i++) {
842
835
  var arm = node.children[i];
843
836
  if (arm.tagNumber === 3 && !_isOrAddress(arm)) return false;
844
837
  if (arm.tagNumber === 4 && !_isNonEmptyName(arm)) return false;
845
- if (arm.tagNumber === 5 && !_isEdiPartyName(arm)) return false;
846
838
  }
847
839
  return true;
848
840
  }
@@ -874,32 +866,6 @@ function _isNonEmptyName(node) {
874
866
  return schema.isUniversal(dn, 16) && !!dn.children && dn.children.length > 0;
875
867
  }
876
868
 
877
- /** @internal The universal tags a DirectoryString is written under (RFC 5280 sec. 4.1.2.4):
878
- * UTF8String, PrintableString, TeletexString, UniversalString and BMPString. */
879
- var DIRECTORY_STRING_TAGS = [12, 19, 20, 28, 30];
880
-
881
- /** @internal Whether a [5] arm holds an EDIPartyName: the optional nameAssigner [0] before the
882
- * required partyName [1], each carrying a DirectoryString. DirectoryString is a CHOICE, so its tag
883
- * is EXPLICIT however the module tags: the arm is constructed and wraps the string itself, and the
884
- * string has to be one, since a [1] wrapping a NULL is tagged like a name and holds none. */
885
- function _isEdiPartyName(node) {
886
- var kids = node.children;
887
- if (!kids || kids.length < 1 || kids.length > 2) return false;
888
- for (var i = 0; i < kids.length; i++) {
889
- var arm = kids[i];
890
- if (!schema.isContextInRange(arm, 0, 1) || !arm.children || arm.children.length !== 1) return false;
891
- if (!schema.isUniversalOneOf(arm.children[0], DIRECTORY_STRING_TAGS)) return false;
892
- /** @internal Read as the string it is tagged as, not counted as one. DirectoryString is
893
- * SIZE (1..MAX), so an empty one names nobody, and a string whose bytes are not the encoding its
894
- * tag claims is not a name either. The codec's own reader answers both. */
895
- var text;
896
- try { text = asn1.read.string(arm.children[0]); }
897
- catch (_e) { return false; } // allow:swallow-unverified a string the codec cannot read names nobody, which the caller's refusal states
898
- if (text.length === 0) return false;
899
- }
900
- if (kids.length === 2) return kids[0].tagNumber === 0 && kids[1].tagNumber === 1;
901
- return kids[0].tagNumber === 1;
902
- }
903
869
 
904
870
  function _nameOrAlternative(parsed, nameField, altExtension) {
905
871
  var name = parsed[nameField];
@@ -460,6 +460,10 @@ var CRL_REASON_NAMES = constants.NAMES.CRL_REASON;
460
460
 
461
461
  var GN_CONSTRUCTED = _assign(_create(null), { 0: 1, 3: 1, 4: 1, 5: 1 });
462
462
  var GN_IA5 = _assign(_create(null), { 1: 1, 2: 1, 6: 1 });
463
+ /** @internal DirectoryString ::= CHOICE { teletexString, printableString, universalString,
464
+ * utf8String, bmpString } (RFC 5280 sec. 4.1.2.4). */
465
+ var DIRECTORY_STRING_TAGS = [asn1.TAGS.UTF8_STRING, asn1.TAGS.PRINTABLE_STRING, asn1.TAGS.TELETEX_STRING,
466
+ asn1.TAGS.UNIVERSAL_STRING, asn1.TAGS.BMP_STRING];
463
467
  function generalName(ns, opts) {
464
468
  opts = opts || {};
465
469
  var code = opts.code || (ns.prefix + "/bad-general-name");
@@ -489,6 +493,40 @@ function generalName(ns, opts) {
489
493
  if (n.children.length !== 1) throw ctx.E(code, "GeneralName directoryName [4] must wrap exactly one Name");
490
494
  var dnMatch = schema.walk(NAME, n.children[0], ctx);
491
495
  if (decodeValue) value = dnMatch.result;
496
+ } else if (t === 3) {
497
+ /** @internal ORAddress opens with the BuiltInStandardAttributes SEQUENCE (X.411), so an arm
498
+ * carrying its tag over some other value is not an address. What the components hold is an
499
+ * X.400 structure this does not read, and every one of them is optional, so an address whose
500
+ * components are all empty is still a conforming address and is not refused here. */
501
+ if (!schema.isUniversal(n.children[0], asn1.TAGS.SEQUENCE)) {
502
+ throw ctx.E(code, "GeneralName x400Address [3] must be an ORAddress opening with the BuiltInStandardAttributes SEQUENCE (X.411)");
503
+ }
504
+ } else if (t === 5) {
505
+ /** @internal EDIPartyName ::= SEQUENCE { nameAssigner [0] DirectoryString OPTIONAL,
506
+ * partyName [1] DirectoryString } (RFC 5280 sec. 4.2.1.6). partyName is not optional, the
507
+ * two arms are ordered, and a DirectoryString is SIZE (1..MAX), so an empty one is not one. */
508
+ var ediKids = n.children;
509
+ if (ediKids.length > 2) throw ctx.E(code, "GeneralName ediPartyName [5] holds at most nameAssigner [0] and partyName [1] (RFC 5280 sec. 4.2.1.6)");
510
+ for (var ei = 0; ei < ediKids.length; ei++) {
511
+ var ediArm = ediKids[ei];
512
+ if (!schema.isContextInRange(ediArm, 0, 1) || !ediArm.children || ediArm.children.length !== 1) {
513
+ throw ctx.E(code, "GeneralName ediPartyName [5] arms must be [0] or [1] wrapping exactly one DirectoryString");
514
+ }
515
+ if (!schema.isUniversalOneOf(ediArm.children[0], DIRECTORY_STRING_TAGS)) {
516
+ throw ctx.E(code, "GeneralName ediPartyName [5] arm must hold a DirectoryString");
517
+ }
518
+ var ediText;
519
+ try { ediText = asn1.read.string(ediArm.children[0]); }
520
+ catch (e) { throw ctx.E(code, "GeneralName ediPartyName [5] arm is not the string its tag claims", e); }
521
+ if (ediText.length === 0) throw ctx.E(code, "GeneralName ediPartyName [5] DirectoryString must be SIZE (1..MAX)");
522
+ }
523
+ if (ediKids.length === 2) {
524
+ if (ediKids[0].tagNumber !== 0 || ediKids[1].tagNumber !== 1) {
525
+ throw ctx.E(code, "GeneralName ediPartyName [5] must order nameAssigner [0] before partyName [1]");
526
+ }
527
+ } else if (ediKids[0].tagNumber !== 1) {
528
+ throw ctx.E(code, "GeneralName ediPartyName [5] requires partyName [1]; nameAssigner [0] alone is not a name");
529
+ }
492
530
  }
493
531
  } else {
494
532
  if (constructed) throw ctx.E(code, "GeneralName [" + t + "] must be primitive (X.690 sec. 10.2)");
package/lib/sigstore.js CHANGED
@@ -290,6 +290,13 @@ function parseBundle(input) {
290
290
  !d.signatures[0] || typeof d.signatures[0] !== "object" || typeof d.signatures[0].sig !== "string") {
291
291
  throw _err("sigstore/bad-dsse", "the DSSE envelope is missing a required field (payload / payloadType / signatures[].sig)");
292
292
  }
293
+ /** @internal The bundle specification states this on the producer and again on the verifier: an
294
+ * envelope in a bundle carries exactly one signature, and a verifier rejects one whose count is
295
+ * not one. Only `signatures[0]` is ever read, so admitting a second would return a verdict that
296
+ * says nothing about content the envelope carries. */
297
+ if (d.signatures.length !== 1) {
298
+ throw _err("sigstore/bad-dsse", "a bundle's DSSE envelope carries exactly one signature; this one carries " + d.signatures.length);
299
+ }
293
300
  return obj;
294
301
  }
295
302
 
@@ -1263,6 +1270,15 @@ async function verifyBundle(bundle, opts) {
1263
1270
 
1264
1271
  var tlogs = _isArray(vm.tlogEntries) ? vm.tlogEntries : [];
1265
1272
  if (!tlogs.length) throw _err("sigstore/bad-bundle", "a keyless bundle requires at least one transparency-log entry");
1273
+ /** @internal An entry is tried until one passes every check that depends on which entry it is,
1274
+ * and those checks include a certification-path validation whose instant comes from the entry
1275
+ * itself unless the caller pins one. The count is therefore a work multiplier, and the byte
1276
+ * budget alone leaves room for far more entries than any real bundle carries. This is a local
1277
+ * resource bound rather than a rule the bundle specification states. */
1278
+ if (tlogs.length > C.LIMITS.TLOG_MAX_COUNT) {
1279
+ throw _err("sigstore/bad-bundle", "a bundle offers at most " + C.LIMITS.TLOG_MAX_COUNT
1280
+ + " transparency-log entries; this one offers " + tlogs.length);
1281
+ }
1266
1282
  /** @internal Selecting an entry means finding one that passes EVERY check deciding WHICH entry it
1267
1283
  * is: that it binds to this signature and certificate, that it records this artifact, and that the
1268
1284
  * certificate chain and the embedded timestamps hold at the instant it attests. Any of those
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blamejs/pki",
3
- "version": "0.7.5",
3
+ "version": "0.7.7",
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:b846e4fa-afc9-4ee9-9405-9ab995704bfd",
5
+ "serialNumber": "urn:uuid:210f0706-3b87-4d45-8f03-f61f7b8bfc45",
6
6
  "version": 1,
7
7
  "metadata": {
8
- "timestamp": "2026-09-10T12:11:13.518Z",
8
+ "timestamp": "2026-09-10T13:10:12.331Z",
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.5",
22
+ "bom-ref": "@blamejs/pki@0.7.7",
23
23
  "type": "application",
24
24
  "name": "pki",
25
- "version": "0.7.5",
25
+ "version": "0.7.7",
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.5",
29
+ "purl": "pkg:npm/%40blamejs/pki@0.7.7",
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.5",
57
+ "ref": "@blamejs/pki@0.7.7",
58
58
  "dependsOn": []
59
59
  }
60
60
  ]