@blamejs/pki 0.5.15 → 0.5.16
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 +27 -0
- package/lib/cmp-verify.js +425 -82
- package/lib/guard-all.js +2 -0
- package/lib/guard-async.js +14 -2
- package/lib/guard-bytes.js +183 -93
- package/lib/guard-compress.js +32 -12
- package/lib/guard-der.js +30 -7
- package/lib/guard-encoding.js +28 -12
- package/lib/guard-header.js +18 -7
- package/lib/guard-identifier.js +146 -79
- package/lib/guard-intrinsic.js +318 -0
- package/lib/guard-json.js +39 -12
- package/lib/guard-limits.js +15 -6
- package/lib/guard-name.js +72 -23
- package/lib/guard-parsed.js +71 -34
- package/lib/guard-range.js +17 -4
- package/lib/guard-secret.js +15 -1
- package/lib/guard-text.js +35 -6
- package/lib/guard-time.js +13 -4
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
package/lib/cmp-verify.js
CHANGED
|
@@ -19,6 +19,54 @@
|
|
|
19
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
|
+
// The slot predicates and the array test, snapshotted at load through the guard family's own home
|
|
23
|
+
// for that. Both decide whether a caller's value gets COPIED, so reading either at call time hands
|
|
24
|
+
// the copy decision to the caller: an `isUint8Array` answering false leaves a live caller buffer
|
|
25
|
+
// where the copy belongs and leaves that buffer out of the wipe list, and an `Array.isArray`
|
|
26
|
+
// answering false routes a trust list past the list door and back out by reference.
|
|
27
|
+
var intrinsic = require("./guard-intrinsic");
|
|
28
|
+
var _sizeOf = intrinsic.sizeOf;
|
|
29
|
+
var util = { types: intrinsic.types };
|
|
30
|
+
// The rest of what this verb reads from the runtime while it is reducing a caller's options, taken
|
|
31
|
+
// at load. The option door decides what gets copied and what gets refused, which is the same kind
|
|
32
|
+
// of decision a guard makes, so it is held to the same rule: the check over lib/ that refuses a
|
|
33
|
+
// live read covers every module importing these, and this one does. See guard-intrinsic.
|
|
34
|
+
var _isBuffer = intrinsic.isBuffer;
|
|
35
|
+
var _bufferFrom = intrinsic.bufferFrom;
|
|
36
|
+
var _fromCharCode = intrinsic.fromCharCode;
|
|
37
|
+
var _stringify = intrinsic.stringify;
|
|
38
|
+
var _getOwnPropertyNames = intrinsic.getOwnPropertyNames;
|
|
39
|
+
var _charAt = intrinsic.uncurry(String.prototype.charAt);
|
|
40
|
+
var _create = intrinsic.create;
|
|
41
|
+
var _floor = intrinsic.floor;
|
|
42
|
+
var _ceil = intrinsic.ceil;
|
|
43
|
+
var _min = intrinsic.min;
|
|
44
|
+
var _charCodeAt = intrinsic.uncurry(String.prototype.charCodeAt);
|
|
45
|
+
var _toLowerCase = intrinsic.uncurry(String.prototype.toLowerCase);
|
|
46
|
+
var _strIndexOf = intrinsic.uncurry(String.prototype.indexOf);
|
|
47
|
+
var _compare = intrinsic.compare;
|
|
48
|
+
// The byte-to-text step of the SAN comparison below, and of the certificate identity key. One
|
|
49
|
+
// returning a constant makes every sender name equal every SAN, and collapses every certificate in
|
|
50
|
+
// the pool to one key.
|
|
51
|
+
var _toString = intrinsic.uncurry(Buffer.prototype.toString);
|
|
52
|
+
var _map = intrinsic.map;
|
|
53
|
+
// The conversions, captured for the same reason as everything above them: `String` is a writable
|
|
54
|
+
// property of globalThis, it renders the sender name that the SAN comparison compares, and it
|
|
55
|
+
// decides which own property names count as array indices in the density check below.
|
|
56
|
+
var _String = intrinsic.String;
|
|
57
|
+
var _isFinite = intrinsic.isFinite;
|
|
58
|
+
// The rest of the string and pattern surface the rfc822Name and URI comparisons below are built
|
|
59
|
+
// out of. Those comparisons decide whether a signer is allowed to speak for a sender, and every
|
|
60
|
+
// step of them -- where the mailbox separator is, which substring is the domain, whether a
|
|
61
|
+
// local-part is well-formed, how a URI splits into scheme and authority -- is one of these calls.
|
|
62
|
+
// A replacement anywhere in the chain moves the boundary, and the identity the verb ends up
|
|
63
|
+
// comparing is no longer the one the certificate carries.
|
|
64
|
+
var _strSlice = intrinsic.uncurry(String.prototype.slice);
|
|
65
|
+
var _strLastIndexOf = intrinsic.uncurry(String.prototype.lastIndexOf);
|
|
66
|
+
var _strSearch = intrinsic.uncurry(String.prototype.search);
|
|
67
|
+
var _reTest = intrinsic.uncurry(RegExp.prototype.test);
|
|
68
|
+
var _reExec = intrinsic.uncurry(RegExp.prototype.exec);
|
|
69
|
+
var _isArray = Array.isArray;
|
|
22
70
|
var asn1 = require("./asn1-der");
|
|
23
71
|
var oid = require("./oid");
|
|
24
72
|
var cmpBuild = require("./cmp-build");
|
|
@@ -54,7 +102,10 @@ var KNOWN_VERIFY_OPTS = {
|
|
|
54
102
|
// omitted-PRF that resolves to it, is unsupported and never MAC-verified under a weak digest).
|
|
55
103
|
// Keyed by the immutable dotted OID, never the display name: pki.oid.register() can rename hmacWithSHA256/384/512,
|
|
56
104
|
// so a name-keyed lookup would reject a valid PBMAC1 message under a renamed registry (the immutable-OID-dispatch rule).
|
|
57
|
-
|
|
105
|
+
// Null-prototype, because this table is read as an allowlist: on a `{}` a planted
|
|
106
|
+
// `Object.prototype["1.2.3"]` names a hash for a PRF OID the table never carried, and an
|
|
107
|
+
// unsupported PRF is then admitted and dispatched.
|
|
108
|
+
var PRF_HASH = _create(null);
|
|
58
109
|
PRF_HASH[oid.byName("hmacWithSHA256")] = "SHA-256";
|
|
59
110
|
PRF_HASH[oid.byName("hmacWithSHA384")] = "SHA-384";
|
|
60
111
|
PRF_HASH[oid.byName("hmacWithSHA512")] = "SHA-512";
|
|
@@ -72,7 +123,7 @@ var PRF_HLEN = { "SHA-256": 32, "SHA-384": 48, "SHA-512": 64 };
|
|
|
72
123
|
// The legacy / KEM MAC protection OIDs (RFC 9810 sec. 5.1.3.1/.2/.4) the v1 verifier recognizes and refuses.
|
|
73
124
|
// Build emits only PBMAC1, so the verifier rejects a legacy/KEM MAC OID and never accepts a construction
|
|
74
125
|
// it does not verify (a silent accept of an unverified algorithm would be fail-open).
|
|
75
|
-
var UNSUPPORTED_MAC_OIDS =
|
|
126
|
+
var UNSUPPORTED_MAC_OIDS = _create(null);
|
|
76
127
|
["passwordBasedMac", "dhBasedMac", "kemBasedMac"].forEach(function (n) {
|
|
77
128
|
var o = oid.byName(n);
|
|
78
129
|
if (o) UNSUPPORTED_MAC_OIDS[o] = n;
|
|
@@ -115,7 +166,7 @@ function _fail(m, type, alg, code, reason, signer) { return _verdict(m, type, al
|
|
|
115
166
|
// extraCerts) and re-parsed, so every security-relevant and returned field derives from the authenticated
|
|
116
167
|
// bytes, never the caller's decoded representation.
|
|
117
168
|
function _coerce(message) {
|
|
118
|
-
if (message && typeof message === "object" && !
|
|
169
|
+
if (message && typeof message === "object" && !_isBuffer(message) && !util.types.isUint8Array(message) &&
|
|
119
170
|
message.headerBytes !== undefined && message.bodyBytes !== undefined &&
|
|
120
171
|
message.header !== undefined && message.body !== undefined) {
|
|
121
172
|
// A malformed raw slice (a null protection.bytes, a non-buffer extraCerts entry) makes _reassemble throw a
|
|
@@ -128,7 +179,9 @@ function _coerce(message) {
|
|
|
128
179
|
// change the returned authenticated fields (transactionID, nonces, header, body) while valid stays true.
|
|
129
180
|
// Parse a PRIVATE copy so every verdict field is bound to the verified snapshot, never the caller's mutable
|
|
130
181
|
// buffer. (A PEM string is immutable and base64-decodes into fresh bytes, so it needs no copy.)
|
|
131
|
-
|
|
182
|
+
// The slot, not the prototype: this decides whether the message is privately copied before it is
|
|
183
|
+
// parsed, and `instanceof` consults `Uint8Array[Symbol.hasInstance]` at call time.
|
|
184
|
+
if (_isBuffer(message) || util.types.isUint8Array(message)) return cmp.parse(_bufferFrom(message));
|
|
132
185
|
return cmp.parse(message); // a PEM CMP string (throws cmp/* on malformed input)
|
|
133
186
|
}
|
|
134
187
|
|
|
@@ -138,16 +191,18 @@ function _coerce(message) {
|
|
|
138
191
|
// field against the raw bytes, discarding any mutated decoded field the caller may have carried.
|
|
139
192
|
function _reassemble(m) {
|
|
140
193
|
var kids = [b.raw(m.headerBytes), b.raw(m.bodyBytes)];
|
|
141
|
-
if (m.protection != null) kids
|
|
194
|
+
if (m.protection != null) _append(kids, b.explicit(0, b.bitString(m.protection.bytes, m.protection.unusedBits)));
|
|
142
195
|
if (m.extraCerts != null && m.extraCerts.length) {
|
|
143
|
-
|
|
196
|
+
// Through the captured `map`: this rebuilds the exact bytes the protection is checked against,
|
|
197
|
+
// so a replacement returning a different array decides what the signature is verified over.
|
|
198
|
+
_append(kids, b.explicit(1, b.sequence(_map(m.extraCerts, function (c) { return b.raw(c); }))));
|
|
144
199
|
}
|
|
145
200
|
return b.sequence(kids);
|
|
146
201
|
}
|
|
147
202
|
|
|
148
203
|
// A certificate DER from a Buffer/Uint8Array (detached-view-safe) or a PEM string.
|
|
149
204
|
function _certDer(cert, what) {
|
|
150
|
-
if (
|
|
205
|
+
if (_isBuffer(cert) || util.types.isUint8Array(cert)) return guard.bytes.view(cert, CmpError, "cmp/bad-input", what);
|
|
151
206
|
if (typeof cert === "string") { try { return x509.pemDecode(cert); } catch (e) { throw _err("cmp/bad-input", what + " PEM could not be decoded", e); } }
|
|
152
207
|
throw _err("cmp/bad-input", what + " must be a certificate DER Buffer/Uint8Array or PEM string");
|
|
153
208
|
}
|
|
@@ -194,7 +249,7 @@ function _senderBoundToCert(sender, parsed) {
|
|
|
194
249
|
if (!sender || !sender.bytes) return false;
|
|
195
250
|
var subjectRdns = parsed.subject.rdns;
|
|
196
251
|
if (subjectRdns.length > 0) {
|
|
197
|
-
var isDirName = sender.tagClass === "context" && sender.tagNumber === 4 && sender.value &&
|
|
252
|
+
var isDirName = sender.tagClass === "context" && sender.tagNumber === 4 && sender.value && _isArray(sender.value.rdns);
|
|
198
253
|
if (isDirName && guard.name.dnEqual(sender.value.rdns, subjectRdns, NS.E, "cmp/sender-mismatch", "the header sender / signer subject")) return true;
|
|
199
254
|
}
|
|
200
255
|
var san = _sanGeneralNames(parsed);
|
|
@@ -212,28 +267,28 @@ function _generalNameMatches(sender, sanNode) {
|
|
|
212
267
|
if (sanNode.tagClass !== sender.tagClass || sanNode.tagNumber !== sender.tagNumber) return false;
|
|
213
268
|
if (sanNode.tagClass === "context") {
|
|
214
269
|
if (sanNode.tagNumber === 2 && sanNode.content) { // dNSName IA5String
|
|
215
|
-
var dnsSan = sanNode.content
|
|
270
|
+
var dnsSan = _toString(sanNode.content, "latin1"), dnsSender = _String(sender.value);
|
|
216
271
|
// Case-fold only a well-formed dNSName (RFC 5280 sec. 4.2.1.6 / RFC 1034, via the shared validator): a
|
|
217
272
|
// malformed name (an empty label like "victim..com", a leading/trailing dot, bad LDH) is compared
|
|
218
273
|
// byte-exact and never folded, so it cannot bind a byte-distinct identity.
|
|
219
274
|
if (pkix.dnsNameProblem(dnsSan) !== null || pkix.dnsNameProblem(dnsSender) !== null) return dnsSan === dnsSender;
|
|
220
|
-
return dnsSender
|
|
275
|
+
return _toLowerCase(dnsSender) === _toLowerCase(dnsSan);
|
|
221
276
|
}
|
|
222
277
|
if (sanNode.tagNumber === 1 && sanNode.content) { // rfc822Name IA5String: case-insensitive domain
|
|
223
|
-
return _rfc822Equal(
|
|
278
|
+
return _rfc822Equal(_String(sender.value), _toString(sanNode.content, "latin1"));
|
|
224
279
|
}
|
|
225
280
|
if (sanNode.tagNumber === 6 && sanNode.content) { // uniformResourceIdentifier IA5String: case-insensitive scheme + host
|
|
226
|
-
return _uriEqual(
|
|
281
|
+
return _uriEqual(_String(sender.value), _toString(sanNode.content, "latin1"));
|
|
227
282
|
}
|
|
228
283
|
if (sanNode.tagNumber === 4 && sanNode.children && sanNode.children.length) { // directoryName: canonical DN
|
|
229
|
-
if (!sender.value || !
|
|
284
|
+
if (!sender.value || !_isArray(sender.value.rdns)) return false;
|
|
230
285
|
var sanName;
|
|
231
286
|
try { sanName = schema.embeddedDer(pkix.name(NS), sanNode.children[0].bytes, NS, { code: "cmp/sender-mismatch", what: "SAN directoryName" }).result; }
|
|
232
287
|
catch (_e) { return false; }
|
|
233
288
|
return guard.name.dnEqual(sender.value.rdns, sanName.rdns, NS.E, "cmp/sender-mismatch", "SAN directoryName");
|
|
234
289
|
}
|
|
235
290
|
}
|
|
236
|
-
return sanNode.bytes
|
|
291
|
+
return _compare(sanNode.bytes, sender.bytes) === 0;
|
|
237
292
|
}
|
|
238
293
|
|
|
239
294
|
// Index of the "@" separating an RFC 5321 addr-spec Local-part from its Domain, honoring a Quoted-string
|
|
@@ -242,30 +297,30 @@ function _generalNameMatches(sender, sanNode) {
|
|
|
242
297
|
// local-part with more than one "@") so the caller compares byte-exact.
|
|
243
298
|
function _mailboxSplit(s) {
|
|
244
299
|
var sep;
|
|
245
|
-
if (s
|
|
300
|
+
if (_charAt(s,0) === "\"") {
|
|
246
301
|
var i = 1;
|
|
247
302
|
while (i < s.length) {
|
|
248
|
-
var c = s
|
|
303
|
+
var c = _charAt(s,i);
|
|
249
304
|
if (c === "\\") { i += 2; continue; } // an escaped pair (\c) -- skip both octets
|
|
250
305
|
if (c === "\"") break;
|
|
251
306
|
i++;
|
|
252
307
|
}
|
|
253
|
-
if (i >= s.length || s
|
|
308
|
+
if (i >= s.length || _charAt(s,i) !== "\"" || _charAt(s,i + 1) !== "@") return -1; // unterminated / not "@"-separated
|
|
254
309
|
sep = i + 1;
|
|
255
310
|
} else {
|
|
256
|
-
sep = s
|
|
257
|
-
if (sep < 0 || sep !== s
|
|
311
|
+
sep = _strIndexOf(s, "@"); // an unquoted (Dot-string) local-part cannot contain a raw "@" -> exactly one total
|
|
312
|
+
if (sep < 0 || sep !== _strLastIndexOf(s, "@")) return -1;
|
|
258
313
|
}
|
|
259
314
|
// The Domain that follows the separator must be a well-formed FQDN (RFC 1034, via the shared validator): an
|
|
260
315
|
// empty / empty-label / extra-"@" domain is malformed. An unquoted Local-part must be a Dot-string: non-empty,
|
|
261
316
|
// no leading/trailing dot, no empty atom ("a..b"). A quoted Local-part is already balanced above. A malformed
|
|
262
317
|
// mailbox returns -1 so _rfc822Equal compares it byte-exact and never case-folds a malformed domain.
|
|
263
|
-
var local = s
|
|
318
|
+
var local = _strSlice(s, 0, sep), domain = _strSlice(s, sep + 1);
|
|
264
319
|
if (pkix.dnsNameProblem(domain) !== null) return -1;
|
|
265
320
|
// An unquoted (Dot-string) Local-part is atext atoms joined by single dots (RFC 5321 sec. 4.1.2): every
|
|
266
321
|
// character must be an atext octet or a "." with no empty atom / leading-trailing dot. A malformed atom
|
|
267
322
|
// character (e.g. a space in "user name") makes the whole address malformed -> byte-exact comparison.
|
|
268
|
-
if (s
|
|
323
|
+
if (_charAt(s, 0) !== "\"" && (local.length === 0 || _charAt(local, 0) === "." || _charAt(local, local.length - 1) === "." || _strIndexOf(local, "..") !== -1 || !_reTest(/^[A-Za-z0-9!#$%&'*+/=?^_`{|}~.-]+$/, local))) return -1;
|
|
269
324
|
return sep;
|
|
270
325
|
}
|
|
271
326
|
|
|
@@ -275,7 +330,8 @@ function _mailboxSplit(s) {
|
|
|
275
330
|
function _rfc822Equal(a, b) {
|
|
276
331
|
var ai = _mailboxSplit(a), bi = _mailboxSplit(b);
|
|
277
332
|
if (ai < 0 || bi < 0) return a === b;
|
|
278
|
-
return a
|
|
333
|
+
return _strSlice(a, 0, ai) === _strSlice(b, 0, bi) &&
|
|
334
|
+
_lowerAsciiDomain(_strSlice(a, ai + 1)) === _lowerAsciiDomain(_strSlice(b, bi + 1));
|
|
279
335
|
}
|
|
280
336
|
|
|
281
337
|
// sec. 7.5 authorizes a case-insensitive ASCII comparison of the host-part, and nothing
|
|
@@ -286,8 +342,8 @@ function _rfc822Equal(a, b) {
|
|
|
286
342
|
function _lowerAsciiDomain(h) {
|
|
287
343
|
var out = "";
|
|
288
344
|
for (var i = 0; i < h.length; i++) {
|
|
289
|
-
var c = h
|
|
290
|
-
out += (c >= 0x41 && c <= 0x5a) ?
|
|
345
|
+
var c = _charCodeAt(h, i);
|
|
346
|
+
out += (c >= 0x41 && c <= 0x5a) ? _fromCharCode(c + 32) : _charAt(h, i);
|
|
291
347
|
}
|
|
292
348
|
return out;
|
|
293
349
|
}
|
|
@@ -300,49 +356,49 @@ function _normalizeUri(u) {
|
|
|
300
356
|
// RFC 3986 syntax: every character must be a URI character and every "%" must introduce a valid pct-encoding
|
|
301
357
|
// (%HH). A URI with an out-of-charset byte or a malformed percent-escape ("%zz") in any component (path,
|
|
302
358
|
// query, fragment, userinfo) is not normalized; return null so it is compared byte-exact.
|
|
303
|
-
if (
|
|
304
|
-
var m = /^([a-zA-Z][a-zA-Z0-9+.-]*):(.*)
|
|
359
|
+
if (!_reTest(/^[A-Za-z0-9._~:/?#@!$&'()*+,;=%[\]-]*$/, u) || _reTest(/%(?![0-9A-Fa-f]{2})/, u)) return null;
|
|
360
|
+
var m = _reExec(/^([a-zA-Z][a-zA-Z0-9+.-]*):(.*)$/, u); // scheme ":" rest (RFC 3986 sec. 3.1)
|
|
305
361
|
if (!m) return null;
|
|
306
|
-
var scheme = m[1]
|
|
362
|
+
var scheme = _toLowerCase(m[1]);
|
|
307
363
|
var rest = m[2];
|
|
308
364
|
// A URI SAN identity is host-based (RFC 5280 sec. 4.2.1.6): an authority-free URI (no "//", e.g. a URN) has
|
|
309
365
|
// no host to anchor the comparison, so compare it byte-exact and never case-fold its scheme; the shared
|
|
310
366
|
// GeneralName parser only enforces printable IA5, not a present authority.
|
|
311
|
-
if (rest
|
|
312
|
-
var body = rest
|
|
313
|
-
var cut = body
|
|
367
|
+
if (_strSlice(rest, 0, 2) !== "//") return null;
|
|
368
|
+
var body = _strSlice(rest, 2);
|
|
369
|
+
var cut = _strSearch(body, /[/?#]/); // authority ends at the first "/", "?" or "#" (RFC 3986 sec. 3.2)
|
|
314
370
|
var split = cut < 0 ? body.length : cut;
|
|
315
|
-
var authority = body
|
|
371
|
+
var authority = _strSlice(body, 0, split), tail = _strSlice(body, split); // tail: path/query/fragment -- exact
|
|
316
372
|
// A raw "@" is not permitted inside userinfo (RFC 3986 sec. 3.2.1); more than one "@" is a MALFORMED
|
|
317
373
|
// authority -> return null so it is compared byte-exact, never split into a userinfo that folds the host.
|
|
318
|
-
if (authority
|
|
319
|
-
var at = authority
|
|
320
|
-
var userinfo = at < 0 ? "" : authority
|
|
321
|
-
var hostport = at < 0 ? authority : authority
|
|
374
|
+
if (_strIndexOf(authority, "@") !== _strLastIndexOf(authority, "@")) return null;
|
|
375
|
+
var at = _strLastIndexOf(authority, "@"); // [ userinfo "@" ] host [ ":" port ] -- userinfo stays case-sensitive
|
|
376
|
+
var userinfo = at < 0 ? "" : _strSlice(authority, 0, at + 1);
|
|
377
|
+
var hostport = at < 0 ? authority : _strSlice(authority, at + 1);
|
|
322
378
|
// Split the host from an optional ":port": an IPv6 literal "[...]" carries the port after the "]", otherwise
|
|
323
379
|
// the port follows the first ":". Case-fold only the host (RFC 3986 sec. 6.2.2.1); the port stays byte-exact
|
|
324
380
|
// so a malformed non-numeric port (":ADMIN" vs ":admin") is never folded into a false identity match.
|
|
325
381
|
var host, port;
|
|
326
|
-
if (hostport
|
|
327
|
-
var rb = hostport
|
|
382
|
+
if (_charAt(hostport, 0) === "[") {
|
|
383
|
+
var rb = _strIndexOf(hostport, "]");
|
|
328
384
|
if (rb < 0) return null; // an unterminated IPv6 literal is malformed -> exact-DER fallback (fail-closed)
|
|
329
|
-
host = hostport
|
|
385
|
+
host = _strSlice(hostport, 0, rb + 1); port = _strSlice(hostport, rb + 1);
|
|
330
386
|
} else {
|
|
331
|
-
var ci = hostport
|
|
332
|
-
host = ci < 0 ? hostport : hostport
|
|
387
|
+
var ci = _strIndexOf(hostport, ":");
|
|
388
|
+
host = ci < 0 ? hostport : _strSlice(hostport, 0, ci); port = ci < 0 ? "" : _strSlice(hostport, ci);
|
|
333
389
|
}
|
|
334
390
|
// The port must be numeric (RFC 3986 sec. 3.2.3 port = *DIGIT). A non-numeric port is a malformed authority:
|
|
335
391
|
// return null so the caller compares the raw values exactly and never case-folds the host of an input the
|
|
336
392
|
// normalizer cannot confidently parse (which would bind two byte-distinct malformed authorities).
|
|
337
|
-
if (port !== "" &&
|
|
393
|
+
if (port !== "" && !_reTest(/^:[0-9]*$/, port)) return null;
|
|
338
394
|
// The authority host must be a well-formed FQDN or IP literal (RFC 5280 sec. 4.2.1.6 requires a URI SAN
|
|
339
395
|
// authority to carry an FQDN/IP host): an empty, malformed ("Victim..COM"), or bad IPv6-literal host is
|
|
340
396
|
// compared byte-exact and never case-folded, since the GeneralName parser only enforces printable IA5.
|
|
341
|
-
var hostOk = host
|
|
342
|
-
? (
|
|
397
|
+
var hostOk = _charAt(host, 0) === "["
|
|
398
|
+
? (_charAt(host, host.length - 1) === "]" && ipUtils.expandIpv6Hex(_strSlice(host, 1, -1)) !== null)
|
|
343
399
|
: (pkix.dnsNameProblem(host) === null);
|
|
344
400
|
if (!hostOk) return null;
|
|
345
|
-
return scheme + "://" + userinfo + host
|
|
401
|
+
return scheme + "://" + userinfo + _toLowerCase(host) + port + tail;
|
|
346
402
|
}
|
|
347
403
|
function _uriEqual(a, b) {
|
|
348
404
|
var na = _normalizeUri(a), nb = _normalizeUri(b);
|
|
@@ -376,7 +432,7 @@ function _resolveSignerCert(m, opts, extra) {
|
|
|
376
432
|
// The caller explicitly selected this certificate, so the senderKID (a hint for narrowing a candidate search)
|
|
377
433
|
// is not applied here: the protection signature verification against this cert's key is the real gate; a
|
|
378
434
|
// valid message from a signer certificate that omits an SKI still resolves to its exact opts.signerCert.
|
|
379
|
-
var scDer =
|
|
435
|
+
var scDer = _bufferFrom(_certDer(opts.signerCert, "opts.signerCert"));
|
|
380
436
|
var scParsed;
|
|
381
437
|
try { scParsed = x509.parse(scDer); }
|
|
382
438
|
catch (e) { throw _err("cmp/bad-input", "opts.signerCert is not a parseable X.509 certificate", e); }
|
|
@@ -392,10 +448,14 @@ function _resolveSignerCert(m, opts, extra) {
|
|
|
392
448
|
|
|
393
449
|
// M20-M22 header consistency (opt-in echoes). Returns { code, reason } on a mismatch, else null. These are
|
|
394
450
|
// public transaction identifiers, not secrets, so a length-checked equality (not a MAC compare) is correct.
|
|
451
|
+
// The comparison runs through the captured `Buffer.compare`, which decides length and content
|
|
452
|
+
// together from the values' slots. `a.equals(x)` reads `equals` off the prototype at call time, so
|
|
453
|
+
// one returning true matches any expected identifier against any received one -- and it sat behind
|
|
454
|
+
// a length gate reading the same replaceable accessor, which cannot refuse what it is told.
|
|
395
455
|
function _bufEq(a, x) {
|
|
396
|
-
if (!
|
|
397
|
-
if (!
|
|
398
|
-
return a
|
|
456
|
+
if (!_isBuffer(a)) return false;
|
|
457
|
+
if (!_isBuffer(x)) { if (util.types.isUint8Array(x)) x = _bufferFrom(x); else return false; }
|
|
458
|
+
return _compare(a, x) === 0;
|
|
399
459
|
}
|
|
400
460
|
function _headerChecks(m, opts) {
|
|
401
461
|
if (opts.transactionID != null && !_bufEq(m.header.transactionID, opts.transactionID)) {
|
|
@@ -441,14 +501,14 @@ async function _verifyMac(m, protectedPart, protectionAlg, protection, opts) {
|
|
|
441
501
|
var kdf = params.kdf; // { salt, iterationCount, keyLength, prfOid, prfName }
|
|
442
502
|
var prfHash = PRF_HASH[kdf.prfOid]; // dispatch on the immutable OID, not the mutable display name
|
|
443
503
|
var macHash = PRF_HASH[params.schemeOid];
|
|
444
|
-
if (!prfHash) return _fail(m, "mac", protectionAlg, "cmp/unsupported-algorithm", "unsupported PBMAC1 PBKDF2 PRF " +
|
|
445
|
-
if (!macHash) return _fail(m, "mac", protectionAlg, "cmp/unsupported-algorithm", "unsupported PBMAC1 messageAuthScheme " +
|
|
504
|
+
if (!prfHash) return _fail(m, "mac", protectionAlg, "cmp/unsupported-algorithm", "unsupported PBMAC1 PBKDF2 PRF " + _stringify(kdf.prfName) + " (SHA-256/384/512 only; RFC 9481 sec. 7, RFC 9579 sec. 7)");
|
|
505
|
+
if (!macHash) return _fail(m, "mac", protectionAlg, "cmp/unsupported-algorithm", "unsupported PBMAC1 messageAuthScheme " + _stringify(params.schemeName) + " (SHA-256/384/512 only)");
|
|
446
506
|
|
|
447
507
|
// M18: bound the attacker-controlled work factors before deriving (CWE-834/400). A config-tier throw.
|
|
448
508
|
_capWork(kdf.iterationCount, kdf.salt, kdf.keyLength, prfHash, opts);
|
|
449
509
|
|
|
450
510
|
// M19: recompute the MAC over the reconstructed ProtectedPart and compare in constant time.
|
|
451
|
-
var secret = typeof opts.sharedSecret === "string" ?
|
|
511
|
+
var secret = typeof opts.sharedSecret === "string" ? _bufferFrom(opts.sharedSecret, "utf8") : guard.bytes.view(opts.sharedSecret, CmpError, "cmp/bad-input", "opts.sharedSecret");
|
|
452
512
|
var computed = await pbes2.pbmac1(secret, kdf.salt, kdf.iterationCount, kdf.keyLength, prfHash, macHash, protectedPart);
|
|
453
513
|
if (!guard.crypto.constantTimeEqual(computed, protection.bytes)) {
|
|
454
514
|
return _fail(m, "mac", protectionAlg, "cmp/protection-failed", "the PBMAC1 MAC does not verify (a wrong shared secret or a tampered ProtectedPart)");
|
|
@@ -461,21 +521,22 @@ async function _verifyMac(m, protectedPart, protectionAlg, protection, opts) {
|
|
|
461
521
|
function _capWork(iterationCount, salt, keyLength, prfHash, opts) {
|
|
462
522
|
var cap = PBMAC1_MAX_ITER;
|
|
463
523
|
if (opts.maxIterations != null) {
|
|
464
|
-
if (typeof opts.maxIterations !== "number" || !
|
|
524
|
+
if (typeof opts.maxIterations !== "number" || !_isFinite(opts.maxIterations) || opts.maxIterations < 1 || _floor(opts.maxIterations) !== opts.maxIterations) {
|
|
465
525
|
throw _err("cmp/bad-input", "opts.maxIterations must be a positive integer");
|
|
466
526
|
}
|
|
467
|
-
cap =
|
|
527
|
+
cap = _min(opts.maxIterations, cap);
|
|
468
528
|
}
|
|
469
529
|
// A trivially small count lets a peer run cheap offline PBMAC1 guesses against a password-like shared secret
|
|
470
530
|
// with parameters pki.cmp.build refuses to produce (RFC 8018 sec. 4.2). Enforce the same floor on verify.
|
|
471
531
|
if (iterationCount < PBMAC1_MIN_ITER) throw _err("cmp/bad-input", "the PBMAC1 iterationCount " + iterationCount + " is below the floor " + PBMAC1_MIN_ITER + " (RFC 8018 sec. 4.2)");
|
|
472
532
|
if (iterationCount > cap) throw _err("cmp/bad-input", "the PBMAC1 iterationCount " + iterationCount + " exceeds the cap " + cap);
|
|
473
|
-
|
|
533
|
+
var saltLen = salt ? _sizeOf(salt) : 0;
|
|
534
|
+
if (!salt || saltLen < PBMAC1_MIN_SALT || saltLen > PBMAC1_MAX_SALT) throw _err("cmp/bad-input", "the PBMAC1 salt length must be in [" + PBMAC1_MIN_SALT + ", " + PBMAC1_MAX_SALT + "] octets (RFC 8018 sec. 4.1)");
|
|
474
535
|
if (keyLength < PBMAC1_KEYLEN_MIN || keyLength > PBMAC1_KEYLEN_MAX) throw _err("cmp/bad-input", "the PBMAC1 keyLength must be in [" + PBMAC1_KEYLEN_MIN + ", " + PBMAC1_KEYLEN_MAX + "] (RFC 9579 sec. 9)");
|
|
475
536
|
// Bound the COMBINED work: PBKDF2 derives ceil(keyLength / hLen) blocks, each costing iterationCount HMACs, so a
|
|
476
537
|
// maximal keyLength multiplies the per-block iteration ceiling (a 1024-octet key over SHA-256 = 32x). Cap the
|
|
477
538
|
// product against the same ceiling so no keyLength/iteration combination exceeds the single-block work (CWE-834/400).
|
|
478
|
-
var blocks =
|
|
539
|
+
var blocks = _ceil(keyLength / (PRF_HLEN[prfHash] || 32));
|
|
479
540
|
if (iterationCount * blocks > cap) throw _err("cmp/bad-input", "the PBMAC1 combined work (iterationCount " + iterationCount + " x " + blocks + " derived blocks) exceeds the cap " + cap);
|
|
480
541
|
}
|
|
481
542
|
|
|
@@ -529,7 +590,7 @@ function _certKey(c) {
|
|
|
529
590
|
try {
|
|
530
591
|
var p = guard.parsed.acceptDerived(c, "certificate", x509.parse, _err, "cmp/bad-input", "a pool certificate");
|
|
531
592
|
if (!guard.parsed.isCert(p)) return null;
|
|
532
|
-
return p.tbsBytes
|
|
593
|
+
return _toString(p.tbsBytes, "base64") + "|" + _toString(p.signatureValue.bytes, "base64");
|
|
533
594
|
} catch (_e) {
|
|
534
595
|
return null; // underivable: kept as its own slot, never merged onto another certificate's
|
|
535
596
|
}
|
|
@@ -545,7 +606,10 @@ async function _chainSigner(signer, m, opts, extra) {
|
|
|
545
606
|
// could otherwise backdate messageTime into the certificate's validity window and be reported trusted.
|
|
546
607
|
// A caller doing historical verification opts into a specific instant via opts.time; a falsy but present
|
|
547
608
|
// opts.time (0 / false / "") is never silently replaced: it reaches path.build and is rejected as bad-input.
|
|
548
|
-
|
|
609
|
+
// The default validation time, from the captured constructor. A replacement chooses the instant
|
|
610
|
+
// every not-before / not-after and every revocation window is judged against, so an expired or
|
|
611
|
+
// revoked certificate can be validated at a moment when it was neither.
|
|
612
|
+
var time = opts.time != null ? opts.time : new _Date();
|
|
549
613
|
var anchors = _certList(opts.trustAnchors);
|
|
550
614
|
// `extra` is the already dedup + validity + count-bounded extraCerts (unsigned pool material), so a flood
|
|
551
615
|
// of duplicate / junk / non-certificate entries cannot exhaust the candidate-search budget or raise a
|
|
@@ -560,11 +624,23 @@ async function _chainSigner(signer, m, opts, extra) {
|
|
|
560
624
|
// useful embedded issuers, not on a redundant copy of the signer or a duplicate the pool already holds.
|
|
561
625
|
// The identity is the canonical tbs+signature key so a Buffer extraCert dedups against a caller intermediate
|
|
562
626
|
// supplied either as raw bytes or as an already-parsed certificate object (path.build accepts both).
|
|
563
|
-
|
|
627
|
+
// Explicit loops, never forEach / filter / concat / slice. Everything from here to the verdict
|
|
628
|
+
// runs after verification suspended, and every one of those is a replaceable global, so a
|
|
629
|
+
// caller who swaps one during the window would have the pool assembled by their code.
|
|
630
|
+
var seen = _create(null);
|
|
564
631
|
var sk = _certKey(signer.parsed); if (sk) seen[sk] = 1;
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
632
|
+
var pi, pk;
|
|
633
|
+
for (pi = 0; pi < pool.length; pi++) { pk = _certKey(pool[pi]); if (pk) seen[pk] = 1; }
|
|
634
|
+
var merged = [];
|
|
635
|
+
// Indexed assignment, never push: push is a replaceable global too, and everything here runs
|
|
636
|
+
// after verification suspended, so a swapped one could put a certificate into the pool that the
|
|
637
|
+
// caller never supplied. Writing the slot dispatches nothing.
|
|
638
|
+
for (pi = 0; pi < pool.length; pi++) _append(merged, pool[pi]);
|
|
639
|
+
for (pi = 0; pi < extra.length && merged.length - pool.length < room; pi++) {
|
|
640
|
+
pk = _certKey(extra[pi]);
|
|
641
|
+
if (pk == null || !seen[pk]) _append(merged, extra[pi]);
|
|
642
|
+
}
|
|
643
|
+
pool = merged;
|
|
568
644
|
}
|
|
569
645
|
var buildOpts = { trustAnchors: anchors, intermediates: pool, validate: true, time: time };
|
|
570
646
|
if (opts.revocationChecker != null) buildOpts.revocationChecker = opts.revocationChecker;
|
|
@@ -577,10 +653,20 @@ async function _chainSigner(signer, m, opts, extra) {
|
|
|
577
653
|
// holds standalone buffers, never slices pinning the whole response. Only certs on the trusted path enter it,
|
|
578
654
|
// so unsigned extraCerts padding is excluded. (A path cert sourced from an already-parsed intermediate carries
|
|
579
655
|
// no DER to copy and is omitted; for a Buffer/DER pool, the cmp.session case, the chain is complete.)
|
|
580
|
-
var byKey =
|
|
656
|
+
var byKey = _create(null);
|
|
581
657
|
var skey = _certKey(signer.parsed); if (skey) byKey[skey] = signer.der;
|
|
582
|
-
|
|
583
|
-
|
|
658
|
+
var ci, ck;
|
|
659
|
+
for (ci = 0; ci < pool.length; ci++) {
|
|
660
|
+
var pc = pool[ci];
|
|
661
|
+
if (!_isBuffer(pc) && !util.types.isUint8Array(pc)) continue;
|
|
662
|
+
ck = _certKey(pc); if (ck && !byKey[ck]) byKey[ck] = pc;
|
|
663
|
+
}
|
|
664
|
+
var chain = [];
|
|
665
|
+
for (ci = 0; ci < res.path.length; ci++) {
|
|
666
|
+
ck = _certKey(res.path[ci]);
|
|
667
|
+
var d = ck ? byKey[ck] : null;
|
|
668
|
+
if (d) _append(chain, _bufferFrom(d));
|
|
669
|
+
}
|
|
584
670
|
return { trusted: true, reason: null, chain: chain };
|
|
585
671
|
} catch (e) {
|
|
586
672
|
// A config-tier fault from path.build (an invalid opts.time, an empty / malformed trustAnchors or
|
|
@@ -602,49 +688,306 @@ async function _chainSigner(signer, m, opts, extra) {
|
|
|
602
688
|
var MAX_EXTRA_CERTS = 32; // valid, distinct certificates kept for signer resolution + path building
|
|
603
689
|
var MAX_EXTRA_SCAN = 256; // total entries examined -- bounds parse work regardless of the flood size
|
|
604
690
|
function _boundExtraCerts(extra) {
|
|
605
|
-
if (!
|
|
606
|
-
var out = [], seen =
|
|
691
|
+
if (!_isArray(extra) || !extra.length) return [];
|
|
692
|
+
var out = [], seen = _create(null);
|
|
607
693
|
for (var i = 0; i < extra.length && out.length < MAX_EXTRA_CERTS && i < MAX_EXTRA_SCAN; i++) {
|
|
608
694
|
var c = extra[i];
|
|
609
|
-
if (!
|
|
610
|
-
|
|
695
|
+
if (!_isBuffer(c) && !util.types.isUint8Array(c)) continue; // the slot, not the prototype
|
|
696
|
+
// The pool's other identity key, through the same capture as _certKey's: one `toString`
|
|
697
|
+
// returning a constant collapses every peer-supplied certificate onto one key, so the first
|
|
698
|
+
// entry scanned suppresses all the rest.
|
|
699
|
+
var key = _toString(_bufferFrom(c), "base64");
|
|
611
700
|
if (seen[key]) continue;
|
|
612
701
|
seen[key] = true;
|
|
613
702
|
// drop a non-certificate entry (unsigned, attacker-supplied pool material) before it reaches path.build
|
|
614
703
|
try { x509.parse(c); }
|
|
615
704
|
catch (_e) { continue; }
|
|
616
|
-
out
|
|
705
|
+
_append(out, c);
|
|
617
706
|
}
|
|
618
707
|
return out;
|
|
619
708
|
}
|
|
620
709
|
|
|
710
|
+
// Appending to a list this module owns, without dispatching anywhere a caller can reach.
|
|
711
|
+
//
|
|
712
|
+
// Everything that assembles a trust set runs AFTER verification has suspended, which is a window a
|
|
713
|
+
// caller can act in. `arr.push(v)` goes through Array.prototype.push, a replaceable global; and
|
|
714
|
+
// `arr[arr.length] = v` is a [[Set]], which walks the prototype chain and finds an
|
|
715
|
+
// `Array.prototype[0]` setter when the fresh array has no own slot there yet. Both hand control to
|
|
716
|
+
// caller code at the moment the anchor set is being decided.
|
|
717
|
+
//
|
|
718
|
+
// defineProperty is [[DefineOwnProperty]]: it creates the own slot outright, consulting no setter.
|
|
719
|
+
// The reference is captured at module load, before any caller code has run, so replacing
|
|
720
|
+
// `Object.defineProperty` afterwards cannot reach this either.
|
|
721
|
+
var _defineOwn = Object.defineProperty;
|
|
722
|
+
// The Date constructor, captured for the same reason and at the same moment. `global.Date` is
|
|
723
|
+
// replaceable, and a replacement asked for a fresh instant could hand back the caller's own object
|
|
724
|
+
// instead -- leaving the "copy" of opts.time as the very Date the caller can still call setTime on.
|
|
725
|
+
var _Date = Date;
|
|
726
|
+
// And Buffer.from, for the string shared secret. `Buffer.from` is a writable own property of the
|
|
727
|
+
// Buffer global, and the FIRST thing the reduction does is read opts.sharedSecret -- so that very
|
|
728
|
+
// accessor can install a replacement and then return the string, and an unguarded conversion would
|
|
729
|
+
// hand the plaintext to caller code and take back whatever it returned as the PBMAC1 key. The
|
|
730
|
+
// reference itself is the one taken at the top of this file, through guard-intrinsic.
|
|
731
|
+
function _append(arr, v) {
|
|
732
|
+
_defineOwn(arr, arr.length, { value: v, writable: true, enumerable: true, configurable: true });
|
|
733
|
+
}
|
|
734
|
+
|
|
621
735
|
// A trust-anchor / pool list from a single cert (DER/PEM) or an array of them.
|
|
736
|
+
//
|
|
737
|
+
// Built with an explicit loop and INDEXED ASSIGNMENT -- never `arr.map`, and never `out.push`. This
|
|
738
|
+
// runs AFTER verification has suspended, and every Array.prototype method is a replaceable global,
|
|
739
|
+
// so a caller who swaps one during that window would otherwise have this call dispatch into their
|
|
740
|
+
// code -- and an implementation that recognized the unrelated-anchor list and answered with the real
|
|
741
|
+
// CA would make a verification started with only the unrelated anchor settle `trusted: true`.
|
|
742
|
+
// Copying the caller's array at the door does not close that on its own: the fresh array is still an
|
|
743
|
+
// Array, and the replacement is on the prototype. Writing `out[out.length]` dispatches nothing.
|
|
622
744
|
function _certList(v) {
|
|
623
745
|
if (v == null) return [];
|
|
624
|
-
var arr =
|
|
625
|
-
|
|
746
|
+
var arr = _isArray(v) ? v : [v];
|
|
747
|
+
var out = [];
|
|
748
|
+
for (var i = 0, n = arr.length; i < n; i++) {
|
|
749
|
+
var c = arr[i];
|
|
750
|
+
_append(out, (_isBuffer(c) || util.types.isUint8Array(c) || typeof c === "string")
|
|
751
|
+
? _certDer(c, "a trust anchor / intermediate") : c);
|
|
752
|
+
}
|
|
753
|
+
return out;
|
|
626
754
|
}
|
|
627
755
|
|
|
628
756
|
// A usable PBMAC1 shared secret: a non-empty string or a non-empty Buffer/Uint8Array (mirrors cmp-build).
|
|
757
|
+
// The size comes from the slot rather than from `s.length`. This gate stands in front of PBMAC1, and
|
|
758
|
+
// a typed array's `length` is a replaceable accessor: one reporting a positive value for an empty
|
|
759
|
+
// buffer lets `sharedSecret: ""` past, and a peer-generated message authenticated under an empty key
|
|
760
|
+
// then verifies. A string secret has already been converted to bytes at the option door, so the
|
|
761
|
+
// value measured here is a view whenever it is not refused outright.
|
|
629
762
|
function _nonEmptySecret(s) {
|
|
630
|
-
if (typeof s === "string") return s
|
|
631
|
-
|
|
763
|
+
if (typeof s === "string") return _sizeOf(s) > 0;
|
|
764
|
+
if (!_isBuffer(s) && !util.types.isUint8Array(s)) return false;
|
|
765
|
+
return _sizeOf(s) > 0;
|
|
632
766
|
}
|
|
633
767
|
|
|
634
768
|
function verify(message, opts) {
|
|
635
|
-
|
|
769
|
+
// The reduced options are built here rather than inside _verify so the copy of a PBMAC1 shared
|
|
770
|
+
// secret has an owner for its whole lifetime. That copy is this module's, holding the plaintext
|
|
771
|
+
// secret, and pbes2.pbmac1 clears only the MAC key it derives -- so without this the secret would
|
|
772
|
+
// sit in the heap until the allocator happened to reuse it. Cleared on every path out: resolved,
|
|
773
|
+
// rejected, or never run.
|
|
774
|
+
// The list is owned HERE and handed down, so a copy is clearable from the moment it exists rather
|
|
775
|
+
// than from the moment the whole reduction succeeds. Reducing the options can fault partway -- a
|
|
776
|
+
// subsequent getter throws, a certificate list turns out not to be dense -- and the secret's
|
|
777
|
+
// copy has already been made by then. Binding it to the returned object would leave that copy with
|
|
778
|
+
// no owner on exactly the paths where the caller learns something went wrong.
|
|
779
|
+
// The cleanup is a language-level try/finally inside the async body, never a `.finally(...)` on
|
|
780
|
+
// the returned promise. `Promise.prototype.finally` is a replaceable global, and reducing the
|
|
781
|
+
// options runs caller getters, so one of those could install a replacement that this very line
|
|
782
|
+
// would then dispatch into -- handing back a promise of its own choosing while the genuine
|
|
783
|
+
// rejection went unobserved and the copied secret went unwiped. `try/finally` is syntax: nothing
|
|
784
|
+
// is looked up, so nothing can be swapped.
|
|
785
|
+
//
|
|
786
|
+
// The `await` below is a different matter, and this comment previously claimed otherwise. `await v`
|
|
787
|
+
// performs PromiseResolve, which reads `v.constructor`; when that no longer names %Promise% the
|
|
788
|
+
// fast path is skipped and `v.then` is called instead -- both properties live on Promise.prototype
|
|
789
|
+
// and both are writable. So a caller CAN interpose on the awaited value, here and at every other
|
|
790
|
+
// await this module makes. That is a toolkit-wide exposure rather than one this function can close,
|
|
791
|
+
// and it is not closed here: see the tracked work on capturing the promise plumbing.
|
|
792
|
+
var made = [];
|
|
793
|
+
return guard.async.deferred(async function () {
|
|
794
|
+
try {
|
|
795
|
+
return await _verify(message, _fixVerifyOptions(opts, made));
|
|
796
|
+
} finally {
|
|
797
|
+
guard.secret.zeroizeAll(made, CmpError, "cmp/bad-input", "the copied PBMAC1 shared secret");
|
|
798
|
+
}
|
|
799
|
+
});
|
|
636
800
|
}
|
|
637
801
|
|
|
638
|
-
|
|
802
|
+
// Every caller-owned option, reduced HERE to a value the caller no longer reaches.
|
|
803
|
+
//
|
|
804
|
+
// Verification suspends this call: PBMAC1 derivation on the MAC path, the signature engine on the
|
|
805
|
+
// other. Options read after it resumes answer for whatever the caller last wrote, so a caller could
|
|
806
|
+
// pass a transactionID that does not match the message, overwrite the buffer while the MAC derived,
|
|
807
|
+
// and be told the message was valid. `transactionID`, `expectRecipNonce`, `trustAnchors`,
|
|
808
|
+
// `intermediates` and `time` are all read past that suspension.
|
|
809
|
+
//
|
|
810
|
+
// The rule is two exhaustive outcomes rather than a list of which fields matter: anything carrying
|
|
811
|
+
// bytes is COPIED, and anything else is passed through by reference. Passing through is what a
|
|
812
|
+
// parsed certificate needs -- guard.parsed records provenance against the object's identity, so a
|
|
813
|
+
// copy is a different object and stops being recognized as parser output -- and it is also right for
|
|
814
|
+
// a caller's revocationChecker, which is a handle rather than data. Only the array CONTAINER of a
|
|
815
|
+
// certificate list is copied, so appending to the caller's array cannot widen the anchor set while
|
|
816
|
+
// the chain is being built.
|
|
817
|
+
//
|
|
818
|
+
// Two things therefore stay caller-owned, and both are safe for their own reason. A parsed anchor or
|
|
819
|
+
// intermediate is held by reference, and path validation re-derives it from the bytes it recorded,
|
|
820
|
+
// so a field edited on the caller's object never reaches the decision (pinned by vector 23l). A
|
|
821
|
+
// revocationChecker is the caller's callback, and what it answers is theirs to decide by definition.
|
|
822
|
+
// Copied through guard.bytes.snapshot, never a bare Buffer.from. The guard is this toolkit's door
|
|
823
|
+
// for a caller-supplied byte source, and it refuses the ones the rest of the code is entitled to
|
|
824
|
+
// assume it never sees -- a detached view, or one backed by a SharedArrayBuffer whose contents
|
|
825
|
+
// another thread can still rewrite. Copying with Buffer.from would launder such a source into an
|
|
826
|
+
// ordinary Buffer that every later check accepts without ever having applied the door's rules.
|
|
827
|
+
// The kind test asks the internal SLOT, never the prototype. `Buffer.isBuffer` is a writable
|
|
828
|
+
// property of the Buffer global and `v instanceof Uint8Array` consults Uint8Array[Symbol.hasInstance],
|
|
829
|
+
// and both are reachable from an option getter that has already run by the time this is evaluated.
|
|
830
|
+
// A caller who makes this answer `false` for their own bytes gets the copy skipped, which puts a
|
|
831
|
+
// live caller buffer where the copy should be -- and one that the wipe would then destroy, memory
|
|
832
|
+
// the toolkit does not own. util.types.isUint8Array is a fact about the value.
|
|
833
|
+
function _fixByteish(v, label) {
|
|
834
|
+
if (!util.types.isUint8Array(v)) return v;
|
|
835
|
+
return guard.bytes.snapshot(v, CmpError, "cmp/bad-input", "opts." + label);
|
|
836
|
+
}
|
|
837
|
+
// An opt-in echo value is a byte buffer or it is absent. A non-buffer (a string from JSON config,
|
|
838
|
+
// say) is a DEPLOYMENT error that throws cmp/bad-input, never a routine transaction/nonce mismatch
|
|
839
|
+
// verdict that would misreport a caller's typing mistake as a peer authentication failure.
|
|
840
|
+
function _assertEchoBytes(v, name) {
|
|
841
|
+
if (v == null) return;
|
|
842
|
+
if (util.types.isUint8Array(v)) return; // the slot, not the prototype -- see _fixByteish
|
|
843
|
+
throw _err("cmp/bad-input", "opts." + name + " must be a Buffer / Uint8Array");
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
// The shared secret, copied and recorded in one step so the copy is never unowned.
|
|
847
|
+
function _fixSecret(v, made) {
|
|
848
|
+
// A string secret is converted to bytes HERE, once, and recorded -- not deeper inside the MAC
|
|
849
|
+
// path. The conversion is the thing that creates a wipeable plaintext copy, so leaving it
|
|
850
|
+
// downstream left that copy with no owner and the wipe never reached it. The immutable string
|
|
851
|
+
// itself still cannot be cleared, which is the reason a caller who needs the secret gone from
|
|
852
|
+
// memory hands over bytes; what changes here is that the toolkit stops adding a second copy it
|
|
853
|
+
// then abandons.
|
|
854
|
+
if (typeof v === "string") {
|
|
855
|
+
var fromString = _bufferFrom(v, "utf8");
|
|
856
|
+
_append(made, fromString);
|
|
857
|
+
return fromString;
|
|
858
|
+
}
|
|
859
|
+
var copy = _fixByteish(v, "sharedSecret");
|
|
860
|
+
// Slot test again, and for the sharper reason: this decides whether the copy is ENTERED in the
|
|
861
|
+
// wipe list. A caller who makes it answer false leaves a genuine toolkit-allocated plaintext copy
|
|
862
|
+
// with no owner, so the wipe never reaches it.
|
|
863
|
+
if (util.types.isUint8Array(copy)) _append(made, copy);
|
|
864
|
+
return copy;
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
function _fixCertList(v, label) {
|
|
868
|
+
if (v == null) return v;
|
|
869
|
+
if (!_isArray(v)) return _fixByteish(v, label);
|
|
870
|
+
// Copied through a plain loop rather than v.map(). `map` is a property of the CALLER's array, and
|
|
871
|
+
// an own property or a subclass can define it to return the receiver -- which would leave this
|
|
872
|
+
// "copy" aliasing the very array the caller can still append a trusted CA to while verification
|
|
873
|
+
// is suspended, defeating the copy entirely. `length` and each index are read once, into a fresh
|
|
874
|
+
// array this module owns.
|
|
875
|
+
//
|
|
876
|
+
// The elements that EXIST are enumerated, rather than walking 0..length. An array's `length` is
|
|
877
|
+
// independent of how many elements it holds, so a sparse list carrying one certificate at index
|
|
878
|
+
// 100000000 costs nothing to build and would cost a dense hundred-million-entry allocation to
|
|
879
|
+
// walk. Own index keys come back in ascending numeric order, so a dense list copies exactly as it
|
|
880
|
+
// did and a sparse one collapses to the certificates it actually carries. No ceiling is imposed
|
|
881
|
+
// here: a trust store is the caller's own configuration and path.build sets no limit on how many
|
|
882
|
+
// anchors it may hold, so capping the list would refuse a large store that verifies today.
|
|
883
|
+
// A certificate list is a PLAIN DENSE array or it is refused. Two outcomes, no third.
|
|
884
|
+
//
|
|
885
|
+
// Faithfully reproducing what an ordinary array operation would consume means handling holes,
|
|
886
|
+
// elements inherited from a subclass or a custom prototype, non-enumerable elements, and indices
|
|
887
|
+
// at the uint32 boundary -- and each of those pulls against the next: reading `length` positions
|
|
888
|
+
// covers inheritance but scans every hole of a sparse list, while enumerating present keys is
|
|
889
|
+
// bounded but silently drops an inherited anchor. Silently dropping one turns a trusted
|
|
890
|
+
// verification into an untrusted one, and that is not a trade to make quietly inside a trust
|
|
891
|
+
// decision.
|
|
892
|
+
//
|
|
893
|
+
// So the shape is refused instead of emulated. The own index properties below `length` are counted
|
|
894
|
+
// -- bounded by how many properties the object actually has, never by `length` -- and the list is
|
|
895
|
+
// accepted only when that count IS the length, which is exactly a dense array with nothing
|
|
896
|
+
// inherited. A caller holding an exotic array normalizes it themselves, and the failure is a typed
|
|
897
|
+
// error naming the problem rather than an anchor that quietly went missing.
|
|
898
|
+
// The door comes first, ahead of every property read below it. `Array.isArray` pierces a Proxy
|
|
899
|
+
// without reaching a trap, so at this point nothing caller-written has run; the `length` read and
|
|
900
|
+
// the descriptor walk are what would hand control over. A Proxy whose target is an array answers
|
|
901
|
+
// both from traps, and its `ownKeys` and `get` traps need not agree with each other or with the
|
|
902
|
+
// answer an element read gives afterwards, so a count through them describes nothing. guard.identifier
|
|
903
|
+
// refuses a Proxy anywhere in the chain by identity, and it has to do that while the list is still
|
|
904
|
+
// untouched for the refusal to mean anything.
|
|
905
|
+
//
|
|
906
|
+
// The elements are then held to the same rule as the option names, through the same primitive: an
|
|
907
|
+
// accessor under an index is caller code exactly as an accessor under a name is, and a list is
|
|
908
|
+
// where it pays -- an element can answer as one certificate to the check and as another to the
|
|
909
|
+
// read, or simply run and rewrite a predicate the reduction has not reached yet. guard-bytes
|
|
910
|
+
// refuses index accessors on the arguments it copies for this reason; a certificate list is the
|
|
911
|
+
// same shape and gets the same treatment.
|
|
912
|
+
var indices = guard.identifier.readableIndices(v, _err, "cmp/bad-input", "opts." + label);
|
|
913
|
+
guard.identifier.refuseAccessorFields(v, indices, _err, "cmp/bad-input", "opts." + label);
|
|
914
|
+
var n = v.length;
|
|
915
|
+
var names = _getOwnPropertyNames(v);
|
|
916
|
+
var own = 0, j, k, ix;
|
|
917
|
+
for (j = 0; j < names.length; j++) {
|
|
918
|
+
k = names[j]; ix = k >>> 0;
|
|
919
|
+
if (_String(ix) === k && ix !== 0xFFFFFFFF && ix < n) own += 1;
|
|
920
|
+
}
|
|
921
|
+
if (own !== n) {
|
|
922
|
+
throw _err("cmp/bad-input", "a certificate list must be a dense array of certificates; this one holds " +
|
|
923
|
+
own + " of its " + n + " positions as its own elements, so the rest are holes or come from its prototype");
|
|
924
|
+
}
|
|
925
|
+
var out = [];
|
|
926
|
+
for (var i = 0; i < n; i++) _append(out, _fixByteish(v[i], label + "[" + i + "]"));
|
|
927
|
+
return out;
|
|
928
|
+
}
|
|
929
|
+
// The evaluation instant, taken from ONE read of the caller's value. A real Date is re-made at the
|
|
930
|
+
// same instant, so setTime on the caller's object cannot move it. Anything else passes through
|
|
931
|
+
// untouched, including a falsy-but-present value, so the rejection it already earns downstream is
|
|
932
|
+
// the one it still gets.
|
|
933
|
+
function _fixTime(v) {
|
|
934
|
+
return guard.time.isDate(v) ? new _Date(guard.time.instantOf(v)) : v;
|
|
935
|
+
}
|
|
936
|
+
function _fixVerifyOptions(opts, made) {
|
|
639
937
|
if (opts == null) opts = {}; // ONLY null / undefined -> the default empty opts (a falsy false/0/"" is a bad config, not a default)
|
|
640
|
-
|
|
938
|
+
// Through the toolkit's own options door, which this verb used to skip in favor of its own two
|
|
939
|
+
// checks. The door does the thing those could not: it REFUSES an options bag whose fields are
|
|
940
|
+
// accessors, and refuses one that changes which fields it carries while they are read.
|
|
941
|
+
//
|
|
942
|
+
// That is what closes this whole class rather than one member of it. An accessor is caller code
|
|
943
|
+
// running inside the call, before the verb has done anything, and from there it can rewrite the
|
|
944
|
+
// very predicates and constructors the verb is about to use -- Buffer.from, Array.isArray,
|
|
945
|
+
// util.types.isUint8Array, Promise.prototype.then. Each one hardened in turn is another entry on
|
|
946
|
+
// a list with no end; refusing the accessor removes the window they all need. What is asked of a
|
|
947
|
+
// caller is only that the options themselves be values.
|
|
948
|
+
opts = guard.identifier.optionsObject(opts, _err, "cmp/bad-input", "pki.cmp.verify options");
|
|
641
949
|
guard.identifier.assertKnownKeys(opts, KNOWN_VERIFY_OPTS, _err, "cmp/bad-input", "unknown opts field ");
|
|
642
|
-
//
|
|
643
|
-
//
|
|
644
|
-
//
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
950
|
+
// Each field is read from the caller's object exactly once AND reduced in the same step, before
|
|
951
|
+
// the next field is read. Both halves matter, and each closes a way the caller could otherwise
|
|
952
|
+
// still be holding the value the verdict turns on:
|
|
953
|
+
//
|
|
954
|
+
// reading once -- a property can be an accessor, and an accessor is free to answer a different
|
|
955
|
+
// value each time it is asked, so a second read would let the value that passed the check
|
|
956
|
+
// differ from the value that was used;
|
|
957
|
+
//
|
|
958
|
+
// reducing immediately -- an accessor invoked for a LATER field can reach back and mutate the
|
|
959
|
+
// buffer an EARLIER field handed over, so reading everything first and copying afterwards would
|
|
960
|
+
// copy whatever the last getter left behind.
|
|
961
|
+
//
|
|
962
|
+
// An object literal evaluates its properties in source order, so each line below is one read
|
|
963
|
+
// followed at once by its copy.
|
|
964
|
+
var f = {
|
|
965
|
+
// Recorded in `made` the instant it is copied. Only the byte form: a string secret is a
|
|
966
|
+
// JavaScript string, immutable and interned wherever the caller created it, so there is nothing
|
|
967
|
+
// here to wipe -- a caller who needs the secret gone from memory passes it as bytes.
|
|
968
|
+
sharedSecret: _fixSecret(opts.sharedSecret, made),
|
|
969
|
+
signerCert: _fixByteish(opts.signerCert, "signerCert"),
|
|
970
|
+
trustAnchors: _fixCertList(opts.trustAnchors, "trustAnchors"),
|
|
971
|
+
intermediates: _fixCertList(opts.intermediates, "intermediates"),
|
|
972
|
+
time: _fixTime(opts.time),
|
|
973
|
+
transactionID: _fixByteish(opts.transactionID, "transactionID"),
|
|
974
|
+
expectRecipNonce: _fixByteish(opts.expectRecipNonce, "expectRecipNonce"),
|
|
975
|
+
revocationChecker: opts.revocationChecker,
|
|
976
|
+
maxIterations: opts.maxIterations,
|
|
977
|
+
};
|
|
978
|
+
|
|
979
|
+
// Checked on the COPY, which is what the comparison later runs against.
|
|
980
|
+
//
|
|
981
|
+
// Written out rather than looped with forEach. The reduction above has already run the caller's
|
|
982
|
+
// getters, so one of them could have replaced Array.prototype.forEach with a no-op -- and this
|
|
983
|
+
// check would then simply not happen, turning a configuration error the caller must see into a
|
|
984
|
+
// routine mismatch verdict that blames the peer. Two named checks dispatch nothing.
|
|
985
|
+
_assertEchoBytes(f.transactionID, "transactionID");
|
|
986
|
+
_assertEchoBytes(f.expectRecipNonce, "expectRecipNonce");
|
|
987
|
+
return f;
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
async function _verify(message, opts) {
|
|
648
991
|
if (_engine == null) throw _err("cmp/bad-input", "the cmp-verify signature engine is not initialized (require pki before use)");
|
|
649
992
|
|
|
650
993
|
var m = _coerce(message);
|