@blamejs/pki 0.6.35 → 0.6.36

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,23 @@ 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.6.36 — 2026-09-04
8
+
9
+ A natively signed C509 certificate is refused when it spells an algorithm generically instead of using the registry entry that names the same value.
10
+
11
+ ### Added
12
+
13
+ - The subjectPublicKeyAlgorithm on a pki.schema.c509.parse result carries a parameters buffer when the certificate used a C509 registry integer, holding the parameters that entry names: the DER NULL for rsaEncryption and the namedCurve OID for an EC key. The generic OID forms already surfaced this field, so it is now present for every form that has parameters.
14
+
15
+ ### Fixed
16
+
17
+ - pki.schema.c509.parse refuses a natively signed certificate (c509CertificateType 2) that writes an algorithm as an OID, or as an OID with parameters, when a C509 registry entry names that same value. Draft section 3.7 reserves the generic form for re-encoded certificates, where it stays accepted. An algorithm with no registry entry, and a registry OID whose parameters differ from the entry's, both keep the generic form.
18
+ - pki.schema.c509.parse refuses an rsaEncryption subjectPublicKeyAlgorithm that does not carry the DER NULL parameters RFC 3279 section 2.3.1 requires, including the bare OID form that carries none. The type-3 reconstruction previously emitted the OID and a NULL regardless, so the rebuilt DER described an algorithm identifier the certificate had not encoded.
19
+ - pki.schema.c509.encode writes an algorithm back as the form naming the same value. It previously chose the form by algorithm name alone, so re-encoding a certificate field by field could move an algorithm that carried no parameters onto a registry entry that names some, and could miss the entry for a key identified by its namedCurve parameters rather than by a curve name.
20
+ - pki.schema.c509.encode refuses a serialNumber, RSA modulus or RSA exponent that is not a BigInt. A string was previously read as hexadecimal and truncated to an even number of digits, so a caller passing "abc" received a certificate carrying the serial 0xab, and a number was silently converted. A missing modulus or exponent reached the emitter as an untyped fault rather than a c509 error naming the field.
21
+ - pki.schema.c509.encode reports every field it reads off a result as a typed c509 error naming that field. An extensions entry or an RDN that is not an object, an extension carrying neither an OID nor a registry name, an extension OID that is not a dotted-decimal string, and a keyUsageBits that is not a safe integer previously surfaced as a TypeError, a RangeError, or a fault from the OID or CBOR builders. An RDN value that is not a string is refused rather than converted, so a caller passing a number no longer receives a certificate naming its decimal spelling.
22
+ - pki.schema.c509.encode refuses a sparse extensions or rdns array. Array iteration skips a hole, so such a list emitted fewer entries than it held without reporting anything.
23
+
7
24
  ## v0.6.35 — 2026-09-04
8
25
 
9
26
  Three redundant C509 spellings are refused, so a certificate has one encoding and an issuer signature cannot cover two different C509 byte strings.
@@ -32,6 +32,20 @@ var b = asn1.build;
32
32
  var C = constants;
33
33
  var C509Error = frameworkError.C509Error;
34
34
  function _err(code, message, cause) { return new C509Error(code, message, cause); }
35
+ /** @internal Name a caller-supplied value in a diagnostic without converting it: a Symbol has no
36
+ * string form and a BigInt has no JSON form, so formatting either one would throw from the message. */
37
+ function _describe(v) {
38
+ if (v === null) return "null";
39
+ var t = typeof v;
40
+ if (t === "string") return JSON.stringify(v);
41
+ return t === "number" || t === "boolean" || t === "undefined" ? String(v) : t;
42
+ }
43
+ /** @internal A hole is skipped by forEach, so a sparse list would emit fewer items than it holds. */
44
+ function _assertDense(list, code, label) {
45
+ for (var i = 0; i < list.length; i++) {
46
+ if (!(i in list)) throw _err(code, label + " must not be a sparse array; index " + i + " is a hole");
47
+ }
48
+ }
35
49
  var MAX_EPOCH_SECONDS = 8640000000000n;
36
50
  var NO_EXPIRY_SECONDS = 253402300799n;
37
51
 
@@ -178,19 +192,58 @@ function _oidName(node, code, label) {
178
192
  return { oid: dotted, name: oid.name(dotted) || dotted };
179
193
  }
180
194
 
