@blamejs/core 0.9.46 → 0.10.1
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 +951 -893
- package/index.js +30 -0
- package/lib/_test/crypto-fixtures.js +67 -0
- package/lib/agent-event-bus.js +52 -6
- package/lib/agent-idempotency.js +169 -16
- package/lib/agent-orchestrator.js +263 -9
- package/lib/agent-posture-chain.js +163 -5
- package/lib/agent-saga.js +146 -16
- package/lib/agent-snapshot.js +349 -19
- package/lib/agent-stream.js +34 -2
- package/lib/agent-tenant.js +179 -23
- package/lib/agent-trace.js +84 -21
- package/lib/auth/aal.js +8 -1
- package/lib/auth/ciba.js +6 -1
- package/lib/auth/dpop.js +7 -2
- package/lib/auth/fal.js +17 -8
- package/lib/auth/jwt-external.js +128 -4
- package/lib/auth/oauth.js +232 -10
- package/lib/auth/oid4vci.js +67 -7
- package/lib/auth/openid-federation.js +71 -25
- package/lib/auth/passkey.js +140 -6
- package/lib/auth/sd-jwt-vc.js +67 -5
- package/lib/circuit-breaker.js +10 -2
- package/lib/compliance.js +176 -8
- package/lib/crypto-field.js +114 -14
- package/lib/crypto.js +216 -20
- package/lib/db.js +1 -0
- package/lib/guard-imap-command.js +335 -0
- package/lib/guard-jmap.js +321 -0
- package/lib/guard-managesieve-command.js +566 -0
- package/lib/guard-pop3-command.js +317 -0
- package/lib/guard-smtp-command.js +58 -3
- package/lib/mail-agent.js +20 -7
- package/lib/mail-arc-sign.js +12 -8
- package/lib/mail-auth.js +323 -34
- package/lib/mail-crypto-pgp.js +934 -0
- package/lib/mail-crypto-smime.js +340 -0
- package/lib/mail-crypto.js +108 -0
- package/lib/mail-dav.js +1224 -0
- package/lib/mail-deploy.js +492 -0
- package/lib/mail-dkim.js +431 -26
- package/lib/mail-journal.js +435 -0
- package/lib/mail-scan.js +502 -0
- package/lib/mail-server-imap.js +1102 -0
- package/lib/mail-server-jmap.js +488 -0
- package/lib/mail-server-managesieve.js +853 -0
- package/lib/mail-server-mx.js +164 -34
- package/lib/mail-server-pop3.js +836 -0
- package/lib/mail-server-rate-limit.js +269 -0
- package/lib/mail-server-submission.js +1032 -0
- package/lib/mail-server-tls.js +445 -0
- package/lib/mail-sieve.js +557 -0
- package/lib/mail-spam-score.js +284 -0
- package/lib/mail.js +99 -0
- package/lib/metrics.js +130 -10
- package/lib/middleware/dpop.js +58 -3
- package/lib/middleware/idempotency-key.js +255 -42
- package/lib/middleware/protected-resource-metadata.js +114 -2
- package/lib/network-dns-resolver.js +33 -0
- package/lib/network-tls.js +46 -0
- package/lib/outbox.js +62 -12
- package/lib/pqc-agent.js +13 -5
- package/lib/retry.js +23 -9
- package/lib/router.js +23 -1
- package/lib/safe-ical.js +634 -0
- package/lib/safe-icap.js +502 -0
- package/lib/safe-mime.js +15 -0
- package/lib/safe-sieve.js +684 -0
- package/lib/safe-smtp.js +57 -0
- package/lib/safe-url.js +37 -0
- package/lib/safe-vcard.js +473 -0
- package/lib/self-update-standalone-verifier.js +32 -3
- package/lib/self-update.js +168 -17
- package/lib/vendor/MANIFEST.json +161 -156
- package/lib/vendor-data.js +127 -9
- package/lib/vex.js +324 -59
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
|
@@ -0,0 +1,445 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* @module b.mail.server.tls
|
|
4
|
+
* @nav Mail
|
|
5
|
+
* @title Mail Server TLS Bootstrap
|
|
6
|
+
* @order 538
|
|
7
|
+
*
|
|
8
|
+
* @intro
|
|
9
|
+
* Operator-UX helper for the `tlsContext` opt on `b.mail.server.mx`
|
|
10
|
+
* and `b.mail.server.submission`. Both listeners refuse to boot
|
|
11
|
+
* without a `tlsContext` by design (no implicit plaintext mode);
|
|
12
|
+
* pre-this-primitive operators had to wire `node:tls.createSecureContext`
|
|
13
|
+
* themselves plus solve cert renewal + sealed-storage-of-keys + in-
|
|
14
|
+
* process reload-on-rotation. This primitive owns the wiring.
|
|
15
|
+
*
|
|
16
|
+
* ```js
|
|
17
|
+
* var tlsCtx = b.mail.server.tls.context({
|
|
18
|
+
* certFile: "/etc/letsencrypt/live/mail.example.com/fullchain.pem",
|
|
19
|
+
* keyFile: "/var/lib/blamejs/mail.example.com.key.sealed",
|
|
20
|
+
* vault: b.vault, // for keyFile unseal
|
|
21
|
+
* watch: true, // auto-reload on rotation
|
|
22
|
+
* });
|
|
23
|
+
*
|
|
24
|
+
* var mx = b.mail.server.mx.create({
|
|
25
|
+
* tlsContext: tlsCtx.secureContext,
|
|
26
|
+
* ...
|
|
27
|
+
* });
|
|
28
|
+
* ```
|
|
29
|
+
*
|
|
30
|
+
* The helper handles the three things operators need but were
|
|
31
|
+
* reinventing per-deployment:
|
|
32
|
+
*
|
|
33
|
+
* 1. **Sealed-key unwrap** — operators who store the private key on
|
|
34
|
+
* disk via `b.vault.sealPemFile` (recommended posture per
|
|
35
|
+
* SECURITY.md) pass `vault: b.vault` here and the helper unseals
|
|
36
|
+
* at load-time, never holding the plaintext key longer than the
|
|
37
|
+
* `tls.createSecureContext` call.
|
|
38
|
+
*
|
|
39
|
+
* 2. **Cert-rotation in-process reload** — when `watch: true`, the
|
|
40
|
+
* helper polls `certFile` + `keyFile` for mtime changes (default
|
|
41
|
+
* 30s poll, matching the framework's vault-pem-file convention).
|
|
42
|
+
* On change, the helper builds a fresh `SecureContext` and emits
|
|
43
|
+
* a `mail.server.tls.context_reloaded` audit event. Operators
|
|
44
|
+
* who wire `tlsCtx.onReload(fn)` get a callback so the running
|
|
45
|
+
* listener's `SecureContext` reference can be swapped.
|
|
46
|
+
*
|
|
47
|
+
* 3. **Boot-fail surface** — missing/unreadable file, unsealable
|
|
48
|
+
* key, mismatched cert/key pair, expired cert — all surfaced at
|
|
49
|
+
* `context()` call with a typed `MailServerTlsError` so the
|
|
50
|
+
* operator's boot path fails fast at the right line, not 20
|
|
51
|
+
* stack frames deep inside the listener.
|
|
52
|
+
*
|
|
53
|
+
* ## ACME provisioning
|
|
54
|
+
*
|
|
55
|
+
* This primitive does NOT drive ACME issuance — that's `b.acme`'s
|
|
56
|
+
* job (RFC 8555 + RFC 9773 ARI). The operator's deployment script /
|
|
57
|
+
* sidecar / systemd-timer orchestrates `b.acme.renewIfDue` and
|
|
58
|
+
* writes the renewed cert + key to `certFile` / `keyFile`. The
|
|
59
|
+
* watch-loop here picks up the change and reloads. Composing this
|
|
60
|
+
* way keeps the TLS-context helper unaware of which ACME provider
|
|
61
|
+
* the operator picked (Let's Encrypt / ZeroSSL / Buypass / step-ca /
|
|
62
|
+
* internal PKI) and unaware of which challenge type (HTTP-01 /
|
|
63
|
+
* DNS-01 / TLS-ALPN-01) the deployment uses.
|
|
64
|
+
*
|
|
65
|
+
* For a turnkey ACME-and-then-load path operators wire the two
|
|
66
|
+
* primitives at deploy-time:
|
|
67
|
+
*
|
|
68
|
+
* ```js
|
|
69
|
+
* // Once per deploy (sidecar / systemd-timer / k8s CronJob):
|
|
70
|
+
* var acme = b.acme.create({ directoryUrl: "https://acme-v02.api.letsencrypt.org/directory", ... });
|
|
71
|
+
* // ... acme.newAccount + acme.newOrder + challenge-solve + acme.finalize ...
|
|
72
|
+
* // → write the issued cert.pem + key.pem to the watched paths
|
|
73
|
+
*
|
|
74
|
+
* // Once per process at boot:
|
|
75
|
+
* var tls = b.mail.server.tls.context({ certFile, keyFile, watch: true });
|
|
76
|
+
* var mx = b.mail.server.mx.create({ tlsContext: tls.secureContext, ... });
|
|
77
|
+
* tls.onReload(function (newCtx) { mx.replaceTlsContext(newCtx); });
|
|
78
|
+
* ```
|
|
79
|
+
*
|
|
80
|
+
* The cleartext-refused error message from `b.mail.server.mx` /
|
|
81
|
+
* `b.mail.server.submission` points at this primitive so the
|
|
82
|
+
* operator's boot dead-end becomes a one-line fix.
|
|
83
|
+
*
|
|
84
|
+
* @card
|
|
85
|
+
* Operator-UX helper for the TLS context required by b.mail.server.mx /
|
|
86
|
+
* .submission. Loads cert + key (with optional vault-sealed-key unwrap),
|
|
87
|
+
* watches for rotation, builds a node:tls SecureContext + emits an
|
|
88
|
+
* audit event on every reload. ACME provisioning stays in b.acme;
|
|
89
|
+
* this primitive just loads what's on disk and reloads when it changes.
|
|
90
|
+
*/
|
|
91
|
+
|
|
92
|
+
var nodeFs = require("node:fs");
|
|
93
|
+
var nodeTls = require("node:tls");
|
|
94
|
+
var lazyRequire = require("./lazy-require");
|
|
95
|
+
var C = require("./constants");
|
|
96
|
+
var validateOpts = require("./validate-opts");
|
|
97
|
+
var { defineClass } = require("./framework-error");
|
|
98
|
+
|
|
99
|
+
var audit = lazyRequire(function () { return require("./audit"); });
|
|
100
|
+
|
|
101
|
+
var MailServerTlsError = defineClass("MailServerTlsError", { alwaysPermanent: true });
|
|
102
|
+
|
|
103
|
+
var DEFAULT_POLL_MS = C.TIME.seconds(30);
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* @primitive b.mail.server.tls.context
|
|
107
|
+
* @signature b.mail.server.tls.context(opts)
|
|
108
|
+
* @since 0.9.48
|
|
109
|
+
* @status stable
|
|
110
|
+
* @related b.mail.server.mx.create, b.mail.server.submission.create, b.vault.sealPemFile, b.acme.create
|
|
111
|
+
*
|
|
112
|
+
* Build a `node:tls` `SecureContext` from cert + key PEM file paths.
|
|
113
|
+
* Returns a handle exposing `secureContext`, `reload()`, `onReload(fn)`,
|
|
114
|
+
* and `stop()`. When `watch: true`, the helper polls both files for
|
|
115
|
+
* mtime changes (default every 30s) and rebuilds the context in-place
|
|
116
|
+
* on change — operators wire `onReload` to swap the running listener's
|
|
117
|
+
* context after cert rotation.
|
|
118
|
+
*
|
|
119
|
+
* @opts
|
|
120
|
+
* certFile: string, // required — PEM-encoded fullchain
|
|
121
|
+
* keyFile: string, // required — PEM-encoded private key (raw OR sealed)
|
|
122
|
+
* vault: object, // optional — b.vault; when supplied + keyFile
|
|
123
|
+
* // starts with the b.vault.sealPemFile magic
|
|
124
|
+
* // ("vault:"), unsealed before use
|
|
125
|
+
* watch: boolean, // default false — when true, poll for rotation
|
|
126
|
+
* pollMs: number, // default 30000; min 1000
|
|
127
|
+
*
|
|
128
|
+
* @example
|
|
129
|
+
* var tls = b.mail.server.tls.context({
|
|
130
|
+
* certFile: "/etc/letsencrypt/live/mail.example.com/fullchain.pem",
|
|
131
|
+
* keyFile: "/etc/letsencrypt/live/mail.example.com/privkey.pem",
|
|
132
|
+
* watch: true,
|
|
133
|
+
* });
|
|
134
|
+
* // Wire `tls.secureContext` into b.mail.server.mx.create / submission.create
|
|
135
|
+
* tls.onReload(function (newCtx) {
|
|
136
|
+
* // operator swaps the running listener's SecureContext via the
|
|
137
|
+
* // listener's reload hook (when the listener exposes one) or via
|
|
138
|
+
* // restart-on-rotation flow
|
|
139
|
+
* });
|
|
140
|
+
*
|
|
141
|
+
* // ... later, on shutdown:
|
|
142
|
+
* tls.stop(); // clears the poll timer
|
|
143
|
+
*/
|
|
144
|
+
function context(opts) {
|
|
145
|
+
validateOpts.requireObject(opts, "b.mail.server.tls.context",
|
|
146
|
+
MailServerTlsError, "mail-server-tls/bad-opts");
|
|
147
|
+
validateOpts.requireNonEmptyString(opts.certFile,
|
|
148
|
+
"b.mail.server.tls.context: opts.certFile",
|
|
149
|
+
MailServerTlsError, "mail-server-tls/bad-cert-file");
|
|
150
|
+
validateOpts.requireNonEmptyString(opts.keyFile,
|
|
151
|
+
"b.mail.server.tls.context: opts.keyFile",
|
|
152
|
+
MailServerTlsError, "mail-server-tls/bad-key-file");
|
|
153
|
+
if (opts.vault !== undefined &&
|
|
154
|
+
(typeof opts.vault !== "object" || opts.vault === null ||
|
|
155
|
+
typeof opts.vault.unseal !== "function")) {
|
|
156
|
+
throw new MailServerTlsError("mail-server-tls/bad-vault",
|
|
157
|
+
"b.mail.server.tls.context: opts.vault must be a b.vault handle (.unseal fn)");
|
|
158
|
+
}
|
|
159
|
+
validateOpts.optionalBoolean(opts.watch,
|
|
160
|
+
"b.mail.server.tls.context: opts.watch",
|
|
161
|
+
MailServerTlsError, "mail-server-tls/bad-watch");
|
|
162
|
+
var pollMs = opts.pollMs === undefined ? DEFAULT_POLL_MS : opts.pollMs;
|
|
163
|
+
if (typeof pollMs !== "number" || !isFinite(pollMs) || pollMs < C.TIME.seconds(1)) {
|
|
164
|
+
throw new MailServerTlsError("mail-server-tls/bad-poll-ms",
|
|
165
|
+
"b.mail.server.tls.context: opts.pollMs must be a finite number >= 1000");
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
var certFile = opts.certFile;
|
|
169
|
+
var keyFile = opts.keyFile;
|
|
170
|
+
var vault = opts.vault || null;
|
|
171
|
+
var watch = opts.watch === true;
|
|
172
|
+
var reloadListeners = [];
|
|
173
|
+
var secureContext = null;
|
|
174
|
+
var lastCertMtime = 0;
|
|
175
|
+
var lastKeyMtime = 0;
|
|
176
|
+
var pollTimer = null;
|
|
177
|
+
var stopped = false;
|
|
178
|
+
|
|
179
|
+
function _readKey() {
|
|
180
|
+
var raw = nodeFs.readFileSync(keyFile, "utf8");
|
|
181
|
+
// b.vault.sealPemFile produces blobs that decrypt via vault.unseal.
|
|
182
|
+
// Detect by the sealed-cell prefix the framework's vault layer
|
|
183
|
+
// already documents (everything else passes through as plain PEM).
|
|
184
|
+
if (vault && raw.indexOf("vault:") === 0) {
|
|
185
|
+
try {
|
|
186
|
+
return vault.unseal(raw).toString("utf8");
|
|
187
|
+
} catch (e) {
|
|
188
|
+
throw new MailServerTlsError("mail-server-tls/unseal-failed",
|
|
189
|
+
"b.mail.server.tls.context: failed to unseal " + keyFile +
|
|
190
|
+
" via b.vault.unseal: " + (e && e.message ? e.message : String(e)));
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
return raw;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function _build() {
|
|
197
|
+
var certPem;
|
|
198
|
+
try {
|
|
199
|
+
certPem = nodeFs.readFileSync(certFile, "utf8");
|
|
200
|
+
} catch (e) {
|
|
201
|
+
throw new MailServerTlsError("mail-server-tls/cert-unreadable",
|
|
202
|
+
"b.mail.server.tls.context: cannot read certFile " + certFile + ": " +
|
|
203
|
+
(e && e.message ? e.message : String(e)));
|
|
204
|
+
}
|
|
205
|
+
var keyPem;
|
|
206
|
+
try {
|
|
207
|
+
keyPem = _readKey();
|
|
208
|
+
} catch (e) {
|
|
209
|
+
if (e && e.isFrameworkError) throw e;
|
|
210
|
+
throw new MailServerTlsError("mail-server-tls/key-unreadable",
|
|
211
|
+
"b.mail.server.tls.context: cannot read keyFile " + keyFile + ": " +
|
|
212
|
+
(e && e.message ? e.message : String(e)));
|
|
213
|
+
}
|
|
214
|
+
var ctx;
|
|
215
|
+
try {
|
|
216
|
+
ctx = nodeTls.createSecureContext({ cert: certPem, key: keyPem });
|
|
217
|
+
} catch (e) {
|
|
218
|
+
throw new MailServerTlsError("mail-server-tls/secure-context-failed",
|
|
219
|
+
"b.mail.server.tls.context: createSecureContext threw (mismatched cert/key? " +
|
|
220
|
+
"expired? bad PEM?): " + (e && e.message ? e.message : String(e)));
|
|
221
|
+
}
|
|
222
|
+
return ctx;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function _emit(action, metadata) {
|
|
226
|
+
try {
|
|
227
|
+
audit().safeEmit({
|
|
228
|
+
action: action,
|
|
229
|
+
outcome: "success",
|
|
230
|
+
metadata: metadata || {},
|
|
231
|
+
});
|
|
232
|
+
} catch (_e) { /* drop-silent — audit best-effort */ }
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function reload() {
|
|
236
|
+
var fresh = _build();
|
|
237
|
+
secureContext = fresh;
|
|
238
|
+
try {
|
|
239
|
+
var cstat = nodeFs.statSync(certFile);
|
|
240
|
+
lastCertMtime = cstat.mtimeMs;
|
|
241
|
+
} catch (_e) { /* file disappeared between read + stat; tolerate */ }
|
|
242
|
+
try {
|
|
243
|
+
var kstat = nodeFs.statSync(keyFile);
|
|
244
|
+
lastKeyMtime = kstat.mtimeMs;
|
|
245
|
+
} catch (_e) { /* same */ }
|
|
246
|
+
_emit("mail.server.tls.context_reloaded",
|
|
247
|
+
{ certFile: certFile, keyFile: keyFile });
|
|
248
|
+
for (var i = 0; i < reloadListeners.length; i++) {
|
|
249
|
+
try { reloadListeners[i](secureContext); }
|
|
250
|
+
catch (_e) { /* listener errors must not break the loop */ }
|
|
251
|
+
}
|
|
252
|
+
return secureContext;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function onReload(fn) {
|
|
256
|
+
if (typeof fn !== "function") {
|
|
257
|
+
throw new MailServerTlsError("mail-server-tls/bad-listener",
|
|
258
|
+
"b.mail.server.tls.context: onReload(fn) requires a function");
|
|
259
|
+
}
|
|
260
|
+
reloadListeners.push(fn);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function _poll() {
|
|
264
|
+
if (stopped) return;
|
|
265
|
+
var changed = false;
|
|
266
|
+
try {
|
|
267
|
+
var cs = nodeFs.statSync(certFile);
|
|
268
|
+
if (cs.mtimeMs !== lastCertMtime) changed = true;
|
|
269
|
+
} catch (_e) { /* file removed transiently mid-rotation; skip */ }
|
|
270
|
+
try {
|
|
271
|
+
var ks = nodeFs.statSync(keyFile);
|
|
272
|
+
if (ks.mtimeMs !== lastKeyMtime) changed = true;
|
|
273
|
+
} catch (_e) { /* same */ }
|
|
274
|
+
if (changed) {
|
|
275
|
+
try { reload(); }
|
|
276
|
+
catch (e) {
|
|
277
|
+
// Reload failed (likely mid-rotation, file half-written).
|
|
278
|
+
// Surface as audit but DON'T overwrite the live context —
|
|
279
|
+
// the listener keeps serving with the prior good cert until
|
|
280
|
+
// the next poll catches a clean snapshot.
|
|
281
|
+
try {
|
|
282
|
+
audit().safeEmit({
|
|
283
|
+
action: "mail.server.tls.reload_failed",
|
|
284
|
+
outcome: "failure",
|
|
285
|
+
metadata: { error: e && e.message ? e.message : String(e) },
|
|
286
|
+
});
|
|
287
|
+
} catch (_e) { /* drop-silent */ }
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function stop() {
|
|
293
|
+
stopped = true;
|
|
294
|
+
if (pollTimer) {
|
|
295
|
+
clearInterval(pollTimer);
|
|
296
|
+
pollTimer = null;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// Initial build — propagates boot-fail typed errors to the caller.
|
|
301
|
+
secureContext = _build();
|
|
302
|
+
try {
|
|
303
|
+
lastCertMtime = nodeFs.statSync(certFile).mtimeMs;
|
|
304
|
+
lastKeyMtime = nodeFs.statSync(keyFile).mtimeMs;
|
|
305
|
+
} catch (_e) { /* file disappeared between read + stat; tolerate */ }
|
|
306
|
+
|
|
307
|
+
if (watch) {
|
|
308
|
+
pollTimer = setInterval(_poll, pollMs);
|
|
309
|
+
if (typeof pollTimer.unref === "function") pollTimer.unref();
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
return {
|
|
313
|
+
get secureContext() { return secureContext; },
|
|
314
|
+
reload: reload,
|
|
315
|
+
onReload: onReload,
|
|
316
|
+
stop: stop,
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* @primitive b.mail.server.tls.upgradeSocket
|
|
322
|
+
* @signature b.mail.server.tls.upgradeSocket(opts)
|
|
323
|
+
* @since 0.9.57
|
|
324
|
+
* @status stable
|
|
325
|
+
* @related b.mail.server.tls.context, b.mail.server.mx.create, b.mail.server.submission.create
|
|
326
|
+
*
|
|
327
|
+
* STARTTLS / STLS upgrade primitive shared by every mail-protocol
|
|
328
|
+
* listener (MX / submission / IMAP / POP3). Wraps the four-step dance
|
|
329
|
+
* every listener was inlining and that has been a recurring source
|
|
330
|
+
* of cleartext-injection bugs (CVE-2021-33515 Dovecot,
|
|
331
|
+
* CVE-2021-38371 Exim) when even one of the four steps is forgotten:
|
|
332
|
+
*
|
|
333
|
+
* 1. Remove ALL `"data"` listeners from the plain socket so any
|
|
334
|
+
* bytes the peer queued in the TCP receive buffer before the
|
|
335
|
+
* handshake do NOT reach the plaintext state machine after the
|
|
336
|
+
* socket has been re-typed as a TLSSocket. Without listener
|
|
337
|
+
* removal, plain-mode bytes pipelined ahead of the handshake
|
|
338
|
+
* reach the post-TLS dispatcher and execute under the
|
|
339
|
+
* authenticated TLS context.
|
|
340
|
+
* 2. Pause the plain socket so no further bytes flow through the
|
|
341
|
+
* old handler in the window before the TLSSocket attaches.
|
|
342
|
+
* 3. Re-arm the idle timeout on the new TLSSocket (the plain
|
|
343
|
+
* socket's `setTimeout` does not survive the upgrade — RFC 5321
|
|
344
|
+
* §4.5.3.2.7 idle timeouts must keep running post-handshake).
|
|
345
|
+
* 4. Wire `"secure"` / `"data"` / `"error"` handlers via callbacks
|
|
346
|
+
* so the caller's per-protocol state machine keeps owning the
|
|
347
|
+
* ingest logic.
|
|
348
|
+
*
|
|
349
|
+
* @opts
|
|
350
|
+
* plainSocket: net.Socket, // pre-upgrade socket
|
|
351
|
+
* secureContext: tls.SecureContext, // from b.mail.server.tls.context
|
|
352
|
+
* idleTimeoutMs: number, // re-armed post-handshake
|
|
353
|
+
* onSecure: function(tlsSocket), // called once "secure" fires
|
|
354
|
+
* onData: function(tlsSocket, chunk), // post-handshake ingest
|
|
355
|
+
* onError: function(err), // handshake / runtime error
|
|
356
|
+
* onTimeout: function(tlsSocket), // optional idle timeout cb
|
|
357
|
+
*
|
|
358
|
+
* @example
|
|
359
|
+
* b.mail.server.tls.upgradeSocket({
|
|
360
|
+
* plainSocket: socket,
|
|
361
|
+
* secureContext: opts.tlsContext,
|
|
362
|
+
* idleTimeoutMs: idleTimeoutMs,
|
|
363
|
+
* onSecure: function (tlsSocket) { state.tls = true; },
|
|
364
|
+
* onData: function (tlsSocket, chunk) { _ingest(state, tlsSocket, chunk); },
|
|
365
|
+
* onError: function (err) { _emit("tls.handshake_failed", { err: err.message }); },
|
|
366
|
+
* });
|
|
367
|
+
*/
|
|
368
|
+
function upgradeSocket(opts) {
|
|
369
|
+
if (!opts || typeof opts !== "object") {
|
|
370
|
+
throw new MailServerTlsError("mail-server-tls/bad-upgrade-opts",
|
|
371
|
+
"upgradeSocket: opts required");
|
|
372
|
+
}
|
|
373
|
+
var plainSocket = opts.plainSocket;
|
|
374
|
+
if (!plainSocket || typeof plainSocket.removeAllListeners !== "function") {
|
|
375
|
+
throw new MailServerTlsError("mail-server-tls/bad-upgrade-socket",
|
|
376
|
+
"upgradeSocket: opts.plainSocket must be a net.Socket");
|
|
377
|
+
}
|
|
378
|
+
if (!opts.secureContext) {
|
|
379
|
+
throw new MailServerTlsError("mail-server-tls/bad-upgrade-context",
|
|
380
|
+
"upgradeSocket: opts.secureContext required");
|
|
381
|
+
}
|
|
382
|
+
if (typeof opts.onSecure !== "function") {
|
|
383
|
+
throw new MailServerTlsError("mail-server-tls/bad-upgrade-onsecure",
|
|
384
|
+
"upgradeSocket: opts.onSecure(tlsSocket) required");
|
|
385
|
+
}
|
|
386
|
+
if (typeof opts.onData !== "function") {
|
|
387
|
+
throw new MailServerTlsError("mail-server-tls/bad-upgrade-ondata",
|
|
388
|
+
"upgradeSocket: opts.onData(tlsSocket, chunk) required");
|
|
389
|
+
}
|
|
390
|
+
if (typeof opts.onError !== "function") {
|
|
391
|
+
throw new MailServerTlsError("mail-server-tls/bad-upgrade-onerror",
|
|
392
|
+
"upgradeSocket: opts.onError(err) required");
|
|
393
|
+
}
|
|
394
|
+
var idleTimeoutMs = opts.idleTimeoutMs;
|
|
395
|
+
if (idleTimeoutMs !== undefined &&
|
|
396
|
+
(typeof idleTimeoutMs !== "number" || !isFinite(idleTimeoutMs) || idleTimeoutMs < 0)) {
|
|
397
|
+
throw new MailServerTlsError("mail-server-tls/bad-upgrade-idle-timeout",
|
|
398
|
+
"upgradeSocket: opts.idleTimeoutMs must be a non-negative finite number");
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// CVE-2021-33515 / CVE-2021-38371 defense: strip every "data"
|
|
402
|
+
// listener on the plain socket BEFORE the TLSSocket wraps it.
|
|
403
|
+
// Without this, plain-mode bytes the peer queued pre-handshake
|
|
404
|
+
// (RFC 2920 PIPELINING + an unsuspecting parser) reach the
|
|
405
|
+
// post-TLS dispatcher and execute as if they had been sent over
|
|
406
|
+
// the authenticated channel.
|
|
407
|
+
plainSocket.removeAllListeners("data");
|
|
408
|
+
// Pause so the kernel TCP buffer doesn't drain into the old
|
|
409
|
+
// handler in the window before TLSSocket attaches its own.
|
|
410
|
+
if (typeof plainSocket.pause === "function") {
|
|
411
|
+
try { plainSocket.pause(); } catch (_e) { /* tolerate already-closed */ }
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
var tlsSocket = new nodeTls.TLSSocket(plainSocket, {
|
|
415
|
+
isServer: true,
|
|
416
|
+
secureContext: opts.secureContext,
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
tlsSocket.on("secure", function () {
|
|
420
|
+
if (idleTimeoutMs !== undefined && typeof tlsSocket.setTimeout === "function") {
|
|
421
|
+
try { tlsSocket.setTimeout(idleTimeoutMs); }
|
|
422
|
+
catch (_e) { /* tolerate */ }
|
|
423
|
+
}
|
|
424
|
+
if (typeof opts.onTimeout === "function") {
|
|
425
|
+
tlsSocket.on("timeout", function () { opts.onTimeout(tlsSocket); });
|
|
426
|
+
}
|
|
427
|
+
try { opts.onSecure(tlsSocket); }
|
|
428
|
+
catch (e) { try { opts.onError(e); } catch (_e) { /* drop-silent */ } }
|
|
429
|
+
});
|
|
430
|
+
tlsSocket.on("data", function (chunk) {
|
|
431
|
+
try { opts.onData(tlsSocket, chunk); }
|
|
432
|
+
catch (e) { try { opts.onError(e); } catch (_e) { /* drop-silent */ } }
|
|
433
|
+
});
|
|
434
|
+
tlsSocket.on("error", function (err) {
|
|
435
|
+
try { opts.onError(err); } catch (_e) { /* drop-silent */ }
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
return tlsSocket;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
module.exports = {
|
|
442
|
+
context: context,
|
|
443
|
+
upgradeSocket: upgradeSocket,
|
|
444
|
+
MailServerTlsError: MailServerTlsError,
|
|
445
|
+
};
|