@blamejs/pki 0.5.29 → 0.5.31
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 +21 -0
- package/lib/asn1-der.js +4 -0
- package/lib/attrcert-sign.js +1 -1
- package/lib/cmp-session.js +14 -5
- package/lib/crl-sign.js +1 -1
- package/lib/ip-utils.js +102 -25
- package/lib/oid.js +24 -0
- package/lib/pki-build.js +314 -15
- package/lib/sign-scheme.js +26 -6
- package/lib/x509-sign.js +8 -4
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,27 @@ 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.5.31 — 2026-08-25
|
|
8
|
+
|
|
9
|
+
The signing verbs accept a WebCrypto CryptoKey as the signing key, and a subjectAltName entry may be a bare string classified into its GeneralName form.
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- Every signing verb -- `pki.x509.sign`, `pki.csr.sign`, `pki.crl.sign`, `pki.cms.sign`, `pki.ocsp.sign`, `pki.attrcert.sign`, `pki.tsp.sign`, and `pki.crmf.build` -- accepts a WebCrypto `CryptoKey` as its signing key, so a key from `pki.key.generate` (or `pki.webcrypto.subtle`) signs directly without first being exported to PKCS#8. A PKCS#8 private key as DER (a `Buffer` or `Uint8Array`) or PEM (a string) continues to work exactly as before, with the signature algorithm resolved from the key's own SubjectPublicKeyInfo. A `CryptoKey`'s own algorithm must be compatible with that resolved scheme, because WebCrypto binds a key to one algorithm: an RSASSA-PSS `CryptoKey` signs RSASSA-PSS, so pass `opts.pss` for it, whereas a raw PKCS#8 RSA key defaults to RSASSA-PKCS1-v1_5.
|
|
14
|
+
- A `subjectAltName` (and any other `GeneralName`) entry may be a bare string, classified fail-closed into its form: a hostname such as `"host.example.com"` (including a `*.` wildcard or an underscore label) becomes a `dNSName`, `"user@example.com"` an `rfc822Name`, an IPv4 or IPv6 literal such as `"192.0.2.1"` an `iPAddress`, and a `"https://host.example/path"` URL a `uniformResourceIdentifier`. Shorthand and object entries interoperate in the same array. A string that does not unambiguously match one form -- a host:port, an opaque `urn:`/`mailto:` URI, or an empty string -- is refused with a typed `bad-input` error rather than guessed; the explicit object form (`{ dNSName: "..." }`) still accepts any value, and a pre-encoded Extension DER passed through the escape-hatch array form is unaffected.
|
|
15
|
+
|
|
16
|
+
### Changed
|
|
17
|
+
|
|
18
|
+
- A signing key that is a public or secret `CryptoKey`, or a `node:crypto` KeyObject rather than a WebCrypto `CryptoKey`, is now refused with a message that names the specific problem -- the wrong key type, or a KeyObject that should be passed as a `CryptoKey` or exported to PKCS#8 -- instead of the generic "must be a CryptoKey or PKCS#8" message. The `"a signing key is required"` errors on `pki.x509.sign`, `pki.crl.sign`, and `pki.attrcert.sign` now name the `CryptoKey` option alongside PKCS#8.
|
|
19
|
+
|
|
20
|
+
## v0.5.30 — 2026-08-24
|
|
21
|
+
|
|
22
|
+
A CMP certReqTemplate keySpec that names an ISO/IEC 9796-2 RSA signature scheme is now refused, instead of being surfaced to the caller as a non-RSA algorithm requirement.
|
|
23
|
+
|
|
24
|
+
### Security
|
|
25
|
+
|
|
26
|
+
- `pki.cmp.session` refuses a certReqTemplate keySpec whose `id-regCtrl-algId` names an ISO/IEC 9796-2 RSA signature-with-message-recovery scheme, with `cmp/bad-info-value`, rather than surfacing it to the caller as a non-RSA algorithm requirement. RFC 9483 sec. 4.3.3 reserves an RSA key requirement for `id-regCtrl-rsaKeyLen`; the 9796-2 schemes are RSA, so a keySpec naming one is now rejected the same way an rsaEncryption or RSASSA-PSS algId already was. The classifier recognizes the deterministic (1.3.36.3.4.2) and randomized (1.3.36.3.4.3) sub-arcs by prefix -- so a hash variant registered later is caught without being listed -- and ISO/IEC 9796-1 (1.3.36.3.4.1) on the same registrant arc. The generic signatureScheme parent (1.3.36.3.4) and the sibling authentication arc (1.3.36.3.5, whose children include ECC) are not treated as RSA, so a non-RSA algorithm under either is still surfaced. Recognizing these OIDs classifies a keySpec requirement; it is not a claim that the toolkit produces or verifies a 9796-2 signature.
|
|
27
|
+
|
|
7
28
|
## v0.5.29 — 2026-08-24
|
|
8
29
|
|
|
9
30
|
pki.path.validate refuses a mis-shaped trust anchor instead of returning a verdict against it, and accepts a parsed certificate directly.
|
package/lib/asn1-der.js
CHANGED
|
@@ -1041,6 +1041,10 @@ var build = {
|
|
|
1041
1041
|
},
|
|
1042
1042
|
raw: function (buf) { return _asBuffer(buf, "build.raw"); },
|
|
1043
1043
|
};
|
|
1044
|
+
// Frozen at load so a builder method cannot be replaced afterward: every format module captures `build`
|
|
1045
|
+
// and calls its methods to produce emitted DER, so a mutable `build.contextPrimitive` (or any builder)
|
|
1046
|
+
// would let co-resident code steer the bytes a certificate carries. Freezing closes that once, toolkit-wide.
|
|
1047
|
+
intrinsic.freeze(build);
|
|
1044
1048
|
|
|
1045
1049
|
// A raw INTEGER/ENUMERATED content Buffer must satisfy the same DER shape
|
|
1046
1050
|
// read.integer enforces: non-empty and minimally encoded (no redundant sign
|
package/lib/attrcert-sign.js
CHANGED
|
@@ -497,7 +497,7 @@ function _sign(spec, issuer, opts) {
|
|
|
497
497
|
if (!spec || typeof spec !== "object" || Buffer.isBuffer(spec)) throw _err("attrcert/bad-input", "the attribute-certificate spec must be an object");
|
|
498
498
|
guard.identifier.assertKnownKeys(spec, KNOWN_SPEC_KEYS, _err, "attrcert/bad-input", "unknown spec field ");
|
|
499
499
|
issuer = issuer || {};
|
|
500
|
-
if (issuer.key == null) throw _err("attrcert/bad-input", "a signing key (issuer.key, the AA's PKCS#8 private key) is required");
|
|
500
|
+
if (issuer.key == null) throw _err("attrcert/bad-input", "a signing key (issuer.key, the AA's WebCrypto CryptoKey or PKCS#8 private key DER/PEM) is required");
|
|
501
501
|
|
|
502
502
|
// Resolve the AA issuer DN + signing-key SPKI. An attribute certificate is NEVER self-signed. No CA
|
|
503
503
|
// gate on issuer.cert -- an AA is NOT a certificate authority (its PKC MUST NOT assert keyCertSign,
|
package/lib/cmp-session.js
CHANGED
|
@@ -135,10 +135,15 @@ var KNOWN_REVOKE_KEYS = { certificate: 1, certDetails: 1, reason: 1 };
|
|
|
135
135
|
// "RSA" is decided by OID FAMILY, not a hand-list. An RSA-DEDICATED arc -- one whose members are RSA --
|
|
136
136
|
// is matched by its prefix, so a standardized member this registry has not named is caught with the ones
|
|
137
137
|
// it has: PKCS#1 (rsaEncryption, PSS, OAEP, every sha*/md*WithRSAEncryption), the TeleTrusT rsaSignature
|
|
138
|
-
// arc (RSA with SHA-1 and the RIPEMD family),
|
|
139
|
-
//
|
|
140
|
-
//
|
|
141
|
-
//
|
|
138
|
+
// arc (RSA with SHA-1 and the RIPEMD family), the TeleTrusT signatureScheme arc (the ISO/IEC 9796-2 RSA
|
|
139
|
+
// signature-with-message-recovery schemes -- deterministic 1.3.36.3.4.2 and randomized .4.3 -- plus ISO/IEC
|
|
140
|
+
// 9796-1 on .4.1), and the BSI TR-03110 id-TA-RSA arc (EAC Terminal Authentication, RSA v1.5/PSS over
|
|
141
|
+
// SHA-1/256/512). Each prefix is the arc a registered member sits directly under, read from the registry,
|
|
142
|
+
// so no dotted-decimal literal appears here and pki.oid.register cannot move it. The strict-descendant
|
|
143
|
+
// match skips an arc NODE itself, so a scheme's bare arc node (the 9796 1.3.36.3.4.1/.2/.3 nodes) is named
|
|
144
|
+
// off-arc below; the generic signatureScheme PARENT (1.3.36.3.4) and the sibling authentication arc
|
|
145
|
+
// (1.3.36.3.5, whose .5.3 children are ECC) are NOT RSA-dedicated, so neither is prefix-matched. The PKCS#1
|
|
146
|
+
// arc also carries two auxiliary identifiers that are NOT RSA algorithms --
|
|
142
147
|
// mgf1 and pSpecified, the mask-generation and OAEP-label parameters -- so those are excluded from the
|
|
143
148
|
// match by name (RSA_ARC_EXCLUDE) and surfaced. RSA also sits off those arcs on ones it SHARES with non-RSA
|
|
144
149
|
// algorithms, where no prefix can isolate it, so those are listed by name: id-rsa-kem (S/MIME) and
|
|
@@ -151,7 +156,8 @@ var KNOWN_REVOKE_KEYS = { certificate: 1, certDetails: 1, reason: 1 };
|
|
|
151
156
|
// Chinese GM/T sm3WithRSAEncryption (1.2.156.10197.1.504), an RSA-over-SM3 signature on the SM arc it
|
|
152
157
|
// shares with SM2 and SM3. A composite signature naming RSA as one half is a composite key, not an RSA
|
|
153
158
|
// one, and lives on the PKIX arc, so it is outside all of these.
|
|
154
|
-
var RSA_ARCS = ["rsaEncryption", "rsaSignatureWithripemd160", "id-TA-RSA-v1-5-SHA-256"
|
|
159
|
+
var RSA_ARCS = ["rsaEncryption", "rsaSignatureWithripemd160", "id-TA-RSA-v1-5-SHA-256",
|
|
160
|
+
"sigS-ISO9796-1-DFUE", "sigS-ISO9796-2Withrsa", "sigS-ISO9796-2rndWithrsa"].map(function (n) {
|
|
155
161
|
return oid.toArcs(oid.byName(n)).slice(0, -1); // the RSA-dedicated arc a registered member sits directly under
|
|
156
162
|
});
|
|
157
163
|
var RSA_OFF_ARC = Object.create(null);
|
|
@@ -165,6 +171,9 @@ var RSA_OFF_ARC = Object.create(null);
|
|
|
165
171
|
"rsa", // the X.509 directory rsa public-key OID (2.5.8.1.1), distinct from PKCS#1 rsaEncryption
|
|
166
172
|
"sqMod-nWithRSA", "mdc2WithRSA", // the X.500 directory signatureAlgorithm RSA OIDs (2.5.8.3.1, 2.5.8.3.100)
|
|
167
173
|
"sm3WithRSAEncryption", // the Chinese GM/T RSA-with-SM3 signature (1.2.156.10197.1.504)
|
|
174
|
+
// The TeleTrusT signatureScheme (ISO/IEC 9796-2) sub-arcs are prefix-matched in RSA_ARCS, but the strict-
|
|
175
|
+
// descendant match skips each arc NODE itself, so the three bare nodes are named here (1.3.36.3.4.1/.2/.3).
|
|
176
|
+
"sigS-ISO9796-1", "sigS-ISO9796-2", "sigS-ISO9796-2rnd",
|
|
168
177
|
].forEach(function (n) { var d = oid.byName(n); if (d) RSA_OFF_ARC[d] = 1; });
|
|
169
178
|
// The PKCS#1 arc carries two identifiers that are NOT RSA algorithms: mgf1 (the mask-generation function
|
|
170
179
|
// RSA-PSS and RSA-OAEP parametrize) and pSpecified (the OAEP label source). The prefix match would sweep
|
package/lib/crl-sign.js
CHANGED
|
@@ -463,7 +463,7 @@ function _sign(spec, issuer, opts) {
|
|
|
463
463
|
: "explicit form; the issuer takes { key, name, publicKey }, or { key, cert } to take both from a certificate");
|
|
464
464
|
});
|
|
465
465
|
guard.identifier.assertKnownKeys(opts, KNOWN_SIGN_OPTS, _err, "crl/bad-input", "pki.crl.sign has an unknown option ");
|
|
466
|
-
if (issuer.key == null) throw _err("crl/bad-input", "a signing key (issuer.key, a PKCS#8 private key) is required");
|
|
466
|
+
if (issuer.key == null) throw _err("crl/bad-input", "a signing key (issuer.key, a WebCrypto CryptoKey or a PKCS#8 private key DER/PEM) is required");
|
|
467
467
|
|
|
468
468
|
// Resolve the issuer name + signing-key SPKI. `{ cert }` supplies both; else `{ name|spec.issuer, publicKey }`.
|
|
469
469
|
var issuerDer, issuerSpki, issuerCert = null;
|
package/lib/ip-utils.js
CHANGED
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
"use strict";
|
|
4
4
|
//
|
|
5
5
|
// @internal -- no operator-facing namespace. Textual IP-literal validation, shared so a
|
|
6
|
-
// consumer (pki.lint's commonName-in-SAN check today;
|
|
7
|
-
// constraint
|
|
6
|
+
// consumer (pki.lint's commonName-in-SAN check today; the SAN iPAddress GeneralName shorthand
|
|
7
|
+
// and name-constraint surfaces) does not hand-roll a partial regex that misses a valid IPv6 form.
|
|
8
8
|
//
|
|
9
9
|
// The IPv4 grammar is strict per-octet 0-255 (RFC 791); the IPv6 parser follows RFC 4291
|
|
10
10
|
// sec. 2.2 (8 groups of 1-4 hex, one "::" run compressing a contiguous zero run) plus the
|
|
@@ -12,43 +12,116 @@
|
|
|
12
12
|
// This mirrors the vetted validation in the sibling blamejs framework (lib/ip-utils.js);
|
|
13
13
|
// this toolkit keeps its own copy, since Hard rule #1 rules out a runtime dependency.
|
|
14
14
|
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
|
|
18
|
-
|
|
15
|
+
// This parser uses no regular expression. packIpLiteral gates a GeneralName form decision (iPAddress
|
|
16
|
+
// vs dNSName), and a regex is matched through a live, replaceable protocol -- RegExp.prototype.exec /
|
|
17
|
+
// test and String.prototype.match / split consult the regex's own `exec` and the `[Symbol.match]` /
|
|
18
|
+
// `[Symbol.split]` hooks, and RegExp.prototype.compile can mutate a pattern in place -- so co-resident
|
|
19
|
+
// code could steer the decision after load. Instead every byte is read directly through captured string
|
|
20
|
+
// primitives, and separation on a literal is done by scanning with captured indexOf/slice, so nothing the
|
|
21
|
+
// decision rests on is re-dispatchable.
|
|
22
|
+
var intrinsic = require("./guard-intrinsic");
|
|
23
|
+
var _slice = intrinsic.uncurry(String.prototype.slice);
|
|
24
|
+
var _padStart = intrinsic.uncurry(String.prototype.padStart);
|
|
25
|
+
var _charCodeAt = intrinsic.uncurry(String.prototype.charCodeAt);
|
|
26
|
+
var _lastIndexOf = intrinsic.uncurry(String.prototype.lastIndexOf);
|
|
27
|
+
var _toLowerCase = intrinsic.toLowerCase;
|
|
28
|
+
var _indexOf = intrinsic.stringIndexOf;
|
|
29
|
+
var _numToString = intrinsic.numberToString;
|
|
30
|
+
var _bufferFrom = intrinsic.bufferFrom;
|
|
31
|
+
var _bufferAlloc = intrinsic.bufferAlloc;
|
|
32
|
+
var _defineProperty = intrinsic.defineProperty;
|
|
33
|
+
var _Number = intrinsic.Number;
|
|
34
|
+
// Every array here is built and read as own elements only. Appending through a captured Object.defineProperty
|
|
35
|
+
// (not `arr[i] = v`) creates an own element, so a co-resident Array.prototype numeric setter is never invoked
|
|
36
|
+
// and the own element shadows any inherited getter on read; and no Array.prototype.map / concat runs, so
|
|
37
|
+
// Array[Symbol.species] is never consulted. Both are routes a hostile prototype could otherwise use to
|
|
38
|
+
// rewrite a packed octet and make a certificate carry an attacker-chosen address. The emitted octets are
|
|
39
|
+
// written straight into a Buffer, whose integer-indexed store bypasses the prototype entirely.
|
|
40
|
+
function _push(arr, v) { _defineProperty(arr, arr.length, { value: v, writable: true, enumerable: true, configurable: true }); }
|
|
41
|
+
// Split on a literal string separator without String.prototype.split, which consults the separator's
|
|
42
|
+
// `[Symbol.split]`; this scans with captured indexOf/slice only.
|
|
43
|
+
function _splitOn(str, sep) {
|
|
44
|
+
var out = [], from = 0, at;
|
|
45
|
+
while ((at = _indexOf(str, sep, from)) !== -1) { _push(out, _slice(str, from, at)); from = at + sep.length; }
|
|
46
|
+
_push(out, _slice(str, from));
|
|
47
|
+
return out;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function _isDigitCode(c) { return c >= 48 && c <= 57; } // 0-9
|
|
51
|
+
function _isHexCode(c) { return (c >= 48 && c <= 57) || (c >= 97 && c <= 102) || (c >= 65 && c <= 70); }
|
|
52
|
+
|
|
53
|
+
// A strict RFC 791 dotted-quad octet: 1-3 digits, value 0-255, no leading zero beyond "0" itself.
|
|
54
|
+
function _isOctet(p) {
|
|
55
|
+
var n = p.length, i, c;
|
|
56
|
+
if (n < 1 || n > 3) return false;
|
|
57
|
+
for (i = 0; i < n; i += 1) { c = _charCodeAt(p, i); if (!_isDigitCode(c)) return false; }
|
|
58
|
+
if (n > 1 && _charCodeAt(p, 0) === 48) return false; // a leading zero is not the canonical form
|
|
59
|
+
return _Number(p) <= 255;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Strict RFC 791 dotted quad: four 0-255 octets, nothing else.
|
|
63
|
+
function isIPv4(s) {
|
|
64
|
+
if (typeof s !== "string") return false;
|
|
65
|
+
var parts = _splitOn(s, ".");
|
|
66
|
+
if (parts.length !== 4) return false;
|
|
67
|
+
for (var i = 0; i < 4; i += 1) { if (!_isOctet(parts[i])) return false; }
|
|
68
|
+
return true;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// A single 1-4 hex-digit IPv6 group.
|
|
72
|
+
function _isHexGroup(g) {
|
|
73
|
+
var n = g.length, i;
|
|
74
|
+
if (n < 1 || n > 4) return false;
|
|
75
|
+
for (i = 0; i < n; i += 1) { if (!_isHexCode(_charCodeAt(g, i))) return false; }
|
|
76
|
+
return true;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// The RFC 4291 sec. 2.5.5.2 dual-stack tail SHAPE: exactly four 1-3 digit groups joined by dots. Validity
|
|
80
|
+
// (0-255 per octet) is a separate isIPv4 check, matching the historical two-step (shape then value).
|
|
81
|
+
function _isDottedQuadShape(t) {
|
|
82
|
+
var q = _splitOn(t, "."), i, p, j;
|
|
83
|
+
if (q.length !== 4) return false;
|
|
84
|
+
for (i = 0; i < 4; i += 1) {
|
|
85
|
+
p = q[i];
|
|
86
|
+
if (p.length < 1 || p.length > 3) return false;
|
|
87
|
+
for (j = 0; j < p.length; j += 1) { if (!_isDigitCode(_charCodeAt(p, j))) return false; }
|
|
88
|
+
}
|
|
89
|
+
return true;
|
|
90
|
+
}
|
|
19
91
|
|
|
20
92
|
// Expand an IPv6 textual address to its 32-hex-character form, or null on any parse failure
|
|
21
93
|
// (bad hex group, group count != 8, more than one "::", an over-length group). Accepts the
|
|
22
94
|
// IPv4-mapped / dual-stack ".d.d.d.d" tail.
|
|
23
95
|
var IPV6_TEXT_MAX_LEN = 45; // 8 groups x4 hex + 7 colons + a dotted-quad tail headroom
|
|
24
96
|
function expandIpv6Hex(ip) {
|
|
25
|
-
if (typeof ip !== "string" || ip.length > IPV6_TEXT_MAX_LEN || ip
|
|
26
|
-
// RFC 4291 sec. 2.5.5.2 IPv4-mapped / dual-stack: fold a trailing dotted-quad
|
|
27
|
-
// 16-bit hex groups before the pure-hex parse.
|
|
28
|
-
var
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
97
|
+
if (typeof ip !== "string" || ip.length > IPV6_TEXT_MAX_LEN || _indexOf(ip, ":") === -1) return null;
|
|
98
|
+
// RFC 4291 sec. 2.5.5.2 IPv4-mapped / dual-stack: fold a trailing dotted-quad (the suffix after the
|
|
99
|
+
// last colon, when it carries a dot) into two 16-bit hex groups before the pure-hex parse.
|
|
100
|
+
var lastColon = _lastIndexOf(ip, ":");
|
|
101
|
+
var tail = _slice(ip, lastColon + 1);
|
|
102
|
+
if (_indexOf(tail, ".") !== -1) {
|
|
103
|
+
if (!_isDottedQuadShape(tail) || !isIPv4(tail)) return null;
|
|
104
|
+
var q = _splitOn(tail, "."), o0 = _Number(q[0]), o1 = _Number(q[1]), o2 = _Number(q[2]), o3 = _Number(q[3]);
|
|
105
|
+
ip = _slice(ip, 0, lastColon) + ":" + _numToString((o0 << 8) | o1, 16) + ":" + _numToString((o2 << 8) | o3, 16);
|
|
33
106
|
}
|
|
34
|
-
var dbl = ip
|
|
107
|
+
var dbl = _splitOn(ip, "::");
|
|
35
108
|
if (dbl.length > 2) return null;
|
|
36
|
-
var left = dbl[0] === "" ? [] : dbl[0]
|
|
37
|
-
var right = dbl.length === 2 ? (dbl[1] === "" ? [] : dbl[1]
|
|
109
|
+
var left = dbl[0] === "" ? [] : _splitOn(dbl[0], ":");
|
|
110
|
+
var right = dbl.length === 2 ? (dbl[1] === "" ? [] : _splitOn(dbl[1], ":")) : [];
|
|
38
111
|
if (dbl.length === 1 && left.length !== 8) return null;
|
|
39
112
|
var fill = 8 - left.length - right.length;
|
|
40
113
|
// A "::" run MUST compress at least one zero group (RFC 4291 sec. 2.2 / RFC 5952 sec.
|
|
41
114
|
// 4.2.2): a "::" with fill 0 is a full 8-group address that must not use compression.
|
|
42
115
|
if (dbl.length === 2 ? fill < 1 : fill !== 0) return null;
|
|
43
|
-
var groups = left, i;
|
|
44
|
-
for (i = 0; i < fill; i += 1) groups
|
|
45
|
-
|
|
116
|
+
var groups = left, i; // `left` is a fresh own-element array; append through _push (no inherited setter)
|
|
117
|
+
for (i = 0; i < fill; i += 1) _push(groups, "0");
|
|
118
|
+
for (i = 0; i < right.length; i += 1) _push(groups, right[i]);
|
|
46
119
|
if (groups.length !== 8) return null;
|
|
47
120
|
var hex = "";
|
|
48
121
|
for (i = 0; i < 8; i += 1) {
|
|
49
122
|
var g = groups[i];
|
|
50
|
-
if (
|
|
51
|
-
hex += g
|
|
123
|
+
if (!_isHexGroup(g)) return null;
|
|
124
|
+
hex += _padStart(_toLowerCase(g), 4, "0");
|
|
52
125
|
}
|
|
53
126
|
return hex;
|
|
54
127
|
}
|
|
@@ -61,9 +134,13 @@ function isIpLiteral(s) { return isIPv4(s) || expandIpv6Hex(s) !== null; }
|
|
|
61
134
|
// expandIpv6Hex validate, so a SAN/GeneralName iPAddress (RFC 5280 sec. 4.2.1.6, always a bare
|
|
62
135
|
// host address of 4 or 16 octets) can be given as a string instead of a pre-packed Buffer.
|
|
63
136
|
function packIpLiteral(s) {
|
|
64
|
-
if (isIPv4(s))
|
|
137
|
+
if (isIPv4(s)) {
|
|
138
|
+
var parts = _splitOn(s, "."), buf = _bufferAlloc(4), i;
|
|
139
|
+
for (i = 0; i < 4; i += 1) buf[i] = _Number(parts[i]); // Buffer integer-index store; each octet range-checked by _isOctet
|
|
140
|
+
return buf;
|
|
141
|
+
}
|
|
65
142
|
var hex = expandIpv6Hex(s);
|
|
66
|
-
return hex === null ? null :
|
|
143
|
+
return hex === null ? null : _bufferFrom(hex, "hex");
|
|
67
144
|
}
|
|
68
145
|
|
|
69
|
-
module.exports = { isIPv4: isIPv4, expandIpv6Hex: expandIpv6Hex, isIpLiteral: isIpLiteral, packIpLiteral: packIpLiteral
|
|
146
|
+
module.exports = { isIPv4: isIPv4, expandIpv6Hex: expandIpv6Hex, isIpLiteral: isIpLiteral, packIpLiteral: packIpLiteral };
|
package/lib/oid.js
CHANGED
|
@@ -456,6 +456,30 @@ var FAMILIES = {
|
|
|
456
456
|
// registered here so that prefix can be derived from the registry rather than a dotted-decimal literal.
|
|
457
457
|
teletrustRsaSig: { base: [1, 3, 36, 3, 3, 1], of: { "rsaSignatureWithripemd160": 2 } },
|
|
458
458
|
|
|
459
|
+
// TeleTrusT (BSI) signatureScheme arc (1.3.36.3.4) -- the ISO/IEC 9796-2 RSA signatures giving message
|
|
460
|
+
// recovery (schemes 1/2/3: two deterministic, one randomized). ISO/IEC 9796-2 is titled "integer
|
|
461
|
+
// factorization based mechanisms", so EVERY scheme it defines is RSA and the standard has no non-RSA
|
|
462
|
+
// member; each node is registered so a consumer can name it -- e.g. the CMP keySpec RSA exclusion, which
|
|
463
|
+
// must recognize RSA wherever it is standardized. ISO/IEC 9796-2 assigns no algorithm OIDs of its own;
|
|
464
|
+
// this registrant tree is the de-facto registry. The .2 (deterministic) and .3 (randomized) sub-arcs are
|
|
465
|
+
// RSA-dedicated in full -- every descendant is RSA -- so a consumer prefix-matches each to catch a hash
|
|
466
|
+
// variant added later; the per-hash leaves are registered for readability. The `...Withrsa-even-exp`
|
|
467
|
+
// nodes (.2.1 / .3.1) are the even-public-exponent RSA variants the registrant marks "not used"; they are
|
|
468
|
+
// RSA (the registrant names them `sigS_ISO9796-2Withrsa_even-exp`), so the sub-arc prefix classifies them
|
|
469
|
+
// correctly, not wrongly. .1 is ISO/IEC 9796-1, also RSA, on the same arc. The PARENT node (.4) is generic
|
|
470
|
+
// "signatureScheme" and the sibling authentication arc (.5) is mixed (its .5.3 children are ECC), so
|
|
471
|
+
// neither is RSA-dedicated -- a consumer must not prefix-match the parent or .5.
|
|
472
|
+
teletrustSigScheme: { base: [1, 3, 36, 3, 4], of: {
|
|
473
|
+
"sigS-ISO9796-1": 1, "sigS-ISO9796-1-DFUE": [1, 1],
|
|
474
|
+
"sigS-ISO9796-2": 2, "sigS-ISO9796-2Withrsa-even-exp": [2, 1], "sigS-ISO9796-2Withrsa": [2, 2],
|
|
475
|
+
"sigS-ISO9796-2Withrsa-sha1": [2, 2, 1], "sigS-ISO9796-2Withrsa-ripemd160": [2, 2, 2],
|
|
476
|
+
"sigS-ISO9796-2Withrsa-sha224": [2, 2, 3], "sigS-ISO9796-2Withrsa-sha256": [2, 2, 4],
|
|
477
|
+
"sigS-ISO9796-2Withrsa-sha384": [2, 2, 5], "sigS-ISO9796-2Withrsa-sha512": [2, 2, 6],
|
|
478
|
+
"sigS-ISO9796-2rnd": 3, "sigS-ISO9796-2rndWithrsa-even-exp": [3, 1], "sigS-ISO9796-2rndWithrsa": [3, 2],
|
|
479
|
+
"sigS-ISO9796-2rndWithrsa-sha1": [3, 2, 1], "sigS-ISO9796-2rndWithrsa-ripemd160": [3, 2, 2],
|
|
480
|
+
"sigS-ISO9796-2rndWithrsa-sha224": [3, 2, 3], "sigS-ISO9796-2rndWithrsa-sha256": [3, 2, 4],
|
|
481
|
+
"sigS-ISO9796-2rndWithrsa-sha384": [3, 2, 5], "sigS-ISO9796-2rndWithrsa-sha512": [3, 2, 6] } },
|
|
482
|
+
|
|
459
483
|
// X.509 directory encryptionAlgorithm arc (2.5.8.1). id-ea-rsa (.1) is the X.509 `rsa` public-key
|
|
460
484
|
// identifier, distinct from PKCS#1 rsaEncryption. Registered so a consumer can name it -- e.g. the
|
|
461
485
|
// CMP keySpec RSA exclusion, which must recognize RSA wherever it is standardized.
|
package/lib/pki-build.js
CHANGED
|
@@ -25,16 +25,279 @@ var _bufferFrom = intrinsic.bufferFrom;
|
|
|
25
25
|
var _bufferEquals = intrinsic.bufferEquals;
|
|
26
26
|
var oid = require("./oid");
|
|
27
27
|
var ipUtils = require("./ip-utils");
|
|
28
|
+
// packIpLiteral decides a GeneralName form (a bare string -> iPAddress or not), so its binding is taken at
|
|
29
|
+
// load: calling it through the exports object would be a live property read, and code that reassigned that
|
|
30
|
+
// property could steer the form decision without touching a regex.
|
|
31
|
+
var _packIpLiteral = ipUtils.packIpLiteral;
|
|
28
32
|
var nodeCrypto = require("crypto");
|
|
29
33
|
// The key-material comparison, taken at load like every other operation a decision here rests on.
|
|
30
34
|
var _keyEquals = intrinsic.uncurry(nodeCrypto.KeyObject.prototype.equals);
|
|
35
|
+
// The bare-string GeneralName classifier (_classifyBareGeneralName) decides which name FORM a SAN string
|
|
36
|
+
// becomes. It uses no regular expression: a regex is matched through a live, replaceable protocol
|
|
37
|
+
// (RegExp.prototype.exec / test, String.prototype.match / split consult the regex's own `exec` and the
|
|
38
|
+
// `[Symbol.match]` / `[Symbol.split]` hooks, and RegExp.prototype.compile can mutate a pattern in place),
|
|
39
|
+
// so co-resident code could steer a form decision without our seeing it -- and the structural URI/email
|
|
40
|
+
// rules a regex cannot state cleanly are what kept drawing edge-case escapes. Instead every form is
|
|
41
|
+
// decided by scanning the string one character code at a time through these captured string primitives,
|
|
42
|
+
// which read the value directly and have no per-character dispatch to replace.
|
|
43
|
+
var _charCodeAt = intrinsic.uncurry(String.prototype.charCodeAt);
|
|
44
|
+
var _stringIndexOf = intrinsic.stringIndexOf;
|
|
45
|
+
var _stringLastIndexOf = intrinsic.uncurry(String.prototype.lastIndexOf);
|
|
46
|
+
var _sliceStr = intrinsic.uncurry(String.prototype.slice);
|
|
47
|
+
var _String = intrinsic.String;
|
|
48
|
+
var _objectKeys = intrinsic.keys;
|
|
49
|
+
var _isBufferChk = intrinsic.isBuffer;
|
|
50
|
+
var _stringify = intrinsic.stringify;
|
|
31
51
|
|
|
32
|
-
|
|
52
|
+
// --- bare-string GeneralName form detection, by character-code scan (no regex) ---
|
|
53
|
+
// Each predicate answers "is this string syntactically THIS name form", reading the string one code unit
|
|
54
|
+
// at a time; a caller cannot steer the answer by replacing a prototype method. The classifier below tries
|
|
55
|
+
// the forms in a fixed precedence and refuses anything none of them accepts (the object form is the escape).
|
|
56
|
+
function _isAlphaCode(c) { return (c >= 65 && c <= 90) || (c >= 97 && c <= 122); } // A-Z a-z
|
|
57
|
+
function _isDigitCode(c) { return c >= 48 && c <= 57; } // 0-9
|
|
58
|
+
function _isHexCode(c) { return (c >= 48 && c <= 57) || (c >= 97 && c <= 102) || (c >= 65 && c <= 70); }
|
|
59
|
+
|
|
60
|
+
// RFC 3986 sec. 2.3 unreserved: ALPHA / DIGIT / "-" / "." / "_" / "~".
|
|
61
|
+
function _isUnreserved(c) { return _isAlphaCode(c) || _isDigitCode(c) || c === 45 || c === 46 || c === 95 || c === 126; }
|
|
62
|
+
// RFC 3986 sec. 2.2 sub-delims: ! $ & ' ( ) * + , ; =
|
|
63
|
+
function _isSubDelim(c) {
|
|
64
|
+
switch (c) {
|
|
65
|
+
case 33: case 36: case 38: case 39: case 40: case 41: case 42: case 43: case 44: case 59: case 61: return true;
|
|
66
|
+
default: return false;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
// A userinfo byte: unreserved / sub-delim / ":" (percent-encoding handled separately). No "@".
|
|
70
|
+
function _isUserinfoByte(c) { return _isUnreserved(c) || _isSubDelim(c) || c === 58; }
|
|
71
|
+
// A path/query/fragment byte: unreserved / sub-delim / a non-bracket gen-delim (":" "/" "?" "#" "@").
|
|
72
|
+
// "%" is handled separately (it must open a "%HEXHEX" triplet); the IP-literal brackets "[" "]" are
|
|
73
|
+
// excluded, so an IPv6-literal host is refused by the shorthand and uses the object form.
|
|
74
|
+
function _isUriByte(c) {
|
|
75
|
+
return _isUnreserved(c) || _isSubDelim(c) || c === 58 || c === 47 || c === 63 || c === 35 || c === 64;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// An RFC 5322 sec. 3.2.3 "atext" byte: ALPHA / DIGIT and a fixed set of specials, used for an unquoted
|
|
79
|
+
// email local part. "." is deliberately excluded so the dot-atom structure is enforced by the scan.
|
|
80
|
+
function _isAtextByte(c) {
|
|
81
|
+
if (_isAlphaCode(c) || _isDigitCode(c)) return true;
|
|
82
|
+
switch (c) {
|
|
83
|
+
case 33: case 35: case 36: case 37: case 38: case 39: // ! # $ % & '
|
|
84
|
+
case 42: case 43: case 45: case 47: case 61: case 63: // * + - / = ?
|
|
85
|
+
case 94: case 95: case 96: case 123: case 124: case 125: case 126: // ^ _ ` { | } ~
|
|
86
|
+
return true;
|
|
87
|
+
default: return false;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// IPv4 SHAPE: one or more digit groups joined by single dots (only digits and dots, no leading/trailing/
|
|
92
|
+
// doubled dot, at least one dot). This is the "looks like an address" test, not a valid-address test --
|
|
93
|
+
// packIpLiteral decides validity; a shape match that is not a valid address is refused as ambiguous.
|
|
94
|
+
function _looksLikeIPv4Shape(s) {
|
|
95
|
+
var n = s.length, i, c, sawDot = false, atomEmpty = true;
|
|
96
|
+
if (n === 0) return false;
|
|
97
|
+
for (i = 0; i < n; i += 1) {
|
|
98
|
+
c = _charCodeAt(s, i);
|
|
99
|
+
if (c === 46) { if (atomEmpty) return false; sawDot = true; atomEmpty = true; } // "."
|
|
100
|
+
else if (_isDigitCode(c)) { atomEmpty = false; }
|
|
101
|
+
else return false;
|
|
102
|
+
}
|
|
103
|
+
return sawDot && !atomEmpty;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Every byte in [from,to) is an allowed component byte (per `byteOk`) or opens a "%HEXHEX" triplet.
|
|
107
|
+
function _componentBytesOk(s, from, to, byteOk) {
|
|
108
|
+
var i = from, c;
|
|
109
|
+
while (i < to) {
|
|
110
|
+
c = _charCodeAt(s, i);
|
|
111
|
+
if (c === 37) { // "%" -> %HEXHEX
|
|
112
|
+
if (i + 2 >= to || !_isHexCode(_charCodeAt(s, i + 1)) || !_isHexCode(_charCodeAt(s, i + 2))) return false;
|
|
113
|
+
i += 3;
|
|
114
|
+
} else if (byteOk(c)) { i += 1; }
|
|
115
|
+
else return false;
|
|
116
|
+
}
|
|
117
|
+
return true;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// The URI host in [from,to): RFC 5280 sec. 4.2.1.6 requires a URI SAN authority to carry a fully-qualified
|
|
121
|
+
// domain name or IP host, stricter than an RFC 3986 reg-name. So the host is either a valid IPv4 literal,
|
|
122
|
+
// or an FQDN of strict-LDH labels (the email-domain grammar: no underscore, no hyphen at a label edge) with
|
|
123
|
+
// at least one dot -- a single-label host such as "localhost" is not fully qualified and uses the object
|
|
124
|
+
// form, matching the email domain. An IPv4-SHAPED host that is not a valid address is refused as ambiguous
|
|
125
|
+
// (the same rule the top-level classifier uses), and an IPv6-literal host -- needing the excluded "[" "]"
|
|
126
|
+
// -- uses the object form.
|
|
127
|
+
function _validUriHost(s, from, to) {
|
|
128
|
+
if (to <= from) return false; // empty host
|
|
129
|
+
var host = _sliceStr(s, from, to), len = host.length;
|
|
130
|
+
if (_looksLikeIPv4Shape(host)) return _packIpLiteral(host) !== null; // IPv4 (first-match-wins)
|
|
131
|
+
var end = _charCodeAt(host, len - 1) === 46 ? len - 1 : len; // ignore an absolute-root "."
|
|
132
|
+
if (end > 253) return false; // RFC 1035 name length (root dot excluded)
|
|
133
|
+
var firstDot = _stringIndexOf(host, ".", 0);
|
|
134
|
+
if (firstDot === -1 || firstDot >= end) return false; // multi-label FQDN ("localhost" is not)
|
|
135
|
+
return _labelsValid(host, 0, len, _isHostLabelRange, true); // strict-LDH FQDN, root "." allowed
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// The authority in [from,to) is "[ userinfo "@" ] host [ ":" port ]" (RFC 3986 sec. 3.2): at most one "@"
|
|
139
|
+
// (userinfo and reg-name each exclude it), a host validated by _validUriHost (RFC 5280 FQDN or IPv4), and
|
|
140
|
+
// an all-digit -- possibly empty -- port after the host's single ":" (a reg-name has no ":"). The userinfo
|
|
141
|
+
// bytes are validated against the RFC 3986 userinfo class.
|
|
142
|
+
function _validUriAuthority(s, from, to) {
|
|
143
|
+
var i, c, at = -1;
|
|
144
|
+
for (i = from; i < to; i += 1) {
|
|
145
|
+
c = _charCodeAt(s, i);
|
|
146
|
+
if (c === 64) { if (at !== -1) return false; at = i; } // "@" -- at most one
|
|
147
|
+
}
|
|
148
|
+
var hostStart = at === -1 ? from : at + 1;
|
|
149
|
+
if (at !== -1 && !_componentBytesOk(s, from, at, _isUserinfoByte)) return false; // userinfo
|
|
150
|
+
var colon = -1;
|
|
151
|
+
for (i = hostStart; i < to; i += 1) { if (_charCodeAt(s, i) === 58) { colon = i; break; } } // ":" -> port
|
|
152
|
+
var hostEnd = colon === -1 ? to : colon;
|
|
153
|
+
if (!_validUriHost(s, hostStart, hostEnd)) return false; // FQDN or IPv4 host
|
|
154
|
+
if (colon !== -1) {
|
|
155
|
+
for (i = colon + 1; i < to; i += 1) { if (!_isDigitCode(_charCodeAt(s, i))) return false; } // port = *DIGIT
|
|
156
|
+
}
|
|
157
|
+
return true;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// scheme "://" authority [ path ] [ "?" query ] [ "#" fragment ], parsed by RFC 3986 component. The
|
|
161
|
+
// authority is validated by _validUriAuthority; the remainder is path/query/fragment bytes with at most
|
|
162
|
+
// one "#" (its single fragment delimiter). A structurally malformed URL is refused; the object form is
|
|
163
|
+
// the escape for one the shorthand does not accept.
|
|
164
|
+
function _looksLikeUri(s) {
|
|
165
|
+
var n = s.length, i, c;
|
|
166
|
+
if (n === 0 || !_isAlphaCode(_charCodeAt(s, 0))) return false; // scheme starts ALPHA
|
|
167
|
+
i = 1;
|
|
168
|
+
while (i < n) {
|
|
169
|
+
c = _charCodeAt(s, i);
|
|
170
|
+
if (_isAlphaCode(c) || _isDigitCode(c) || c === 43 || c === 45 || c === 46) i += 1; // + - . in scheme
|
|
171
|
+
else break;
|
|
172
|
+
}
|
|
173
|
+
if (i + 3 > n || _charCodeAt(s, i) !== 58 || _charCodeAt(s, i + 1) !== 47 || _charCodeAt(s, i + 2) !== 47) return false;
|
|
174
|
+
i += 3;
|
|
175
|
+
var authStart = i, authEnd = n;
|
|
176
|
+
for (; i < n; i += 1) { c = _charCodeAt(s, i); if (c === 47 || c === 63 || c === 35) { authEnd = i; break; } }
|
|
177
|
+
if (authEnd === authStart) return false; // empty authority
|
|
178
|
+
if (!_validUriAuthority(s, authStart, authEnd)) return false;
|
|
179
|
+
var sawHash = false; // path / query / fragment
|
|
180
|
+
i = authEnd;
|
|
181
|
+
while (i < n) {
|
|
182
|
+
c = _charCodeAt(s, i);
|
|
183
|
+
if (c === 37) { // "%" -> %HEXHEX
|
|
184
|
+
if (i + 2 >= n || !_isHexCode(_charCodeAt(s, i + 1)) || !_isHexCode(_charCodeAt(s, i + 2))) return false;
|
|
185
|
+
i += 3;
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
if (c === 35) { if (sawHash) return false; sawHash = true; } // one "#" fragment delimiter
|
|
189
|
+
if (!_isUriByte(c)) return false;
|
|
190
|
+
i += 1;
|
|
191
|
+
}
|
|
192
|
+
return true;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// A single dNSName label over [from,to): 1-63 bytes, LDH plus a LEADING or EMBEDDED underscore, no hyphen
|
|
196
|
+
// at either edge, and no underscore at the trailing edge. The underscore tolerance is deliberate and
|
|
197
|
+
// leading/embedded only: the real forms are _acme-challenge, _dmarc, _25._tcp (a label-initial "_"); a
|
|
198
|
+
// label ending in "_" has no such use. An email DOMAIN is stricter still -- see _isHostLabelRange.
|
|
199
|
+
function _isDnsLabelRange(s, from, to) {
|
|
200
|
+
var len = to - from, i, c;
|
|
201
|
+
if (len < 1 || len > 63) return false;
|
|
202
|
+
for (i = from; i < to; i += 1) {
|
|
203
|
+
c = _charCodeAt(s, i);
|
|
204
|
+
if (_isAlphaCode(c) || _isDigitCode(c)) continue; // A-Z a-z 0-9 anywhere
|
|
205
|
+
if (i === to - 1) return false; // last byte must be alnum
|
|
206
|
+
if (c === 95) continue; // "_" leading or embedded
|
|
207
|
+
if (c === 45 && i !== from) continue; // "-" embedded (not leading)
|
|
208
|
+
return false;
|
|
209
|
+
}
|
|
210
|
+
return true;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// A single RFC 5321 sec. 2.3.5 hostname label over [from,to): the strict letter-digit-hyphen grammar with
|
|
214
|
+
// no hyphen at either edge and NO underscore. This is the email-domain label rule (a mail domain is a real
|
|
215
|
+
// hostname), stricter than the underscore-tolerant dNSName label above.
|
|
216
|
+
function _isHostLabelRange(s, from, to) {
|
|
217
|
+
var len = to - from, i, c, edge;
|
|
218
|
+
if (len < 1 || len > 63) return false;
|
|
219
|
+
for (i = from; i < to; i += 1) {
|
|
220
|
+
c = _charCodeAt(s, i);
|
|
221
|
+
edge = (i === from || i === to - 1);
|
|
222
|
+
if (_isAlphaCode(c) || _isDigitCode(c)) continue; // A-Z a-z 0-9
|
|
223
|
+
if (c === 45 && !edge) continue; // "-" not at an edge
|
|
224
|
+
return false;
|
|
225
|
+
}
|
|
226
|
+
return true;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Every dot-separated label in [from,to) satisfies `labelFn`. Scans with indexOf; no split allocation. When
|
|
230
|
+
// `allowRootDot` is set, a single trailing "." (the absolute-FQDN root) is stripped before validating, so
|
|
231
|
+
// "example.com." checks as "example.com"; an interior empty label (a doubled or leading dot) still fails.
|
|
232
|
+
function _labelsValid(s, from, to, labelFn, allowRootDot) {
|
|
233
|
+
if (allowRootDot && to - from >= 1 && _charCodeAt(s, to - 1) === 46) to -= 1; // an absolute-root "."
|
|
234
|
+
if (to <= from) return false; // nothing (a lone ".")
|
|
235
|
+
var start = from, dot;
|
|
236
|
+
for (;;) {
|
|
237
|
+
dot = _stringIndexOf(s, ".", start);
|
|
238
|
+
var end = (dot === -1 || dot >= to) ? to : dot;
|
|
239
|
+
if (!labelFn(s, start, end)) return false;
|
|
240
|
+
if (end === to) return true;
|
|
241
|
+
start = end + 1;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
33
244
|
|
|
34
|
-
// A
|
|
35
|
-
//
|
|
36
|
-
|
|
37
|
-
var
|
|
245
|
+
// A dNSName: at most 253 bytes, an optional single leading "*." wildcard, then dot-separated dNSName labels
|
|
246
|
+
// (underscore-tolerant), with an optional trailing absolute-root ".".
|
|
247
|
+
function _looksLikeDnsName(s) {
|
|
248
|
+
var n = s.length, start = 0;
|
|
249
|
+
if (n < 1) return false;
|
|
250
|
+
var end = _charCodeAt(s, n - 1) === 46 ? n - 1 : n; // exclude an absolute-root "."
|
|
251
|
+
if (end < 1 || end > 253) return false; // RFC 1035 name length (root dot excluded)
|
|
252
|
+
if (end >= 2 && _charCodeAt(s, 0) === 42 && _charCodeAt(s, 1) === 46) start = 2; // "*."
|
|
253
|
+
if (start >= end) return false;
|
|
254
|
+
return _labelsValid(s, start, n, _isDnsLabelRange, true);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// An rfc822Name: an unquoted RFC 5321 dot-atom local part (atext runs joined by single dots), one "@",
|
|
258
|
+
// and a dotted hostname domain (at least one dot -- an FQDN, no wildcard, strict LDH labels, no underscore).
|
|
259
|
+
function _looksLikeEmail(s) {
|
|
260
|
+
if (s.length > 254) return false; // the whole mailbox <= 254 octets (RFC 5321 sec. 4.5.3.1.3)
|
|
261
|
+
var at = _stringIndexOf(s, "@", 0);
|
|
262
|
+
if (at < 1 || at > 64 || _stringLastIndexOf(s, "@") !== at) return false; // one "@", a 1-64 octet local part (RFC 5321 sec. 4.5.3.1.1)
|
|
263
|
+
var i, c, atomEmpty = true;
|
|
264
|
+
for (i = 0; i < at; i += 1) { // local part = dot-atom
|
|
265
|
+
c = _charCodeAt(s, i);
|
|
266
|
+
if (c === 46) { if (atomEmpty) return false; atomEmpty = true; } // no leading/doubled dot
|
|
267
|
+
else if (_isAtextByte(c)) { atomEmpty = false; }
|
|
268
|
+
else return false;
|
|
269
|
+
}
|
|
270
|
+
if (atomEmpty) return false; // no trailing dot / empty
|
|
271
|
+
var dStart = at + 1, dLen = s.length - dStart;
|
|
272
|
+
if (dLen < 1 || dLen > 253 || _stringIndexOf(s, ".", dStart) === -1) return false; // domain must be dotted
|
|
273
|
+
return _labelsValid(s, dStart, s.length, _isHostLabelRange, false); // strict LDH, no underscore or root dot
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// A well-formed dotted-decimal OID SHAPE (no regex, by the same character scan the name forms use): a first
|
|
277
|
+
// arc 0-2, then one or more arcs each introduced by "." and written without a leading zero ("0" alone, or a
|
|
278
|
+
// non-zero digit followed by more digits). Used to accept a raw OID string (an unregistered KeyPurposeId /
|
|
279
|
+
// policy OID) where a registered name would go; the encoder (encodeOidContent) remains the authoritative
|
|
280
|
+
// validator of the arc-value bounds.
|
|
281
|
+
function _isDottedOid(s) {
|
|
282
|
+
var n = s.length, i, c, d0, arcs = 0;
|
|
283
|
+
if (n < 3) return false; // shortest is "X.Y"
|
|
284
|
+
c = _charCodeAt(s, 0);
|
|
285
|
+
if (c < 48 || c > 50) return false; // first arc is a single digit 0-2
|
|
286
|
+
i = 1;
|
|
287
|
+
while (i < n) {
|
|
288
|
+
if (_charCodeAt(s, i) !== 46) return false; // every following arc is introduced by "."
|
|
289
|
+
i += 1;
|
|
290
|
+
if (i >= n) return false; // a trailing "." has no arc
|
|
291
|
+
d0 = _charCodeAt(s, i);
|
|
292
|
+
if (d0 === 48) { i += 1; } // "0" is a complete arc; a leading zero is refused
|
|
293
|
+
else if (d0 >= 49 && d0 <= 57) { i += 1; while (i < n && _isDigitCode(_charCodeAt(s, i))) i += 1; }
|
|
294
|
+
else { return false; } // an arc must start with a digit
|
|
295
|
+
arcs += 1;
|
|
296
|
+
}
|
|
297
|
+
return arcs >= 1;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
var b = asn1.build;
|
|
38
301
|
|
|
39
302
|
// KeyUsage named-bit positions (RFC 5280 sec. 4.2.1.3); contentCommitment is the RFC 5280 rename of the
|
|
40
303
|
// X.509 nonRepudiation bit (1).
|
|
@@ -112,15 +375,51 @@ function makeBuilder(ctx) {
|
|
|
112
375
|
|
|
113
376
|
// ---- GeneralName encoding (RFC 5280 sec. 4.2.1.6) ----
|
|
114
377
|
function ia5Content(s) {
|
|
115
|
-
|
|
378
|
+
// Captured String / charCodeAt / bufferFrom: this converts a GeneralName value (a classified bare
|
|
379
|
+
// string or an object-form value) to the emitted IA5String bytes, so co-resident code replacing any of
|
|
380
|
+
// them after load could otherwise change which host the certificate names from the one that was checked.
|
|
381
|
+
s = _String(s);
|
|
116
382
|
for (var i = 0; i < s.length; i++) {
|
|
117
|
-
if (s
|
|
383
|
+
if (_charCodeAt(s, i) > 0x7F) throw E("bad-input", "value requires 7-bit ASCII (IA5String): " + _stringify(s));
|
|
118
384
|
}
|
|
119
|
-
return
|
|
385
|
+
return _bufferFrom(s, "latin1");
|
|
386
|
+
}
|
|
387
|
+
// A bare-string GeneralName shorthand: classify a string into exactly one form, FAIL-CLOSED. RFC 5280
|
|
388
|
+
// sec. 4.2.1.6 GeneralName is a CHOICE, so an input that does not UNAMBIGUOUSLY match one form throws
|
|
389
|
+
// rather than being guessed -- the explicit object form ({ dNSName: s } etc.) is always the escape. The
|
|
390
|
+
// per-form syntax is decided by the character-scan predicates above (no regex). Precedence resolves the
|
|
391
|
+
// overlaps of the later tests: an IP literal is also LDH-with-dots (so it is tested first), and a
|
|
392
|
+
// scheme://authority URL can carry userinfo that reads like an email (so URI is tested before rfc822Name).
|
|
393
|
+
// Form detection only -- IA5String (7-bit) enforcement stays at ia5Content, so a non-ASCII value still
|
|
394
|
+
// throws at encode.
|
|
395
|
+
function _classifyBareGeneralName(s) {
|
|
396
|
+
if (_packIpLiteral(s) !== null) return { iPAddress: s };
|
|
397
|
+
// A string of only digit groups separated by dots is an IPv4 ATTEMPT; if packIpLiteral rejected it,
|
|
398
|
+
// it is a MALFORMED address, not a dNSName. It reads two ways (an invalid IP and a syntactically
|
|
399
|
+
// valid all-numeric name), so it is ambiguous and refused rather than silently encoded as a name the
|
|
400
|
+
// caller did not intend -- a genuine all-numeric name uses the explicit { dNSName: ... } form.
|
|
401
|
+
if (_looksLikeIPv4Shape(s)) {
|
|
402
|
+
throw E("bad-input", "the bare GeneralName string " + _stringify(s) + " looks like an IPv4 address but is not a valid one; pass { iPAddress: ... } for an address or { dNSName: ... } for a name");
|
|
403
|
+
}
|
|
404
|
+
if (_looksLikeUri(s)) return { uniformResourceIdentifier: s };
|
|
405
|
+
if (_looksLikeEmail(s)) return { rfc822Name: s };
|
|
406
|
+
if (_looksLikeDnsName(s)) return { dNSName: s };
|
|
407
|
+
var _qs = _stringify(s);
|
|
408
|
+
throw E("bad-input", "cannot classify the bare GeneralName string " + _qs +
|
|
409
|
+
" as a dNSName, rfc822Name, iPAddress, or URI; pass an explicit form object, e.g. { dNSName: " + _qs + " }");
|
|
120
410
|
}
|
|
121
411
|
function encodeGeneralName(entry) {
|
|
122
|
-
|
|
123
|
-
|
|
412
|
+
// A bare non-empty string is shorthand -- normalized to its single-key object form, fail-closed,
|
|
413
|
+
// before the object checks below; an empty string is refused here (never classified).
|
|
414
|
+
if (typeof entry === "string") {
|
|
415
|
+
if (entry === "") throw E("bad-input", "an empty GeneralName value is not permitted (RFC 5280 sec. 4.2.1.6)");
|
|
416
|
+
entry = _classifyBareGeneralName(entry);
|
|
417
|
+
}
|
|
418
|
+
if (!entry || typeof entry !== "object" || _isBufferChk(entry)) throw E("bad-input", "a GeneralName must be an object with exactly one name form");
|
|
419
|
+
// Captured Object.keys / property read: this reads which form and value the certificate will emit for a
|
|
420
|
+
// GeneralName (a freshly classified bare string, or a caller object), so a replaced Object.keys must not
|
|
421
|
+
// be able to relabel the classified result into a different, attacker-chosen form after validation.
|
|
422
|
+
var keys = _objectKeys(entry);
|
|
124
423
|
if (keys.length !== 1) throw E("bad-input", "a GeneralName entry must have exactly one form, got " + keys.length);
|
|
125
424
|
var k = keys[0], v = entry[k];
|
|
126
425
|
if (v == null || v === "") throw E("bad-input", "an empty GeneralName value is not permitted (RFC 5280 sec. 4.2.1.6)");
|
|
@@ -134,10 +433,10 @@ function makeBuilder(ctx) {
|
|
|
134
433
|
// octets by hand (a GeneralName iPAddress is a bare host address, RFC 5280 sec. 4.2.1.6).
|
|
135
434
|
var ipBuf = v;
|
|
136
435
|
if (typeof v === "string") {
|
|
137
|
-
ipBuf =
|
|
138
|
-
if (ipBuf === null) throw E("bad-input", "iPAddress string is not a valid IPv4 or IPv6 literal: " +
|
|
436
|
+
ipBuf = _packIpLiteral(v);
|
|
437
|
+
if (ipBuf === null) throw E("bad-input", "iPAddress string is not a valid IPv4 or IPv6 literal: " + _stringify(v));
|
|
139
438
|
}
|
|
140
|
-
if (!
|
|
439
|
+
if (!_isBufferChk(ipBuf) || (ipBuf.length !== 4 && ipBuf.length !== 16)) throw E("bad-input", "iPAddress must be a 4- or 16-octet Buffer or an IPv4/IPv6 string");
|
|
141
440
|
return b.contextPrimitive(7, ipBuf);
|
|
142
441
|
case "directoryName": return b.explicit(4, encodeName(v)); // Name is a CHOICE -> the context tag is EXPLICIT
|
|
143
442
|
case "otherName":
|
|
@@ -187,8 +486,8 @@ function makeBuilder(ctx) {
|
|
|
187
486
|
function _resolveOid(n, label) {
|
|
188
487
|
var dotted = O(n);
|
|
189
488
|
if (dotted != null) return dotted;
|
|
190
|
-
if (typeof n === "string" &&
|
|
191
|
-
// The
|
|
489
|
+
if (typeof n === "string" && _isDottedOid(n)) {
|
|
490
|
+
// The shape scan fixes the general form; b.oid's encoder is the authoritative X.660 arc-bounds check
|
|
192
491
|
// (a first arc 0/1 caps the second at 39; an arc must DER-encode). A failure there is this producer's
|
|
193
492
|
// bad-input, not a leaked oid/* code, so every shared-builder consumer keeps its own error contract.
|
|
194
493
|
try { b.oid(n); }
|
package/lib/sign-scheme.js
CHANGED
|
@@ -222,7 +222,13 @@ function _assertKeyMatchesScheme(key, imp, E) {
|
|
|
222
222
|
var ka = key.algorithm || {};
|
|
223
223
|
// WebCrypto algorithm names are ASCII-case-insensitive (RSASSA-PKCS1-v1_5 and RSASSA-PKCS1-V1_5 are one
|
|
224
224
|
// algorithm), so compare case-folded; a genuine mismatch (e.g. RSASSA-PKCS1-v1_5 vs ECDSA) still fails.
|
|
225
|
-
if (_toUpperCase(_String(ka.name)) !== _toUpperCase(_String(imp.name)))
|
|
225
|
+
if (_toUpperCase(_String(ka.name)) !== _toUpperCase(_String(imp.name))) {
|
|
226
|
+
// A WebCrypto RSA-PSS CryptoKey is bound to PSS and cannot produce the default RSASSA-PKCS1-v1_5
|
|
227
|
+
// signature (which a raw PKCS#8 RSA key can), so name the fix rather than only the mismatch.
|
|
228
|
+
var hint = (_toUpperCase(_String(ka.name)) === "RSA-PSS" && _toUpperCase(_String(imp.name)) === "RSASSA-PKCS1-V1_5")
|
|
229
|
+
? "; this CryptoKey is bound to RSASSA-PSS -- pass opts.pss to sign with RSASSA-PSS" : "";
|
|
230
|
+
throw E("bad-input", "the signer CryptoKey algorithm (" + ka.name + ") does not match the certificate's key algorithm (" + imp.name + ")" + hint);
|
|
231
|
+
}
|
|
226
232
|
if (imp.hash && (!ka.hash || ka.hash.name !== imp.hash)) throw E("bad-input", "the signer CryptoKey hash (" + (ka.hash && ka.hash.name) + ") does not match the signing digest (" + imp.hash + ")");
|
|
227
233
|
if (imp.namedCurve && ka.namedCurve !== imp.namedCurve) throw E("bad-input", "the signer CryptoKey curve (" + ka.namedCurve + ") does not match the certificate curve (" + imp.namedCurve + ")");
|
|
228
234
|
}
|
|
@@ -242,11 +248,21 @@ function _normCompositeKeys(key, comp, E) {
|
|
|
242
248
|
return { mldsa: _normPkcs8(key.mldsa, "the composite ML-DSA component key", E), trad: _normPkcs8(key.trad, "the composite traditional component key", E) };
|
|
243
249
|
}
|
|
244
250
|
function _importKey(key, imp, E) {
|
|
245
|
-
|
|
251
|
+
// A WebCrypto CryptoKey is detected structurally (a `.type` string + an `.algorithm` object, and not
|
|
252
|
+
// a byte view), never by instanceof (cross-realm / a second installed copy) or util.types.isCryptoKey
|
|
253
|
+
// (false for this engine's own keys). The gate is broader than webcrypto.isCryptoKeyLike on purpose:
|
|
254
|
+
// an incomplete CryptoKey-shaped object (e.g. one missing its `usages`) still routes here so adoptKey
|
|
255
|
+
// reports the specific reason (usages / not-exportable) rather than the generic byte-form message. It
|
|
256
|
+
// signs, so it must be a PRIVATE key: a public or secret CryptoKey cannot produce a signature and is
|
|
257
|
+
// refused precisely.
|
|
258
|
+
if (key && typeof key === "object" && !_isBuffer(key) && !_isU8(key) && typeof key.type === "string" && key.algorithm != null) {
|
|
259
|
+
if (key.type !== "private") {
|
|
260
|
+
throw E("bad-input", "the signing key is a WebCrypto CryptoKey of type " + _stringify(key.type) + "; a signing key must be a private key (type \"private\")");
|
|
261
|
+
}
|
|
246
262
|
_assertKeyMatchesScheme(key, imp, E);
|
|
247
|
-
//
|
|
248
|
-
//
|
|
249
|
-
//
|
|
263
|
+
// A CryptoKey created outside this engine signs here too -- it is re-imported through this engine,
|
|
264
|
+
// because it carries none of the key material this engine signs with. One whose material this
|
|
265
|
+
// process cannot reach is refused with that as the reason.
|
|
250
266
|
return webcrypto.adoptKey(key, imp, ["sign"], E, "bad-input");
|
|
251
267
|
}
|
|
252
268
|
// A Uint8Array is copied here and a PEM string is decoded here, so both leave a second copy of a
|
|
@@ -260,7 +276,11 @@ function _importKey(key, imp, E) {
|
|
|
260
276
|
try { der = pkcs8.pemDecode(key); }
|
|
261
277
|
catch (e) { throw E("bad-input", "the signer PEM private key could not be decoded", e); }
|
|
262
278
|
owned = true;
|
|
263
|
-
} else
|
|
279
|
+
} else if (intrinsic.types.isKeyObject(key)) {
|
|
280
|
+
// A node:crypto KeyObject carries a `.type` but not the WebCrypto surface (algorithm / usages), so
|
|
281
|
+
// it fails the CryptoKey gate above and would otherwise land on the generic message -- name it precisely.
|
|
282
|
+
throw E("bad-input", "the signing key is a node:crypto KeyObject, not a WebCrypto CryptoKey; pass a CryptoKey (e.g. from pki.key.generate) or export it to a PKCS#8 DER Buffer or PEM string");
|
|
283
|
+
} else throw E("bad-input", "a signer key must be a WebCrypto CryptoKey, a PKCS#8 DER Buffer, or a PKCS#8 PEM string");
|
|
264
284
|
var imported = subtle.importKey("pkcs8", der, imp, false, ["sign"]);
|
|
265
285
|
if (!owned) return imported;
|
|
266
286
|
// Wiped on the reject path too: a malformed key is not a way to leave the copy in memory.
|
package/lib/x509-sign.js
CHANGED
|
@@ -264,7 +264,12 @@ function _hasCriticalSan(extSpec) {
|
|
|
264
264
|
* key being certified), `notBefore` / `notAfter` (`Date`s), an optional `serialNumber`, and an optional
|
|
265
265
|
* `extensions` object. `issuer` is the signing side: `{ key }` alone issues a self-signed certificate
|
|
266
266
|
* (issuer = subject, signed with the subject's own key); `{ name, publicKey, key }` or `{ cert, key }`
|
|
267
|
-
* issues a CA-signed one. The
|
|
267
|
+
* issues a CA-signed one. The signing key `key` is a WebCrypto `CryptoKey` -- a `pki.key.generate`
|
|
268
|
+
* private key passed directly, without exporting -- or a PKCS#8 private key as DER (`Buffer`) or PEM
|
|
269
|
+
* (`string`); a public or secret CryptoKey, or a `node:crypto` KeyObject, is refused. A `subjectAltName`
|
|
270
|
+
* (or any `GeneralName`) entry may be a form object (`{ dNSName: "..." }`) or a bare string classified
|
|
271
|
+
* fail-closed into its form (`"host.example"`, `"a@b.example"`, `"10.0.0.1"`, `"https://host.example/"`).
|
|
272
|
+
* The signature algorithm is resolved from the signing key: RSA (PKCS#1 v1.5
|
|
268
273
|
* or PSS via `opts.pss`), ECDSA, EdDSA, ML-DSA, SLH-DSA, or a composite arm, so every algorithm the
|
|
269
274
|
* toolkit signs with is available here without a per-algorithm branch.
|
|
270
275
|
*
|
|
@@ -283,12 +288,11 @@ function _hasCriticalSan(extSpec) {
|
|
|
283
288
|
* @example
|
|
284
289
|
* var pair = await pki.key.generate("Ed25519");
|
|
285
290
|
* var signerSpki = await pki.key.export(pair.publicKey);
|
|
286
|
-
* var signerKeyPkcs8 = await pki.key.export(pair.privateKey);
|
|
287
291
|
* var root = await pki.x509.sign(
|
|
288
292
|
* { subject: "Example Root CA", subjectPublicKey: signerSpki,
|
|
289
293
|
* notBefore: new Date("2026-01-01T00:00:00Z"), notAfter: new Date("2036-01-01T00:00:00Z"),
|
|
290
294
|
* extensions: { basicConstraints: { cA: true }, keyUsage: ["keyCertSign", "cRLSign"], subjectKeyIdentifier: true } },
|
|
291
|
-
* { key:
|
|
295
|
+
* { key: pair.privateKey }); // a WebCrypto CryptoKey signs directly (or pass a PKCS#8 DER Buffer / PEM string)
|
|
292
296
|
* pki.schema.x509.parse(root).subject.dn; // "CN=Example Root CA"
|
|
293
297
|
*/
|
|
294
298
|
function sign(spec, issuer, opts) {
|
|
@@ -324,7 +328,7 @@ function _sign(spec, issuer, opts) {
|
|
|
324
328
|
: "explicit form; the issuer takes { key, name, publicKey }, or { key, cert } to take both from a certificate");
|
|
325
329
|
});
|
|
326
330
|
guard.identifier.assertKnownKeys(opts, KNOWN_SIGN_OPTS, _err, "x509/bad-input", "pki.x509.sign has an unknown option ");
|
|
327
|
-
if (issuer.key == null) throw _err("x509/bad-input", "a signing key (issuer.key, a PKCS#8 private key) is required");
|
|
331
|
+
if (issuer.key == null) throw _err("x509/bad-input", "a signing key (issuer.key, a WebCrypto CryptoKey or a PKCS#8 private key DER/PEM) is required");
|
|
328
332
|
|
|
329
333
|
var spki = _reqDer(spec.subjectPublicKey, "spec.subjectPublicKey (the SPKI DER of the certified key)");
|
|
330
334
|
_assertValidSpki(spki, "spec.subjectPublicKey");
|
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:ebd39ca0-3971-47df-8cbe-c00499f2e5c6",
|
|
6
6
|
"version": 1,
|
|
7
7
|
"metadata": {
|
|
8
|
-
"timestamp": "2026-08-
|
|
8
|
+
"timestamp": "2026-08-25T04:07:42.047Z",
|
|
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.5.
|
|
22
|
+
"bom-ref": "@blamejs/pki@0.5.31",
|
|
23
23
|
"type": "application",
|
|
24
24
|
"name": "pki",
|
|
25
|
-
"version": "0.5.
|
|
25
|
+
"version": "0.5.31",
|
|
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.5.
|
|
29
|
+
"purl": "pkg:npm/%40blamejs/pki@0.5.31",
|
|
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.5.
|
|
57
|
+
"ref": "@blamejs/pki@0.5.31",
|
|
58
58
|
"dependsOn": []
|
|
59
59
|
}
|
|
60
60
|
]
|