181
- function _algorithm(node, byInt, code, label) {
195
+ /**
196
+ * @internal Draft sec. 8.14 and 8.15 give each registry row a full DER AlgorithmIdentifier, so the
197
+ * row's parameters are part of the value it denotes: a signature row carries none, the rsaEncryption
198
+ * row carries NULL, and an ecPublicKey row carries its namedCurve.
199
+ */
200
+ function _rowParameters(row) {
201
+ if (typeof row === "string") return null;
202
+ if (row.curve) return asn1.build.oid(oid.byName(row.curve));
203
+ return asn1.build.nullValue();
204
+ }
205
+ function _rowParametersMatch(row, params) {
206
+ if (row === undefined) return false;
207
+ var rowParams = _rowParameters(row);
208
+ return rowParams === null ? params === null : (params !== null && rowParams.equals(params));
209
+ }
210
+ function _specificInt(byInt, dotted, params, curve) {
211
+ var keys = Object.keys(byInt);
212
+ for (var i = 0; i < keys.length; i++) {
213
+ var row = byInt[keys[i]];
214
+ if (oid.byName(typeof row === "string" ? row : row.alg) !== dotted) continue;
215
+ var hit = params === null && curve ? (typeof row !== "string" && row.curve === curve)
216
+ : _rowParametersMatch(row, params);
217
+ if (hit) return Number(keys[i]);
218
+ }
219
+ return undefined;
220
+ }
221
+ function _hasSpecificForm(byInt, dotted, params) {
222
+ return _specificInt(byInt, dotted, params) !== undefined;
223
+ }
224
+ function _assertSpecificForm(byInt, dotted, params, isNative, label) {
225
+ if (!isNative || !_hasSpecificForm(byInt, dotted, params)) return;
226
+ throw _err("c509/non-specific-encoding", label + " has a C509 registry row, and a natively signed certificate uses only the specific CBOR encoding (draft sec. 3.7)");
227
+ }
228
+
229
+ function _algorithm(node, byInt, code, label, isNative) {
182
230
  if (node.majorType === 0 || node.majorType === 1) {
183
231
  var i = Number(cbor.read.int(node));
184
232
  var mapped = byInt[i];
185
233
  if (mapped === undefined) throw _err("c509/unknown-algorithm", label + " integer " + i + " has no C509 registry row");
186
234
  if (typeof mapped === "string") return { name: mapped, oid: oid.byName(mapped) };
187
- return { name: mapped.alg, oid: oid.byName(mapped.alg), curve: mapped.curve || null };
235
+ return { name: mapped.alg, oid: oid.byName(mapped.alg), curve: mapped.curve || null, parameters: _rowParameters(mapped) };
236
+ }
237
+ if (node.majorType === 2) {
238
+ var r = _oidName(node, code, label);
239
+ _assertSpecificForm(byInt, r.oid, null, isNative, label);
240
+ return { name: r.name, oid: r.oid };
188
241
  }
189
- if (node.majorType === 2) { var r = _oidName(node, code, label); return { name: r.name, oid: r.oid }; }
190
242
  if (node.majorType === 4 && node.children && node.children.length === 2) {
191
243
  var a = _oidName(node.children[0], code, label);
192
244
  if (node.children[1].majorType !== 2) throw _err(code, label + " algorithm parameters must be a CBOR byte string");
193
245
  if (node.children[1].content.length === 0) throw _err(code, label + " algorithm parameters must not be an empty byte string; omit them with the bare ~oid form (draft sec. 3.1.3)");
246
+ _assertSpecificForm(byInt, a.oid, node.children[1].content, isNative, label);
194
247
  return { name: a.name, oid: a.oid, parameters: node.children[1].content };
195
248
  }
196
249
  throw _err(code, label + " is not a C509 AlgorithmIdentifier (int / ~oid / [~oid, params])");
@@ -1420,6 +1473,10 @@ function _reconSpki(spkAlg, keyBytes, rsaKey) {
1420
1473
  return b.sequence([b.sequence([b.oid(oid.byName("ecPublicKey")), b.oid(oid.byName(spkAlg.curve))]), b.bitString(point, 0)]);
1421
1474
  }
1422
1475
  if (spkAlg.name === "rsaEncryption") {
1476
+ /** @internal RFC 3279 sec. 2.3.1 fixes rsaEncryption parameters to the DER NULL. */
1477
+ if (!spkAlg.parameters || !spkAlg.parameters.equals(b.nullValue())) {
1478
+ throw _err("c509/bad-spki", "rsaEncryption parameters must be the DER NULL (RFC 3279 sec. 2.3.1); the bare ~oid form carries none");
1479
+ }
1423
1480
  var rsaPk = b.sequence([b.integer(rsaKey.modulus), b.integer(rsaKey.exponent)]);
1424
1481
  return b.sequence([b.sequence([b.oid(oid.byName("rsaEncryption")), b.nullValue()]), b.bitString(rsaPk, 0)]);
1425
1482
  }
@@ -1523,7 +1580,7 @@ function parse(input) {
1523
1580
  var serial = _biguint(serialBytes, "c509/bad-serial", "certificateSerialNumber");
1524
1581
  var sHex = serialBytes.content.toString("hex");
1525
1582
 
1526
- var sigAlg = _algorithm(f[2], SIG_ALG_BY_INT, "c509/unknown-algorithm", "issuerSignatureAlgorithm");
1583
+ var sigAlg = _algorithm(f[2], SIG_ALG_BY_INT, "c509/unknown-algorithm", "issuerSignatureAlgorithm", type === 2);
1527
1584
  var issuer = _name509(f[3], false, type === 2);
1528
1585
  var notBefore = _time(f[4], false, "validityNotBefore");
1529
1586
  var notAfter = _time(f[5], true, "validityNotAfter");
@@ -1531,7 +1588,7 @@ function parse(input) {
1531
1588
  if (issuer === null && (!subject || !subject.rdns || subject.rdns.length === 0)) {
1532
1589
  throw _err("c509/bad-name", "a self-signed C509 (issuer == subject) requires a non-empty subject, since it is also the issuer (RFC 5280 sec. 4.1.2.4)");
1533
1590
  }
1534
- var spkAlg = _algorithm(f[7], PK_ALG_BY_INT, "c509/unknown-algorithm", "subjectPublicKeyAlgorithm");
1591
+ var spkAlg = _algorithm(f[7], PK_ALG_BY_INT, "c509/unknown-algorithm", "subjectPublicKeyAlgorithm", type === 2);
1535
1592
  var subjectPublicKey = null, rsaKey = null;
1536
1593
  if (spkAlg.name === "rsaEncryption") {
1537
1594
  if (f[8].majorType === 2) rsaKey = { modulus: _biguint(f[8], "c509/bad-spki", "RSA modulus"), exponent: 65537n };
@@ -1578,29 +1635,35 @@ function matches(node) {
1578
1635
  }
1579
1636
 
1580
1637
 
1581
- var SIG_ALG_TO_INT = Object.assign(Object.create(null), { ecdsaWithSHA256: 0, ecdsaWithSHA384: 1, ecdsaWithSHA512: 2 });
1582
- var PK_ALG_TO_INT = Object.assign(Object.create(null), { rsaEncryption: 0, "ecPublicKey|prime256v1": 1, "ecPublicKey|secp384r1": 2, "ecPublicKey|secp521r1": 3 });
1583
1638
  var ATTR_TO_INT = Object.create(null);
1584
1639
  Object.keys(ATTR_BY_INT).forEach(function (k) { ATTR_TO_INT[ATTR_BY_INT[k]] = Number(k); });
1585
1640
  var EXT_TO_INT = Object.create(null);
1586
1641
  Object.keys(EXT_BY_INT).forEach(function (k) { if (EXT_COMPACT[EXT_BY_INT[k]]) EXT_TO_INT[EXT_BY_INT[k]] = Number(k); });
1587
1642
 
1588
1643
  function _minBytes(n) {
1644
+ if (typeof n !== "bigint") throw _err("c509/bad-serial", "a ~biguint value must be a BigInt, got " + _describe(n));
1589
1645
  if (n < 0n) throw _err("c509/bad-serial", "a ~biguint value must be non-negative");
1590
1646
  if (n === 0n) return Buffer.alloc(0);
1591
1647
  var hex = n.toString(16); if (hex.length % 2) hex = "0" + hex;
1592
1648
  return Buffer.from(hex, "hex");
1593
1649
  }
1594
- function _encAlgorithm(alg, toInt, key) {
1595
- var i = toInt[key];
1596
- if (i !== undefined && !(alg.parameters && alg.parameters.length)) return cbor.build.int(BigInt(i));
1597
- var oidBytes = cbor.build.byteString(asn1.encodeOidContent(alg.oid));
1598
- if (alg.parameters && alg.parameters.length) return cbor.build.array([oidBytes, cbor.build.byteString(alg.parameters)]);
1650
+ function _encAlgorithm(alg, byInt) {
1651
+ var dotted = alg.oid || oid.byName(alg.name);
1652
+ if (!dotted) throw _err("c509/bad-input", "algorithm " + alg.name + " carries no OID and its name is not in the OID registry");
1653
+ var supplied = alg.parameters && alg.parameters.length ? alg.parameters : null;
1654
+ var i = _specificInt(byInt, dotted, supplied, alg.curve);
1655
+ if (i !== undefined) return cbor.build.int(BigInt(i));
1656
+ var oidBytes = cbor.build.byteString(asn1.encodeOidContent(dotted));
1657
+ if (supplied !== null) return cbor.build.array([oidBytes, cbor.build.byteString(supplied)]);
1599
1658
  return oidBytes;
1600
1659
  }
1601
1660
  function _encSpecialText(rdn) {
1602
- if (rdn.eui64) return cbor.build.tag(48, cbor.build.byteString(rdn.eui64));
1603
- var v = String(rdn.value);
1661
+ if (rdn.eui64 != null) {
1662
+ if (!Buffer.isBuffer(rdn.eui64)) throw _err("c509/bad-name", "an RDN eui64 must be a Buffer, got " + _describe(rdn.eui64));
1663
+ return cbor.build.tag(48, cbor.build.byteString(rdn.eui64));
1664
+ }
1665
+ if (typeof rdn.value !== "string") throw _err("c509/bad-name", "an RDN value must be a string, got " + _describe(rdn.value));
1666
+ var v = rdn.value;
1604
1667
  if (v.length >= 2 && _isEvenHexLower(v)) return cbor.build.byteString(Buffer.from(v, "hex"));
1605
1668
  return cbor.build.textString(v);
1606
1669
  }
@@ -1610,11 +1673,15 @@ function _encName(name, isSubject) {
1610
1673
  throw _err("c509/bad-name", "the subject Name is required");
1611
1674
  }
1612
1675
  var rdns = name.rdns || [];
1676
+ _assertDense(rdns, "c509/bad-name", "the rdns array");
1677
+ rdns.forEach(function (rdn) {
1678
+ if (!rdn || typeof rdn !== "object") throw _err("c509/bad-name", "each RDN must be an object, got " + _describe(rdn));
1679
+ });
1613
1680
  if (rdns.length === 1 && rdns[0].type === "commonName" && !rdns[0].printable) return _encSpecialText(rdns[0]);
1614
1681
  var items = [];
1615
1682
  rdns.forEach(function (rdn) {
1616
1683
  var ai = ATTR_TO_INT[rdn.type];
1617
- if (ai === undefined) throw _err("c509/bad-name", "attribute type " + rdn.type + " has no C509 registry int");
1684
+ if (ai === undefined) throw _err("c509/bad-name", "attribute type " + _describe(rdn.type) + " has no C509 registry int");
1618
1685
  items.push(cbor.build.int(BigInt(rdn.printable ? -ai : ai)));
1619
1686
  items.push(_encSpecialText(rdn));
1620
1687
  });
@@ -1630,6 +1697,18 @@ function _encSpk(r) {
1630
1697
  return cbor.build.byteString(r.subjectPublicKey);
1631
1698
  }
1632
1699
  function _encExtensions(exts) {
1700
+ _assertDense(exts, "c509/bad-input", "the extensions array");
1701
+ exts.forEach(function (ext) {
1702
+ if (!ext || typeof ext !== "object") throw _err("c509/bad-input", "each extension must be an object, got " + _describe(ext));
1703
+ if (ext.oid != null) {
1704
+ if (typeof ext.oid !== "string") throw _err("c509/bad-input", "an extension oid must be a dotted-decimal string, got " + _describe(ext.oid));
1705
+ try { asn1.encodeOidContent(ext.oid); }
1706
+ catch (_oe) { throw _err("c509/bad-input", "an extension oid must be a dotted-decimal string, got " + _describe(ext.oid), _oe); }
1707
+ }
1708
+ if (ext.keyUsageBits != null && (typeof ext.keyUsageBits !== "number" || !Number.isSafeInteger(ext.keyUsageBits))) {
1709
+ throw _err("c509/bad-extensions", "keyUsageBits must be a safe integer, got " + _describe(ext.keyUsageBits));
1710
+ }
1711
+ });
1633
1712
  if (exts.length === 1 && exts[0].name === "keyUsage" && typeof exts[0].keyUsageBits === "number") {
1634
1713
  return cbor.build.int(BigInt(exts[0].critical ? -exts[0].keyUsageBits : exts[0].keyUsageBits));
1635
1714
  }
@@ -1641,7 +1720,9 @@ function _encExtensions(exts) {
1641
1720
  items.push(cbor.build.int(BigInt(ext.critical ? -ei : ei)));
1642
1721
  items.push(compact);
1643
1722
  } else {
1644
- items.push(cbor.build.byteString(asn1.encodeOidContent(ext.oid || oid.byName(ext.name))));
1723
+ var extDotted = ext.oid || (typeof ext.name === "string" ? oid.byName(ext.name) : null);
1724
+ if (!extDotted) throw _err("c509/bad-input", "an extension must carry an OID or a name in the OID registry, got " + _describe(ext.name));
1725
+ items.push(cbor.build.byteString(asn1.encodeOidContent(extDotted)));
1645
1726
  if (!Buffer.isBuffer(ext.value)) throw _err("c509/non-invertible", "extension " + (ext.oid || ext.name) + " has no byte-string value to encode");
1646
1727
  var bs = cbor.build.byteString(ext.value);
1647
1728
  items.push(ext.critical ? cbor.build.array([bs]) : bs);
@@ -1673,16 +1754,15 @@ function _encodeC509Array(r) {
1673
1754
  return out;
1674
1755
  }
1675
1756
  _requireResultShape(r);
1676
- var pkKey = r.subjectPublicKeyAlgorithm.curve ? r.subjectPublicKeyAlgorithm.name + "|" + r.subjectPublicKeyAlgorithm.curve : r.subjectPublicKeyAlgorithm.name;
1677
1757
  var arr = cbor.build.array([
1678
1758
  cbor.build.int(BigInt(r.certificateType)),
1679
1759
  cbor.build.byteString(r.serialNumberHex != null ? _hexBytes(r.serialNumberHex, "c509/bad-serial", "serialNumberHex") : _minBytes(r.serialNumber)),
1680
- _encAlgorithm(r.signatureAlgorithm, SIG_ALG_TO_INT, r.signatureAlgorithm.name),
1760
+ _encAlgorithm(r.signatureAlgorithm, SIG_ALG_BY_INT),
1681
1761
  _encName(r.issuer, false),
1682
1762
  _validityUint(r.validity.notBefore, "validityNotBefore"),
1683
1763
  r.validity.notAfter === null ? cbor.build.nullValue() : _validityUint(r.validity.notAfter, "validityNotAfter"),
1684
1764
  _encName(r.subject, true),
1685
- _encAlgorithm(r.subjectPublicKeyAlgorithm, PK_ALG_TO_INT, pkKey),
1765
+ _encAlgorithm(r.subjectPublicKeyAlgorithm, PK_ALG_BY_INT),
1686
1766
  _encSpk(r),
1687
1767
  _encExtensions(r.extensions),
1688
1768
  cbor.build.byteString(r.signatureValue),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blamejs/pki",
3
- "version": "0.6.35",
3
+ "version": "0.6.36",
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:3601ee14-7f92-4a66-bdb2-bab4e6360e45",
5
+ "serialNumber": "urn:uuid:8d50e6dd-1ddf-46f8-b8db-ed97195096c3",
6
6
  "version": 1,
7
7
  "metadata": {
8
- "timestamp": "2026-09-04T23:41:31.757Z",
8
+ "timestamp": "2026-09-05T01:58:33.232Z",
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.35",
22
+ "bom-ref": "@blamejs/pki@0.6.36",
23
23
  "type": "application",
24
24
  "name": "pki",
25
- "version": "0.6.35",
25
+ "version": "0.6.36",
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.35",
29
+ "purl": "pkg:npm/%40blamejs/pki@0.6.36",
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.35",
57
+ "ref": "@blamejs/pki@0.6.36",
58
58
  "dependsOn": []
59
59
  }
60
60
  ]