@blamejs/pki 0.6.36 → 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,17 @@ 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
+
7
18
  ## v0.6.36 — 2026-09-04
8
19
 
9
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.
@@ -63,6 +63,11 @@ var PK_ALG_BY_INT = {
63
63
  3: { alg: _name("ecPublicKey"), curve: _name("secp521r1") },
64
64
  };
65
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]; });
66
71
 
67
72
  var ATTR_BY_INT = {
68
73
  0: _name("emailAddress"),
@@ -1351,7 +1356,9 @@ function _sdaFromDer(node) {
1351
1356
  var setNode = attr.children[1];
1352
1357
  if (setNode.tagClass !== "universal" || setNode.tagNumber !== asn1.TAGS.SET || !setNode.children || setNode.children.length < 1) return null;
1353
1358
  var dotted = asn1.read.oid(attr.children[0]);
1354
- 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];
1355
1362
  var intForm = _sdaTryIntForm(tint, nm, setNode.children);
1356
1363
  if (intForm != null) {
1357
1364
  out.push(cbor.build.int(BigInt(intForm.sign < 0 ? -tint : tint)));
@@ -1364,12 +1371,24 @@ function _sdaFromDer(node) {
1364
1371
  return cbor.build.array(out);
1365
1372
  }
1366
1373
 
1367
- 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) {
1368
1387
  var compact = _extValueFromDer(name, der);
1369
1388
  if (compact == null) return null;
1370
1389
  var recon;
1371
1390
  try {
1372
- recon = _extValueToDer(name, cbor.decode(compact));
1391
+ recon = _extValueToDer(name, cbor.decode(compact), isNative);
1373
1392
  } catch (_e) {
1374
1393
  return null;
1375
1394
  }
@@ -1418,6 +1437,7 @@ function _extensions(node, isNative) {
1418
1437
  if (valNode.majorType !== 2) throw _err("c509/bad-extensions", "a non-critical ~oid extension value must be a byte string");
1419
1438
  valContent = valNode.content;
1420
1439
  }
1440
+ _assertSpecificExtension(extOid, valContent, isNative);
1421
1441
  }
1422
1442
  out.push({ name: name, oid: extOid, critical: critical, value: valContent || null });
1423
1443
  }
@@ -1637,8 +1657,14 @@ function matches(node) {
1637
1657
 
1638
1658
  var ATTR_TO_INT = Object.create(null);
1639
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]; });
1640
1663
  var EXT_TO_INT = Object.create(null);
1641
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]; });
1642
1668
 
1643
1669
  function _minBytes(n) {
1644
1670
  if (typeof n !== "bigint") throw _err("c509/bad-serial", "a ~biguint value must be a BigInt, got " + _describe(n));
@@ -1696,7 +1722,24 @@ function _encSpk(r) {
1696
1722
  if (!Buffer.isBuffer(r.subjectPublicKey)) throw _err("c509/bad-spki", "the subjectPublicKey bytes are missing");
1697
1723
  return cbor.build.byteString(r.subjectPublicKey);
1698
1724
  }
1699
- function _encExtensions(exts) {
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) {
1700
1743
  _assertDense(exts, "c509/bad-input", "the extensions array");
1701
1744
  exts.forEach(function (ext) {
1702
1745
  if (!ext || typeof ext !== "object") throw _err("c509/bad-input", "each extension must be an object, got " + _describe(ext));
@@ -1709,18 +1752,20 @@ function _encExtensions(exts) {
1709
1752
  throw _err("c509/bad-extensions", "keyUsageBits must be a safe integer, got " + _describe(ext.keyUsageBits));
1710
1753
  }
1711
1754
  });
1712
- if (exts.length === 1 && exts[0].name === "keyUsage" && typeof exts[0].keyUsageBits === "number") {
1713
- return cbor.build.int(BigInt(exts[0].critical ? -exts[0].keyUsageBits : exts[0].keyUsageBits));
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));
1714
1758
  }
1715
1759
  var items = [];
1716
1760
  exts.forEach(function (ext) {
1717
- var ei = EXT_TO_INT[ext.name];
1718
- 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;
1719
1765
  if (compact != null) {
1720
1766
  items.push(cbor.build.int(BigInt(ext.critical ? -ei : ei)));
1721
1767
  items.push(compact);
1722
1768
  } else {
1723
- var extDotted = ext.oid || (typeof ext.name === "string" ? oid.byName(ext.name) : null);
1724
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));
1725
1770
  items.push(cbor.build.byteString(asn1.encodeOidContent(extDotted)));
1726
1771
  if (!Buffer.isBuffer(ext.value)) throw _err("c509/non-invertible", "extension " + (ext.oid || ext.name) + " has no byte-string value to encode");
@@ -1764,7 +1809,7 @@ function _encodeC509Array(r) {
1764
1809
  _encName(r.subject, true),
1765
1810
  _encAlgorithm(r.subjectPublicKeyAlgorithm, PK_ALG_BY_INT),
1766
1811
  _encSpk(r),
1767
- _encExtensions(r.extensions),
1812
+ _encExtensions(r.extensions, Number(r.certificateType) === 2),
1768
1813
  cbor.build.byteString(r.signatureValue),
1769
1814
  ]);
