@blamejs/core 0.12.68 → 0.12.70
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 +4 -0
- package/README.md +1 -1
- package/lib/middleware/bot-guard.js +43 -6
- package/lib/network-tsig.js +404 -0
- package/lib/network.js +1 -0
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,10 @@ upgrading across more than a few patches at a time.
|
|
|
8
8
|
|
|
9
9
|
## v0.12.x
|
|
10
10
|
|
|
11
|
+
- v0.12.70 (2026-05-26) — **`b.network.dns.tsig` — RFC 8945 DNS transaction signatures.** Sign and verify DNS messages with RFC 8945 TSIG — the shared-key HMAC that authenticates a DNS transaction (zone transfers, dynamic updates, query/response pairs) and proves it was not altered in flight. b.network.dns.tsig.sign(message, opts) appends a TSIG resource record and returns the signed wire; b.network.dns.tsig.verify(message, opts) locates the TSIG record, recomputes the HMAC over the RFC 8945 §4.3.3 digest, compares it in constant time, and checks the time window (valid only within `fudge` seconds of `timeSigned`). HMAC-SHA-256 is the default; SHA-384 / SHA-512 are available and the broken HMAC-MD5 / HMAC-SHA-1 algorithms are refused unless allowLegacy is set. Signing a response chains the request MAC into the digest. Verified byte-for-byte against dnspython 2.8.0 reference signatures. TSIG completes the DNS-trust set alongside the existing DNSSEC (zone-data authentication) and DANE primitives — DNSSEC authenticates the data end-to-end, TSIG authenticates a single hop's transaction with a pre-shared key. **Added:** *`b.network.dns.tsig.sign` / `b.network.dns.tsig.verify`* — RFC 8945 TSIG transaction authentication. `sign(message, { keyName, secret, algorithm, fudge, time, requestMac })` appends a TSIG RR to a DNS wire message and returns `{ wire, mac }`; `verify(message, { keys, now, requestMac })` returns `{ valid, keyName, algorithm, timeSigned, error, macValid, timeValid, reason }`, with a constant-time MAC compare (via `b.crypto.timingSafeEqual`), a `fudge`-second time-window check, truncated-MAC handling per §5.2.2.1, and request-MAC chaining for responses (§5.4.1). HMAC-SHA-256 default; HMAC-SHA-384 / SHA-512 supported; HMAC-MD5 / HMAC-SHA-1 refused unless `allowLegacy: true`. The transaction-level companion to `b.network.dns.dnssec` and `b.network.dns.dane`.
|
|
12
|
+
|
|
13
|
+
- v0.12.69 (2026-05-26) — **`b.middleware.botGuard` no longer blocks browsers that omit Sec-Fetch-Mode.** b.middleware.botGuard treated a missing Sec-Fetch-Mode header as a bot signal and returned 403 Forbidden, which refused legitimate browsers on any origin where the browser does not emit Fetch Metadata: every plain-HTTP non-localhost origin (Umbrel apps, LAN and *.local reverse-proxy deployments) and Safari before 16.4 even over HTTPS. Browsers only send Sec-Fetch-* in a secure context, so its absence is normal there — not a bot. Sec-Fetch-Mode is now advisory only: it never blocks, and it sets req.suspectedBot in mode:"tag" only on a secure-context HTML GET where a modern browser would have sent it. Drive-by bots are still blocked by the missing-Accept-Language and User-Agent heuristics. No configuration change is needed; if you had widened skipPaths or disabled bot-guard to work around this, you can revert that. **Fixed:** *`b.middleware.botGuard` no longer 403s browsers over plain HTTP or older Safari* — A missing `Sec-Fetch-Mode` was a blocking heuristic, but browsers omit Fetch Metadata outside a secure context (every plain-HTTP non-localhost origin — Umbrel, LAN, `*.local` proxies) and Safari < 16.4 omits it even over HTTPS. Those legitimate browsers were refused with `403 Forbidden`. `Sec-Fetch-Mode` is now advisory: it never blocks, and only sets `req.suspectedBot` in `mode: "tag"` on a secure-context HTML GET. The `Accept-Language` and User-Agent heuristics (which catch the same bots) are unchanged. **Detectors:** *reserved-hostname trailing-dot detector recognizes regex strips* — The codebase-patterns gate that requires stripping the RFC 1034 trailing root-zone dot before a reserved-hostname comparison now also recognizes end-anchored regex strips (`.replace(/\.$/, …)`), not only the `charAt` / `while`-loop forms.
|
|
14
|
+
|
|
11
15
|
- v0.12.68 (2026-05-26) — **`b.jwk` — RFC 7638 JWK thumbprint.** Compute the RFC 7638 thumbprint of a JSON Web Key — the canonical base64url(SHA-256(canonical-JSON)) identifier used to name a key (DPoP jkt bindings, ACME account-key thumbprints, DBSC session pins, kid derivation). b.jwk.thumbprint(jwk) returns the digest; b.jwk.canonicalize(jwk) returns the exact JSON that is hashed — only the key-type's required members, member names in lexicographic order, no whitespace, so the same key always yields the same thumbprint regardless of how its JWK was serialized. The standard key types are supported (EC, RSA, oct, OKP per RFC 8037) plus AKP, the IANA key type Node uses for ML-DSA / SLH-DSA post-quantum public keys; SHA-256 is the default, with hash: "sha384" | "sha512" for RFC 9278 thumbprint-with-hash. Verified against the RFC 7638 §3.1 worked example. b.auth.dpop, b.acme, and b.dbsc now compute their thumbprints through this primitive. **Added:** *`b.jwk.thumbprint` / `b.jwk.canonicalize`* — RFC 7638 JWK thumbprint. `thumbprint(jwk, opts)` returns `base64url(hash(canonical-JSON))` — only the key-type's required members feed the hash, so optional fields (`kid`, `use`, `alg`, …) never change the result. `canonicalize(jwk)` returns the canonical JSON string itself. Supports EC / RSA / oct / OKP and the AKP post-quantum key type; SHA-256 default, `hash` selects SHA-384 / SHA-512. Throws `JwkError` on an invalid key or unknown hash. **Changed:** *DPoP, ACME, and DBSC compose `b.jwk`* — `b.auth.dpop` (the `jkt` proof-key thumbprint), `b.acme` (the RFC 8555 account-key authorization), and `b.dbsc` (the session-pin thumbprint) now compute RFC 7638 thumbprints through `b.jwk` instead of carrying their own implementations. Behavior is unchanged — DPoP still refuses symmetric key types, and each surface keeps its own error codes.
|
|
12
16
|
|
|
13
17
|
- v0.12.66 (2026-05-26) — **`b.uriTemplate` — RFC 6570 URI Template expansion.** Expand RFC 6570 URI Templates — the {var} syntax that OpenAPI links, HAL _links, and hypermedia API clients use to turn a template plus a set of variables into a concrete URI. The full Level 4 grammar is supported: every operator ({+var} reserved, {#var} fragment, {.var} label, {/var} path, {;var} path-style parameters, {?var} query, {&var} query continuation), the {var:3} prefix modifier, and the {var*} explode modifier for lists and associative arrays. b.uriTemplate.expand(template, vars) returns the expanded string; b.uriTemplate.compile(template) parses once for templates applied to many variable sets. A malformed template (unclosed expression, reserved operator, non-numeric prefix, unmatched brace) throws UriTemplateError. Verified against the official uritemplate-test conformance suite (all 135 spec, extended, and negative cases). **Added:** *`b.uriTemplate.expand` / `b.uriTemplate.compile`* — RFC 6570 URI Template expansion, full Level 4. `expand(template, vars)` substitutes variables into a template and returns the URI; `compile(template)` returns a reusable `{ expand }` for repeated use. Variable values may be strings, numbers, booleans, arrays (lists), or plain objects (associative arrays); undefined, null, and empty list/map variables are omitted. All eight operators, the `:N` prefix modifier, and the `*` explode modifier follow §3.2, including reserved-set encoding for `{+var}` / `{#var}`. Composes naturally with `b.hal`, `b.linkHeader`, and `b.openapi` link objects. A malformed template throws `UriTemplateError`.
|
package/README.md
CHANGED
|
@@ -131,7 +131,7 @@ The framework bundles the surface a typical Node app reaches for. Every primitiv
|
|
|
131
131
|
- In-process CIDR fence (`b.middleware.networkAllowlist`)
|
|
132
132
|
- `Cache-Control: no-store` on every 401 from `requireAuth` / `requireAal` / `requireStepUp` per RFC 9111 §5.2.2.5
|
|
133
133
|
- **Outbound HTTP client** — HTTP/1.1 + HTTP/2 with SSRF gate (cloud-metadata IPs hard-denied; private / loopback / link-local overridable per call); scheme + userinfo + per-host destination allowlist; redirects, multipart, interceptors, progress, encrypted cookie jar (`b.httpClient`, `b.ssrfGuard`, `b.safeUrl`)
|
|
134
|
-
- **Network configurability (`b.network`)** — env-driven NTP / NTS (RFC 8915), IPv4/IPv6 NTP, DNS with IPv6 / DoH / DoT (private-CA pinning) / cache / lookup timeout; local DNSSEC signature verification (RFC 4035 — `b.network.dns.dnssec.verifyRrset` over a canonicalised RRset against RSA / ECDSA P-256·P-384 / Ed25519 DNSKEYs, plus DS-digest + key-tag, plus `verifyDenial` for NSEC / NSEC3 (RFC 5155) NXDOMAIN / NODATA proofs with iteration caps + Opt-Out handling, plus `verifyChain` to validate a full root→TLD→zone delegation chain against the pinned IANA root anchors) so a resolver client can verify both positive and negative answers instead of trusting the upstream AD bit; DANE / TLSA certificate matching (RFC 6698/7671 — `b.network.dns.dane.matchCertificate`) to pin a service's key through DNSSEC instead of a public CA; outbound HTTP proxy (`HTTP_PROXY` / `HTTPS_PROXY` / `NO_PROXY`); runtime DPI trust-store CA additions; application-level heartbeats; TCP socket defaults
|
|
134
|
+
- **Network configurability (`b.network`)** — env-driven NTP / NTS (RFC 8915), IPv4/IPv6 NTP, DNS with IPv6 / DoH / DoT (private-CA pinning) / cache / lookup timeout; local DNSSEC signature verification (RFC 4035 — `b.network.dns.dnssec.verifyRrset` over a canonicalised RRset against RSA / ECDSA P-256·P-384 / Ed25519 DNSKEYs, plus DS-digest + key-tag, plus `verifyDenial` for NSEC / NSEC3 (RFC 5155) NXDOMAIN / NODATA proofs with iteration caps + Opt-Out handling, plus `verifyChain` to validate a full root→TLD→zone delegation chain against the pinned IANA root anchors) so a resolver client can verify both positive and negative answers instead of trusting the upstream AD bit; DANE / TLSA certificate matching (RFC 6698/7671 — `b.network.dns.dane.matchCertificate`) to pin a service's key through DNSSEC instead of a public CA; TSIG transaction signatures (RFC 8945 — `b.network.dns.tsig.sign` / `verify`) for shared-key HMAC authentication of zone transfers, dynamic updates, and query/response pairs, with constant-time MAC compare + fudge-window check (verified against dnspython); outbound HTTP proxy (`HTTP_PROXY` / `HTTPS_PROXY` / `NO_PROXY`); runtime DPI trust-store CA additions; application-level heartbeats; TCP socket defaults
|
|
135
135
|
- **Error pages** — operator-rendered, no app-frame leakage (`b.errorPage`)
|
|
136
136
|
### Defensive parsers
|
|
137
137
|
|
|
@@ -6,8 +6,12 @@
|
|
|
6
6
|
*
|
|
7
7
|
* Heuristics (all combined):
|
|
8
8
|
* - Missing Accept-Language header (real browsers always send one)
|
|
9
|
-
* - Missing Sec-Fetch-Mode header
|
|
10
|
-
*
|
|
9
|
+
* - Missing Sec-Fetch-Mode header — ADVISORY ONLY (never blocks). Tagged
|
|
10
|
+
* in mode:"tag" on secure-context HTML GETs where a modern browser
|
|
11
|
+
* would have sent it. It cannot block because the header is absent for
|
|
12
|
+
* entire browser families (Safari < 16.4) and for every plain-HTTP
|
|
13
|
+
* non-localhost origin (Umbrel, LAN / *.local proxies) — a 403 on it
|
|
14
|
+
* alone would refuse real users.
|
|
11
15
|
* - User-Agent matches known automation libraries (curl, wget, python-
|
|
12
16
|
* requests, axios, Go-http-client) — operators can add or remove
|
|
13
17
|
* entries via config
|
|
@@ -86,9 +90,13 @@ function _xffIpFor(trustProxy) {
|
|
|
86
90
|
* Cheap fingerprint-based detection of obviously-non-browser requests.
|
|
87
91
|
* Constructed via `b.middleware.botGuard(opts)`; the resulting
|
|
88
92
|
* middleware has the `(req, res, next)` shape shown above.
|
|
89
|
-
*
|
|
90
|
-
*
|
|
91
|
-
*
|
|
93
|
+
* Two blocking heuristics — missing `Accept-Language` and a User-Agent
|
|
94
|
+
* regex match against a default list (curl / wget / python-requests /
|
|
95
|
+
* axios / etc.) — plus one advisory signal: a missing `Sec-Fetch-Mode`
|
|
96
|
+
* on a secure-context HTML GET sets `req.suspectedBot` in `mode: "tag"`
|
|
97
|
+
* but NEVER blocks (the header is absent for Safari < 16.4 and every
|
|
98
|
+
* plain-HTTP non-localhost origin, so blocking on it refuses real
|
|
99
|
+
* users). Not
|
|
92
100
|
* a substitute for proper authentication — catches drive-by scrapers
|
|
93
101
|
* and low-effort bots. In `mode: "block"` (default) the request is
|
|
94
102
|
* refused; in `mode: "tag"` `req.suspectedBot = true` is set and the
|
|
@@ -152,6 +160,28 @@ function create(opts) {
|
|
|
152
160
|
return /^\/api\//.test(path);
|
|
153
161
|
}
|
|
154
162
|
|
|
163
|
+
// Browsers only emit Fetch Metadata (Sec-Fetch-*) in a *secure context*
|
|
164
|
+
// (W3C Secure Contexts): an HTTPS origin, or a localhost-family origin
|
|
165
|
+
// even over plain HTTP. On a plain-HTTP non-localhost origin — an Umbrel
|
|
166
|
+
// app, a LAN / *.local reverse-proxy deployment — the browser omits
|
|
167
|
+
// Sec-Fetch-* entirely, so a missing Sec-Fetch-Mode is NORMAL there and
|
|
168
|
+
// must not be read as a bot signal. The effective scheme honours
|
|
169
|
+
// X-Forwarded-Proto only under trustProxy (otherwise it is forgeable).
|
|
170
|
+
function _isSecureContext(req) {
|
|
171
|
+
if (requestHelpers.requestProtocol(req, { trustProxy: trustProxy }) === "https") return true;
|
|
172
|
+
var host = (req.headers && req.headers.host) || "";
|
|
173
|
+
host = String(host).toLowerCase().replace(/:\d+$/, ""); // strip :port
|
|
174
|
+
if (host.charAt(0) === "[") { // [::1] IPv6 literal
|
|
175
|
+
var end = host.indexOf("]");
|
|
176
|
+
host = end === -1 ? host.slice(1) : host.slice(1, end);
|
|
177
|
+
}
|
|
178
|
+
host = host.replace(/\.$/, ""); // strip trailing root-zone dot (RFC 1034 §3.1) so "localhost." matches
|
|
179
|
+
if (host === "localhost" || /\.localhost$/.test(host)) return true;
|
|
180
|
+
if (host === "::1") return true;
|
|
181
|
+
if (/^127\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(host)) return true; // allow:regex-no-length-cap — bounded dotted-quad loopback
|
|
182
|
+
return false;
|
|
183
|
+
}
|
|
184
|
+
|
|
155
185
|
function _checkHeuristics(req) {
|
|
156
186
|
var headers = req.headers || {};
|
|
157
187
|
var ua = headers["user-agent"] || "";
|
|
@@ -167,7 +197,14 @@ function create(opts) {
|
|
|
167
197
|
return null;
|
|
168
198
|
}
|
|
169
199
|
if (!headers["accept-language"]) return "missing-accept-language";
|
|
170
|
-
|
|
200
|
+
// Missing Sec-Fetch-Mode NEVER blocks: the header is absent for entire
|
|
201
|
+
// browser families (Safari < 16.4 omits Fetch Metadata even over HTTPS)
|
|
202
|
+
// and for every plain-HTTP non-localhost origin (Umbrel, LAN / *.local
|
|
203
|
+
// reverse proxies), so a 403 on it alone refuses real users. It survives
|
|
204
|
+
// only as an advisory TAG in mode:"tag", and even then only in a secure
|
|
205
|
+
// context where a modern browser would have sent it. Drive-by bots are
|
|
206
|
+
// still blocked by missing Accept-Language + the User-Agent deny-list.
|
|
207
|
+
if (mode === "tag" && req.method === "GET" && _isSecureContext(req) && !headers["sec-fetch-mode"]) return "missing-sec-fetch-mode";
|
|
171
208
|
return null;
|
|
172
209
|
}
|
|
173
210
|
|
|
@@ -0,0 +1,404 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* @module b.network.dns.tsig
|
|
4
|
+
* @nav Network
|
|
5
|
+
* @title DNS TSIG
|
|
6
|
+
*
|
|
7
|
+
* @intro
|
|
8
|
+
* Sign and verify DNS messages with <a
|
|
9
|
+
* href="https://www.rfc-editor.org/rfc/rfc8945">RFC 8945</a> TSIG
|
|
10
|
+
* (Transaction SIGnature) — the shared-key HMAC that authenticates the
|
|
11
|
+
* transaction between a resolver and a server (zone transfers, dynamic
|
|
12
|
+
* updates, and any query/response pair) and proves it was not tampered
|
|
13
|
+
* with in flight. TSIG complements the existing DNSSEC and DANE
|
|
14
|
+
* primitives: DNSSEC authenticates zone <em>data</em> end-to-end, while
|
|
15
|
+
* TSIG authenticates a single hop's <em>transaction</em> with a
|
|
16
|
+
* pre-shared key.
|
|
17
|
+
*
|
|
18
|
+
* <code>sign(message, opts)</code> appends a TSIG resource record to a
|
|
19
|
+
* DNS message and returns the signed wire bytes;
|
|
20
|
+
* <code>verify(message, opts)</code> locates the TSIG record, recomputes
|
|
21
|
+
* the HMAC over the RFC 8945 §4.3.3 digest, compares it in constant time,
|
|
22
|
+
* and checks the time window (the signature is only valid within
|
|
23
|
+
* <code>fudge</code> seconds of <code>timeSigned</code>). The default MAC
|
|
24
|
+
* algorithm is HMAC-SHA-256; SHA-384 / SHA-512 are available, and the
|
|
25
|
+
* broken HMAC-MD5 / HMAC-SHA-1 algorithms are refused unless
|
|
26
|
+
* <code>allowLegacy</code> is set. Signing a response chains the
|
|
27
|
+
* request's MAC into the digest (<code>requestMac</code>) per §5.4.1.
|
|
28
|
+
*
|
|
29
|
+
* @card
|
|
30
|
+
* RFC 8945 DNS TSIG — shared-key HMAC transaction authentication for DNS
|
|
31
|
+
* messages (sign / verify, constant-time MAC compare, time-window check,
|
|
32
|
+
* HMAC-SHA-256 default). The transaction-level companion to DNSSEC + DANE.
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
var nodeCrypto = require("node:crypto");
|
|
36
|
+
var validateOpts = require("./validate-opts");
|
|
37
|
+
var { timingSafeEqual } = require("./crypto");
|
|
38
|
+
var { defineClass } = require("./framework-error");
|
|
39
|
+
|
|
40
|
+
var TsigError = defineClass("TsigError", { alwaysPermanent: true });
|
|
41
|
+
|
|
42
|
+
var TYPE_TSIG = 250; // allow:raw-byte-literal — IANA RR type TSIG
|
|
43
|
+
var CLASS_ANY = 255; // allow:raw-byte-literal — TSIG RRs use CLASS ANY
|
|
44
|
+
var DEFAULT_FUDGE = 300; // allow:raw-time-literal — RFC 8945 recommended fudge window (seconds)
|
|
45
|
+
|
|
46
|
+
// Algorithm name → Node hash. The strong HMAC-SHA-2 family is the safe set;
|
|
47
|
+
// HMAC-MD5 and HMAC-SHA-1 are refused unless allowLegacy (kept only for
|
|
48
|
+
// interop with legacy nameservers).
|
|
49
|
+
var ALGORITHMS = {
|
|
50
|
+
"hmac-sha256": "sha256",
|
|
51
|
+
"hmac-sha384": "sha384",
|
|
52
|
+
"hmac-sha512": "sha512",
|
|
53
|
+
"hmac-sha224": "sha224",
|
|
54
|
+
};
|
|
55
|
+
var LEGACY_ALGORITHMS = {
|
|
56
|
+
"hmac-sha1": "sha1",
|
|
57
|
+
"hmac-md5": "md5",
|
|
58
|
+
};
|
|
59
|
+
// RFC 8945 §5.2.2.1 — TSIG error RCODEs.
|
|
60
|
+
var ERROR = { NOERROR: 0, BADSIG: 16, BADKEY: 17, BADTIME: 18, BADTRUNC: 22 }; // allow:raw-byte-literal — RFC 8945 extended-RCODE values
|
|
61
|
+
|
|
62
|
+
function _normAlg(name, allowLegacy) {
|
|
63
|
+
var key = String(name || "hmac-sha256").toLowerCase().replace(/\.$/, "");
|
|
64
|
+
if (ALGORITHMS[key]) return { name: key, hash: ALGORITHMS[key] };
|
|
65
|
+
if (LEGACY_ALGORITHMS[key]) {
|
|
66
|
+
if (!allowLegacy) throw new TsigError("tsig/legacy-algorithm", "tsig: algorithm '" + key + "' is broken; pass allowLegacy:true to permit it for legacy interop");
|
|
67
|
+
return { name: key, hash: LEGACY_ALGORITHMS[key] };
|
|
68
|
+
}
|
|
69
|
+
throw new TsigError("tsig/bad-algorithm", "tsig: unknown algorithm '" + key + "'");
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function _secretBuf(secret) {
|
|
73
|
+
if (Buffer.isBuffer(secret)) return secret;
|
|
74
|
+
if (typeof secret === "string") {
|
|
75
|
+
// TSIG keys are conventionally transported as base64.
|
|
76
|
+
var b = Buffer.from(secret, "base64");
|
|
77
|
+
if (b.length === 0 && secret.length > 0) throw new TsigError("tsig/bad-secret", "tsig: secret must be base64 or a Buffer");
|
|
78
|
+
return b;
|
|
79
|
+
}
|
|
80
|
+
throw new TsigError("tsig/bad-secret", "tsig: secret must be a base64 string or Buffer");
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Encode a domain name to uncompressed wire form (labels), lower-casing is
|
|
84
|
+
// NOT applied — TSIG uses the names as presented (key names are
|
|
85
|
+
// conventionally lower-case already; algorithm names are canonical).
|
|
86
|
+
function _encodeName(name) {
|
|
87
|
+
var n = String(name).replace(/\.$/, "");
|
|
88
|
+
if (n === "") return Buffer.from([0]);
|
|
89
|
+
var parts = n.split(".");
|
|
90
|
+
var out = [];
|
|
91
|
+
for (var i = 0; i < parts.length; i++) {
|
|
92
|
+
var lab = Buffer.from(parts[i], "ascii");
|
|
93
|
+
if (lab.length === 0 || lab.length > 63) throw new TsigError("tsig/bad-name", "tsig: invalid label in name '" + name + "'"); // allow:raw-byte-literal — RFC 1035 max label length
|
|
94
|
+
out.push(Buffer.from([lab.length]), lab);
|
|
95
|
+
}
|
|
96
|
+
out.push(Buffer.from([0]));
|
|
97
|
+
return Buffer.concat(out);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Read a domain name starting at off, returning { name, end }. Handles a
|
|
101
|
+
// compression pointer as a terminal jump (TSIG only needs the END offset to
|
|
102
|
+
// keep walking; the pointed-at labels are resolved for the name string).
|
|
103
|
+
function _readName(buf, off) {
|
|
104
|
+
var labels = [];
|
|
105
|
+
var i = off;
|
|
106
|
+
var end = -1;
|
|
107
|
+
var jumps = 0;
|
|
108
|
+
for (;;) {
|
|
109
|
+
if (i >= buf.length) throw new TsigError("tsig/truncated", "tsig: truncated name in message");
|
|
110
|
+
var len = buf[i];
|
|
111
|
+
if (len === 0) { if (end === -1) end = i + 1; break; }
|
|
112
|
+
if ((len & 0xc0) === 0xc0) { // allow:raw-byte-literal — RFC 1035 §4.1.4 compression-pointer flag
|
|
113
|
+
if (i + 1 >= buf.length) throw new TsigError("tsig/truncated", "tsig: truncated compression pointer");
|
|
114
|
+
if (end === -1) end = i + 2;
|
|
115
|
+
var ptr = ((len & 0x3f) << 8) | buf[i + 1]; // allow:raw-byte-literal — 14-bit pointer offset
|
|
116
|
+
if (++jumps > 128) throw new TsigError("tsig/bad-name", "tsig: compression-pointer loop"); // allow:raw-byte-literal — pointer-chase cap
|
|
117
|
+
i = ptr;
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
if ((len & 0xc0) !== 0) throw new TsigError("tsig/bad-name", "tsig: reserved label-length bits set"); // allow:raw-byte-literal — RFC 1035 label top-bits
|
|
121
|
+
i++;
|
|
122
|
+
labels.push(buf.slice(i, i + len).toString("ascii"));
|
|
123
|
+
i += len;
|
|
124
|
+
}
|
|
125
|
+
return { name: labels.length ? labels.join(".") + "." : ".", end: end };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Skip a name, returning the offset after it (compression-pointer aware).
|
|
129
|
+
function _skipName(buf, off) {
|
|
130
|
+
var i = off;
|
|
131
|
+
for (;;) {
|
|
132
|
+
if (i >= buf.length) throw new TsigError("tsig/truncated", "tsig: truncated name");
|
|
133
|
+
var len = buf[i];
|
|
134
|
+
if (len === 0) return i + 1;
|
|
135
|
+
if ((len & 0xc0) === 0xc0) return i + 2; // allow:raw-byte-literal — compression pointer is terminal
|
|
136
|
+
if ((len & 0xc0) !== 0) throw new TsigError("tsig/bad-name", "tsig: reserved label-length bits set"); // allow:raw-byte-literal — RFC 1035 label top-bits
|
|
137
|
+
i += 1 + len;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Walk the message to the start of the LAST resource record, which a
|
|
142
|
+
// TSIG-bearing message requires to be the TSIG RR (RFC 8945 §5.1).
|
|
143
|
+
function _findTsigRr(buf) {
|
|
144
|
+
if (buf.length < 12) throw new TsigError("tsig/truncated", "tsig: message shorter than the 12-byte header"); // allow:raw-byte-literal — DNS header length
|
|
145
|
+
var qd = buf.readUInt16BE(4), an = buf.readUInt16BE(6), ns = buf.readUInt16BE(8), ar = buf.readUInt16BE(10);
|
|
146
|
+
if (ar < 1) throw new TsigError("tsig/no-tsig", "tsig: message has no additional records (no TSIG)");
|
|
147
|
+
var off = 12; // allow:raw-byte-literal — past the DNS header
|
|
148
|
+
var q;
|
|
149
|
+
for (q = 0; q < qd; q++) { off = _skipName(buf, off); off += 4; } // allow:raw-byte-literal — QTYPE + QCLASS
|
|
150
|
+
var total = an + ns + ar;
|
|
151
|
+
var rrStart = -1;
|
|
152
|
+
for (var r = 0; r < total; r++) {
|
|
153
|
+
rrStart = off;
|
|
154
|
+
off = _skipName(buf, off);
|
|
155
|
+
if (off + 10 > buf.length) throw new TsigError("tsig/truncated", "tsig: truncated RR header"); // allow:raw-byte-literal — type+class+ttl+rdlength
|
|
156
|
+
var rdlen = buf.readUInt16BE(off + 8); // allow:raw-byte-literal — rdlength offset within RR header
|
|
157
|
+
off += 10 + rdlen; // allow:raw-byte-literal — RR fixed header before RDATA
|
|
158
|
+
}
|
|
159
|
+
if (off !== buf.length) throw new TsigError("tsig/trailing-bytes", "tsig: trailing bytes after the final record");
|
|
160
|
+
return rrStart;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Build the TSIG-variables byte block (RFC 8945 §4.3.3).
|
|
164
|
+
function _tsigVariables(keyName, algName, timeSigned, fudge, error, otherData) {
|
|
165
|
+
var time = Buffer.alloc(6); // allow:raw-byte-literal — 48-bit time-signed field
|
|
166
|
+
time.writeUIntBE(timeSigned, 0, 6); // allow:raw-byte-literal — 48-bit big-endian
|
|
167
|
+
var head = Buffer.alloc(6); // allow:raw-byte-literal — CLASS(2) + TTL(4)
|
|
168
|
+
head.writeUInt16BE(CLASS_ANY, 0);
|
|
169
|
+
head.writeUInt32BE(0, 2); // TTL is always 0 (4 bytes)
|
|
170
|
+
var tail = Buffer.alloc(6); // allow:raw-byte-literal — fudge(2)+error(2)+otherlen(2)
|
|
171
|
+
tail.writeUInt16BE(fudge, 0);
|
|
172
|
+
tail.writeUInt16BE(error, 2);
|
|
173
|
+
tail.writeUInt16BE(otherData.length, 4);
|
|
174
|
+
// DNS names are case-insensitive and the TSIG digest uses their canonical
|
|
175
|
+
// (lower-cased) form (RFC 8945 §4.3.3 / RFC 4034 §6.2) — the on-the-wire
|
|
176
|
+
// RR may carry any case, but both signer and verifier digest the
|
|
177
|
+
// lower-cased name, so the MAC is stable across case differences.
|
|
178
|
+
return Buffer.concat([_encodeName(String(keyName).toLowerCase()), head, _encodeName(String(algName).toLowerCase()), time, tail, otherData]);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function _requestMacPrefix(requestMac) {
|
|
182
|
+
if (!requestMac) return Buffer.alloc(0);
|
|
183
|
+
var len = Buffer.alloc(2);
|
|
184
|
+
len.writeUInt16BE(requestMac.length, 0);
|
|
185
|
+
return Buffer.concat([len, requestMac]);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* @primitive b.network.dns.tsig.sign
|
|
190
|
+
* @signature b.network.dns.tsig.sign(message, opts)
|
|
191
|
+
* @since 0.12.70
|
|
192
|
+
* @status stable
|
|
193
|
+
* @related b.network.dns.tsig.verify
|
|
194
|
+
*
|
|
195
|
+
* Append a TSIG resource record to a DNS message (a Buffer of wire bytes)
|
|
196
|
+
* and return the signed wire Buffer. The MAC is the HMAC over the message
|
|
197
|
+
* plus the RFC 8945 §4.3.3 TSIG variables. Returns
|
|
198
|
+
* <code>{ wire, mac }</code> — <code>wire</code> is the message with the
|
|
199
|
+
* TSIG RR appended and ARCOUNT incremented, and <code>mac</code> is the raw
|
|
200
|
+
* HMAC (keep it to verify the matching response).
|
|
201
|
+
*
|
|
202
|
+
* @opts
|
|
203
|
+
* keyName: string, // REQUIRED — the shared-key name
|
|
204
|
+
* secret: string | Buffer, // REQUIRED — base64 string or raw bytes
|
|
205
|
+
* algorithm: string, // default: "hmac-sha256"
|
|
206
|
+
* fudge: number, // default: 300 (seconds)
|
|
207
|
+
* time: number, // default: now (Unix seconds)
|
|
208
|
+
* originalId: number, // default: the message's own ID
|
|
209
|
+
* requestMac: Buffer, // when signing a response (§5.4.1)
|
|
210
|
+
* error: number, // default: 0 (NOERROR)
|
|
211
|
+
* otherData: Buffer, // default: empty
|
|
212
|
+
* allowLegacy: boolean, // permit HMAC-MD5 / HMAC-SHA-1
|
|
213
|
+
*
|
|
214
|
+
* @example
|
|
215
|
+
* var signed = b.network.dns.tsig.sign(queryWire, {
|
|
216
|
+
* keyName: "update.key.", secret: "<base64-secret>",
|
|
217
|
+
* });
|
|
218
|
+
* socket.send(signed.wire);
|
|
219
|
+
*/
|
|
220
|
+
function sign(message, opts) {
|
|
221
|
+
opts = opts || {};
|
|
222
|
+
if (!Buffer.isBuffer(message) || message.length < 12) throw new TsigError("tsig/bad-message", "tsig.sign: message must be a DNS wire Buffer");
|
|
223
|
+
validateOpts.requireNonEmptyString(opts.keyName, "tsig.sign: keyName", TsigError, "tsig/bad-opt");
|
|
224
|
+
var alg = _normAlg(opts.algorithm, opts.allowLegacy === true);
|
|
225
|
+
var secret = _secretBuf(opts.secret);
|
|
226
|
+
var fudge = opts.fudge == null ? DEFAULT_FUDGE : opts.fudge;
|
|
227
|
+
if (typeof fudge !== "number" || !isFinite(fudge) || fudge < 0 || fudge > 0xffff) throw new TsigError("tsig/bad-opt", "tsig.sign: fudge must be 0..65535 seconds"); // allow:raw-byte-literal — 16-bit fudge field
|
|
228
|
+
var time = opts.time == null ? Math.floor(Date.now() / 1000) : opts.time; // allow:raw-time-literal — ms→s
|
|
229
|
+
if (typeof time !== "number" || !isFinite(time) || time < 0) throw new TsigError("tsig/bad-opt", "tsig.sign: time must be a non-negative Unix-seconds number");
|
|
230
|
+
var error = opts.error == null ? 0 : opts.error;
|
|
231
|
+
var otherData = Buffer.isBuffer(opts.otherData) ? opts.otherData : Buffer.alloc(0);
|
|
232
|
+
var originalId = opts.originalId == null ? message.readUInt16BE(0) : opts.originalId;
|
|
233
|
+
var algName = alg.name + ".";
|
|
234
|
+
|
|
235
|
+
var digest = Buffer.concat([
|
|
236
|
+
_requestMacPrefix(opts.requestMac),
|
|
237
|
+
message,
|
|
238
|
+
_tsigVariables(opts.keyName, algName, time, fudge, error, otherData),
|
|
239
|
+
]);
|
|
240
|
+
var mac = nodeCrypto.createHmac(alg.hash, secret).update(digest).digest();
|
|
241
|
+
|
|
242
|
+
// TSIG RDATA: algorithm name, time signed, fudge, MAC size + MAC,
|
|
243
|
+
// original ID, error, other len + other data.
|
|
244
|
+
var rtime = Buffer.alloc(6); rtime.writeUIntBE(time, 0, 6); // allow:raw-byte-literal — 48-bit time-signed
|
|
245
|
+
var fixed = Buffer.alloc(4); // allow:raw-byte-literal — fudge(2)+macsize(2)
|
|
246
|
+
fixed.writeUInt16BE(fudge, 0);
|
|
247
|
+
fixed.writeUInt16BE(mac.length, 2);
|
|
248
|
+
var trailer = Buffer.alloc(6); // allow:raw-byte-literal — origid(2)+error(2)+otherlen(2)
|
|
249
|
+
trailer.writeUInt16BE(originalId, 0);
|
|
250
|
+
trailer.writeUInt16BE(error, 2);
|
|
251
|
+
trailer.writeUInt16BE(otherData.length, 4);
|
|
252
|
+
var rdata = Buffer.concat([_encodeName(algName), rtime, fixed, mac, trailer, otherData]);
|
|
253
|
+
|
|
254
|
+
var rrHead = Buffer.alloc(10); // allow:raw-byte-literal — type+class+ttl+rdlength
|
|
255
|
+
rrHead.writeUInt16BE(TYPE_TSIG, 0);
|
|
256
|
+
rrHead.writeUInt16BE(CLASS_ANY, 2);
|
|
257
|
+
rrHead.writeUInt32BE(0, 4); // TTL 0
|
|
258
|
+
rrHead.writeUInt16BE(rdata.length, 8); // allow:raw-byte-literal — rdlength offset within the 10-byte RR header
|
|
259
|
+
var tsigRr = Buffer.concat([_encodeName(opts.keyName), rrHead, rdata]);
|
|
260
|
+
|
|
261
|
+
var out = Buffer.from(message); // copy so we can bump ARCOUNT
|
|
262
|
+
out.writeUInt16BE(out.readUInt16BE(10) + 1, 10); // allow:raw-byte-literal — ARCOUNT offset
|
|
263
|
+
return { wire: Buffer.concat([out, tsigRr]), mac: mac };
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function _parseTsigRr(buf, rrStart) {
|
|
267
|
+
var n = _readName(buf, rrStart);
|
|
268
|
+
var off = n.end;
|
|
269
|
+
var type = buf.readUInt16BE(off);
|
|
270
|
+
if (type !== TYPE_TSIG) throw new TsigError("tsig/not-tsig", "tsig: the final record is not a TSIG RR (type " + type + ")");
|
|
271
|
+
// The MAC digest hard-codes CLASS ANY / TTL 0, so the on-wire CLASS and
|
|
272
|
+
// TTL are outside the signed data — they MUST be validated explicitly or
|
|
273
|
+
// an attacker could flip them in transit and still verify (RFC 8945 §4.2:
|
|
274
|
+
// CLASS = ANY, TTL = 0).
|
|
275
|
+
var rrClass = buf.readUInt16BE(off + 2); // allow:raw-byte-literal — CLASS offset within RR header
|
|
276
|
+
var rrTtl = buf.readUInt32BE(off + 4); // allow:raw-byte-literal — TTL offset within RR header
|
|
277
|
+
if (rrClass !== CLASS_ANY) throw new TsigError("tsig/bad-rr", "tsig: TSIG RR CLASS must be ANY (255), got " + rrClass);
|
|
278
|
+
if (rrTtl !== 0) throw new TsigError("tsig/bad-rr", "tsig: TSIG RR TTL must be 0, got " + rrTtl);
|
|
279
|
+
off += 8; // allow:raw-byte-literal — type(2)+class(2)+ttl(4)
|
|
280
|
+
var rdlen = buf.readUInt16BE(off); off += 2;
|
|
281
|
+
var rdStart = off;
|
|
282
|
+
var alg = _readName(buf, off); off = alg.end;
|
|
283
|
+
var timeSigned = buf.readUIntBE(off, 6); off += 6; // allow:raw-byte-literal — 48-bit time-signed
|
|
284
|
+
var fudge = buf.readUInt16BE(off); off += 2;
|
|
285
|
+
var macSize = buf.readUInt16BE(off); off += 2;
|
|
286
|
+
var mac = buf.slice(off, off + macSize); off += macSize;
|
|
287
|
+
var originalId = buf.readUInt16BE(off); off += 2;
|
|
288
|
+
var error = buf.readUInt16BE(off); off += 2;
|
|
289
|
+
var otherLen = buf.readUInt16BE(off); off += 2;
|
|
290
|
+
var otherData = buf.slice(off, off + otherLen); off += otherLen;
|
|
291
|
+
if (off !== rdStart + rdlen) throw new TsigError("tsig/bad-rdata", "tsig: RDATA length mismatch");
|
|
292
|
+
return {
|
|
293
|
+
keyName: n.name, algName: alg.name.replace(/\.$/, ""), timeSigned: timeSigned, fudge: fudge,
|
|
294
|
+
mac: mac, originalId: originalId, error: error, otherData: otherData, rrStart: rrStart,
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* @primitive b.network.dns.tsig.verify
|
|
300
|
+
* @signature b.network.dns.tsig.verify(message, opts)
|
|
301
|
+
* @since 0.12.70
|
|
302
|
+
* @status stable
|
|
303
|
+
* @related b.network.dns.tsig.sign
|
|
304
|
+
*
|
|
305
|
+
* Verify the TSIG record on a DNS message: locate the trailing TSIG RR,
|
|
306
|
+
* recompute the HMAC over the RFC 8945 §4.3.3 digest, compare it in
|
|
307
|
+
* constant time, and check that <code>now</code> is within
|
|
308
|
+
* <code>fudge</code> seconds of <code>timeSigned</code>. Returns
|
|
309
|
+
* <code>{ valid, keyName, algorithm, timeSigned, fudge, error, macValid,
|
|
310
|
+
* timeValid, reason }</code>; <code>valid</code> is true only when the MAC
|
|
311
|
+
* matches, the time window holds, and the embedded error is NOERROR. Never
|
|
312
|
+
* throws for an authentication failure — only for a malformed message or
|
|
313
|
+
* unknown key shape.
|
|
314
|
+
*
|
|
315
|
+
* @opts
|
|
316
|
+
* keys: object, // { "<keyName>": { secret, algorithm } }
|
|
317
|
+
* keyName: string, // single-key form (with secret)
|
|
318
|
+
* secret: string | Buffer, // single-key form
|
|
319
|
+
* algorithm: string, // expected algorithm (single-key form)
|
|
320
|
+
* now: number, // default: now (Unix seconds)
|
|
321
|
+
* requestMac: Buffer, // when verifying a response (§5.4.1)
|
|
322
|
+
* allowLegacy: boolean,
|
|
323
|
+
*
|
|
324
|
+
* @example
|
|
325
|
+
* var r = b.network.dns.tsig.verify(received, {
|
|
326
|
+
* keys: { "update.key.": { secret: "<base64>" } },
|
|
327
|
+
* });
|
|
328
|
+
* if (!r.valid) refuse(r.reason);
|
|
329
|
+
*/
|
|
330
|
+
function verify(message, opts) {
|
|
331
|
+
opts = opts || {};
|
|
332
|
+
if (!Buffer.isBuffer(message) || message.length < 12) throw new TsigError("tsig/bad-message", "tsig.verify: message must be a DNS wire Buffer");
|
|
333
|
+
var rrStart = _findTsigRr(message);
|
|
334
|
+
var rr = _parseTsigRr(message, rrStart);
|
|
335
|
+
|
|
336
|
+
// Resolve the key for this RR's owner name. DNS names are
|
|
337
|
+
// case-insensitive, so match the lower-cased, dot-trimmed forms.
|
|
338
|
+
function _normName(s) { return String(s).toLowerCase().replace(/\.$/, ""); }
|
|
339
|
+
var rrKeyNorm = _normName(rr.keyName);
|
|
340
|
+
var keyEntry = null;
|
|
341
|
+
if (opts.keys && typeof opts.keys === "object") {
|
|
342
|
+
var ks = Object.keys(opts.keys);
|
|
343
|
+
for (var ki = 0; ki < ks.length; ki++) {
|
|
344
|
+
if (_normName(ks[ki]) === rrKeyNorm) { keyEntry = opts.keys[ks[ki]]; break; }
|
|
345
|
+
}
|
|
346
|
+
} else if (opts.keyName != null && _normName(opts.keyName) === rrKeyNorm) {
|
|
347
|
+
keyEntry = { secret: opts.secret, algorithm: opts.algorithm };
|
|
348
|
+
}
|
|
349
|
+
if (!keyEntry) {
|
|
350
|
+
return { valid: false, keyName: rr.keyName, algorithm: rr.algName, timeSigned: rr.timeSigned, fudge: rr.fudge, error: ERROR.BADKEY, macValid: false, timeValid: false, reason: "unknown key '" + rr.keyName + "'" };
|
|
351
|
+
}
|
|
352
|
+
var alg = _normAlg(keyEntry.algorithm || rr.algName, opts.allowLegacy === true);
|
|
353
|
+
// The RR's algorithm must match the key's expected algorithm
|
|
354
|
+
// (case-insensitive — _normAlg already lower-cases alg.name).
|
|
355
|
+
if (alg.name !== rr.algName.toLowerCase()) {
|
|
356
|
+
return { valid: false, keyName: rr.keyName, algorithm: rr.algName, timeSigned: rr.timeSigned, fudge: rr.fudge, error: ERROR.BADKEY, macValid: false, timeValid: false, reason: "algorithm mismatch (key expects " + alg.name + ", message used " + rr.algName + ")" };
|
|
357
|
+
}
|
|
358
|
+
var secret = _secretBuf(keyEntry.secret);
|
|
359
|
+
|
|
360
|
+
// Reconstruct the digested message: bytes before the TSIG RR, with
|
|
361
|
+
// ARCOUNT decremented and the ID restored to the original ID.
|
|
362
|
+
var digestMsg = Buffer.from(message.slice(0, rrStart));
|
|
363
|
+
digestMsg.writeUInt16BE(rr.originalId, 0);
|
|
364
|
+
digestMsg.writeUInt16BE(digestMsg.readUInt16BE(10) - 1, 10); // allow:raw-byte-literal — ARCOUNT offset
|
|
365
|
+
|
|
366
|
+
var digest = Buffer.concat([
|
|
367
|
+
_requestMacPrefix(opts.requestMac),
|
|
368
|
+
digestMsg,
|
|
369
|
+
_tsigVariables(rr.keyName, rr.algName + ".", rr.timeSigned, rr.fudge, rr.error, rr.otherData),
|
|
370
|
+
]);
|
|
371
|
+
var expected = nodeCrypto.createHmac(alg.hash, secret).update(digest).digest();
|
|
372
|
+
|
|
373
|
+
// Constant-time compare. A truncated MAC (macSize < full) is only valid
|
|
374
|
+
// down to the RFC 8945 §5.2.2.1 floor of max(10, fullLen/2) octets.
|
|
375
|
+
var macValid = false;
|
|
376
|
+
if (rr.mac.length === expected.length) {
|
|
377
|
+
macValid = timingSafeEqual(rr.mac, expected);
|
|
378
|
+
} else if (rr.mac.length >= Math.max(10, expected.length / 2) && rr.mac.length < expected.length) { // allow:raw-byte-literal — RFC 8945 §5.2.2.1 minimum truncated-MAC length
|
|
379
|
+
macValid = timingSafeEqual(rr.mac, expected.slice(0, rr.mac.length));
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
var now = opts.now == null ? Math.floor(Date.now() / 1000) : opts.now; // allow:raw-time-literal — ms→s
|
|
383
|
+
var timeValid = Math.abs(now - rr.timeSigned) <= rr.fudge;
|
|
384
|
+
|
|
385
|
+
var reason = null;
|
|
386
|
+
if (!macValid) reason = "MAC mismatch";
|
|
387
|
+
else if (!timeValid) reason = "time outside fudge window";
|
|
388
|
+
else if (rr.error !== ERROR.NOERROR) reason = "TSIG error code " + rr.error;
|
|
389
|
+
|
|
390
|
+
return {
|
|
391
|
+
valid: macValid && timeValid && rr.error === ERROR.NOERROR,
|
|
392
|
+
keyName: rr.keyName, algorithm: rr.algName, timeSigned: rr.timeSigned, fudge: rr.fudge,
|
|
393
|
+
error: macValid ? (timeValid ? rr.error : ERROR.BADTIME) : ERROR.BADSIG,
|
|
394
|
+
macValid: macValid, timeValid: timeValid, reason: reason,
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
module.exports = {
|
|
399
|
+
sign: sign,
|
|
400
|
+
verify: verify,
|
|
401
|
+
ALGORITHMS: ALGORITHMS,
|
|
402
|
+
ERROR: ERROR,
|
|
403
|
+
TsigError: TsigError,
|
|
404
|
+
};
|
package/lib/network.js
CHANGED
|
@@ -37,6 +37,7 @@ var networkDns = require("./network-dns");
|
|
|
37
37
|
networkDns.resolver = require("./network-dns-resolver");
|
|
38
38
|
networkDns.dnssec = require("./network-dnssec");
|
|
39
39
|
networkDns.dane = require("./network-dane");
|
|
40
|
+
networkDns.tsig = require("./network-tsig");
|
|
40
41
|
var networkProxy = require("./network-proxy");
|
|
41
42
|
var networkTls = require("./network-tls");
|
|
42
43
|
var heartbeat = require("./network-heartbeat");
|
package/package.json
CHANGED
package/sbom.cdx.json
CHANGED
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
"$schema": "http://cyclonedx.org/schema/bom-1.5.schema.json",
|
|
3
3
|
"bomFormat": "CycloneDX",
|
|
4
4
|
"specVersion": "1.5",
|
|
5
|
-
"serialNumber": "urn:uuid:
|
|
5
|
+
"serialNumber": "urn:uuid:70f206dc-ef72-4080-8b01-8febb27e0978",
|
|
6
6
|
"version": 1,
|
|
7
7
|
"metadata": {
|
|
8
|
-
"timestamp": "2026-05-
|
|
8
|
+
"timestamp": "2026-05-26T17:53:56.836Z",
|
|
9
9
|
"lifecycles": [
|
|
10
10
|
{
|
|
11
11
|
"phase": "build"
|
|
@@ -19,14 +19,14 @@
|
|
|
19
19
|
}
|
|
20
20
|
],
|
|
21
21
|
"component": {
|
|
22
|
-
"bom-ref": "@blamejs/core@0.12.
|
|
22
|
+
"bom-ref": "@blamejs/core@0.12.70",
|
|
23
23
|
"type": "application",
|
|
24
24
|
"name": "blamejs",
|
|
25
|
-
"version": "0.12.
|
|
25
|
+
"version": "0.12.70",
|
|
26
26
|
"scope": "required",
|
|
27
27
|
"author": "blamejs contributors",
|
|
28
28
|
"description": "The Node framework that owns its stack.",
|
|
29
|
-
"purl": "pkg:npm/%40blamejs/core@0.12.
|
|
29
|
+
"purl": "pkg:npm/%40blamejs/core@0.12.70",
|
|
30
30
|
"properties": [],
|
|
31
31
|
"externalReferences": [
|
|
32
32
|
{
|
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
"components": [],
|
|
55
55
|
"dependencies": [
|
|
56
56
|
{
|
|
57
|
-
"ref": "@blamejs/core@0.12.
|
|
57
|
+
"ref": "@blamejs/core@0.12.70",
|
|
58
58
|
"dependsOn": []
|
|
59
59
|
}
|
|
60
60
|
]
|