A pure-Zig DNS library: a strict wire-format codec (RFC 1035), DNSSEC validation of responses (RFC 4033–4035, RFC 5155) with chain-of-trust from a configured trust anchor, and a validating stub client that queries over DNS-over-HTTPS (RFC 8484).
|
Warning
|
Experimental. Not yet independently reviewed. Do not rely on this validator for any security decision until it has had an independent review by someone who did not write it. What has and has not been verified is set out in the verification report; the known gaps and the trust-anchor assumptions are in the threat model. |
It is not a recursive resolver: it asks an upstream resolver (with CD=1) and
authenticates what comes back itself, fetching the DS and DNSKEY records it needs
through the same transport.
-
Wire codec — bounds-checked decoding that rejects malformed messages (compression pointers must point strictly backwards; names in RDATA are decompressed only where RFC 3597 permits; trailing data, misplaced OPT and meta-types are rejected), and an encoder with name compression.
-
Signatures — RSASHA1, RSASHA1-NSEC3-SHA1, RSASHA256, RSASHA512, ECDSAP256SHA256, ECDSAP384SHA384 and ED25519, all through Zig’s standard library; DS digests SHA-1, SHA-256 and SHA-384. No cryptographic primitive is implemented here.
-
Chain of trust — DS → DNSKEY → RRSIG from the signer up to the closest trust anchor, RRSIG validity windows in RFC 1982 serial arithmetic, CNAME and DNAME chains, wildcard expansion.
-
Denial of existence — NSEC and NSEC3 proofs for NXDOMAIN, NODATA, wildcards and insecure delegations, including Opt-Out and the RFC 6840 restrictions.
-
Verdicts that cannot be downgraded silently — the result is
secure | insecure | bogus | indeterminate. Answer data is reachable only from thesecurearm, or from theinsecurearm together with the reason and the zone at which insecurity was proven. Every failure isbogusorindeterminate. -
Stub client — DoH over TLS 1.2/1.3 with certificate verification, optional HTTP CONNECT proxy (TLS end to end), RFC 8467 query padding.
Requires Zig 0.17.0.
const dnssec = @import("dnssec");
var anchors = try dnssec.sec.anchors.TrustAnchors.ianaRoot(gpa);
defer anchors.deinit();
var bundle: std.crypto.Certificate.Bundle = .empty;
try bundle.rescan(gpa, io, std.Io.Clock.real.now(io));
var doh = try dnssec.stub.doh.DohTransport.init(io, "https://dns.example/dns-query", &bundle, null);
var stub: dnssec.stub.client.StubClient = .{ .transport = doh.transport(), .anchors = &anchors };
var result = try stub.resolve(gpa, try dnssec.wire.Name.fromText("example.org."), .aaaa, now);
defer result.deinit();
switch (result.verdict) {
.secure => |answer| use(answer.rrsets),
.insecure => |i| decideWhetherUnsignedIsAcceptable(i.reason, i.zone, i.answer),
.bogus, .indeterminate => |failure| reject(failure.reason),
}StubClient.resolveSecure returns an error for anything that is not secure.
The command-line tool prints the verdict:
$ zig build -Doptimize=ReleaseSafe
$ ./zig-out/bin/dnssec-stub --doh https://cloudflare-dns.com/dns-query example.com. A
verdict=secure reason=- at=- rcode=noerror
example.com. 260 IN A 104.20.23.154
...
$ ./zig-out/bin/dnssec-stub --doh https://cloudflare-dns.com/dns-query dnssec-failed.org. A
verdict=bogus reason=dnskey_not_authenticated at=dnssec-failed.org. type=dnskeyzig build ffi builds libzig_dnssec (static and shared, pure Zig, no libc) and
installs zig_dnssec.h. The header is the contract: a verdict whose security is never
SECURE unless the call succeeded, and answer text only for SECURE and INSECURE.
The caller supplies the transport as a callback. An Idris2 model of the interface in
src/interface/Abi/ states the same rule as a type. See
src/interface/README.adoc.
Say this to any AI assistant that can read a URL and run commands (or give you commands to paste):
Set up zig-dnssec from https://github.com/hyperpolymath/zig-dnssecThe URL leads the assistant to
docs/AI_INSTALLATION_GUIDE.adoc. It clones the
repository, installs the toolchain pinned in mise.lock (Zig 0.17.0) with just setup,
checks the result with just doctor, and runs just test.
./launcher.sh --ai-setup (or just ai-setup) prints the same line.
|
Important
|
Privacy: setting up clones this repository and installs developer tools per user; nothing
is sent anywhere. Running the tests needs no network. Only the live examples above, and
|
zig build test # unit, RFC vector, denial, replay, fuzz smoke and C ABI tests
idris2 --typecheck src/interface/abi.ipkg # proofs of the Idris2 ABI model
just abi-e2e # Idris2 -> C ABI -> Zig end to end
scripts/theory-receipts.sh # chain-of-trust theory: Agda, Rocq and Lean, with receipts
zig build fuzz --fuzz=500K -Doptimize=ReleaseSafe # coverage-guided fuzzing
zig build fuzz-tool -Doptimize=ReleaseSafe && ./zig-out/bin/dnssec-fuzz all 60
tests/crosscheck/make-zones.sh && tests/crosscheck/servers.sh start && tests/crosscheck/run.shWhat is proved, tested or only argued, and how strongly, is the proof ledger in EXPLAINME.adoc: 8 proven of 18 rows; 6 tested-only, 4 argued, 0 open. The theory needs Agda 2.6.3, Coq 8.18 and Lean 4.15; versions and receipts are in docs/status/PROOF-STATUS.adoc.
The cross-check needs NSD, Unbound, ldns tools and haproxy; build/container/Containerfile
provides them. Its latest output is tests/crosscheck/REPORT.adoc.