@blamejs/pki 0.6.35 → 0.6.37

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,34 @@ 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.37 — 2026-09-05
8
+
9
+ A natively signed C509 certificate is refused when it writes an extension in the generic OID form that the registry can carry as an identifier.
10
+
11
+ ### Fixed
12
+
13
+ - pki.schema.c509.parse refuses a natively signed certificate (c509CertificateType 2) that writes an extension as an OID and a DER value when a C509 registry identifier carries the same extension value. Draft section 3.7 reserves the generic form for re-encoded certificates, where it stays accepted. An extension whose OID has no registry identifier, and one whose value the compact encodings cannot represent under that certificate type's rules, both keep the generic form.
14
+ - pki.schema.c509.encode chooses an extension's form under the rules of the certificate type it is writing. It previously judged whether a value could be written compactly using the rules for a re-encoded certificate, so a natively signed certificate could be given a compact form its own decoder does not accept.
15
+ - pki.schema.c509.encode resolves an extension named only by its OID to the matching registry identifier. It looked the identifier up by name alone, so an extension supplied without one was written in the generic OID form even where a compact encoding carried it.
16
+ - pki.schema.c509 decides what belongs to the C509 registries by dotted OID rather than by the name pki.oid.register maps that OID to. Registering a different name for a built-in OID previously changed what the toolkit emitted for an unrelated certificate, and for commonName, a curve, ecPublicKey or an ECDSA signature algorithm it stopped a certificate converting to C509 at all.
17
+
18
+ ## v0.6.36 — 2026-09-04
19
+
20
+ A natively signed C509 certificate is refused when it spells an algorithm generically instead of using the registry entry that names the same value.
21
+
22
+ ### Added
23
+
24
+ - 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.
25
+
26
+ ### Fixed
27
+
28
+ - 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.
29
+ - 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.
30
+ - 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.
31
+ - 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.
32
+ - 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.
33
+ - 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.
34
+
7
35
  ## v0.6.35 — 2026-09-04
8
36
 
9
37
  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
 
@@ -49,6 +63,11 @@ var PK_ALG_BY_INT = {
49
63
  3: { alg: _name("ecPublicKey"), curve: _name("secp521r1") },
50
64
  };
51
65
  var EC_FIELD_BYTES = Object.assign(Object.create(null), { "prime256v1": 32, "secp384r1": 48, "secp521r1": 66 });
66
+ /** @internal Curve identity follows the dotted OID; pki.oid.register can rename one at any time. */
67
+ var EC_CURVE_BY_OID = Object.create(null);
68
+ Object.keys(EC_FIELD_BYTES).forEach(function (n) { EC_CURVE_BY_OID[oid.byName(n)] = n; });
69
+ var SIG_ALG_BY_OID = Object.create(null);
70
+ Object.keys(SIG_ALG_BY_INT).forEach(function (k) { SIG_ALG_BY_OID[oid.byName(SIG_ALG_BY_INT[k])] = SIG_ALG_BY_INT[k]; });
52
71
 
