@blamejs/pki 0.7.6 → 0.7.8
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 +23 -0
- package/lib/crl-sign.js +22 -0
- package/lib/crmf-sign.js +6 -40
- package/lib/schema-pkix.js +38 -0
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,29 @@ 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.8 — 2026-09-10
|
|
8
|
+
|
|
9
|
+
A pre-encoded CRL extension whose value does not decode is refused instead of signed.
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
|
|
13
|
+
- A pre-encoded extension supplied to pki.crl.sign is put through the extension decoder registered for its identifier, and a value that does not decode is refused with the decoder's own typed code, the same one pki.x509.sign raises for the same bytes. An issuerAltName whose value is a NULL now raises crl/bad-extension-value and a keyUsage whose value is a NULL raises crl/bad-key-usage, where both were previously signed. Revoked-entry extensions take the same check.
|
|
14
|
+
- The check runs after the per-extension profile rules this verb already applied, so a CRL whose only fault is one malformed extension keeps the code it has always raised: a pre-encoded cRLNumber that is not an INTEGER stays crl/bad-crl-number, and the criticality, delta-ordering and issuingDistributionPoint rules are unchanged. No CRL that was accepted before is refused for a different reason; the extensions newly refused are ones nothing was reading, and an extension with no registered decoder is unaffected.
|
|
15
|
+
- Where a CRL carries more than one fault, the reported code can differ from earlier releases. The rules that compare extensions to each other, such as freshestCRL not appearing in a delta CRL and a delta's cRLNumber exceeding its baseCRLNumber, run once every extension has been read, so a malformed value found first is now reported first. Such a CRL was refused before and is refused now; only which of its faults is named has changed. A caller matching on a specific code for a CRL with several faults should re-check the code it expects.
|
|
16
|
+
|
|
17
|
+
## v0.7.7 — 2026-09-10
|
|
18
|
+
|
|
19
|
+
The two GeneralName arms that were taken on their tag alone are now read.
|
|
20
|
+
|
|
21
|
+
### Changed
|
|
22
|
+
|
|
23
|
+
- Every verb that walks a name is affected, since they share one reader: certification-path validation, certificate and attribute-certificate parsing, CRL and CMP name handling, certificate linting, and the name-carrying extension decoders. A certificate carrying one of these two arms malformed was accepted before and is refused now, with the typed code that verb already raises for a bad name. A conforming arm is unaffected, and no error code was added or changed.
|
|
24
|
+
|
|
25
|
+
### Fixed
|
|
26
|
+
|
|
27
|
+
- GeneralName ediPartyName [5] is read as the EDIPartyName it holds: an optional nameAssigner [0] before a required partyName [1], each wrapping a DirectoryString of at least one character, in that order and with nothing else alongside. It was previously accepted as any non-empty constructed value, so a [5] over a NULL, over an INTEGER, or carrying nameAssigner without partyName all decoded as names while encoding none. GeneralName is defined in the RFC 5280 Appendix A.2 implicitly tagged module, so the [5] tag replaces the EDIPartyName SEQUENCE tag and its children are the two arms directly.
|
|
28
|
+
- GeneralName x400Address [3] must open with the BuiltInStandardAttributes SEQUENCE that begins an ORAddress, so an arm carrying that tag over some other value is refused. What the components hold is an X.400 structure this does not read, and every one of them is optional, so an address whose components are all empty is conforming and is not refused.
|
|
29
|
+
|
|
7
30
|
## v0.7.6 — 2026-09-10
|
|
8
31
|
|
|
9
32
|
A Sigstore bundle is held to the shape its specification states, in signature count and in entry count.
|
package/lib/crl-sign.js
CHANGED
|
@@ -44,6 +44,9 @@ function _err(code, message, cause) { return new CrlError(code, message, cause);
|
|
|
44
44
|
function _signE(kind, message, cause) { return new CrlError("crl/" + kind, message, cause); }
|
|
45
45
|
function O(n) { return oid.byName(n); }
|
|
46
46
|
|
|
47
|
+
/** @internal The builder is handed no decoder map: this module does not use the builder's
|
|
48
|
+
* extension-building path, it walks the pre-encoded array itself below and validates each value
|
|
49
|
+
* against this map there. */
|
|
47
50
|
var _b = pkiBuild.makeBuilder({ ErrorClass: CrlError, prefix: "crl", O: O, NS: NS, NAME_SCHEMA: NAME_SCHEMA, SPKI_SCHEMA: SPKI_SCHEMA, EXT_DECODERS: {} });
|
|
48
51
|
var _encodeName = _b.encodeName, _isEmptyName = _b.isEmptyName, _reqDer = _b.reqDer,
|
|
49
52
|
_assertValidSpki = _b.assertValidSpki, _assertValidExtension = _b.assertValidExtension,
|
|
@@ -52,6 +55,23 @@ var _encodeName = _b.encodeName, _isEmptyName = _b.isEmptyName, _reqDer = _b.req
|
|
|
52
55
|
_certLikeFromSpki = _b.certLikeFromSpki, _assertSignatureVerifies = _b.assertSignatureVerifies;
|
|
53
56
|
var EXT_DECODERS = pkix.certExtensionDecoders(NS).byOid;
|
|
54
57
|
|
|
58
|
+
/** @internal A pre-encoded extension is admitted on the shape of its Extension SEQUENCE, which says
|
|
59
|
+
* nothing about whether extnValue decodes as the structure its extnID names. Where a decoder is
|
|
60
|
+
* registered for that OID, the value is put through it, so a value that cannot be read is refused
|
|
61
|
+
* here rather than signed and left for a relying party to find. The decoder's own typed code is
|
|
62
|
+
* kept, since it names the field that failed. Called AFTER this module's own profile checks, so an
|
|
63
|
+
* extension it already answers for keeps the code it has always raised and this only reaches the
|
|
64
|
+
* ones nothing was checking. */
|
|
65
|
+
function _assertExtensionValueDecodes(node, extnId, what) {
|
|
66
|
+
var dec = EXT_DECODERS[extnId];
|
|
67
|
+
if (!dec) return;
|
|
68
|
+
try { dec(asn1.read.octetString(node.children[node.children.length - 1])); }
|
|
69
|
+
catch (err) {
|
|
70
|
+
if (err instanceof CrlError) throw err;
|
|
71
|
+
throw _err("crl/bad-input", "pre-encoded " + what + " " + (oid.name(extnId) || extnId) + " value is malformed", err);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
55
75
|
var OID_SKI = O("subjectKeyIdentifier");
|
|
56
76
|
|
|
57
77
|
var CRL_REASON = constants.NAMES.CRL_REASON;
|
|
@@ -221,6 +241,7 @@ function _buildCrlExtensions(spec, ctx) {
|
|
|
221
241
|
if (extnId === O("issuingDistributionPoint")) _validatePreEncodedIdp(inner);
|
|
222
242
|
}
|
|
223
243
|
}
|
|
244
|
+
_assertExtensionValueDecodes(node, extnId, "extension");
|
|
224
245
|
if (extnId === O("deltaCRLIndicator")) isDelta = true;
|
|
225
246
|
out.push(b.raw(der));
|
|
226
247
|
});
|
|
@@ -307,6 +328,7 @@ function _buildRevoked(entryList, isDelta) {
|
|
|
307
328
|
try { asn1.read.time(einner); } catch (e) { throw _err("crl/bad-input", "pre-encoded invalidityDate is not a well-formed GeneralizedTime (RFC 5280 sec. 5.3.2)", e); }
|
|
308
329
|
}
|
|
309
330
|
}
|
|
331
|
+
_assertExtensionValueDecodes(xnode, extnId, "entry extension");
|
|
310
332
|
entryExts.push(b.raw(der));
|
|
311
333
|
});
|
|
312
334
|
}
|
package/lib/crmf-sign.js
CHANGED
|
@@ -825,24 +825,16 @@ function _namesSomebody(node) {
|
|
|
825
825
|
if (!_isUniversal(node, 16) || !node.children || node.children.length === 0) return false;
|
|
826
826
|
try { schema.walk(pkix.generalNames(NS, { code: "crmf/bad-popo" }), node, NS); }
|
|
827
827
|
catch (_e) { return false; } // allow:swallow-unverified a list the shared reader refuses names nobody, which the caller's refusal states
|
|
828
|
-
/** @internal The shared reader
|
|
829
|
-
*
|
|
830
|
-
*
|
|
831
|
-
*
|
|
832
|
-
*
|
|
833
|
-
*
|
|
834
|
-
* hash alike. */
|
|
835
|
-
/** @internal Walked arm by arm, because the reader answers for most of them and not for all. It
|
|
836
|
-
* requires the IA5 arms [1] [2] [6] to carry characters, [7] to be an address of a real width and
|
|
837
|
-
* [8] to be an OBJECT IDENTIFIER, and it walks [0] and [4] into their own shapes. What it does not
|
|
838
|
-
* do is refuse the two empty forms below, and x400Address [3] it takes on its tag alone, which is
|
|
839
|
-
* left as it is: the derivation hashes these bytes rather than reading them, so an arm neither
|
|
840
|
-
* side reads is still an arm both sides hash alike. */
|
|
828
|
+
/** @internal The shared reader answers for the SHAPE of every arm, including ediPartyName [5] and
|
|
829
|
+
* the ORAddress opening of x400Address [3]. What remains here is the separate question this
|
|
830
|
+
* derivation asks, which is whether the arm names SOMEBODY rather than whether it is well formed:
|
|
831
|
+
* a Name with no RDNs and an ORAddress whose components are all empty are both conforming and
|
|
832
|
+
* neither identifies a subject. Splitting it this way keeps a naming-strength rule out of a
|
|
833
|
+
* structural reader every other consumer shares. */
|
|
841
834
|
for (var i = 0; i < node.children.length; i++) {
|
|
842
835
|
var arm = node.children[i];
|
|
843
836
|
if (arm.tagNumber === 3 && !_isOrAddress(arm)) return false;
|
|
844
837
|
if (arm.tagNumber === 4 && !_isNonEmptyName(arm)) return false;
|
|
845
|
-
if (arm.tagNumber === 5 && !_isEdiPartyName(arm)) return false;
|
|
846
838
|
}
|
|
847
839
|
return true;
|
|
848
840
|
}
|
|
@@ -874,32 +866,6 @@ function _isNonEmptyName(node) {
|
|
|
874
866
|
return schema.isUniversal(dn, 16) && !!dn.children && dn.children.length > 0;
|
|
875
867
|
}
|
|
876
868
|
|
|
877
|
-
/** @internal The universal tags a DirectoryString is written under (RFC 5280 sec. 4.1.2.4):
|
|
878
|
-
* UTF8String, PrintableString, TeletexString, UniversalString and BMPString. */
|
|
879
|
-
var DIRECTORY_STRING_TAGS = [12, 19, 20, 28, 30];
|
|
880
|
-
|
|
881
|
-
/** @internal Whether a [5] arm holds an EDIPartyName: the optional nameAssigner [0] before the
|
|
882
|
-
* required partyName [1], each carrying a DirectoryString. DirectoryString is a CHOICE, so its tag
|
|
883
|
-
* is EXPLICIT however the module tags: the arm is constructed and wraps the string itself, and the
|
|
884
|
-
* string has to be one, since a [1] wrapping a NULL is tagged like a name and holds none. */
|
|
885
|
-
function _isEdiPartyName(node) {
|
|
886
|
-
var kids = node.children;
|
|
887
|
-
if (!kids || kids.length < 1 || kids.length > 2) return false;
|
|
888
|
-
for (var i = 0; i < kids.length; i++) {
|
|
889
|
-
var arm = kids[i];
|
|
890
|
-
if (!schema.isContextInRange(arm, 0, 1) || !arm.children || arm.children.length !== 1) return false;
|
|
891
|
-
if (!schema.isUniversalOneOf(arm.children[0], DIRECTORY_STRING_TAGS)) return false;
|
|
892
|
-
/** @internal Read as the string it is tagged as, not counted as one. DirectoryString is
|
|
893
|
-
* SIZE (1..MAX), so an empty one names nobody, and a string whose bytes are not the encoding its
|
|
894
|
-
* tag claims is not a name either. The codec's own reader answers both. */
|
|
895
|
-
var text;
|
|
896
|
-
try { text = asn1.read.string(arm.children[0]); }
|
|
897
|
-
catch (_e) { return false; } // allow:swallow-unverified a string the codec cannot read names nobody, which the caller's refusal states
|
|
898
|
-
if (text.length === 0) return false;
|
|
899
|
-
}
|
|
900
|
-
if (kids.length === 2) return kids[0].tagNumber === 0 && kids[1].tagNumber === 1;
|
|
901
|
-
return kids[0].tagNumber === 1;
|
|
902
|
-
}
|
|
903
869
|
|
|
904
870
|
function _nameOrAlternative(parsed, nameField, altExtension) {
|
|
905
871
|
var name = parsed[nameField];
|
package/lib/schema-pkix.js
CHANGED
|
@@ -460,6 +460,10 @@ var CRL_REASON_NAMES = constants.NAMES.CRL_REASON;
|
|
|
460
460
|
|
|
461
461
|
var GN_CONSTRUCTED = _assign(_create(null), { 0: 1, 3: 1, 4: 1, 5: 1 });
|
|
462
462
|
var GN_IA5 = _assign(_create(null), { 1: 1, 2: 1, 6: 1 });
|
|
463
|
+
/** @internal DirectoryString ::= CHOICE { teletexString, printableString, universalString,
|
|
464
|
+
* utf8String, bmpString } (RFC 5280 sec. 4.1.2.4). */
|
|
465
|
+
var DIRECTORY_STRING_TAGS = [asn1.TAGS.UTF8_STRING, asn1.TAGS.PRINTABLE_STRING, asn1.TAGS.TELETEX_STRING,
|
|
466
|
+
asn1.TAGS.UNIVERSAL_STRING, asn1.TAGS.BMP_STRING];
|
|
463
467
|
function generalName(ns, opts) {
|
|
464
468
|
opts = opts || {};
|
|
465
469
|
var code = opts.code || (ns.prefix + "/bad-general-name");
|
|
@@ -489,6 +493,40 @@ function generalName(ns, opts) {
|
|
|
489
493
|
if (n.children.length !== 1) throw ctx.E(code, "GeneralName directoryName [4] must wrap exactly one Name");
|
|
490
494
|
var dnMatch = schema.walk(NAME, n.children[0], ctx);
|
|
491
495
|
if (decodeValue) value = dnMatch.result;
|
|
496
|
+
} else if (t === 3) {
|
|
497
|
+
/** @internal ORAddress opens with the BuiltInStandardAttributes SEQUENCE (X.411), so an arm
|
|
498
|
+
* carrying its tag over some other value is not an address. What the components hold is an
|
|
499
|
+
* X.400 structure this does not read, and every one of them is optional, so an address whose
|
|
500
|
+
* components are all empty is still a conforming address and is not refused here. */
|
|
501
|
+
if (!schema.isUniversal(n.children[0], asn1.TAGS.SEQUENCE)) {
|
|
502
|
+
throw ctx.E(code, "GeneralName x400Address [3] must be an ORAddress opening with the BuiltInStandardAttributes SEQUENCE (X.411)");
|
|
503
|
+
}
|
|
504
|
+
} else if (t === 5) {
|
|
505
|
+
/** @internal EDIPartyName ::= SEQUENCE { nameAssigner [0] DirectoryString OPTIONAL,
|
|
506
|
+
* partyName [1] DirectoryString } (RFC 5280 sec. 4.2.1.6). partyName is not optional, the
|
|
507
|
+
* two arms are ordered, and a DirectoryString is SIZE (1..MAX), so an empty one is not one. */
|
|
508
|
+
var ediKids = n.children;
|
|
509
|
+
if (ediKids.length > 2) throw ctx.E(code, "GeneralName ediPartyName [5] holds at most nameAssigner [0] and partyName [1] (RFC 5280 sec. 4.2.1.6)");
|
|
510
|
+
for (var ei = 0; ei < ediKids.length; ei++) {
|
|
511
|
+
var ediArm = ediKids[ei];
|
|
512
|
+
if (!schema.isContextInRange(ediArm, 0, 1) || !ediArm.children || ediArm.children.length !== 1) {
|
|
513
|
+
throw ctx.E(code, "GeneralName ediPartyName [5] arms must be [0] or [1] wrapping exactly one DirectoryString");
|
|
514
|
+
}
|
|
515
|
+
if (!schema.isUniversalOneOf(ediArm.children[0], DIRECTORY_STRING_TAGS)) {
|
|
516
|
+
throw ctx.E(code, "GeneralName ediPartyName [5] arm must hold a DirectoryString");
|
|
517
|
+
}
|
|
518
|
+
var ediText;
|
|
519
|
+
try { ediText = asn1.read.string(ediArm.children[0]); }
|
|
520
|
+
catch (e) { throw ctx.E(code, "GeneralName ediPartyName [5] arm is not the string its tag claims", e); }
|
|
521
|
+
if (ediText.length === 0) throw ctx.E(code, "GeneralName ediPartyName [5] DirectoryString must be SIZE (1..MAX)");
|
|
522
|
+
}
|
|
523
|
+
if (ediKids.length === 2) {
|
|
524
|
+
if (ediKids[0].tagNumber !== 0 || ediKids[1].tagNumber !== 1) {
|
|
525
|
+
throw ctx.E(code, "GeneralName ediPartyName [5] must order nameAssigner [0] before partyName [1]");
|
|
526
|
+
}
|
|
527
|
+
} else if (ediKids[0].tagNumber !== 1) {
|
|
528
|
+
throw ctx.E(code, "GeneralName ediPartyName [5] requires partyName [1]; nameAssigner [0] alone is not a name");
|
|
529
|
+
}
|
|
492
530
|
}
|
|
493
531
|
} else {
|
|
494
532
|
if (constructed) throw ctx.E(code, "GeneralName [" + t + "] must be primitive (X.690 sec. 10.2)");
|
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:eb6258f5-7cf4-4371-80b7-ed98047003ac",
|
|
6
6
|
"version": 1,
|
|
7
7
|
"metadata": {
|
|
8
|
-
"timestamp": "2026-09-
|
|
8
|
+
"timestamp": "2026-09-10T13:51:21.922Z",
|
|
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.8",
|
|
23
23
|
"type": "application",
|
|
24
24
|
"name": "pki",
|
|
25
|
-
"version": "0.7.
|
|
25
|
+
"version": "0.7.8",
|
|
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.8",
|
|
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.8",
|
|
58
58
|
"dependsOn": []
|
|
59
59
|
}
|
|
60
60
|
]
|