@blamejs/pki 0.6.33 → 0.6.35
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 +22 -0
- package/lib/acme.js +4 -2
- package/lib/cmp-build.js +1 -1
- package/lib/cmp-session.js +1 -1
- package/lib/est.js +2 -2
- package/lib/guard-async.js +17 -1
- package/lib/scep.js +1 -1
- package/lib/schema-c509.js +21 -3
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,28 @@ 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.35 — 2026-09-04
|
|
8
|
+
|
|
9
|
+
Three redundant C509 spellings are refused, so a certificate has one encoding and an issuer signature cannot cover two different C509 byte strings.
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
|
|
13
|
+
- pki.schema.c509.encode refuses a serialNumberHex that is not an even-length hexadecimal string instead of decoding a prefix of it. An odd-length or partly non-hexadecimal value previously became a different, valid serial number without any error, which the encoder's own re-parse could not catch. The check applies whether the result is re-emitted from its preserved field bytes or built field by field, so a caller is told the value is malformed rather than having it quietly ignored.
|
|
14
|
+
|
|
15
|
+
### Security
|
|
16
|
+
|
|
17
|
+
- pki.schema.c509.parse refuses an unsigned bignum whose first octet is 0x00, including a value that is only that octet, so every number has exactly one C509 encoding (draft sec. 3.1.2). A zero serial previously had two spellings that reconstructed byte-identical DER, letting one issuer signature cover two distinct C509 certificates. The rule covers the certificate serial number, the authorityKeyIdentifier serial, and the RSA modulus and exponent.
|
|
18
|
+
- pki.schema.c509.parse refuses an RSA subjectPublicKey written as [modulus, 65537] instead of the bare modulus. Draft sec. 3.1.9 omits the array and the exponent when the exponent is 65537, so the array spelling of such a key is a second encoding of one certificate, reconstructing byte-identical DER. An RSA key with any other exponent still uses the array form.
|
|
19
|
+
- pki.schema.c509.parse refuses a notAfter written as the epoch 253402300799 rather than the CBOR simple value null. Draft sec. 3.1.5 encodes the no-expiration time 99991231235959Z as null, so the explicit epoch is a second spelling that reconstructs the same GeneralizedTime and therefore the same DER. A notBefore is unaffected, and pki.schema.c509.encode refuses that Date on notAfter instead of emitting a form the decoder rejects.
|
|
20
|
+
|
|
21
|
+
## v0.6.34 — 2026-09-04
|
|
22
|
+
|
|
23
|
+
An injected transport that does not return a promise is reported as a typed option error naming the option, in every client that accepts one.
|
|
24
|
+
|
|
25
|
+
### Changed
|
|
26
|
+
|
|
27
|
+
- pki.est, pki.acme, pki.cmp, and pki.scep reject with est/bad-input, acme/bad-input, cmp/bad-input, or scep/bad-input when opts.transport returns something that is not a promise of the response, instead of failing with a raw TypeError or a misleading response verdict. A transport returning any thenable is still accepted, and the documented signature now states the promise contract.
|
|
28
|
+
|
|
7
29
|
## v0.6.33 — 2026-09-04
|
|
8
30
|
|
|
9
31
|
pki.transport reports the RFC 9266 tls-exporter channel binding of a TLS 1.3 connection, so every connection the toolkit opens now carries the binding its protocol version defines.
|
package/lib/acme.js
CHANGED
|
@@ -1763,7 +1763,7 @@ function _parseLinkNext(headers, base) { var m = _parseLinkRelation(headers, bas
|
|
|
1763
1763
|
*
|
|
1764
1764
|
* @opts
|
|
1765
1765
|
* - `accountKey` / `accountJwk` / `alg` -- REQUIRED: the account private key, its public JWK, and the JWS alg.
|
|
1766
|
-
* - `transport` -- injectable transport(request) -> {status, headers, body}
|
|
1766
|
+
* - `transport` -- injectable `transport(request) -> Promise<{status, headers, body}>`; default pki.transport.https. It must return a promise of the response; anything else is `acme/bad-input`.
|
|
1767
1767
|
* - `tls` -- { anchors, useSystemStore, cert, key, minVersion, servername, checkServerIdentity } for the default transport.
|
|
1768
1768
|
* - `timeout` / `maxResponseBytes` / `maxRedirects` -- transport budgets; `maxNonceRetries` -- badNonce retry cap (default 1).
|
|
1769
1769
|
* - `proxy` -- reach the ACME server through a forward HTTP proxy (`{ url, auth?, tls? }`; see pki.transport).
|
|
@@ -1911,7 +1911,9 @@ function client(directoryUrl, opts) {
|
|
|
1911
1911
|
function _send(method, url, headers, body) { return _sendFollowing(method, url, headers, body, 0); }
|
|
1912
1912
|
function _sendFollowing(method, url, headers, body, redirects) {
|
|
1913
1913
|
var reqHeaders = Object.assign({ "user-agent": CLIENT_USER_AGENT }, headers || {});
|
|
1914
|
-
return
|
|
1914
|
+
return guard.async.deferred(function () {
|
|
1915
|
+
return guard.async.awaited(transport({ method: method, url: url, headers: reqHeaders, body: body, tls: _tlsFor(url), proxy: budgets.proxy, timeout: budgets.timeout, maxResponseBytes: budgets.maxResponseBytes }), E, "acme/bad-input", "opts.transport");
|
|
1916
|
+
}).then(function (res) {
|
|
1915
1917
|
res = res || {};
|
|
1916
1918
|
var h = {};
|
|
1917
1919
|
Object.keys(res.headers || {}).forEach(function (k) { h[k.toLowerCase()] = res.headers[k]; });
|
package/lib/cmp-build.js
CHANGED
|
@@ -809,7 +809,7 @@ function _transfer(url, message, opts) {
|
|
|
809
809
|
});
|
|
810
810
|
headers["content-type"] = PKIXCMP;
|
|
811
811
|
var tls = _tlsForTransfer(opts);
|
|
812
|
-
return
|
|
812
|
+
return guard.async.awaited(transport({ method: "POST", url: parsedUrl.href, headers: headers, body: der, tls: tls, proxy: proxy, timeout: timeout, maxResponseBytes: maxResponseBytes }), _err, "cmp/bad-input", "opts.transport").then(function (res) {
|
|
813
813
|
res = res || {};
|
|
814
814
|
var h = {};
|
|
815
815
|
Object.keys(res.headers || {}).forEach(function (k) { h[k.toLowerCase()] = res.headers[k]; });
|
package/lib/cmp-session.js
CHANGED
|
@@ -285,7 +285,7 @@ function _certConfHash(certDer) {
|
|
|
285
285
|
* - `expectedSender` -- optional CA signer certificate (DER Buffer / PEM string / already-parsed `pki.schema.x509.parse` object); when set, every signed response's authenticated header sender MUST bind to it under the RFC 5280 sec. 7 subject-or-subjectAltName rule cmp.verify uses, so a re-encoded-but-equivalent DN and an empty-subject CA named only by a directoryName SAN both match. Given as bytes/PEM it also resolves a first response that omits its own extraCerts (a CA that assumes the client already holds its certificate). Absent, the session pins the first signed response's signer certificate and requires every later leg's sender to bind to it, rejecting a switch to a different trusted signer while permitting same-identity certificate/key rotation.
|
|
286
286
|
* - `implicitConfirm` -- request implicit confirmation (skip the certConf leg when the CA grants it).
|
|
287
287
|
* - `acceptCert` -- an async policy `(certDer, { status, grantedWithMods }) => boolean` consulted before the certConf; return true to accept, anything else to veto (a `grantedWithMods` certificate the CA changed). A veto sends a rejecting certConf and yields `outcome: "rejected"` with the certificate still surfaced. Incompatible with `implicitConfirm` (no reject leg exists), so the combination throws at construction.
|
|
288
|
-
* - `transport` -- injectable transport(request) -> {status, headers, body}
|
|
288
|
+
* - `transport` -- injectable `transport(request) -> Promise<{status, headers, body}>`; default pki.transport.https. It must return a promise of the response; anything else is `cmp/bad-input`.
|
|
289
289
|
* - `tls` / `headers` / `timeout` / `maxResponseBytes` -- transport config + budgets.
|
|
290
290
|
* - `proxy` -- reach the CA through a forward HTTP proxy (`{ url, auth?, tls? }`; see pki.transport).
|
|
291
291
|
* - `maxPolls` / `maxTotalWait` / `sleep` -- poll-loop budgets + an injectable sleeper; `time` -- verify-time clock.
|
package/lib/est.js
CHANGED
|
@@ -882,7 +882,7 @@ function _drive(method, url, body, headers, opts, transport, budgets) {
|
|
|
882
882
|
}
|
|
883
883
|
function step() {
|
|
884
884
|
var hopBody = (typeof body === "function" && budgets.onAttempt) ? budgets.onAttempt() : body;
|
|
885
|
-
return transport({ method: method, url: url.href, headers: _headersFor(url), body: hopBody, tls: _tlsFor(url), proxy: budgets.proxy, timeout: budgets.timeout, maxResponseBytes: budgets.maxResponseBytes }).then(function (res) {
|
|
885
|
+
return guard.async.awaited(transport({ method: method, url: url.href, headers: _headersFor(url), body: hopBody, tls: _tlsFor(url), proxy: budgets.proxy, timeout: budgets.timeout, maxResponseBytes: budgets.maxResponseBytes }), E, "est/bad-input", "opts.transport").then(function (res) {
|
|
886
886
|
res = res || {};
|
|
887
887
|
var blen = _bodyLen(res);
|
|
888
888
|
if (blen > budgets.maxResponseBytes) throw E("est/response-too-large", "the response body (" + blen + " bytes) exceeds the " + budgets.maxResponseBytes + "-byte cap (RFC 7030 sec. 6)");
|
|
@@ -1486,7 +1486,7 @@ function _enroll(op, baseUrl, csrInput, opts, proxySnap) {
|
|
|
1486
1486
|
* anchor on the next call.
|
|
1487
1487
|
*
|
|
1488
1488
|
* @opts
|
|
1489
|
-
* - `transport`: an injected transport(request) -> {status, headers, body, tls}
|
|
1489
|
+
* - `transport`: an injected `transport(request) -> Promise<{status, headers, body, tls}>`; default pki.transport.https. It must return a promise of the response; anything else is `est/bad-input`.
|
|
1490
1490
|
* - `tls` -- { anchors, useSystemStore, cert, key, minVersion, servername, checkServerIdentity }.
|
|
1491
1491
|
* - `label` -- an OPTIONAL CA label path segment; `timeout` / `maxResponseBytes` / `maxRedirects` -- budgets.
|
|
1492
1492
|
* - `proxy` -- reach the CA through a forward HTTP proxy (`{ url, auth?, tls? }`; see pki.transport).
|
package/lib/guard-async.js
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
var _intrinsic = require("./guard-intrinsic");
|
|
6
6
|
var _resolve = _intrinsic.uncurry(_intrinsic.promiseResolve);
|
|
7
7
|
var _reject = _intrinsic.uncurry(_intrinsic.promiseReject);
|
|
8
|
+
var _apply = _intrinsic.apply;
|
|
8
9
|
var _Promise = Promise;
|
|
9
10
|
// @internal
|
|
10
11
|
// @enforced-by behavioral -- the rule has no rename-proof code shape (it is the ABSENCE of a wrapper
|
|
@@ -13,4 +14,19 @@ function deferred(body) {
|
|
|
13
14
|
catch (e) { return _reject(_Promise, e); }
|
|
14
15
|
}
|
|
15
16
|
|
|
16
|
-
|
|
17
|
+
// @internal
|
|
18
|
+
// @enforced-by behavioral -- a RED vector in each client suite refuses a transport yielding no thenable
|
|
19
|
+
function awaited(value, E, code, label) {
|
|
20
|
+
var thenFn;
|
|
21
|
+
try { thenFn = value == null ? null : value.then; }
|
|
22
|
+
catch (_te) { thenFn = null; }
|
|
23
|
+
if (typeof thenFn !== "function") {
|
|
24
|
+
throw E(code, label + " must return a promise of the response; got " +
|
|
25
|
+
(value === null ? "null" : typeof value));
|
|
26
|
+
}
|
|
27
|
+
return _resolve(_Promise, {
|
|
28
|
+
then: function (resolve, reject) { _apply(thenFn, value, [resolve, reject]); },
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
module.exports = _intrinsic.freeze({ deferred: deferred, awaited: awaited });
|
package/lib/scep.js
CHANGED
|
@@ -700,7 +700,7 @@ function _drive(method, url, body, headers, opts, transport, budgets) {
|
|
|
700
700
|
var req = { method: curMethod, url: current.href, headers: headers, tls: _hopTls(current), timeout: budgets.timeout, maxResponseBytes: budgets.maxResponseBytes };
|
|
701
701
|
if (proxySnap != null) req.proxy = proxySnap;
|
|
702
702
|
if (curBody != null) req.body = curBody;
|
|
703
|
-
return transport(req).then(function (res) {
|
|
703
|
+
return guard.async.awaited(transport(req), _err, "scep/bad-input", "opts.transport").then(function (res) {
|
|
704
704
|
res = res || {};
|
|
705
705
|
var blen = _responseLen(res.body);
|
|
706
706
|
if (blen > budgets.maxResponseBytes) throw _err("scep/response-too-large", "the response body (" + blen + " bytes) exceeds the " + budgets.maxResponseBytes + "-byte cap");
|
package/lib/schema-c509.js
CHANGED
|
@@ -33,6 +33,7 @@ var C = constants;
|
|
|
33
33
|
var C509Error = frameworkError.C509Error;
|
|
34
34
|
function _err(code, message, cause) { return new C509Error(code, message, cause); }
|
|
35
35
|
var MAX_EPOCH_SECONDS = 8640000000000n;
|
|
36
|
+
var NO_EXPIRY_SECONDS = 253402300799n;
|
|
36
37
|
|
|
37
38
|
function _name(n) { var d = oid.byName(n); if (!d) { throw new Error("schema-c509: unregistered OID name " + JSON.stringify(n)); } return n; }
|
|
38
39
|
|
|
@@ -142,14 +143,29 @@ function _biguint(node, code, label) {
|
|
|
142
143
|
if (!node || node.majorType !== 2) throw _err(code, label + " must be an unwrapped CBOR byte string (~biguint)");
|
|
143
144
|
var b = node.content;
|
|
144
145
|
if (b.length > C.LIMITS.CBOR_MAX_BIGUINT_BYTES) throw _err(code, label + " exceeds the ~biguint byte cap");
|
|
145
|
-
if (b.length >
|
|
146
|
+
if (b.length > 0 && b[0] === 0x00) throw _err("c509/non-minimal-serial", label + " has a redundant leading 0x00 (~biguint omits the sign octet)");
|
|
146
147
|
return b.length ? BigInt("0x" + b.toString("hex")) : 0n;
|
|
147
148
|
}
|
|
148
149
|
|
|
150
|
+
function _hexBytes(s, code, label) {
|
|
151
|
+
if (typeof s !== "string") throw _err(code, label + " must be a hexadecimal string");
|
|
152
|
+
if ((s.length % 2) !== 0) throw _err(code, label + " must have an even number of hexadecimal digits");
|
|
153
|
+
for (var i = 0; i < s.length; i++) {
|
|
154
|
+
var c = s.charCodeAt(i);
|
|
155
|
+
if (!((c >= 48 && c <= 57) || (c >= 97 && c <= 102) || (c >= 65 && c <= 70))) {
|
|
156
|
+
throw _err(code, label + " must be hexadecimal");
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
return Buffer.from(s, "hex");
|
|
160
|
+
}
|
|
161
|
+
|
|
149
162
|
function _time(node, allowNull, label) {
|
|
150
163
|
if (allowNull && node.majorType === 7 && node.ai === 22) return null;
|
|
151
164
|
if (node.majorType !== 0) throw _err("c509/bad-validity", label + " must be an unwrapped CBOR epoch integer (~time)");
|
|
152
165
|
var secs = node.argument;
|
|
166
|
+
if (allowNull && secs === NO_EXPIRY_SECONDS) {
|
|
167
|
+
throw _err("c509/bad-validity", label + " of 99991231235959Z is encoded as the CBOR simple value null (draft sec. 3.1.5), not as the epoch " + NO_EXPIRY_SECONDS);
|
|
168
|
+
}
|
|
153
169
|
if (secs > MAX_EPOCH_SECONDS) throw _err("c509/bad-validity", label + " is outside the representable Date range");
|
|
154
170
|
return new Date(C.TIME.seconds(Number(secs)));
|
|
155
171
|
}
|
|
@@ -1521,6 +1537,7 @@ function parse(input) {
|
|
|
1521
1537
|
if (f[8].majorType === 2) rsaKey = { modulus: _biguint(f[8], "c509/bad-spki", "RSA modulus"), exponent: 65537n };
|
|
1522
1538
|
else if (f[8].majorType === 4 && f[8].children && f[8].children.length === 2) {
|
|
1523
1539
|
rsaKey = { modulus: _biguint(f[8].children[0], "c509/bad-spki", "RSA modulus"), exponent: _biguint(f[8].children[1], "c509/bad-spki", "RSA exponent") };
|
|
1540
|
+
if (rsaKey.exponent === 65537n) throw _err("c509/bad-spki", "an RSA subjectPublicKey with exponent 65537 omits the array and the exponent (sec. 3.1.9)");
|
|
1524
1541
|
} else throw _err("c509/bad-spki", "an RSA subjectPublicKey must be a ~biguint modulus or [modulus, exponent]");
|
|
1525
1542
|
if (rsaKey.modulus < 1n || rsaKey.exponent < 1n) throw _err("c509/bad-spki", "an RSA modulus and public exponent must be positive");
|
|
1526
1543
|
} else {
|
|
@@ -1650,6 +1667,7 @@ function _validityUint(date, label) {
|
|
|
1650
1667
|
function _encodeC509Array(r) {
|
|
1651
1668
|
if (Buffer.isBuffer(r._fieldBytes)) {
|
|
1652
1669
|
if (!Buffer.isBuffer(r.signatureValue)) throw _err("c509/bad-input", "a re-emitted certificate must carry a Buffer signatureValue");
|
|
1670
|
+
if (r.serialNumberHex != null) _hexBytes(r.serialNumberHex, "c509/bad-serial", "serialNumberHex");
|
|
1653
1671
|
var out = Buffer.concat([Buffer.from([0x8b]), r._fieldBytes, cbor.build.byteString(r.signatureValue)]);
|
|
1654
1672
|
parse(out);
|
|
1655
1673
|
return out;
|
|
@@ -1658,7 +1676,7 @@ function _encodeC509Array(r) {
|
|
|
1658
1676
|
var pkKey = r.subjectPublicKeyAlgorithm.curve ? r.subjectPublicKeyAlgorithm.name + "|" + r.subjectPublicKeyAlgorithm.curve : r.subjectPublicKeyAlgorithm.name;
|
|
1659
1677
|
var arr = cbor.build.array([
|
|
1660
1678
|
cbor.build.int(BigInt(r.certificateType)),
|
|
1661
|
-
cbor.build.byteString(r.serialNumberHex != null ?
|
|
1679
|
+
cbor.build.byteString(r.serialNumberHex != null ? _hexBytes(r.serialNumberHex, "c509/bad-serial", "serialNumberHex") : _minBytes(r.serialNumber)),
|
|
1662
1680
|
_encAlgorithm(r.signatureAlgorithm, SIG_ALG_TO_INT, r.signatureAlgorithm.name),
|
|
1663
1681
|
_encName(r.issuer, false),
|
|
1664
1682
|
_validityUint(r.validity.notBefore, "validityNotBefore"),
|
|
@@ -1753,7 +1771,7 @@ function _keyUsageBitsFromDer(extnValue) {
|
|
|
1753
1771
|
}
|
|
1754
1772
|
return _namedBitsFromContent(bs.bytes, bs.unusedBits);
|
|
1755
1773
|
}
|
|
1756
|
-
var _NO_EXPIRY =
|
|
1774
|
+
var _NO_EXPIRY = C.TIME.seconds(Number(NO_EXPIRY_SECONDS));
|
|
1757
1775
|
var NODE_TO_WEBCRYPTO = Object.assign(Object.create(null), { "prime256v1": "P-256", "secp384r1": "P-384", "secp521r1": "P-521" });
|
|
1758
1776
|
var WEBCRYPTO_FIELD_BYTES = Object.assign(Object.create(null), { "P-256": 32, "P-384": 48, "P-521": 66 });
|
|
1759
1777
|
var SIG_ALG_TO_CURVE = Object.assign(Object.create(null), { "ecdsaWithSHA256": "P-256", "ecdsaWithSHA384": "P-384", "ecdsaWithSHA512": "P-521" });
|
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:3601ee14-7f92-4a66-bdb2-bab4e6360e45",
|
|
6
6
|
"version": 1,
|
|
7
7
|
"metadata": {
|
|
8
|
-
"timestamp": "2026-09-
|
|
8
|
+
"timestamp": "2026-09-04T23:41:31.757Z",
|
|
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.35",
|
|
23
23
|
"type": "application",
|
|
24
24
|
"name": "pki",
|
|
25
|
-
"version": "0.6.
|
|
25
|
+
"version": "0.6.35",
|
|
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.35",
|
|
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.35",
|
|
58
58
|
"dependsOn": []
|
|
59
59
|
}
|
|
60
60
|
]
|