53
72
  var ATTR_BY_INT = {
54
73
  0: _name("emailAddress"),
@@ -178,19 +197,58 @@ function _oidName(node, code, label) {
178
197
  return { oid: dotted, name: oid.name(dotted) || dotted };
179
198
  }
180
199
 
181
- function _algorithm(node, byInt, code, label) {
200
+ /**
201
+ * @internal Draft sec. 8.14 and 8.15 give each registry row a full DER AlgorithmIdentifier, so the
202
+ * row's parameters are part of the value it denotes: a signature row carries none, the rsaEncryption
203
+ * row carries NULL, and an ecPublicKey row carries its namedCurve.
204
+ */
205
+ function _rowParameters(row) {
206
+ if (typeof row === "string") return null;
207
+ if (row.curve) return asn1.build.oid(oid.byName(row.curve));
208
+ return asn1.build.nullValue();
209
+ }
210
+ function _rowParametersMatch(row, params) {
211
+ if (row === undefined) return false;
212
+ var rowParams = _rowParameters(row);
213
+ return rowParams === null ? params === null : (params !== null && rowParams.equals(params));
214
+ }
215
+ function _specificInt(byInt, dotted, params, curve) {
216
+ var keys = Object.keys(byInt);
217
+ for (var i = 0; i < keys.length; i++) {
218
+ var row = byInt[keys[i]];
219
+ if (oid.byName(typeof row === "string" ? row : row.alg) !== dotted) continue;
220
+ var hit = params === null && curve ? (typeof row !== "string" && row.curve === curve)
221
+ : _rowParametersMatch(row, params);
222
+ if (hit) return Number(keys[i]);
223
+ }
224
+ return undefined;
225
+ }
226
+ function _hasSpecificForm(byInt, dotted, params) {
227
+ return _specificInt(byInt, dotted, params) !== undefined;
228
+ }
229
+ function _assertSpecificForm(byInt, dotted, params, isNative, label) {
230
+ if (!isNative || !_hasSpecificForm(byInt, dotted, params)) return;
231
+ 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)");
232
+ }
233
+
234
+ function _algorithm(node, byInt, code, label, isNative) {
182
235
  if (node.majorType === 0 || node.majorType === 1) {
183
236
  var i = Number(cbor.read.int(node));
184
237
  var mapped = byInt[i];
185
238
  if (mapped === undefined) throw _err("c509/unknown-algorithm", label + " integer " + i + " has no C509 registry row");
186
239
  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 };
240
+ return { name: mapped.alg, oid: oid.byName(mapped.alg), curve: mapped.curve || null, parameters: _rowParameters(mapped) };
241
+ }
242
+ if (node.majorType === 2) {
243
+ var r = _oidName(node, code, label);
244
+ _assertSpecificForm(byInt, r.oid, null, isNative, label);
245
+ return { name: r.name, oid: r.oid };
188
246
  }
189
- if (node.majorType === 2) { var r = _oidName(node, code, label); return { name: r.name, oid: r.oid }; }
190
247
  if (node.majorType === 4 && node.children && node.children.length === 2) {
191
248
  var a = _oidName(node.children[0], code, label);
192
249
  if (node.children[1].majorType !== 2) throw _err(code, label + " algorithm parameters must be a CBOR byte string");
193
250
  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)");
251
+ _assertSpecificForm(byInt, a.oid, node.children[1].content, isNative, label);
194
252
  return { name: a.name, oid: a.oid, parameters: node.children[1].content };
195
253
  }
196
254
  throw _err(code, label + " is not a C509 AlgorithmIdentifier (int / ~oid / [~oid, params])");
