@blamejs/pki 0.4.0 → 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 +31 -0
- package/README.md +2 -2
- package/lib/asn1-der.js +22 -1
- package/lib/oid.js +1 -0
- package/lib/schema-c509.js +341 -40
- package/lib/schema-pkix.js +9 -0
- package/lib/vendor/MANIFEST.json +1 -1
- package/lib/vendor/README.md +1 -1
- package/package.json +4 -2
- package/sbom.cdx.json +7 -7
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,37 @@ 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.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
|
|
25
|
+
|
|
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.
|
|
27
|
+
|
|
28
|
+
### Added
|
|
29
|
+
|
|
30
|
+
- pki.schema.c509 encodes and decodes the compact value form for the subjectDirectoryAttributes extension (draft-ietf-cose-cbor-encoded-cert-20 sec. 3.3): a flat array of (attribute type, attribute values) pairs where each type is a sec. 8.6 registry integer (the sign selecting the directory-string type) or an unwrapped OID, and each values slot is a non-empty array holding the attribute's SET of one or more values -- the string values for a registry-integer type, or the raw DER attribute values for an unwrapped-OID type. Both directions invert to the DER extnValue byte-for-byte, so a certificate carrying the extension is the specific compact shape a conformant C509 implementation reads rather than an opaque DER byte string. The encoder is guarded per attribute and per extension: an attribute whose value is not a directory string, or whose value SET mixes string types, uses the unwrapped-OID form for that attribute (keeping the rest of the extension compact); a value the compact form cannot represent falls the whole extension back to the unwrapped-OID byte-string form instead of encoding lossily; and a malformed compact value fails closed with a typed C509Error.
|
|
31
|
+
- The OID registry gains the subjectDirectoryAttributes certificate-extension identifier (2.5.29.9), resolvable through pki.oid.byName / pki.oid.name.
|
|
32
|
+
- The ASN.1 codec reads NumericString (pki.asn1.TAGS.NUMERIC_STRING), the X.520 syntax of the x121Address and internationalISDNNumber directory attributes. It is validated strictly like every other string type: a value outside the digits-and-space set the type permits is rejected as malformed rather than decoded.
|
|
33
|
+
|
|
34
|
+
### Changed
|
|
35
|
+
|
|
36
|
+
- A C509 certificate carrying subjectDirectoryAttributes now encodes to -- and decodes from -- its compact CBOR value shape rather than the unwrapped-OID byte-string form an earlier release emitted; both reconstruct the same DER, but the CBOR bytes differ, so re-encode any C509 produced by an earlier release.
|
|
37
|
+
|
|
7
38
|
## v0.4.0 — 2026-08-06
|
|
8
39
|
|
|
9
40
|
CRL issuance and verification, PKCS#12 build and open, attribute-certificate issuance, and the key-material lifecycle graduate to stable, and pki.schema.c509 adds the compact policyMappings and policyConstraints value forms -- a C509 certificate's policy mappings and policy constraints now ride their specific draft-20 CBOR shape and interoperate with a conformant C509 implementation rather than only this decoder.
|
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
|
|
@@ -204,7 +204,7 @@ is callable today; nothing below is a stub.
|
|
|
204
204
|
| `pki.webcrypto` | A W3C WebCrypto (`SubtleCrypto`) engine over `node:crypto` — `sign`/`verify`/`encrypt`/`decrypt`/`deriveBits`/`digest`/`generateKey`/`importKey`/`exportKey` across RSA, ECDSA, ECDH, Ed25519/Ed448, AES, HMAC, HKDF, PBKDF2, SHA — **and** post-quantum ML-DSA-44/65/87 and SLH-DSA signatures, plus ML-KEM-512/768/1024 key generation and certificate/PKCS#8 import — the RFC 9935 seed / expandedKey / both private-key CHOICE is validated fail-closed, so an OpenSSL-legacy bare-seed or an internally inconsistent key is rejected with a typed error (KEM encapsulation lands with CMS KEM-decrypt). Zero-dependency, OpenSSL-interoperable |
|
|
205
205
|
| `pki.schema` | The schema family — `parse` detects which PKI format DER / PEM encodes and routes to the right parser, `all` enumerates the registered formats, and the engine + per-format members are grouped here |
|
|
206
206
|
| `pki.schema.x509` | Parse DER / PEM certificates into structured, validated fields, with named + partly-decoded extensions — including the RFC 3739 / ETSI EN 319 412-5 qualified-certificate `qcStatements` (EU-qualified declaration, reliance limit, QSCD flag, certificate type, retention, PDS URLs, country of qualification; unknown statements preserved opaque) and the Microsoft Active Directory Certificate Services enrollment extensions (certificate template, CA version, previous-CA-certificate hash, application policies), fail-closed — `parse`, `pemDecode`, `pemEncode` |
|
|
207
|
-
| `pki.schema.c509` | Parse **and encode** C509 CBOR-encoded certificates (draft-ietf-cose-cbor-encoded-cert) — the compact CBOR profile of X.509, decoded fail-closed under deterministic CBOR; an explicit `parse` call (CBOR, not DER, so not auto-routed). `encode(input)` is the byte-exact inverse: a DER X.509 v3 certificate forward-transforms to a compact type-3 C509 whose reconstruction reproduces the original DER byte for byte (so the original signature still verifies), or a `parse` result re-emits its native array — canonical deterministic CBOR with the registry integer shorthands, the C509 compressions, and the compact draft-20 per-extension value forms — the scalar extensions (keyUsage, basicConstraints, extended key usage, subject key identifier, and more), the general-name-bearing extensions (subjectAltName, issuer alt name, name constraints, CRL distribution points, authority/subject information access, and the full authority key identifier) over one shared GeneralNames value codec, certificate policies (registry-integer or OID policy identifiers with their CPS-URI and UserNotice qualifiers), and policy mappings and policy constraints; a certificate outside the invertible set throws a typed `C509Error` |
|
|
207
|
+
| `pki.schema.c509` | Parse **and encode** C509 CBOR-encoded certificates (draft-ietf-cose-cbor-encoded-cert) — the compact CBOR profile of X.509, decoded fail-closed under deterministic CBOR; an explicit `parse` call (CBOR, not DER, so not auto-routed). `encode(input)` is the byte-exact inverse: a DER X.509 v3 certificate forward-transforms to a compact type-3 C509 whose reconstruction reproduces the original DER byte for byte (so the original signature still verifies), or a `parse` result re-emits its native array — canonical deterministic CBOR with the registry integer shorthands, the C509 compressions, and the compact draft-20 per-extension value forms — the scalar extensions (keyUsage, basicConstraints, extended key usage, subject key identifier, and more), the general-name-bearing extensions (subjectAltName, issuer alt name, name constraints, CRL distribution points, authority/subject information access, and the full authority key identifier) over one shared GeneralNames value codec, certificate policies (registry-integer or OID policy identifiers with their CPS-URI and UserNotice qualifiers), and policy mappings and policy constraints, and subject directory attributes; a certificate outside the invertible set throws a typed `C509Error` |
|
|
208
208
|
| `pki.schema.crl` | Parse DER / PEM X.509 CRLs per RFC 5280 §5 — revoked serials with real-`Date` revocation times, named + partly-decoded extensions, fail-closed — `parse`, `pemDecode`, `pemEncode` |
|
|
209
209
|
| `pki.schema.csr` | Parse DER / PEM PKCS#10 certification requests per RFC 2986 — subject DN, public key, requested attributes, signature, fail-closed — `parse`, `pemDecode`, `pemEncode` |
|
|
210
210
|
| `pki.schema.pkcs8` | Parse DER / PEM PKCS#8 private keys per RFC 5208 / 5958 — algorithm, raw key bytes, attributes, optional public key, fail-closed; encrypted keys recognized (not decrypted) — `parse`, `parseEncrypted`, `pemDecode`, `pemEncode` |
|
package/lib/asn1-der.js
CHANGED
|
@@ -66,6 +66,7 @@ var UNIVERSAL_TYPES = {
|
|
|
66
66
|
UTF8_STRING: { tag: 0x0c, form: "primitive" },
|
|
67
67
|
SEQUENCE: { tag: 0x10, form: "constructed" },
|
|
68
68
|
SET: { tag: 0x11, form: "constructed" },
|
|
69
|
+
NUMERIC_STRING: { tag: 0x12, form: "primitive" },
|
|
69
70
|
PRINTABLE_STRING: { tag: 0x13, form: "primitive" },
|
|
70
71
|
TELETEX_STRING: { tag: 0x14, form: "primitive" },
|
|
71
72
|
IA5_STRING: { tag: 0x16, form: "primitive" },
|
|
@@ -95,7 +96,7 @@ var CONSTRUCTED_BIT = 0x20;
|
|
|
95
96
|
// constructed-capable set is DERIVED from the registry above and keyed by tag
|
|
96
97
|
// for an O(1) decode-time form check. It is a WHITELIST: a universal tag
|
|
97
98
|
// outside it -- a registered primitive type and an unregistered one
|
|
98
|
-
// (
|
|
99
|
+
// (GeneralString, ObjectDescriptor, REAL, ...) alike -- has no
|
|
99
100
|
// DER constructed form, so the default is reject.
|
|
100
101
|
var CONSTRUCTED_ONLY_UNIVERSAL_TAGS = Object.create(null);
|
|
101
102
|
Object.keys(UNIVERSAL_TYPES).forEach(function (k) {
|
|
@@ -622,6 +623,24 @@ function _decodePrintable(buf) {
|
|
|
622
623
|
return s;
|
|
623
624
|
}
|
|
624
625
|
|
|
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");
|
|
640
|
+
if (!NUMERIC_RE.test(s)) throw new Asn1Error("asn1/bad-numeric-string", "NumericString has characters outside the digits-and-space set");
|
|
641
|
+
return s;
|
|
642
|
+
}
|
|
643
|
+
|
|
625
644
|
// UTF8String must be well-formed UTF-8; a lenient decoder substitutes U+FFFD
|
|
626
645
|
// for invalid sequences, silently mangling hostile input into valid text.
|
|
627
646
|
function _decodeUtf8Strict(buf) {
|
|
@@ -870,6 +889,7 @@ function encodeOidContent(dotted) {
|
|
|
870
889
|
}
|
|
871
890
|
|
|
872
891
|
var PRINTABLE_RE = /^[A-Za-z0-9 '()+,\-./:=?]*$/;
|
|
892
|
+
var NUMERIC_RE = /^[0-9 ]*$/;
|
|
873
893
|
|
|
874
894
|
function _fmtTwo(n) { return (n < 10 ? "0" : "") + n; }
|
|
875
895
|
|
|
@@ -1067,6 +1087,7 @@ module.exports = {
|
|
|
1067
1087
|
booleanImplicit: readBooleanImplicit,
|
|
1068
1088
|
oid: readOid,
|
|
1069
1089
|
string: readString,
|
|
1090
|
+
numericString: readNumericString,
|
|
1070
1091
|
time: readTime,
|
|
1071
1092
|
},
|
|
1072
1093
|
};
|
package/lib/oid.js
CHANGED
|
@@ -60,6 +60,7 @@ var FAMILIES = {
|
|
|
60
60
|
|
|
61
61
|
// RFC 5280 certificate extensions.
|
|
62
62
|
certExtension: { base: [2, 5, 29], of: {
|
|
63
|
+
subjectDirectoryAttributes: 9, // id-ce 9 (RFC 5280 sec. 4.2.1.8) -> 2.5.29.9
|
|
63
64
|
subjectKeyIdentifier: 14, keyUsage: 15, subjectAltName: 17, issuerAltName: 18,
|
|
64
65
|
basicConstraints: 19,
|
|
65
66
|
// CRL + CRL-entry extensions (RFC 5280 sec. 5.2, sec. 5.3)
|
package/lib/schema-c509.js
CHANGED
|
@@ -57,10 +57,12 @@ var PK_ALG_BY_INT = {
|
|
|
57
57
|
// EC curve -> field size in bytes (the SEC1 coordinate width), for point-length validation.
|
|
58
58
|
var EC_FIELD_BYTES = { "prime256v1": 32, "secp384r1": 48, "secp521r1": 66 };
|
|
59
59
|
|
|
60
|
-
// sec. 8.6 RDN attribute types (abs(int) -> name; the sign selects the X.509 string type
|
|
61
|
-
//
|
|
62
|
-
//
|
|
60
|
+
// sec. 8.6 RDN attribute types (abs(int) -> name; the sign selects the X.509 string type: positive utf8String,
|
|
61
|
+
// negative printableString). An attribute whose value is ALWAYS an IA5String is instead "unambiguously
|
|
62
|
+
// represented using a non-negative int" (draft sec. 3.1.4) -- emailAddress is int 0, so its value reconstructs
|
|
63
|
+
// as an IA5String and its sign carries no string-type meaning.
|
|
63
64
|
var ATTR_BY_INT = {
|
|
65
|
+
0: _name("emailAddress"),
|
|
64
66
|
1: _name("commonName"),
|
|
65
67
|
2: _name("surname"),
|
|
66
68
|
3: _name("serialNumber"),
|
|
@@ -87,6 +89,7 @@ var EXT_BY_INT = {
|
|
|
87
89
|
7: _name("authorityKeyIdentifier"),
|
|
88
90
|
8: _name("extKeyUsage"),
|
|
89
91
|
9: _name("authorityInfoAccess"),
|
|
92
|
+
24: _name("subjectDirectoryAttributes"),
|
|
90
93
|
25: _name("issuerAltName"),
|
|
91
94
|
26: _name("nameConstraints"),
|
|
92
95
|
27: _name("policyMappings"),
|
|
@@ -104,7 +107,7 @@ var EXT_COMPACT = {
|
|
|
104
107
|
extKeyUsage: 1, inhibitAnyPolicy: 1, ocspNoCheck: 1, tlsFeature: 1,
|
|
105
108
|
subjectAltName: 1, issuerAltName: 1, nameConstraints: 1, cRLDistributionPoints: 1,
|
|
106
109
|
freshestCRL: 1, authorityInfoAccess: 1, subjectInfoAccess: 1, certificatePolicies: 1,
|
|
107
|
-
policyMappings: 1, policyConstraints: 1,
|
|
110
|
+
policyMappings: 1, policyConstraints: 1, subjectDirectoryAttributes: 1,
|
|
108
111
|
};
|
|
109
112
|
// sec. 8.12 Extended Key Usages registry (C509 int -> registered id-kp purpose name). A KeyPurposeId
|
|
110
113
|
// outside this set encodes as an unwrapped ~oid; a C509 int outside it fails closed on decode.
|
|
@@ -240,14 +243,35 @@ function _specialText(node) {
|
|
|
240
243
|
|
|
241
244
|
// A single Name (sec. 3.1.4/sec. 3.1.6): the CBOR simple null (issuer only) | a bare SpecialText single
|
|
242
245
|
// commonName | an array of RDNAttributes. Surfaces { dn, rdns, eui64? } shape-compatible with x509.
|
|
243
|
-
|
|
246
|
+
// An rdn's (attribute type, sign) pair DECLARES an X.509 string type, so its text must be valid for that type --
|
|
247
|
+
// a printableString sign over a non-PrintableString character, a non-ASCII emailAddress (IA5String-only), or a
|
|
248
|
+
// value outside the type's SIZE. Assert it by running the SAME builder the reconstruction uses, so a Name this
|
|
249
|
+
// parse accepts is always one it can rebuild, and a natively-signed certificate (which never reconstructs) is
|
|
250
|
+
// held to the identical rule. Applied to BOTH Name forms -- the array form and the bare single-commonName form.
|
|
251
|
+
// The builder's asn1/* fault is re-thrown in this module's domain rather than leaking onto the parse surface.
|
|
252
|
+
function _assertAttrValue(rdn) {
|
|
253
|
+
try { _reconAttrValue(rdn); }
|
|
254
|
+
catch (e) {
|
|
255
|
+
if (e instanceof C509Error) throw e;
|
|
256
|
+
throw _err("c509/bad-name", "the " + rdn.type + " value is not valid for the string type its attribute integer declares", e);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function _name509(node, isSubject, isNative) {
|
|
244
261
|
if (!isSubject && node.majorType === 7 && node.ai === 22) return null; // issuer == subject (self-signed)
|
|
245
262
|
// A bare SpecialText (not an array) is a single commonName attribute (attributeType == +1).
|
|
246
263
|
if (node.majorType === 3 || node.majorType === 2 || node.majorType === 6) {
|
|
247
264
|
var sv = _specialText(node);
|
|
265
|
+
// The bare form is a single commonName -- hold its value to the SAME rules the array form applies, so a
|
|
266
|
+
// natively-signed certificate (which never reconstructs) cannot carry a value the reconstruction would refuse.
|
|
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.
|
|
248
270
|
if (sv.eui64) return { rdns: [{ type: "commonName", eui64: sv.eui64 }], eui64: sv.eui64, dn: "CN=" + _macToEui64String(sv.eui64) };
|
|
249
271
|
var val = sv.text !== undefined ? sv.text : sv.hex;
|
|
250
|
-
|
|
272
|
+
var bareRdn = { type: "commonName", value: val };
|
|
273
|
+
_assertAttrValue(bareRdn);
|
|
274
|
+
return { rdns: [bareRdn], dn: "CN=" + guard.name.escapeDnValue(val) };
|
|
251
275
|
}
|
|
252
276
|
if (node.majorType !== 4) throw _err("c509/bad-name", "a C509 Name must be null, a SpecialText, or an array of RDN attributes");
|
|
253
277
|
var rdns = [];
|
|
@@ -256,6 +280,10 @@ function _name509(node, isSubject) {
|
|
|
256
280
|
// Each RDN attribute is an (attributeType, attributeValue) pair; an odd-length array is a dangling
|
|
257
281
|
// attribute type with no value -- reject rather than silently drop the trailing element.
|
|
258
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)");
|
|
259
287
|
for (var i = 0; i + 1 < kids.length; i += 2) {
|
|
260
288
|
// The attributeType slot must be a CBOR integer (major type 0/1). Guard the major type BEFORE the read
|
|
261
289
|
// so a non-integer type slot fails in this module's own domain (c509/bad-name), matching the value slot,
|
|
@@ -264,15 +292,30 @@ function _name509(node, isSubject) {
|
|
|
264
292
|
// Identifier issuer, cRLDistributionPoints cRLIssuer, nameConstraints subtree base) and the top-level Name.
|
|
265
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");
|
|
266
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);
|
|
267
300
|
var tname = ATTR_BY_INT[Math.abs(ti)];
|
|
268
301
|
if (tname === undefined) throw _err("c509/bad-name", "attribute type integer " + ti + " has no C509 registry row");
|
|
269
302
|
var v = _specialText(kids[i + 1]);
|
|
270
303
|
var vv = v.text !== undefined ? v.text : (v.hex !== undefined ? v.hex : _macToEui64String(v.eui64));
|
|
271
|
-
|
|
272
|
-
|
|
304
|
+
// The RENDERED text is what carries into the rdn -- deliberately NOT the raw eui64, because the EUI shortcut
|
|
305
|
+
// in _reconAttrValue is unconditional and would take precedence over the attribute's declared string type,
|
|
306
|
+
// rebuilding a tag-48 emailAddress or countryName as a UTF8String. Only the bare-SpecialText form above is
|
|
307
|
+
// always a commonName and may carry it; here the value is held to whatever type the attribute integer declares.
|
|
308
|
+
var rdn = { type: tname, value: vv, printable: ti < 0 };
|
|
309
|
+
_assertAttrValue(rdn);
|
|
310
|
+
rdns.push(rdn);
|
|
311
|
+
parts.push(_shortName(tname) + "=" + guard.name.escapeDnValue(vv));
|
|
273
312
|
}
|
|
274
313
|
return { rdns: rdns, dn: parts.join(",") };
|
|
275
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.
|
|
276
319
|
function _shortName(n) { return n === "commonName" ? "CN" : n === "countryName" ? "C" : n === "organizationName" ? "O" : n === "organizationalUnitName" ? "OU" : n === "localityName" ? "L" : n === "stateOrProvinceName" ? "ST" : n; }
|
|
277
320
|
|
|
278
321
|
// ---- compact per-extension value codec (draft-20 sec. 3.3) -------------------
|
|
@@ -361,9 +404,9 @@ function _namedBitsFromContent(bytes, unusedBits) {
|
|
|
361
404
|
|
|
362
405
|
// One C509 (int, value) general name -> one DER GeneralName TLV. `ipMode` (name-constraints subtree base)
|
|
363
406
|
// only changes the iPAddress arm: SAN is a bare 4/16-octet address, a subtree base is the RFC 9549 form.
|
|
364
|
-
function _generalNameToDer(intVal, valueNode, ipMode) {
|
|
407
|
+
function _generalNameToDer(intVal, valueNode, ipMode, isNative) {
|
|
365
408
|
if (intVal === 1 || intVal === 2 || intVal === 6) return b.contextPrimitive(intVal, _ia5Bytes(valueNode, intVal)); // IMPLICIT IA5String
|
|
366
|
-
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
|
|
367
410
|
if (intVal === 7) return b.contextPrimitive(7, ipMode ? _ncIpToDer(valueNode) : _sanIpBytes(valueNode)); // iPAddress [7] IMPLICIT OCTET STRING
|
|
368
411
|
if (intVal === 8) return b.contextPrimitive(8, asn1.encodeOidContent(_oidName(valueNode, "c509/bad-extensions", "a registeredID [8]").oid)); // registeredID [8] IMPLICIT OID
|
|
369
412
|
if (intVal === 0 || intVal === -1 || intVal === -2 || intVal === -3) return _otherNameToDer(valueNode, intVal); // otherName [0]
|
|
@@ -389,12 +432,12 @@ function _generalNameFromDer(gn, ipMode) {
|
|
|
389
432
|
// them: a universal SEQUENCE for SAN/IAN, an implicit [n] for AKI issuer / DP cRLIssuer). These are always
|
|
390
433
|
// SAN-form (the RFC 9549 subtree-base iPAddress form is name-constraints-only and routes through
|
|
391
434
|
// _subtreesToDer / _subtreesFromDer, which call the singular codec with ipMode directly).
|
|
392
|
-
function _generalNamesToDer(node) {
|
|
435
|
+
function _generalNamesToDer(node, isNative) {
|
|
393
436
|
if (node.majorType !== 4 || !node.children) throw _err("c509/bad-extensions", "a GeneralNames value must be a CBOR array");
|
|
394
437
|
var kids = node.children;
|
|
395
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)");
|
|
396
439
|
var out = [];
|
|
397
|
-
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));
|
|
398
441
|
return out;
|
|
399
442
|
}
|
|
400
443
|
// The inverse: a list of DER GeneralName nodes -> the flat CBOR items, or null if ANY member is not
|
|
@@ -420,8 +463,8 @@ function _otherNameToDer(node, intVal) {
|
|
|
420
463
|
if (node.majorType !== 4 || !node.children || node.children.length !== 2) throw _err("c509/bad-extensions", "a generic otherName value must be a CBOR [ ~oid, bytes ] pair");
|
|
421
464
|
typeId = _oidName(node.children[0], "c509/bad-extensions", "an otherName type-id").oid;
|
|
422
465
|
if (node.children[1].majorType !== 2) throw _err("c509/bad-extensions", "an otherName value must be a CBOR byte string (the [0] EXPLICIT inner TLV)");
|
|
423
|
-
|
|
424
|
-
|
|
466
|
+
// The [0] EXPLICIT inner TLV is an ANY spliced verbatim -- strict-validate it, not just its framing.
|
|
467
|
+
inner = _requireStrictDerTlv(node.children[1].content, "c509/bad-extensions", "an otherName value");
|
|
425
468
|
} else if (intVal === -1) { // id-on-hardwareModuleName: [ ~oid(hwType), bytes(hwSerialNum) ] -> HardwareModuleName
|
|
426
469
|
if (node.majorType !== 4 || !node.children || node.children.length !== 2) throw _err("c509/bad-extensions", "an id-on-hardwareModuleName value must be a CBOR [ ~oid, bytes ] pair");
|
|
427
470
|
if (node.children[1].majorType !== 2) throw _err("c509/bad-extensions", "a hardwareModuleName hwSerialNum must be a CBOR byte string");
|
|
@@ -515,12 +558,12 @@ function _ncIpFromDer(buf) {
|
|
|
515
558
|
// GeneralSubtrees = [ + GeneralName ] (the flat int/value array) <-> the concatenated GeneralSubtree
|
|
516
559
|
// SEQUENCEs (RFC 5280 sec. 4.2.1.10: SEQUENCE { base, minimum [0] DEFAULT 0, maximum [1] OPTIONAL }); the
|
|
517
560
|
// C509 profile omits minimum/maximum, so each GeneralSubtree is base-only.
|
|
518
|
-
function _subtreesToDer(node) {
|
|
561
|
+
function _subtreesToDer(node, isNative) {
|
|
519
562
|
if (node.majorType !== 4 || !node.children) throw _err("c509/bad-extensions", "a GeneralSubtrees value must be a CBOR array");
|
|
520
563
|
var kids = node.children;
|
|
521
564
|
if (kids.length === 0 || kids.length % 2 !== 0) throw _err("c509/bad-extensions", "a GeneralSubtrees array must be non-empty (int, value) pairs");
|
|
522
565
|
var out = [];
|
|
523
|
-
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)]));
|
|
524
567
|
return Buffer.concat(out);
|
|
525
568
|
}
|
|
526
569
|
function _subtreesFromDer(subtreeNodes) {
|
|
@@ -628,7 +671,7 @@ function _qualifierFromDer(pq) {
|
|
|
628
671
|
// One CBOR DistributionPointName [ fullName, reasons, cRLIssuer ] -> one DER DistributionPoint SEQUENCE.
|
|
629
672
|
// distributionPoint [0] is EXPLICIT (it wraps the DistributionPointName CHOICE); fullName [0], reasons [1],
|
|
630
673
|
// cRLIssuer [2] are IMPLICIT (RFC 5280 sec. 4.2.1.13) -- mixing these is the classic byte-exactness trap.
|
|
631
|
-
function _dpToDer(dpNode) {
|
|
674
|
+
function _dpToDer(dpNode, isNative) {
|
|
632
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");
|
|
633
676
|
var fullName = dpNode.children[0], reasons = dpNode.children[1], crlIssuer = dpNode.children[2];
|
|
634
677
|
var uris;
|
|
@@ -639,7 +682,7 @@ function _dpToDer(dpNode) {
|
|
|
639
682
|
} else throw _err("c509/bad-extensions", "a DistributionPoint fullName must be a URI text or an array of URIs");
|
|
640
683
|
var fields = [b.explicit(0, b.contextConstructed(0, Buffer.concat(uris)))]; // distributionPoint [0] EXPLICIT { fullName [0] IMPLICIT GeneralNames }
|
|
641
684
|
if (!_isCborNull(reasons)) fields.push(_reasonsBitsToDer(Number(_cborUint(reasons, "cRLDistributionPoints reasons"))));
|
|
642
|
-
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 }
|
|
643
686
|
return b.sequence(fields);
|
|
644
687
|
}
|
|
645
688
|
// One DER DistributionPoint SEQUENCE -> the pieces for the CBOR DistributionPointName, or null when the DP
|
|
@@ -686,7 +729,7 @@ function _dpFromDer(dp) {
|
|
|
686
729
|
|
|
687
730
|
// Decode a compact extension value (a decoded CBOR node) to the DER extnValue inner content. Fails closed
|
|
688
731
|
// (c509/bad-extensions) on a CBOR shape the named extension does not define.
|
|
689
|
-
function _extValueToDer(name, node) {
|
|
732
|
+
function _extValueToDer(name, node, isNative) {
|
|
690
733
|
switch (name) {
|
|
691
734
|
case "subjectKeyIdentifier": // KeyIdentifier = bytes -> OCTET STRING(keyid)
|
|
692
735
|
if (node.majorType !== 2) throw _err("c509/bad-extensions", "a subjectKeyIdentifier value must be a CBOR byte string");
|
|
@@ -706,7 +749,7 @@ function _extValueToDer(name, node) {
|
|
|
706
749
|
if (node.children[0].majorType !== 2) throw _err("c509/bad-extensions", "an authorityKeyIdentifier keyIdentifier must be a CBOR byte string");
|
|
707
750
|
return b.sequence([
|
|
708
751
|
b.contextPrimitive(0, node.children[0].content), // keyIdentifier [0] IMPLICIT OCTET STRING
|
|
709
|
-
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
|
|
710
753
|
b.contextPrimitive(2, _serialIntContent(node.children[2])), // authorityCertSerialNumber [2] IMPLICIT INTEGER
|
|
711
754
|
]);
|
|
712
755
|
}
|
|
@@ -733,12 +776,12 @@ function _extValueToDer(name, node) {
|
|
|
733
776
|
case "subjectAltName":
|
|
734
777
|
case "issuerAltName": // SubjectAltName = GeneralNames / text (exactly one dNSName -> bare text)
|
|
735
778
|
if (node.majorType === 3) return b.sequence([b.contextPrimitive(2, _ia5Bytes(node, 2))]);
|
|
736
|
-
return b.sequence(_generalNamesToDer(node));
|
|
779
|
+
return b.sequence(_generalNamesToDer(node, isNative));
|
|
737
780
|
case "nameConstraints": { // [ permittedSubtrees / null, excludedSubtrees / null ]
|
|
738
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)");
|
|
739
782
|
var ncFields = [];
|
|
740
|
-
if (!_isCborNull(node.children[0])) ncFields.push(b.contextConstructed(0, _subtreesToDer(node.children[0]))); // permittedSubtrees [0]
|
|
741
|
-
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]
|
|
742
785
|
if (ncFields.length === 0) throw _err("c509/bad-extensions", "nameConstraints must contain permittedSubtrees or excludedSubtrees (RFC 5280 sec. 4.2.1.10)");
|
|
743
786
|
return b.sequence(ncFields);
|
|
744
787
|
}
|
|
@@ -755,7 +798,7 @@ function _extValueToDer(name, node) {
|
|
|
755
798
|
case "freshestCRL": // [ + DistributionPointName ] / text (one DP, one-URI fullName -> bare text)
|
|
756
799
|
if (node.majorType === 3) return b.sequence([b.sequence([b.explicit(0, b.contextConstructed(0, b.contextPrimitive(6, _ia5Bytes(node, 6))))])]);
|
|
757
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)");
|
|
758
|
-
return b.sequence(node.children.map(function (dp) { return _dpToDer(dp); }));
|
|
801
|
+
return b.sequence(node.children.map(function (dp) { return _dpToDer(dp, isNative); }));
|
|
759
802
|
case "certificatePolicies": { // [ pid, [ *(qid, qtext) ], ... ] -> SEQUENCE OF PolicyInformation
|
|
760
803
|
if (node.majorType !== 4 || !node.children) throw _err("c509/bad-extensions", "a certificatePolicies value must be a CBOR array");
|
|
761
804
|
var cpKids = node.children;
|
|
@@ -802,6 +845,8 @@ function _extValueToDer(name, node) {
|
|
|
802
845
|
if (pcFields.length === 0) throw _err("c509/bad-extensions", "policyConstraints must contain requireExplicitPolicy or inhibitPolicyMapping (RFC 5280 sec. 4.2.1.11)");
|
|
803
846
|
return b.sequence(pcFields); // positional slot -> tag makes [0] < [1] unique + ascending by construction
|
|
804
847
|
}
|
|
848
|
+
case "subjectDirectoryAttributes": // [ type, values, type, values, ... ] -> SEQUENCE OF Attribute (sec. 3.3)
|
|
849
|
+
return _sdaToDer(node, isNative);
|
|
805
850
|
default:
|
|
806
851
|
throw _err("c509/bad-extensions", "extension " + name + " has no compact value decoder");
|
|
807
852
|
}
|
|
@@ -964,6 +1009,8 @@ function _extValueFromDer(name, der) {
|
|
|
964
1009
|
}
|
|
965
1010
|
return cbor.build.array([repC, ipmC]);
|
|
966
1011
|
}
|
|
1012
|
+
case "subjectDirectoryAttributes": // SEQUENCE OF Attribute -> [ type, values, ... ]
|
|
1013
|
+
return _sdaFromDer(node);
|
|
967
1014
|
default:
|
|
968
1015
|
return null;
|
|
969
1016
|
}
|
|
@@ -972,6 +1019,225 @@ function _extValueFromDer(name, der) {
|
|
|
972
1019
|
}
|
|
973
1020
|
}
|
|
974
1021
|
|
|
1022
|
+
// ---- subjectDirectoryAttributes compact value codec (draft-20 sec. 3.3 / 8.6 / 8.8, RFC 5280 sec. 4.2.1.8) ----
|
|
1023
|
+
// SubjectDirectoryAttributes ::= SEQUENCE SIZE(1..MAX) OF Attribute ::= SEQUENCE { type, values SET OF value }.
|
|
1024
|
+
// The compact form is a flat CBOR array [ type1, values1, ... ] where each values slot is ITSELF a non-empty
|
|
1025
|
+
// CBOR array -- an Attribute's SET may hold more than one value, unlike a Name's single-value RDN. type is a
|
|
1026
|
+
// sec. 8.6 registry int (the sign selects the string type) with text values, or an unwrapped ~oid with raw DER
|
|
1027
|
+
// value bytes (an unregistered type stays compact via its own ~oid form, never dropping the whole extension).
|
|
1028
|
+
// The extension-level _tryCompactExtValue round-trip guard is the byte-exactness net.
|
|
1029
|
+
|
|
1030
|
+
// A ~oid-form attributeValue must be exactly ONE non-empty, well-formed DER AttributeValue TLV. An empty byte
|
|
1031
|
+
// string would build a SET OF SIZE 0 (RFC 5280 sec. 4.2.1.8 requires at least one value); a byte string carrying
|
|
1032
|
+
// more than one TLV would fan one declared value into several SET members (draft-20 sec. 3.3: one `bytes` is one
|
|
1033
|
+
// AttributeValue). There is no downstream DER decoder to catch either (SDA ships c509-only), so the reconstruct
|
|
1034
|
+
// arm self-enforces the single-TLV shape rather than splice a degenerate or invalid Attribute.
|
|
1035
|
+
// ---- the shared strict-DER gate for a raw-spliced ANY value --------------------------------------
|
|
1036
|
+
// Several reconstruct sites splice CALLER/ATTACKER-supplied bytes into the rebuilt certificate verbatim (an
|
|
1037
|
+
// AlgorithmIdentifier's `parameters`, a generic otherName's `value [0] EXPLICIT ANY`, a subjectDirectoryAttributes
|
|
1038
|
+
// `~oid`-form AttributeValue). asn1.decode validates TLV FRAMING ONLY -- it rejects truncation, trailing bytes and
|
|
1039
|
+
// non-minimal lengths, and enforces the DER primitive/constructed rules, but it does NOT check per-type CONTENT:
|
|
1040
|
+
// it frames an empty INTEGER, the reserved end-of-contents tag 0, and an out-of-alphabet string quite happily.
|
|
1041
|
+
// Splicing those produces a certificate an independent decoder refuses to load, and one this toolkit's OWN readers
|
|
1042
|
+
// reject -- so every such site routes through the one gate below instead of a bare asn1.decode.
|
|
1043
|
+
|
|
1044
|
+
// Every universal PRIMITIVE tag whose content this toolkit can strictly validate, mapped to its reader. A
|
|
1045
|
+
// universal primitive OUTSIDE this map has no strict content validator here, so it is REJECTED rather than
|
|
1046
|
+
// spliced unchecked. The documented residual: a value whose type this toolkit cannot yet strictly validate is
|
|
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
|
|
1049
|
+
// and RFC 3161 timestamping, and must not creep into a third consumer). On the ENCODE path such a value simply
|
|
1050
|
+
// degrades to the byte-exact ~oid + byte-string form, losing nothing; on the DECODE path it is a fail-closed
|
|
1051
|
+
// verdict rather than a guess.
|
|
1052
|
+
var _ANY_VALUE_READERS = (function () {
|
|
1053
|
+
var m = {}, R = asn1.read, T = asn1.TAGS;
|
|
1054
|
+
m[T.BOOLEAN] = R.boolean; m[T.INTEGER] = R.integer; m[T.ENUMERATED] = R.enumerated;
|
|
1055
|
+
m[T.BIT_STRING] = R.bitString; m[T.OCTET_STRING] = R.octetString; m[T.NULL] = R.nullValue;
|
|
1056
|
+
m[T.OBJECT_IDENTIFIER] = R.oid; m[T.UTC_TIME] = R.time; m[T.GENERALIZED_TIME] = R.time;
|
|
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; });
|
|
1061
|
+
return m;
|
|
1062
|
+
})();
|
|
1063
|
+
|
|
1064
|
+
// A universal SET's required member order depends on a type the ANY does not carry: X.690 sec. 11.6 orders a
|
|
1065
|
+
// SET OF by the members' full encodings, while a structured SET is ordered by TAG (X.680 sec. 8.6) -- and the two
|
|
1066
|
+
// differ whenever the constructed bit does (a SEQUENCE member, tag 16, sorts BEFORE a PrintableString, tag 19, by
|
|
1067
|
+
// tag but AFTER it by octets). A structured SET cannot repeat a tag, so a repeated tag proves SET OF and the
|
|
1068
|
+
// octet rule binds; with all-distinct tags either reading is possible, so accept a value that satisfies EITHER
|
|
1069
|
+
// (rejecting only what is non-canonical under BOTH readings -- sound in both directions, never a guess).
|
|
1070
|
+
var _TAG_CLASS_RANK = { universal: 0, application: 1, context: 2, private: 3 };
|
|
1071
|
+
function _setOrderOk(kids) {
|
|
1072
|
+
var i, dup = false, seen = {};
|
|
1073
|
+
for (i = 0; i < kids.length; i++) {
|
|
1074
|
+
var key = kids[i].tagClass + ":" + kids[i].tagNumber;
|
|
1075
|
+
if (seen[key]) { dup = true; break; }
|
|
1076
|
+
seen[key] = true;
|
|
1077
|
+
}
|
|
1078
|
+
var octetAsc = true, tagAsc = true;
|
|
1079
|
+
for (i = 1; i < kids.length; i++) {
|
|
1080
|
+
if (Buffer.compare(kids[i - 1].bytes, kids[i].bytes) > 0) octetAsc = false;
|
|
1081
|
+
// Tag order ranks by CLASS first (universal < application < context < private, X.680 sec. 8.6), then by tag
|
|
1082
|
+
// number -- compare the class's NUMBER, never its name (the names do not sort in class order).
|
|
1083
|
+
var pc = _TAG_CLASS_RANK[kids[i - 1].tagClass], cc = _TAG_CLASS_RANK[kids[i].tagClass];
|
|
1084
|
+
if (pc !== cc ? pc > cc : kids[i - 1].tagNumber > kids[i].tagNumber) tagAsc = false;
|
|
1085
|
+
}
|
|
1086
|
+
return dup ? octetAsc : (octetAsc || tagAsc);
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
// Strict-validate a decoded DER element at ANY depth, in the caller's error domain. Rejects the reserved EOC tag
|
|
1090
|
+
// 0; runs a universal primitive through its strict content reader (or rejects a type with none); recurses into a
|
|
1091
|
+
// constructed element's children; and holds a universal SET to a canonical order. Only SEQUENCE and SET are
|
|
1092
|
+
// accepted as universal CONSTRUCTED types -- an EXTERNAL / EMBEDDED PDV / CHARACTER STRING has mandatory
|
|
1093
|
+
// components this gate cannot verify (the degenerate empty form is not a valid encoding of any of them), so it is
|
|
1094
|
+
// refused for the same reason an unvalidatable primitive is. A NON-universal element (a legitimately context- or
|
|
1095
|
+
// application-tagged ANY) passes on its framing, but its constructed children are still walked.
|
|
1096
|
+
function _strictDerElement(node, code, label) {
|
|
1097
|
+
if (node.tagClass === "universal" && node.tagNumber === 0) throw _err(code, label + " must not use the reserved end-of-contents encoding (tag 0)");
|
|
1098
|
+
if (node.constructed) {
|
|
1099
|
+
if (node.tagClass === "universal" && node.tagNumber !== asn1.TAGS.SEQUENCE && node.tagNumber !== asn1.TAGS.SET) {
|
|
1100
|
+
throw _err(code, label + " of universal constructed type " + node.tagNumber + " has no strict DER structure validator here");
|
|
1101
|
+
}
|
|
1102
|
+
var kids = node.children; // asn1.decode always sets a (possibly empty) children array on a constructed node
|
|
1103
|
+
for (var i = 0; i < kids.length; i++) _strictDerElement(kids[i], code, label);
|
|
1104
|
+
if (node.tagClass === "universal" && node.tagNumber === asn1.TAGS.SET && !_setOrderOk(kids)) {
|
|
1105
|
+
throw _err(code, label + " has a SET whose members are in no canonical DER order (X.690 sec. 11.6 / X.680 sec. 8.6)");
|
|
1106
|
+
}
|
|
1107
|
+
return;
|
|
1108
|
+
}
|
|
1109
|
+
if (node.tagClass === "universal") {
|
|
1110
|
+
// Validate-or-reject: a universal primitive with no strict content reader is NOT accepted on framing alone
|
|
1111
|
+
// (asn1.decode frames a malformed NumericString "12 01 40" happily), so the map is exhaustive by refusal.
|
|
1112
|
+
var reader = _ANY_VALUE_READERS[node.tagNumber];
|
|
1113
|
+
if (!reader) throw _err(code, label + " of universal type " + node.tagNumber + " has no strict DER content validator here");
|
|
1114
|
+
try { reader(node); } catch (e) { throw _err(code, label + " is not a valid DER element for its type", e); }
|
|
1115
|
+
}
|
|
1116
|
+
}
|
|
1117
|
+
// Raw ANY bytes about to be spliced verbatim must be exactly ONE non-empty, well-formed AND strictly-valid DER
|
|
1118
|
+
// element: framing + no-trailing-data via asn1.decode, then content / structure / SET order via
|
|
1119
|
+
// _strictDerElement, both reported in the CALLER's error domain. Returns the bytes so a call site can wrap a
|
|
1120
|
+
// splice inline. Used by every reconstruct site that emits caller-supplied ANY bytes verbatim.
|
|
1121
|
+
function _requireStrictDerTlv(content, code, label) {
|
|
1122
|
+
if (content.length === 0) throw _err(code, label + " must be a non-empty DER element");
|
|
1123
|
+
var node;
|
|
1124
|
+
try { node = asn1.decode(content); } catch (e) { throw _err(code, label + " must be exactly one well-formed DER element (no trailing data)", e); }
|
|
1125
|
+
_strictDerElement(node, code, label);
|
|
1126
|
+
return content;
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
// asn1.build.set SORTS its members, so handing it a non-canonically-ordered values list would SILENTLY rewrite
|
|
1130
|
+
// the declared order -- many distinct C509 encodings would reconstruct one and the same certificate. Require the
|
|
1131
|
+
// declared order to already BE canonical, so the compact form maps one-to-one onto the DER it rebuilds.
|
|
1132
|
+
function _derSetInDeclaredOrder(vals) {
|
|
1133
|
+
for (var i = 1; i < vals.length; i++) {
|
|
1134
|
+
if (Buffer.compare(vals[i - 1], vals[i]) > 0) throw _err("c509/bad-extensions", "a subjectDirectoryAttributes attributeValue list must be in DER ascending order (X.690 sec. 11.6)");
|
|
1135
|
+
}
|
|
1136
|
+
return b.set(vals);
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
// [ type1, values1, ... ] -> the DER SubjectDirectoryAttributes. A malformed native value fails closed
|
|
1140
|
+
// (c509/bad-extensions); on the encode path the same throw is a round-trip mismatch -> whole-ext ~oid fallback.
|
|
1141
|
+
function _sdaToDer(node, isNative) {
|
|
1142
|
+
if (node.majorType !== 4 || !node.children) throw _err("c509/bad-extensions", "a subjectDirectoryAttributes value must be a CBOR array");
|
|
1143
|
+
var kids = node.children;
|
|
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)");
|
|
1145
|
+
var attrs = [];
|
|
1146
|
+
for (var i = 0; i + 1 < kids.length; i += 2) {
|
|
1147
|
+
var typeNode = kids[i], valuesNode = kids[i + 1];
|
|
1148
|
+
if (valuesNode.majorType !== 4 || !valuesNode.children || valuesNode.children.length < 1) throw _err("c509/bad-extensions", "a subjectDirectoryAttributes attributeValue must be a non-empty CBOR array (SET OF, SIZE 1..MAX, RFC 5280 sec. 4.2.1.8)");
|
|
1149
|
+
var vals = [];
|
|
1150
|
+
if (typeNode.majorType === 0 || typeNode.majorType === 1) { // int form: a sec. 8.6 registry alias, text values
|
|
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);
|
|
1154
|
+
var tname = ATTR_BY_INT[Math.abs(ti)];
|
|
1155
|
+
if (tname === undefined) throw _err("c509/bad-extensions", "a subjectDirectoryAttributes attribute type int " + ti + " has no C509 sec. 8.6 registry row");
|
|
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.
|
|
1161
|
+
for (var vi = 0; vi < valuesNode.children.length; vi++) {
|
|
1162
|
+
var vn = valuesNode.children[vi];
|
|
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)");
|
|
1164
|
+
// _reconAttrValue -> b.printable / b.utf8 throws only an Asn1Error for a value outside its string type's
|
|
1165
|
+
// alphabet (a printableString sign over non-PrintableString characters); remap it to this module's
|
|
1166
|
+
// domain so attacker-controlled native input fails closed as c509/bad-extensions, not a leaked asn1/*.
|
|
1167
|
+
try { vals.push(_reconAttrValue({ type: tname, value: cbor.read.textString(vn), printable: ti < 0 })); }
|
|
1168
|
+
catch (e) { throw _err("c509/bad-extensions", "a subjectDirectoryAttributes " + tname + " value is not valid for its string type", e); }
|
|
1169
|
+
}
|
|
1170
|
+
attrs.push(b.sequence([b.oid(oid.byName(tname)), _derSetInDeclaredOrder(vals)]));
|
|
1171
|
+
} else if (typeNode.majorType === 2) { // ~oid form: raw AttributeValue TLVs
|
|
1172
|
+
var dotted = _oidName(typeNode, "c509/bad-extensions", "a subjectDirectoryAttributes attribute type").oid;
|
|
1173
|
+
for (var vj = 0; vj < valuesNode.children.length; vj++) {
|
|
1174
|
+
var vb = valuesNode.children[vj];
|
|
1175
|
+
if (vb.majorType !== 2) throw _err("c509/bad-extensions", "a ~oid-form subjectDirectoryAttributes value must be a CBOR byte string (a raw DER AttributeValue)");
|
|
1176
|
+
vals.push(b.raw(_requireStrictDerTlv(vb.content, "c509/bad-extensions", "a ~oid-form subjectDirectoryAttributes AttributeValue")));
|
|
1177
|
+
}
|
|
1178
|
+
attrs.push(b.sequence([b.oid(dotted), _derSetInDeclaredOrder(vals)]));
|
|
1179
|
+
} else {
|
|
1180
|
+
throw _err("c509/bad-extensions", "a subjectDirectoryAttributes attribute type must be a CBOR integer (sec. 8.6) or a ~oid");
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
1183
|
+
return b.sequence(attrs);
|
|
1184
|
+
}
|
|
1185
|
+
|
|
1186
|
+
// One DER Attribute's SET members -> the int-form { sign, values:[CBOR text] }, or null when the int form is
|
|
1187
|
+
// not confidently byte-exact (an unregistered type, a non-utf8/printable value, or a SET mixing utf8String and
|
|
1188
|
+
// printableString -- one sign cannot express it). A null routes that ONE attribute to the always-byte-exact
|
|
1189
|
+
// ~oid + raw-bytes form (a still-compact sec. 3.3 SDA form); the extension round-trip guard is the ultimate net.
|
|
1190
|
+
function _sdaTryIntForm(tint, name, valueNodes) {
|
|
1191
|
+
if (tint === undefined) return null;
|
|
1192
|
+
var i;
|
|
1193
|
+
// An IA5String-only attribute (emailAddress) rides its non-negative int with IA5String values -- its type,
|
|
1194
|
+
// not a sign, fixes the string type, so it is matched separately from the utf8/printable sign convention.
|
|
1195
|
+
if (name === "emailAddress") {
|
|
1196
|
+
var mails = [];
|
|
1197
|
+
for (i = 0; i < valueNodes.length; i++) {
|
|
1198
|
+
if (valueNodes[i].tagClass !== "universal" || valueNodes[i].tagNumber !== asn1.TAGS.IA5_STRING) return null;
|
|
1199
|
+
mails.push(cbor.build.textString(asn1.read.string(valueNodes[i])));
|
|
1200
|
+
}
|
|
1201
|
+
return { sign: 1, values: mails };
|
|
1202
|
+
}
|
|
1203
|
+
var sign = 0, out = []; // sign: -1 printableString, +1 utf8String
|
|
1204
|
+
for (i = 0; i < valueNodes.length; i++) {
|
|
1205
|
+
var vn = valueNodes[i], s;
|
|
1206
|
+
if (vn.tagClass === "universal" && vn.tagNumber === asn1.TAGS.UTF8_STRING) s = 1;
|
|
1207
|
+
else if (vn.tagClass === "universal" && vn.tagNumber === asn1.TAGS.PRINTABLE_STRING) s = -1;
|
|
1208
|
+
else return null; // any other value tag -> the ~oid form preserves it exactly
|
|
1209
|
+
if (sign === 0) sign = s;
|
|
1210
|
+
else if (sign !== s) return null; // a mixed utf8/printable SET -- the headline trap
|
|
1211
|
+
out.push(cbor.build.textString(asn1.read.string(vn)));
|
|
1212
|
+
}
|
|
1213
|
+
return { sign: sign, values: out };
|
|
1214
|
+
}
|
|
1215
|
+
|
|
1216
|
+
// The DER SubjectDirectoryAttributes -> the flat compact CBOR array, or null (whole-ext ~oid fallback). Per
|
|
1217
|
+
// attribute: the int form when the type is sec. 8.6-registered AND every value is a single-signed utf8/
|
|
1218
|
+
// printable string, else the ~oid + raw-bytes form (always byte-exact).
|
|
1219
|
+
function _sdaFromDer(node) {
|
|
1220
|
+
if (node.tagClass !== "universal" || node.tagNumber !== asn1.TAGS.SEQUENCE || !node.children || node.children.length < 1) return null;
|
|
1221
|
+
var out = [];
|
|
1222
|
+
for (var ai = 0; ai < node.children.length; ai++) {
|
|
1223
|
+
var attr = node.children[ai];
|
|
1224
|
+
if (attr.tagClass !== "universal" || attr.tagNumber !== asn1.TAGS.SEQUENCE || !attr.children || attr.children.length !== 2) return null;
|
|
1225
|
+
var setNode = attr.children[1];
|
|
1226
|
+
if (setNode.tagClass !== "universal" || setNode.tagNumber !== asn1.TAGS.SET || !setNode.children || setNode.children.length < 1) return null; // SET OF, non-empty
|
|
1227
|
+
var dotted = asn1.read.oid(attr.children[0]);
|
|
1228
|
+
var nm = oid.name(dotted), tint = nm != null ? ATTR_TO_INT[nm] : undefined;
|
|
1229
|
+
var intForm = _sdaTryIntForm(tint, nm, setNode.children);
|
|
1230
|
+
if (intForm != null) {
|
|
1231
|
+
out.push(cbor.build.int(BigInt(intForm.sign < 0 ? -tint : tint)));
|
|
1232
|
+
out.push(cbor.build.array(intForm.values));
|
|
1233
|
+
} else {
|
|
1234
|
+
out.push(_oidCbor(dotted));
|
|
1235
|
+
out.push(cbor.build.array(setNode.children.map(function (v) { return cbor.build.byteString(v.bytes); })));
|
|
1236
|
+
}
|
|
1237
|
+
}
|
|
1238
|
+
return cbor.build.array(out);
|
|
1239
|
+
}
|
|
1240
|
+
|
|
975
1241
|
// Encode-side guard: emit the compact value only when it decodes back to the EXACT DER extnValue, so a
|
|
976
1242
|
// non-canonical or unrepresentable value can never produce a lossy compact form (it falls back to ~oid).
|
|
977
1243
|
// A value _extValueFromDer produced but _extValueToDer cannot reconstruct -- an empty GeneralNames the DER
|
|
@@ -992,7 +1258,7 @@ function _tryCompactExtValue(name, der) {
|
|
|
992
1258
|
}
|
|
993
1259
|
|
|
994
1260
|
// extensions (sec. 3.1.10/sec. 3.3/sec. 8.8): [ * Extension ] | a single keyUsage int-shortcut.
|
|
995
|
-
function _extensions(node) {
|
|
1261
|
+
function _extensions(node, isNative) {
|
|
996
1262
|
// The keyUsage int-shortcut (sec. 3.1.10): a bare int -> one keyUsage extension, criticality from the
|
|
997
1263
|
// sign, value = abs(int) (Appendix A.1.1: the single int 1 -> non-critical keyUsage digitalSignature).
|
|
998
1264
|
if (node.majorType === 0 || node.majorType === 1) {
|
|
@@ -1019,7 +1285,7 @@ function _extensions(node) {
|
|
|
1019
1285
|
// non-byte-string value there is an unsupported compact form and MUST fail closed -- a text/array value
|
|
1020
1286
|
// is NOT raw DER, and copying its bytes would reconstruct a structurally invalid extension.
|
|
1021
1287
|
if (EXT_COMPACT[name]) {
|
|
1022
|
-
valContent = _extValueToDer(name, valNode);
|
|
1288
|
+
valContent = _extValueToDer(name, valNode, isNative);
|
|
1023
1289
|
} else if (valNode.majorType === 2) {
|
|
1024
1290
|
valContent = valNode.content;
|
|
1025
1291
|
} else {
|
|
@@ -1063,9 +1329,27 @@ function _macToEui64String(buf) {
|
|
|
1063
1329
|
// One RDN attribute value -> its DER string. The C509 sign convention: a positive attribute int ->
|
|
1064
1330
|
// utf8String, a negative -> printableString; countryName / serialNumber are PrintableString-restricted.
|
|
1065
1331
|
function _reconAttrValue(rdn) {
|
|
1066
|
-
if (rdn.eui64) return b.utf8(_macToEui64String(rdn.eui64));
|
|
1067
|
-
|
|
1068
|
-
|
|
1332
|
+
if (rdn.eui64) return b.utf8(_macToEui64String(rdn.eui64)); // the bare-form commonName MAC (a fixed 17 chars)
|
|
1333
|
+
var s = String(rdn.value);
|
|
1334
|
+
// The attribute's registered ASN.1 type carries a SIZE constraint as well as an alphabet, and a value outside
|
|
1335
|
+
// it is not a valid encoding of that type: every DirectoryString-valued attribute is SIZE (1..MAX) and
|
|
1336
|
+
// emailAddress is IA5String SIZE (1..MAX) (RFC 5280 App. A.1), so an empty value is never valid; countryName
|
|
1337
|
+
// "SHALL have length 2" (draft sec. 3.1.4, X.520 SIZE (2)). Enforced HERE, the single place the value is built
|
|
1338
|
+
// for its declared type, so the decode, reconstruct and subject-directory-attributes paths cannot disagree.
|
|
1339
|
+
if (s.length === 0) throw _err("c509/bad-name", "a " + rdn.type + " value must be non-empty (SIZE (1..MAX))");
|
|
1340
|
+
if (rdn.type === "countryName" && s.length !== 2) throw _err("c509/bad-name", "a countryName value must have length 2 (draft sec. 3.1.4)");
|
|
1341
|
+
// emailAddress is an IA5String-only attribute (draft sec. 3.1.4): its type, not the int's sign, fixes the
|
|
1342
|
+
// string type, so it reconstructs as an IA5String whichever way the non-negative int was written.
|
|
1343
|
+
if (rdn.type === "emailAddress") return b.ia5(s);
|
|
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);
|
|
1352
|
+
return rdn.printable ? b.printable(s) : b.utf8(s);
|
|
1069
1353
|
}
|
|
1070
1354
|
|
|
1071
1355
|
// A Name -> the DER RDNSequence (SEQUENCE OF SET OF SEQUENCE{ type, value }); one attribute per RDN.
|
|
@@ -1149,13 +1433,11 @@ function _reconExtensions(exts) {
|
|
|
1149
1433
|
function _reconAlgId(alg) {
|
|
1150
1434
|
var fields = [b.oid(alg.oid)];
|
|
1151
1435
|
if (alg.parameters && alg.parameters.length) {
|
|
1152
|
-
// AlgorithmIdentifier.parameters is ANY --
|
|
1153
|
-
//
|
|
1154
|
-
//
|
|
1155
|
-
//
|
|
1156
|
-
|
|
1157
|
-
catch (e) { throw _err("c509/non-invertible", "algorithm parameters are not a single well-formed DER element", e); }
|
|
1158
|
-
fields.push(b.raw(alg.parameters));
|
|
1436
|
+
// AlgorithmIdentifier.parameters is ANY, spliced verbatim -- so it gets the SAME strict gate as every other
|
|
1437
|
+
// raw-ANY splice, not just a framing check. Framing alone admits an empty INTEGER / reserved tag 0 / a
|
|
1438
|
+
// non-minimal INTEGER, each of which reconstructs an AlgorithmIdentifier that an independent X.509 decoder
|
|
1439
|
+
// refuses to load and that this toolkit's own readers reject.
|
|
1440
|
+
fields.push(b.raw(_requireStrictDerTlv(alg.parameters, "c509/non-invertible", "algorithm parameters")));
|
|
1159
1441
|
}
|
|
1160
1442
|
return b.sequence(fields);
|
|
1161
1443
|
}
|
|
@@ -1226,6 +1508,9 @@ function parse(input) {
|
|
|
1226
1508
|
var f = root.children;
|
|
1227
1509
|
if (f.length !== 11) throw _err("c509/bad-tbs", "a C509 certificate must be an array of exactly 11 elements, got " + f.length);
|
|
1228
1510
|
|
|
1511
|
+
// The type slot must be a CBOR integer -- guard the major type here so a text/array/byte-string field 0 fails
|
|
1512
|
+
// in this module's domain rather than leaking cbor.read.int's cbor/unexpected-major fault to the caller.
|
|
1513
|
+
if (f[0].majorType !== 0 && f[0].majorType !== 1) throw _err("c509/bad-certificate-type", "c509CertificateType must be a CBOR integer");
|
|
1229
1514
|
var type = Number(cbor.read.int(f[0]));
|
|
1230
1515
|
if (type !== 2 && type !== 3) throw _err("c509/bad-certificate-type", "c509CertificateType must be 2 (native) or 3 (re-encoded), got " + type);
|
|
1231
1516
|
|
|
@@ -1234,10 +1519,16 @@ function parse(input) {
|
|
|
1234
1519
|
var sHex = serialBytes.content.toString("hex");
|
|
1235
1520
|
|
|
1236
1521
|
var sigAlg = _algorithm(f[2], SIG_ALG_BY_INT, "c509/unknown-algorithm", "issuerSignatureAlgorithm");
|
|
1237
|
-
var issuer = _name509(f[3], false);
|
|
1522
|
+
var issuer = _name509(f[3], false, type === 2);
|
|
1238
1523
|
var notBefore = _time(f[4], false, "validityNotBefore");
|
|
1239
1524
|
var notAfter = _time(f[5], true, "validityNotAfter");
|
|
1240
|
-
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
|
+
}
|
|
1241
1532
|
var spkAlg = _algorithm(f[7], PK_ALG_BY_INT, "c509/unknown-algorithm", "subjectPublicKeyAlgorithm");
|
|
1242
1533
|
var subjectPublicKey = null, rsaKey = null;
|
|
1243
1534
|
if (spkAlg.name === "rsaEncryption") {
|
|
@@ -1251,7 +1542,7 @@ function parse(input) {
|
|
|
1251
1542
|
if (f[8].majorType !== 2) throw _err("c509/bad-spki", "subjectPublicKey must be a CBOR byte string");
|
|
1252
1543
|
subjectPublicKey = f[8].content;
|
|
1253
1544
|
}
|
|
1254
|
-
var extensions = _extensions(f[9]);
|
|
1545
|
+
var extensions = _extensions(f[9], type === 2);
|
|
1255
1546
|
if (f[10].majorType !== 2) throw _err("c509/bad-signature", "issuerSignatureValue must be a CBOR byte string");
|
|
1256
1547
|
var signatureValue = f[10].content;
|
|
1257
1548
|
|
|
@@ -1491,7 +1782,17 @@ function _c509NameFromDer(nameBytes) {
|
|
|
1491
1782
|
var attrName = oid.name(asn1.read.oid(attr.children[0]));
|
|
1492
1783
|
if (attrName == null || ATTR_TO_INT[attrName] === undefined) throw _err("c509/non-invertible", "attribute type " + attrName + " has no C509 registry integer");
|
|
1493
1784
|
var valNode = attr.children[1];
|
|
1494
|
-
|
|
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); }
|
|
1791
|
+
// The IA5String type belongs ONLY to an IA5-only attribute (emailAddress, draft sec. 3.1.4), whose value
|
|
1792
|
+
// reconstructs from its type rather than the int's sign. Refuse either mismatch here with a precise verdict
|
|
1793
|
+
// instead of emitting an int form whose reconstruction would differ from the source bytes.
|
|
1794
|
+
var isIa5 = valNode.tagClass === "universal" && valNode.tagNumber === asn1.TAGS.IA5_STRING;
|
|
1795
|
+
if ((attrName === "emailAddress") !== isIa5) throw _err("c509/non-invertible", "attribute " + attrName + " carries a " + (isIa5 ? "IA5String" : "non-IA5String") + " value the C509 sec. 8.6 int form cannot represent");
|
|
1495
1796
|
var eui = attrName === "commonName" ? _euiFromCn(value) : null;
|
|
1496
1797
|
if (eui) rdns.push({ type: attrName, value: value, eui64: eui }); // tag-48 MAC commonName
|
|
1497
1798
|
else rdns.push({ type: attrName, value: value, printable: valNode.tagNumber === asn1.TAGS.PRINTABLE_STRING });
|
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",
|
|
@@ -68,6 +68,8 @@
|
|
|
68
68
|
"coverage": "c8 --include=lib/** --include=index.js --reporter=text-summary --reporter=lcov node test/smoke.js",
|
|
69
69
|
"check:swallows": "node scripts/check-swallow-coverage.js",
|
|
70
70
|
"coverage:gated": "npm run coverage && npm run check:swallows",
|
|
71
|
+
"coverage:unified": "node scripts/coverage-unified.js",
|
|
72
|
+
"coverage:unified:gated": "npm run coverage:unified && npm run check:swallows",
|
|
71
73
|
"prepack": "node scripts/check-pack-against-gitignore.js",
|
|
72
74
|
"check:vendor-currency": "node scripts/check-vendor-currency.js"
|
|
73
75
|
},
|
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
|
]
|