1770
1815
  parse(arr);
@@ -1828,7 +1873,9 @@ function _c509NameFromDer(nameBytes) {
1828
1873
  (node.children || []).forEach(function (rdnSet) {
1829
1874
  if (!rdnSet.children || rdnSet.children.length !== 1) throw _err("c509/non-invertible", "a C509 Name requires single-attribute RDNs");
1830
1875
  var attr = rdnSet.children[0];
1831
- 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;
1832
1879
  if (attrName == null || ATTR_TO_INT[attrName] === undefined) throw _err("c509/non-invertible", "attribute type " + attrName + " has no C509 registry integer");
1833
1880
  var valNode = attr.children[1];
1834
1881
  var value;
@@ -1875,19 +1922,19 @@ function _minCurveForMag(mag) {
1875
1922
  for (var i = 0; i < CURVE_ORDER.length; i++) { if (mag <= WEBCRYPTO_FIELD_BYTES[CURVE_ORDER[i]]) return CURVE_ORDER[i]; }
1876
1923
  return null;
1877
1924
  }
1878
- function _resolveIssuerSigCurve(c, opts) {
1925
+ function _resolveIssuerSigCurve(c, opts, sigAlgName) {
1879
1926
  var mag = _sigMagWidth(c.signatureValue.bytes), curve;
1880
1927
  if (opts && opts.issuerCurve != null) {
1881
1928
  curve = String(opts.issuerCurve);
1882
1929
  if (NODE_TO_WEBCRYPTO[curve]) curve = NODE_TO_WEBCRYPTO[curve];
1883
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);
1884
1931
  } else {
1885
- curve = SIG_ALG_TO_CURVE[c.signatureAlgorithm.name];
1886
- 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);
1887
1934
  var minCurve = _minCurveForMag(mag);
1888
1935
  if (!minCurve) throw _err("c509/non-invertible", "the ECDSA signature r/s width " + mag + " exceeds every supported curve field");
1889
- 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)");
1890
- 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)");
1891
1938
  }
1892
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");
1893
1940
  return curve;
@@ -1902,13 +1949,16 @@ _derToType3 = function (input, opts) {
1902
1949
  var c;
1903
1950
  try { c = x509.parse(input); } catch (e) { throw _err("c509/bad-input", "the input is not a valid X.509 certificate", e); }
1904
1951
  if (c.version !== 3) throw _err("c509/non-invertible", "C509 covers X.509 v3 certificates; got v" + c.version);
1905
- 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"));
1906
- 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"));
1907
1956
  var curveOid = asn1.read.oid(asn1.decode(c.subjectPublicKeyInfo.algorithm.parameters));
1908
- var curve = oid.name(curveOid);
1957
+ var curveCanon = EC_CURVE_BY_OID[curveOid];
1958
+ var curve = curveCanon === undefined ? oid.name(curveOid) : curveCanon;
1909
1959
  var coordLen = EC_FIELD_BYTES[curve];
1910
1960
  if (!coordLen) throw _err("c509/non-invertible", "unsupported EC subject curve " + (curve || curveOid));
1911
- var sigCurve = _resolveIssuerSigCurve(c, opts);
1961
+ var sigCurve = _resolveIssuerSigCurve(c, opts, sigAlgName);
1912
1962
  var spkiNode = asn1.decode(c.subjectPublicKeyInfo.bytes);
1913
1963
  return {
1914
1964
  certificateType: 3,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blamejs/pki",
3
- "version": "0.6.36",
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:8d50e6dd-1ddf-46f8-b8db-ed97195096c3",
5
+ "serialNumber": "urn:uuid:82faf919-c224-4430-992b-dd3055e65ed3",
6
6
  "version": 1,
7
7
  "metadata": {
8
- "timestamp": "2026-09-05T01:58:33.232Z",
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.36",
22
+ "bom-ref": "@blamejs/pki@0.6.37",
23
23
  "type": "application",
24
24
  "name": "pki",
25
- "version": "0.6.36",
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.36",
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.36",
57
+ "ref": "@blamejs/pki@0.6.37",
58
58
  "dependsOn": []
59
59
  }
60
60
  ]