@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 +11 -0
- package/lib/schema-c509.js +70 -20
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
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.
|
package/lib/schema-c509.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
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"
|
|
1713
|
-
|
|
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
|
|
1718
|
-
var
|
|
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
|
|
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[
|
|
1886
|
-
if (!curve) throw _err("c509/non-invertible", "cannot resolve the issuer signing curve for signature algorithm " +
|
|
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 " +
|
|
1890
|
-
if (WEBCRYPTO_FIELD_BYTES[curve] > WEBCRYPTO_FIELD_BYTES[minCurve]) throw _err("c509/non-invertible", "signature algorithm " +
|
|
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
|
-
|
|
1906
|
-
|
|
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
|
|
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
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:
|
|
5
|
+
"serialNumber": "urn:uuid:82faf919-c224-4430-992b-dd3055e65ed3",
|
|
6
6
|
"version": 1,
|
|
7
7
|
"metadata": {
|
|
8
|
-
"timestamp": "2026-09-
|
|
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.
|
|
22
|
+
"bom-ref": "@blamejs/pki@0.6.37",
|
|
23
23
|
"type": "application",
|
|
24
24
|
"name": "pki",
|
|
25
|
-
"version": "0.6.
|
|
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.
|
|
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.
|
|
57
|
+
"ref": "@blamejs/pki@0.6.37",
|
|
58
58
|
"dependsOn": []
|
|
59
59
|
}
|
|
60
60
|
]
|