dnsfence
v0.1.0
Published
dns over https with dnssec validation, zero deps
Maintainers
Readme
dnsfence
dns over https that checks the signatures itself. zero deps.
npm i dnsfencewhy
every javascript dns library stops at parsing. dns-packet does 17M
downloads a week and reads the wire format perfectly — and does no dnssec at
all. so the answer you get is whatever the resolver said, and the AD bit
you might check is just a claim made by the party you are trying not to have
to trust.
on a hostile network, or against a resolver you did not choose, that is the whole problem.
this validates the RRSIG chain locally with webcrypto.
use
import { resolve, resolveSecure } from 'dnsfence'
const r = await resolve('cloudflare.com', 'A')
r.answers // [{ name, type: 'A', ttl, data: '104.16.132.229' }]
r.security // 'secure' | 'insecure' | 'bogus'
r.reason // why, when it is not secure
r.resolverSaidAuthenticated // what the resolver claimed, for comparisonsecurity means:
| | |
|---|---|
| secure | signatures were present and this library verified them |
| insecure | the zone is not signed, so there was nothing to check |
| bogus | signatures were present and did not verify |
when you want it to be an error instead:
await resolveSecure('cloudflare.com', 'A') // throws unless securewhat it validates
RRSIGover the answer rrset, rebuilt in canonical form — records sorted by their encoded bytes, owner names lowercased, and the original ttl put back rather than the decremented one in the packet. getting that last part wrong is the usual reason a hand rolled validator never verifies anything.- the validity window, checked before any crypto runs.
- the key tag, trying every candidate key, because the tag is a checksum and two keys can share one.
algorithms: ECDSA P-256 / P-384, Ed25519, RSA/SHA-256, RSA/SHA-512.
SHA-1 based algorithms (5, 7) and SHA-1 DS digests are refused, not
trusted. they are still published, and they are not worth believing.
it does not trust the packet
a dns response is remote input, and message compression makes it easy to walk
off the end of one. all of these throw a typed DnsError:
| input | result |
|---|---|
| a compression pointer that points at itself | COMPRESSION_LOOP |
| a pointer outside the message | MALFORMED |
| a label or name past the length limits | NAME_TOO_LONG |
| a record body running past the end | TRUNCATED |
| a TXT chunk longer than its record | MALFORMED |
correctness
28 unit tests, plus live tests against real resolvers (DNSFENCE_LIVE=1).
the one that matters is the tamper test: it fetches a genuinely signed answer
for cloudflare.com, confirms it validates, then flips one byte of the
signature and replays it. if the result were still secure, nothing was
ever being verified. it comes back bogus.
the live suite also checks that github.com — which really is unsigned —
reports insecure rather than being quietly waved through, and that the same
answers validate through a second resolver, so nothing is cloudflare specific.
options
resolve(name, type, {
url: 'https://dns.google/dns-query', // cloudflare by default
validate: true, // false to just parse
dnssec: true, // false to not even ask
timeoutMs: 10000,
fetch: myFetch
})types: A AAAA CNAME MX TXT NS SOA SRV DS DNSKEY RRSIG CAA, or a number.
runtime
fetch and crypto.subtle, so node 20+, workers, deno or bun. no udp, no
native module, no dependencies.
license
MIT
