A zone digest is a cryptographic digest, or hash, of the data in a DNS zone which is embedded in the zone data itself as a ZONEMD resource record. It is computed upon publishing the zone, and it can be verified by zone recipients. ZONEMD is specified in RFC 8976.

In order to compute the hash, zone data is fed to a digest function using a well-defined and consistent record ordering and format. The data given to the hash excludes the ZONEMD record itself and its signatures if the zone is signed. The digest is then added to the apex of the zone itself and signed with DNSSEC (for signed zones).

Let’s see an example at work (with my new favorite TXT record in it):

$ORIGIN example.aa.
$TTL 3600
@    SOA   ns root 4 3H 1H 1W 1H
     NS    ns
     TXT   "DNS is innocent"
ns   A     127.0.0.1

I use ldns-zone-digest to create the digest (hash) and add the ZONEMD record to the zone:

$ ldns-zone-digest -c  -p 1,1 -o example.aa.digest example.aa ../example.aa
Loading Zone...4 records
Remove existing ZONEMD RRset
Add placeholder ZONEMD with scheme 1 and hash algorithm 1

$ cat example.aa.digest
example.aa.	3600	IN	NS	ns.example.aa.
example.aa.	3600	IN	SOA	ns.example.aa. root.example.aa. 4 10800 3600 604800 3600
example.aa.	3600	IN	TXT	"DNS is innocent"
example.aa.	3600	IN	ZONEMD	4 1 1 83dbb84f8b78e9bea8badede9316fb238f5c923440def32534aa147298d0912752aaf9b287823df1d1b737e43e71396d
ns.example.aa.	3600	IN	A	127.0.0.1

$ named-checkzone -q -F text -s relative -o - example.aa example.aa.digest
$ORIGIN .
$TTL 3600	; 1 hour
example.aa		IN SOA	ns.example.aa. root.example.aa. (
				4          ; serial
				10800      ; refresh (3 hours)
				3600       ; retry (1 hour)
				604800     ; expire (1 week)
				3600       ; minimum (1 hour)
				)
			NS	ns.example.aa.
			TXT	"DNS is innocent"
			ZONEMD	4 1 1 (
				83DBB84F8B78E9BEA8BADEDE9316FB238F5C923440DE
				F32534AA147298D0912752AAF9B287823DF1D1B737E4
				3E71396D )
$ORIGIN example.aa.
ns			A	127.0.0.1

As is to be expected, even if I change the order of the records in the input, the digest remains identical, as it should.

For a signed zone, I use ldns-zone-digest ... -z <ZSK.private> so that the utility can compute and add the RRSIG to the ZONEMD record.

The rdata of the ZONEMD record contains the following fields:

  1. serial, which must match the SOA serial of the zone the ZONEMD is being added to
  2. scheme, wich currently is 1 (SIMPLE)
  3. hash algorithm, 1 for SHA-384 and 2 for SHA-512
  4. digest field with 48 octets for SHA-384 and 64 octets for SHA-512

Another offline utility I can use is dns-tools; a single binary program written in Golang which has different functions. The one to create a ZONEMD record in a zone is:

$ dns-tools digest -f example.aa -o example.aa.digest -d 1 -z example.aa
[dns-tools] 2023/04/16 10:56:42 Using config file: /tmp/dns-tools/dns-tools-config.json
[dns-tools] 2023/04/16 10:56:42 Reading and parsing zone example.aa (updateSerial=false)
[dns-tools] 2023/04/16 10:56:42 Sorting zone
[dns-tools] 2023/04/16 10:56:42 Zone Sorted
[dns-tools] 2023/04/16 10:56:42 Updating ZONEMD Digest
[dns-tools] 2023/04/16 10:56:42 Started digest calculation.
[dns-tools] 2023/04/16 10:56:42 Stopped digest calculation.
[dns-tools] 2023/04/16 10:56:42 Writing zone
[dns-tools] 2023/04/16 10:56:42 Zone written
[dns-tools] 2023/04/16 10:56:42 zone digested successfully in example.aa.digest.