@@ -1298,7 +1356,9 @@ function _sdaFromDer(node) {
1298
1356
  var setNode = attr.children[1];
1299
1357
  if (setNode.tagClass !== "universal" || setNode.tagNumber !== asn1.TAGS.SET || !setNode.children || setNode.children.length < 1) return null;
1300
1358
  var dotted = asn1.read.oid(attr.children[0]);
1301
- var nm = oid.name(dotted), tint = nm != null ? ATTR_TO_INT[nm] : undefined;
1359
+ var canonAttr = ATTR_NAME_BY_OID[dotted];
1360
+ var nm = canonAttr === undefined ? oid.name(dotted) : canonAttr;
1361
+ var tint = canonAttr === undefined ? undefined : ATTR_TO_INT[canonAttr];
1302
1362
  var intForm = _sdaTryIntForm(tint, nm, setNode.children);
1303
1363
  if (intForm != null) {
1304
1364
  out.push(cbor.build.int(BigInt(intForm.sign < 0 ? -tint : tint)));
@@ -1311,12 +1371,24 @@ function _sdaFromDer(node) {
1311
1371
  return cbor.build.array(out);
1312
1372
  }
1313
1373
 
1314
- function _tryCompactExtValue(name, der) {
1374
+ /**
1375
+ * @internal Draft sec. 3.7 for extensions. The specific form was available when the registry names
1376
+ * this extension AND the int form can carry this exact value: a compact-codec extension only when
1377
+ * the codec represents it, and one without a codec always, since its int form carries the same
1378
+ * bytes. An extension the codec cannot represent keeps the generic form on every certificate type.
1379
+ */
1380
+ function _assertSpecificExtension(dotted, valContent, isNative) {
1381
+ var canon = EXT_NAME_BY_OID[dotted];
1382
+ if (!isNative || canon === undefined) return;
1383
+ if (EXT_COMPACT[canon] && (!Buffer.isBuffer(valContent) || _tryCompactExtValue(canon, valContent, isNative) == null)) return;
1384
+ throw _err("c509/non-specific-encoding", "extension " + canon + " has a C509 registry id that carries this value, and a natively signed certificate uses only the specific CBOR encoding (draft sec. 3.7)");
1385
+ }
1386
+ function _tryCompactExtValue(name, der, isNative) {
1315
1387
  var compact = _extValueFromDer(name, der);
1316
1388
  if (compact == null) return null;
1317
1389
  var recon;
1318
1390
  try {
1319
- recon = _extValueToDer(name, cbor.decode(compact));
1391
+ recon = _extValueToDer(name, cbor.decode(compact), isNative);
1320
1392
  } catch (_e) {
1321
1393
  return null;
1322
1394
  }
@@ -1365,6 +1437,7 @@ function _extensions(node, isNative) {
1365
1437
  if (valNode.majorType !== 2) throw _err("c509/bad-extensions", "a non-critical ~oid extension value must be a byte string");
1366
1438
  valContent = valNode.content;
1367
1439
  }
1440
+ _assertSpecificExtension(extOid, valContent, isNative);
1368
1441
  }
1369
1442
  out.push({ name: name, oid: extOid, critical: critical, value: valContent || null });
1370
1443
  }
@@ -1420,6 +1493,10 @@ function _reconSpki(spkAlg, keyBytes, rsaKey) {
1420
1493
  return b.sequence([b.sequence([b.oid(oid.byName("ecPublicKey")), b.oid(oid.byName(spkAlg.curve))]), b.bitString(point, 0)]);
1421
1494
  }
1422
1495
  if (spkAlg.name === "rsaEncryption") {
1496
+ /** @internal RFC 3279 sec. 2.3.1 fixes rsaEncryption parameters to the DER NULL. */
1497
+ if (!spkAlg.parameters || !spkAlg.parameters.equals(b.nullValue())) {
1498
+ throw _err("c509/bad-spki", "rsaEncryption parameters must be the DER NULL (RFC 3279 sec. 2.3.1); the bare ~oid form carries none");
1499
+ }
1423
1500
  var rsaPk = b.sequence([b.integer(rsaKey.modulus), b.integer(rsaKey.exponent)]);
1424
1501
  return b.sequence([b.sequence([b.oid(oid.byName("rsaEncryption")), b.nullValue()]), b.bitString(rsaPk, 0)]);
1425
1502
  }
@@ -1523,7 +1600,7 @@ function parse(input) {
1523
1600
  var serial = _biguint(serialBytes, "c509/bad-serial", "certificateSerialNumber");
1524
1601
  var sHex = serialBytes.content.toString("hex");
1525
1602
 
1526
- var sigAlg = _algorithm(f[2], SIG_ALG_BY_INT, "c509/unknown-algorithm", "issuerSignatureAlgorithm");
1603
+ var sigAlg = _algorithm(f[2], SIG_ALG_BY_INT, "c509/unknown-algorithm", "issuerSignatureAlgorithm", type === 2);
1527
1604
  var issuer = _name509(f[3], false, type === 2);
1528
1605
  var notBefore = _time(f[4], false, "validityNotBefore");
1529
1606
  var notAfter = _time(f[5], true, "validityNotAfter");
@@ -1531,7 +1608,7 @@ function parse(input) {
1531
1608
  if (issuer === null && (!subject || !subject.rdns || subject.rdns.length === 0)) {
1532
1609
  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
1610
  }
1534
- var spkAlg = _algorithm(f[7], PK_ALG_BY_INT, "c509/unknown-algorithm", "subjectPublicKeyAlgorithm");
1611
+ var spkAlg = _algorithm(f[7], PK_ALG_BY_INT, "c509/unknown-algorithm", "subjectPublicKeyAlgorithm", type === 2);
1535
1612
  var subjectPublicKey = null, rsaKey = null;
1536
1613
  if (spkAlg.name === "rsaEncryption") {
1537
1614
  if (f[8].majorType === 2) rsaKey = { modulus: _biguint(f[8], "c509/bad-spki", "RSA modulus"), exponent: 65537n };
@@ -1578,29 +1655,41 @@ function matches(node) {
1578
1655
  }
1579
1656
 
1580
1657
 
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
1658
  var ATTR_TO_INT = Object.create(null);
1584
1659
  Object.keys(ATTR_BY_INT).forEach(function (k) { ATTR_TO_INT[ATTR_BY_INT[k]] = Number(k); });
1660
+ /** @internal An attribute's C509 registry membership follows its dotted OID, not its display name. */
1661
+ var ATTR_NAME_BY_OID = Object.create(null);
1662
+ Object.keys(ATTR_BY_INT).forEach(function (k) { ATTR_NAME_BY_OID[oid.byName(ATTR_BY_INT[k])] = ATTR_BY_INT[k]; });
1585
1663
  var EXT_TO_INT = Object.create(null);
1586
1664
  Object.keys(EXT_BY_INT).forEach(function (k) { if (EXT_COMPACT[EXT_BY_INT[k]]) EXT_TO_INT[EXT_BY_INT[k]] = Number(k); });
1665
+ /** @internal An extension's C509 registry membership follows its dotted OID, not its display name. */
1666
+ var EXT_NAME_BY_OID = Object.create(null);
1667
+ Object.keys(EXT_BY_INT).forEach(function (k) { EXT_NAME_BY_OID[oid.byName(EXT_BY_INT[k])] = EXT_BY_INT[k]; });
1587
1668
 
1588
1669
  function _minBytes(n) {
1670
+ if (typeof n !== "bigint") throw _err("c509/bad-serial", "a ~biguint value must be a BigInt, got " + _describe(n));
1589
1671
  if (n < 0n) throw _err("c509/bad-serial", "a ~biguint value must be non-negative");
1590
1672
  if (n === 0n) return Buffer.alloc(0);
1591
1673
  var hex = n.toString(16); if (hex.length % 2) hex = "0" + hex;
1592
1674
  return Buffer.from(hex, "hex");
1593
1675
  }
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)]);
1676
+ function _encAlgorithm(alg, byInt) {
1677
+ var dotted = alg.oid || oid.byName(alg.name);
1678
+ if (!dotted) throw _err("c509/bad-input", "algorithm " + alg.name + " carries no OID and its name is not in the OID registry");
1679
+ var supplied = alg.parameters && alg.parameters.length ? alg.parameters : null;
1680
+ var i = _specificInt(byInt, dotted, supplied, alg.curve);
1681
+ if (i !== undefined) return cbor.build.int(BigInt(i));
1682
+ var oidBytes = cbor.build.byteString(asn1.encodeOidContent(dotted));
1683
+ if (supplied !== null) return cbor.build.array([oidBytes, cbor.build.byteString(supplied)]);
1599
1684
  return oidBytes;
1600
1685
  }
1601
1686
  function _encSpecialText(rdn) {
1602
- if (rdn.eui64) return cbor.build.tag(48, cbor.build.byteString(rdn.eui64));
1603
- var v = String(rdn.value);
1687
+ if (rdn.eui64 != null) {
1688
+ if (!Buffer.isBuffer(rdn.eui64)) throw _err("c509/bad-name", "an RDN eui64 must be a Buffer, got " + _describe(rdn.eui64));
1689
+ return cbor.build.tag(48, cbor.build.byteString(rdn.eui64));
1690
+ }
1691
+ if (typeof rdn.value !== "string") throw _err("c509/bad-name", "an RDN value must be a string, got " + _describe(rdn.value));
1692
+ var v = rdn.value;
1604
1693
  if (v.length >= 2 && _isEvenHexLower(v)) return cbor.build.byteString(Buffer.from(v, "hex"));
1605
1694
  return cbor.build.textString(v);
1606
1695
  }
@@ -1610,11 +1699,15 @@ function _encName(name, isSubject) {
1610
1699
  throw _err("c509/bad-name", "the subject Name is required");
1611
1700
  }
1612
1701
  var rdns = name.rdns || [];
1702
+ _assertDense(rdns, "c509/bad-name", "the rdns array");
1703
+ rdns.forEach(function (rdn) {
1704
+ if (!rdn || typeof rdn !== "object") throw _err("c509/bad-name", "each RDN must be an object, got " + _describe(rdn));
1705
+ });
1613
1706
  if (rdns.length === 1 && rdns[0].type === "commonName" && !rdns[0].printable) return _encSpecialText(rdns[0]);
1614
1707
  var items = [];
1615
1708
  rdns.forEach(function (rdn) {
1616
1709
  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");
1710
+ if (ai === undefined) throw _err("c509/bad-name", "attribute type " + _describe(rdn.type) + " has no C509 registry int");
1618
1711
  items.push(cbor.build.int(BigInt(rdn.printable ? -ai : ai)));
1619
1712
  items.push(_encSpecialText(rdn));
1620
1713
  });
@@ -1629,19 +1722,52 @@ function _encSpk(r) {
1629
1722
  if (!Buffer.isBuffer(r.subjectPublicKey)) throw _err("c509/bad-spki", "the subjectPublicKey bytes are missing");
1630
1723
  return cbor.build.byteString(r.subjectPublicKey);
1631
1724
  }
1632
- function _encExtensions(exts) {
1633
- if (exts.length === 1 && exts[0].name === "keyUsage" && typeof exts[0].keyUsageBits === "number") {
1634
- return cbor.build.int(BigInt(exts[0].critical ? -exts[0].keyUsageBits : exts[0].keyUsageBits));
1725
+ /** @internal An extension names itself by name, by OID, or by both; the registry is keyed by name. */
1726
+ function _extIdentity(ext) {
1727
+ var dotted = ext.oid || (typeof ext.name === "string" ? oid.byName(ext.name) : null);
1728
+ var canon = dotted ? EXT_NAME_BY_OID[dotted] : undefined;
1729
+ if (canon !== undefined) return { dotted: dotted, name: canon };
1730
+ return { dotted: dotted, name: typeof ext.name === "string" ? ext.name : (dotted ? oid.name(dotted) : null) };
1731
+ }
1732
+ /**
1733
+ * @internal The bits a lone keyUsage is written as. Derived from a DER value only when the compact
1734
+ * form reproduces that value byte for byte, since the bare integer covers the nine defined bits and
1735
+ * a wider BIT STRING would lose the rest.
1736
+ */
1737
+ function _extKeyUsageBits(ext, isNative) {
1738
+ if (typeof ext.keyUsageBits === "number") return ext.keyUsageBits;
1739
+ if (!Buffer.isBuffer(ext.value) || _tryCompactExtValue("keyUsage", ext.value, isNative) == null) return null;
1740
+ return _keyUsageBitsFromDer(ext.value);
1741
+ }
1742
+ function _encExtensions(exts, isNative) {
1743
+ _assertDense(exts, "c509/bad-input", "the extensions array");
1744
+ exts.forEach(function (ext) {
1745
+ if (!ext || typeof ext !== "object") throw _err("c509/bad-input", "each extension must be an object, got " + _describe(ext));
1746
+ if (ext.oid != null) {
1747
+ if (typeof ext.oid !== "string") throw _err("c509/bad-input", "an extension oid must be a dotted-decimal string, got " + _describe(ext.oid));
1748
+ try { asn1.encodeOidContent(ext.oid); }
1749
+ catch (_oe) { throw _err("c509/bad-input", "an extension oid must be a dotted-decimal string, got " + _describe(ext.oid), _oe); }
1750
+ }
1751
+ if (ext.keyUsageBits != null && (typeof ext.keyUsageBits !== "number" || !Number.isSafeInteger(ext.keyUsageBits))) {
1752
+ throw _err("c509/bad-extensions", "keyUsageBits must be a safe integer, got " + _describe(ext.keyUsageBits));
1753
+ }
1754
+ });
1755
+ if (exts.length === 1 && _extIdentity(exts[0]).name === "keyUsage") {
1756
+ var onlyBits = _extKeyUsageBits(exts[0], isNative);
1757
+ if (typeof onlyBits === "number") return cbor.build.int(BigInt(exts[0].critical ? -onlyBits : onlyBits));
1635
1758
  }
1636
1759
  var items = [];
1637
1760
  exts.forEach(function (ext) {
1638
- var ei = EXT_TO_INT[ext.name];
1639
- var compact = (ei !== undefined && Buffer.isBuffer(ext.value)) ? _tryCompactExtValue(ext.name, ext.value) : null;
1761
+ var ident = _extIdentity(ext);
1762
+ var extDotted = ident.dotted, extName = ident.name;
1763
+ var ei = extName == null ? undefined : EXT_TO_INT[extName];
1764
+ var compact = (ei !== undefined && Buffer.isBuffer(ext.value)) ? _tryCompactExtValue(extName, ext.value, isNative) : null;
1640
1765
  if (compact != null) {
1641
1766
  items.push(cbor.build.int(BigInt(ext.critical ? -ei : ei)));
1642
1767
  items.push(compact);
1643
1768
  } else {
1644
- items.push(cbor.build.byteString(asn1.encodeOidContent(ext.oid || oid.byName(ext.name))));
1769
+ if (!extDotted) throw _err("c509/bad-input", "an extension must carry an OID or a name in the OID registry, got " + _describe(ext.name));
1770
+ items.push(cbor.build.byteString(asn1.encodeOidContent(extDotted)));
1645
1771
  if (!Buffer.isBuffer(ext.value)) throw _err("c509/non-invertible", "extension " + (ext.oid || ext.name) + " has no byte-string value to encode");
1646
1772
  var bs = cbor.build.byteString(ext.value);
1647
1773
  items.push(ext.critical ? cbor.build.array([bs]) : bs);
@@ -1673,18 +1799,17 @@ function _encodeC509Array(r) {
1673
1799
  return out;
1674
1800
  }
1675
1801
  _requireResultShape(r);
1676
- var pkKey = r.subjectPublicKeyAlgorithm.curve ? r.subjectPublicKeyAlgorithm.name + "|" + r.subjectPublicKeyAlgorithm.curve : r.subjectPublicKeyAlgorithm.name;
1677
1802
  var arr = cbor.build.array([
1678
1803
  cbor.build.int(BigInt(r.certificateType)),
1679
1804
  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),
1805
+ _encAlgorithm(r.signatureAlgorithm, SIG_ALG_BY_INT),
1681
1806
  _encName(r.issuer, false),
1682
1807
  _validityUint(r.validity.notBefore, "validityNotBefore"),
1683
1808
  r.validity.notAfter === null ? cbor.build.nullValue() : _validityUint(r.validity.notAfter, "validityNotAfter"),
1684
1809
  _encName(r.subject, true),
1685
- _encAlgorithm(r.subjectPublicKeyAlgorithm, PK_ALG_TO_INT, pkKey),
1810
+ _encAlgorithm(r.subjectPublicKeyAlgorithm, PK_ALG_BY_INT),
1686
1811
  _encSpk(r),
1687
- _encExtensions(r.extensions),
1812
+ _encExtensions(r.extensions, Number(r.certificateType) === 2),
1688
1813
  cbor.build.byteString(r.signatureValue),
1689
1814
  ]);
1690
1815
  parse(arr);
@@ -1748,7 +1873,9 @@ function _c509NameFromDer(nameBytes) {
1748
1873
  (node.children || []).forEach(function (rdnSet) {
1749
1874
  if (!rdnSet.children || rdnSet.children.length !== 1) throw _err("c509/non-invertible", "a C509 Name requires single-attribute RDNs");
1750
1875
  var attr = rdnSet.children[0];
1751
- var attrName = oid.name(asn1.read.oid(attr.children[0]));
1876
+ var attrDotted = asn1.read.oid(attr.children[0]);
1877
+ var attrCanon = ATTR_NAME_BY_OID[attrDotted];
1878
+ var attrName = attrCanon === undefined ? oid.name(attrDotted) : attrCanon;
1752
1879
  if (attrName == null || ATTR_TO_INT[attrName] === undefined) throw _err("c509/non-invertible", "attribute type " + attrName + " has no C509 registry integer");
1753
1880
  var valNode = attr.children[1];
1754
1881
  var value;
@@ -1795,19 +1922,19 @@ function _minCurveForMag(mag) {
1795
1922
  for (var i = 0; i < CURVE_ORDER.length; i++) { if (mag <= WEBCRYPTO_FIELD_BYTES[CURVE_ORDER[i]]) return CURVE_ORDER[i]; }
1796
1923
  return null;
1797
1924
  }
1798
- function _resolveIssuerSigCurve(c, opts) {
1925
+ function _resolveIssuerSigCurve(c, opts, sigAlgName) {
1799
1926
  var mag = _sigMagWidth(c.signatureValue.bytes), curve;
1800
1927
  if (opts && opts.issuerCurve != null) {
1801
1928
  curve = String(opts.issuerCurve);
1802
1929
  if (NODE_TO_WEBCRYPTO[curve]) curve = NODE_TO_WEBCRYPTO[curve];
1803
1930
  if (!WEBCRYPTO_FIELD_BYTES[curve]) throw _err("c509/bad-input", "opts.issuerCurve must be P-256 / P-384 / P-521 (or prime256v1 / secp384r1 / secp521r1); got " + opts.issuerCurve);
1804
1931
  } else {
1805
- curve = SIG_ALG_TO_CURVE[c.signatureAlgorithm.name];
1806
- if (!curve) throw _err("c509/non-invertible", "cannot resolve the issuer signing curve for signature algorithm " + c.signatureAlgorithm.name);
1932
+ curve = SIG_ALG_TO_CURVE[sigAlgName];
1933
+ if (!curve) throw _err("c509/non-invertible", "cannot resolve the issuer signing curve for signature algorithm " + sigAlgName);
1807
1934
  var minCurve = _minCurveForMag(mag);
1808
1935
  if (!minCurve) throw _err("c509/non-invertible", "the ECDSA signature r/s width " + mag + " exceeds every supported curve field");
1809
- if (WEBCRYPTO_FIELD_BYTES[curve] < mag) throw _err("c509/non-invertible", "the issuer signed with a non-standard digest/curve pairing (r/s width " + mag + " exceeds the " + curve + " field implied by " + c.signatureAlgorithm.name + "); pass opts.issuerCurve (P-256 / P-384 / P-521)");
1810
- if (WEBCRYPTO_FIELD_BYTES[curve] > WEBCRYPTO_FIELD_BYTES[minCurve]) throw _err("c509/non-invertible", "signature algorithm " + c.signatureAlgorithm.name + " does not uniquely determine the issuer curve (r/s width " + mag + " also fits " + minCurve + "); pass opts.issuerCurve (P-256 / P-384 / P-521)");
1936
+ if (WEBCRYPTO_FIELD_BYTES[curve] < mag) throw _err("c509/non-invertible", "the issuer signed with a non-standard digest/curve pairing (r/s width " + mag + " exceeds the " + curve + " field implied by " + sigAlgName + "); pass opts.issuerCurve (P-256 / P-384 / P-521)");
1937
+ if (WEBCRYPTO_FIELD_BYTES[curve] > WEBCRYPTO_FIELD_BYTES[minCurve]) throw _err("c509/non-invertible", "signature algorithm " + sigAlgName + " does not uniquely determine the issuer curve (r/s width " + mag + " also fits " + minCurve + "); pass opts.issuerCurve (P-256 / P-384 / P-521)");
1811
1938
  }
1812
1939
  if (mag > WEBCRYPTO_FIELD_BYTES[curve]) throw _err("c509/non-invertible", "the ECDSA signature r/s width " + mag + " does not fit the resolved " + curve + " field");
1813
1940
  return curve;
@@ -1822,13 +1949,16 @@ _derToType3 = function (input, opts) {
1822
1949
  var c;
1823
1950
  try { c = x509.parse(input); } catch (e) { throw _err("c509/bad-input", "the input is not a valid X.509 certificate", e); }
1824
1951
  if (c.version !== 3) throw _err("c509/non-invertible", "C509 covers X.509 v3 certificates; got v" + c.version);
1825
- if (String(c.signatureAlgorithm.name || "").toLowerCase().indexOf("ecdsa") !== 0) throw _err("c509/non-invertible", "type-3 C509 encoding covers only ECDSA-signed certificates; got " + (c.signatureAlgorithm.name || "an unregistered algorithm"));
1826
- if (c.subjectPublicKeyInfo.algorithm.name !== "ecPublicKey") throw _err("c509/non-invertible", "type-3 C509 encoding covers only EC (ecPublicKey) certificates in v1; got " + (c.subjectPublicKeyInfo.algorithm.name || "an unregistered algorithm"));
1952
+ var sigCanon = SIG_ALG_BY_OID[c.signatureAlgorithm.oid];
1953
+ var sigAlgName = sigCanon === undefined ? c.signatureAlgorithm.name : sigCanon;
1954
+ if (String(sigAlgName || "").toLowerCase().indexOf("ecdsa") !== 0) throw _err("c509/non-invertible", "type-3 C509 encoding covers only ECDSA-signed certificates; got " + (sigAlgName || "an unregistered algorithm"));
1955
+ if (c.subjectPublicKeyInfo.algorithm.oid !== oid.byName("ecPublicKey")) throw _err("c509/non-invertible", "type-3 C509 encoding covers only EC (ecPublicKey) certificates in v1; got " + (c.subjectPublicKeyInfo.algorithm.name || "an unregistered algorithm"));
1827
1956
  var curveOid = asn1.read.oid(asn1.decode(c.subjectPublicKeyInfo.algorithm.parameters));
1828
- var curve = oid.name(curveOid);
1957
+ var curveCanon = EC_CURVE_BY_OID[curveOid];
1958
+ var curve = curveCanon === undefined ? oid.name(curveOid) : curveCanon;
1829
1959
  var coordLen = EC_FIELD_BYTES[curve];
1830
1960
  if (!coordLen) throw _err("c509/non-invertible", "unsupported EC subject curve " + (curve || curveOid));
1831
- var sigCurve = _resolveIssuerSigCurve(c, opts);
1961
+ var sigCurve = _resolveIssuerSigCurve(c, opts, sigAlgName);
1832
1962
  var spkiNode = asn1.decode(c.subjectPublicKeyInfo.bytes);
1833
1963
  return {
1834
1964
  certificateType: 3,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blamejs/pki",
3
- "version": "0.6.35",
3
+ "version": "0.6.37",
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:82faf919-c224-4430-992b-dd3055e65ed3",
6
6
  "version": 1,
7
7
  "metadata": {
8
- "timestamp": "2026-09-04T23:41:31.757Z",
8
+ "timestamp": "2026-09-05T03:43:35.188Z",
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.37",
23
23
  "type": "application",
24
24
  "name": "pki",
25
- "version": "0.6.35",
25
+ "version": "0.6.37",
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.37",
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.37",
58
58
  "dependsOn": []
59
59
  }
60
60
  ]