@blamejs/core 0.18.51 → 0.18.54
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 +185 -0
- package/NOTICE +1 -1
- package/README.md +3 -3
- package/lib/ai-adverse-decision.js +18 -2
- package/lib/codepoint-class.js +72 -0
- package/lib/cookies.js +7 -10
- package/lib/credential-hash.js +8 -1
- package/lib/crypto.js +7 -5
- package/lib/guard-auth.js +34 -11
- package/lib/guard-filename.js +39 -16
- package/lib/guard-managesieve-command.js +49 -9
- package/lib/guard-regex.js +3 -5
- package/lib/guard-yaml.js +212 -31
- package/lib/mail-agent.js +23 -9
- package/lib/mail-arc-sign.js +40 -7
- package/lib/mail-auth.js +75 -20
- package/lib/mail-crypto-pgp.js +1 -1
- package/lib/mail-dkim.js +80 -11
- package/lib/mail-helo.js +10 -0
- package/lib/mail-rbl.js +10 -3
- package/lib/mail-send-deliver.js +151 -32
- package/lib/mail-server-imap.js +121 -55
- package/lib/mail-server-jmap.js +31 -4
- package/lib/mail-server-managesieve.js +168 -25
- package/lib/mail-server-mx.js +76 -4
- package/lib/mail-server-net.js +126 -0
- package/lib/mail-server-pop3.js +73 -22
- package/lib/mail-server-submission.js +21 -2
- package/lib/mail-store.js +33 -11
- package/lib/mail.js +355 -17
- package/lib/middleware/bearer-auth.js +6 -1
- package/lib/middleware/fetch-metadata.js +5 -1
- package/lib/middleware/headers.js +7 -10
- package/lib/network-dns-resolver.js +71 -8
- package/lib/network-dns.js +26 -0
- package/lib/network-smtp-policy.js +42 -10
- package/lib/parsers/safe-yaml.js +24 -3
- package/lib/redact.js +13 -3
- package/lib/retention.js +22 -2
- package/lib/vendor/MANIFEST.json +12 -12
- package/lib/vendor/blamejs-pki.cjs +278 -40
- package/lib/yaml-lex.js +587 -0
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
package/lib/mail-arc-sign.js
CHANGED
|
@@ -128,7 +128,12 @@ function _bodyHashB64(body, algorithm) {
|
|
|
128
128
|
var canonical = _canonRelaxedBody(body);
|
|
129
129
|
// RFC 6376 §3.4.4 — empty body canon is `\r\n` (one CRLF). Hash
|
|
130
130
|
// includes that CRLF.
|
|
131
|
-
|
|
131
|
+
//
|
|
132
|
+
// latin1, for the reason DKIM uses it: ARC seals a message the same way DKIM
|
|
133
|
+
// signs one, over the OCTETS on the wire. `update(string)` defaults to UTF-8,
|
|
134
|
+
// which re-encodes every high byte and seals a body that is not the one the
|
|
135
|
+
// message carries.
|
|
136
|
+
return nodeCrypto.createHash(hashAlgo).update(canonical, "latin1").digest("base64");
|
|
132
137
|
}
|
|
133
138
|
|
|
134
139
|
// RFC 8617 §5 — ARC chains MUST NOT exceed 50 hops. The verifier
|
|
@@ -182,8 +187,27 @@ function sign(opts) {
|
|
|
182
187
|
"headersToSign", "timestamp", "audit", "excludeAarFromAms",
|
|
183
188
|
], "mail.arc.sign");
|
|
184
189
|
|
|
185
|
-
|
|
186
|
-
|
|
190
|
+
// A seal covers the OCTETS on the wire, so a caller holding them seals them
|
|
191
|
+
// rather than a UTF-8 decode of them. One code unit per octet from here down.
|
|
192
|
+
//
|
|
193
|
+
// Through `dkim._toWire`, which is the SAME function the DKIM signer uses.
|
|
194
|
+
// Converting the Buffer here by hand and leaving a string untouched was the
|
|
195
|
+
// first version, and it left every non-ASCII string message signed over the
|
|
196
|
+
// low byte of each code unit: the hashing and signing paths below read a
|
|
197
|
+
// string as latin1 octets, so a message that is not already ASCII has to be
|
|
198
|
+
// put into that form before it reaches them, not after. Two functions
|
|
199
|
+
// answering "which bytes is this message" is how they come to disagree.
|
|
200
|
+
var gaveBuffer = Buffer.isBuffer(opts.rfc822);
|
|
201
|
+
if (gaveBuffer) {
|
|
202
|
+
if (opts.rfc822.length === 0) {
|
|
203
|
+
throw new MailAuthError("arc-sign/bad-input",
|
|
204
|
+
"sign: rfc822 must be a non-empty Buffer or string");
|
|
205
|
+
}
|
|
206
|
+
} else {
|
|
207
|
+
validateOpts.requireNonEmptyString(opts.rfc822, "sign: rfc822",
|
|
208
|
+
MailAuthError, "arc-sign/bad-input");
|
|
209
|
+
}
|
|
210
|
+
opts = Object.assign({}, opts, { rfc822: dkim._toWire(opts.rfc822) });
|
|
187
211
|
if (typeof opts.instance !== "number" || !isFinite(opts.instance) ||
|
|
188
212
|
opts.instance < 1 || opts.instance > 50 || // RFC 8617 §5 chain bound
|
|
189
213
|
Math.floor(opts.instance) !== opts.instance) {
|
|
@@ -387,7 +411,14 @@ function sign(opts) {
|
|
|
387
411
|
"ARC-Seal: " + asValue + "\r\n" +
|
|
388
412
|
"ARC-Message-Signature: " + amsValue + "\r\n" +
|
|
389
413
|
"ARC-Authentication-Results: " + aarValue + "\r\n";
|
|
390
|
-
|
|
414
|
+
// Returned in the shape it arrived in, so a caller relaying the sealed
|
|
415
|
+
// message relays the octets they handed over. See the same note in
|
|
416
|
+
// lib/mail-dkim.js sign(): a string caller gets their own string back, which
|
|
417
|
+
// is the UTF-8 decode of the wire form rather than the wire form itself.
|
|
418
|
+
var sealedWire = prependedHeaders + opts.rfc822;
|
|
419
|
+
var sealedRfc822 = gaveBuffer
|
|
420
|
+
? Buffer.from(sealedWire, "latin1")
|
|
421
|
+
: Buffer.from(sealedWire, "latin1").toString("utf8");
|
|
391
422
|
|
|
392
423
|
if (auditOn) {
|
|
393
424
|
try {
|
|
@@ -418,15 +449,17 @@ function sign(opts) {
|
|
|
418
449
|
}
|
|
419
450
|
|
|
420
451
|
function _signOne(canonInput, keyObject, algorithm) {
|
|
452
|
+
// The canonicalized header octets, one code unit per octet — same boundary as
|
|
453
|
+
// the body hash above and as DKIM's.
|
|
454
|
+
var toSign = Buffer.from(canonInput, "latin1");
|
|
421
455
|
if (algorithm === "ed25519-sha256") {
|
|
422
456
|
// Ed25519 prehash variant — Node's `crypto.sign(null, msg, key)`
|
|
423
457
|
// accepts the message directly.
|
|
424
|
-
return nodeCrypto.sign(null,
|
|
425
|
-
.toString("base64");
|
|
458
|
+
return nodeCrypto.sign(null, toSign, keyObject).toString("base64");
|
|
426
459
|
}
|
|
427
460
|
// RSA-SHA256 / default — createSign + update + sign.
|
|
428
461
|
var signer = nodeCrypto.createSign("RSA-SHA256");
|
|
429
|
-
signer.update(
|
|
462
|
+
signer.update(toSign);
|
|
430
463
|
return signer.sign(keyObject).toString("base64");
|
|
431
464
|
}
|
|
432
465
|
|
package/lib/mail-auth.js
CHANGED
|
@@ -1926,10 +1926,15 @@ function _arcInstanceOf(value) {
|
|
|
1926
1926
|
}
|
|
1927
1927
|
|
|
1928
1928
|
async function arcVerify(rfc822, opts) {
|
|
1929
|
-
if (typeof rfc822 !== "string" || rfc822.length === 0) {
|
|
1929
|
+
if ((!Buffer.isBuffer(rfc822) && typeof rfc822 !== "string") || rfc822.length === 0) {
|
|
1930
1930
|
throw new MailAuthError("mail-auth/arc-bad-input",
|
|
1931
|
-
"arc.verify: rfc822 must be a non-empty string");
|
|
1931
|
+
"arc.verify: rfc822 must be a non-empty Buffer or string");
|
|
1932
1932
|
}
|
|
1933
|
+
// A seal covers the octets on the wire, so a caller holding them verifies
|
|
1934
|
+
// against them. Same boundary as DKIM, and the SAME function, so a string
|
|
1935
|
+
// cannot come to mean one thing here and another there: a Buffer is octets,
|
|
1936
|
+
// a string is text and its octets are its UTF-8 encoding.
|
|
1937
|
+
rfc822 = dkim._toWire(rfc822);
|
|
1933
1938
|
opts = opts || {};
|
|
1934
1939
|
// RFC 8617 §5.1.1 / RFC 6376 §3.4 — ARC-Message-Signature verification
|
|
1935
1940
|
// reuses the DKIM verifier, whose header/body split REQUIRES CRLF CRLF and
|
|
@@ -2333,7 +2338,10 @@ async function _verifyAmsViaDkim(rfc822, hop, sigValue, tags, dkim, dnsLookup) {
|
|
|
2333
2338
|
// unreachable from b.mail.dkim.verify.
|
|
2334
2339
|
var verifyOpts = { dnsLookup: dnsLookup };
|
|
2335
2340
|
verifyOpts[ARC_AMS_REUSE] = true;
|
|
2336
|
-
|
|
2341
|
+
// As OCTETS. `synthetic` is already the wire form, one code unit per octet,
|
|
2342
|
+
// and handing it over as a string would have the verifier read it as text and
|
|
2343
|
+
// re-encode it as UTF-8 — hashing bytes this message does not contain.
|
|
2344
|
+
var rv = await dkim.verify(dkim._wireBytes(synthetic), verifyOpts);
|
|
2337
2345
|
if (!Array.isArray(rv) || rv.length === 0) {
|
|
2338
2346
|
return { result: "permerror", errors: ["ams: dkim verifier returned no results"] };
|
|
2339
2347
|
}
|
|
@@ -2401,7 +2409,11 @@ function _runVerify(signedString, sigB64, algorithm, keyB64, label) {
|
|
|
2401
2409
|
var sigBuf = Buffer.from(sigB64, "base64");
|
|
2402
2410
|
var verified;
|
|
2403
2411
|
try {
|
|
2404
|
-
|
|
2412
|
+
// latin1: the signer sealed the canonicalized OCTETS (lib/mail-arc-sign.js),
|
|
2413
|
+
// and the message reaches here one code unit per octet. Re-encoding as UTF-8
|
|
2414
|
+
// would verify against bytes the signer never saw, for any message carrying
|
|
2415
|
+
// 8-bit content.
|
|
2416
|
+
verified = nodeCrypto.verify(nodeAlgo, Buffer.from(signedString, "latin1"), keyObj, sigBuf);
|
|
2405
2417
|
} catch (e) {
|
|
2406
2418
|
return { result: "permerror",
|
|
2407
2419
|
errors: [label + ": verify threw: " + ((e && e.message) || String(e))] };
|
|
@@ -2429,10 +2441,19 @@ void C; // C is imported for future TIME constants in policy fetchers.
|
|
|
2429
2441
|
// // trustedDomain: "mailgun.net" }
|
|
2430
2442
|
|
|
2431
2443
|
async function arcEvaluate(rfc822, opts) {
|
|
2432
|
-
if (typeof rfc822 !== "string" || rfc822.length === 0) {
|
|
2444
|
+
if ((!Buffer.isBuffer(rfc822) && typeof rfc822 !== "string") || rfc822.length === 0) {
|
|
2433
2445
|
throw new MailAuthError("mail-auth/arc-bad-input",
|
|
2434
|
-
"arc.evaluate: rfc822 must be a non-empty string");
|
|
2435
|
-
}
|
|
2446
|
+
"arc.evaluate: rfc822 must be a non-empty Buffer or string");
|
|
2447
|
+
}
|
|
2448
|
+
// Two views, and the distinction matters. The header re-scan below reads the
|
|
2449
|
+
// message as a wire STRING; `arcVerify` resolves its own argument, and
|
|
2450
|
+
// `_toWire` is idempotent on a Buffer but NOT on a string — applied twice it
|
|
2451
|
+
// reads the latin1 wire form as text and re-encodes it as UTF-8, so the
|
|
2452
|
+
// octets checked stop being the octets signed. Passing the wire string on
|
|
2453
|
+
// made a Unicode message pass `arc.verify` and fail `arc.evaluate`, which are
|
|
2454
|
+
// supposed to answer the same question.
|
|
2455
|
+
rfc822 = dkim._toWire(rfc822);
|
|
2456
|
+
var rfc822Octets = dkim._wireBytes(rfc822);
|
|
2436
2457
|
opts = opts || {};
|
|
2437
2458
|
if (!Array.isArray(opts.trustedSealers)) {
|
|
2438
2459
|
throw new MailAuthError("mail-auth/arc-bad-trusted-sealers",
|
|
@@ -2448,7 +2469,7 @@ async function arcEvaluate(rfc822, opts) {
|
|
|
2448
2469
|
trusted[d.toLowerCase()] = true;
|
|
2449
2470
|
}
|
|
2450
2471
|
|
|
2451
|
-
var verdict = await arcVerify(
|
|
2472
|
+
var verdict = await arcVerify(rfc822Octets, opts);
|
|
2452
2473
|
var out = {
|
|
2453
2474
|
chainStatus: verdict.chainStatus,
|
|
2454
2475
|
hopCount: verdict.hopCount,
|
|
@@ -2695,7 +2716,7 @@ function authResultsEmit(opts) {
|
|
|
2695
2716
|
// message: rfc5322Bytes, // string or Buffer
|
|
2696
2717
|
// authservId: "mx.example.com",
|
|
2697
2718
|
// });
|
|
2698
|
-
// // → { spf, dkim, from, dmarc, authResults }
|
|
2719
|
+
// // → { spf, dkim, from, dmarc, arc, authResults }
|
|
2699
2720
|
// if (v.dmarc.recommendedAction === "reject") { /* refuse 550 5.7.1 */ }
|
|
2700
2721
|
//
|
|
2701
2722
|
// From-header discipline (RFC 9989 §5.3.1, RFC 7489 §6.6.1 before it):
|
|
@@ -2834,14 +2855,13 @@ async function inboundVerify(opts) {
|
|
|
2834
2855
|
}
|
|
2835
2856
|
var message = opts.message;
|
|
2836
2857
|
if (Buffer.isBuffer(message)) {
|
|
2837
|
-
//
|
|
2838
|
-
//
|
|
2839
|
-
//
|
|
2840
|
-
//
|
|
2841
|
-
//
|
|
2842
|
-
//
|
|
2843
|
-
|
|
2844
|
-
message = message.toString("utf8");
|
|
2858
|
+
// One code unit per octet, which is what the DKIM verifier now hashes.
|
|
2859
|
+
// This used to decode as UTF-8, on the reasoning that the verifier
|
|
2860
|
+
// re-encoded as UTF-8 and so only valid-UTF-8 content could round-trip.
|
|
2861
|
+
// That was accurate and it meant a message carrying ordinary 8-bit content
|
|
2862
|
+
// verified as a DKIM fail, leaving DMARC to fall back to the SPF identity
|
|
2863
|
+
// — and nothing to fall back to once the message had been forwarded.
|
|
2864
|
+
message = message.toString("latin1");
|
|
2845
2865
|
}
|
|
2846
2866
|
if (typeof message !== "string" || message.length === 0) {
|
|
2847
2867
|
throw new MailAuthError("mail-auth/inbound-bad-message",
|
|
@@ -2893,10 +2913,40 @@ async function inboundVerify(opts) {
|
|
|
2893
2913
|
if (opts.clockSkewMs !== undefined) dkimVerifyOpts.clockSkewMs = opts.clockSkewMs;
|
|
2894
2914
|
if (opts.maxSignatures !== undefined) dkimVerifyOpts.maxSignatures = opts.maxSignatures;
|
|
2895
2915
|
if (opts.minRsaBits !== undefined) dkimVerifyOpts.minRsaBits = opts.minRsaBits;
|
|
2896
|
-
|
|
2916
|
+
// As OCTETS. `message` is the wire form here, one code unit per octet, and
|
|
2917
|
+
// both verifiers read a string as text — handing one over would re-encode it
|
|
2918
|
+
// as UTF-8 and verify against bytes the sender never signed.
|
|
2919
|
+
var messageOctets = dkim._wireBytes(message);
|
|
2920
|
+
var dkimResults = await dkim.verify(messageOctets, dkimVerifyOpts);
|
|
2921
|
+
|
|
2922
|
+
// ARC (RFC 8617) — the chain a forwarder or list left behind, carrying the
|
|
2923
|
+
// authentication verdict from before it rewrote the message. A receiver
|
|
2924
|
+
// without it sees forwarded mail fail DMARC on the forwarder's identity with
|
|
2925
|
+
// no way to recover the original result, which is the case ARC exists for.
|
|
2926
|
+
//
|
|
2927
|
+
// The chain is EVIDENCE, not a gate: a malformed one is reported as a fail
|
|
2928
|
+
// verdict and the pipeline continues, because refusing mail over a defect in
|
|
2929
|
+
// some intermediary's headers would be the receiver punishing the wrong
|
|
2930
|
+
// party. Deciding what a passing chain buys a sender is local policy, which
|
|
2931
|
+
// is what b.mail.arc.evaluate + trustedSealers is for.
|
|
2932
|
+
var arcResult;
|
|
2933
|
+
try {
|
|
2934
|
+
arcResult = await arcVerify(messageOctets, { dnsLookup: opts.dnsLookup });
|
|
2935
|
+
} catch (e) {
|
|
2936
|
+
arcResult = { chainStatus: "fail", hopCount: 0, hops: [],
|
|
2937
|
+
reason: "arc-verify-error: " + ((e && e.message) || String(e)) };
|
|
2938
|
+
}
|
|
2897
2939
|
|
|
2898
2940
|
// From header + DMARC policy/alignment.
|
|
2899
|
-
|
|
2941
|
+
// The From header is read as TEXT, while DKIM and ARC above read the same
|
|
2942
|
+
// message as OCTETS. Both are needed and they are not the same view: a
|
|
2943
|
+
// signature covers the bytes on the wire, and an address is characters —
|
|
2944
|
+
// an RFC 6531 local part written in UTF-8 aligns against a DMARC record
|
|
2945
|
+
// only once it is decoded as UTF-8, and reading it octet-per-code-unit
|
|
2946
|
+
// would compare mojibake. Only the header block is decoded, so the body's
|
|
2947
|
+
// size does not pay for it.
|
|
2948
|
+
var headerOctets = _splitHeaderBlock(message).headers;
|
|
2949
|
+
var from = _extractFromHeaders(Buffer.from(headerOctets, "latin1").toString("utf8"));
|
|
2900
2950
|
var dmarc;
|
|
2901
2951
|
if (from.count === 1 && from.address && from.domain) {
|
|
2902
2952
|
dmarc = await dmarcEvaluate({
|
|
@@ -2956,10 +3006,15 @@ async function inboundVerify(opts) {
|
|
|
2956
3006
|
var dmarcEntry = { method: "dmarc", result: dmarc.result };
|
|
2957
3007
|
if (from.address) dmarcEntry.from = from.address;
|
|
2958
3008
|
arResults.push(dmarcEntry);
|
|
3009
|
+
// RFC 8601 §2.7.6 — the arc method. "none" is reported like any other
|
|
3010
|
+
// verdict: a receiver reading the header can tell a chain that was checked
|
|
3011
|
+
// and found absent from a receiver that does not check.
|
|
3012
|
+
arResults.push({ method: "arc", result: arcResult.chainStatus });
|
|
2959
3013
|
authResults = authResultsEmit({ authservId: opts.authservId, results: arResults });
|
|
2960
3014
|
}
|
|
2961
3015
|
|
|
2962
|
-
return { spf: spf, dkim: dkimResults, from: from, dmarc: dmarc,
|
|
3016
|
+
return { spf: spf, dkim: dkimResults, from: from, dmarc: dmarc,
|
|
3017
|
+
arc: arcResult, authResults: authResults };
|
|
2963
3018
|
}
|
|
2964
3019
|
|
|
2965
3020
|
// ---- DMARC aggregate (RUA) report parser (RFC 9990 §3 / draft-ietf-dmarc-aggregate-reporting) ----
|
package/lib/mail-crypto-pgp.js
CHANGED
|
@@ -81,7 +81,7 @@
|
|
|
81
81
|
* Surface:
|
|
82
82
|
* var sigBundle = b.mail.crypto.pgp.sign({
|
|
83
83
|
* message: "rfc822 body bytes",
|
|
84
|
-
* privateKeyPem: "
|
|
84
|
+
* privateKeyPem: "<PEM-encoded PKCS#8 private key>",
|
|
85
85
|
* passphrase: undefined | "...", // optional
|
|
86
86
|
* audit: opts.audit, // optional b.audit handle
|
|
87
87
|
* });
|
package/lib/mail-dkim.js
CHANGED
|
@@ -179,6 +179,51 @@ function _parseHeaders(rawHeaders) {
|
|
|
179
179
|
return out;
|
|
180
180
|
}
|
|
181
181
|
|
|
182
|
+
// ---- The octet boundary ----
|
|
183
|
+
//
|
|
184
|
+
// RFC 6376 §3.4 signs a canonicalized OCTET stream, and a JavaScript string
|
|
185
|
+
// cannot hold one: a message decoded as UTF-8 loses every sequence that is not
|
|
186
|
+
// valid UTF-8 to U+FFFD, and no string re-encodes back to those bytes. Signing
|
|
187
|
+
// hashed `utf8(decode_utf8(original))`, equal to the original only when the
|
|
188
|
+
// original was already valid UTF-8 — 128 of the 256 single-octet values do not
|
|
189
|
+
// survive it.
|
|
190
|
+
//
|
|
191
|
+
// So the message is carried as latin1 from here down. latin1 is a bijection
|
|
192
|
+
// between bytes and code units 0x00-0xFF, which makes every string operation
|
|
193
|
+
// below an operation on the octets themselves. That is not an approximation of a
|
|
194
|
+
// byte implementation, it IS one: §3.4 canonicalization touches only SP, HTAB,
|
|
195
|
+
// CR and LF, none of which can appear inside a multi-byte UTF-8 sequence, so a
|
|
196
|
+
// byte scan and this character scan agree on every input.
|
|
197
|
+
//
|
|
198
|
+
// A string argument keeps its present meaning — the octets are that string's
|
|
199
|
+
// UTF-8 encoding — so it is encoded once here and rejoins the same path. One
|
|
200
|
+
// path, and the caller's existing signatures are unchanged.
|
|
201
|
+
// NOT IDEMPOTENT ON A STRING. Applied twice to a string, the second pass reads
|
|
202
|
+
// the latin1 wire form as text and re-encodes it as UTF-8, so the octets are no
|
|
203
|
+
// longer the message's. It IS idempotent on a Buffer. So a function that
|
|
204
|
+
// resolves a message and then hands it to another function that resolves it
|
|
205
|
+
// again must pass OCTETS — `_wireBytes(_toWire(x))` — not the wire string.
|
|
206
|
+
// `arc.evaluate` passed the string to `arc.verify` and the two then disagreed
|
|
207
|
+
// about whether the same Unicode message's chain passed.
|
|
208
|
+
//
|
|
209
|
+
// Refuses anything else rather than coercing it. `String(x)` on an object
|
|
210
|
+
// yields "[object Object]", which is a message this would go on to sign — and
|
|
211
|
+
// the caller gets a valid signature over eleven characters instead of an error.
|
|
212
|
+
function _toWire(rfc822) {
|
|
213
|
+
if (Buffer.isBuffer(rfc822)) return rfc822.toString("latin1");
|
|
214
|
+
if (typeof rfc822 !== "string") {
|
|
215
|
+
throw new DkimError("dkim/bad-input",
|
|
216
|
+
"message must be a Buffer or a string — got " +
|
|
217
|
+
(rfc822 === null ? "null" : typeof rfc822));
|
|
218
|
+
}
|
|
219
|
+
return Buffer.from(rfc822, "utf8").toString("latin1");
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// The octets of a string that is already in the latin1 wire form above.
|
|
223
|
+
function _wireBytes(wire) {
|
|
224
|
+
return Buffer.from(wire, "latin1");
|
|
225
|
+
}
|
|
226
|
+
|
|
182
227
|
// ---- Hashing + signing ----
|
|
183
228
|
|
|
184
229
|
function _bodyHashB64(body, algorithm, canonBody, lcap) {
|
|
@@ -192,25 +237,29 @@ function _bodyHashB64(body, algorithm, canonBody, lcap) {
|
|
|
192
237
|
// octet stream to lcap (slicing the raw body before canonicalizing diverges
|
|
193
238
|
// whenever relaxed canon changes the byte count within the first lcap
|
|
194
239
|
// octets — WSP-run collapse, trailing-WSP strip, CRLF normalization).
|
|
240
|
+
var buf = _wireBytes(canonicalized);
|
|
195
241
|
if (typeof lcap === "number" && isFinite(lcap) && lcap >= 0) {
|
|
196
|
-
var buf = Buffer.from(canonicalized, "utf8");
|
|
197
242
|
hash.update(lcap < buf.length ? buf.subarray(0, lcap) : buf);
|
|
198
243
|
} else {
|
|
199
|
-
hash.update(
|
|
244
|
+
hash.update(buf);
|
|
200
245
|
}
|
|
201
246
|
return hash.digest("base64");
|
|
202
247
|
}
|
|
203
248
|
|
|
204
249
|
function _signString(strToSign, privateKey, algorithm) {
|
|
250
|
+
// §3.7's data hash is assembled from canonicalized header octets, so it takes
|
|
251
|
+
// the same treatment as the body: a header field carrying unencoded 8-bit
|
|
252
|
+
// content is signed as the octets it holds.
|
|
253
|
+
var toSign = _wireBytes(strToSign);
|
|
205
254
|
if (algorithm === "rsa-sha256") {
|
|
206
255
|
return nodeCrypto.createSign("RSA-SHA256")
|
|
207
|
-
.update(
|
|
256
|
+
.update(toSign).sign(privateKey).toString("base64");
|
|
208
257
|
}
|
|
209
258
|
if (algorithm === "ed25519-sha256") {
|
|
210
259
|
// Ed25519 in node:crypto signs the raw message (it hashes
|
|
211
260
|
// internally as part of EdDSA). Per RFC 8463 the verifier still
|
|
212
261
|
// sees `a=ed25519-sha256` because the body hash is sha256.
|
|
213
|
-
return nodeCrypto.sign(null,
|
|
262
|
+
return nodeCrypto.sign(null, toSign, privateKey)
|
|
214
263
|
.toString("base64");
|
|
215
264
|
}
|
|
216
265
|
throw new DkimError("dkim/bad-algorithm",
|
|
@@ -335,11 +384,15 @@ function create(opts) {
|
|
|
335
384
|
}
|
|
336
385
|
|
|
337
386
|
function sign(rfc822) {
|
|
338
|
-
|
|
387
|
+
var gaveBuffer = Buffer.isBuffer(rfc822);
|
|
388
|
+
if ((!gaveBuffer && typeof rfc822 !== "string") || rfc822.length === 0) {
|
|
339
389
|
throw new DkimError("dkim/bad-input",
|
|
340
|
-
"sign() requires the rfc822 wire format as a non-empty string");
|
|
390
|
+
"sign() requires the rfc822 wire format as a non-empty Buffer or string");
|
|
341
391
|
}
|
|
342
392
|
var t0 = Date.now();
|
|
393
|
+
// A Buffer's own octets, or a string's UTF-8 encoding, one code unit per
|
|
394
|
+
// octet from here down.
|
|
395
|
+
rfc822 = _toWire(rfc822);
|
|
343
396
|
var split = _splitHeadersBody(rfc822);
|
|
344
397
|
var parsedHeaders = _parseHeaders(split.headers);
|
|
345
398
|
|
|
@@ -455,7 +508,17 @@ function create(opts) {
|
|
|
455
508
|
durationMs: Date.now() - t0,
|
|
456
509
|
});
|
|
457
510
|
|
|
458
|
-
|
|
511
|
+
// Returned in the shape it arrived in. A caller who handed over octets is
|
|
512
|
+
// relaying what comes back, and handing them a string would put the
|
|
513
|
+
// corruption this signs around straight back into the message.
|
|
514
|
+
var out = dkimHeaderLine + rfc822;
|
|
515
|
+
// A string caller gets their own string back, not the latin1 wire form of
|
|
516
|
+
// it: the octets are that string's UTF-8 encoding, so decoding them as
|
|
517
|
+
// UTF-8 reproduces exactly what they passed, with the ASCII signature
|
|
518
|
+
// header in front. Returning the wire form instead handed back a mojibake
|
|
519
|
+
// string that no longer re-encoded to the bytes just signed, so the
|
|
520
|
+
// message failed its own verify.
|
|
521
|
+
return gaveBuffer ? _wireBytes(out) : _wireBytes(out).toString("utf8");
|
|
459
522
|
}
|
|
460
523
|
|
|
461
524
|
return {
|
|
@@ -808,7 +871,7 @@ function _verifySingleSignature(rfc822, parsedHeaders, sigHeader, keyTags, sigTa
|
|
|
808
871
|
// accept legacy l= senders opts in via verify({ acceptBodyLengthLimit: true }).
|
|
809
872
|
if (lcap !== undefined && !verifyOpts.acceptBodyLengthLimit) {
|
|
810
873
|
var fullCanon = canonBody === "simple" ? _canonBodySimple(body) : _canonBodyRelaxed(body);
|
|
811
|
-
if (lcap < Buffer.byteLength(fullCanon, "
|
|
874
|
+
if (lcap < Buffer.byteLength(fullCanon, "latin1")) {
|
|
812
875
|
return { result: "fail",
|
|
813
876
|
errors: ["DKIM-Signature l= leaves appended body content unsigned " +
|
|
814
877
|
"(RFC 6376 §8.2 append-after-signature)"] };
|
|
@@ -924,7 +987,7 @@ function _verifySingleSignature(rfc822, parsedHeaders, sigHeader, keyTags, sigTa
|
|
|
924
987
|
var verified;
|
|
925
988
|
try {
|
|
926
989
|
verified = nodeCrypto.verify(nodeAlgo,
|
|
927
|
-
|
|
990
|
+
_wireBytes(canonicalizedHeaders), keyObj, sigBuf);
|
|
928
991
|
} catch (e) {
|
|
929
992
|
return { result: "permerror",
|
|
930
993
|
errors: ["DKIM verify threw: " + ((e && e.message) || String(e))] };
|
|
@@ -943,10 +1006,14 @@ var DKIM_CLOCK_SKEW_MS_MAX = C.TIME.hours(24);
|
|
|
943
1006
|
var DKIM_CLOCK_SKEW_MS_DEFAULT = C.TIME.minutes(5);
|
|
944
1007
|
|
|
945
1008
|
async function verify(rfc822, opts) {
|
|
946
|
-
if (typeof rfc822 !== "string" || rfc822.length === 0) {
|
|
1009
|
+
if ((!Buffer.isBuffer(rfc822) && typeof rfc822 !== "string") || rfc822.length === 0) {
|
|
947
1010
|
throw new DkimError("dkim/bad-input",
|
|
948
|
-
"verify(): rfc822 must be a non-empty string");
|
|
1011
|
+
"verify(): rfc822 must be a non-empty Buffer or string");
|
|
949
1012
|
}
|
|
1013
|
+
// The same octet boundary as sign(): a receiver holding wire bytes verifies
|
|
1014
|
+
// against those bytes rather than against a UTF-8 decode of them, which is
|
|
1015
|
+
// what a sender's signature actually covers.
|
|
1016
|
+
rfc822 = _toWire(rfc822);
|
|
950
1017
|
opts = opts || {};
|
|
951
1018
|
validateOpts(opts, ["dnsLookup", "audit", "clockSkewMs", "maxSignatures",
|
|
952
1019
|
"minRsaBits", "acceptBodyLengthLimit"], "mail.dkim.verify");
|
|
@@ -1392,6 +1459,8 @@ module.exports = {
|
|
|
1392
1459
|
_canonBodyRelaxedForTest: _canonBodyRelaxed,
|
|
1393
1460
|
_canonBodySimpleForTest: _canonBodySimple,
|
|
1394
1461
|
_stripBTagValue: _stripBTagValue, // RFC 6376 §3.5 — tag-aware b= zeroing; shared by the ARC seal verifier (internal cross-module helper)
|
|
1462
|
+
_toWire: _toWire, // Buffer|string → the message one code unit per octet; shared with the ARC verifier so both modules answer "which bytes is this message" the same way (internal cross-module helper)
|
|
1463
|
+
_wireBytes: _wireBytes, // the octets of a string already in that wire form (internal cross-module helper)
|
|
1395
1464
|
_stripBTagValueForTest: _stripBTagValue,
|
|
1396
1465
|
// The header-block parser that produces the { name, value } pairs fed to the
|
|
1397
1466
|
// canonicalizers. Exposed so a golden-vector test can pin its byte-exact
|
package/lib/mail-helo.js
CHANGED
|
@@ -370,6 +370,16 @@ async function _runFcrdns(ip, resolver) {
|
|
|
370
370
|
if (!rev) {
|
|
371
371
|
return result; // unparseable IP — caller already rejected
|
|
372
372
|
}
|
|
373
|
+
// The resolver must be able to answer the question before the answer means
|
|
374
|
+
// anything. A missing method is not a DNS condition, and letting it fall into
|
|
375
|
+
// the catch below turned a broken call into a clean "no reverse name" for
|
|
376
|
+
// every address the check ever ran on — `passed` stayed false, which looks
|
|
377
|
+
// exactly like an address with no PTR record.
|
|
378
|
+
if (typeof resolver.queryPtr !== "function") {
|
|
379
|
+
throw new MailHeloError("mail-helo/resolver-missing-queryptr",
|
|
380
|
+
"fcrdns requires resolver.queryPtr(name); the supplied resolver has none, " +
|
|
381
|
+
"so reverse DNS cannot be checked and a pass cannot be claimed");
|
|
382
|
+
}
|
|
373
383
|
try {
|
|
374
384
|
var ptr = await resolver.queryPtr(rev);
|
|
375
385
|
if (ptr && ptr.rrs) {
|
package/lib/mail-rbl.js
CHANGED
|
@@ -253,9 +253,16 @@ function create(opts) {
|
|
|
253
253
|
} catch (e) {
|
|
254
254
|
// NXDOMAIN is the expected "not listed" response, not an error
|
|
255
255
|
// condition. RFC 5782 §2.1.1 — absence of any A record means
|
|
256
|
-
// "not in list".
|
|
257
|
-
//
|
|
258
|
-
|
|
256
|
+
// "not in list".
|
|
257
|
+
//
|
|
258
|
+
// ONLY NXDOMAIN. The resolver used to report every non-zero RCODE under
|
|
259
|
+
// one code, so this branch also caught SERVFAIL and REFUSED and returned
|
|
260
|
+
// the same clean verdict — a blocklist lookup that failed read exactly
|
|
261
|
+
// like a host that is not on the list. Anyone able to break the query
|
|
262
|
+
// could therefore clear themselves, which is the wrong direction for a
|
|
263
|
+
// blocklist to fail in. A failure now falls through to `rv.error` below,
|
|
264
|
+
// where the caller can see it.
|
|
265
|
+
if (e && e.code === "resolver/nxdomain") {
|
|
259
266
|
// Neutral — not listed; not an error.
|
|
260
267
|
return rv;
|
|
261
268
|
}
|