@blamejs/core 0.12.46 → 0.12.48
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 +2 -2
- package/lib/cose.js +189 -0
- package/lib/network-dnssec.js +328 -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.48 (2026-05-25) — **`b.network.dns.dnssec` — local DNSSEC signature verification (RFC 4035).** Verify a DNS answer's RRSIG signature yourself instead of trusting the upstream resolver's AD bit. b.network.dns.dnssec.verifyRrset reconstructs the RFC 4034 §3.1.8.1 signed data — the RRSIG RDATA without the signature, followed by the RRset in canonical form (owner names lowercased, RRs ordered by canonical RDATA, the RRSIG's Original TTL) — and checks the signature against the DNSKEY, enforcing the inception / expiration window. Supports RSA/SHA-256 (alg 8), ECDSA P-256/SHA-256 (13), ECDSA P-384/SHA-384 (14), and Ed25519 (15) — the modern deployed set. verifyDs checks a delegation-signer digest against a DNSKEY (SHA-256 / SHA-384) and keyTag computes the RFC 4034 Appendix B key tag. The verification core is what a chain-walker composes; it defends against a compromised or on-path resolver that lies about authentication. **Added:** *`b.network.dns.dnssec.verifyRrset(opts)`* — Verifies an RRSIG over a canonicalised RRset against a DNSKEY. `opts` carries the owner `name`, the RR `type`, the wire-format `rdatas`, the parsed `rrsig` (algorithm / labels / originalTtl / inception / expiration / keyTag / signerName / signature), and the `dnskey` (algorithm + raw public key). The signed data is rebuilt per RFC 4034 §3.1.8.1: the RRSIG prefix (type covered | algorithm | labels | original TTL | expiration | inception | key tag | canonical signer name) followed by each RR in canonical form (lowercased owner | type | class | original TTL | rdlen | rdata), sorted by `Buffer.compare` on the RDATA. The validity window is enforced against `opts.at` (defaults to now; an invalid Date is refused, not treated as now). An RRSIG whose algorithm disagrees with the DNSKEY is refused before any key is built. RR types that embed domain names in their RDATA (NS, CNAME, SOA, MX, SRV, …) need RDATA-internal name-lowercasing this version does not perform, so they are refused with `dnssec/uncanonicalizable-type` rather than mis-validated; the security-critical DNSKEY / DS and the name-free address / text types (A, AAAA, TXT, CAA, TLSA, …) are fully supported. · *`b.network.dns.dnssec.verifyDs(opts)` / `b.network.dns.dnssec.keyTag(dnskeyRdata)`* — `verifyDs` confirms a delegation-signer record matches a DNSKEY: it checks the key tag, then compares the DS digest (SHA-256 type 2 / SHA-384 type 4) against the digest computed over the canonical owner name and the DNSKEY RDATA, constant-time. `keyTag` computes the RFC 4034 Appendix B 16-bit key tag from a DNSKEY's full RDATA — the identifier an RRSIG or DS uses to select the signing key. Together with `verifyRrset` these are the per-RRset building blocks a recursive chain-walk (root → TLD → zone) composes; the chain-walk itself, NSEC / NSEC3 denial-of-existence, and the bundled IANA root trust anchor are not part of this core.
|
|
12
|
+
|
|
13
|
+
- v0.12.47 (2026-05-25) — **`b.cose.mac0` / `b.cose.macVerify0` — COSE_Mac0 (RFC 9052 §6.2).** Completes the COSE message-type set (COSE_Sign1 / COSE_Encrypt0 / COSE_Mac0) with single shared-key MACs. b.cose.mac0 produces a tagged COSE_Mac0 over a payload using HMAC-SHA-256/384/512 (the COSE-standard MAC algorithms; HMAC is symmetric, so its post-quantum strength is preserved). b.cose.macVerify0 recomputes the tag over the MAC_structure and compares it in constant time, with a mandatory algorithm allowlist. Use when both parties hold a shared key — e.g. an ECDH-derived key — and a non-repudiable signature is not wanted; detached payloads are supported (the proximity mdoc device-MAC variant and MACed CWTs are the consumers). Composes b.cbor + the framework's constant-time compare; no new runtime dependency. **Added:** *`b.cose.mac0(payload, opts)` / `b.cose.macVerify0(coseMac0, opts)`* — `mac0` emits a tagged COSE_Mac0 (tag 17) with `alg` (`HMAC-256/256` | `HMAC-384/384` | `HMAC-512/512`) in the protected header and the HMAC tag computed over the MAC_structure `["MAC0", protected, external_aad, payload]`; `detached: true` emits a nil payload. `macVerify0` reads the algorithm from the protected header (must be in the required `opts.algorithms` allowlist), recomputes the tag, and compares it constant-time — a wrong key, tampered tag, or `external_aad` mismatch is refused with `cose/bad-tag`; a detached payload is supplied via `opts.externalPayload`. `external_aad` binds context into the tag.
|
|
14
|
+
|
|
11
15
|
- v0.12.46 (2026-05-25) — **`b.mdoc.verifyDeviceAuth` — ISO 18013-5 mdoc device authentication.** Completes mdoc verification with the holder-binding half (ISO 18013-5 §9.1.3, signature variant). verifyIssuerSigned proves the data is issuer-signed; verifyDeviceAuth proves the presenter controls the device key the issuer bound into the MSO, so a captured issuer-signed document cannot be replayed by anyone else. The device's COSE_Sign1 (deviceSigned.deviceAuth.deviceSignature) is verified over the detached DeviceAuthentication structure ["DeviceAuthentication", SessionTranscript, DocType, DeviceNameSpacesBytes] using the device key from verifyIssuerSigned().deviceKey (now surfaced) and the operator-supplied SessionTranscript that binds the proof to this exact exchange (the presentation protocol — e.g. OpenID4VP — defines the transcript). Composes the v0.12.45 b.cose detached-payload verify + importKey. The MAC variant (deviceMac / COSE_Mac0, used in proximity flows with a reader ephemeral key) is deferred and refused with mdoc/device-mac-unsupported. No new runtime dependency. **Added:** *`b.mdoc.verifyDeviceAuth(opts)` + `deviceKey` on the verifyIssuerSigned result* — `verifyDeviceAuth({ deviceKey, deviceSigned, docType, sessionTranscript, algorithms })` imports the device key (a COSE_Key via `b.cose.importKey`, or a KeyObject), reconstructs the detached `DeviceAuthentication` payload, and verifies the `deviceSignature` COSE_Sign1 against the mandatory algorithm allowlist — a mismatched `sessionTranscript` or `docType` fails the signature. `verifyIssuerSigned` now returns `deviceKey` (the MSO `deviceKeyInfo.deviceKey`) so the two checks chain. The MAC variant (`deviceMac`) is refused with `mdoc/device-mac-unsupported` pending COSE_Mac0 + reader-key support.
|
|
12
16
|
|
|
13
17
|
- v0.12.45 (2026-05-25) — **`b.cose` adds detached-payload sign/verify + `b.cose.importKey` (COSE_Key).** Two RFC 9052 / 9053 completions to the COSE substrate, both useable today and the prerequisites for mdoc device authentication and C2PA claim verification. Detached payloads (RFC 9052 §4.1): b.cose.sign with detached:true emits a COSE_Sign1 whose payload slot is nil — the signature still covers the payload, and the caller transmits it out of band; b.cose.verify takes the payload back as opts.externalPayload and binds it into the Sig_structure. A detached token verified without externalPayload is refused, and supplying externalPayload for an attached token is refused as ambiguous. COSE_Key import (RFC 9052 §7): b.cose.importKey turns a COSE_Key CBOR map into a node:crypto public KeyObject for b.cose.verify, accepting EC2 (P-256 / P-384 / P-521) and OKP (Ed25519) with the curve allowlisted so an unexpected key type is refused. No new runtime dependency. **Added:** *Detached COSE_Sign1 payloads + `b.cose.importKey(coseKey)`* — `b.cose.sign(payload, { detached: true })` emits a nil-payload COSE_Sign1 (the signature covers the payload regardless); `b.cose.verify(coseSign1, { externalPayload })` reconstructs the Sig_structure from the supplied payload, refusing a detached token with no `externalPayload` (`cose/detached-no-payload`) and refusing `externalPayload` on an attached token (`cose/payload-ambiguous`). `b.cose.importKey(coseKey)` maps a COSE_Key map (`kty` 2/EC2 with `crv` P-256/384/521, or `kty` 1/OKP with Ed25519) to a public KeyObject, allowlisting `kty`/`crv` and refusing anything else with `cose/unsupported-key` — the verification key embedded in an mdoc MSO or COSE_Key header is consumed this way.
|
package/README.md
CHANGED
|
@@ -120,14 +120,14 @@ The framework bundles the surface a typical Node app reaches for. Every primitiv
|
|
|
120
120
|
- In-process CIDR fence (`b.middleware.networkAllowlist`)
|
|
121
121
|
- `Cache-Control: no-store` on every 401 from `requireAuth` / `requireAal` / `requireStepUp` per RFC 9111 §5.2.2.5
|
|
122
122
|
- **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`)
|
|
123
|
-
- **Network configurability (`b.network`)** — env-driven NTP / NTS (RFC 8915), IPv4/IPv6 NTP, DNS with IPv6 / DoH / DoT (private-CA pinning) / cache / lookup timeout; outbound HTTP proxy (`HTTP_PROXY` / `HTTPS_PROXY` / `NO_PROXY`); runtime DPI trust-store CA additions; application-level heartbeats; TCP socket defaults
|
|
123
|
+
- **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) so a resolver client can verify an answer instead of trusting the upstream AD bit; outbound HTTP proxy (`HTTP_PROXY` / `HTTPS_PROXY` / `NO_PROXY`); runtime DPI trust-store CA additions; application-level heartbeats; TCP socket defaults
|
|
124
124
|
- **Error pages** — operator-rendered, no app-frame leakage (`b.errorPage`)
|
|
125
125
|
### Defensive parsers
|
|
126
126
|
|
|
127
127
|
- **JSON / SQL / schema** — `b.safeJson` (with `maxKeys` cap defending CVE-2026-21717 V8 HashDoS), `b.safeBuffer`, `b.safeSql`, `b.safeSchema`
|
|
128
128
|
- **URL + path** — `b.safeUrl` (IDN mixed-script / homograph refuse); `b.safeJsonPath` (refuses filter `?(...)`, deep-scan `$..`, script-shape `(@.x)` for safe Postgres JSONB ops)
|
|
129
129
|
- **Binary codec** — `b.cbor` bounded deterministic CBOR (RFC 8949 §4.2): depth/size caps, indefinite-length + reserved-info + tag + duplicate-key refusal, `requireDeterministic` canonical-form check; the in-tree substrate under COSE / CWT / SCITT / WebAuthn attestation
|
|
130
|
-
- **COSE
|
|
130
|
+
- **COSE messages** — `b.cose` the full RFC 9052 message-type set over `b.cbor`: COSE_Sign1 sign/verify (attached or detached payload), COSE_Encrypt0 single-recipient AEAD, COSE_Mac0 shared-key HMAC (mac0/macVerify0), plus `importKey` (COSE_Key → KeyObject). Signatures use classical ES256/384/512 + EdDSA (final COSE ids, interoperable today) plus ML-DSA-87 (PQC-forward, draft id); bounded + alg-allowlisted + crit-bypass-checked verification; AEAD ChaCha20/Poly1305 default (AES-GCM opt-in); the signed-statement substrate under SCITT / CWT / mdoc / C2PA
|
|
131
131
|
- **CBOR Web Token** — `b.cwt` CWT sign/verify (RFC 8392) over `b.cose`: standard-claim mapping (iss/sub/aud/exp/nbf/iat/cti) + `exp`/`nbf` clock-skew enforcement + `iss`/`aud` matching; the CBOR-native JWT for constrained / IoT / FIDO / verifiable-credential contexts
|
|
132
132
|
- **Entity Attestation Token** — `b.eat` EAT sign/verify (RFC 9711) over `b.cwt`: device + software attestation claims (ueid / oemid / hwmodel / measurements / submods) with verifier-nonce freshness binding, `dbgstat` debug-status policy, and `eat_profile` pinning
|
|
133
133
|
- **SCITT signed statements** — `b.scitt` sign/verify a signed, attributable claim about an artifact (signed SBOM, build attestation, release approval) over `b.cose`: the issuer + subject bind in the integrity-protected CWT_Claims header (RFC 9597); verification refuses any statement missing the iss/sub binding. The issuer side, on finalized RFCs; the transparency receipt (COSE Receipts draft) opts in on publication
|
package/lib/cose.js
CHANGED
|
@@ -53,6 +53,7 @@
|
|
|
53
53
|
|
|
54
54
|
var nodeCrypto = require("node:crypto");
|
|
55
55
|
var cbor = require("./cbor");
|
|
56
|
+
var bCrypto = require("./crypto");
|
|
56
57
|
var validateOpts = require("./validate-opts");
|
|
57
58
|
var { defineClass } = require("./framework-error");
|
|
58
59
|
|
|
@@ -548,6 +549,190 @@ function decrypt0(coseEncrypt0, opts) {
|
|
|
548
549
|
return { plaintext: pt, alg: algName, protectedHeaders: protMap, unprotectedHeaders: unprotected };
|
|
549
550
|
}
|
|
550
551
|
|
|
552
|
+
// ---- COSE_Mac0 (RFC 9052 §6.2) — single shared-key MAC ----
|
|
553
|
+
|
|
554
|
+
var COSE_MAC0_TAG = 17; // allow:raw-byte-literal — RFC 9052 COSE_Mac0 CBOR tag
|
|
555
|
+
// HMAC algorithms (RFC 9053 §3.1). Only the full-length tags are offered —
|
|
556
|
+
// the truncated HMAC 256/64 (id 4) is omitted. HMAC is symmetric, so its
|
|
557
|
+
// post-quantum strength is fine; these are the COSE-standard MAC algs.
|
|
558
|
+
var HMAC_NAME_TO_ID = { "HMAC-256/256": 5, "HMAC-384/384": 6, "HMAC-512/512": 7 }; // allow:raw-byte-literal — COSE HMAC algorithm ids (RFC 9053)
|
|
559
|
+
var HMAC_ID_TO_NAME = {};
|
|
560
|
+
Object.keys(HMAC_NAME_TO_ID).forEach(function (k) { HMAC_ID_TO_NAME[HMAC_NAME_TO_ID[k]] = k; });
|
|
561
|
+
function _hmacHash(algId) {
|
|
562
|
+
switch (algId) {
|
|
563
|
+
case 5: return "sha256";
|
|
564
|
+
case 6: return "sha384";
|
|
565
|
+
case 7: return "sha512";
|
|
566
|
+
default: throw new CoseError("cose/unknown-alg", "cose: unrecognized HMAC COSE alg id " + algId);
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
// MAC_structure (§6.3) = [ "MAC0", body_protected (bstr), external_aad (bstr), payload (bstr) ].
|
|
571
|
+
function _macStructure(protectedBstr, externalAad, payload) {
|
|
572
|
+
return cbor.encode(["MAC0", protectedBstr, externalAad, payload]);
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
/**
|
|
576
|
+
* @primitive b.cose.mac0
|
|
577
|
+
* @signature b.cose.mac0(payload, opts)
|
|
578
|
+
* @since 0.12.47
|
|
579
|
+
* @status stable
|
|
580
|
+
* @related b.cose.macVerify0, b.cose.sign
|
|
581
|
+
*
|
|
582
|
+
* Produce a tagged COSE_Mac0 (RFC 9052 §6.2) — a single shared-key MAC
|
|
583
|
+
* over <code>payload</code>. The MAC is HMAC-SHA-256 / 384 / 512 (the
|
|
584
|
+
* COSE-standard MAC algorithms; HMAC is symmetric, so post-quantum
|
|
585
|
+
* strength is preserved). Use when both parties hold a shared key (e.g.
|
|
586
|
+
* an ECDH-derived key) and a non-repudiable signature is not wanted.
|
|
587
|
+
* <code>detached: true</code> emits a nil payload, verified later with
|
|
588
|
+
* <code>opts.externalPayload</code>.
|
|
589
|
+
*
|
|
590
|
+
* @opts
|
|
591
|
+
* {
|
|
592
|
+
* alg: string, // "HMAC-256/256" | "HMAC-384/384" | "HMAC-512/512"
|
|
593
|
+
* key: Buffer, // shared symmetric key
|
|
594
|
+
* externalAad?: Buffer, // bound into the MAC
|
|
595
|
+
* detached?: boolean, // emit a nil payload (caller re-supplies it on verify)
|
|
596
|
+
* unprotectedHeaders?: object,
|
|
597
|
+
* }
|
|
598
|
+
*
|
|
599
|
+
* @example
|
|
600
|
+
* var mac = b.cose.mac0(Buffer.from("data"), { alg: "HMAC-256/256", key: sharedKey });
|
|
601
|
+
*/
|
|
602
|
+
function mac0(payload, opts) {
|
|
603
|
+
validateOpts.requireObject(opts, "cose.mac0", CoseError);
|
|
604
|
+
validateOpts(opts, ["alg", "key", "externalAad", "detached", "unprotectedHeaders"], "cose.mac0");
|
|
605
|
+
if (!(opts.alg in HMAC_NAME_TO_ID)) {
|
|
606
|
+
throw new CoseError("cose/unsignable-alg", "cose.mac0: alg must be one of " + Object.keys(HMAC_NAME_TO_ID).join(" / "));
|
|
607
|
+
}
|
|
608
|
+
var key = _bstr(opts.key);
|
|
609
|
+
var algId = HMAC_NAME_TO_ID[opts.alg];
|
|
610
|
+
var protMap = new Map();
|
|
611
|
+
protMap.set(HDR_ALG, algId);
|
|
612
|
+
var protectedBstr = cbor.encode(protMap);
|
|
613
|
+
|
|
614
|
+
var unprot = new Map();
|
|
615
|
+
if (opts.unprotectedHeaders && typeof opts.unprotectedHeaders === "object") {
|
|
616
|
+
var uk = Object.keys(opts.unprotectedHeaders);
|
|
617
|
+
for (var i = 0; i < uk.length; i++) unprot.set(Number(uk[i]), opts.unprotectedHeaders[uk[i]]);
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
var payloadBytes = _bstr(payload);
|
|
621
|
+
var externalAad = opts.externalAad == null ? Buffer.alloc(0) : _bstr(opts.externalAad);
|
|
622
|
+
var tag = nodeCrypto.createHmac(_hmacHash(algId), key).update(_macStructure(protectedBstr, externalAad, payloadBytes)).digest();
|
|
623
|
+
|
|
624
|
+
var mac0arr = [protectedBstr, unprot, opts.detached ? null : payloadBytes, tag];
|
|
625
|
+
return cbor.encode(new cbor.Tag(COSE_MAC0_TAG, mac0arr));
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
/**
|
|
629
|
+
* @primitive b.cose.macVerify0
|
|
630
|
+
* @signature b.cose.macVerify0(coseMac0, opts)
|
|
631
|
+
* @since 0.12.47
|
|
632
|
+
* @status stable
|
|
633
|
+
* @related b.cose.mac0
|
|
634
|
+
*
|
|
635
|
+
* Verify a COSE_Mac0 (RFC 9052 §6.2) and return its payload. The HMAC
|
|
636
|
+
* tag is recomputed over the MAC_structure and compared in constant
|
|
637
|
+
* time; the <code>alg</code> from the protected header must be in
|
|
638
|
+
* <code>opts.algorithms</code>. A detached (nil) payload is supplied via
|
|
639
|
+
* <code>opts.externalPayload</code>.
|
|
640
|
+
*
|
|
641
|
+
* @opts
|
|
642
|
+
* {
|
|
643
|
+
* algorithms: string[], // required — accepted HMAC alg names (allowlist)
|
|
644
|
+
* key: Buffer, // the shared symmetric key
|
|
645
|
+
* externalAad?: Buffer,
|
|
646
|
+
* externalPayload?: Buffer, // required for a detached payload
|
|
647
|
+
* maxBytes?: number,
|
|
648
|
+
* maxDepth?: number,
|
|
649
|
+
* }
|
|
650
|
+
*
|
|
651
|
+
* @example
|
|
652
|
+
* var out = b.cose.macVerify0(mac, { algorithms: ["HMAC-256/256"], key: sharedKey });
|
|
653
|
+
* // → { payload: <Buffer>, alg: "HMAC-256/256", protectedHeaders: Map, unprotectedHeaders: Map }
|
|
654
|
+
*/
|
|
655
|
+
function macVerify0(coseMac0, opts) {
|
|
656
|
+
validateOpts.requireObject(opts, "cose.macVerify0", CoseError);
|
|
657
|
+
validateOpts(opts, ["algorithms", "key", "externalAad", "externalPayload", "maxBytes", "maxDepth"], "cose.macVerify0");
|
|
658
|
+
if (!Array.isArray(opts.algorithms) || opts.algorithms.length === 0) {
|
|
659
|
+
throw new CoseError("cose/algorithms-required", "cose.macVerify0: opts.algorithms is required");
|
|
660
|
+
}
|
|
661
|
+
for (var ai = 0; ai < opts.algorithms.length; ai++) {
|
|
662
|
+
if (!(opts.algorithms[ai] in HMAC_NAME_TO_ID)) {
|
|
663
|
+
throw new CoseError("cose/unknown-alg", "cose.macVerify0: unknown algorithm '" + opts.algorithms[ai] + "'");
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
if (opts.key == null) throw new CoseError("cose/no-key", "cose.macVerify0: opts.key is required");
|
|
667
|
+
var key = _bstr(opts.key);
|
|
668
|
+
|
|
669
|
+
var decoded = cbor.decode(_bstr(coseMac0), { allowedTags: [COSE_MAC0_TAG], maxBytes: opts.maxBytes, maxDepth: opts.maxDepth });
|
|
670
|
+
var arr = (decoded instanceof cbor.Tag && decoded.tag === COSE_MAC0_TAG) ? decoded.value : decoded;
|
|
671
|
+
if (!Array.isArray(arr) || arr.length !== 4) {
|
|
672
|
+
throw new CoseError("cose/malformed", "cose.macVerify0: not a COSE_Mac0 (expected a 4-element array)");
|
|
673
|
+
}
|
|
674
|
+
var protectedBstr = arr[0];
|
|
675
|
+
var unprotected = arr[1];
|
|
676
|
+
var payload = arr[2];
|
|
677
|
+
var tag = arr[3];
|
|
678
|
+
if (!Buffer.isBuffer(protectedBstr) || !Buffer.isBuffer(tag)) {
|
|
679
|
+
throw new CoseError("cose/malformed", "cose.macVerify0: protected header and tag must be byte strings");
|
|
680
|
+
}
|
|
681
|
+
if (!(unprotected instanceof Map)) {
|
|
682
|
+
throw new CoseError("cose/malformed", "cose.macVerify0: unprotected header must be a CBOR map");
|
|
683
|
+
}
|
|
684
|
+
if (payload === null || payload === undefined) {
|
|
685
|
+
if (opts.externalPayload == null) {
|
|
686
|
+
throw new CoseError("cose/detached-no-payload", "cose.macVerify0: detached (nil) payload — pass opts.externalPayload");
|
|
687
|
+
}
|
|
688
|
+
payload = _bstr(opts.externalPayload);
|
|
689
|
+
} else if (opts.externalPayload != null) {
|
|
690
|
+
throw new CoseError("cose/payload-ambiguous", "cose.macVerify0: externalPayload supplied but the COSE_Mac0 carries an attached payload");
|
|
691
|
+
} else if (!Buffer.isBuffer(payload)) {
|
|
692
|
+
throw new CoseError("cose/malformed", "cose.macVerify0: payload must be a byte string (bstr)");
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
var protMap = protectedBstr.length === 0 ? new Map()
|
|
696
|
+
: cbor.decode(protectedBstr, { maxBytes: opts.maxBytes, maxDepth: opts.maxDepth });
|
|
697
|
+
if (!(protMap instanceof Map)) {
|
|
698
|
+
throw new CoseError("cose/malformed", "cose.macVerify0: protected header is not a CBOR map");
|
|
699
|
+
}
|
|
700
|
+
// crit-bypass defense (RFC 9052 §3.1) — same as b.cose.verify: every
|
|
701
|
+
// label a crit array names must be one this verifier understands AND
|
|
702
|
+
// be present in the protected header.
|
|
703
|
+
if (protMap.has(HDR_CRIT)) {
|
|
704
|
+
var crit = protMap.get(HDR_CRIT);
|
|
705
|
+
if (!Array.isArray(crit)) {
|
|
706
|
+
throw new CoseError("cose/bad-crit", "cose.macVerify0: crit (label 2) must be an array");
|
|
707
|
+
}
|
|
708
|
+
for (var ci = 0; ci < crit.length; ci++) {
|
|
709
|
+
if (UNDERSTOOD_LABELS.indexOf(crit[ci]) === -1) {
|
|
710
|
+
throw new CoseError("cose/crit-unknown",
|
|
711
|
+
"cose.macVerify0: crit lists header label " + crit[ci] + " which is not understood (RFC 9052 §3.1)");
|
|
712
|
+
}
|
|
713
|
+
if (!protMap.has(crit[ci])) {
|
|
714
|
+
throw new CoseError("cose/crit-absent",
|
|
715
|
+
"cose.macVerify0: crit lists label " + crit[ci] + " not present in the protected header");
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
var algId = protMap.get(HDR_ALG);
|
|
720
|
+
var algName = HMAC_ID_TO_NAME[algId];
|
|
721
|
+
if (algName === undefined) {
|
|
722
|
+
throw new CoseError("cose/unknown-alg", "cose.macVerify0: unrecognized protected MAC alg id " + algId);
|
|
723
|
+
}
|
|
724
|
+
if (opts.algorithms.indexOf(algName) === -1) {
|
|
725
|
+
throw new CoseError("cose/alg-not-allowed", "cose.macVerify0: alg '" + algName + "' is not in the allowlist");
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
var externalAad = opts.externalAad == null ? Buffer.alloc(0) : _bstr(opts.externalAad);
|
|
729
|
+
var expected = nodeCrypto.createHmac(_hmacHash(algId), key).update(_macStructure(protectedBstr, externalAad, payload)).digest();
|
|
730
|
+
if (!bCrypto.timingSafeEqual(expected, tag)) {
|
|
731
|
+
throw new CoseError("cose/bad-tag", "cose.macVerify0: MAC tag verification failed");
|
|
732
|
+
}
|
|
733
|
+
return { payload: payload, alg: algName, protectedHeaders: protMap, unprotectedHeaders: unprotected };
|
|
734
|
+
}
|
|
735
|
+
|
|
551
736
|
// ---- COSE_Key (RFC 9052 §7 / RFC 9053 §7) → KeyObject ----
|
|
552
737
|
|
|
553
738
|
// COSE_Key EC2 curve identifiers (RFC 9053 §7.1) → JWK crv names. Only
|
|
@@ -623,8 +808,12 @@ module.exports = {
|
|
|
623
808
|
verify: verify,
|
|
624
809
|
encrypt0: encrypt0,
|
|
625
810
|
decrypt0: decrypt0,
|
|
811
|
+
mac0: mac0,
|
|
812
|
+
macVerify0: macVerify0,
|
|
626
813
|
importKey: importKey,
|
|
627
814
|
ALGORITHMS: ALG_NAME_TO_ID,
|
|
815
|
+
MAC_ALGORITHMS: HMAC_NAME_TO_ID,
|
|
816
|
+
COSE_MAC0_TAG: COSE_MAC0_TAG,
|
|
628
817
|
AEAD_ALGORITHMS: AEAD_NAME_TO_ID,
|
|
629
818
|
COSE_SIGN1_TAG: COSE_SIGN1_TAG,
|
|
630
819
|
COSE_ENCRYPT0_TAG: COSE_ENCRYPT0_TAG,
|
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* @module b.network.dns.dnssec
|
|
4
|
+
* @nav Network
|
|
5
|
+
* @title DNSSEC validation
|
|
6
|
+
*
|
|
7
|
+
* @intro
|
|
8
|
+
* Local DNSSEC signature verification (RFC 4033–4035 / 6605 / 8080) —
|
|
9
|
+
* the cryptographic core that lets a resolver client verify a DNS
|
|
10
|
+
* answer itself instead of trusting the upstream resolver's AD bit.
|
|
11
|
+
* <code>b.network.dns.resolver</code> checks the AD flag; this module
|
|
12
|
+
* verifies the actual RRSIG signature over the canonicalised RRset,
|
|
13
|
+
* defending against a compromised or on-path resolver.
|
|
14
|
+
*
|
|
15
|
+
* <code>verifyRrset</code> reconstructs the RFC 4034 §3.1.8.1 signed
|
|
16
|
+
* data (the RRSIG RDATA without the signature, followed by the RRset
|
|
17
|
+
* in canonical form — owner names lowercased, RRs ordered by canonical
|
|
18
|
+
* RDATA, the RRSIG's Original TTL) and verifies it with the DNSKEY,
|
|
19
|
+
* enforcing the signature's inception / expiration window. The DNSKEY
|
|
20
|
+
* algorithms are RSA/SHA-256 (8), ECDSA P-256/SHA-256 (13), ECDSA
|
|
21
|
+
* P-384/SHA-384 (14), and Ed25519 (15) — the modern, deployed set.
|
|
22
|
+
* <code>verifyDs</code> checks a delegation-signer digest against a
|
|
23
|
+
* DNSKEY (SHA-256 / SHA-384), and <code>keyTag</code> computes the
|
|
24
|
+
* RFC 4034 Appendix B key tag.
|
|
25
|
+
*
|
|
26
|
+
* <strong>Scope.</strong> This is the verification core. RR types that
|
|
27
|
+
* carry domain names in their RDATA (NS, CNAME, SOA, MX, SRV, …) need
|
|
28
|
+
* name-lowercasing inside the RDATA (RFC 4034 §6.2) that this version
|
|
29
|
+
* does not perform, so they are refused with
|
|
30
|
+
* <code>dnssec/uncanonicalizable-type</code> rather than mis-validated
|
|
31
|
+
* — the security-critical DNSKEY / DS and the name-free address /
|
|
32
|
+
* text types (A, AAAA, TXT, …) are fully supported. The recursive
|
|
33
|
+
* chain-walk (root → TLD → zone), NSEC / NSEC3 denial-of-existence,
|
|
34
|
+
* and the IANA root trust-anchor bundle are deferred: these primitives
|
|
35
|
+
* are the per-RRset building blocks a chain-walker composes.
|
|
36
|
+
*
|
|
37
|
+
* @card
|
|
38
|
+
* Local DNSSEC verification (RFC 4035) — verify an RRSIG over a
|
|
39
|
+
* canonicalised RRset against a DNSKEY (RSA / ECDSA P-256·P-384 /
|
|
40
|
+
* Ed25519), plus DS-digest + key-tag. Don't trust the upstream AD bit;
|
|
41
|
+
* verify the signature. Name-bearing RR types are refused, not
|
|
42
|
+
* mis-validated; chain-walk + NSEC3 deferred.
|
|
43
|
+
*/
|
|
44
|
+
|
|
45
|
+
var nodeCrypto = require("node:crypto");
|
|
46
|
+
var bCrypto = require("./crypto");
|
|
47
|
+
var validateOpts = require("./validate-opts");
|
|
48
|
+
var { defineClass } = require("./framework-error");
|
|
49
|
+
|
|
50
|
+
var DnssecError = defineClass("DnssecError", { alwaysPermanent: true });
|
|
51
|
+
|
|
52
|
+
// DNSSEC algorithm numbers (IANA DNSSEC Algorithm Numbers) → verify params.
|
|
53
|
+
var ALGS = {
|
|
54
|
+
8: { name: "RSASHA256", kind: "rsa", hash: "sha256" }, // allow:raw-byte-literal — IANA DNSSEC algorithm number
|
|
55
|
+
13: { name: "ECDSAP256SHA256", kind: "ec", hash: "sha256", crv: "P-256", coord: 32 }, // allow:raw-byte-literal — P-256 coordinate size
|
|
56
|
+
14: { name: "ECDSAP384SHA384", kind: "ec", hash: "sha384", crv: "P-384", coord: 48 }, // allow:raw-byte-literal — P-384 coordinate size
|
|
57
|
+
15: { name: "ED25519", kind: "okp", hash: null, crv: "Ed25519" },
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
// DS digest algorithms (IANA) → node hash.
|
|
61
|
+
var DS_DIGESTS = { 2: "sha256", 4: "sha384" };
|
|
62
|
+
|
|
63
|
+
// RR types whose RDATA contains NO embedded domain name, so the wire
|
|
64
|
+
// RDATA is already in canonical form (RFC 4034 §6.2 needs no rewrite).
|
|
65
|
+
// Name-bearing types are refused rather than silently mis-canonicalised.
|
|
66
|
+
// (type numbers IANA): A 1, AAAA 28, TXT 16, DNSKEY 48, DS 43, CAA 257,
|
|
67
|
+
// TLSA 52, SSHFP 44, HINFO 13, CDS 59, CDNSKEY 60, OPENPGPKEY 61, SMIMEA 53.
|
|
68
|
+
var NAME_FREE_TYPE_NUMS = [1, 28, 16, 48, 43, 257, 52, 44, 13, 59, 60, 61, 53]; // allow:raw-byte-literal allow:raw-time-literal — IANA DNS type numbers (no embedded names)
|
|
69
|
+
var TYPE_NUM = {
|
|
70
|
+
A: 1, NS: 2, CNAME: 5, SOA: 6, PTR: 12, MX: 15, TXT: 16, AAAA: 28, SRV: 33,
|
|
71
|
+
DS: 43, SSHFP: 44, RRSIG: 46, DNSKEY: 48, TLSA: 52, SMIMEA: 53, CDS: 59, CDNSKEY: 60, // allow:raw-byte-literal allow:raw-time-literal — IANA DNS type numbers
|
|
72
|
+
OPENPGPKEY: 61, CAA: 257, HINFO: 13,
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
function _bytes(x, what) {
|
|
76
|
+
if (Buffer.isBuffer(x)) return x;
|
|
77
|
+
if (x instanceof Uint8Array) return Buffer.from(x);
|
|
78
|
+
throw new DnssecError("dnssec/bad-bytes", "dnssec: " + what + " must be a Buffer");
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Canonical wire form of a domain name (RFC 4034 §6.2): each label
|
|
82
|
+
// length-prefixed, ASCII lowercased, terminated by the root label.
|
|
83
|
+
function _canonicalName(name) {
|
|
84
|
+
if (typeof name !== "string") throw new DnssecError("dnssec/bad-name", "dnssec: name must be a string");
|
|
85
|
+
var n = name.replace(/\.$/, "");
|
|
86
|
+
if (n === "") return Buffer.from([0]);
|
|
87
|
+
var labels = n.split(".");
|
|
88
|
+
var parts = [];
|
|
89
|
+
for (var i = 0; i < labels.length; i++) {
|
|
90
|
+
var lab = Buffer.from(labels[i].toLowerCase(), "ascii");
|
|
91
|
+
if (lab.length === 0 || lab.length > 63) { // allow:raw-byte-literal — DNS label length cap (RFC 1035)
|
|
92
|
+
throw new DnssecError("dnssec/bad-name", "dnssec: invalid label in '" + name + "'");
|
|
93
|
+
}
|
|
94
|
+
parts.push(Buffer.from([lab.length]), lab);
|
|
95
|
+
}
|
|
96
|
+
parts.push(Buffer.from([0]));
|
|
97
|
+
return Buffer.concat(parts);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function _u16(n) { return Buffer.from([(n >> 8) & 0xff, n & 0xff]); } // allow:raw-byte-literal — 16-bit big-endian split
|
|
101
|
+
function _u32(n) {
|
|
102
|
+
var b = Buffer.alloc(4);
|
|
103
|
+
b.writeUInt32BE(n >>> 0, 0);
|
|
104
|
+
return b;
|
|
105
|
+
}
|
|
106
|
+
function _typeNumber(type) {
|
|
107
|
+
if (typeof type === "number") return type;
|
|
108
|
+
var t = TYPE_NUM[String(type).toUpperCase()];
|
|
109
|
+
if (t === undefined) throw new DnssecError("dnssec/unknown-type", "dnssec: unknown RR type '" + type + "'");
|
|
110
|
+
return t;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// DNSKEY public-key RDATA → JWK (kty/crv allowlisted; RFC 3110 RSA,
|
|
114
|
+
// RFC 6605 ECDSA, RFC 8080 Ed25519). publicKey is the key bytes after
|
|
115
|
+
// the DNSKEY flags/protocol/algorithm fields.
|
|
116
|
+
function _dnskeyToKey(algId, publicKey) {
|
|
117
|
+
var alg = ALGS[algId];
|
|
118
|
+
if (!alg) throw new DnssecError("dnssec/unsupported-alg", "dnssec: unsupported DNSKEY algorithm " + algId);
|
|
119
|
+
var pk = _bytes(publicKey, "dnskey publicKey");
|
|
120
|
+
if (alg.kind === "rsa") {
|
|
121
|
+
// RFC 3110: exponent length is 1 byte, or (if that byte is 0) the
|
|
122
|
+
// next 2 bytes; then exponent, then modulus.
|
|
123
|
+
var off = 0, explen = pk[0];
|
|
124
|
+
off = 1;
|
|
125
|
+
if (explen === 0) { explen = (pk[1] << 8) | pk[2]; off = 3; } // allow:raw-byte-literal — RFC 3110 3-byte exponent length
|
|
126
|
+
if (explen === 0 || off + explen >= pk.length) {
|
|
127
|
+
throw new DnssecError("dnssec/bad-key", "dnssec: malformed RSA DNSKEY public key");
|
|
128
|
+
}
|
|
129
|
+
var exponent = pk.slice(off, off + explen);
|
|
130
|
+
var modulus = pk.slice(off + explen);
|
|
131
|
+
return _jwkKey({ kty: "RSA", n: modulus.toString("base64url"), e: exponent.toString("base64url") });
|
|
132
|
+
}
|
|
133
|
+
if (alg.kind === "ec") {
|
|
134
|
+
if (pk.length !== alg.coord * 2) {
|
|
135
|
+
throw new DnssecError("dnssec/bad-key", "dnssec: " + alg.crv + " key must be " + (alg.coord * 2) + " bytes (x||y)");
|
|
136
|
+
}
|
|
137
|
+
return _jwkKey({ kty: "EC", crv: alg.crv, x: pk.slice(0, alg.coord).toString("base64url"), y: pk.slice(alg.coord).toString("base64url") });
|
|
138
|
+
}
|
|
139
|
+
// Ed25519
|
|
140
|
+
if (pk.length !== 32) throw new DnssecError("dnssec/bad-key", "dnssec: Ed25519 key must be 32 bytes"); // allow:raw-byte-literal — Ed25519 key size
|
|
141
|
+
return _jwkKey({ kty: "OKP", crv: "Ed25519", x: pk.toString("base64url") });
|
|
142
|
+
}
|
|
143
|
+
function _jwkKey(jwk) {
|
|
144
|
+
try { return nodeCrypto.createPublicKey({ key: jwk, format: "jwk" }); }
|
|
145
|
+
catch (e) { throw new DnssecError("dnssec/bad-key", "dnssec: could not import DNSKEY: " + ((e && e.message) || e)); }
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* @primitive b.network.dns.dnssec.keyTag
|
|
150
|
+
* @signature b.network.dns.dnssec.keyTag(dnskeyRdata)
|
|
151
|
+
* @since 0.12.48
|
|
152
|
+
* @status stable
|
|
153
|
+
* @related b.network.dns.dnssec.verifyDs, b.network.dns.dnssec.verifyRrset
|
|
154
|
+
*
|
|
155
|
+
* Compute the RFC 4034 Appendix B key tag of a DNSKEY from its full
|
|
156
|
+
* RDATA (flags || protocol || algorithm || public key) — the 16-bit
|
|
157
|
+
* identifier an RRSIG / DS references to select the signing key.
|
|
158
|
+
*
|
|
159
|
+
* @example
|
|
160
|
+
* var tag = b.network.dns.dnssec.keyTag(dnskeyRdata);
|
|
161
|
+
*/
|
|
162
|
+
function keyTag(dnskeyRdata) {
|
|
163
|
+
var rd = _bytes(dnskeyRdata, "dnskeyRdata");
|
|
164
|
+
var acc = 0;
|
|
165
|
+
for (var i = 0; i < rd.length; i++) {
|
|
166
|
+
acc += (i & 1) ? rd[i] : (rd[i] << 8); // allow:raw-byte-literal — RFC 4034 App B key-tag accumulation
|
|
167
|
+
}
|
|
168
|
+
acc += (acc >> 16) & 0xffff; // allow:raw-byte-literal — App B fold
|
|
169
|
+
return acc & 0xffff; // allow:raw-byte-literal — App B 16-bit tag
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* @primitive b.network.dns.dnssec.verifyDs
|
|
174
|
+
* @signature b.network.dns.dnssec.verifyDs(opts)
|
|
175
|
+
* @since 0.12.48
|
|
176
|
+
* @status stable
|
|
177
|
+
* @related b.network.dns.dnssec.verifyRrset
|
|
178
|
+
*
|
|
179
|
+
* Verify a DS (Delegation Signer) record against a child DNSKEY — the
|
|
180
|
+
* link that lets a parent zone vouch for a child's key. The DS digest
|
|
181
|
+
* (SHA-256 / SHA-384) is recomputed over the owner name plus the DNSKEY
|
|
182
|
+
* RDATA and compared to the DS, with the key tag and algorithm checked.
|
|
183
|
+
*
|
|
184
|
+
* @opts
|
|
185
|
+
* {
|
|
186
|
+
* ownerName: string, // the child zone name (the DNSKEY owner)
|
|
187
|
+
* dnskeyRdata: Buffer, // full DNSKEY RDATA (flags||protocol||alg||publicKey)
|
|
188
|
+
* ds: { keyTag, algorithm, digestType, digest: Buffer }, // the parent DS
|
|
189
|
+
* }
|
|
190
|
+
*
|
|
191
|
+
* @example
|
|
192
|
+
* b.network.dns.dnssec.verifyDs({ ownerName: "example.com", dnskeyRdata: ksk, ds: parentDs });
|
|
193
|
+
*/
|
|
194
|
+
function verifyDs(opts) {
|
|
195
|
+
validateOpts.requireObject(opts, "dnssec.verifyDs", DnssecError);
|
|
196
|
+
validateOpts(opts, ["ownerName", "dnskeyRdata", "ds"], "dnssec.verifyDs");
|
|
197
|
+
var ds = opts.ds;
|
|
198
|
+
if (!ds || typeof ds !== "object") throw new DnssecError("dnssec/bad-ds", "dnssec.verifyDs: opts.ds is required");
|
|
199
|
+
var hashName = DS_DIGESTS[ds.digestType];
|
|
200
|
+
if (!hashName) throw new DnssecError("dnssec/unsupported-digest", "dnssec.verifyDs: unsupported DS digest type " + ds.digestType);
|
|
201
|
+
var rd = _bytes(opts.dnskeyRdata, "dnskeyRdata");
|
|
202
|
+
if (keyTag(rd) !== ds.keyTag) {
|
|
203
|
+
throw new DnssecError("dnssec/keytag-mismatch", "dnssec.verifyDs: DNSKEY key tag does not match the DS");
|
|
204
|
+
}
|
|
205
|
+
var digestInput = Buffer.concat([_canonicalName(opts.ownerName), rd]);
|
|
206
|
+
var expected = nodeCrypto.createHash(hashName).update(digestInput).digest();
|
|
207
|
+
var actual = _bytes(ds.digest, "ds.digest");
|
|
208
|
+
if (!bCrypto.timingSafeEqual(expected, actual)) {
|
|
209
|
+
throw new DnssecError("dnssec/ds-mismatch", "dnssec.verifyDs: DS digest does not match the DNSKEY");
|
|
210
|
+
}
|
|
211
|
+
return { ok: true, keyTag: ds.keyTag, digestType: ds.digestType };
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* @primitive b.network.dns.dnssec.verifyRrset
|
|
216
|
+
* @signature b.network.dns.dnssec.verifyRrset(opts)
|
|
217
|
+
* @since 0.12.48
|
|
218
|
+
* @status stable
|
|
219
|
+
* @compliance soc2
|
|
220
|
+
* @related b.network.dns.dnssec.verifyDs, b.network.dns.resolver.create
|
|
221
|
+
*
|
|
222
|
+
* Verify an RRSIG over an RRset against a DNSKEY (RFC 4035 §5.3). The
|
|
223
|
+
* signed data is reconstructed in canonical form — the RRSIG RDATA
|
|
224
|
+
* without the signature, then the RRset's records ordered by canonical
|
|
225
|
+
* RDATA with the RRSIG Original TTL — and the signature is verified with
|
|
226
|
+
* the DNSKEY (RSA/SHA-256, ECDSA P-256/384, Ed25519). The signature's
|
|
227
|
+
* inception / expiration window is enforced against <code>opts.at</code>.
|
|
228
|
+
* RR types carrying embedded domain names are refused
|
|
229
|
+
* (<code>dnssec/uncanonicalizable-type</code>) rather than mis-validated.
|
|
230
|
+
*
|
|
231
|
+
* @opts
|
|
232
|
+
* {
|
|
233
|
+
* name: string, // owner name of the RRset
|
|
234
|
+
* type: string|number, // RR type (e.g. "DNSKEY", "A")
|
|
235
|
+
* class?: number, // default 1 (IN)
|
|
236
|
+
* rdatas: Buffer[], // each record's wire-format RDATA
|
|
237
|
+
* rrsig: { // the RRSIG covering the RRset
|
|
238
|
+
* algorithm, labels, originalTtl, expiration, inception, keyTag,
|
|
239
|
+
* signerName: string, signature: Buffer,
|
|
240
|
+
* },
|
|
241
|
+
* dnskey: { algorithm, publicKey: Buffer }, // the signing DNSKEY (publicKey = bytes after flags/proto/alg)
|
|
242
|
+
* at?: Date, // validity instant (default now); must be a valid Date
|
|
243
|
+
* }
|
|
244
|
+
*
|
|
245
|
+
* @example
|
|
246
|
+
* b.network.dns.dnssec.verifyRrset({ name: "example.com", type: "DNSKEY", rdatas: keys, rrsig: sig, dnskey: ksk });
|
|
247
|
+
*/
|
|
248
|
+
function verifyRrset(opts) {
|
|
249
|
+
validateOpts.requireObject(opts, "dnssec.verifyRrset", DnssecError);
|
|
250
|
+
validateOpts(opts, ["name", "type", "class", "rdatas", "rrsig", "dnskey", "at"], "dnssec.verifyRrset");
|
|
251
|
+
var rrsig = opts.rrsig;
|
|
252
|
+
var dnskey = opts.dnskey;
|
|
253
|
+
if (!rrsig || typeof rrsig !== "object") throw new DnssecError("dnssec/bad-rrsig", "dnssec.verifyRrset: opts.rrsig is required");
|
|
254
|
+
if (!dnskey || typeof dnskey !== "object") throw new DnssecError("dnssec/bad-key", "dnssec.verifyRrset: opts.dnskey is required");
|
|
255
|
+
if (!Array.isArray(opts.rdatas) || opts.rdatas.length === 0) {
|
|
256
|
+
throw new DnssecError("dnssec/empty-rrset", "dnssec.verifyRrset: opts.rdatas must be a non-empty array");
|
|
257
|
+
}
|
|
258
|
+
var alg = ALGS[rrsig.algorithm];
|
|
259
|
+
if (!alg) throw new DnssecError("dnssec/unsupported-alg", "dnssec.verifyRrset: unsupported algorithm " + rrsig.algorithm);
|
|
260
|
+
if (dnskey.algorithm !== rrsig.algorithm) {
|
|
261
|
+
throw new DnssecError("dnssec/alg-mismatch", "dnssec.verifyRrset: DNSKEY algorithm does not match the RRSIG");
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
var typeNum = _typeNumber(opts.type);
|
|
265
|
+
if (NAME_FREE_TYPE_NUMS.indexOf(typeNum) === -1) {
|
|
266
|
+
throw new DnssecError("dnssec/uncanonicalizable-type",
|
|
267
|
+
"dnssec.verifyRrset: RR type " + typeNum + " carries embedded names; RDATA-name canonicalisation is not supported (refused, not mis-validated)");
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Validity window (fail closed on a bad opts.at).
|
|
271
|
+
var atMs;
|
|
272
|
+
if (opts.at !== undefined && opts.at !== null) {
|
|
273
|
+
if (!(opts.at instanceof Date) || !isFinite(opts.at.getTime())) {
|
|
274
|
+
throw new DnssecError("dnssec/bad-at", "dnssec.verifyRrset: opts.at must be a valid Date");
|
|
275
|
+
}
|
|
276
|
+
atMs = opts.at.getTime();
|
|
277
|
+
} else {
|
|
278
|
+
atMs = Date.now();
|
|
279
|
+
}
|
|
280
|
+
var nowSec = Math.floor(atMs / 1000); // allow:raw-time-literal — ms→NumericDate seconds (RRSIG inception/expiration are seconds since epoch, RFC 4034 §3.1.5)
|
|
281
|
+
if (nowSec < (rrsig.inception >>> 0)) throw new DnssecError("dnssec/not-yet-valid", "dnssec.verifyRrset: RRSIG inception is in the future");
|
|
282
|
+
if (nowSec > (rrsig.expiration >>> 0)) throw new DnssecError("dnssec/expired", "dnssec.verifyRrset: RRSIG has expired");
|
|
283
|
+
|
|
284
|
+
var klass = typeof opts.class === "number" ? opts.class : 1;
|
|
285
|
+
var ownerWire = _canonicalName(opts.name);
|
|
286
|
+
var ttl = _u32(rrsig.originalTtl);
|
|
287
|
+
|
|
288
|
+
// Canonical RRset (RFC 4034 §6.3): order records by canonical RDATA.
|
|
289
|
+
var rdatas = opts.rdatas.map(function (r, i) { return _bytes(r, "rdatas[" + i + "]"); });
|
|
290
|
+
var sorted = rdatas.slice().sort(Buffer.compare);
|
|
291
|
+
var rrParts = [];
|
|
292
|
+
for (var i = 0; i < sorted.length; i++) {
|
|
293
|
+
rrParts.push(ownerWire, _u16(typeNum), _u16(klass), ttl, _u16(sorted[i].length), sorted[i]);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// RRSIG RDATA without the signature (RFC 4034 §3.1.8.1).
|
|
297
|
+
var rrsigPrefix = Buffer.concat([
|
|
298
|
+
_u16(typeNum), Buffer.from([rrsig.algorithm & 0xff, rrsig.labels & 0xff]), // allow:raw-byte-literal — single-octet alg + labels fields
|
|
299
|
+
_u32(rrsig.originalTtl), _u32(rrsig.expiration), _u32(rrsig.inception),
|
|
300
|
+
_u16(rrsig.keyTag), _canonicalName(rrsig.signerName),
|
|
301
|
+
]);
|
|
302
|
+
var signedData = Buffer.concat([rrsigPrefix].concat(rrParts));
|
|
303
|
+
|
|
304
|
+
var key = _dnskeyToKey(dnskey.algorithm, dnskey.publicKey);
|
|
305
|
+
var signature = _bytes(rrsig.signature, "rrsig.signature");
|
|
306
|
+
var ok;
|
|
307
|
+
try {
|
|
308
|
+
if (alg.kind === "okp") {
|
|
309
|
+
ok = nodeCrypto.verify(null, signedData, key, signature);
|
|
310
|
+
} else if (alg.kind === "ec") {
|
|
311
|
+
ok = nodeCrypto.verify(alg.hash, signedData, { key: key, dsaEncoding: "ieee-p1363" }, signature);
|
|
312
|
+
} else {
|
|
313
|
+
ok = nodeCrypto.verify(alg.hash, signedData, key, signature);
|
|
314
|
+
}
|
|
315
|
+
} catch (e) {
|
|
316
|
+
throw new DnssecError("dnssec/verify-threw", "dnssec.verifyRrset: signature verification threw: " + ((e && e.message) || e));
|
|
317
|
+
}
|
|
318
|
+
if (!ok) throw new DnssecError("dnssec/bad-signature", "dnssec.verifyRrset: RRSIG signature did not verify");
|
|
319
|
+
return { ok: true, algorithm: alg.name, keyTag: rrsig.keyTag, signerName: rrsig.signerName };
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
module.exports = {
|
|
323
|
+
verifyRrset: verifyRrset,
|
|
324
|
+
verifyDs: verifyDs,
|
|
325
|
+
keyTag: keyTag,
|
|
326
|
+
ALGORITHMS: ALGS,
|
|
327
|
+
DnssecError: DnssecError,
|
|
328
|
+
};
|
package/lib/network.js
CHANGED
|
@@ -35,6 +35,7 @@ var ntpCheck = require("./ntp-check");
|
|
|
35
35
|
var nts = require("./network-nts");
|
|
36
36
|
var networkDns = require("./network-dns");
|
|
37
37
|
networkDns.resolver = require("./network-dns-resolver");
|
|
38
|
+
networkDns.dnssec = require("./network-dnssec");
|
|
38
39
|
var networkProxy = require("./network-proxy");
|
|
39
40
|
var networkTls = require("./network-tls");
|
|
40
41
|
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:230d6fb7-7a35-40e7-990b-d931f5a43248",
|
|
6
6
|
"version": 1,
|
|
7
7
|
"metadata": {
|
|
8
|
-
"timestamp": "2026-05-
|
|
8
|
+
"timestamp": "2026-05-25T12:11:44.437Z",
|
|
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.48",
|
|
23
23
|
"type": "application",
|
|
24
24
|
"name": "blamejs",
|
|
25
|
-
"version": "0.12.
|
|
25
|
+
"version": "0.12.48",
|
|
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.48",
|
|
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.48",
|
|
58
58
|
"dependsOn": []
|
|
59
59
|
}
|
|
60
60
|
]
|