@blamejs/pki 0.7.12 → 0.7.14
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 +25 -0
- package/lib/jose.js +5 -0
- package/lib/pki-build.js +22 -0
- package/lib/smime.js +11 -7
- package/lib/webauthn.js +5 -0
- package/lib/x509-sign.js +2 -0
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,31 @@ 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.7.14 — 2026-09-11
|
|
8
|
+
|
|
9
|
+
Every option a verify verb decides with is taken once.
|
|
10
|
+
|
|
11
|
+
### Changed
|
|
12
|
+
|
|
13
|
+
- The pki.smime verbs take their options once at entry. sign, verify, encrypt, decrypt, compress, decompress and buildCertsOnly share one option-checking helper, and it now returns the options it checked rather than leaving the caller's object to be read again by each later pass.
|
|
14
|
+
|
|
15
|
+
### Security
|
|
16
|
+
|
|
17
|
+
- pki.jose.verify took opts.key seven times: a type gate, the RFC 7638 thumbprint comparison against the jwk the JWS embeds, and the key the signature is verified under. An accessor-backed opts.key could satisfy the comparison with the embedded key and hand a different one to the verification, so a JWS naming a signer the caller did not pin returned a verdict of keySource "opts.key" instead of the jose/key-mismatch refusal a plain object gets. The option is taken once, so the key compared is the key used.
|
|
18
|
+
- pki.webauthn.parseClientData took each expected* option twice: once to decide whether to compare, once for the value compared, and the returned checked field then recorded that the comparison ran. An accessor-backed expectedChallenge could enter the branch on the challenge a ceremony issued and be compared against the challenge the response carried, returning checked.challenge true for a comparison that never ran against the caller's value. expectedType and expectedOrigin behaved the same way. Each option is taken once, so checked answers for the value the caller supplied. pki.webauthn.verify and pki.webauthn.verifyAssertion already copied their inputs and were not affected.
|
|
19
|
+
|
|
20
|
+
## v0.7.13 — 2026-09-10
|
|
21
|
+
|
|
22
|
+
The certificate signer refuses a pre-encoded extension whose criticality RFC 5280 fixes.
|
|
23
|
+
|
|
24
|
+
### Changed
|
|
25
|
+
|
|
26
|
+
- A call that supplied one of the nine fixed-criticality extensions with the wrong flag now throws where it previously produced a certificate or a request. Set the flag the clause requires: omit the critical BOOLEAN for the non-critical set, and encode it TRUE for the critical set. The extensions and extensionRequest object forms already emitted the correct flag and are unaffected, as are the CRL and attribute-certificate signers, which enforce the criticality their own profiles fix.
|
|
27
|
+
|
|
28
|
+
### Fixed
|
|
29
|
+
|
|
30
|
+
- pki.x509.sign and pki.csr.sign now refuse a pre-encoded extension carrying a criticality RFC 5280 does not permit, throwing x509/bad-input or csr/bad-input with the governing clause in the message. Nine extensions leave the issuer no choice. authorityKeyIdentifier (section 4.2.1.1), subjectKeyIdentifier (section 4.2.1.2), subjectDirectoryAttributes (section 4.2.1.8), freshestCRL (section 4.2.1.15), authorityInfoAccess (section 4.2.2.1) and subjectInfoAccess (section 4.2.2.2) must be non-critical. nameConstraints (section 4.2.1.10), policyConstraints (section 4.2.1.11) and inhibitAnyPolicy (section 4.2.1.14) must be critical. The check covers the pre-encoded array form on both verbs, which is the only way to supply these six extensions.
|
|
31
|
+
|
|
7
32
|
## v0.7.12 — 2026-09-10
|
|
8
33
|
|
|
9
34
|
An issuer can draw a certificate serial number before signing with it.
|
package/lib/jose.js
CHANGED
|
@@ -319,6 +319,11 @@ var _VERIFY_KEYS = intrinsic.assign(intrinsic.create(null), { key: 1, profile: 1
|
|
|
319
319
|
async function verify(jws, opts) {
|
|
320
320
|
opts = opts || {};
|
|
321
321
|
guard.identifier.assertKnownKeys(opts, _VERIFY_KEYS, E, "jose/bad-input", "unknown pki.jose.verify option ");
|
|
322
|
+
/** @internal opts.key is examined several times: the type gate, the RFC 7638 thumbprint comparison
|
|
323
|
+
* against the embedded jwk, and the key the signature is verified under. An option reached through
|
|
324
|
+
* an accessor answers each read separately, so the key the comparison ran on need not be the key
|
|
325
|
+
* the verification used. Take each option once. */
|
|
326
|
+
opts = guard.identifier.snapshotOptions(opts, _VERIFY_KEYS);
|
|
322
327
|
if (!jws || typeof jws !== "object" || _isArray(jws)) throw E("jose/bad-jws", "a flattened JWS object is required");
|
|
323
328
|
if (_hasOwn(jws, "signatures")) throw E("jose/bad-jws", "the multi-signature signatures member is forbidden");
|
|
324
329
|
if (_hasOwn(jws, "header")) throw E("jose/bad-jws", "the unprotected header member is forbidden");
|
package/lib/pki-build.js
CHANGED
|
@@ -273,6 +273,17 @@ function reqDenseArrayImpl(list, what, E, code) {
|
|
|
273
273
|
return out;
|
|
274
274
|
}
|
|
275
275
|
|
|
276
|
+
/** @internal RFC 5280 sec. 4.2 fixes the criticality of these certificate extensions. Each entry maps
|
|
277
|
+
* the extension OID to the flag the RFC requires and the clause that sets it. */
|
|
278
|
+
var CERT_FIXED_CRITICALITY = intrinsic.create(null);
|
|
279
|
+
[["authorityKeyIdentifier", false, "RFC 5280 sec. 4.2.1.1"], ["subjectKeyIdentifier", false, "RFC 5280 sec. 4.2.1.2"],
|
|
280
|
+
["subjectDirectoryAttributes", false, "RFC 5280 sec. 4.2.1.8"], ["nameConstraints", true, "RFC 5280 sec. 4.2.1.10"],
|
|
281
|
+
["policyConstraints", true, "RFC 5280 sec. 4.2.1.11"], ["inhibitAnyPolicy", true, "RFC 5280 sec. 4.2.1.14"],
|
|
282
|
+
["freshestCRL", false, "RFC 5280 sec. 4.2.1.15"], ["authorityInfoAccess", false, "RFC 5280 sec. 4.2.2.1"],
|
|
283
|
+
["subjectInfoAccess", false, "RFC 5280 sec. 4.2.2.2"]].forEach(function (r) {
|
|
284
|
+
CERT_FIXED_CRITICALITY[oid.byName(r[0])] = { critical: r[1], citation: r[2] };
|
|
285
|
+
});
|
|
286
|
+
|
|
276
287
|
function makeBuilder(ctx) {
|
|
277
288
|
var ErrorClass = ctx.ErrorClass, O = ctx.O, NS = ctx.NS;
|
|
278
289
|
var NAME_SCHEMA = ctx.NAME_SCHEMA, SPKI_SCHEMA = ctx.SPKI_SCHEMA;
|
|
@@ -498,6 +509,15 @@ function makeBuilder(ctx) {
|
|
|
498
509
|
var last = n.children[n.children.length - 1];
|
|
499
510
|
if (last.tagNumber !== asn1.TAGS.OCTET_STRING || last.tagClass !== "universal") throw E("bad-input", "pre-encoded extension [" + idx + "] extnValue must be an OCTET STRING");
|
|
500
511
|
}
|
|
512
|
+
/** @internal Applies the certificate profile's fixed-criticality table. The CRL, CRL-entry and
|
|
513
|
+
* attribute-certificate profiles set their own criticality in their own signers. */
|
|
514
|
+
function assertCertCriticality(extnId, isCritical, label) {
|
|
515
|
+
if (!intrinsic.hasOwn(CERT_FIXED_CRITICALITY, extnId)) return;
|
|
516
|
+
var req = CERT_FIXED_CRITICALITY[extnId];
|
|
517
|
+
if (isCritical === req.critical) return;
|
|
518
|
+
throw E("bad-input", label + " " + (oid.name(extnId) || extnId) + " extension must be marked " +
|
|
519
|
+
(req.critical ? "critical" : "non-critical") + " (" + req.citation + ")");
|
|
520
|
+
}
|
|
501
521
|
var BC_KEYS = { cA: 1, pathLen: 1, critical: 1 };
|
|
502
522
|
var REQ_EXT_KEYS = {
|
|
503
523
|
subjectAltName: 1, keyUsage: 1, keyUsageCritical: 1, extendedKeyUsage: 1, extendedKeyUsageCritical: 1,
|
|
@@ -515,6 +535,7 @@ function makeBuilder(ctx) {
|
|
|
515
535
|
var extnId = asn1.read.oid(n.children[0]);
|
|
516
536
|
if (seen[extnId]) throw E("bad-input", "duplicate requested extension " + extnId + " (RFC 5280 sec. 4.2)");
|
|
517
537
|
seen[extnId] = true;
|
|
538
|
+
assertCertCriticality(extnId, n.children.length === 3, "requested");
|
|
518
539
|
var dec = EXT_DECODERS && EXT_DECODERS[extnId];
|
|
519
540
|
if (dec) {
|
|
520
541
|
try { dec(asn1.read.octetString(n.children[n.children.length - 1])); }
|
|
@@ -607,6 +628,7 @@ function makeBuilder(ctx) {
|
|
|
607
628
|
extSan: extSan, extCertPolicies: extCertPolicies, ext: ext,
|
|
608
629
|
spkiKeyId: spkiKeyId, skiKeyId: skiKeyId,
|
|
609
630
|
reqDer: reqDer, reqDenseArray: reqDenseArray, reqDerSequence: reqDerSequence, assertValidSpki: assertValidSpki, assertValidExtension: assertValidExtension,
|
|
631
|
+
assertCertCriticality: assertCertCriticality,
|
|
610
632
|
certLikeFromSpki: certLikeFromSpki, assertSignatureVerifies: assertSignatureVerifies,
|
|
611
633
|
samePublicKey: samePublicKey,
|
|
612
634
|
};
|
package/lib/smime.js
CHANGED
|
@@ -64,11 +64,15 @@ var DECRYPT_OPTS = Object.assign(Object.create(null), { recipientIndex: 1, maxIt
|
|
|
64
64
|
var COMPRESS_OPTS = Object.assign(Object.create(null), { entity: 1, contentType: 1, level: 1 });
|
|
65
65
|
var DECOMPRESS_OPTS = Object.assign(Object.create(null), { maxOutputBytes: 1 });
|
|
66
66
|
|
|
67
|
+
/** @internal Checks the option names and RETURNS the options taken once. A verb reads one option to
|
|
68
|
+
* decide whether a rule applies and reads it again for the value the rule uses, and an option
|
|
69
|
+
* reached through an accessor answers each read separately. Callers assign the return value. */
|
|
67
70
|
function _knownOpts(opts, known, verb) {
|
|
68
71
|
guard.identifier.assertKnownKeys(opts, known, _err, "smime/bad-input", function (k) {
|
|
69
72
|
return "unknown option " + JSON.stringify(k) + " for pki.smime." + verb + " -- accepted: " +
|
|
70
73
|
Object.keys(known).sort().join(", ");
|
|
71
74
|
});
|
|
75
|
+
return guard.identifier.snapshotOptions(opts, known);
|
|
72
76
|
}
|
|
73
77
|
|
|
74
78
|
function _isPkcs7(type, kind) {
|
|
@@ -500,7 +504,7 @@ function _base64Body(der) {
|
|
|
500
504
|
*/
|
|
501
505
|
async function sign(content, signers, opts) {
|
|
502
506
|
opts = opts || {};
|
|
503
|
-
_knownOpts(opts, SIGN_OPTS, "sign");
|
|
507
|
+
opts = _knownOpts(opts, SIGN_OPTS, "sign");
|
|
504
508
|
var hp = opts.protectHeaders === true;
|
|
505
509
|
var hpList = hp ? _hpHeaderList(opts.headers) : null;
|
|
506
510
|
var outerList = hp ? _outerHeaderList(hpList, "clear", opts.hcp) : null;
|
|
@@ -612,7 +616,7 @@ function _capped(msg) {
|
|
|
612
616
|
*/
|
|
613
617
|
async function verify(message, opts) {
|
|
614
618
|
opts = opts || {};
|
|
615
|
-
_knownOpts(opts, VERIFY_OPTS, "verify");
|
|
619
|
+
opts = _knownOpts(opts, VERIFY_OPTS, "verify");
|
|
616
620
|
var ent = mime.parse(message, SmimeError, "smime/bad-mime");
|
|
617
621
|
var ct = ent.contentType;
|
|
618
622
|
var vOpts = {};
|
|
@@ -720,7 +724,7 @@ function _cmsEncryptOpts(opts) {
|
|
|
720
724
|
*/
|
|
721
725
|
async function encrypt(content, recipients, opts) {
|
|
722
726
|
opts = opts || {};
|
|
723
|
-
_knownOpts(opts, ENCRYPT_OPTS, "encrypt");
|
|
727
|
+
opts = _knownOpts(opts, ENCRYPT_OPTS, "encrypt");
|
|
724
728
|
var hp = opts.protectHeaders === true;
|
|
725
729
|
var hpList = hp ? _hpHeaderList(opts.headers) : null;
|
|
726
730
|
var outerList = hp ? _outerHeaderList(hpList, "cipher", opts.hcp) : null;
|
|
@@ -776,7 +780,7 @@ async function encrypt(content, recipients, opts) {
|
|
|
776
780
|
*/
|
|
777
781
|
async function decrypt(message, keyMaterial, opts) {
|
|
778
782
|
opts = opts || {};
|
|
779
|
-
_knownOpts(opts, DECRYPT_OPTS, "decrypt");
|
|
783
|
+
opts = _knownOpts(opts, DECRYPT_OPTS, "decrypt");
|
|
780
784
|
var ent = mime.parse(message, SmimeError, "smime/bad-mime");
|
|
781
785
|
var ct = ent.contentType;
|
|
782
786
|
if (!_isPkcs7(ct.type, "mime")) throw _err("smime/unsupported-type", "not an encrypted S/MIME message (Content-Type " + JSON.stringify(ct.type) + ")");
|
|
@@ -821,7 +825,7 @@ async function decrypt(message, keyMaterial, opts) {
|
|
|
821
825
|
*/
|
|
822
826
|
async function compress(content, opts) {
|
|
823
827
|
opts = opts || {};
|
|
824
|
-
_knownOpts(opts, COMPRESS_OPTS, "compress");
|
|
828
|
+
opts = _knownOpts(opts, COMPRESS_OPTS, "compress");
|
|
825
829
|
var entity = _entityBytes(content, opts);
|
|
826
830
|
var cOpts = {};
|
|
827
831
|
if (opts.level !== undefined) cOpts.level = opts.level;
|
|
@@ -855,7 +859,7 @@ async function compress(content, opts) {
|
|
|
855
859
|
*/
|
|
856
860
|
async function decompress(message, opts) {
|
|
857
861
|
opts = opts || {};
|
|
858
|
-
_knownOpts(opts, DECOMPRESS_OPTS, "decompress");
|
|
862
|
+
opts = _knownOpts(opts, DECOMPRESS_OPTS, "decompress");
|
|
859
863
|
var ent = mime.parse(message, SmimeError, "smime/bad-mime");
|
|
860
864
|
var ct = ent.contentType;
|
|
861
865
|
if (!_isPkcs7(ct.type, "mime")) throw _err("smime/unsupported-type", "not a compressed S/MIME message (Content-Type " + JSON.stringify(ct.type) + ")");
|
|
@@ -901,7 +905,7 @@ var CERTS_ONLY_OPTS = Object.assign(Object.create(null), { crls: 1 });
|
|
|
901
905
|
*/
|
|
902
906
|
function buildCertsOnly(certs, opts) {
|
|
903
907
|
opts = opts || {};
|
|
904
|
-
_knownOpts(opts, CERTS_ONLY_OPTS, "buildCertsOnly");
|
|
908
|
+
opts = _knownOpts(opts, CERTS_ONLY_OPTS, "buildCertsOnly");
|
|
905
909
|
var der = _toBuf(cms.certsOnly(certs, { crls: opts.crls }));
|
|
906
910
|
return _capped(Buffer.concat([_pkcs7MimeHead("certs-only", "smime.p7c"), _base64Body(der), mime.CRLF]));
|
|
907
911
|
}
|
package/lib/webauthn.js
CHANGED
|
@@ -1275,6 +1275,11 @@ function parseClientData(bytes, opts) {
|
|
|
1275
1275
|
opts = opts || {};
|
|
1276
1276
|
if (!_isPlainObject(opts)) throw _err("webauthn/bad-input", "opts must be an object");
|
|
1277
1277
|
guard.identifier.assertKnownKeys(opts, _CLIENT_DATA_OPTS, _err, "webauthn/bad-input", "opts has an unknown key ");
|
|
1278
|
+
/** @internal Each expected* option is read to decide whether to compare and read again for the
|
|
1279
|
+
* value compared, and `checked` then records that the comparison ran. An option reached through an
|
|
1280
|
+
* accessor answers each read separately, so `checked` could describe a comparison against a value
|
|
1281
|
+
* the caller never supplied. Take each option once. */
|
|
1282
|
+
opts = guard.identifier.snapshotOptions(opts, _CLIENT_DATA_OPTS);
|
|
1278
1283
|
bytes = _bytesArg(bytes, "clientDataJSON");
|
|
1279
1284
|
var doc = guard.json.parse(Buffer.from(bytes), _err, {
|
|
1280
1285
|
maxBytes: constants.LIMITS.JSON_MAX_BYTES, maxDepth: constants.LIMITS.JSON_MAX_DEPTH,
|
package/lib/x509-sign.js
CHANGED
|
@@ -56,6 +56,7 @@ var KNOWN_EXT_KEYS = intrinsic.assign(intrinsic.create(null), {
|
|
|
56
56
|
var _b = pkiBuild.makeBuilder({ ErrorClass: CertificateError, prefix: "x509", O: O, NS: NS, NAME_SCHEMA: NAME_SCHEMA, SPKI_SCHEMA: SPKI_SCHEMA, EXT_DECODERS: EXT_DECODERS });
|
|
57
57
|
var _encodeName = _b.encodeName, _isEmptyName = _b.isEmptyName, _reqDer = _b.reqDer,
|
|
58
58
|
_assertValidSpki = _b.assertValidSpki, _assertValidExtension = _b.assertValidExtension,
|
|
59
|
+
_assertCertCriticality = _b.assertCertCriticality,
|
|
59
60
|
_certLikeFromSpki = _b.certLikeFromSpki, _assertCertVerifies = _b.assertSignatureVerifies,
|
|
60
61
|
_ext = _b.ext, _extBasicConstraints = _b.extBasicConstraints, _validateBcSpec = _b.validateBcSpec,
|
|
61
62
|
_extKeyUsage = _b.extKeyUsage, _extExtKeyUsage = _b.extExtKeyUsage, _extSki = _b.extSki,
|
|
@@ -89,6 +90,7 @@ function _buildExtensions(extSpec, ctx) {
|
|
|
89
90
|
var extnId = asn1.read.oid(n.children[0]);
|
|
90
91
|
if (seenExt[extnId]) throw _err("x509/bad-input", "duplicate extension " + extnId + " in the extensions array (RFC 5280 sec. 4.2 -- at most one instance of an extension)");
|
|
91
92
|
seenExt[extnId] = true;
|
|
93
|
+
_assertCertCriticality(extnId, n.children.length === 3, "pre-encoded");
|
|
92
94
|
var dec = EXT_DECODERS[extnId];
|
|
93
95
|
if (dec) {
|
|
94
96
|
var decoded;
|
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:bf9440b0-2e9b-45e1-8551-36b17188bd96",
|
|
6
6
|
"version": 1,
|
|
7
7
|
"metadata": {
|
|
8
|
-
"timestamp": "2026-09-
|
|
8
|
+
"timestamp": "2026-09-11T02:22:59.455Z",
|
|
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.7.
|
|
22
|
+
"bom-ref": "@blamejs/pki@0.7.14",
|
|
23
23
|
"type": "application",
|
|
24
24
|
"name": "pki",
|
|
25
|
-
"version": "0.7.
|
|
25
|
+
"version": "0.7.14",
|
|
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.7.
|
|
29
|
+
"purl": "pkg:npm/%40blamejs/pki@0.7.14",
|
|
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.7.
|
|
57
|
+
"ref": "@blamejs/pki@0.7.14",
|
|
58
58
|
"dependsOn": []
|
|
59
59
|
}
|
|
60
60
|
]
|