@blamejs/pki 0.1.1 → 0.1.3
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 +36 -0
- package/lib/asn1-der.js +202 -25
- package/lib/constants.js +19 -1
- package/lib/framework-error.js +2 -0
- package/lib/oid.js +145 -88
- package/lib/webcrypto.js +62 -7
- package/lib/x509.js +101 -10
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,42 @@ 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.1.3 — 2026-07-04
|
|
8
|
+
|
|
9
|
+
WebCrypto EC key import validates the curve against the key material.
|
|
10
|
+
|
|
11
|
+
### Security
|
|
12
|
+
|
|
13
|
+
- pki.webcrypto.subtle.importKey now derives an imported EC key's named curve from the key material and enforces it across the spki, pkcs8 and jwk formats. Previously it trusted the caller-supplied namedCurve without checking it against the key, so a key on an unsupported curve (for example secp256k1) imported as an approved curve, and a key on one curve could be labelled as another — an algorithm-confusion vector in which the CryptoKey's algorithm disagreed with its key material. A curve the framework does not support is now rejected (NotSupportedError) and a namedCurve that does not match the key is rejected (DataError); generateKey already enforced this, and import now matches it. The raw-key format was already validated against its declared curve and is unchanged.
|
|
14
|
+
|
|
15
|
+
## v0.1.2 — 2026-07-04
|
|
16
|
+
|
|
17
|
+
Fail-closed hardening across the DER codec, WebCrypto engine, and X.509 parser.
|
|
18
|
+
|
|
19
|
+
### Changed
|
|
20
|
+
|
|
21
|
+
- pki.oid gains registerFamily(base, members): register a whole OID arc family in one call by its shared base arc and each member's trailing leaf. The built-in registry is now declared this way, so a new object identifier is a data entry under its family rather than a re-spelled full path.
|
|
22
|
+
- Every primitive now declares the normative reference it is derived from (@spec) and, where it guards a known attack, the class it defends (@defends). The generated reference documentation links each citation to its source — RFC section anchors, NIST FIPS, ITU-T, W3C, CVE and CWE — so the surface is traceable to the standards it implements.
|
|
23
|
+
|
|
24
|
+
### Fixed
|
|
25
|
+
|
|
26
|
+
- pki.asn1.read.time rejects semantically invalid UTCTime/GeneralizedTime values (Feb 30, month 13, hour 25, second 60, day 00) instead of silently normalizing them, and preserves a four-digit GeneralizedTime year below 100 instead of remapping it a century, so a malformed or edge-case certificate validity window no longer parses to a shifted instant that disagrees with a strict verifier.
|
|
27
|
+
- The DER encoder is now symmetric with the decoder — no builder can emit DER the decoder would reject: build.utcTime rejects a year outside RFC 5280's 1950-2049 window rather than wrapping it a century, build.generalizedTime zero-pads the year to four digits, build.set orders its components as DER requires, build.integer/enumerated reject an empty or non-minimal content buffer, build.oid caps each sub-identifier, and build.ia5 rejects non-ASCII bytes.
|
|
28
|
+
- String decoding validates each restricted type: IA5String and VisibleString reject bytes outside their permitted range, PrintableString rejects characters outside its restricted set, and UTF8String rejects malformed UTF-8 instead of substituting the Unicode replacement character — closing a parser-differential on certificate name fields.
|
|
29
|
+
- BIT STRING decoding enforces DER's requirement that unused trailing bits be zero and rejects an empty BIT STRING that declares unused bits; UniversalString and BMPString decoding reject out-of-range and lone-surrogate code points with a typed Asn1Error instead of a bare RangeError.
|
|
30
|
+
- HMAC verify resolves false for a wrong-length signature instead of throwing, per the Web Cryptography API. AES-CTR encrypt/decrypt reject a counter length other than 128 rather than silently ignoring the parameter.
|
|
31
|
+
- pki.x509.parse raises a typed CertificateError (not a generic TypeError) for a truncated tbsCertificate, rejects a certificate carrying duplicate extensions (RFC 5280 §4.2), rejects a tbsCertificate with a repeated or out-of-order trailing field — a second extensions [3] wrapper (which would otherwise hide the first extension block and split duplicate extension OIDs across two wrappers past the per-extension check), or an out-of-order or unknown context field (RFC 5280 §4.1), rejects an empty issuer distinguished name (RFC 5280 §4.1.2.4) while still permitting an empty subject for the subjectAltName case, rejects an empty or non-SEQUENCE extensions field (RFC 5280 §4.1.2.9) with a typed error rather than a raw TypeError, validates the certificate version against the RFC 5280 set, and fails closed on a malformed string in a distinguished name (an invalid-UTF8 or out-of-range name value) instead of hex-escaping the invalid bytes away, so the decoder's strict string validation is enforced on the name path; a genuinely non-string attribute value (a primitive ANY-typed value, or a constructed non-string type such as a SEQUENCE) still renders as its RFC 4514 hex-encoded DER so the name stays representable.
|
|
32
|
+
- pki.oid.fromArcs rejects a negative or unsafe-integer arc instead of emitting a malformed OID string; the OID sub-identifier ceiling admits a 128-bit UUID-based arc; and the INTEGER ceiling admits a key at the magnitude cap with its DER sign octet.
|
|
33
|
+
- pki.version, pki.C.version, and the CLI now report the installed package version — the value is single-sourced from the package manifest and can no longer drift from the published release.
|
|
34
|
+
|
|
35
|
+
### Security
|
|
36
|
+
|
|
37
|
+
- The DER decoder now builds every INTEGER and OID sub-identifier in a single linear pass and refuses any that exceed a per-value byte ceiling (C.LIMITS.DER_MAX_INTEGER_BYTES / OID sub-identifier limit), before reading them. Previously these values were accumulated a byte at a time, which is quadratic in their length: a certificate carrying an oversized serial number or OID arc — well within the overall size cap — could pin a CPU for minutes. This closes a remotely-triggerable decode denial-of-service reachable through pki.x509.parse and pki.asn1.read.*.
|
|
38
|
+
- The DER decoder rejects a primitive-encoded SEQUENCE or SET (X.690 §8.9.1/§8.11.1 require these to be constructed) rather than producing a leaf node. Previously such input decoded to a leaf that pki.x509.parse dereferenced as a structured node, crashing with an uncaught TypeError on attacker-controlled bytes; it now fails closed with a typed error.
|
|
39
|
+
- The DER decoder also rejects the mirror violation — a constructed encoding of a universal primitive-only type (INTEGER, OBJECT IDENTIFIER, BOOLEAN, the restricted strings, UTCTime/GeneralizedTime, BIT/OCTET STRING), which is valid BER but not valid DER (X.690 §10.2). Previously a constructed string tag decoded to a childless node that a certificate distinguished name would hex-render, letting an invalid BER/DER name value parse despite the restricted-string content checks; it now fails closed at decode.
|
|
40
|
+
- pki.webcrypto.subtle.unwrapKey now enforces the 'unwrapKey' key usage on every unwrap path, including the RSA-OAEP and AES-GCM delegate paths that previously skipped it — an unwrapping key without the 'unwrapKey' usage is now rejected. deriveKey now enforces the distinct 'deriveKey' usage rather than inheriting 'deriveBits'. Both close cases where an operator-set key-usage restriction could be bypassed.
|
|
41
|
+
- pki.x509.parse now rejects a certificate whose outer signatureAlgorithm does not match the signature algorithm inside the signed tbsCertificate (RFC 5280 §4.1.1.2). Surfacing the two AlgorithmIdentifiers without enforcing their equality let a certificate claim one algorithm in the signed body and another in the outer wrapper — a signature-algorithm-substitution vector; the two fields must now be identical.
|
|
42
|
+
|
|
7
43
|
## v0.1.1 — 2026-07-04
|
|
8
44
|
|
|
9
45
|
First published release of the 0.1.x foundation.
|
package/lib/asn1-der.js
CHANGED
|
@@ -65,6 +65,20 @@ var CLASS_CONTEXT = 0x80;
|
|
|
65
65
|
var CLASS_PRIVATE = 0xc0;
|
|
66
66
|
var CONSTRUCTED_BIT = 0x20;
|
|
67
67
|
|
|
68
|
+
// X.690 §8.x / §10.2 (DER) — every universal type the codec handles other than
|
|
69
|
+
// SEQUENCE and SET is a primitive-only type: it MUST be encoded primitive, and
|
|
70
|
+
// a constructed encoding (BOOLEAN, INTEGER, OID, the restricted strings,
|
|
71
|
+
// UTC/GeneralizedTime, and — DER-strict — BIT STRING / OCTET STRING) is not
|
|
72
|
+
// valid DER. Derived from TAGS so a newly-registered primitive type is covered
|
|
73
|
+
// automatically. This is the mirror of the primitive-SEQUENCE/SET rejection.
|
|
74
|
+
var PRIMITIVE_ONLY_UNIVERSAL_TAGS = (function () {
|
|
75
|
+
var s = Object.create(null);
|
|
76
|
+
Object.keys(TAGS).forEach(function (k) {
|
|
77
|
+
if (k !== "SEQUENCE" && k !== "SET") s[TAGS[k]] = true;
|
|
78
|
+
});
|
|
79
|
+
return s;
|
|
80
|
+
})();
|
|
81
|
+
|
|
68
82
|
function _className(bits) {
|
|
69
83
|
switch (bits) {
|
|
70
84
|
case CLASS_UNIVERSAL: return "universal";
|
|
@@ -88,6 +102,8 @@ function _asBuffer(input, who) {
|
|
|
88
102
|
* @signature pki.asn1.decode(bytes, opts?) -> node
|
|
89
103
|
* @since 0.1.0
|
|
90
104
|
* @status stable
|
|
105
|
+
* @spec X.690, ISO/IEC 8825-1
|
|
106
|
+
* @defends ASN.1-parser-DoS (CWE-400)
|
|
91
107
|
* @related pki.asn1.encode
|
|
92
108
|
*
|
|
93
109
|
* Parse DER into a node tree. Each node is
|
|
@@ -156,6 +172,22 @@ function _decodeTLV(buf, start, limit, depth, maxDepth) {
|
|
|
156
172
|
}
|
|
157
173
|
}
|
|
158
174
|
|
|
175
|
+
// X.690 8.9.1 / 8.11.1 — a universal SEQUENCE or SET is always constructed;
|
|
176
|
+
// a primitive-tagged 0x10 / 0x11 is not valid DER (and would decode to a
|
|
177
|
+
// leaf that constructed-structure consumers dereference as a parent).
|
|
178
|
+
if (tagClassBits === CLASS_UNIVERSAL && (tagNumber === TAGS.SEQUENCE || tagNumber === TAGS.SET) && !constructed) {
|
|
179
|
+
throw new Asn1Error("asn1/bad-tlv", "SEQUENCE/SET must be constructed");
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// X.690 §8.x / §10.2 — the mirror rule: a universal primitive-only type
|
|
183
|
+
// (INTEGER, OID, BOOLEAN, the restricted strings, times, BIT/OCTET STRING)
|
|
184
|
+
// encoded constructed is not valid DER. Without this a constructed string
|
|
185
|
+
// tag decodes to a childless node that later paths (an X.509 DN attribute
|
|
186
|
+
// value) would hex-render or dereference instead of failing closed.
|
|
187
|
+
if (tagClassBits === CLASS_UNIVERSAL && constructed && PRIMITIVE_ONLY_UNIVERSAL_TAGS[tagNumber]) {
|
|
188
|
+
throw new Asn1Error("asn1/constructed-primitive-type", "a universal primitive-only type must be encoded primitive in DER");
|
|
189
|
+
}
|
|
190
|
+
|
|
159
191
|
if (p >= limit) throw new Asn1Error("asn1/truncated", "expected a length octet");
|
|
160
192
|
var lenByte = buf[p]; p += 1;
|
|
161
193
|
var length;
|
|
@@ -239,15 +271,23 @@ function readInteger(node) {
|
|
|
239
271
|
_expectPrimitive(node, "readInteger");
|
|
240
272
|
var c = node.content;
|
|
241
273
|
if (c.length === 0) throw new Asn1Error("asn1/bad-integer", "INTEGER must have at least 1 content octet");
|
|
274
|
+
// Defense-in-depth: refuse an over-cap magnitude before touching BigInt.
|
|
275
|
+
// A one-shot hex parse is linear, but the length still bounds worst-case
|
|
276
|
+
// work — reject a hostile length prefix up front. The cap bounds the
|
|
277
|
+
// MAGNITUDE; a positive INTEGER whose top bit is set carries one leading
|
|
278
|
+
// 0x00 DER sign octet, so allow cap + 1 content bytes (an RSA-131072
|
|
279
|
+
// modulus is 16384 magnitude bytes plus that sign pad).
|
|
280
|
+
if (c.length > constants.LIMITS.DER_MAX_INTEGER_BYTES + 1) {
|
|
281
|
+
throw new Asn1Error("asn1/integer-too-large",
|
|
282
|
+
"INTEGER content " + c.length + " bytes exceeds cap " + (constants.LIMITS.DER_MAX_INTEGER_BYTES + 1));
|
|
283
|
+
}
|
|
242
284
|
if (c.length > 1) {
|
|
243
285
|
if (c[0] === 0x00 && (c[1] & 0x80) === 0) throw new Asn1Error("asn1/non-minimal-integer", "non-minimal positive INTEGER");
|
|
244
286
|
if (c[0] === 0xff && (c[1] & 0x80) !== 0) throw new Asn1Error("asn1/non-minimal-integer", "non-minimal negative INTEGER");
|
|
245
287
|
}
|
|
246
288
|
var neg = (c[0] & 0x80) !== 0;
|
|
247
|
-
var
|
|
248
|
-
|
|
249
|
-
if (neg) v -= (1n << BigInt(8 * c.length));
|
|
250
|
-
return v;
|
|
289
|
+
var mag = c.length ? BigInt("0x" + Buffer.from(c).toString("hex")) : 0n;
|
|
290
|
+
return neg ? mag - (1n << BigInt(c.length * 8)) : mag;
|
|
251
291
|
}
|
|
252
292
|
|
|
253
293
|
function readBitString(node) {
|
|
@@ -258,6 +298,12 @@ function readBitString(node) {
|
|
|
258
298
|
var unusedBits = c[0];
|
|
259
299
|
if (unusedBits > 7) throw new Asn1Error("asn1/bad-bit-string", "unused-bit count " + unusedBits + " > 7");
|
|
260
300
|
if (unusedBits > 0 && c.length === 1) throw new Asn1Error("asn1/bad-bit-string", "unused bits declared over an empty body");
|
|
301
|
+
// X.690 11.2.1 — the declared unused low bits of the final octet must be
|
|
302
|
+
// zero in DER.
|
|
303
|
+
if (unusedBits > 0 && c.length > 1) {
|
|
304
|
+
var mask = (1 << unusedBits) - 1;
|
|
305
|
+
if ((c[c.length - 1] & mask) !== 0) throw new Asn1Error("asn1/bad-bit-string", "DER requires unused bits to be zero");
|
|
306
|
+
}
|
|
261
307
|
return { unusedBits: unusedBits, bytes: c.subarray(1) };
|
|
262
308
|
}
|
|
263
309
|
|
|
@@ -279,6 +325,7 @@ function readNull(node) {
|
|
|
279
325
|
* @signature pki.asn1.read.oid(node) -> "1.2.840.113549.1.1.11"
|
|
280
326
|
* @since 0.1.0
|
|
281
327
|
* @status stable
|
|
328
|
+
* @spec X.690 §8.19
|
|
282
329
|
* @related pki.oid.name
|
|
283
330
|
*
|
|
284
331
|
* Decode an OBJECT IDENTIFIER node to its dotted-decimal string, enforcing
|
|
@@ -296,16 +343,27 @@ function readOid(node) {
|
|
|
296
343
|
function decodeOidContent(buf) {
|
|
297
344
|
if (buf.length === 0) throw new OidError("oid/empty", "OBJECT IDENTIFIER content is empty");
|
|
298
345
|
var arcs = [];
|
|
299
|
-
var
|
|
300
|
-
var started = false;
|
|
346
|
+
var arcStart = 0;
|
|
301
347
|
for (var i = 0; i < buf.length; i++) {
|
|
302
348
|
var b = buf[i];
|
|
303
|
-
if (
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
if (
|
|
349
|
+
if (i === arcStart && b === 0x80) throw new OidError("oid/non-minimal", "non-minimal sub-identifier (leading 0x80)");
|
|
350
|
+
// Cap the continuation-byte run so a single arc can't drive unbounded
|
|
351
|
+
// BigInt growth before it terminates.
|
|
352
|
+
if (i - arcStart >= constants.LIMITS.OID_MAX_SUBIDENTIFIER_BYTES) {
|
|
353
|
+
throw new OidError("oid/subidentifier-too-large",
|
|
354
|
+
"OID sub-identifier exceeds " + constants.LIMITS.OID_MAX_SUBIDENTIFIER_BYTES + " octets");
|
|
355
|
+
}
|
|
356
|
+
if ((b & 0x80) === 0) {
|
|
357
|
+
// Terminal octet: fold the whole [arcStart..i] slice base-128 in one
|
|
358
|
+
// bounded pass (the run is capped above), no per-bit accumulation
|
|
359
|
+
// across the full OID content.
|
|
360
|
+
var value = 0n;
|
|
361
|
+
for (var k = arcStart; k <= i; k++) value = value * 128n + BigInt(buf[k] & 0x7f);
|
|
362
|
+
arcs.push(value);
|
|
363
|
+
arcStart = i + 1;
|
|
364
|
+
}
|
|
307
365
|
}
|
|
308
|
-
if (
|
|
366
|
+
if (arcStart !== buf.length) throw new OidError("oid/truncated", "OBJECT IDENTIFIER ends mid sub-identifier");
|
|
309
367
|
var first = arcs[0];
|
|
310
368
|
var a1, a2;
|
|
311
369
|
if (first < 40n) { a1 = 0n; a2 = first; }
|
|
@@ -320,15 +378,49 @@ function _decodeText(buf, encoding) {
|
|
|
320
378
|
return buf.toString(encoding);
|
|
321
379
|
}
|
|
322
380
|
|
|
381
|
+
// IA5String is 7-bit ASCII (T.50 / IA5) — every content octet is 0x00..0x7F.
|
|
382
|
+
function _decodeIa5(buf) {
|
|
383
|
+
for (var i = 0; i < buf.length; i++) {
|
|
384
|
+
if (buf[i] > 0x7F) throw new Asn1Error("asn1/bad-ia5-string", "IA5String requires 7-bit ASCII");
|
|
385
|
+
}
|
|
386
|
+
return buf.toString("latin1");
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// VisibleString (ISO 646 IRV / X.690) is the printable ASCII range 0x20..0x7E
|
|
390
|
+
// — no control characters and no bytes with the high bit set.
|
|
391
|
+
function _decodeVisible(buf) {
|
|
392
|
+
for (var i = 0; i < buf.length; i++) {
|
|
393
|
+
if (buf[i] < 0x20 || buf[i] > 0x7E) throw new Asn1Error("asn1/bad-visible-string", "VisibleString must be 0x20..0x7E");
|
|
394
|
+
}
|
|
395
|
+
return buf.toString("latin1");
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// PrintableString is restricted to A-Z a-z 0-9 space and ' ( ) + , - . / : = ?
|
|
399
|
+
function _decodePrintable(buf) {
|
|
400
|
+
var s = buf.toString("latin1");
|
|
401
|
+
if (!PRINTABLE_RE.test(s)) throw new Asn1Error("asn1/bad-printable-string", "PrintableString has characters outside the restricted set");
|
|
402
|
+
return s;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// UTF8String must be well-formed UTF-8; a lenient decoder substitutes U+FFFD
|
|
406
|
+
// for invalid sequences, silently mangling hostile input into valid text.
|
|
407
|
+
function _decodeUtf8Strict(buf) {
|
|
408
|
+
try {
|
|
409
|
+
return new TextDecoder("utf-8", { fatal: true }).decode(buf);
|
|
410
|
+
} catch (_e) {
|
|
411
|
+
throw new Asn1Error("asn1/bad-utf8-string", "invalid UTF-8 in UTF8String");
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
323
415
|
function readString(node) {
|
|
324
416
|
if (node.tagClass !== "universal") throw new Asn1Error("asn1/expected-string", "readString: not a universal string type");
|
|
325
417
|
_expectPrimitive(node, "readString");
|
|
326
418
|
switch (node.tagNumber) {
|
|
327
|
-
case TAGS.UTF8_STRING: return
|
|
328
|
-
case TAGS.PRINTABLE_STRING: return
|
|
329
|
-
case TAGS.IA5_STRING: return
|
|
419
|
+
case TAGS.UTF8_STRING: return _decodeUtf8Strict(node.content);
|
|
420
|
+
case TAGS.PRINTABLE_STRING: return _decodePrintable(node.content);
|
|
421
|
+
case TAGS.IA5_STRING: return _decodeIa5(node.content);
|
|
330
422
|
case TAGS.TELETEX_STRING: return _decodeText(node.content, "latin1");
|
|
331
|
-
case TAGS.VISIBLE_STRING: return
|
|
423
|
+
case TAGS.VISIBLE_STRING: return _decodeVisible(node.content);
|
|
332
424
|
case TAGS.BMP_STRING: return _decodeUtf16be(node.content);
|
|
333
425
|
case TAGS.UNIVERSAL_STRING: return _decodeUtf32be(node.content);
|
|
334
426
|
default:
|
|
@@ -338,6 +430,12 @@ function readString(node) {
|
|
|
338
430
|
|
|
339
431
|
function _decodeUtf16be(buf) {
|
|
340
432
|
if (buf.length % 2 !== 0) throw new Asn1Error("asn1/bad-bmp-string", "BMPString length must be even");
|
|
433
|
+
// BMPString encodes BMP scalar values only; a UTF-16 surrogate code unit
|
|
434
|
+
// here is a lone surrogate, not valid text.
|
|
435
|
+
for (var i = 0; i < buf.length; i += 2) {
|
|
436
|
+
var u = (buf[i] << 8) | buf[i + 1];
|
|
437
|
+
if (u >= 0xD800 && u <= 0xDFFF) throw new Asn1Error("asn1/bad-bmp-string", "code point out of range");
|
|
438
|
+
}
|
|
341
439
|
var swapped = Buffer.from(buf);
|
|
342
440
|
swapped.swap16();
|
|
343
441
|
return swapped.toString("utf16le");
|
|
@@ -348,6 +446,11 @@ function _decodeUtf32be(buf) {
|
|
|
348
446
|
var out = "";
|
|
349
447
|
for (var i = 0; i < buf.length; i += 4) {
|
|
350
448
|
var cp = (buf[i] * 0x1000000) + (buf[i + 1] << 16) + (buf[i + 2] << 8) + buf[i + 3];
|
|
449
|
+
// Reject non-scalar values (> U+10FFFF or a surrogate) before
|
|
450
|
+
// String.fromCodePoint would throw a bare RangeError / emit a lone surrogate.
|
|
451
|
+
if (cp > 0x10FFFF || (cp >= 0xD800 && cp <= 0xDFFF)) {
|
|
452
|
+
throw new Asn1Error("asn1/bad-universal-string", "code point out of range");
|
|
453
|
+
}
|
|
351
454
|
out += String.fromCodePoint(cp);
|
|
352
455
|
}
|
|
353
456
|
return out;
|
|
@@ -379,9 +482,23 @@ function readTime(node) {
|
|
|
379
482
|
var hour = parseInt(m[4], 10);
|
|
380
483
|
var min = parseInt(m[5], 10);
|
|
381
484
|
var sec = parseInt(m[6], 10);
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
485
|
+
// Build from the LITERAL year via setUTCFullYear: the Date.UTC() / new Date()
|
|
486
|
+
// constructors remap a year in 0..99 to 1900..1999, which would corrupt a
|
|
487
|
+
// GeneralizedTime year below 100 (0099 -> 1999). setUTCFullYear(year, ...)
|
|
488
|
+
// takes the year literally and uses that year's own calendar, so years
|
|
489
|
+
// below 100 and proleptic leap years are handled correctly.
|
|
490
|
+
var d = new Date(0);
|
|
491
|
+
d.setUTCFullYear(year, month - 1, day);
|
|
492
|
+
d.setUTCHours(hour, min, sec, 0);
|
|
493
|
+
if (isNaN(d.getTime())) throw new Asn1Error("asn1/bad-time", "unparseable time " + JSON.stringify(s));
|
|
494
|
+
// Date/setUTC* silently roll over out-of-range fields (Feb 30 -> Mar 2,
|
|
495
|
+
// month 13 -> next Jan, hour 25 -> +1 day). Reject unless every component
|
|
496
|
+
// round-trips exactly.
|
|
497
|
+
if (d.getUTCFullYear() !== year || d.getUTCMonth() !== month - 1 || d.getUTCDate() !== day ||
|
|
498
|
+
d.getUTCHours() !== hour || d.getUTCMinutes() !== min || d.getUTCSeconds() !== sec) {
|
|
499
|
+
throw new Asn1Error("asn1/bad-time", "time component out of range " + JSON.stringify(s));
|
|
500
|
+
}
|
|
501
|
+
return d;
|
|
385
502
|
}
|
|
386
503
|
|
|
387
504
|
// ---- Encoder / builders ---------------------------------------------
|
|
@@ -413,6 +530,7 @@ function encodeIdentifier(classBits, constructed, tagNumber) {
|
|
|
413
530
|
* @signature pki.asn1.encode(classBits, constructed, tagNumber, content) -> Buffer
|
|
414
531
|
* @since 0.1.0
|
|
415
532
|
* @status stable
|
|
533
|
+
* @spec X.690, ISO/IEC 8825-1
|
|
416
534
|
* @related pki.asn1.decode
|
|
417
535
|
*
|
|
418
536
|
* Low-level TLV encoder — prepend the identifier + DER length to a content
|
|
@@ -471,6 +589,12 @@ function encodeOidContent(dotted) {
|
|
|
471
589
|
var body = [];
|
|
472
590
|
var v = subids[s];
|
|
473
591
|
do { body.unshift(Number(v & 0x7fn)); v >>= 7n; } while (v > 0n);
|
|
592
|
+
// Keep the encoder symmetric with the decoder's per-arc cap so build.oid
|
|
593
|
+
// can't emit a sub-identifier read.oid would reject as hostile.
|
|
594
|
+
if (body.length > constants.LIMITS.OID_MAX_SUBIDENTIFIER_BYTES) {
|
|
595
|
+
throw new OidError("oid/subidentifier-too-large",
|
|
596
|
+
"OID sub-identifier exceeds " + constants.LIMITS.OID_MAX_SUBIDENTIFIER_BYTES + " octets");
|
|
597
|
+
}
|
|
474
598
|
for (var k = 0; k < body.length - 1; k++) body[k] |= 0x80;
|
|
475
599
|
for (var j = 0; j < body.length; j++) out.push(body[j]);
|
|
476
600
|
}
|
|
@@ -482,13 +606,22 @@ var PRINTABLE_RE = /^[A-Za-z0-9 '()+,\-./:=?]*$/;
|
|
|
482
606
|
function _fmtTwo(n) { return (n < 10 ? "0" : "") + n; }
|
|
483
607
|
|
|
484
608
|
function _generalizedTimeString(date) {
|
|
485
|
-
|
|
609
|
+
// GeneralizedTime is YYYYMMDDHHMMSSZ — a year below 1000 must still emit
|
|
610
|
+
// four digits, or read.time rejects the short (12-14 char) content.
|
|
611
|
+
var y = date.getUTCFullYear();
|
|
612
|
+
if (y < 0 || y > 9999) throw new Asn1Error("asn1/bad-generalizedtime", "GeneralizedTime year " + y + " outside 0000..9999");
|
|
613
|
+
var yyyy = ("000" + y).slice(-4);
|
|
614
|
+
return yyyy +
|
|
486
615
|
_fmtTwo(date.getUTCMonth() + 1) + _fmtTwo(date.getUTCDate()) +
|
|
487
616
|
_fmtTwo(date.getUTCHours()) + _fmtTwo(date.getUTCMinutes()) + _fmtTwo(date.getUTCSeconds()) + "Z";
|
|
488
617
|
}
|
|
489
618
|
|
|
490
619
|
function _utcTimeString(date) {
|
|
491
|
-
|
|
620
|
+
// RFC 5280 §4.1.2.5.1 restricts UTCTime to 1950..2049; a year outside that
|
|
621
|
+
// wraps to the wrong century (2050 -> "50" -> decodes as 1950).
|
|
622
|
+
var y = date.getUTCFullYear();
|
|
623
|
+
if (y < 1950 || y > 2049) throw new Asn1Error("asn1/bad-utctime", "UTCTime year " + y + " outside 1950..2049; use GeneralizedTime");
|
|
624
|
+
var yy = y % 100;
|
|
492
625
|
return _fmtTwo(yy) +
|
|
493
626
|
_fmtTwo(date.getUTCMonth() + 1) + _fmtTwo(date.getUTCDate()) +
|
|
494
627
|
_fmtTwo(date.getUTCHours()) + _fmtTwo(date.getUTCMinutes()) + _fmtTwo(date.getUTCSeconds()) + "Z";
|
|
@@ -499,6 +632,7 @@ function _utcTimeString(date) {
|
|
|
499
632
|
* @signature pki.asn1.build.sequence([ ...tlvBuffers ]) -> Buffer
|
|
500
633
|
* @since 0.1.0
|
|
501
634
|
* @status stable
|
|
635
|
+
* @spec X.690, ISO/IEC 8825-1
|
|
502
636
|
* @related pki.asn1.encode
|
|
503
637
|
*
|
|
504
638
|
* Canonical-DER value builders. Each returns the full TLV Buffer for one
|
|
@@ -513,18 +647,36 @@ function _utcTimeString(date) {
|
|
|
513
647
|
*/
|
|
514
648
|
var build = {
|
|
515
649
|
boolean: function (v) { return _universal(TAGS.BOOLEAN, false, Buffer.from([v ? 0xff : 0x00])); },
|
|
516
|
-
integer: function (v) { return _universal(TAGS.INTEGER, false,
|
|
517
|
-
enumerated: function (v) { return _universal(TAGS.ENUMERATED, false,
|
|
650
|
+
integer: function (v) { return _universal(TAGS.INTEGER, false, _intContent(v, "build.integer")); },
|
|
651
|
+
enumerated: function (v) { return _universal(TAGS.ENUMERATED, false, _intContent(v, "build.enumerated")); },
|
|
518
652
|
nullValue: function () { return _universal(TAGS.NULL, false, Buffer.alloc(0)); },
|
|
519
653
|
oid: function (dotted) { return _universal(TAGS.OBJECT_IDENTIFIER, false, encodeOidContent(dotted)); },
|
|
520
654
|
octetString: function (buf) { return _universal(TAGS.OCTET_STRING, false, _asBuffer(buf, "build.octetString")); },
|
|
521
655
|
bitString: function (buf, unusedBits) {
|
|
522
656
|
var u = unusedBits || 0;
|
|
523
657
|
if (u < 0 || u > 7) throw new Asn1Error("asn1/bad-bit-string", "unusedBits must be 0..7");
|
|
524
|
-
|
|
658
|
+
var body = _asBuffer(buf, "build.bitString");
|
|
659
|
+
// X.690 8.6.2.3: an empty BIT STRING has no content octets, so its
|
|
660
|
+
// unused-bit count must be zero — there are no bits to leave unused.
|
|
661
|
+
if (u > 0 && body.length === 0) throw new Asn1Error("asn1/bad-bit-string", "empty BIT STRING must declare zero unused bits");
|
|
662
|
+
// Stay canonical: refuse a tail whose declared unused low bits are set.
|
|
663
|
+
if (u > 0 && body.length > 0) {
|
|
664
|
+
var mask = (1 << u) - 1;
|
|
665
|
+
if ((body[body.length - 1] & mask) !== 0) throw new Asn1Error("asn1/bad-bit-string", "unused bits must be zero");
|
|
666
|
+
}
|
|
667
|
+
return _universal(TAGS.BIT_STRING, false, Buffer.concat([Buffer.from([u]), body]));
|
|
525
668
|
},
|
|
526
669
|
utf8: function (s) { return _universal(TAGS.UTF8_STRING, false, Buffer.from(String(s), "utf8")); },
|
|
527
|
-
ia5: function (s) {
|
|
670
|
+
ia5: function (s) {
|
|
671
|
+
// Validate the INPUT code points, not the latin1 output bytes: latin1
|
|
672
|
+
// truncates a code point > 0xFF to its low byte (U+0141 -> 0x41), which
|
|
673
|
+
// would silently pass a non-IA5 character through as a different one.
|
|
674
|
+
s = String(s);
|
|
675
|
+
for (var i = 0; i < s.length; i++) {
|
|
676
|
+
if (s.charCodeAt(i) > 0x7F) throw new Asn1Error("asn1/bad-ia5-string", "IA5String requires 7-bit ASCII");
|
|
677
|
+
}
|
|
678
|
+
return _universal(TAGS.IA5_STRING, false, Buffer.from(s, "latin1"));
|
|
679
|
+
},
|
|
528
680
|
printable: function (s) {
|
|
529
681
|
s = String(s);
|
|
530
682
|
if (!PRINTABLE_RE.test(s)) throw new Asn1Error("asn1/bad-printable-string", "value has characters outside the PrintableString set");
|
|
@@ -533,7 +685,12 @@ var build = {
|
|
|
533
685
|
utcTime: function (date) { return _universal(TAGS.UTC_TIME, false, Buffer.from(_utcTimeString(date), "latin1")); },
|
|
534
686
|
generalizedTime: function (date) { return _universal(TAGS.GENERALIZED_TIME, false, Buffer.from(_generalizedTimeString(date), "latin1")); },
|
|
535
687
|
sequence: function (children) { return _universal(TAGS.SEQUENCE, true, Buffer.concat(_asBufferArray(children, "build.sequence"))); },
|
|
536
|
-
set: function (children) {
|
|
688
|
+
set: function (children) {
|
|
689
|
+
// DER (X.690 11.6) requires SET component encodings in ascending order.
|
|
690
|
+
var arr = _asBufferArray(children, "build.set").slice();
|
|
691
|
+
arr.sort(Buffer.compare);
|
|
692
|
+
return _universal(TAGS.SET, true, Buffer.concat(arr));
|
|
693
|
+
},
|
|
537
694
|
setOf: function (children) {
|
|
538
695
|
var arr = _asBufferArray(children, "build.setOf").slice();
|
|
539
696
|
arr.sort(Buffer.compare);
|
|
@@ -546,6 +703,26 @@ var build = {
|
|
|
546
703
|
raw: function (buf) { return _asBuffer(buf, "build.raw"); },
|
|
547
704
|
};
|
|
548
705
|
|
|
706
|
+
// A raw INTEGER/ENUMERATED content Buffer must satisfy the same DER shape
|
|
707
|
+
// read.integer enforces: non-empty and minimally encoded (no redundant sign
|
|
708
|
+
// octet). A BigInt/number goes through intToDer, which is minimal by build.
|
|
709
|
+
function _intContent(v, who) {
|
|
710
|
+
if (Buffer.isBuffer(v)) {
|
|
711
|
+
if (v.length === 0) throw new Asn1Error("asn1/bad-integer", who + ": INTEGER content is empty");
|
|
712
|
+
if (v.length > 1) {
|
|
713
|
+
if (v[0] === 0x00 && (v[1] & 0x80) === 0) throw new Asn1Error("asn1/non-minimal-integer", who + ": non-minimal positive INTEGER");
|
|
714
|
+
if (v[0] === 0xff && (v[1] & 0x80) !== 0) throw new Asn1Error("asn1/non-minimal-integer", who + ": non-minimal negative INTEGER");
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
var content = Buffer.isBuffer(v) ? v : intToDer(v);
|
|
718
|
+
// Symmetric with read.integer's per-value ceiling (magnitude + DER sign
|
|
719
|
+
// octet), so a builder can't emit an INTEGER the decoder would refuse.
|
|
720
|
+
if (content.length > constants.LIMITS.DER_MAX_INTEGER_BYTES + 1) {
|
|
721
|
+
throw new Asn1Error("asn1/integer-too-large", who + ": INTEGER content " + content.length + " bytes exceeds cap " + (constants.LIMITS.DER_MAX_INTEGER_BYTES + 1));
|
|
722
|
+
}
|
|
723
|
+
return content;
|
|
724
|
+
}
|
|
725
|
+
|
|
549
726
|
function _asBufferArray(arr, who) {
|
|
550
727
|
if (!Array.isArray(arr)) throw new Asn1Error("asn1/bad-children", who + ": expected an array of TLV buffers");
|
|
551
728
|
return arr.map(function (b) { return _asBuffer(b, who); });
|
package/lib/constants.js
CHANGED
|
@@ -52,6 +52,7 @@ var DAYS_PER_WEEK = 7;
|
|
|
52
52
|
* @signature C.TIME.days(n) -> milliseconds
|
|
53
53
|
* @since 0.1.0
|
|
54
54
|
* @status stable
|
|
55
|
+
* @spec internal (design: functional time-scale helpers)
|
|
55
56
|
*
|
|
56
57
|
* Duration helpers. Each returns an integer count of milliseconds so the
|
|
57
58
|
* value drops straight into `setTimeout`, a validity window, or an OCSP
|
|
@@ -78,6 +79,7 @@ var BYTES_PER_KIB = 1024;
|
|
|
78
79
|
* @signature C.BYTES.mib(n) -> bytes
|
|
79
80
|
* @since 0.1.0
|
|
80
81
|
* @status stable
|
|
82
|
+
* @spec IEC 80000-13
|
|
81
83
|
*
|
|
82
84
|
* Binary-magnitude size helpers. Each returns an integer byte count for
|
|
83
85
|
* codec limits (max DER input, max PEM block) so a size bound reads as
|
|
@@ -99,13 +101,29 @@ var BYTES = {
|
|
|
99
101
|
// unbounded length prefix or a pathologically-nested SEQUENCE is a
|
|
100
102
|
// classic decoder-DoS, so the bound is a fail-closed default rather
|
|
101
103
|
// than a tunable an operator must remember to set.
|
|
104
|
+
//
|
|
105
|
+
// DER_MAX_INTEGER_BYTES and OID_MAX_SUBIDENTIFIER_BYTES are per-value
|
|
106
|
+
// ceilings the whole-document cap can't provide: a single INTEGER or OID
|
|
107
|
+
// sub-identifier under the 16 MiB limit can still carry hundreds of KiB,
|
|
108
|
+
// so a value reader that scales super-linearly in its content length is a
|
|
109
|
+
// decoder-DoS the document cap does not stop. 16 KiB covers any real key
|
|
110
|
+
// material (an RSA-131072 modulus), and 32 bytes covers any real OID arc:
|
|
111
|
+
// the largest standard sub-identifier is a 128-bit UUID-based arc (X.667,
|
|
112
|
+
// e.g. 2.25.<uuid>), which is 19 base-128 bytes — so the cap must exceed
|
|
113
|
+
// that, not sit at 16. Anything larger than 32 bytes is refused as hostile.
|
|
102
114
|
var LIMITS = {
|
|
103
115
|
DER_MAX_BYTES: BYTES.mib(16),
|
|
104
116
|
DER_MAX_DEPTH: 64,
|
|
105
117
|
PEM_MAX_BYTES: BYTES.mib(16),
|
|
118
|
+
DER_MAX_INTEGER_BYTES: BYTES.kib(16),
|
|
119
|
+
OID_MAX_SUBIDENTIFIER_BYTES: 32,
|
|
106
120
|
};
|
|
107
121
|
|
|
108
|
-
|
|
122
|
+
// Single-sourced from the package manifest so the reported version can
|
|
123
|
+
// never drift from the published package (package.json is always present
|
|
124
|
+
// in the installed tarball). A hand-maintained literal here silently
|
|
125
|
+
// shipped 0.1.0 on a 0.1.1 release.
|
|
126
|
+
var VERSION = require("../package.json").version;
|
|
109
127
|
|
|
110
128
|
module.exports = {
|
|
111
129
|
TIME: TIME,
|
package/lib/framework-error.js
CHANGED
|
@@ -32,6 +32,7 @@
|
|
|
32
32
|
* @signature new PkiError(message, code)
|
|
33
33
|
* @since 0.1.0
|
|
34
34
|
* @status stable
|
|
35
|
+
* @spec internal (design: error taxonomy base class)
|
|
35
36
|
*
|
|
36
37
|
* Base class every toolkit error extends. Provides the unified
|
|
37
38
|
* `instanceof` check plus the `{ name, code, isPkiError }` shape.
|
|
@@ -57,6 +58,7 @@ class PkiError extends Error {
|
|
|
57
58
|
* @signature pki.errors.defineClass(name, opts?) -> constructor
|
|
58
59
|
* @since 0.1.0
|
|
59
60
|
* @status stable
|
|
61
|
+
* @spec internal (design: error-class factory)
|
|
60
62
|
*
|
|
61
63
|
* Factory that produces a `PkiError` subclass with the standard shape —
|
|
62
64
|
* eliminating the per-domain boilerplate. The returned constructor takes
|
package/lib/oid.js
CHANGED
|
@@ -17,14 +17,20 @@
|
|
|
17
17
|
* codebase — is what lets a new algorithm be a data entry instead of a
|
|
18
18
|
* code change.
|
|
19
19
|
*
|
|
20
|
-
* The seed set
|
|
20
|
+
* The seed set is declared by FAMILY: an OID belongs to a class with a
|
|
21
|
+
* shared base arc (the "starting variable" — `2.5.4` for the RFC 5280
|
|
22
|
+
* attribute types, `2.5.29` for the extensions, `2.16.840.1.101.3.4` for
|
|
23
|
+
* the NIST algorithms), and each member names only its trailing arc. The
|
|
24
|
+
* full OID is derived from base + leaf at load, so the arc hierarchy that
|
|
25
|
+
* IS the OID namespace is modelled directly instead of re-spelled per
|
|
26
|
+
* entry. It covers the RFC 5280 attribute types and extensions, the
|
|
21
27
|
* classical signature / public-key / digest algorithms, and the
|
|
22
28
|
* NIST-assigned post-quantum arcs (ML-DSA, ML-KEM, SLH-DSA). Operators
|
|
23
|
-
* extend it with `register
|
|
29
|
+
* extend it with `register` (one OID) or `registerFamily` (a whole arc).
|
|
24
30
|
*
|
|
25
31
|
* @card
|
|
26
|
-
* Two-way OID ↔ name registry with arc conversion, seeded
|
|
27
|
-
* RFC 5280 and NIST post-quantum object identifiers.
|
|
32
|
+
* Two-way OID ↔ name registry with arc conversion, seeded by family from
|
|
33
|
+
* the RFC 5280 and NIST post-quantum object identifiers.
|
|
28
34
|
*/
|
|
29
35
|
|
|
30
36
|
var asn1 = require("./asn1-der");
|
|
@@ -32,77 +38,64 @@ var frameworkError = require("./framework-error");
|
|
|
32
38
|
|
|
33
39
|
var OidError = frameworkError.OidError;
|
|
34
40
|
|
|
35
|
-
//
|
|
36
|
-
//
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
[
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
[
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
[
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
[
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
[
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
[
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
[
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
[
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
[
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
[
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
// FIPS 204 ML-DSA (signature-algorithm arc 2.16.840.1.101.3.4.3.*).
|
|
94
|
-
["2.16.840.1.101.3.4.3.17", "id-ml-dsa-44"],
|
|
95
|
-
["2.16.840.1.101.3.4.3.18", "id-ml-dsa-65"],
|
|
96
|
-
["2.16.840.1.101.3.4.3.19", "id-ml-dsa-87"],
|
|
97
|
-
// FIPS 203 ML-KEM (KEM arc 2.16.840.1.101.3.4.4.*).
|
|
98
|
-
["2.16.840.1.101.3.4.4.1", "id-ml-kem-512"],
|
|
99
|
-
["2.16.840.1.101.3.4.4.2", "id-ml-kem-768"],
|
|
100
|
-
["2.16.840.1.101.3.4.4.3", "id-ml-kem-1024"],
|
|
101
|
-
// FIPS 205 SLH-DSA (a representative slice of the SHAKE-256 family).
|
|
102
|
-
["2.16.840.1.101.3.4.3.20", "id-slh-dsa-sha2-128s"],
|
|
103
|
-
["2.16.840.1.101.3.4.3.24", "id-slh-dsa-shake-128s"],
|
|
104
|
-
["2.16.840.1.101.3.4.3.27", "id-slh-dsa-shake-256s"],
|
|
105
|
-
];
|
|
41
|
+
// FAMILIES — OIDs grouped by their shared base arc (the "similar starting
|
|
42
|
+
// variable" that defines a class). A member is `name: leaf`, where leaf is a
|
|
43
|
+
// trailing arc (number) or a short arc array for a multi-level leaf; the full
|
|
44
|
+
// arc is derived at load via base.concat(leaf). Declaring by family means no
|
|
45
|
+
// dotted-decimal OID literal appears in this source at all, and adding a
|
|
46
|
+
// member is one `name: leaf` line under its class — no base to re-type.
|
|
47
|
+
var FAMILIES = {
|
|
48
|
+
// RFC 5280 attribute types.
|
|
49
|
+
attributeType: { base: [2, 5, 4], of: {
|
|
50
|
+
commonName: 3, surname: 4, serialNumber: 5, countryName: 6,
|
|
51
|
+
localityName: 7, stateOrProvinceName: 8, streetAddress: 9,
|
|
52
|
+
organizationName: 10, organizationalUnitName: 11, title: 12, givenName: 42 } },
|
|
53
|
+
|
|
54
|
+
// RFC 5280 certificate extensions.
|
|
55
|
+
certExtension: { base: [2, 5, 29], of: {
|
|
56
|
+
subjectKeyIdentifier: 14, keyUsage: 15, subjectAltName: 17, issuerAltName: 18,
|
|
57
|
+
basicConstraints: 19, nameConstraints: 30, cRLDistributionPoints: 31,
|
|
58
|
+
certificatePolicies: 32, authorityKeyIdentifier: 35, extKeyUsage: 37 } },
|
|
59
|
+
|
|
60
|
+
// PKIX private extensions (authorityInfoAccess et al).
|
|
61
|
+
pkixAccess: { base: [1, 3, 6, 1, 5, 5, 7, 1], of: { authorityInfoAccess: 1 } },
|
|
62
|
+
|
|
63
|
+
// PKCS#1 RSA public-key + RSASSA signature algorithms.
|
|
64
|
+
rsa: { base: [1, 2, 840, 113549, 1, 1], of: {
|
|
65
|
+
rsaEncryption: 1, rsassaPss: 10, sha256WithRSAEncryption: 11,
|
|
66
|
+
sha384WithRSAEncryption: 12, sha512WithRSAEncryption: 13 } },
|
|
67
|
+
|
|
68
|
+
// PKCS#9 attribute types.
|
|
69
|
+
pkcs9: { base: [1, 2, 840, 113549, 1, 9], of: { emailAddress: 1 } },
|
|
70
|
+
|
|
71
|
+
// ANSI X9.62 EC public key, named curve, and ECDSA signatures.
|
|
72
|
+
ansiX962: { base: [1, 2, 840, 10045], of: {
|
|
73
|
+
ecPublicKey: [2, 1], prime256v1: [3, 1, 7],
|
|
74
|
+
ecdsaWithSHA256: [4, 3, 2], ecdsaWithSHA384: [4, 3, 3], ecdsaWithSHA512: [4, 3, 4] } },
|
|
75
|
+
|
|
76
|
+
// SECG named curves.
|
|
77
|
+
secg: { base: [1, 3, 132, 0], of: { secp384r1: 34, secp521r1: 35 } },
|
|
78
|
+
|
|
79
|
+
// Edwards / Montgomery curves (RFC 8410).
|
|
80
|
+
edwards: { base: [1, 3, 101], of: { X25519: 110, X448: 111, Ed25519: 112, Ed448: 113 } },
|
|
81
|
+
|
|
82
|
+
// NIST hash functions (SHA-2, SHA-3, SHAKE).
|
|
83
|
+
nistHash: { base: [2, 16, 840, 1, 101, 3, 4, 2], of: {
|
|
84
|
+
sha256: 1, sha384: 2, sha512: 3, "sha3-256": 8, "sha3-512": 10, shake256: 12 } },
|
|
85
|
+
|
|
86
|
+
// NIST signature algorithms — FIPS 204 ML-DSA + FIPS 205 SLH-DSA share the
|
|
87
|
+
// signature arc 2.16.840.1.101.3.4.3.
|
|
88
|
+
nistSig: { base: [2, 16, 840, 1, 101, 3, 4, 3], of: {
|
|
89
|
+
"id-ml-dsa-44": 17, "id-ml-dsa-65": 18, "id-ml-dsa-87": 19,
|
|
90
|
+
"id-slh-dsa-sha2-128s": 20, "id-slh-dsa-shake-128s": 24, "id-slh-dsa-shake-256s": 27 } },
|
|
91
|
+
|
|
92
|
+
// NIST KEM — FIPS 203 ML-KEM (arc 2.16.840.1.101.3.4.4).
|
|
93
|
+
nistKem: { base: [2, 16, 840, 1, 101, 3, 4, 4], of: {
|
|
94
|
+
"id-ml-kem-512": 1, "id-ml-kem-768": 2, "id-ml-kem-1024": 3 } },
|
|
95
|
+
|
|
96
|
+
// Misc datatypes (RFC 4519 domainComponent).
|
|
97
|
+
datatype: { base: [0, 9, 2342, 19200300, 100, 1], of: { domainComponent: 25 } },
|
|
98
|
+
};
|
|
106
99
|
|
|
107
100
|
var _byOid = new Map();
|
|
108
101
|
var _byName = new Map();
|
|
@@ -113,7 +106,20 @@ function _index(dotted, name) {
|
|
|
113
106
|
if (!_byName.has(name)) _byName.set(name, dotted);
|
|
114
107
|
}
|
|
115
108
|
|
|
116
|
-
|
|
109
|
+
// An arc is a non-negative integer. Large arcs (a 128-bit UUID-based arc
|
|
110
|
+
// exceeds 2^53) must be BigInt to survive without precision loss, so both
|
|
111
|
+
// a non-negative safe-integer Number and a non-negative BigInt are valid.
|
|
112
|
+
function _isArc(a) {
|
|
113
|
+
if (typeof a === "bigint") return a >= 0n;
|
|
114
|
+
return typeof a === "number" && Number.isSafeInteger(a) && a >= 0;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Seed the registry family by family — the built-in set is registered through
|
|
118
|
+
// the exact same primitive an operator uses (registerFamily), so there is one
|
|
119
|
+
// path from a declared family to indexed OIDs and no dotted literals anywhere.
|
|
120
|
+
Object.keys(FAMILIES).forEach(function (fam) {
|
|
121
|
+
registerFamily(FAMILIES[fam].base, FAMILIES[fam].of);
|
|
122
|
+
});
|
|
117
123
|
|
|
118
124
|
function _assertDotted(dotted, who) {
|
|
119
125
|
if (typeof dotted !== "string" || !/^\d+(\.\d+)+$/.test(dotted)) {
|
|
@@ -126,6 +132,7 @@ function _assertDotted(dotted, who) {
|
|
|
126
132
|
* @signature pki.oid.name(dotted) -> string | undefined
|
|
127
133
|
* @since 0.1.0
|
|
128
134
|
* @status stable
|
|
135
|
+
* @spec X.660, RFC 5280
|
|
129
136
|
* @related pki.oid.byName, pki.oid.register
|
|
130
137
|
*
|
|
131
138
|
* Resolve a dotted OID to its registered name. Returns `undefined` for an
|
|
@@ -155,7 +162,8 @@ function has(dotted) {
|
|
|
155
162
|
* @signature pki.oid.register(dotted, name) -> void
|
|
156
163
|
* @since 0.1.0
|
|
157
164
|
* @status stable
|
|
158
|
-
* @
|
|
165
|
+
* @spec X.660, RFC 5280
|
|
166
|
+
* @related pki.oid.registerFamily, pki.oid.name
|
|
159
167
|
*
|
|
160
168
|
* Add (or override) an OID → name mapping so an operator's private or
|
|
161
169
|
* newly-standardized arc resolves through the same registry as the seed
|
|
@@ -171,6 +179,51 @@ function register(dotted, n) {
|
|
|
171
179
|
_index(dotted, n);
|
|
172
180
|
}
|
|
173
181
|
|
|
182
|
+
/**
|
|
183
|
+
* @primitive pki.oid.registerFamily
|
|
184
|
+
* @signature pki.oid.registerFamily(base, members) -> void
|
|
185
|
+
* @since 0.1.2
|
|
186
|
+
* @status stable
|
|
187
|
+
* @spec X.660
|
|
188
|
+
* @related pki.oid.register, pki.oid.name
|
|
189
|
+
*
|
|
190
|
+
* Register a whole OID family in one call. `base` is the shared arc prefix
|
|
191
|
+
* (the starting variable a class of OIDs has in common) and `members` maps
|
|
192
|
+
* each name to its trailing arc — a number, or a short arc array for a
|
|
193
|
+
* multi-level leaf. Each full OID is derived as `base` followed by the leaf,
|
|
194
|
+
* so a family is declared as its hierarchy rather than as re-spelled full
|
|
195
|
+
* paths. This is the primitive the built-in seed set itself is built from.
|
|
196
|
+
*
|
|
197
|
+
* @opts
|
|
198
|
+
* base: number[], // the shared arc prefix, e.g. [1,3,6,1,4,1,99999]
|
|
199
|
+
* members: object, // name -> number | number[] trailing arc
|
|
200
|
+
*
|
|
201
|
+
* @example
|
|
202
|
+
* pki.oid.registerFamily([1, 3, 6, 1, 4, 1, 99999], {
|
|
203
|
+
* widgetPolicy: 1,
|
|
204
|
+
* gadgetPolicy: [2, 4],
|
|
205
|
+
* });
|
|
206
|
+
* pki.oid.name("1.3.6.1.4.1.99999.2.4"); // -> "gadgetPolicy"
|
|
207
|
+
*/
|
|
208
|
+
function registerFamily(base, members) {
|
|
209
|
+
if (!Array.isArray(base) || base.length < 1 || !base.every(_isArc)) {
|
|
210
|
+
throw new OidError("oid/bad-input", "registerFamily: base must be a non-empty array of non-negative integer arcs");
|
|
211
|
+
}
|
|
212
|
+
if (!members || typeof members !== "object") {
|
|
213
|
+
throw new OidError("oid/bad-input", "registerFamily: members must be an object of name -> leaf arc(s)");
|
|
214
|
+
}
|
|
215
|
+
Object.keys(members).forEach(function (nm) {
|
|
216
|
+
if (typeof nm !== "string" || nm.length === 0) {
|
|
217
|
+
throw new OidError("oid/bad-input", "registerFamily: member names must be non-empty strings");
|
|
218
|
+
}
|
|
219
|
+
var arcs = base.concat(members[nm]);
|
|
220
|
+
if (!arcs.every(_isArc)) {
|
|
221
|
+
throw new OidError("oid/bad-arc", "registerFamily: member " + JSON.stringify(nm) + " has a non-arc leaf");
|
|
222
|
+
}
|
|
223
|
+
_index(arcs.join("."), nm);
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
|
|
174
227
|
function all() {
|
|
175
228
|
var out = {};
|
|
176
229
|
_byOid.forEach(function (v, k) { out[k] = v; });
|
|
@@ -191,8 +244,11 @@ function toArcs(dotted) {
|
|
|
191
244
|
function fromArcs(arcs) {
|
|
192
245
|
if (!Array.isArray(arcs) || arcs.length < 2) throw new OidError("oid/bad-input", "fromArcs: expected an array of >= 2 arcs");
|
|
193
246
|
return arcs.map(function (a) {
|
|
194
|
-
if (typeof a === "bigint")
|
|
195
|
-
|
|
247
|
+
if (typeof a === "bigint") {
|
|
248
|
+
if (a < 0n) throw new OidError("oid/bad-arc", "fromArcs: arc " + String(a) + " is not a non-negative integer");
|
|
249
|
+
return a.toString();
|
|
250
|
+
}
|
|
251
|
+
if (typeof a === "number" && Number.isSafeInteger(a) && a >= 0) return String(a);
|
|
196
252
|
throw new OidError("oid/bad-arc", "fromArcs: arc " + String(a) + " is not a non-negative integer");
|
|
197
253
|
}).join(".");
|
|
198
254
|
}
|
|
@@ -207,13 +263,14 @@ function fromDER(input) {
|
|
|
207
263
|
}
|
|
208
264
|
|
|
209
265
|
module.exports = {
|
|
210
|
-
name:
|
|
211
|
-
byName:
|
|
212
|
-
has:
|
|
213
|
-
register:
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
266
|
+
name: name,
|
|
267
|
+
byName: byName,
|
|
268
|
+
has: has,
|
|
269
|
+
register: register,
|
|
270
|
+
registerFamily: registerFamily,
|
|
271
|
+
all: all,
|
|
272
|
+
toArcs: toArcs,
|
|
273
|
+
fromArcs: fromArcs,
|
|
274
|
+
toDER: toDER,
|
|
275
|
+
fromDER: fromDER,
|
|
219
276
|
};
|
package/lib/webcrypto.js
CHANGED
|
@@ -111,6 +111,7 @@ var SLH_DSA_NODE = {};
|
|
|
111
111
|
* @signature new pki.webcrypto.CryptoKey(type, extractable, algorithm, usages, handle)
|
|
112
112
|
* @since 0.1.0
|
|
113
113
|
* @status stable
|
|
114
|
+
* @spec W3C WebCrypto §cryptokey
|
|
114
115
|
*
|
|
115
116
|
* Opaque handle to key material, matching the W3C `CryptoKey` shape:
|
|
116
117
|
* `{ type, extractable, algorithm, usages }`. The underlying
|
|
@@ -266,7 +267,12 @@ SubtleCrypto.prototype.verify = async function verify(algorithm, key, signature,
|
|
|
266
267
|
if (name === "HMAC") {
|
|
267
268
|
var hm = nodeCrypto.createHmac(_hashNode(key.algorithm.hash, "verify"), key._handle);
|
|
268
269
|
hm.update(buf);
|
|
269
|
-
|
|
270
|
+
// Verify must RESOLVE false for any invalid signature, including one of
|
|
271
|
+
// the wrong length. timingSafeEqual throws RangeError on a length
|
|
272
|
+
// mismatch, so gate on length first; the length is already public, so
|
|
273
|
+
// this leaks nothing a constant-time compare would protect.
|
|
274
|
+
var mac = hm.digest();
|
|
275
|
+
return mac.length === sig.length && nodeCrypto.timingSafeEqual(mac, sig);
|
|
270
276
|
}
|
|
271
277
|
throw new WebCryptoError("webcrypto/not-supported", "verify: unsupported algorithm " + JSON.stringify(name));
|
|
272
278
|
};
|
|
@@ -295,6 +301,7 @@ SubtleCrypto.prototype.encrypt = async function encrypt(algorithm, key, data) {
|
|
|
295
301
|
return _toArrayBuffer(Buffer.concat([c2.update(buf), c2.final()]));
|
|
296
302
|
}
|
|
297
303
|
if (name === "AES-CTR") {
|
|
304
|
+
_requireCtrLength128(alg);
|
|
298
305
|
var c3 = nodeCrypto.createCipheriv("aes-" + key.algorithm.length + "-ctr", _secretBytes(key), _toBuf(alg.counter, "AES-CTR counter"));
|
|
299
306
|
return _toArrayBuffer(Buffer.concat([c3.update(buf), c3.final()]));
|
|
300
307
|
}
|
|
@@ -328,6 +335,7 @@ SubtleCrypto.prototype.decrypt = async function decrypt(algorithm, key, data) {
|
|
|
328
335
|
return _toArrayBuffer(Buffer.concat([d2.update(buf), d2.final()]));
|
|
329
336
|
}
|
|
330
337
|
if (name === "AES-CTR") {
|
|
338
|
+
_requireCtrLength128(alg);
|
|
331
339
|
var d3 = nodeCrypto.createDecipheriv("aes-" + key.algorithm.length + "-ctr", _secretBytes(key), _toBuf(alg.counter, "AES-CTR counter"));
|
|
332
340
|
return _toArrayBuffer(Buffer.concat([d3.update(buf), d3.final()]));
|
|
333
341
|
}
|
|
@@ -336,9 +344,21 @@ SubtleCrypto.prototype.decrypt = async function decrypt(algorithm, key, data) {
|
|
|
336
344
|
|
|
337
345
|
function _secretBytes(key) { return key._handle.export(); }
|
|
338
346
|
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
347
|
+
// AES-CTR: node always treats the full 128-bit block as the counter and
|
|
348
|
+
// never reads the spec's `length` (counter-width) parameter. A length < 128
|
|
349
|
+
// would silently diverge from the W3C definition, so fail closed — accept
|
|
350
|
+
// only the one value node can honor.
|
|
351
|
+
function _requireCtrLength128(alg) {
|
|
352
|
+
if (alg.length !== 128) {
|
|
353
|
+
throw new WebCryptoError("webcrypto/not-supported", "AES-CTR length must be 128");
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// Raw key-agreement / KDF derivation with NO usage check — the usage a
|
|
358
|
+
// caller must hold differs by entry point (deriveBits requires "deriveBits",
|
|
359
|
+
// deriveKey requires "deriveKey"), so each public method checks its own
|
|
360
|
+
// usage and then routes the actual derivation through here.
|
|
361
|
+
function _deriveBitsRaw(alg, key, length) {
|
|
342
362
|
var name = alg.name;
|
|
343
363
|
if (name === "ECDH" || name === "X25519" || name === "X448") {
|
|
344
364
|
var secret = nodeCrypto.diffieHellman({ privateKey: key._handle, publicKey: alg.public._handle });
|
|
@@ -353,12 +373,24 @@ SubtleCrypto.prototype.deriveBits = async function deriveBits(algorithm, key, le
|
|
|
353
373
|
return _toArrayBuffer(out);
|
|
354
374
|
}
|
|
355
375
|
throw new WebCryptoError("webcrypto/not-supported", "deriveBits: unsupported algorithm " + JSON.stringify(name));
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
SubtleCrypto.prototype.deriveBits = async function deriveBits(algorithm, key, length) {
|
|
379
|
+
var alg = _normalizeAlg(algorithm, "deriveBits");
|
|
380
|
+
_requireUsage(key, "deriveBits");
|
|
381
|
+
return _deriveBitsRaw(alg, key, length);
|
|
356
382
|
};
|
|
357
383
|
|
|
358
384
|
SubtleCrypto.prototype.deriveKey = async function deriveKey(algorithm, baseKey, derivedKeyType, extractable, keyUsages) {
|
|
385
|
+
// deriveKey requires the "deriveKey" usage — NOT "deriveBits". Delegating
|
|
386
|
+
// to this.deriveBits would false-reject a key created with ["deriveKey"]
|
|
387
|
+
// and fail-open on a ["deriveBits"]-only key, so check the correct usage
|
|
388
|
+
// here and route the raw derivation past deriveBits' own usage gate.
|
|
389
|
+
_requireUsage(baseKey, "deriveKey");
|
|
390
|
+
var alg = _normalizeAlg(algorithm, "deriveKey");
|
|
359
391
|
var dk = _normalizeAlg(derivedKeyType, "deriveKey");
|
|
360
392
|
var bits = dk.length || (dk.name.indexOf("AES") === 0 ? dk.length : 256);
|
|
361
|
-
var raw =
|
|
393
|
+
var raw = _deriveBitsRaw(alg, baseKey, bits);
|
|
362
394
|
return this.importKey("raw", raw, dk, extractable, keyUsages);
|
|
363
395
|
};
|
|
364
396
|
|
|
@@ -378,9 +410,14 @@ SubtleCrypto.prototype.wrapKey = async function wrapKey(format, key, wrappingKey
|
|
|
378
410
|
|
|
379
411
|
SubtleCrypto.prototype.unwrapKey = async function unwrapKey(format, wrappedKey, unwrappingKey, unwrapAlgorithm, unwrappedKeyAlgorithm, extractable, keyUsages) {
|
|
380
412
|
var alg = _normalizeAlg(unwrapAlgorithm, "unwrapKey");
|
|
413
|
+
// Enforce the "unwrapKey" usage before EITHER path. The delegated
|
|
414
|
+
// (RSA-OAEP / AES-GCM) branch clones the key with "decrypt" and hands off
|
|
415
|
+
// to this.decrypt, so without this top-level check the else branch would
|
|
416
|
+
// never verify the caller was actually permitted to unwrap (mirrors
|
|
417
|
+
// wrapKey, which checks before both of its paths).
|
|
418
|
+
_requireUsage(unwrappingKey, "unwrapKey");
|
|
381
419
|
var bytes;
|
|
382
420
|
if (alg.name === "AES-KW") {
|
|
383
|
-
_requireUsage(unwrappingKey, "unwrapKey");
|
|
384
421
|
var d = nodeCrypto.createDecipheriv("aes" + unwrappingKey.algorithm.length + "-wrap", _secretBytes(unwrappingKey), Buffer.from("A6A6A6A6A6A6A6A6", "hex"));
|
|
385
422
|
bytes = Buffer.concat([d.update(_toBuf(wrappedKey, "unwrapKey")), d.final()]);
|
|
386
423
|
} else {
|
|
@@ -453,7 +490,23 @@ function _importRawPublic(name, alg, raw, extractable, usages) {
|
|
|
453
490
|
}
|
|
454
491
|
|
|
455
492
|
function _algFromImport(name, alg, keyObject) {
|
|
456
|
-
if (name === "ECDSA" || name === "ECDH")
|
|
493
|
+
if (name === "ECDSA" || name === "ECDH") {
|
|
494
|
+
// W3C WebCrypto EC import — the curve is a property of the KEY, not the
|
|
495
|
+
// caller's claim. Derive it from the imported key material; reject an
|
|
496
|
+
// unsupported curve (NotSupportedError, matching generateKey) and reject a
|
|
497
|
+
// requested namedCurve that disagrees with the key (DataError). Trusting
|
|
498
|
+
// alg.namedCurve would mislabel the CryptoKey (algorithm confusion) and let
|
|
499
|
+
// a non-approved curve import as an approved one.
|
|
500
|
+
var actualCurve = _curveFromKey(keyObject);
|
|
501
|
+
if (!actualCurve) {
|
|
502
|
+
throw new WebCryptoError("webcrypto/not-supported", name + ": imported key uses an unsupported EC curve");
|
|
503
|
+
}
|
|
504
|
+
if (alg.namedCurve && alg.namedCurve !== actualCurve) {
|
|
505
|
+
throw new WebCryptoError("webcrypto/data", name + ": importKey namedCurve " + JSON.stringify(alg.namedCurve) +
|
|
506
|
+
" does not match the imported key's curve " + JSON.stringify(actualCurve));
|
|
507
|
+
}
|
|
508
|
+
return { name: name, namedCurve: actualCurve };
|
|
509
|
+
}
|
|
457
510
|
if (name === "RSASSA-PKCS1-V1_5" || name === "RSA-PSS" || name === "RSA-OAEP") return { name: name, hash: _hashObj(alg.hash) };
|
|
458
511
|
if (name === "ED25519") return { name: "Ed25519" };
|
|
459
512
|
if (name === "ED448") return { name: "Ed448" };
|
|
@@ -470,6 +523,7 @@ function _curveFromKey(ko) {
|
|
|
470
523
|
* @signature await pki.webcrypto.subtle.exportKey(format, key)
|
|
471
524
|
* @since 0.1.0
|
|
472
525
|
* @status stable
|
|
526
|
+
* @spec W3C WebCrypto §subtlecrypto, FIPS 186-5, FIPS 203, FIPS 204, FIPS 205, RFC 8017
|
|
473
527
|
* @related pki.webcrypto.CryptoKey
|
|
474
528
|
*
|
|
475
529
|
* Export a `CryptoKey` to `spki` (public), `pkcs8` (private), `jwk`
|
|
@@ -507,6 +561,7 @@ function _rawPublic(key) {
|
|
|
507
561
|
* @signature pki.webcrypto.getRandomValues(typedArray) / pki.webcrypto.subtle
|
|
508
562
|
* @since 0.1.0
|
|
509
563
|
* @status stable
|
|
564
|
+
* @spec W3C WebCrypto
|
|
510
565
|
* @related pki.webcrypto.subtle
|
|
511
566
|
*
|
|
512
567
|
* A ready `Crypto` instance (the shape of `globalThis.crypto`) exposing
|
package/lib/x509.js
CHANGED
|
@@ -64,6 +64,7 @@ var PEM_RE = /-----BEGIN ([A-Z0-9 ]+)-----([\s\S]*?)-----END \1-----/;
|
|
|
64
64
|
* @signature pki.x509.pemDecode(text, label?) -> Buffer
|
|
65
65
|
* @since 0.1.0
|
|
66
66
|
* @status stable
|
|
67
|
+
* @spec RFC 7468, RFC 5280
|
|
67
68
|
* @related pki.x509.pemEncode
|
|
68
69
|
*
|
|
69
70
|
* Extract the DER bytes from a PEM block. With `label` given (e.g.
|
|
@@ -91,6 +92,7 @@ function pemDecode(text, label) {
|
|
|
91
92
|
* @signature pki.x509.pemEncode(der, label) -> string
|
|
92
93
|
* @since 0.1.0
|
|
93
94
|
* @status stable
|
|
95
|
+
* @spec RFC 7468, RFC 5280
|
|
94
96
|
* @related pki.x509.pemDecode
|
|
95
97
|
*
|
|
96
98
|
* Wrap DER bytes in a PEM envelope with 64-column base64 lines.
|
|
@@ -121,10 +123,25 @@ function _algId(node) {
|
|
|
121
123
|
|
|
122
124
|
function _attrValueToString(node) {
|
|
123
125
|
try { return asn1.read.string(node); }
|
|
124
|
-
catch (
|
|
125
|
-
//
|
|
126
|
-
//
|
|
127
|
-
|
|
126
|
+
catch (e) {
|
|
127
|
+
// A malformed KNOWN string type (invalid UTF-8, a non-IA5 byte, a
|
|
128
|
+
// PrintableString character outside its set, ...) is a malformed
|
|
129
|
+
// certificate and must fail closed — do NOT hex-encode it away, or the
|
|
130
|
+
// decoder's strict string validation is silently bypassed on the DN path.
|
|
131
|
+
// Those surface as asn1/bad-* content errors; raise a CertificateError so
|
|
132
|
+
// parse fails closed on the documented x509/* contract.
|
|
133
|
+
//
|
|
134
|
+
// A value that simply is not a decodable primitive string is NOT malformed
|
|
135
|
+
// and must stay representable: an ANY-typed non-string tag
|
|
136
|
+
// (asn1/expected-string) or a constructed universal type such as a SEQUENCE
|
|
137
|
+
// (asn1/expected-primitive). Per RFC 4514 §2.4 it is rendered as "#" plus
|
|
138
|
+
// the hex of its FULL DER encoding (tag + length + content — node.bytes),
|
|
139
|
+
// so a constructed value round-trips intact rather than being dropped or
|
|
140
|
+
// rejecting the whole certificate.
|
|
141
|
+
if (!e || (e.code !== "asn1/expected-string" && e.code !== "asn1/expected-primitive")) {
|
|
142
|
+
throw new CertificateError("x509/bad-atv", "malformed string in attribute value: " + ((e && e.message) || String(e)));
|
|
143
|
+
}
|
|
144
|
+
return "#" + node.bytes.toString("hex");
|
|
128
145
|
}
|
|
129
146
|
}
|
|
130
147
|
|
|
@@ -171,13 +188,30 @@ function _parseValidityTime(node) {
|
|
|
171
188
|
}
|
|
172
189
|
|
|
173
190
|
function _parseExtensions(seqNode) {
|
|
191
|
+
// RFC 5280 §4.1.2.9 — Extensions ::= SEQUENCE SIZE (1..MAX) OF Extension.
|
|
192
|
+
// The wrapped value must be a SEQUENCE (not a primitive, which would read
|
|
193
|
+
// seqNode.children as null and throw a raw TypeError) and must carry at
|
|
194
|
+
// least one extension.
|
|
195
|
+
if (seqNode.tagClass !== "universal" || seqNode.tagNumber !== TAGS.SEQUENCE || !seqNode.children) {
|
|
196
|
+
throw new CertificateError("x509/bad-extensions", "extensions [3] must wrap a SEQUENCE");
|
|
197
|
+
}
|
|
198
|
+
if (seqNode.children.length === 0) {
|
|
199
|
+
throw new CertificateError("x509/bad-extensions", "extensions SEQUENCE must contain at least one extension");
|
|
200
|
+
}
|
|
174
201
|
var out = [];
|
|
202
|
+
var seen = new Set();
|
|
175
203
|
for (var i = 0; i < seqNode.children.length; i++) {
|
|
176
204
|
var ext = seqNode.children[i];
|
|
177
205
|
if (!ext.children || ext.children.length < 2) {
|
|
178
206
|
throw new CertificateError("x509/bad-extension", "Extension must be a SEQUENCE");
|
|
179
207
|
}
|
|
180
208
|
var extnID = asn1.read.oid(ext.children[0]);
|
|
209
|
+
// RFC 5280 §4.2 — a certificate MUST NOT include more than one
|
|
210
|
+
// instance of a particular extension.
|
|
211
|
+
if (seen.has(extnID)) {
|
|
212
|
+
throw new CertificateError("x509/duplicate-extension", "certificate repeats extension " + extnID);
|
|
213
|
+
}
|
|
214
|
+
seen.add(extnID);
|
|
181
215
|
var critical = false;
|
|
182
216
|
var valueNode;
|
|
183
217
|
if (ext.children.length === 3) {
|
|
@@ -203,6 +237,8 @@ function _parseExtensions(seqNode) {
|
|
|
203
237
|
* @signature pki.x509.parse(input) -> certificate
|
|
204
238
|
* @since 0.1.0
|
|
205
239
|
* @status stable
|
|
240
|
+
* @spec RFC 5280, X.509
|
|
241
|
+
* @defends malformed-certificate-parse (CWE-20)
|
|
206
242
|
* @related pki.asn1.decode, pki.oid.name
|
|
207
243
|
*
|
|
208
244
|
* Parse a DER `Buffer` or a PEM string/Buffer into a structured
|
|
@@ -245,17 +281,37 @@ function parse(input) {
|
|
|
245
281
|
var tbs = root.children[0];
|
|
246
282
|
var outerSigAlg = root.children[1];
|
|
247
283
|
var sigValueNode = root.children[2];
|
|
248
|
-
if (!tbs.children || tbs.children.length
|
|
249
|
-
throw new CertificateError("x509/bad-tbs", "tbsCertificate is
|
|
284
|
+
if (!tbs.children || !tbs.children.length) {
|
|
285
|
+
throw new CertificateError("x509/bad-tbs", "tbsCertificate is empty");
|
|
250
286
|
}
|
|
251
287
|
|
|
252
288
|
var idx = 0;
|
|
253
|
-
var version
|
|
289
|
+
var version;
|
|
254
290
|
var first = tbs.children[0];
|
|
255
291
|
if (first.tagClass === "context" && first.tagNumber === 0) {
|
|
256
292
|
if (!first.children || !first.children.length) throw new CertificateError("x509/bad-version", "version [0] must wrap an INTEGER");
|
|
257
|
-
|
|
293
|
+
// RFC 5280 §4.1.2.1 — version is INTEGER { v1(0), v2(1), v3(2) }; read
|
|
294
|
+
// it as a BigInt so an out-of-range value can't be coerced to a float,
|
|
295
|
+
// and reject an explicitly-encoded v1 (DER forbids encoding the DEFAULT).
|
|
296
|
+
var versionValue = asn1.read.integer(first.children[0]);
|
|
297
|
+
if (versionValue === 0n) {
|
|
298
|
+
throw new CertificateError("x509/bad-version", "DER forbids explicitly encoding the default version v1");
|
|
299
|
+
} else if (versionValue === 1n) {
|
|
300
|
+
version = 2;
|
|
301
|
+
} else if (versionValue === 2n) {
|
|
302
|
+
version = 3;
|
|
303
|
+
} else {
|
|
304
|
+
throw new CertificateError("x509/bad-version", "unsupported certificate version " + versionValue.toString());
|
|
305
|
+
}
|
|
258
306
|
idx = 1;
|
|
307
|
+
} else {
|
|
308
|
+
version = 1;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// Six positional fields follow any [0] version; a shorter tbs would read
|
|
312
|
+
// `undefined` and throw a raw TypeError on the first property access.
|
|
313
|
+
if (tbs.children.length < idx + 6) {
|
|
314
|
+
throw new CertificateError("x509/bad-tbs", "tbsCertificate is too short");
|
|
259
315
|
}
|
|
260
316
|
|
|
261
317
|
var serialNode = tbs.children[idx++];
|
|
@@ -266,6 +322,21 @@ function parse(input) {
|
|
|
266
322
|
var subject = _parseName(tbs.children[idx++]);
|
|
267
323
|
var spkiNode = tbs.children[idx++];
|
|
268
324
|
|
|
325
|
+
// RFC 5280 §4.1.1.2 — the outer signatureAlgorithm MUST contain the same
|
|
326
|
+
// AlgorithmIdentifier (OID and parameters) as tbsCertificate.signature. A
|
|
327
|
+
// mismatch is a signature-algorithm-substitution vector, so compare the full
|
|
328
|
+
// DER of both fields rather than surfacing two disagreeing algorithms.
|
|
329
|
+
if (!outerSigAlg.bytes.equals(innerSigAlg.bytes)) {
|
|
330
|
+
throw new CertificateError("x509/bad-signature-algorithm", "signatureAlgorithm must match tbsCertificate.signature (RFC 5280 §4.1.1.2)");
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// RFC 5280 §4.1.2.4 — the issuer field MUST contain a non-empty distinguished
|
|
334
|
+
// name. (An empty subject is permitted when a subjectAltName carries the
|
|
335
|
+
// identity — §4.1.2.6 — so only the issuer is required non-empty here.)
|
|
336
|
+
if (!issuer.rdns.length) {
|
|
337
|
+
throw new CertificateError("x509/bad-issuer", "issuer must be a non-empty distinguished name");
|
|
338
|
+
}
|
|
339
|
+
|
|
269
340
|
if (!validityNode.children || validityNode.children.length !== 2) {
|
|
270
341
|
throw new CertificateError("x509/bad-validity", "Validity must be a SEQUENCE of {notBefore, notAfter}");
|
|
271
342
|
}
|
|
@@ -285,16 +356,36 @@ function parse(input) {
|
|
|
285
356
|
};
|
|
286
357
|
|
|
287
358
|
// Remaining tbs children: optional issuerUniqueID [1], subjectUniqueID
|
|
288
|
-
// [2], and extensions [3] EXPLICIT. Only extensions are surfaced.
|
|
359
|
+
// [2], and extensions [3] EXPLICIT. Only extensions are surfaced. RFC 5280
|
|
360
|
+
// §4.1 fixes these as the trailing fields, each at most once and in strictly
|
|
361
|
+
// increasing tag order; a repeated or out-of-order tag (or an unknown /
|
|
362
|
+
// non-context field) is malformed. Without the monotonic guard a second [3]
|
|
363
|
+
// silently overwrites the first, hiding its extensions and splitting
|
|
364
|
+
// duplicate extension OIDs across two wrappers past the per-sequence check.
|
|
289
365
|
var extensions = [];
|
|
366
|
+
var hasExtensions = false;
|
|
367
|
+
var lastTrailingTag = 0;
|
|
290
368
|
for (; idx < tbs.children.length; idx++) {
|
|
291
369
|
var t = tbs.children[idx];
|
|
292
|
-
if (t.tagClass
|
|
370
|
+
if (t.tagClass !== "context" || t.tagNumber < 1 || t.tagNumber > 3) {
|
|
371
|
+
throw new CertificateError("x509/bad-tbs", "unexpected field after subjectPublicKeyInfo; tbsCertificate allows only issuerUniqueID [1], subjectUniqueID [2], extensions [3]");
|
|
372
|
+
}
|
|
373
|
+
if (t.tagNumber <= lastTrailingTag) {
|
|
374
|
+
throw new CertificateError("x509/bad-tbs", "tbsCertificate trailing field [" + t.tagNumber + "] is repeated or out of order");
|
|
375
|
+
}
|
|
376
|
+
lastTrailingTag = t.tagNumber;
|
|
377
|
+
if (t.tagNumber === 3) {
|
|
378
|
+
hasExtensions = true;
|
|
293
379
|
if (!t.children || !t.children.length) throw new CertificateError("x509/bad-extensions", "extensions [3] must wrap a SEQUENCE");
|
|
294
380
|
extensions = _parseExtensions(t.children[0]);
|
|
295
381
|
}
|
|
296
382
|
}
|
|
297
383
|
|
|
384
|
+
// RFC 5280 §4.1.2.9 — the extensions field appears only in a v3 cert.
|
|
385
|
+
if (hasExtensions && version !== 3) {
|
|
386
|
+
throw new CertificateError("x509/bad-version", "extensions are only permitted in a v3 certificate");
|
|
387
|
+
}
|
|
388
|
+
|
|
298
389
|
var sigBits = asn1.read.bitString(sigValueNode);
|
|
299
390
|
|
|
300
391
|
return {
|
package/package.json
CHANGED
package/sbom.cdx.json
CHANGED
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
"$schema": "http://cyclonedx.org/schema/bom-1.5.schema.json",
|
|
3
3
|
"bomFormat": "CycloneDX",
|
|
4
4
|
"specVersion": "1.5",
|
|
5
|
-
"serialNumber": "urn:uuid:
|
|
5
|
+
"serialNumber": "urn:uuid:2e1fe0ea-94e8-4271-ae62-9f7de0920674",
|
|
6
6
|
"version": 1,
|
|
7
7
|
"metadata": {
|
|
8
|
-
"timestamp": "2026-07-
|
|
8
|
+
"timestamp": "2026-07-04T19:30:56.018Z",
|
|
9
9
|
"lifecycles": [
|
|
10
10
|
{
|
|
11
11
|
"phase": "build"
|
|
@@ -19,14 +19,14 @@
|
|
|
19
19
|
}
|
|
20
20
|
],
|
|
21
21
|
"component": {
|
|
22
|
-
"bom-ref": "@blamejs/pki@0.1.
|
|
22
|
+
"bom-ref": "@blamejs/pki@0.1.3",
|
|
23
23
|
"type": "application",
|
|
24
24
|
"name": "pki",
|
|
25
|
-
"version": "0.1.
|
|
25
|
+
"version": "0.1.3",
|
|
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.1.
|
|
29
|
+
"purl": "pkg:npm/%40blamejs/pki@0.1.3",
|
|
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.1.
|
|
57
|
+
"ref": "@blamejs/pki@0.1.3",
|
|
58
58
|
"dependsOn": []
|
|
59
59
|
}
|
|
60
60
|
]
|