@blamejs/pki 0.4.1 → 0.4.2
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 +18 -1
- package/README.md +1 -1
- package/lib/asn1-der.js +15 -6
- package/lib/schema-c509.js +74 -38
- package/lib/schema-pkix.js +9 -0
- package/lib/vendor/MANIFEST.json +1 -1
- package/lib/vendor/README.md +1 -1
- package/package.json +2 -2
- package/sbom.cdx.json +7 -7
package/CHANGELOG.md
CHANGED
|
@@ -4,7 +4,24 @@ 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.4.
|
|
7
|
+
## v0.4.2 — 2026-08-08
|
|
8
|
+
|
|
9
|
+
A NumericString attribute value no longer shares distinguished-name identity with a printable or UTF-8 value of the same characters -- the comparison that decides name chaining, revocation-issuer matching and name constraints. Alongside it, several C509 name-encoding conformance fixes and a move to Node 24.19.0.
|
|
10
|
+
|
|
11
|
+
### Changed
|
|
12
|
+
|
|
13
|
+
- The supported Node floor moves to 24.19.0, the current long-term-support release. Nothing is transpiled, so the supported version is the version the source runs on; the release is verified against that runtime.
|
|
14
|
+
- pki.asn1.read.numericString reads a NumericString value, validated strictly to the digits and space the type permits. The shared string reader no longer accepts the type, so a caller that wants it asks for it by name.
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
|
|
18
|
+
- A NumericString attribute value no longer compares equal to a PrintableString or UTF8String attribute value carrying the same characters. RFC 5280 sec. 7.1 name comparison folds the directory-string types into one identity class, and NumericString is not one of them; because the previous release read it through the shared string reader, it entered that class and was treated as the same name by the comparison that decides certificate chaining, revocation-issuer matching and name-constraint evaluation. It now reads through its own reader and, as before, renders in the RFC 4514 hexadecimal form rather than as a plain string.
|
|
19
|
+
- A natively signed C509 certificate is no longer accepted with, or built carrying, a negative attribute-type integer. The sign of that integer exists only to reproduce the string type of an original X.509 encoding, which a natively signed certificate does not have, so all of its integers are non-negative (draft-ietf-cose-cbor-encoded-cert-20 sec. 3.1.4); the toolkit previously read such a certificate and could also emit one that a conformant implementation must reject.
|
|
20
|
+
- A country name or serial number attribute now keeps the string type its attribute integer's sign declares, and its restriction to the printable-string character subset is enforced on the characters instead. Both signs previously rebuilt the same certificate bytes, so two distinct compact encodings of one value produced one identical certificate under a single signature.
|
|
21
|
+
- The rendered distinguished-name string now escapes its values (RFC 4514 sec. 2.4), so an attribute value containing a comma can no longer read as though the name held several attributes, and a control byte can no longer reach a log line unescaped.
|
|
22
|
+
- An empty issuer name is now refused (RFC 5280 sec. 4.1.2.4 requires a non-empty issuer). It previously parsed and rebuilt a certificate that this toolkit's own certificate parser declines to load. An empty subject is still accepted; the profile pairs that with a subject alternative name, which this codec does not yet require.
|
|
23
|
+
|
|
24
|
+
## v0.4.1 — 2026-08-07
|
|
8
25
|
|
|
9
26
|
pki.schema.c509 encodes and decodes the compact subjectDirectoryAttributes value form -- a C509 certificate's subject directory attributes ride their draft-20 registry integers (or unwrapped OIDs) with their directory-string values, interoperating with a conformant C509 implementation rather than only this decoder.
|
|
10
27
|
|
package/README.md
CHANGED
|
@@ -63,7 +63,7 @@ Web Crypto API with its limits on streaming, opaque keys, and algorithm reach.
|
|
|
63
63
|
npm i @blamejs/pki
|
|
64
64
|
```
|
|
65
65
|
|
|
66
|
-
Requires Node.js 24.
|
|
66
|
+
Requires Node.js 24.19+ (runs on the shipped runtime — no build step, no
|
|
67
67
|
transpilation).
|
|
68
68
|
|
|
69
69
|
```js
|
package/lib/asn1-der.js
CHANGED
|
@@ -623,11 +623,20 @@ function _decodePrintable(buf) {
|
|
|
623
623
|
return s;
|
|
624
624
|
}
|
|
625
625
|
|
|
626
|
-
// NumericString (X.680 sec. 41, Table 9) permits ONLY the digits and SPACE. It carries the X.520 syntax of
|
|
627
|
-
//
|
|
628
|
-
//
|
|
629
|
-
|
|
630
|
-
|
|
626
|
+
// NumericString (X.680 sec. 41, Table 9) permits ONLY the digits and SPACE. It carries the X.520 syntax of the
|
|
627
|
+
// x121Address and internationalISDNNumber directory attributes.
|
|
628
|
+
//
|
|
629
|
+
// It is read through its OWN reader, deliberately NOT through readString. Every type readString returns a
|
|
630
|
+
// plain string for enters the RFC 5280 sec. 7.1 name-comparison identity class, and
|
|
631
|
+
// guard.name.dnEqual -- the single choke point for name chaining, revocation-issuer matching and name
|
|
632
|
+
// constraints -- compares the strings it returns. Admitting NumericString there would make a NumericString
|
|
633
|
+
// attribute value compare EQUAL to a PrintableString / UTF8String one with the same characters, silently
|
|
634
|
+
// widening distinguished-name identity across the whole toolkit. A caller that genuinely wants this type asks
|
|
635
|
+
// for it by name.
|
|
636
|
+
function readNumericString(node) {
|
|
637
|
+
_expectUniversal(node, TAGS.NUMERIC_STRING, "readNumericString");
|
|
638
|
+
_expectPrimitive(node, "readNumericString");
|
|
639
|
+
var s = node.content.toString("latin1");
|
|
631
640
|
if (!NUMERIC_RE.test(s)) throw new Asn1Error("asn1/bad-numeric-string", "NumericString has characters outside the digits-and-space set");
|
|
632
641
|
return s;
|
|
633
642
|
}
|
|
@@ -647,7 +656,6 @@ function readString(node) {
|
|
|
647
656
|
_expectPrimitive(node, "readString");
|
|
648
657
|
switch (node.tagNumber) {
|
|
649
658
|
case TAGS.UTF8_STRING: return _decodeUtf8Strict(node.content);
|
|
650
|
-
case TAGS.NUMERIC_STRING: return _decodeNumeric(node.content);
|
|
651
659
|
case TAGS.PRINTABLE_STRING: return _decodePrintable(node.content);
|
|
652
660
|
case TAGS.IA5_STRING: return _decodeIa5(node.content);
|
|
653
661
|
case TAGS.TELETEX_STRING: return _decodeText(node.content, "latin1");
|
|
@@ -1079,6 +1087,7 @@ module.exports = {
|
|
|
1079
1087
|
booleanImplicit: readBooleanImplicit,
|
|
1080
1088
|
oid: readOid,
|
|
1081
1089
|
string: readString,
|
|
1090
|
+
numericString: readNumericString,
|
|
1082
1091
|
time: readTime,
|
|
1083
1092
|
},
|
|
1084
1093
|
};
|
package/lib/schema-c509.js
CHANGED
|
@@ -257,22 +257,21 @@ function _assertAttrValue(rdn) {
|
|
|
257
257
|
}
|
|
258
258
|
}
|
|
259
259
|
|
|
260
|
-
function _name509(node, isSubject) {
|
|
260
|
+
function _name509(node, isSubject, isNative) {
|
|
261
261
|
if (!isSubject && node.majorType === 7 && node.ai === 22) return null; // issuer == subject (self-signed)
|
|
262
262
|
// A bare SpecialText (not an array) is a single commonName attribute (attributeType == +1).
|
|
263
263
|
if (node.majorType === 3 || node.majorType === 2 || node.majorType === 6) {
|
|
264
264
|
var sv = _specialText(node);
|
|
265
265
|
// The bare form is a single commonName -- hold its value to the SAME rules the array form applies, so a
|
|
266
266
|
// natively-signed certificate (which never reconstructs) cannot carry a value the reconstruction would refuse.
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
}
|
|
267
|
+
// A tag-48 MAC renders to a fixed 17-character EUI-64 string that satisfies every value rule by
|
|
268
|
+
// construction, and _reconAttrValue short-circuits on eui64 before any of them -- so asserting here would
|
|
269
|
+
// be a no-op. The rules bind on the TEXT forms below.
|
|
270
|
+
if (sv.eui64) return { rdns: [{ type: "commonName", eui64: sv.eui64 }], eui64: sv.eui64, dn: "CN=" + _macToEui64String(sv.eui64) };
|
|
272
271
|
var val = sv.text !== undefined ? sv.text : sv.hex;
|
|
273
272
|
var bareRdn = { type: "commonName", value: val };
|
|
274
273
|
_assertAttrValue(bareRdn);
|
|
275
|
-
return { rdns: [bareRdn], dn: "CN=" + val };
|
|
274
|
+
return { rdns: [bareRdn], dn: "CN=" + guard.name.escapeDnValue(val) };
|
|
276
275
|
}
|
|
277
276
|
if (node.majorType !== 4) throw _err("c509/bad-name", "a C509 Name must be null, a SpecialText, or an array of RDN attributes");
|
|
278
277
|
var rdns = [];
|
|
@@ -281,6 +280,10 @@ function _name509(node, isSubject) {
|
|
|
281
280
|
// Each RDN attribute is an (attributeType, attributeValue) pair; an odd-length array is a dangling
|
|
282
281
|
// attribute type with no value -- reject rather than silently drop the trailing element.
|
|
283
282
|
if (kids.length % 2 !== 0) throw _err("c509/bad-name", "a C509 Name array must be attribute-type/value pairs (dangling attribute type)");
|
|
283
|
+
// RFC 5280 sec. 4.1.2.4 -- the issuer MUST be a non-empty distinguished name (only the SUBJECT may be empty,
|
|
284
|
+
// for the subjectAltName case). An empty issuer array would reconstruct a certificate this toolkit's OWN
|
|
285
|
+
// x509.parse refuses to load (x509/bad-issuer), so the codec must not produce one.
|
|
286
|
+
if (kids.length === 0 && !isSubject) throw _err("c509/bad-name", "the issuer Name must not be empty (RFC 5280 sec. 4.1.2.4)");
|
|
284
287
|
for (var i = 0; i + 1 < kids.length; i += 2) {
|
|
285
288
|
// The attributeType slot must be a CBOR integer (major type 0/1). Guard the major type BEFORE the read
|
|
286
289
|
// so a non-integer type slot fails in this module's own domain (c509/bad-name), matching the value slot,
|
|
@@ -289,6 +292,11 @@ function _name509(node, isSubject) {
|
|
|
289
292
|
// Identifier issuer, cRLDistributionPoints cRLIssuer, nameConstraints subtree base) and the top-level Name.
|
|
290
293
|
if (kids[i].majorType !== 0 && kids[i].majorType !== 1) throw _err("c509/bad-name", "a C509 Name attribute type must be a CBOR integer");
|
|
291
294
|
var ti = Number(cbor.read.int(kids[i]));
|
|
295
|
+
// draft sec. 3.1.4: "in natively signed C509 certificates all CBOR ints SHALL be non-negative." The sign
|
|
296
|
+
// exists ONLY to reproduce the string type of an original X.509 DER, which a native certificate does not
|
|
297
|
+
// have -- so a negative attributeType there is an encoding no conformant producer emits, and accepting it
|
|
298
|
+
// would let this parser read a natively-signed name a conformant peer must reject.
|
|
299
|
+
if (isNative && ti < 0) throw _err("c509/bad-name", "a natively signed C509 Name attribute type integer must be non-negative (draft sec. 3.1.4), got " + ti);
|
|
292
300
|
var tname = ATTR_BY_INT[Math.abs(ti)];
|
|
293
301
|
if (tname === undefined) throw _err("c509/bad-name", "attribute type integer " + ti + " has no C509 registry row");
|
|
294
302
|
var v = _specialText(kids[i + 1]);
|
|
@@ -300,10 +308,14 @@ function _name509(node, isSubject) {
|
|
|
300
308
|
var rdn = { type: tname, value: vv, printable: ti < 0 };
|
|
301
309
|
_assertAttrValue(rdn);
|
|
302
310
|
rdns.push(rdn);
|
|
303
|
-
parts.push(_shortName(tname) + "=" + vv);
|
|
311
|
+
parts.push(_shortName(tname) + "=" + guard.name.escapeDnValue(vv));
|
|
304
312
|
}
|
|
305
313
|
return { rdns: rdns, dn: parts.join(",") };
|
|
306
314
|
}
|
|
315
|
+
// The rendered dn is an RFC 4514 string, so every VALUE is escaped through the shared guard the rest of the
|
|
316
|
+
// toolkit renders names with. Raw concatenation would let a single attribute whose value contains a comma
|
|
317
|
+
// render identically to a genuine multi-RDN name (a spoofable identity string, and the ONLY name surface a
|
|
318
|
+
// natively signed certificate has, since it never reconstructs DER), and would carry control bytes into logs.
|
|
307
319
|
function _shortName(n) { return n === "commonName" ? "CN" : n === "countryName" ? "C" : n === "organizationName" ? "O" : n === "organizationalUnitName" ? "OU" : n === "localityName" ? "L" : n === "stateOrProvinceName" ? "ST" : n; }
|
|
308
320
|
|
|
309
321
|
// ---- compact per-extension value codec (draft-20 sec. 3.3) -------------------
|
|
@@ -392,9 +404,9 @@ function _namedBitsFromContent(bytes, unusedBits) {
|
|
|
392
404
|
|
|
393
405
|
// One C509 (int, value) general name -> one DER GeneralName TLV. `ipMode` (name-constraints subtree base)
|
|
394
406
|
// only changes the iPAddress arm: SAN is a bare 4/16-octet address, a subtree base is the RFC 9549 form.
|
|
395
|
-
function _generalNameToDer(intVal, valueNode, ipMode) {
|
|
407
|
+
function _generalNameToDer(intVal, valueNode, ipMode, isNative) {
|
|
396
408
|
if (intVal === 1 || intVal === 2 || intVal === 6) return b.contextPrimitive(intVal, _ia5Bytes(valueNode, intVal)); // IMPLICIT IA5String
|
|
397
|
-
if (intVal === 4) return b.explicit(4, _reconName(_name509(valueNode, true))); // directoryName [4] EXPLICIT Name
|
|
409
|
+
if (intVal === 4) return b.explicit(4, _reconName(_name509(valueNode, true, isNative))); // directoryName [4] EXPLICIT Name
|
|
398
410
|
if (intVal === 7) return b.contextPrimitive(7, ipMode ? _ncIpToDer(valueNode) : _sanIpBytes(valueNode)); // iPAddress [7] IMPLICIT OCTET STRING
|
|
399
411
|
if (intVal === 8) return b.contextPrimitive(8, asn1.encodeOidContent(_oidName(valueNode, "c509/bad-extensions", "a registeredID [8]").oid)); // registeredID [8] IMPLICIT OID
|
|
400
412
|
if (intVal === 0 || intVal === -1 || intVal === -2 || intVal === -3) return _otherNameToDer(valueNode, intVal); // otherName [0]
|
|
@@ -420,12 +432,12 @@ function _generalNameFromDer(gn, ipMode) {
|
|
|
420
432
|
// them: a universal SEQUENCE for SAN/IAN, an implicit [n] for AKI issuer / DP cRLIssuer). These are always
|
|
421
433
|
// SAN-form (the RFC 9549 subtree-base iPAddress form is name-constraints-only and routes through
|
|
422
434
|
// _subtreesToDer / _subtreesFromDer, which call the singular codec with ipMode directly).
|
|
423
|
-
function _generalNamesToDer(node) {
|
|
435
|
+
function _generalNamesToDer(node, isNative) {
|
|
424
436
|
if (node.majorType !== 4 || !node.children) throw _err("c509/bad-extensions", "a GeneralNames value must be a CBOR array");
|
|
425
437
|
var kids = node.children;
|
|
426
438
|
if (kids.length === 0 || kids.length % 2 !== 0) throw _err("c509/bad-extensions", "a GeneralNames array must be non-empty (int, value) pairs (sec. 3.3)");
|
|
427
439
|
var out = [];
|
|
428
|
-
for (var i = 0; i + 1 < kids.length; i += 2) out.push(_generalNameToDer(Number(_cborIntVal(kids[i], "a GeneralName type")), kids[i + 1], false));
|
|
440
|
+
for (var i = 0; i + 1 < kids.length; i += 2) out.push(_generalNameToDer(Number(_cborIntVal(kids[i], "a GeneralName type")), kids[i + 1], false, isNative));
|
|
429
441
|
return out;
|
|
430
442
|
}
|
|
431
443
|
// The inverse: a list of DER GeneralName nodes -> the flat CBOR items, or null if ANY member is not
|
|
@@ -546,12 +558,12 @@ function _ncIpFromDer(buf) {
|
|
|
546
558
|
// GeneralSubtrees = [ + GeneralName ] (the flat int/value array) <-> the concatenated GeneralSubtree
|
|
547
559
|
// SEQUENCEs (RFC 5280 sec. 4.2.1.10: SEQUENCE { base, minimum [0] DEFAULT 0, maximum [1] OPTIONAL }); the
|
|
548
560
|
// C509 profile omits minimum/maximum, so each GeneralSubtree is base-only.
|
|
549
|
-
function _subtreesToDer(node) {
|
|
561
|
+
function _subtreesToDer(node, isNative) {
|
|
550
562
|
if (node.majorType !== 4 || !node.children) throw _err("c509/bad-extensions", "a GeneralSubtrees value must be a CBOR array");
|
|
551
563
|
var kids = node.children;
|
|
552
564
|
if (kids.length === 0 || kids.length % 2 !== 0) throw _err("c509/bad-extensions", "a GeneralSubtrees array must be non-empty (int, value) pairs");
|
|
553
565
|
var out = [];
|
|
554
|
-
for (var i = 0; i + 1 < kids.length; i += 2) out.push(b.sequence([_generalNameToDer(Number(_cborIntVal(kids[i], "a GeneralSubtree base type")), kids[i + 1], true)]));
|
|
566
|
+
for (var i = 0; i + 1 < kids.length; i += 2) out.push(b.sequence([_generalNameToDer(Number(_cborIntVal(kids[i], "a GeneralSubtree base type")), kids[i + 1], true, isNative)]));
|
|
555
567
|
return Buffer.concat(out);
|
|
556
568
|
}
|
|
557
569
|
function _subtreesFromDer(subtreeNodes) {
|
|
@@ -659,7 +671,7 @@ function _qualifierFromDer(pq) {
|
|
|
659
671
|
// One CBOR DistributionPointName [ fullName, reasons, cRLIssuer ] -> one DER DistributionPoint SEQUENCE.
|
|
660
672
|
// distributionPoint [0] is EXPLICIT (it wraps the DistributionPointName CHOICE); fullName [0], reasons [1],
|
|
661
673
|
// cRLIssuer [2] are IMPLICIT (RFC 5280 sec. 4.2.1.13) -- mixing these is the classic byte-exactness trap.
|
|
662
|
-
function _dpToDer(dpNode) {
|
|
674
|
+
function _dpToDer(dpNode, isNative) {
|
|
663
675
|
if (dpNode.majorType !== 4 || !dpNode.children || dpNode.children.length !== 3) throw _err("c509/bad-extensions", "a DistributionPoint must be a CBOR [ fullName, reasons, cRLIssuer ] array");
|
|
664
676
|
var fullName = dpNode.children[0], reasons = dpNode.children[1], crlIssuer = dpNode.children[2];
|
|
665
677
|
var uris;
|
|
@@ -670,7 +682,7 @@ function _dpToDer(dpNode) {
|
|
|
670
682
|
} else throw _err("c509/bad-extensions", "a DistributionPoint fullName must be a URI text or an array of URIs");
|
|
671
683
|
var fields = [b.explicit(0, b.contextConstructed(0, Buffer.concat(uris)))]; // distributionPoint [0] EXPLICIT { fullName [0] IMPLICIT GeneralNames }
|
|
672
684
|
if (!_isCborNull(reasons)) fields.push(_reasonsBitsToDer(Number(_cborUint(reasons, "cRLDistributionPoints reasons"))));
|
|
673
|
-
if (!_isCborNull(crlIssuer)) fields.push(b.contextConstructed(2, b.explicit(4, _reconName(_name509(crlIssuer, true))))); // cRLIssuer [2] { [4] directoryName }
|
|
685
|
+
if (!_isCborNull(crlIssuer)) fields.push(b.contextConstructed(2, b.explicit(4, _reconName(_name509(crlIssuer, true, isNative))))); // cRLIssuer [2] { [4] directoryName }
|
|
674
686
|
return b.sequence(fields);
|
|
675
687
|
}
|
|
676
688
|
// One DER DistributionPoint SEQUENCE -> the pieces for the CBOR DistributionPointName, or null when the DP
|
|
@@ -717,7 +729,7 @@ function _dpFromDer(dp) {
|
|
|
717
729
|
|
|
718
730
|
// Decode a compact extension value (a decoded CBOR node) to the DER extnValue inner content. Fails closed
|
|
719
731
|
// (c509/bad-extensions) on a CBOR shape the named extension does not define.
|
|
720
|
-
function _extValueToDer(name, node) {
|
|
732
|
+
function _extValueToDer(name, node, isNative) {
|
|
721
733
|
switch (name) {
|
|
722
734
|
case "subjectKeyIdentifier": // KeyIdentifier = bytes -> OCTET STRING(keyid)
|
|
723
735
|
if (node.majorType !== 2) throw _err("c509/bad-extensions", "a subjectKeyIdentifier value must be a CBOR byte string");
|
|
@@ -737,7 +749,7 @@ function _extValueToDer(name, node) {
|
|
|
737
749
|
if (node.children[0].majorType !== 2) throw _err("c509/bad-extensions", "an authorityKeyIdentifier keyIdentifier must be a CBOR byte string");
|
|
738
750
|
return b.sequence([
|
|
739
751
|
b.contextPrimitive(0, node.children[0].content), // keyIdentifier [0] IMPLICIT OCTET STRING
|
|
740
|
-
b.contextConstructed(1, Buffer.concat(_generalNamesToDer(node.children[1]))), // authorityCertIssuer [1] IMPLICIT GeneralNames
|
|
752
|
+
b.contextConstructed(1, Buffer.concat(_generalNamesToDer(node.children[1], isNative))), // authorityCertIssuer [1] IMPLICIT GeneralNames
|
|
741
753
|
b.contextPrimitive(2, _serialIntContent(node.children[2])), // authorityCertSerialNumber [2] IMPLICIT INTEGER
|
|
742
754
|
]);
|
|
743
755
|
}
|
|
@@ -764,12 +776,12 @@ function _extValueToDer(name, node) {
|
|
|
764
776
|
case "subjectAltName":
|
|
765
777
|
case "issuerAltName": // SubjectAltName = GeneralNames / text (exactly one dNSName -> bare text)
|
|
766
778
|
if (node.majorType === 3) return b.sequence([b.contextPrimitive(2, _ia5Bytes(node, 2))]);
|
|
767
|
-
return b.sequence(_generalNamesToDer(node));
|
|
779
|
+
return b.sequence(_generalNamesToDer(node, isNative));
|
|
768
780
|
case "nameConstraints": { // [ permittedSubtrees / null, excludedSubtrees / null ]
|
|
769
781
|
if (node.majorType !== 4 || !node.children || node.children.length !== 2) throw _err("c509/bad-extensions", "a nameConstraints value must be a 2-element CBOR array [ permitted, excluded ] (sec. 3.3)");
|
|
770
782
|
var ncFields = [];
|
|
771
|
-
if (!_isCborNull(node.children[0])) ncFields.push(b.contextConstructed(0, _subtreesToDer(node.children[0]))); // permittedSubtrees [0]
|
|
772
|
-
if (!_isCborNull(node.children[1])) ncFields.push(b.contextConstructed(1, _subtreesToDer(node.children[1]))); // excludedSubtrees [1]
|
|
783
|
+
if (!_isCborNull(node.children[0])) ncFields.push(b.contextConstructed(0, _subtreesToDer(node.children[0], isNative))); // permittedSubtrees [0]
|
|
784
|
+
if (!_isCborNull(node.children[1])) ncFields.push(b.contextConstructed(1, _subtreesToDer(node.children[1], isNative))); // excludedSubtrees [1]
|
|
773
785
|
if (ncFields.length === 0) throw _err("c509/bad-extensions", "nameConstraints must contain permittedSubtrees or excludedSubtrees (RFC 5280 sec. 4.2.1.10)");
|
|
774
786
|
return b.sequence(ncFields);
|
|
775
787
|
}
|
|
@@ -786,7 +798,7 @@ function _extValueToDer(name, node) {
|
|
|
786
798
|
case "freshestCRL": // [ + DistributionPointName ] / text (one DP, one-URI fullName -> bare text)
|
|
787
799
|
if (node.majorType === 3) return b.sequence([b.sequence([b.explicit(0, b.contextConstructed(0, b.contextPrimitive(6, _ia5Bytes(node, 6))))])]);
|
|
788
800
|
if (node.majorType !== 4 || !node.children || node.children.length < 1) throw _err("c509/bad-extensions", "a " + name + " value must be a CBOR array of DistributionPoints or a bare URI text (sec. 3.3)");
|
|
789
|
-
return b.sequence(node.children.map(function (dp) { return _dpToDer(dp); }));
|
|
801
|
+
return b.sequence(node.children.map(function (dp) { return _dpToDer(dp, isNative); }));
|
|
790
802
|
case "certificatePolicies": { // [ pid, [ *(qid, qtext) ], ... ] -> SEQUENCE OF PolicyInformation
|
|
791
803
|
if (node.majorType !== 4 || !node.children) throw _err("c509/bad-extensions", "a certificatePolicies value must be a CBOR array");
|
|
792
804
|
var cpKids = node.children;
|
|
@@ -834,7 +846,7 @@ function _extValueToDer(name, node) {
|
|
|
834
846
|
return b.sequence(pcFields); // positional slot -> tag makes [0] < [1] unique + ascending by construction
|
|
835
847
|
}
|
|
836
848
|
case "subjectDirectoryAttributes": // [ type, values, type, values, ... ] -> SEQUENCE OF Attribute (sec. 3.3)
|
|
837
|
-
return _sdaToDer(node);
|
|
849
|
+
return _sdaToDer(node, isNative);
|
|
838
850
|
default:
|
|
839
851
|
throw _err("c509/bad-extensions", "extension " + name + " has no compact value decoder");
|
|
840
852
|
}
|
|
@@ -1032,8 +1044,8 @@ function _extValueFromDer(name, der) {
|
|
|
1032
1044
|
// Every universal PRIMITIVE tag whose content this toolkit can strictly validate, mapped to its reader. A
|
|
1033
1045
|
// universal primitive OUTSIDE this map has no strict content validator here, so it is REJECTED rather than
|
|
1034
1046
|
// spliced unchecked. The documented residual: a value whose type this toolkit cannot yet strictly validate is
|
|
1035
|
-
// not compact-representable --
|
|
1036
|
-
// and a fractional-seconds GeneralizedTime (the X.690 sec. 11.7 relaxation is deliberately scoped to the codec
|
|
1047
|
+
// not compact-representable -- VideotexString / GraphicString / GeneralString / ObjectDescriptor / REAL (no
|
|
1048
|
+
// content reader in this toolkit), and a fractional-seconds GeneralizedTime (the X.690 sec. 11.7 relaxation is deliberately scoped to the codec
|
|
1037
1049
|
// and RFC 3161 timestamping, and must not creep into a third consumer). On the ENCODE path such a value simply
|
|
1038
1050
|
// degrades to the byte-exact ~oid + byte-string form, losing nothing; on the DECODE path it is a fail-closed
|
|
1039
1051
|
// verdict rather than a guess.
|
|
@@ -1042,7 +1054,10 @@ var _ANY_VALUE_READERS = (function () {
|
|
|
1042
1054
|
m[T.BOOLEAN] = R.boolean; m[T.INTEGER] = R.integer; m[T.ENUMERATED] = R.enumerated;
|
|
1043
1055
|
m[T.BIT_STRING] = R.bitString; m[T.OCTET_STRING] = R.octetString; m[T.NULL] = R.nullValue;
|
|
1044
1056
|
m[T.OBJECT_IDENTIFIER] = R.oid; m[T.UTC_TIME] = R.time; m[T.GENERALIZED_TIME] = R.time;
|
|
1045
|
-
|
|
1057
|
+
// NumericString reads through its OWN reader: it is not a DirectoryString type, and routing it through
|
|
1058
|
+
// read.string would fold it into the RFC 5280 sec. 7.1 name-comparison identity class (see asn1-der.js).
|
|
1059
|
+
m[T.NUMERIC_STRING] = R.numericString;
|
|
1060
|
+
[T.UTF8_STRING, T.PRINTABLE_STRING, T.IA5_STRING, T.TELETEX_STRING, T.VISIBLE_STRING, T.BMP_STRING, T.UNIVERSAL_STRING].forEach(function (t) { m[t] = R.string; });
|
|
1046
1061
|
return m;
|
|
1047
1062
|
})();
|
|
1048
1063
|
|
|
@@ -1123,7 +1138,7 @@ function _derSetInDeclaredOrder(vals) {
|
|
|
1123
1138
|
|
|
1124
1139
|
// [ type1, values1, ... ] -> the DER SubjectDirectoryAttributes. A malformed native value fails closed
|
|
1125
1140
|
// (c509/bad-extensions); on the encode path the same throw is a round-trip mismatch -> whole-ext ~oid fallback.
|
|
1126
|
-
function _sdaToDer(node) {
|
|
1141
|
+
function _sdaToDer(node, isNative) {
|
|
1127
1142
|
if (node.majorType !== 4 || !node.children) throw _err("c509/bad-extensions", "a subjectDirectoryAttributes value must be a CBOR array");
|
|
1128
1143
|
var kids = node.children;
|
|
1129
1144
|
if (kids.length === 0 || kids.length % 2 !== 0) throw _err("c509/bad-extensions", "a subjectDirectoryAttributes array must be non-empty (attributeType, attributeValue) pairs (sec. 3.3)");
|
|
@@ -1134,12 +1149,15 @@ function _sdaToDer(node) {
|
|
|
1134
1149
|
var vals = [];
|
|
1135
1150
|
if (typeNode.majorType === 0 || typeNode.majorType === 1) { // int form: a sec. 8.6 registry alias, text values
|
|
1136
1151
|
var ti = Number(cbor.read.int(typeNode));
|
|
1152
|
+
// draft sec. 3.1.4 applies to EVERY int in a natively signed certificate, not just the top-level Name's.
|
|
1153
|
+
if (isNative && ti < 0) throw _err("c509/bad-extensions", "a natively signed C509 subjectDirectoryAttributes attribute type integer must be non-negative (draft sec. 3.1.4), got " + ti);
|
|
1137
1154
|
var tname = ATTR_BY_INT[Math.abs(ti)];
|
|
1138
1155
|
if (tname === undefined) throw _err("c509/bad-extensions", "a subjectDirectoryAttributes attribute type int " + ti + " has no C509 sec. 8.6 registry row");
|
|
1139
|
-
// countryName / serialNumber
|
|
1140
|
-
//
|
|
1141
|
-
//
|
|
1142
|
-
|
|
1156
|
+
// countryName / serialNumber carry a CHARACTER restriction (draft sec. 3.1.4 "SHALL contain only
|
|
1157
|
+
// characters from the 74-character ASCII subset permitted by PrintableString"), NOT a sign override --
|
|
1158
|
+
// _reconAttrValue asserts the charset and honours the declared string type. Requiring the negative sign
|
|
1159
|
+
// here would also make these attributes unrepresentable in a NATIVE certificate, whose ints SHALL all be
|
|
1160
|
+
// non-negative (same sec.), so the rule is the charset, not the sign.
|
|
1143
1161
|
for (var vi = 0; vi < valuesNode.children.length; vi++) {
|
|
1144
1162
|
var vn = valuesNode.children[vi];
|
|
1145
1163
|
if (vn.majorType !== 3) throw _err("c509/bad-extensions", "a subjectDirectoryAttributes int-form attribute value must be a CBOR text string (a non-string value requires the ~oid form)");
|
|
@@ -1240,7 +1258,7 @@ function _tryCompactExtValue(name, der) {
|
|
|
1240
1258
|
}
|
|
1241
1259
|
|
|
1242
1260
|
// extensions (sec. 3.1.10/sec. 3.3/sec. 8.8): [ * Extension ] | a single keyUsage int-shortcut.
|
|
1243
|
-
function _extensions(node) {
|
|
1261
|
+
function _extensions(node, isNative) {
|
|
1244
1262
|
// The keyUsage int-shortcut (sec. 3.1.10): a bare int -> one keyUsage extension, criticality from the
|
|
1245
1263
|
// sign, value = abs(int) (Appendix A.1.1: the single int 1 -> non-critical keyUsage digitalSignature).
|
|
1246
1264
|
if (node.majorType === 0 || node.majorType === 1) {
|
|
@@ -1267,7 +1285,7 @@ function _extensions(node) {
|
|
|
1267
1285
|
// non-byte-string value there is an unsupported compact form and MUST fail closed -- a text/array value
|
|
1268
1286
|
// is NOT raw DER, and copying its bytes would reconstruct a structurally invalid extension.
|
|
1269
1287
|
if (EXT_COMPACT[name]) {
|
|
1270
|
-
valContent = _extValueToDer(name, valNode);
|
|
1288
|
+
valContent = _extValueToDer(name, valNode, isNative);
|
|
1271
1289
|
} else if (valNode.majorType === 2) {
|
|
1272
1290
|
valContent = valNode.content;
|
|
1273
1291
|
} else {
|
|
@@ -1323,7 +1341,14 @@ function _reconAttrValue(rdn) {
|
|
|
1323
1341
|
// emailAddress is an IA5String-only attribute (draft sec. 3.1.4): its type, not the int's sign, fixes the
|
|
1324
1342
|
// string type, so it reconstructs as an IA5String whichever way the non-negative int was written.
|
|
1325
1343
|
if (rdn.type === "emailAddress") return b.ia5(s);
|
|
1326
|
-
|
|
1344
|
+
// serialNumber / countryName carry a CHARACTER restriction, not a string-type override: draft sec. 3.1.4
|
|
1345
|
+
// "SHALL contain only characters from the 74-character ASCII subset permitted by PrintableString". Enforce
|
|
1346
|
+
// that on the CHARACTERS and still honour the sign for the string type -- coercing them to PrintableString
|
|
1347
|
+
// regardless of sign would make the +N and -N encodings of one value reconstruct IDENTICAL DER, so a single
|
|
1348
|
+
// X.509 signature would cover two distinct C509 encodings (a malleability window in the type-3 transform).
|
|
1349
|
+
// b.printable IS the PrintableString charset authority -- run it for the assert even when the sign selects
|
|
1350
|
+
// utf8String, so the restriction binds on the characters without overriding the declared string type.
|
|
1351
|
+
if (!rdn.printable && (rdn.type === "countryName" || rdn.type === "serialNumber")) b.printable(s);
|
|
1327
1352
|
return rdn.printable ? b.printable(s) : b.utf8(s);
|
|
1328
1353
|
}
|
|
1329
1354
|
|
|
@@ -1494,10 +1519,16 @@ function parse(input) {
|
|
|
1494
1519
|
var sHex = serialBytes.content.toString("hex");
|
|
1495
1520
|
|
|
1496
1521
|
var sigAlg = _algorithm(f[2], SIG_ALG_BY_INT, "c509/unknown-algorithm", "issuerSignatureAlgorithm");
|
|
1497
|
-
var issuer = _name509(f[3], false);
|
|
1522
|
+
var issuer = _name509(f[3], false, type === 2);
|
|
1498
1523
|
var notBefore = _time(f[4], false, "validityNotBefore");
|
|
1499
1524
|
var notAfter = _time(f[5], true, "validityNotAfter");
|
|
1500
|
-
var subject = _name509(f[6], true);
|
|
1525
|
+
var subject = _name509(f[6], true, type === 2);
|
|
1526
|
+
// A CBOR-null issuer means issuer == subject (self-signed), so the EFFECTIVE issuer is the subject -- and it
|
|
1527
|
+
// must still satisfy RFC 5280 sec. 4.1.2.4. Checking only the array form would let the null form rebuild the
|
|
1528
|
+
// very empty issuer the array form is refused for.
|
|
1529
|
+
if (issuer === null && (!subject || !subject.rdns || subject.rdns.length === 0)) {
|
|
1530
|
+
throw _err("c509/bad-name", "a self-signed C509 (issuer == subject) requires a non-empty subject, since it is also the issuer (RFC 5280 sec. 4.1.2.4)");
|
|
1531
|
+
}
|
|
1501
1532
|
var spkAlg = _algorithm(f[7], PK_ALG_BY_INT, "c509/unknown-algorithm", "subjectPublicKeyAlgorithm");
|
|
1502
1533
|
var subjectPublicKey = null, rsaKey = null;
|
|
1503
1534
|
if (spkAlg.name === "rsaEncryption") {
|
|
@@ -1511,7 +1542,7 @@ function parse(input) {
|
|
|
1511
1542
|
if (f[8].majorType !== 2) throw _err("c509/bad-spki", "subjectPublicKey must be a CBOR byte string");
|
|
1512
1543
|
subjectPublicKey = f[8].content;
|
|
1513
1544
|
}
|
|
1514
|
-
var extensions = _extensions(f[9]);
|
|
1545
|
+
var extensions = _extensions(f[9], type === 2);
|
|
1515
1546
|
if (f[10].majorType !== 2) throw _err("c509/bad-signature", "issuerSignatureValue must be a CBOR byte string");
|
|
1516
1547
|
var signatureValue = f[10].content;
|
|
1517
1548
|
|
|
@@ -1751,7 +1782,12 @@ function _c509NameFromDer(nameBytes) {
|
|
|
1751
1782
|
var attrName = oid.name(asn1.read.oid(attr.children[0]));
|
|
1752
1783
|
if (attrName == null || ATTR_TO_INT[attrName] === undefined) throw _err("c509/non-invertible", "attribute type " + attrName + " has no C509 registry integer");
|
|
1753
1784
|
var valNode = attr.children[1];
|
|
1754
|
-
|
|
1785
|
+
// A value whose string type this codec cannot represent (NumericString and the other non-DirectoryString
|
|
1786
|
+
// types read.string declines) is NOT compact-representable -- report that in THIS module's domain rather
|
|
1787
|
+
// than leaking the codec's own asn1/* fault out of encode(), which is what every sibling shape does.
|
|
1788
|
+
var value;
|
|
1789
|
+
try { value = asn1.read.string(valNode); }
|
|
1790
|
+
catch (e) { throw _err("c509/non-invertible", "attribute " + attrName + " carries a value whose string type the C509 sec. 8.6 int form cannot represent", e); }
|
|
1755
1791
|
// The IA5String type belongs ONLY to an IA5-only attribute (emailAddress, draft sec. 3.1.4), whose value
|
|
1756
1792
|
// reconstructs from its type rather than the int's sign. Refuse either mismatch here with a precise verdict
|
|
1757
1793
|
// instead of emitting an int form whose reconstruction would differ from the source bytes.
|
package/lib/schema-pkix.js
CHANGED
|
@@ -331,6 +331,15 @@ function attrValueToString(ns) {
|
|
|
331
331
|
if (!e || (e.code !== "asn1/expected-string" && e.code !== "asn1/expected-primitive")) {
|
|
332
332
|
throw ns.E(ns.prefix + "/bad-atv", "malformed string in attribute value: " + ((e && e.message) || String(e)));
|
|
333
333
|
}
|
|
334
|
+
// NumericString is deliberately NOT read by asn1.read.string -- a value that returns a plain string joins
|
|
335
|
+
// the RFC 5280 sec. 7.1 name-comparison identity class, and NumericString is not a DirectoryString type.
|
|
336
|
+
// It does have its own strict reader though, so VALIDATE the content here (an out-of-alphabet value is
|
|
337
|
+
// malformed DER and must not be accepted merely because it falls to the opaque form) while still
|
|
338
|
+
// surfacing it in the type-distinct RFC 4514 hex form, which keeps it out of that identity class.
|
|
339
|
+
if (node.tagClass === "universal" && node.tagNumber === asn1.TAGS.NUMERIC_STRING) {
|
|
340
|
+
try { asn1.read.numericString(node); }
|
|
341
|
+
catch (e2) { throw ns.E(ns.prefix + "/bad-atv", "malformed NumericString in attribute value: " + ((e2 && e2.message) || String(e2))); }
|
|
342
|
+
}
|
|
334
343
|
return "#" + node.bytes.toString("hex");
|
|
335
344
|
}
|
|
336
345
|
if (s.charAt(0) === "#" || s.charAt(0) === "\\") return "\\" + s;
|
package/lib/vendor/MANIFEST.json
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
{
|
|
2
|
-
"_comment": "Vendored dependencies — none currently. @blamejs/pki's cryptography runs entirely on Node's built-in node:crypto: the classical algorithm set (RSA, ECDSA, EdDSA, ECDH, AES, HMAC, HKDF, PBKDF2, the SHA family) AND the FIPS 203/204/205 post-quantum algorithms (ML-KEM, ML-DSA, SLH-DSA) via the platform OpenSSL 3.5 that the Node engine floor (>=24.
|
|
2
|
+
"_comment": "Vendored dependencies — none currently. @blamejs/pki's cryptography runs entirely on Node's built-in node:crypto: the classical algorithm set (RSA, ECDSA, EdDSA, ECDH, AES, HMAC, HKDF, PBKDF2, the SHA family) AND the FIPS 203/204/205 post-quantum algorithms (ML-KEM, ML-DSA, SLH-DSA) via the platform OpenSSL 3.5 that the Node engine floor (>=24.19) ships. A built-in ships zero bytes and is OpenSSL-interoperable by construction, so no crypto bundle is vendored. A package is added here ONLY when a specific operation is confirmed missing from the engine floor; see lib/vendor/README.md for the policy.",
|
|
3
3
|
"packages": {}
|
|
4
4
|
}
|
package/lib/vendor/README.md
CHANGED
|
@@ -6,7 +6,7 @@ vendors **nothing** — this directory holds only the manifest.
|
|
|
6
6
|
## Native-first crypto
|
|
7
7
|
|
|
8
8
|
The toolkit's cryptography runs entirely on Node's built-in `node:crypto`. The
|
|
9
|
-
engine floor (Node `>=24.
|
|
9
|
+
engine floor (Node `>=24.19`) links OpenSSL 3.5, which provides:
|
|
10
10
|
|
|
11
11
|
- the full classical set — RSA (PKCS#1 v1.5, PSS, OAEP), ECDSA, EdDSA
|
|
12
12
|
(Ed25519/Ed448), ECDH (incl. X25519/X448), AES (GCM/CBC/CTR/KW), HMAC, HKDF,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@blamejs/pki",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.2",
|
|
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",
|
|
@@ -47,7 +47,7 @@
|
|
|
47
47
|
"owns-its-stack"
|
|
48
48
|
],
|
|
49
49
|
"engines": {
|
|
50
|
-
"node": ">=24.
|
|
50
|
+
"node": ">=24.19.0"
|
|
51
51
|
},
|
|
52
52
|
"files": [
|
|
53
53
|
"index.js",
|
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:484717d3-b574-44fc-ba53-e13d70ea5f85",
|
|
6
6
|
"version": 1,
|
|
7
7
|
"metadata": {
|
|
8
|
-
"timestamp": "2026-08-
|
|
8
|
+
"timestamp": "2026-08-08T05:35:44.402Z",
|
|
9
9
|
"lifecycles": [
|
|
10
10
|
{
|
|
11
11
|
"phase": "build"
|
|
@@ -15,18 +15,18 @@
|
|
|
15
15
|
{
|
|
16
16
|
"vendor": "npm",
|
|
17
17
|
"name": "cli",
|
|
18
|
-
"version": "11.
|
|
18
|
+
"version": "11.17.0"
|
|
19
19
|
}
|
|
20
20
|
],
|
|
21
21
|
"component": {
|
|
22
|
-
"bom-ref": "@blamejs/pki@0.4.
|
|
22
|
+
"bom-ref": "@blamejs/pki@0.4.2",
|
|
23
23
|
"type": "application",
|
|
24
24
|
"name": "pki",
|
|
25
|
-
"version": "0.4.
|
|
25
|
+
"version": "0.4.2",
|
|
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.4.
|
|
29
|
+
"purl": "pkg:npm/%40blamejs/pki@0.4.2",
|
|
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.4.
|
|
57
|
+
"ref": "@blamejs/pki@0.4.2",
|
|
58
58
|
"dependsOn": []
|
|
59
59
|
}
|
|
60
60
|
]
|