@blamejs/pki 0.5.7 → 0.5.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +405 -386
- package/MIGRATING.md +43 -0
- package/README.md +12 -12
- package/lib/acme.js +31 -31
- package/lib/asn1-der.js +10 -10
- package/lib/attrcert-sign.js +19 -19
- package/lib/byte-reader.js +6 -6
- package/lib/byte-writer.js +5 -5
- package/lib/cbor-det.js +27 -24
- package/lib/cmc-build.js +97 -92
- package/lib/cmc-verify.js +106 -30
- package/lib/cmp-build.js +25 -25
- package/lib/cmp-session.js +70 -70
- package/lib/cmp-verify.js +71 -57
- package/lib/cms-compress.js +7 -7
- package/lib/cms-decrypt.js +90 -72
- package/lib/cms-encrypt.js +32 -32
- package/lib/cms-sign.js +74 -55
- package/lib/cms-verify.js +97 -75
- package/lib/composite-sig.js +13 -13
- package/lib/constants.js +4 -4
- package/lib/crl-sign.js +22 -22
- package/lib/crl-verify.js +7 -6
- package/lib/crmf-sign.js +14 -14
- package/lib/csr-sign.js +8 -8
- package/lib/ct.js +37 -37
- package/lib/edwards-point.js +7 -7
- package/lib/est.js +98 -55
- package/lib/framework-error.js +5 -5
- package/lib/guard-all.js +3 -3
- package/lib/guard-async.js +4 -4
- package/lib/guard-bytes.js +79 -79
- package/lib/guard-compress.js +17 -17
- package/lib/guard-crypto.js +1 -1
- package/lib/guard-encoding.js +15 -15
- package/lib/guard-header.js +3 -3
- package/lib/guard-identifier.js +16 -16
- package/lib/guard-json.js +15 -15
- package/lib/guard-limits.js +7 -7
- package/lib/guard-name.js +81 -16
- package/lib/guard-parsed.js +80 -80
- package/lib/guard-range.js +19 -19
- package/lib/guard-secret.js +11 -10
- package/lib/guard-text.js +6 -6
- package/lib/guard-time.js +10 -10
- package/lib/hpke.js +18 -17
- package/lib/http-digest.js +35 -35
- package/lib/http-retry-after.js +13 -13
- package/lib/http-transport.js +20 -19
- package/lib/inspect.js +53 -53
- package/lib/ip-utils.js +2 -2
- package/lib/jose.js +13 -13
- package/lib/key.js +16 -16
- package/lib/lint.js +51 -51
- package/lib/merkle.js +51 -36
- package/lib/mime.js +18 -18
- package/lib/ocsp-verify.js +10 -10
- package/lib/ocsp.js +13 -13
- package/lib/oid.js +29 -29
- package/lib/path-validate.js +114 -113
- package/lib/pbes2.js +16 -16
- package/lib/pkcs12-build.js +53 -53
- package/lib/pki-build.js +21 -19
- package/lib/rc2.js +1 -1
- package/lib/rfc3339.js +5 -5
- package/lib/schema-all.js +31 -31
- package/lib/schema-attrcert.js +12 -12
- package/lib/schema-c509.js +144 -142
- package/lib/schema-cmc.js +58 -58
- package/lib/schema-cmp.js +43 -43
- package/lib/schema-cms.js +45 -45
- package/lib/schema-crl.js +7 -7
- package/lib/schema-crmf.js +28 -28
- package/lib/schema-csr.js +12 -12
- package/lib/schema-csrattrs.js +16 -16
- package/lib/schema-engine.js +18 -18
- package/lib/schema-ocsp.js +15 -15
- package/lib/schema-pkcs12.js +20 -20
- package/lib/schema-pkcs8.js +2 -2
- package/lib/schema-pkix.js +131 -126
- package/lib/schema-smime.js +19 -19
- package/lib/schema-tsp.js +12 -12
- package/lib/schema-x509.js +3 -3
- package/lib/shbs.js +18 -18
- package/lib/sign-scheme.js +13 -13
- package/lib/sigstore.js +10 -11
- package/lib/sleep.js +1 -1
- package/lib/smime.js +308 -96
- package/lib/tls-cert-compress.js +18 -18
- package/lib/trust.js +27 -27
- package/lib/tsp-sign.js +17 -17
- package/lib/validator-all.js +1 -1
- package/lib/validator-attcert.js +1 -1
- package/lib/validator-cose.js +43 -44
- package/lib/validator-keydesc.js +3 -3
- package/lib/validator-sig.js +13 -13
- package/lib/validator-tls.js +11 -11
- package/lib/validator-tpm.js +20 -19
- package/lib/webauthn-mds.js +66 -66
- package/lib/webauthn.js +33 -33
- package/lib/webcrypto.js +15 -15
- package/lib/x509-sign.js +13 -13
- package/package.json +3 -2
- package/sbom.cdx.json +6 -6
package/lib/cmp-verify.js
CHANGED
|
@@ -11,12 +11,12 @@
|
|
|
11
11
|
// dependency set (the path-validate signature engine + full path building) that pki.cmp.build never
|
|
12
12
|
// pulls, and re-exports the cmp-build producing surface so the whole pki.cmp namespace is wired here.
|
|
13
13
|
//
|
|
14
|
-
// The load-bearing rule: the protection is recomputed over the
|
|
15
|
-
// signed
|
|
16
|
-
// (schema-cmp),
|
|
14
|
+
// The load-bearing rule: the protection is recomputed over the exact ProtectedPart bytes the builder
|
|
15
|
+
// signed, a SEQUENCE { header, body } reconstructed from the parser-surfaced raw headerBytes / bodyBytes
|
|
16
|
+
// (schema-cmp), never a re-serialization of the decoded structs. A re-encoder that normalized a malleable
|
|
17
17
|
// interior field would run the recompute over different bytes than a strict signer covered: both a false
|
|
18
|
-
// reject for a canonical peer
|
|
19
|
-
// (with the EdDSA low-order-point gate) injected via setEngine
|
|
18
|
+
// reject for a canonical peer and a bypass. The signature path routes through the single path-validate engine
|
|
19
|
+
// (with the EdDSA low-order-point gate) injected via setEngine, never build's self-check, which skips it.
|
|
20
20
|
// RFC 9810 sec. 5.1.3, algorithms RFC 9481 / RFC 9579, out-of-path signer profile RFC 9483.
|
|
21
21
|
|
|
22
22
|
var asn1 = require("./asn1-der");
|
|
@@ -40,7 +40,7 @@ function _err(code, message, cause) { return new CmpError(code, message, cause);
|
|
|
40
40
|
var NS = pkix.makeNS("cmp", CmpError, oid);
|
|
41
41
|
var PBMAC1_PARAMS = pkix.pbmac1Params(NS);
|
|
42
42
|
// The shared RFC 5280 sec. 4.2.1 extension-value decoders (the acme / inspect home): the signer keyUsage gate
|
|
43
|
-
// reads through the
|
|
43
|
+
// reads through the single structurally-strict decoder (X.690 sec. 11.2.2 minimal NamedBitList), not a
|
|
44
44
|
// hand-rolled bit test, so a malformed KeyUsage fails the gate independently of the path validator also rejecting it.
|
|
45
45
|
var _certExtDecoders = pkix.certExtensionDecoders(NS).byOid;
|
|
46
46
|
|
|
@@ -50,9 +50,9 @@ var KNOWN_VERIFY_OPTS = {
|
|
|
50
50
|
};
|
|
51
51
|
|
|
52
52
|
// PBMAC1 PBKDF2-PRF / messageAuthScheme name -> WebCrypto hash. Only SHA-256/384/512 are supported
|
|
53
|
-
// (RFC 9481 sec. 7 mandates SHA-256; RFC 9579 sec. 7 bars a <= 160-bit digest, so hmacWithSHA1
|
|
54
|
-
// omitted-PRF that resolves to it
|
|
55
|
-
// Keyed by the
|
|
53
|
+
// (RFC 9481 sec. 7 mandates SHA-256; RFC 9579 sec. 7 bars a <= 160-bit digest, so hmacWithSHA1, and an
|
|
54
|
+
// omitted-PRF that resolves to it, is unsupported and never MAC-verified under a weak digest).
|
|
55
|
+
// Keyed by the immutable dotted OID, never the display name: pki.oid.register() can rename hmacWithSHA256/384/512,
|
|
56
56
|
// so a name-keyed lookup would reject a valid PBMAC1 message under a renamed registry (the immutable-OID-dispatch rule).
|
|
57
57
|
var PRF_HASH = {};
|
|
58
58
|
PRF_HASH[oid.byName("hmacWithSHA256")] = "SHA-256";
|
|
@@ -69,8 +69,8 @@ var PBMAC1_KEYLEN_MAX = 1024;
|
|
|
69
69
|
// PBKDF2 PRF output length (octets) -- the size of one derived block; keyLength beyond it costs extra HMAC rounds.
|
|
70
70
|
var PRF_HLEN = { "SHA-256": 32, "SHA-384": 48, "SHA-512": 64 };
|
|
71
71
|
|
|
72
|
-
// The legacy / KEM MAC protection OIDs (RFC 9810 sec. 5.1.3.1/.2/.4) the v1 verifier recognizes and refuses
|
|
73
|
-
//
|
|
72
|
+
// The legacy / KEM MAC protection OIDs (RFC 9810 sec. 5.1.3.1/.2/.4) the v1 verifier recognizes and refuses.
|
|
73
|
+
// Build emits only PBMAC1, so the verifier rejects a legacy/KEM MAC OID and never accepts a construction
|
|
74
74
|
// it does not verify (a silent accept of an unverified algorithm would be fail-open).
|
|
75
75
|
var UNSUPPORTED_MAC_OIDS = {};
|
|
76
76
|
["passwordBasedMac", "dhBasedMac", "kemBasedMac"].forEach(function (n) {
|
|
@@ -133,7 +133,7 @@ function _coerce(message) {
|
|
|
133
133
|
}
|
|
134
134
|
|
|
135
135
|
// Rebuild the PKIMessage DER from a parsed object's raw slices: SEQUENCE { header, body, protection [0]
|
|
136
|
-
// EXPLICIT BIT STRING OPTIONAL, extraCerts [1] EXPLICIT SEQUENCE OPTIONAL }
|
|
136
|
+
// EXPLICIT BIT STRING OPTIONAL, extraCerts [1] EXPLICIT SEQUENCE OPTIONAL }, the exact inverse of the
|
|
137
137
|
// parser's surfaced fields (cmp-build.js assembles the identical shape). Re-parsing this authenticates every
|
|
138
138
|
// field against the raw bytes, discarding any mutated decoded field the caller may have carried.
|
|
139
139
|
function _reassemble(m) {
|
|
@@ -166,7 +166,7 @@ function _certSki(parsed) {
|
|
|
166
166
|
function _subjectDn(parsed) { return parsed.subject.dn || null; }
|
|
167
167
|
|
|
168
168
|
// The decoded GeneralName nodes of a parsed certificate's subjectAltName (empty if none / malformed).
|
|
169
|
-
// x509.parse surfaces `value` as the unwrapped extnValue content
|
|
169
|
+
// x509.parse surfaces `value` as the unwrapped extnValue content; for SAN that is the SEQUENCE OF GeneralName.
|
|
170
170
|
function _sanGeneralNames(parsed) {
|
|
171
171
|
var sanOid = oid.byName("subjectAltName");
|
|
172
172
|
var exts = parsed.extensions;
|
|
@@ -189,7 +189,7 @@ function _sanGeneralNames(parsed) {
|
|
|
189
189
|
// it equals the POPULATED subject DN (a [4] directoryName under the RFC 5280 sec. 7.1 canonical comparison) OR
|
|
190
190
|
// a subjectAltName GeneralName entry -- checked regardless of whether the subject is empty, so a certificate
|
|
191
191
|
// that carries both a subject and a SAN can be identified by either, and an empty subject (RFC 5280 sec.
|
|
192
|
-
// 4.1.2.6 requires a critical SAN) is bound to its SAN identity
|
|
192
|
+
// 4.1.2.6 requires a critical SAN) is bound to its SAN identity, never accepted with an anonymous sender.
|
|
193
193
|
function _senderBoundToCert(sender, parsed) {
|
|
194
194
|
if (!sender || !sender.bytes) return false;
|
|
195
195
|
var subjectRdns = parsed.subject.rdns;
|
|
@@ -203,7 +203,7 @@ function _senderBoundToCert(sender, parsed) {
|
|
|
203
203
|
}
|
|
204
204
|
|
|
205
205
|
// GeneralName equality (RFC 5280 sec. 7) for the sender <-> SAN binding, applying the per-type comparison
|
|
206
|
-
// rules
|
|
206
|
+
// rules in place of a universal byte compare: a dNSName [2] case-insensitively (sec. 4.2.1.6 / 7.2), an
|
|
207
207
|
// rfc822Name [1] with a case-insensitive domain (sec. 7.5), a uniformResourceIdentifier [6] with a
|
|
208
208
|
// case-insensitive scheme + host (sec. 4.2.1.6 / RFC 3986 sec. 6.2.2.1), a directoryName [4] under the sec.
|
|
209
209
|
// 7.1 canonical DN comparison, and every other type by exact DER (fail-closed). `sender` is the parsed [n]
|
|
@@ -213,9 +213,9 @@ function _generalNameMatches(sender, sanNode) {
|
|
|
213
213
|
if (sanNode.tagClass === "context") {
|
|
214
214
|
if (sanNode.tagNumber === 2 && sanNode.content) { // dNSName IA5String
|
|
215
215
|
var dnsSan = sanNode.content.toString("latin1"), dnsSender = String(sender.value);
|
|
216
|
-
// Case-fold
|
|
216
|
+
// Case-fold only a well-formed dNSName (RFC 5280 sec. 4.2.1.6 / RFC 1034, via the shared validator): a
|
|
217
217
|
// malformed name (an empty label like "victim..com", a leading/trailing dot, bad LDH) is compared
|
|
218
|
-
// byte-exact
|
|
218
|
+
// byte-exact and never folded, so it cannot bind a byte-distinct identity.
|
|
219
219
|
if (pkix.dnsNameProblem(dnsSan) !== null || pkix.dnsNameProblem(dnsSender) !== null) return dnsSan === dnsSender;
|
|
220
220
|
return dnsSender.toLowerCase() === dnsSan.toLowerCase();
|
|
221
221
|
}
|
|
@@ -256,10 +256,10 @@ function _mailboxSplit(s) {
|
|
|
256
256
|
sep = s.indexOf("@"); // an unquoted (Dot-string) local-part cannot contain a raw "@" -> exactly one total
|
|
257
257
|
if (sep < 0 || sep !== s.lastIndexOf("@")) return -1;
|
|
258
258
|
}
|
|
259
|
-
// The Domain that follows the separator must be a well-formed FQDN (RFC 1034, via the shared validator)
|
|
259
|
+
// The Domain that follows the separator must be a well-formed FQDN (RFC 1034, via the shared validator): an
|
|
260
260
|
// empty / empty-label / extra-"@" domain is malformed. An unquoted Local-part must be a Dot-string: non-empty,
|
|
261
261
|
// no leading/trailing dot, no empty atom ("a..b"). A quoted Local-part is already balanced above. A malformed
|
|
262
|
-
// mailbox returns -1 so _rfc822Equal compares it byte-exact
|
|
262
|
+
// mailbox returns -1 so _rfc822Equal compares it byte-exact and never case-folds a malformed domain.
|
|
263
263
|
var local = s.slice(0, sep), domain = s.slice(sep + 1);
|
|
264
264
|
if (pkix.dnsNameProblem(domain) !== null) return -1;
|
|
265
265
|
// An unquoted (Dot-string) Local-part is atext atoms joined by single dots (RFC 5321 sec. 4.1.2): every
|
|
@@ -275,7 +275,21 @@ function _mailboxSplit(s) {
|
|
|
275
275
|
function _rfc822Equal(a, b) {
|
|
276
276
|
var ai = _mailboxSplit(a), bi = _mailboxSplit(b);
|
|
277
277
|
if (ai < 0 || bi < 0) return a === b;
|
|
278
|
-
return a.slice(0, ai) === b.slice(0, bi) && a.slice(ai + 1)
|
|
278
|
+
return a.slice(0, ai) === b.slice(0, bi) && _lowerAsciiDomain(a.slice(ai + 1)) === _lowerAsciiDomain(b.slice(bi + 1));
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// sec. 7.5 authorizes a case-insensitive ASCII comparison of the host-part, and nothing
|
|
282
|
+
// wider. String.toLowerCase() is Unicode-aware and folds well beyond A-Z: U+212A KELVIN
|
|
283
|
+
// SIGN lowercases to "k", so a host carrying it would compare EQUAL to the ASCII spelling
|
|
284
|
+
// and two distinct domains would read as one identity. Fold only A-Z and leave every other
|
|
285
|
+
// octet alone, so a non-ASCII host can differ from an ASCII one but never collide with it.
|
|
286
|
+
function _lowerAsciiDomain(h) {
|
|
287
|
+
var out = "";
|
|
288
|
+
for (var i = 0; i < h.length; i++) {
|
|
289
|
+
var c = h.charCodeAt(i);
|
|
290
|
+
out += (c >= 0x41 && c <= 0x5a) ? String.fromCharCode(c + 32) : h.charAt(i);
|
|
291
|
+
}
|
|
292
|
+
return out;
|
|
279
293
|
}
|
|
280
294
|
|
|
281
295
|
// RFC 5280 sec. 4.2.1.6 / RFC 3986 sec. 6.2.2.1 URI comparison: the scheme and the authority host are
|
|
@@ -284,15 +298,15 @@ function _rfc822Equal(a, b) {
|
|
|
284
298
|
// -- never normalize an input we cannot confidently parse into a false identity match).
|
|
285
299
|
function _normalizeUri(u) {
|
|
286
300
|
// RFC 3986 syntax: every character must be a URI character and every "%" must introduce a valid pct-encoding
|
|
287
|
-
// (%HH). A URI with an out-of-charset byte or a malformed percent-escape ("%zz") in
|
|
288
|
-
// query, fragment, userinfo) is not normalized
|
|
301
|
+
// (%HH). A URI with an out-of-charset byte or a malformed percent-escape ("%zz") in any component (path,
|
|
302
|
+
// query, fragment, userinfo) is not normalized; return null so it is compared byte-exact.
|
|
289
303
|
if (!/^[A-Za-z0-9._~:/?#@!$&'()*+,;=%[\]-]*$/.test(u) || /%(?![0-9A-Fa-f]{2})/.test(u)) return null;
|
|
290
304
|
var m = /^([a-zA-Z][a-zA-Z0-9+.-]*):(.*)$/.exec(u); // scheme ":" rest (RFC 3986 sec. 3.1)
|
|
291
305
|
if (!m) return null;
|
|
292
306
|
var scheme = m[1].toLowerCase();
|
|
293
307
|
var rest = m[2];
|
|
294
308
|
// A URI SAN identity is host-based (RFC 5280 sec. 4.2.1.6): an authority-free URI (no "//", e.g. a URN) has
|
|
295
|
-
// no host to anchor the comparison, so compare it byte-exact
|
|
309
|
+
// no host to anchor the comparison, so compare it byte-exact and never case-fold its scheme; the shared
|
|
296
310
|
// GeneralName parser only enforces printable IA5, not a present authority.
|
|
297
311
|
if (rest.slice(0, 2) !== "//") return null;
|
|
298
312
|
var body = rest.slice(2);
|
|
@@ -306,7 +320,7 @@ function _normalizeUri(u) {
|
|
|
306
320
|
var userinfo = at < 0 ? "" : authority.slice(0, at + 1);
|
|
307
321
|
var hostport = at < 0 ? authority : authority.slice(at + 1);
|
|
308
322
|
// Split the host from an optional ":port": an IPv6 literal "[...]" carries the port after the "]", otherwise
|
|
309
|
-
// the port follows the
|
|
323
|
+
// the port follows the first ":". Case-fold only the host (RFC 3986 sec. 6.2.2.1); the port stays byte-exact
|
|
310
324
|
// so a malformed non-numeric port (":ADMIN" vs ":admin") is never folded into a false identity match.
|
|
311
325
|
var host, port;
|
|
312
326
|
if (hostport.charAt(0) === "[") {
|
|
@@ -317,13 +331,13 @@ function _normalizeUri(u) {
|
|
|
317
331
|
var ci = hostport.indexOf(":");
|
|
318
332
|
host = ci < 0 ? hostport : hostport.slice(0, ci); port = ci < 0 ? "" : hostport.slice(ci);
|
|
319
333
|
}
|
|
320
|
-
// The port must be numeric (RFC 3986 sec. 3.2.3 port = *DIGIT). A non-numeric port is a
|
|
321
|
-
// return null so the caller compares the raw values exactly
|
|
334
|
+
// The port must be numeric (RFC 3986 sec. 3.2.3 port = *DIGIT). A non-numeric port is a malformed authority:
|
|
335
|
+
// return null so the caller compares the raw values exactly and never case-folds the host of an input the
|
|
322
336
|
// normalizer cannot confidently parse (which would bind two byte-distinct malformed authorities).
|
|
323
337
|
if (port !== "" && !/^:[0-9]*$/.test(port)) return null;
|
|
324
338
|
// The authority host must be a well-formed FQDN or IP literal (RFC 5280 sec. 4.2.1.6 requires a URI SAN
|
|
325
339
|
// authority to carry an FQDN/IP host): an empty, malformed ("Victim..COM"), or bad IPv6-literal host is
|
|
326
|
-
// compared byte-exact
|
|
340
|
+
// compared byte-exact and never case-folded, since the GeneralName parser only enforces printable IA5.
|
|
327
341
|
var hostOk = host.charAt(0) === "["
|
|
328
342
|
? (host.charAt(host.length - 1) === "]" && ipUtils.expandIpv6Hex(host.slice(1, -1)) !== null)
|
|
329
343
|
: (pkix.dnsNameProblem(host) === null);
|
|
@@ -337,8 +351,8 @@ function _uriEqual(a, b) {
|
|
|
337
351
|
}
|
|
338
352
|
|
|
339
353
|
// M11/M14: resolve the signature-protection signer certificate. An explicit opts.signerCert wins (with the
|
|
340
|
-
// senderKID SKI binding when present); else `extra` (the
|
|
341
|
-
// senderKID, else RFC 9483 sec. 3.3
|
|
354
|
+
// senderKID SKI binding when present); else `extra` (the already dedup+validity+count-bounded extraCerts) by
|
|
355
|
+
// senderKID, else RFC 9483 sec. 3.3, where extraCerts[0] is the protection certificate. `extra` is bounded before
|
|
342
356
|
// this search so an unsigned flood cannot force unbounded X.509 parsing. Returns { der, spki, subject, parsed }
|
|
343
357
|
// or null (never verify against an unauthenticated key).
|
|
344
358
|
function _resolveSignerCert(m, opts, extra) {
|
|
@@ -359,8 +373,8 @@ function _resolveSignerCert(m, opts, extra) {
|
|
|
359
373
|
// would change verdict.signer.cert/.spki). Parse it SEPARATELY: a corrupt / mistyped certificate is a
|
|
360
374
|
// deployment error that THROWS cmp/bad-input, never a routine signer-cert-not-found verdict indistinguishable
|
|
361
375
|
// from a message that simply omitted its signer (the nullable path is reserved for message candidates).
|
|
362
|
-
// The caller
|
|
363
|
-
// is
|
|
376
|
+
// The caller explicitly selected this certificate, so the senderKID (a hint for narrowing a candidate search)
|
|
377
|
+
// is not applied here: the protection signature verification against this cert's key is the real gate; a
|
|
364
378
|
// valid message from a signer certificate that omits an SKI still resolves to its exact opts.signerCert.
|
|
365
379
|
var scDer = Buffer.from(_certDer(opts.signerCert, "opts.signerCert"));
|
|
366
380
|
var scParsed;
|
|
@@ -402,9 +416,9 @@ function _keyUsageAllowsSigning(parsed) {
|
|
|
402
416
|
for (var i = 0; i < exts.length; i++) {
|
|
403
417
|
if (exts[i].oid !== kuOid) continue;
|
|
404
418
|
var ku;
|
|
405
|
-
// Decode through the shared keyUsage decoder
|
|
419
|
+
// Decode through the shared keyUsage decoder: it enforces the RFC 5280 sec. 4.2.1.3 BIT STRING form + the
|
|
406
420
|
// X.690 sec. 11.2.2 minimal NamedBitList rule (rejecting a non-minimal 03 02 00 80). A malformed KeyUsage
|
|
407
|
-
// fails this gate
|
|
421
|
+
// fails this gate, so no hand-rolled bit test authorizes it and leans on path validation to reject it.
|
|
408
422
|
try { ku = _certExtDecoders[kuOid](exts[i].value); }
|
|
409
423
|
catch (_e) { return false; }
|
|
410
424
|
return ku.digitalSignature === true;
|
|
@@ -430,7 +444,7 @@ async function _verifyMac(m, protectedPart, protectionAlg, protection, opts) {
|
|
|
430
444
|
if (!prfHash) return _fail(m, "mac", protectionAlg, "cmp/unsupported-algorithm", "unsupported PBMAC1 PBKDF2 PRF " + JSON.stringify(kdf.prfName) + " (SHA-256/384/512 only; RFC 9481 sec. 7, RFC 9579 sec. 7)");
|
|
431
445
|
if (!macHash) return _fail(m, "mac", protectionAlg, "cmp/unsupported-algorithm", "unsupported PBMAC1 messageAuthScheme " + JSON.stringify(params.schemeName) + " (SHA-256/384/512 only)");
|
|
432
446
|
|
|
433
|
-
// M18: bound the attacker-controlled work factors
|
|
447
|
+
// M18: bound the attacker-controlled work factors before deriving (CWE-834/400). A config-tier throw.
|
|
434
448
|
_capWork(kdf.iterationCount, kdf.salt, kdf.keyLength, prfHash, opts);
|
|
435
449
|
|
|
436
450
|
// M19: recompute the MAC over the reconstructed ProtectedPart and compare in constant time.
|
|
@@ -465,16 +479,16 @@ function _capWork(iterationCount, salt, keyLength, prfHash, opts) {
|
|
|
465
479
|
if (iterationCount * blocks > cap) throw _err("cmp/bad-input", "the PBMAC1 combined work (iterationCount " + iterationCount + " x " + blocks + " derived blocks) exceeds the cap " + cap);
|
|
466
480
|
}
|
|
467
481
|
|
|
468
|
-
// M9-M14 + M20-M22: verify signature protection, then (with a trust store)
|
|
482
|
+
// M9-M14 + M20-M22: verify signature protection, then (with a trust store) full out-of-path cert validation.
|
|
469
483
|
async function _verifySignature(m, protectedPart, protectionAlg, protection, opts) {
|
|
470
|
-
// Bound the
|
|
484
|
+
// Bound the unsigned extraCerts once (dedup + drop non-certificates + cap + scan cap) before either the
|
|
471
485
|
// senderKID signer search or path building, so a hostile peer padding extraCerts cannot force unbounded
|
|
472
486
|
// X.509 parsing or a candidate-pool overflow.
|
|
473
487
|
var extra = _boundExtraCerts(m.extraCerts);
|
|
474
488
|
var signer = _resolveSignerCert(m, opts, extra);
|
|
475
489
|
if (!signer) return _fail(m, "signature", protectionAlg, "cmp/signer-cert-not-found", "no signer certificate resolved (opts.signerCert, senderKID, or extraCerts)");
|
|
476
490
|
|
|
477
|
-
// M2/M9: verify over the reconstructed ProtectedPart through the
|
|
491
|
+
// M2/M9: verify over the reconstructed ProtectedPart through the single path-validate engine (the EdDSA
|
|
478
492
|
// low-order-point gate + the sig-OID<->key-OID algorithm-confusion gate apply); fail-closed to false.
|
|
479
493
|
var ok = await _engine.verifyWithSpki(protectionAlg, protection.bytes, signer.spki, protectedPart);
|
|
480
494
|
if (ok !== true) return _fail(m, "signature", protectionAlg, "cmp/protection-failed", "the protection signature does not verify over the ProtectedPart under the declared protectionAlg", signer);
|
|
@@ -487,18 +501,18 @@ async function _verifySignature(m, protectedPart, protectionAlg, protection, opt
|
|
|
487
501
|
return _fail(m, "signature", protectionAlg, "cmp/sender-mismatch", "the header sender field does not match the signer certificate subject, or (for an empty subject) a subjectAltName entry (RFC 9483 sec. 3.1, RFC 5280 sec. 7.1)", signer);
|
|
488
502
|
}
|
|
489
503
|
|
|
490
|
-
// A failed opt-in echo check is a
|
|
504
|
+
// A failed opt-in echo check is a rejection verdict (valid:false), consistent with the MAC path: a caller
|
|
491
505
|
// that requested a transactionID / recipNonce binding must not see valid:true when it does not hold.
|
|
492
506
|
var hc = _headerChecks(m, opts);
|
|
493
507
|
if (hc) return _fail(m, "signature", protectionAlg, hc.code, hc.reason, signer);
|
|
494
508
|
|
|
495
509
|
// M5/M12: with no trust store the verdict is crypto-only (trusted:false) and the signer is surfaced for
|
|
496
|
-
// the caller to anchor. With a trust store the signer cert gets the
|
|
510
|
+
// the caller to anchor. With a trust store the signer cert gets the full RFC 5280 sec. 6.1 path gates.
|
|
497
511
|
if (opts.trustAnchors == null) return _ok(m, "signature", protectionAlg, false, signer);
|
|
498
512
|
var trust = await _chainSigner(signer, m, opts, extra);
|
|
499
|
-
// Surface the
|
|
500
|
-
// caller can cache exactly the certificates that established trust
|
|
501
|
-
// can pad
|
|
513
|
+
// Surface the chain that actually validated (signer + the intermediates path.build used) on a trusted verdict,
|
|
514
|
+
// so a caller can cache exactly the certificates that established trust, not the unsigned extraCerts a peer
|
|
515
|
+
// can pad: an appended-but-unused certificate never enters this chain.
|
|
502
516
|
if (trust.chain) signer.chain = trust.chain;
|
|
503
517
|
return _verdict(m, "signature", protectionAlg, true, trust.trusted, trust.trusted ? null : "cmp/untrusted-signer", trust.reason, signer);
|
|
504
518
|
}
|
|
@@ -526,26 +540,26 @@ async function _chainSigner(signer, m, opts, extra) {
|
|
|
526
540
|
if (!_keyUsageAllowsSigning(signer.parsed)) {
|
|
527
541
|
return { trusted: false, reason: "the signer certificate keyUsage does not assert digitalSignature (RFC 9483 sec. 3.2)" };
|
|
528
542
|
}
|
|
529
|
-
// Validate the signer path at a
|
|
543
|
+
// Validate the signer path at a trusted current time by default, never the message's self-asserted
|
|
530
544
|
// messageTime, which the sender controls: a holder of a now-expired but once-valid signer certificate
|
|
531
545
|
// could otherwise backdate messageTime into the certificate's validity window and be reported trusted.
|
|
532
|
-
// A caller doing historical verification opts into a specific instant via opts.time; a falsy but
|
|
533
|
-
// opts.time (0 / false / "") is
|
|
546
|
+
// A caller doing historical verification opts into a specific instant via opts.time; a falsy but present
|
|
547
|
+
// opts.time (0 / false / "") is never silently replaced: it reaches path.build and is rejected as bad-input.
|
|
534
548
|
var time = opts.time != null ? opts.time : new Date();
|
|
535
549
|
var anchors = _certList(opts.trustAnchors);
|
|
536
550
|
// `extra` is the already dedup + validity + count-bounded extraCerts (unsigned pool material), so a flood
|
|
537
551
|
// of duplicate / junk / non-certificate entries cannot exhaust the candidate-search budget or raise a
|
|
538
|
-
// path/bad-input from a malformed pool certificate. Add it only up to the path-builder pool ceiling
|
|
552
|
+
// path/bad-input from a malformed pool certificate. Add it only up to the path-builder pool ceiling, after
|
|
539
553
|
// the caller's intermediates, so unsigned extraCerts can never push a valid caller configuration over the
|
|
540
554
|
// ceiling (if the caller's intermediates alone exceed it, that is a genuine config error path.build reports).
|
|
541
555
|
var pool = _certList(opts.intermediates);
|
|
542
556
|
var room = constants.LIMITS.PATH_BUILD_MAX_CANDIDATES - pool.length;
|
|
543
557
|
if (room > 0) {
|
|
544
558
|
// Drop the signer leaf (already passed to build as the path TARGET) and any extraCert duplicating a caller
|
|
545
|
-
// intermediate
|
|
546
|
-
// useful embedded issuers
|
|
559
|
+
// intermediate before truncating to the remaining slots, so a tight ceiling spends those slots on genuinely
|
|
560
|
+
// useful embedded issuers, not on a redundant copy of the signer or a duplicate the pool already holds.
|
|
547
561
|
// The identity is the canonical tbs+signature key so a Buffer extraCert dedups against a caller intermediate
|
|
548
|
-
// supplied either as raw bytes
|
|
562
|
+
// supplied either as raw bytes or as an already-parsed certificate object (path.build accepts both).
|
|
549
563
|
var seen = Object.create(null);
|
|
550
564
|
var sk = _certKey(signer.parsed); if (sk) seen[sk] = 1;
|
|
551
565
|
pool.forEach(function (c) { var k = _certKey(c); if (k) seen[k] = 1; });
|
|
@@ -560,9 +574,9 @@ async function _chainSigner(signer, m, opts, extra) {
|
|
|
560
574
|
// res.path is the ordered certificates path.build assembled (signer + the intermediates it used), as PARSED
|
|
561
575
|
// objects whose byte fields are subarrays of the (possibly multi-MB) response allocation. Map each back to its
|
|
562
576
|
// source DER (the signer, or a DER pool entry) and return an INDEPENDENT copy, so a caller caching the chain
|
|
563
|
-
// holds standalone buffers
|
|
577
|
+
// holds standalone buffers, never slices pinning the whole response. Only certs on the trusted path enter it,
|
|
564
578
|
// so unsigned extraCerts padding is excluded. (A path cert sourced from an already-parsed intermediate carries
|
|
565
|
-
// no DER to copy and is omitted; for a Buffer/DER pool
|
|
579
|
+
// no DER to copy and is omitted; for a Buffer/DER pool, the cmp.session case, the chain is complete.)
|
|
566
580
|
var byKey = Object.create(null);
|
|
567
581
|
var skey = _certKey(signer.parsed); if (skey) byKey[skey] = signer.der;
|
|
568
582
|
pool.forEach(function (c) { if (Buffer.isBuffer(c) || c instanceof Uint8Array) { var k = _certKey(c); if (k && !byKey[k]) byKey[k] = c; } });
|
|
@@ -570,7 +584,7 @@ async function _chainSigner(signer, m, opts, extra) {
|
|
|
570
584
|
return { trusted: true, reason: null, chain: chain };
|
|
571
585
|
} catch (e) {
|
|
572
586
|
// A config-tier fault from path.build (an invalid opts.time, an empty / malformed trustAnchors or
|
|
573
|
-
// intermediate) is a
|
|
587
|
+
// intermediate) is a deployment error, not an untrusted signer, so rethrow it as cmp/bad-input and it is
|
|
574
588
|
// not masked as a routine trust failure (the API contract: a bad required opt throws). A genuine
|
|
575
589
|
// no-assemblable-path / validation failure collapses to an untrusted-signer verdict.
|
|
576
590
|
if (e && e.code === "path/bad-input") {
|
|
@@ -643,7 +657,7 @@ async function _verify(message, opts) {
|
|
|
643
657
|
return _fail(m, null, protectionAlg, "cmp/no-protection", "the PKIMessage carries no protection (RFC 9810 sec. 5.1.3); an unprotected message is never verified");
|
|
644
658
|
}
|
|
645
659
|
|
|
646
|
-
// M2: reconstruct the ProtectedPart from the
|
|
660
|
+
// M2: reconstruct the ProtectedPart from the raw surfaced slices, byte-identical to what build signed,
|
|
647
661
|
// never a re-serialization of the decoded header/body structs.
|
|
648
662
|
var protectedPart = b.sequence([b.raw(m.headerBytes), b.raw(m.bodyBytes)]);
|
|
649
663
|
|
|
@@ -678,7 +692,7 @@ async function _verify(message, opts) {
|
|
|
678
692
|
* @related pki.cmp.build, pki.schema.cmp.parse
|
|
679
693
|
* @defends cmp-unverified-protection (CWE-347), cmp-mac-timing (CWE-208)
|
|
680
694
|
*
|
|
681
|
-
* Verify the protection on an incoming RFC 9810 CMP `PKIMessage
|
|
695
|
+
* Verify the protection on an incoming RFC 9810 CMP `PKIMessage`, the verify-inverse of
|
|
682
696
|
* `pki.cmp.build`. `message` is a raw DER `Buffer`, a PEM `CMP` string, or an already-parsed
|
|
683
697
|
* `pki.schema.cmp.parse` result (the protection is always recomputed from the parser-surfaced raw
|
|
684
698
|
* `headerBytes` / `bodyBytes`, so a mutated display field on a parsed object cannot desync the crypto).
|
|
@@ -693,7 +707,7 @@ async function _verify(message, opts) {
|
|
|
693
707
|
* protection is cryptographically intact under the declared algorithm; `trusted` is whether a MAC secret
|
|
694
708
|
* matched or a signature signer certificate chained to a supplied trust anchor. On a trusted signature
|
|
695
709
|
* verdict `signer.chain` is the validated certificate path as independent DER buffers (the signer plus the
|
|
696
|
-
* intermediates that chained it to the anchor)
|
|
710
|
+
* intermediates that chained it to the anchor): the certificates actually used, never the unsigned
|
|
697
711
|
* `extraCerts` a peer can pad, and never a slice pinning the response allocation. A
|
|
698
712
|
* well-formed but unverifiable message is a `{ valid: false }` verdict carrying a `cmp/*` code, not a throw;
|
|
699
713
|
* only malformed input (a non-PKIMessage, a bad required opt, a flavor/credential mismatch) throws a typed `CmpError`.
|
|
@@ -706,11 +720,11 @@ async function _verify(message, opts) {
|
|
|
706
720
|
* (RFC 5280 sec. 6.1 plus the `keyUsage.digitalSignature` gate) to report `trusted`; absent -> the
|
|
707
721
|
* verdict is crypto-only (`trusted: false`) and the signer certificate is surfaced for the caller to
|
|
708
722
|
* anchor. The signature verify never routes through build's self-check (which skips the EdDSA
|
|
709
|
-
* low-order-point gate)
|
|
723
|
+
* low-order-point gate); it uses the same engine `pki.crl.verify` / `pki.ocsp.verify` do.
|
|
710
724
|
* - `intermediates` (Buffer|Buffer[]|PEM) -- extra untrusted pool certificates for path building
|
|
711
725
|
* (`extraCerts` are added automatically, as untrusted pool material).
|
|
712
726
|
* - `time` (Date) -- the validity instant for path validation. Defaults to the current time (the message's
|
|
713
|
-
* self-asserted `messageTime` is
|
|
727
|
+
* self-asserted `messageTime` is not trusted for this); pass an explicit instant for historical verification.
|
|
714
728
|
* - `transactionID` (Buffer) -- opt-in: require `header.transactionID` to equal it (response-echo defense).
|
|
715
729
|
* - `expectRecipNonce` (Buffer) -- opt-in: require `header.recipNonce` to echo the sent sender nonce.
|
|
716
730
|
* - `revocationChecker` -- forwarded to `pki.path.validate` when chaining the signer certificate.
|
package/lib/cms-compress.js
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
// absent-or-NULL params (RFC 3274 sec. 2), and BOUNDED-inflates the eContent. There is NO crypto
|
|
13
13
|
// here -- CompressedData provides no integrity / confidentiality / authentication (RFC 8551 sec.
|
|
14
14
|
// 2.4.5); it is purely a size transform. The one load-bearing defense is the decompression-bomb
|
|
15
|
-
// cap: inflate stops at C.LIMITS.COMPRESS_MAX_BYTES
|
|
15
|
+
// cap: inflate stops at C.LIMITS.COMPRESS_MAX_BYTES before the output is materialized (a
|
|
16
16
|
// resource-exhaustion defense, CWE-409), and every zlib fault collapses to a uniform typed error.
|
|
17
17
|
|
|
18
18
|
var zlib = require("zlib");
|
|
@@ -70,7 +70,7 @@ async function decompress(input, opts) {
|
|
|
70
70
|
if (parsed.contentTypeName !== "compressedData") throw _err("cms/unsupported-content-type", "input is not a CMS CompressedData (got " + parsed.contentTypeName + ")");
|
|
71
71
|
var alg = parsed.compressionAlgorithm;
|
|
72
72
|
if (alg.oid !== OID_ZLIB) throw _err("cms/unsupported-algorithm", "unsupported compressionAlgorithm " + (alg.name || alg.oid) + " (only id-alg-zlibCompress, RFC 3274 sec. 2)");
|
|
73
|
-
// RFC 3274 sec. 2: the parameters SHOULD be omitted but MAY be NULL
|
|
73
|
+
// RFC 3274 sec. 2: the parameters SHOULD be omitted but MAY be NULL. Reject any other encoding.
|
|
74
74
|
if (alg.parameters != null && Buffer.compare(alg.parameters, NULL_PARAMS_DER) !== 0) throw _err("cms/bad-algorithm-parameters", "id-alg-zlibCompress parameters must be absent or NULL (RFC 3274 sec. 2)");
|
|
75
75
|
var eci = parsed.encapContentInfo;
|
|
76
76
|
if (eci.eContent == null) throw _err("cms/no-encapsulated-content", "the CompressedData carries no encapsulated content (a detached CompressedData cannot be decompressed)");
|
|
@@ -92,12 +92,12 @@ function _toDer(input) {
|
|
|
92
92
|
}
|
|
93
93
|
|
|
94
94
|
// Inflate the RFC 1950 ZLIB stream through the shared bounded-decompression guard, which applies
|
|
95
|
-
// the output cap at the decompressor (so a bomb is refused mid-stream
|
|
96
|
-
//
|
|
97
|
-
//
|
|
98
|
-
//
|
|
95
|
+
// the output cap at the decompressor (so a bomb is refused mid-stream, before it is allocated) and
|
|
96
|
+
// requires the whole octet string to be exactly one compressed frame (so trailing bytes cannot give
|
|
97
|
+
// one content a second encoding). This module keeps its own verdict codes: a cap breach is
|
|
98
|
+
// cms/decompress-too-large, every other fault the uniform cms/decompress-failed.
|
|
99
99
|
//
|
|
100
|
-
// `_err` is a
|
|
100
|
+
// `_err` is a factory and the guard calls it as one. Handing it CmsError instead would throw
|
|
101
101
|
// "class cannot be invoked without new" on the rejection path, turning a refusal into a crash.
|
|
102
102
|
function _inflateBounded(stream, cap) {
|
|
103
103
|
var view = guard.bytes.view(stream, CmsError, "cms/decompress-failed", "the compressed content");
|