$ cat example.aa.digest
example.aa.	3600	IN	SOA	ns.example.aa. root.example.aa. 4 10800 3600 604800 3600
example.aa.	3600	IN	NS	ns.example.aa.
example.aa.	3600	IN	TXT	"DNS is innocent"
ns.example.aa.	3600	IN	A	127.0.0.1
example.aa.	3600	IN	ZONEMD	4 1 1 83dbb84f8b78e9bea8badede9316fb238f5c923440def32534aa147298d0912752aaf9b287823df1d1b737e43e71396d

Knot-DNS can add the digest automatically, if I configure the zone appropriately:

zone:
  - domain: example.aa
    template: cmember
    zonemd-generate: zonemd-sha384

Note how the digest equals our examples above as the zone contains the same data and has the same SOA serial number.

$ dig @192.168.1.170 +noall +answer +onesoa example.aa AXFR +multi
example.aa.		3600 IN	SOA ns.example.aa. root.example.aa. (
				4          ; serial
				10800      ; refresh (3 hours)
				3600       ; retry (1 hour)
				604800     ; expire (1 week)
				3600       ; minimum (1 hour)
				)
example.aa.		3600 IN	NS ns.example.aa.
example.aa.		3600 IN	TXT "DNS is innocent"
example.aa.		3600 IN	ZONEMD 4 1 1 (
				83DBB84F8B78E9BEA8BADEDE9316FB238F5C923440DE
				F32534AA147298D0912752AAF9B287823DF1D1B737E4
				3E71396D )
ns.example.aa.		3600 IN	A 127.0.0.1

Verification

Unbound has supported ZONEMD for some time now, and I configure an authoritative zone as follows:

server:
     verbosity: 3

auth-zone:
        name: "example.aa"
        primary: 192.168.1.170@5354
        zonemd-check: yes
        zonemd-reject-absence: yes
        zonefile: "example.aa"

When I reload the server, I can follow the digest verification in the log file:

[1681636411] unbound[47600:0] debug: auth-zone example.aa. ZONEMD hash is correct
[1681636411] unbound[47600:0] debug: auth zone example.aa. ZONEMD verification successful

If I configure Unbound to load the zone from a file, and I change the SOA serial in the file without recomputing ZONEMD, Unbound warns upon loading the zone:

[1681636681] unbound[47687:0] debug: auth-zone example.aa. ZONEMD failed: ZONEMD serial is wrong
[1681636681] unbound[47687:0] warning: auth zone example.aa.: ZONEMD verification failed: ZONEMD serial is wrong

PowerDNS has support for ZONEMD in pdnsutil and in validating ZONEMD in the PowerDNS Recursor in the zoneToCache function.

NSD parses ZONEMD since release 4.3.4, and BIND parses the record, but cannot as yet produce the record.

ldns has support for ZONEMD in both ldns-signzone and ldns-verify-zone:

$ ldns-keygen -a13 -k example.aa
Kexample.aa.+013+31040

$ ldns-signzone -z 1:1 example.aa Kexample.aa.+013+31040

$ ldns-verify-zone -ZZ example.aa.signed   # Requires a valid signed ZONEMD RR
Zone is verified and complete

$ ldns-zone-digest -v example.aa example.aa.signed
Loading Zone...16 records
Found and calculated digests for scheme:hashalg 1:1 do MATCH.

There are ongoing tests in adding ZONEMD to the root zone itself.

ZONEMD protects zone data “at rest” and is useful when transferring data between primaries and secondaries. (I think of it as being like a checksum for a zone which is contained in the zone.) Not only in cases in which servers AXFR the zone but also when zones are distributed outside of the DNS, as is the case, for instance, with the DNS root, published via the Web or on FTP. In all cases the integrity of the zone can be verified after downloading the zone. ZONEMD doesn’t provide origin authenticity; DNSSEC is required for that.

Otto makes a good point:

it is also interesting to note that glue records in a zone do note have DNSSEC signatures, but are covered by the ZONEMD record. So the signature of a ZONEMD record does cover glue records in an indirect way.

Further reading