@blamejs/core 0.8.89 → 0.9.0
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 +847 -845
- package/index.js +6 -0
- package/lib/ai-pref.js +8 -2
- package/lib/auth/step-up.js +14 -5
- package/lib/cdn-cache-control.js +473 -0
- package/lib/client-hints.js +318 -0
- package/lib/http-client-cache.js +15 -8
- package/lib/http-client-cookie-jar.js +18 -7
- package/lib/http-message-signature.js +18 -11
- package/lib/log-stream.js +25 -1
- package/lib/mail-auth.js +3 -2
- package/lib/mail-require-tls.js +198 -0
- package/lib/mail.js +3 -1
- package/lib/middleware/body-parser.js +24 -12
- package/lib/middleware/scim-server.js +2 -2
- package/lib/middleware/tus-upload.js +12 -7
- package/lib/network-dns.js +178 -0
- package/lib/network-smtp-policy.js +2 -2
- package/lib/request-helpers.js +15 -0
- package/lib/security-assert.js +23 -1
- package/lib/structured-fields.js +244 -0
- package/lib/websocket.js +15 -9
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* @module b.mail.requireTls
|
|
4
|
+
* @nav Mail
|
|
5
|
+
* @title REQUIRETLS — RFC 8689
|
|
6
|
+
* @order 460
|
|
7
|
+
*
|
|
8
|
+
* @intro
|
|
9
|
+
* RFC 8689 SMTP REQUIRETLS — per-message TLS-requirement signaling
|
|
10
|
+
* between sender and receiver MTAs. The sender advertises that the
|
|
11
|
+
* message MUST NOT be relayed over a cleartext (non-TLS) hop; if
|
|
12
|
+
* no downstream MTA can deliver under TLS, the message bounces
|
|
13
|
+
* instead of falling back to cleartext. Complements MTA-STS / DANE
|
|
14
|
+
* (which are policy-side, domain-scoped) with a per-message
|
|
15
|
+
* knob that overrides the policy when the operator wants
|
|
16
|
+
* stricter-than-policy delivery.
|
|
17
|
+
*
|
|
18
|
+
* Wire surface (RFC 8689 §3):
|
|
19
|
+
*
|
|
20
|
+
* EHLO peer advertises: 250 REQUIRETLS
|
|
21
|
+
* Client sends: MAIL FROM:<sender> REQUIRETLS
|
|
22
|
+
* Server replies: 250 OK (or 550 if it can't honor)
|
|
23
|
+
*
|
|
24
|
+
* Header surface (RFC 8689 §5):
|
|
25
|
+
*
|
|
26
|
+
* TLS-Required: No Explicit operator override; sender
|
|
27
|
+
* requests REQUIRETLS-style behavior be
|
|
28
|
+
* DISABLED for this message even if the
|
|
29
|
+
* policy infrastructure (MTA-STS / DANE)
|
|
30
|
+
* says otherwise. Use sparingly — primary
|
|
31
|
+
* use case is delivery to legacy peers
|
|
32
|
+
* during a controlled migration.
|
|
33
|
+
*
|
|
34
|
+
* This module ships:
|
|
35
|
+
*
|
|
36
|
+
* b.mail.requireTls.peerSupports(ehloLines) → boolean
|
|
37
|
+
* Walks EHLO response lines and returns true when the peer
|
|
38
|
+
* advertised the REQUIRETLS keyword.
|
|
39
|
+
*
|
|
40
|
+
* b.mail.requireTls.mailFromExtension({ requireTls }) → string
|
|
41
|
+
* Returns the trailing " REQUIRETLS" token (or empty string)
|
|
42
|
+
* to append to a MAIL FROM line.
|
|
43
|
+
*
|
|
44
|
+
* b.mail.requireTls.parseTlsRequiredHeader(headerValue) → "yes" | "no" | null
|
|
45
|
+
* Parses the TLS-Required header field per §5. Returns "no"
|
|
46
|
+
* only when the value is the literal token "no" (case-
|
|
47
|
+
* insensitive); any other value returns "yes" (the conservative
|
|
48
|
+
* default — operators must opt OUT explicitly, never default to
|
|
49
|
+
* fall-back-to-cleartext). null when the header is absent.
|
|
50
|
+
*
|
|
51
|
+
* @card
|
|
52
|
+
* RFC 8689 REQUIRETLS — per-message TLS-requirement signaling between MTAs (EHLO keyword + MAIL FROM extension + TLS-Required header parser).
|
|
53
|
+
*/
|
|
54
|
+
|
|
55
|
+
var structuredFields = require("./structured-fields");
|
|
56
|
+
var validateOpts = require("./validate-opts");
|
|
57
|
+
var { defineClass } = require("./framework-error");
|
|
58
|
+
|
|
59
|
+
var RequireTlsError = defineClass("RequireTlsError", { alwaysPermanent: true });
|
|
60
|
+
|
|
61
|
+
var REQUIRETLS_TOKEN = "REQUIRETLS";
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* @primitive b.mail.requireTls.peerSupports
|
|
65
|
+
* @signature b.mail.requireTls.peerSupports(ehloLines)
|
|
66
|
+
* @since 0.8.90
|
|
67
|
+
* @status stable
|
|
68
|
+
*
|
|
69
|
+
* Walk a parsed EHLO response and return `true` when the peer
|
|
70
|
+
* advertised the `REQUIRETLS` keyword. `ehloLines` is the array of
|
|
71
|
+
* post-greeting capability lines returned by the SMTP transport
|
|
72
|
+
* (each entry is the capability token, e.g. `"SIZE 10485760"`,
|
|
73
|
+
* `"PIPELINING"`, `"REQUIRETLS"`). Case-insensitive match per RFC
|
|
74
|
+
* 5321 §2.4 (EHLO keywords are uppercase by convention but
|
|
75
|
+
* comparison is case-insensitive).
|
|
76
|
+
*
|
|
77
|
+
* Returns `false` for empty / non-array input — operators who can't
|
|
78
|
+
* parse the EHLO get a definitive "not supported" verdict rather
|
|
79
|
+
* than a throw, matching the "defensive request-shape reader"
|
|
80
|
+
* convention used elsewhere.
|
|
81
|
+
*
|
|
82
|
+
* @example
|
|
83
|
+
* var ehlo = ["mail.example.com", "PIPELINING", "SIZE 10485760", "REQUIRETLS", "STARTTLS"];
|
|
84
|
+
* b.mail.requireTls.peerSupports(ehlo); // → true
|
|
85
|
+
*
|
|
86
|
+
* b.mail.requireTls.peerSupports(["PIPELINING", "SIZE 10485760"]); // → false
|
|
87
|
+
*/
|
|
88
|
+
function peerSupports(ehloLines) {
|
|
89
|
+
if (!Array.isArray(ehloLines)) return false;
|
|
90
|
+
for (var i = 0; i < ehloLines.length; i += 1) {
|
|
91
|
+
var line = ehloLines[i];
|
|
92
|
+
if (typeof line !== "string") continue;
|
|
93
|
+
// Keyword is everything up to the first space (RFC 5321 §4.1.1.1).
|
|
94
|
+
var sp = line.indexOf(" ");
|
|
95
|
+
var keyword = sp === -1 ? line : line.slice(0, sp);
|
|
96
|
+
if (keyword.toUpperCase() === REQUIRETLS_TOKEN) return true;
|
|
97
|
+
}
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* @primitive b.mail.requireTls.mailFromExtension
|
|
103
|
+
* @signature b.mail.requireTls.mailFromExtension(opts)
|
|
104
|
+
* @since 0.8.90
|
|
105
|
+
* @status stable
|
|
106
|
+
*
|
|
107
|
+
* Build the trailing SMTP MAIL FROM extension token for REQUIRETLS.
|
|
108
|
+
* Returns `" REQUIRETLS"` (with a leading space, ready to append)
|
|
109
|
+
* when `opts.requireTls === true`; empty string otherwise. The
|
|
110
|
+
* primitive does NOT validate the operator's address — that's the
|
|
111
|
+
* SMTP transport's job. This only emits the standard-defined token
|
|
112
|
+
* suffix.
|
|
113
|
+
*
|
|
114
|
+
* Refuses non-object opts. `requireTls` must be a boolean when
|
|
115
|
+
* provided (any other type throws `mail-require-tls/bad-flag`) so
|
|
116
|
+
* a truthy-but-wrong-shape value (e.g. `"yes"`) doesn't silently
|
|
117
|
+
* succeed.
|
|
118
|
+
*
|
|
119
|
+
* @opts
|
|
120
|
+
* requireTls: boolean, // true to emit " REQUIRETLS"; falsy/absent → ""
|
|
121
|
+
*
|
|
122
|
+
* @example
|
|
123
|
+
* var line = "MAIL FROM:<alice@example.com>" +
|
|
124
|
+
* b.mail.requireTls.mailFromExtension({ requireTls: true });
|
|
125
|
+
* // → "MAIL FROM:<alice@example.com> REQUIRETLS"
|
|
126
|
+
*/
|
|
127
|
+
function mailFromExtension(opts) {
|
|
128
|
+
if (!opts || typeof opts !== "object" || Array.isArray(opts)) {
|
|
129
|
+
throw new RequireTlsError("mail-require-tls/bad-opts",
|
|
130
|
+
"mailFromExtension: opts must be a non-null object", true);
|
|
131
|
+
}
|
|
132
|
+
if (opts.requireTls === undefined || opts.requireTls === false) return "";
|
|
133
|
+
if (opts.requireTls !== true) {
|
|
134
|
+
throw new RequireTlsError("mail-require-tls/bad-flag",
|
|
135
|
+
"mailFromExtension: requireTls must be a boolean (got " + typeof opts.requireTls + ")");
|
|
136
|
+
}
|
|
137
|
+
return " " + REQUIRETLS_TOKEN;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* @primitive b.mail.requireTls.parseTlsRequiredHeader
|
|
142
|
+
* @signature b.mail.requireTls.parseTlsRequiredHeader(headerValue)
|
|
143
|
+
* @since 0.8.90
|
|
144
|
+
* @status stable
|
|
145
|
+
*
|
|
146
|
+
* Parse the RFC 8689 §5 `TLS-Required` header field. Returns:
|
|
147
|
+
*
|
|
148
|
+
* - `"no"` when the value is the literal token `no` (case-
|
|
149
|
+
* insensitive, ignoring surrounding whitespace) — the sender
|
|
150
|
+
* EXPLICITLY opts out of REQUIRETLS-style behavior for this
|
|
151
|
+
* message.
|
|
152
|
+
* - `"yes"` for any other non-empty value — conservative default
|
|
153
|
+
* so an operator who set a typo / malformed value still gets
|
|
154
|
+
* the strict path (RFC 8689 §5: "if a recipient receives a
|
|
155
|
+
* message containing a TLS-Required field with any value other
|
|
156
|
+
* than 'No', it MUST be treated as if the field had been
|
|
157
|
+
* absent").
|
|
158
|
+
* - `null` when the header is absent / empty / not a string —
|
|
159
|
+
* operator code branches on null vs "yes" / "no".
|
|
160
|
+
*
|
|
161
|
+
* Refuses CR / LF / NUL in the value (header-injection-shape inputs
|
|
162
|
+
* shouldn't reach a parser that's downstream of header splitters
|
|
163
|
+
* anyway, but a defensive check here catches operator-side mistakes).
|
|
164
|
+
*
|
|
165
|
+
* @example
|
|
166
|
+
* b.mail.requireTls.parseTlsRequiredHeader("No"); // → "no"
|
|
167
|
+
* b.mail.requireTls.parseTlsRequiredHeader("no"); // → "no"
|
|
168
|
+
* b.mail.requireTls.parseTlsRequiredHeader(" no "); // → "no"
|
|
169
|
+
* b.mail.requireTls.parseTlsRequiredHeader("yes"); // → "yes"
|
|
170
|
+
* b.mail.requireTls.parseTlsRequiredHeader("anything"); // → "yes" (RFC 8689 §5 default)
|
|
171
|
+
* b.mail.requireTls.parseTlsRequiredHeader(""); // → null
|
|
172
|
+
* b.mail.requireTls.parseTlsRequiredHeader(undefined); // → null
|
|
173
|
+
*/
|
|
174
|
+
function parseTlsRequiredHeader(headerValue) {
|
|
175
|
+
if (typeof headerValue !== "string") return null;
|
|
176
|
+
structuredFields.refuseControlBytes(headerValue, {
|
|
177
|
+
ErrorClass: RequireTlsError,
|
|
178
|
+
code: "mail-require-tls/bad-header-value",
|
|
179
|
+
label: "parseTlsRequiredHeader",
|
|
180
|
+
});
|
|
181
|
+
var trimmed = headerValue.trim();
|
|
182
|
+
if (trimmed.length === 0) return null;
|
|
183
|
+
if (trimmed.toLowerCase() === "no") return "no";
|
|
184
|
+
// RFC 8689 §5 — any other value treated as if absent (strict path).
|
|
185
|
+
return "yes";
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
module.exports = {
|
|
189
|
+
peerSupports: peerSupports,
|
|
190
|
+
mailFromExtension: mailFromExtension,
|
|
191
|
+
parseTlsRequiredHeader: parseTlsRequiredHeader,
|
|
192
|
+
REQUIRETLS_TOKEN: REQUIRETLS_TOKEN,
|
|
193
|
+
RequireTlsError: RequireTlsError,
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
// Reserved for future field validation paths; kept in canonical
|
|
197
|
+
// require ordering.
|
|
198
|
+
void validateOpts;
|
package/lib/mail.js
CHANGED
|
@@ -1818,11 +1818,13 @@ function feedbackId(opts) {
|
|
|
1818
1818
|
return parts.join(":");
|
|
1819
1819
|
}
|
|
1820
1820
|
|
|
1821
|
-
var
|
|
1821
|
+
var mailRequireTls = require("./mail-require-tls");
|
|
1822
|
+
var mailSrs = require("./mail-srs");
|
|
1822
1823
|
|
|
1823
1824
|
module.exports = {
|
|
1824
1825
|
create: create,
|
|
1825
1826
|
feedbackId: feedbackId,
|
|
1827
|
+
requireTls: mailRequireTls,
|
|
1826
1828
|
srs: mailSrs,
|
|
1827
1829
|
MailError: MailError,
|
|
1828
1830
|
unsubscribe: mailUnsubscribe,
|
|
@@ -112,13 +112,14 @@ var fs = require("fs");
|
|
|
112
112
|
var os = require("os");
|
|
113
113
|
var path = require("path");
|
|
114
114
|
var nodeCrypto = require("node:crypto");
|
|
115
|
-
var atomicFile
|
|
116
|
-
var crypto
|
|
117
|
-
var lazyRequire
|
|
118
|
-
var requestHelpers
|
|
119
|
-
var safeBuffer
|
|
120
|
-
var safeJson
|
|
121
|
-
var
|
|
115
|
+
var atomicFile = require("../atomic-file");
|
|
116
|
+
var crypto = require("../crypto");
|
|
117
|
+
var lazyRequire = require("../lazy-require");
|
|
118
|
+
var requestHelpers = require("../request-helpers");
|
|
119
|
+
var safeBuffer = require("../safe-buffer");
|
|
120
|
+
var safeJson = require("../safe-json");
|
|
121
|
+
var structuredFields = require("../structured-fields");
|
|
122
|
+
var validateOpts = require("../validate-opts");
|
|
122
123
|
var C = require("../constants");
|
|
123
124
|
var { defineClass } = require("../framework-error");
|
|
124
125
|
|
|
@@ -214,14 +215,20 @@ function _contentType(req) {
|
|
|
214
215
|
var params = {};
|
|
215
216
|
if (idx !== -1) {
|
|
216
217
|
var rest = ct.slice(idx + 1);
|
|
217
|
-
|
|
218
|
+
// RFC 9110 §8.3 + §5.6.6 — parameter values may be quoted-string
|
|
219
|
+
// (e.g. `boundary="foo;bar"`, `charset="x;y"`). Bare `.split(";")`
|
|
220
|
+
// would slice through quoted commas/semicolons and corrupt the
|
|
221
|
+
// multipart boundary. Use the shared quote-aware splitter that
|
|
222
|
+
// tracks RFC 8941 §3.3.3 quoted-string state with backslash-escape.
|
|
223
|
+
var parts = structuredFields.splitTopLevel(rest, ";");
|
|
218
224
|
for (var i = 0; i < parts.length; i++) {
|
|
219
225
|
var p = parts[i].trim();
|
|
220
226
|
var eq = p.indexOf("=");
|
|
221
227
|
if (eq === -1) continue;
|
|
222
228
|
var k = p.slice(0, eq).trim().toLowerCase();
|
|
223
229
|
var v = p.slice(eq + 1).trim();
|
|
224
|
-
|
|
230
|
+
var _unq = structuredFields.unquoteSfString(v);
|
|
231
|
+
if (_unq !== null) v = _unq;
|
|
225
232
|
params[k] = v;
|
|
226
233
|
}
|
|
227
234
|
}
|
|
@@ -305,7 +312,7 @@ function _detectSmuggling(req) {
|
|
|
305
312
|
// (RFC 9112 §6.1). Anything else is a smuggling vector or
|
|
306
313
|
// server-side decode error.
|
|
307
314
|
if (typeof te === "string" && te.length > 0) {
|
|
308
|
-
var tokens = te.toLowerCase().split(",").map(function (t) { return t.trim(); });
|
|
315
|
+
var tokens = te.toLowerCase().split(",").map(function (t) { return t.trim(); }); // allow:bare-split-on-quoted-header — RFC 9112 §6.1 Transfer-Encoding values (chunked / gzip / deflate / identity) are token-only; no quoted-string in the grammar
|
|
309
316
|
var last = tokens[tokens.length - 1];
|
|
310
317
|
if (last !== "chunked") {
|
|
311
318
|
return {
|
|
@@ -621,7 +628,11 @@ function _parseHeaderParams(headerValue) {
|
|
|
621
628
|
// `filename` so downstream consumers don't need parser-aware code.
|
|
622
629
|
var out = { _value: "" };
|
|
623
630
|
if (!headerValue) return out;
|
|
624
|
-
|
|
631
|
+
// RFC 6266 §4.1 + RFC 9110 §5.6.6 — parameter values may be
|
|
632
|
+
// quoted-string (e.g. `filename="weird;name.txt"`). Bare
|
|
633
|
+
// `.split(";")` would slice through the quoted semicolon and
|
|
634
|
+
// corrupt the filename. Quote-aware shared splitter.
|
|
635
|
+
var parts = structuredFields.splitTopLevel(headerValue, ";");
|
|
625
636
|
out._value = parts[0].trim().toLowerCase();
|
|
626
637
|
var extName = null;
|
|
627
638
|
for (var i = 1; i < parts.length; i++) {
|
|
@@ -630,7 +641,8 @@ function _parseHeaderParams(headerValue) {
|
|
|
630
641
|
if (eq === -1) continue;
|
|
631
642
|
var k = p.slice(0, eq).trim().toLowerCase();
|
|
632
643
|
var v = p.slice(eq + 1).trim();
|
|
633
|
-
|
|
644
|
+
var _unq = structuredFields.unquoteSfString(v);
|
|
645
|
+
if (_unq !== null) v = _unq;
|
|
634
646
|
if (k.charAt(k.length - 1) === "*") {
|
|
635
647
|
var decoded = _decodeRfc5987(v);
|
|
636
648
|
if (decoded !== null) {
|
|
@@ -177,8 +177,8 @@ async function _dispatch(req, res, basePath, bearer, opts, maxPageSize) {
|
|
|
177
177
|
count: pageSize,
|
|
178
178
|
sortBy: query.sortBy || null,
|
|
179
179
|
sortOrder: query.sortOrder || null,
|
|
180
|
-
attributes: query.attributes ? query.attributes.split(",") : null,
|
|
181
|
-
excludedAttributes: query.excludedAttributes ? query.excludedAttributes.split(",") : null,
|
|
180
|
+
attributes: query.attributes ? query.attributes.split(",") : null, // allow:bare-split-on-quoted-header — RFC 7644 §3.9 attributes/excludedAttributes are SCIM attribute paths (URN-ish identifiers); grammar excludes DQUOTE
|
|
181
|
+
excludedAttributes: query.excludedAttributes ? query.excludedAttributes.split(",") : null, // allow:bare-split-on-quoted-header — same SCIM attribute-name grammar
|
|
182
182
|
}, ctx);
|
|
183
183
|
_writeJson(res, H.OK, {
|
|
184
184
|
schemas: [SCIM_MESSAGE_LIST],
|
|
@@ -40,13 +40,14 @@
|
|
|
40
40
|
* cannot satisfy.
|
|
41
41
|
*/
|
|
42
42
|
|
|
43
|
-
var nodeCrypto
|
|
44
|
-
var C
|
|
45
|
-
var bCrypto
|
|
46
|
-
var lazyRequire
|
|
47
|
-
var safeAsync
|
|
48
|
-
var safeBuffer
|
|
49
|
-
var
|
|
43
|
+
var nodeCrypto = require("crypto"); // for createHash() in checksum extension
|
|
44
|
+
var C = require("../constants");
|
|
45
|
+
var bCrypto = require("../crypto");
|
|
46
|
+
var lazyRequire = require("../lazy-require");
|
|
47
|
+
var safeAsync = require("../safe-async");
|
|
48
|
+
var safeBuffer = require("../safe-buffer");
|
|
49
|
+
var structuredFields = require("../structured-fields");
|
|
50
|
+
var validateOpts = require("../validate-opts");
|
|
50
51
|
var { defineClass } = require("../framework-error");
|
|
51
52
|
|
|
52
53
|
// Observability metric prefix for the TUS middleware. The framework
|
|
@@ -145,6 +146,10 @@ function _serializeMetadata(metaObj) {
|
|
|
145
146
|
function _parseChecksumHeader(headerValue, allowedSet) {
|
|
146
147
|
// tus.io 1.0.0 §3.5: `Upload-Checksum: <algo> <base64-digest>`.
|
|
147
148
|
if (typeof headerValue !== "string") return null;
|
|
149
|
+
// The tus.io grammar implicitly excludes C0 / DEL (token + base64
|
|
150
|
+
// alphabet); refuse those on the RAW value BEFORE the slice/trim
|
|
151
|
+
// normalisation (same v0.8.90 trim-before-validate bug class).
|
|
152
|
+
if (structuredFields.containsControlBytes(headerValue)) return { error: "malformed" };
|
|
148
153
|
var sp = headerValue.indexOf(" ");
|
|
149
154
|
if (sp === -1) return { error: "malformed" };
|
|
150
155
|
var algo = headerValue.slice(0, sp).trim().toLowerCase();
|
package/lib/network-dns.js
CHANGED
|
@@ -1708,9 +1708,187 @@ function isNullMx(mxRecords) {
|
|
|
1708
1708
|
return only.exchange === "" || only.exchange === ".";
|
|
1709
1709
|
}
|
|
1710
1710
|
|
|
1711
|
+
// RFC 9905 — Deprecating DNSSEC SHA-1 Usage. The IANA DNSSEC Algorithm
|
|
1712
|
+
// Numbers registry classifies SHA-1-based DNSKEY algorithms (5
|
|
1713
|
+
// RSASHA1, 7 RSASHA1-NSEC3-SHA1, 10 RSASHA512-using-SHA1-NSEC3) and
|
|
1714
|
+
// SHA-1 DS digest type 1 as "MUST NOT be used" / "MUST NOT be
|
|
1715
|
+
// supported". Operators auditing inbound DNSSEC chain-of-trust data
|
|
1716
|
+
// classify a record's algorithm number to decide whether to refuse
|
|
1717
|
+
// the validation as deprecated.
|
|
1718
|
+
//
|
|
1719
|
+
// Returns the classification verdict object:
|
|
1720
|
+
// {
|
|
1721
|
+
// deprecated: boolean, // true when SHA-1 family per RFC 9905 §3-§4
|
|
1722
|
+
// algorithm: number, // echo of input
|
|
1723
|
+
// name: string, // human-readable label
|
|
1724
|
+
// reason: string, // citation
|
|
1725
|
+
// }
|
|
1726
|
+
// for any IANA DNSKEY algorithm number, or null for unknown / non-
|
|
1727
|
+
// numeric input. Defensive request-shape reader — never throws.
|
|
1728
|
+
|
|
1729
|
+
/**
|
|
1730
|
+
* @primitive b.network.dns.classifyDnskeyAlgorithm
|
|
1731
|
+
* @signature b.network.dns.classifyDnskeyAlgorithm(algorithm)
|
|
1732
|
+
* @since 0.8.91
|
|
1733
|
+
* @status stable
|
|
1734
|
+
* @related b.network.dns.classifyDsDigestType, b.network.dns.isNullMx
|
|
1735
|
+
*
|
|
1736
|
+
* Classify a DNSKEY / RRSIG algorithm number against the IANA DNS
|
|
1737
|
+
* Security Algorithm Numbers registry, flagging SHA-1-based and
|
|
1738
|
+
* other deprecated algorithms per RFC 9905 (Deprecating DNSSEC
|
|
1739
|
+
* SHA-1 Usage), RFC 8624 (Algorithm Implementation Requirements),
|
|
1740
|
+
* and RFC 6944 / RFC 6725 (RSAMD5 deprecation).
|
|
1741
|
+
*
|
|
1742
|
+
* Returns `{ algorithm, name, deprecated, reason, known }` for any
|
|
1743
|
+
* IANA-assigned number; `known: false` for unassigned numbers
|
|
1744
|
+
* (operators decide whether unassigned == deprecated for their
|
|
1745
|
+
* threat model). Returns `null` for non-integer / non-finite input.
|
|
1746
|
+
*
|
|
1747
|
+
* Operators auditing inbound DNSSEC chain-of-trust evidence call
|
|
1748
|
+
* this on each link's algorithm number and refuse the validation
|
|
1749
|
+
* when `deprecated === true`. Defensive request-shape reader —
|
|
1750
|
+
* never throws.
|
|
1751
|
+
*
|
|
1752
|
+
* @example
|
|
1753
|
+
* var v = b.network.dns.classifyDnskeyAlgorithm(5);
|
|
1754
|
+
* // → { algorithm: 5, name: "RSASHA1", deprecated: true,
|
|
1755
|
+
* // reason: "SHA-1 deprecated (RFC 9905 §3)", known: true }
|
|
1756
|
+
* if (v && v.deprecated) throw new Error("refuse DNSSEC algo " + v.name);
|
|
1757
|
+
*
|
|
1758
|
+
* b.network.dns.classifyDnskeyAlgorithm(13);
|
|
1759
|
+
* // → { algorithm: 13, name: "ECDSAP256SHA256", deprecated: false, ... }
|
|
1760
|
+
*/
|
|
1761
|
+
|
|
1762
|
+
// Canonical DNSKEY algorithm vocabulary (IANA DNS Security Algorithm
|
|
1763
|
+
// Numbers registry — https://www.iana.org/assignments/dns-sec-alg-numbers).
|
|
1764
|
+
// Operators looking up the human-readable label or computing whether
|
|
1765
|
+
// the framework's own DNSSEC paths use a deprecated algorithm walk
|
|
1766
|
+
// this table. Every IANA-assigned number gets an entry (including
|
|
1767
|
+
// Reserved / Private-use values) so `classifyDnskeyAlgorithm()`
|
|
1768
|
+
// returns `known: true` for the full assigned space; the "Unassigned"
|
|
1769
|
+
// range (17-122, 124-251) is the only set that surfaces as
|
|
1770
|
+
// `known: false`. Marked-deprecated entries cite the controlling
|
|
1771
|
+
// RFC; Reserved / Private-use entries are flagged so operators
|
|
1772
|
+
// auditing DNSSEC chain-of-trust evidence know they cannot validate
|
|
1773
|
+
// the entry against a public algorithm registry.
|
|
1774
|
+
var DNSKEY_ALGORITHMS = Object.freeze({
|
|
1775
|
+
1: { name: "RSAMD5", deprecated: true, reason: "MD5 broken (RFC 6944 §2.1, RFC 6725)" },
|
|
1776
|
+
2: { name: "DH", deprecated: true, reason: "Diffie-Hellman key (RFC 2539) — never widely deployed; superseded by signature algorithms" },
|
|
1777
|
+
3: { name: "DSA", deprecated: true, reason: "DSA deprecated (RFC 8624 §3.1)" },
|
|
1778
|
+
4: { name: "Reserved", deprecated: true, reason: "Reserved (RFC 4034 §A.1) — not for production use" },
|
|
1779
|
+
5: { name: "RSASHA1", deprecated: true, reason: "SHA-1 deprecated (RFC 9905 §3)" },
|
|
1780
|
+
6: { name: "DSA-NSEC3-SHA1", deprecated: true, reason: "SHA-1 deprecated (RFC 9905 §3); DSA deprecated (RFC 8624 §3.1)" },
|
|
1781
|
+
7: { name: "RSASHA1-NSEC3-SHA1", deprecated: true, reason: "SHA-1 deprecated (RFC 9905 §3)" },
|
|
1782
|
+
8: { name: "RSASHA256", deprecated: false, reason: "current — RFC 5702" }, // allow:raw-byte-literal — IANA DNSKEY algorithm number
|
|
1783
|
+
9: { name: "Reserved", deprecated: true, reason: "Reserved (RFC 5155) — not for production use" },
|
|
1784
|
+
10: { name: "RSASHA512", deprecated: false, reason: "current — RFC 5702" },
|
|
1785
|
+
11: { name: "Reserved", deprecated: true, reason: "Reserved (RFC 5155) — not for production use" },
|
|
1786
|
+
12: { name: "ECC-GOST", deprecated: true, reason: "deprecated (RFC 8624 §3.1)" },
|
|
1787
|
+
13: { name: "ECDSAP256SHA256", deprecated: false, reason: "current — RFC 6605" },
|
|
1788
|
+
14: { name: "ECDSAP384SHA384", deprecated: false, reason: "current — RFC 6605" },
|
|
1789
|
+
15: { name: "ED25519", deprecated: false, reason: "current — RFC 8080" },
|
|
1790
|
+
16: { name: "ED448", deprecated: false, reason: "current — RFC 8080" }, // allow:raw-byte-literal — IANA DNSKEY algorithm number
|
|
1791
|
+
// 17-122: Unassigned per IANA. Operators that see one of these
|
|
1792
|
+
// get known: false from classifyDnskeyAlgorithm() — the entry
|
|
1793
|
+
// is not a typo against the framework table, it's a value the
|
|
1794
|
+
// registry hasn't allocated yet.
|
|
1795
|
+
// 123-251: Reserved per IANA.
|
|
1796
|
+
252: { name: "INDIRECT", deprecated: true, reason: "Reserved indirect-keys placeholder (RFC 4034 §A.1) — not usable for signing/verification" }, // allow:raw-byte-literal — IANA DNSKEY algorithm number
|
|
1797
|
+
253: { name: "PRIVATEDNS", deprecated: false, reason: "Private algorithm identified by domain name (RFC 4034 §A.1.1) — operators using this assume the private algorithm itself is acceptable" },
|
|
1798
|
+
254: { name: "PRIVATEOID", deprecated: false, reason: "Private algorithm identified by OID (RFC 4034 §A.1.2) — operators using this assume the private algorithm itself is acceptable" },
|
|
1799
|
+
255: { name: "Reserved", deprecated: true, reason: "Reserved (RFC 4034 §A.1) — not for production use" },
|
|
1800
|
+
});
|
|
1801
|
+
|
|
1802
|
+
/**
|
|
1803
|
+
* @primitive b.network.dns.classifyDsDigestType
|
|
1804
|
+
* @signature b.network.dns.classifyDsDigestType(digestType)
|
|
1805
|
+
* @since 0.8.91
|
|
1806
|
+
* @status stable
|
|
1807
|
+
* @related b.network.dns.classifyDnskeyAlgorithm, b.network.dns.isNullMx
|
|
1808
|
+
*
|
|
1809
|
+
* Classify a DS-record digest type against the IANA DNSSEC Delegation
|
|
1810
|
+
* Signer (DS) Resource Record (RR) Type Digest Algorithms registry,
|
|
1811
|
+
* flagging SHA-1 (digest type 1) as deprecated per RFC 9905 §4.
|
|
1812
|
+
*
|
|
1813
|
+
* Returns `{ digestType, name, deprecated, reason, known }` for any
|
|
1814
|
+
* IANA-assigned number; `null` for non-integer input.
|
|
1815
|
+
*
|
|
1816
|
+
* @example
|
|
1817
|
+
* var v = b.network.dns.classifyDsDigestType(1);
|
|
1818
|
+
* // → { digestType: 1, name: "SHA-1", deprecated: true,
|
|
1819
|
+
* // reason: "SHA-1 deprecated (RFC 9905 §4)", known: true }
|
|
1820
|
+
*
|
|
1821
|
+
* b.network.dns.classifyDsDigestType(2);
|
|
1822
|
+
* // → { digestType: 2, name: "SHA-256", deprecated: false, ... }
|
|
1823
|
+
*/
|
|
1824
|
+
|
|
1825
|
+
// DS digest-type vocabulary (RFC 4034 §5.1 + RFC 6605 §6 + RFC 8624
|
|
1826
|
+
// §3.2 + RFC 9558). Digest type 1 = SHA-1 is deprecated per RFC 9905
|
|
1827
|
+
// §4. Digest types 5 (GOST R 34.11-2012) and 6 (SM3) added by RFC
|
|
1828
|
+
// 9558. Reserved value 0 surfaced for completeness.
|
|
1829
|
+
var DS_DIGEST_TYPES = Object.freeze({
|
|
1830
|
+
0: { name: "Reserved", deprecated: true, reason: "Reserved (RFC 3658) — not for production use" },
|
|
1831
|
+
1: { name: "SHA-1", deprecated: true, reason: "SHA-1 deprecated (RFC 9905 §4)" },
|
|
1832
|
+
2: { name: "SHA-256", deprecated: false, reason: "current — RFC 4509" },
|
|
1833
|
+
3: { name: "GOST R 34.11-94", deprecated: true, reason: "deprecated (RFC 8624 §3.2; superseded by GOST 2012 in RFC 9558)" },
|
|
1834
|
+
4: { name: "SHA-384", deprecated: false, reason: "current — RFC 6605 §6" },
|
|
1835
|
+
5: { name: "GOST R 34.11-2012", deprecated: false, reason: "current — RFC 9558 §3" },
|
|
1836
|
+
6: { name: "SM3", deprecated: false, reason: "current — RFC 9558 §3 (Chinese national standard)" },
|
|
1837
|
+
});
|
|
1838
|
+
|
|
1839
|
+
function classifyDnskeyAlgorithm(algorithm) {
|
|
1840
|
+
if (typeof algorithm !== "number" || !isFinite(algorithm) || Math.floor(algorithm) !== algorithm) {
|
|
1841
|
+
return null;
|
|
1842
|
+
}
|
|
1843
|
+
var row = DNSKEY_ALGORITHMS[algorithm];
|
|
1844
|
+
if (!row) {
|
|
1845
|
+
return {
|
|
1846
|
+
algorithm: algorithm,
|
|
1847
|
+
name: "unassigned",
|
|
1848
|
+
deprecated: false,
|
|
1849
|
+
reason: "no IANA assignment for algorithm " + algorithm,
|
|
1850
|
+
known: false,
|
|
1851
|
+
};
|
|
1852
|
+
}
|
|
1853
|
+
return {
|
|
1854
|
+
algorithm: algorithm,
|
|
1855
|
+
name: row.name,
|
|
1856
|
+
deprecated: row.deprecated,
|
|
1857
|
+
reason: row.reason,
|
|
1858
|
+
known: true,
|
|
1859
|
+
};
|
|
1860
|
+
}
|
|
1861
|
+
|
|
1862
|
+
function classifyDsDigestType(digestType) {
|
|
1863
|
+
if (typeof digestType !== "number" || !isFinite(digestType) || Math.floor(digestType) !== digestType) {
|
|
1864
|
+
return null;
|
|
1865
|
+
}
|
|
1866
|
+
var row = DS_DIGEST_TYPES[digestType];
|
|
1867
|
+
if (!row) {
|
|
1868
|
+
return {
|
|
1869
|
+
digestType: digestType,
|
|
1870
|
+
name: "unassigned",
|
|
1871
|
+
deprecated: false,
|
|
1872
|
+
reason: "no IANA assignment for digest type " + digestType,
|
|
1873
|
+
known: false,
|
|
1874
|
+
};
|
|
1875
|
+
}
|
|
1876
|
+
return {
|
|
1877
|
+
digestType: digestType,
|
|
1878
|
+
name: row.name,
|
|
1879
|
+
deprecated: row.deprecated,
|
|
1880
|
+
reason: row.reason,
|
|
1881
|
+
known: true,
|
|
1882
|
+
};
|
|
1883
|
+
}
|
|
1884
|
+
|
|
1711
1885
|
module.exports = {
|
|
1712
1886
|
setServers: setServers,
|
|
1713
1887
|
isNullMx: isNullMx,
|
|
1888
|
+
classifyDnskeyAlgorithm: classifyDnskeyAlgorithm,
|
|
1889
|
+
classifyDsDigestType: classifyDsDigestType,
|
|
1890
|
+
DNSKEY_ALGORITHMS: DNSKEY_ALGORITHMS,
|
|
1891
|
+
DS_DIGEST_TYPES: DS_DIGEST_TYPES,
|
|
1714
1892
|
getServers: getServers,
|
|
1715
1893
|
setResultOrder: setResultOrder,
|
|
1716
1894
|
setFamily: setFamily,
|
|
@@ -598,7 +598,7 @@ async function tlsRptFetchPolicy(domain, opts) {
|
|
|
598
598
|
if (/^v=TLSRPTv1\b/i.test(s)) { joined = s; break; }
|
|
599
599
|
}
|
|
600
600
|
if (joined.length === 0) return null;
|
|
601
|
-
var parts = joined.split(";");
|
|
601
|
+
var parts = joined.split(";"); // allow:bare-split-on-quoted-header — allow:raw-time-literal — TLS-RPT record grammar (RFC 8460 §3): `tlsrpt-record = "v=TLSRPTv1;" *(WSP) tlsrpt-rua` with token-only values; no quoted-string
|
|
602
602
|
var rua = [];
|
|
603
603
|
for (var p = 0; p < parts.length; p += 1) {
|
|
604
604
|
var t = parts[p].trim();
|
|
@@ -607,7 +607,7 @@ async function tlsRptFetchPolicy(domain, opts) {
|
|
|
607
607
|
var k = t.slice(0, eq).trim().toLowerCase();
|
|
608
608
|
var v = t.slice(eq + 1).trim();
|
|
609
609
|
if (k === "rua") {
|
|
610
|
-
var uris = v.split(",");
|
|
610
|
+
var uris = v.split(","); // allow:bare-split-on-quoted-header — allow:raw-time-literal — TLS-RPT rua grammar (RFC 8460 §3): rua = tlsrpt-uri *("," tlsrpt-uri); URIs percent-encode reserved chars, no quoted-string
|
|
611
611
|
for (var u = 0; u < uris.length; u += 1) {
|
|
612
612
|
var uri = uris[u].trim();
|
|
613
613
|
if (uri.length > 0) rua.push(uri);
|
package/lib/request-helpers.js
CHANGED
|
@@ -41,6 +41,8 @@
|
|
|
41
41
|
// values (RFC 9110), not byte sizes. Names are RFC 9110 reason phrases;
|
|
42
42
|
// every consumer reads HTTP_STATUS.<NAME> rather than the underlying
|
|
43
43
|
// integer, so the hex form is purely an internal storage detail.
|
|
44
|
+
var structuredFields = require("./structured-fields");
|
|
45
|
+
|
|
44
46
|
var HTTP_STATUS = Object.freeze({
|
|
45
47
|
OK: 0xC8,
|
|
46
48
|
PARTIAL_CONTENT: 0xCE,
|
|
@@ -354,6 +356,19 @@ function parseListHeader(value, opts) {
|
|
|
354
356
|
opts = opts || {};
|
|
355
357
|
var s = typeof value === "string" ? value : String(value);
|
|
356
358
|
if (s.length === 0) return [];
|
|
359
|
+
if (opts.strictToken) {
|
|
360
|
+
// RFC 9110 §5.6.2 token grammar excludes C0 / DEL. Scan the RAW
|
|
361
|
+
// value BEFORE the comma split + trim so a leading/trailing
|
|
362
|
+
// `\r\n\t` byte can't slip through (the trim() below would strip
|
|
363
|
+
// it before RFC_9110_TOKEN_RE saw it, matching the v0.8.90
|
|
364
|
+
// `parseTlsRequiredHeader` bug class).
|
|
365
|
+
structuredFields.refuseControlBytes(s, {
|
|
366
|
+
ErrorClass: TypeError,
|
|
367
|
+
code: "parseListHeader/control-character",
|
|
368
|
+
label: "parseListHeader",
|
|
369
|
+
useNativeError: true,
|
|
370
|
+
});
|
|
371
|
+
}
|
|
357
372
|
var parts = s.split(",");
|
|
358
373
|
var out = [];
|
|
359
374
|
for (var i = 0; i < parts.length; i++) {
|
package/lib/security-assert.js
CHANGED
|
@@ -202,7 +202,29 @@ async function assertProduction(opts) {
|
|
|
202
202
|
var want = opts.minTlsVersion;
|
|
203
203
|
// Compare TLSv1.3 > TLSv1.2 > TLSv1.1 > TLSv1.0 by string.
|
|
204
204
|
var order = ["TLSv1", "TLSv1.1", "TLSv1.2", "TLSv1.3"];
|
|
205
|
-
|
|
205
|
+
// Validate BOTH the operator-supplied required version AND the
|
|
206
|
+
// currently-active version against the known-vocabulary BEFORE
|
|
207
|
+
// the rank compare. A typo'd `want` (e.g. "TLS1.3" or "TLSv1.4")
|
|
208
|
+
// maps to indexOf === -1; without this check, the comparison
|
|
209
|
+
// `3 < -1 === false` silently passes even though the operator
|
|
210
|
+
// asked for a version the framework doesn't recognize.
|
|
211
|
+
// Throw at config time — this is a production-posture entry
|
|
212
|
+
// point and operator typos must be loud at boot, not deferred
|
|
213
|
+
// to per-request audit.
|
|
214
|
+
if (order.indexOf(want) === -1) {
|
|
215
|
+
throw new TypeError(
|
|
216
|
+
"assertProductionPosture: opts.minTlsVersion '" + want +
|
|
217
|
+
"' is not one of " + order.join(" / "));
|
|
218
|
+
}
|
|
219
|
+
if (order.indexOf(got) === -1) {
|
|
220
|
+
// Node's DEFAULT_MIN_VERSION shouldn't ever drift outside the
|
|
221
|
+
// canonical 4-value vocabulary, but if it does (future Node
|
|
222
|
+
// version, monkey-patched runtime), surface the failure
|
|
223
|
+
// rather than silently treating it as "below required".
|
|
224
|
+
failures.push({ ok: false, code: "security/tls-min-version",
|
|
225
|
+
message: "Node TLS DEFAULT_MIN_VERSION is an unrecognized value '" + got +
|
|
226
|
+
"' (expected one of " + order.join(" / ") + "); required '" + want + "'" });
|
|
227
|
+
} else if (order.indexOf(got) < order.indexOf(want)) {
|
|
206
228
|
failures.push({ ok: false, code: "security/tls-min-version",
|
|
207
229
|
message: "Node TLS DEFAULT_MIN_VERSION is '" + got + "', required '" + want + "'" });
|
|
208
230
|
}
|