Repository: miekg/dns
Pull request #1208 (2021-01-22 13:01:06)

Implementation of zone digest (ZONEMD)
https://tools.ietf.org/html/draft-ietf-dnsop-dns-zone-digest-14

miekg: ah nice. Can you clean up this PR and remove any non go files? Also revert the go.mod updates
miekg: also needs a bit of tests
miekg: think all requested changes have been implemented.
miekg: wait, where are the tests?
eriverosr: I've just added a test that checks if the library is able to parse the example ZONEMD RRs from [RFC 8976](https://tools.ietf.org/html/rfc8976). Tell us if you can think of any other type of tests to add.

Patch:
diff --git a/scan_rr.go b/scan_rr.go
index 1f2392a5..a19ba4ae 100644
--- a/scan_rr.go
+++ b/scan_rr.go
@@ -846,6 +846,38 @@ func (rr *CSYNC) parse(c *zlexer, o string) *ParseError {
 	return nil
 }
 
+func (rr *ZONEMD) parse(c *zlexer, o string) *ParseError {
+	l, _ := c.Next()
+	i, e := strconv.ParseUint(l.token, 10, 32)
+	if e != nil || l.err {
+		return &ParseError{"", "bad ZONEMD Serial", l}
+	}
+	rr.Serial = uint32(i)
+
+	c.Next() // zBlank
+	l, _ = c.Next()
+	i, e1 := strconv.ParseUint(l.token, 10, 8)
+	if e1 != nil || l.err {
+		return &ParseError{"", "bad ZONEMD Scheme", l}
+	}
+	rr.Scheme = uint8(i)
+
+	c.Next() // zBlank
+	l, _ = c.Next()
+	i, err := strconv.ParseUint(l.token, 10, 8); 
+	if err != nil || l.err {
+		return &ParseError{"", "bad ZONEMD Hash Algorithm", l}
+	}
+	rr.Hash = uint8(i)
+
+	s, e2 := endingToString(c, "bad ZONEMD Digest")
+	if e2 != nil {
+		return e2
+	}
+	rr.Digest = s
+	return nil
+}
+
 func (rr *SIG) parse(c *zlexer, o string) *ParseError { return rr.RRSIG.parse(c, o) }
 
 func (rr *RRSIG) parse(c *zlexer, o string) *ParseError {
diff --git a/types.go b/types.go
index 2ed2720a..99dd315b 100644
--- a/types.go
+++ b/types.go
@@ -81,6 +81,7 @@ const (
 	TypeCDNSKEY    uint16 = 60
 	TypeOPENPGPKEY uint16 = 61
 	TypeCSYNC      uint16 = 62
+	TypeZONEMD     uint16 = 63
 	TypeSVCB       uint16 = 64
 	TypeHTTPS      uint16 = 65
 	TypeSPF        uint16 = 99
@@ -150,6 +151,17 @@ const (
 	OpcodeUpdate = 5
 )
 
+// Used in ZONEMD https://tools.ietf.org/html/rfc8976
+
+const (
+	// ZoneMD Accepted Schemes
+	ZoneMDSchemeSimple = 1
+
+	// ZoneMD Hash Algorithms
+	ZoneMDHashAlgSHA384 = 1
+	ZoneMDHashAlgSHA512 = 2
+)
+
 // Header is the wire format for the DNS packet header.
 type Header struct {
 	Id                                 uint16
@@ -1361,6 +1373,23 @@ func (rr *CSYNC) len(off int, compression map[string]struct{}) int {
 	return l
 }
 
+// ZONEMD RR, from draft-ietf-dnsop-dns-zone-digest
+type ZONEMD struct {
+	Hdr    RR_Header
+	Serial uint32
+	Scheme uint8
+	Hash   uint8
+	Digest string `dns:"hex"`
+}
+
+func (rr *ZONEMD) String() string {
+	return rr.Hdr.String() +
+		strconv.Itoa(int(rr.Serial)) +
+		" " + strconv.Itoa(int(rr.Scheme)) +
+		" " + strconv.Itoa(int(rr.Hash)) +
+		" " + rr.Digest
+}
+
 // APL RR. See RFC 3123.
 type APL struct {
 	Hdr      RR_Header
diff --git a/zduplicate.go b/zduplicate.go
index 0d3b34bd..9eb1dac2 100644
--- a/zduplicate.go
+++ b/zduplicate.go
@@ -1317,3 +1317,24 @@ func (r1 *X25) isDuplicate(_r2 RR) bool {
 	}
 	return true
 }
+
+func (r1 *ZONEMD) isDuplicate(_r2 RR) bool {
+	r2, ok := _r2.(*ZONEMD)
+	if !ok {
+		return false
+	}
+	_ = r2
+	if r1.Serial != r2.Serial {
+		return false
+	}
+	if r1.Scheme != r2.Scheme {
+		return false
+	}
+	if r1.Hash != r2.Hash {
+		return false
+	}
+	if r1.Digest != r2.Digest {
+		return false
+	}
+	return true
+}
diff --git a/zmsg.go b/zmsg.go
index d24a10fa..fc0822f9 100644
--- a/zmsg.go
+++ b/zmsg.go
@@ -1118,6 +1118,26 @@ func (rr *X25) pack(msg []byte, off int, compression compressionMap, compress bo
 	return off, nil
 }
 
+func (rr *ZONEMD) pack(msg []byte, off int, compression compressionMap, compress bool) (off1 int, err error) {
+	off, err = packUint32(rr.Serial, msg, off)
+	if err != nil {
+		return off, err
+	}
+	off, err = packUint8(rr.Scheme, msg, off)
+	if err != nil {
+		return off, err
+	}
+	off, err = packUint8(rr.Hash, msg, off)
+	if err != nil {
+		return off, err
+	}
+	off, err = packStringHex(rr.Digest, msg, off)
+	if err != nil {
+		return off, err
+	}
+	return off, nil
+}
+
 // unpack*() functions
 
 func (rr *A) unpack(msg []byte, off int) (off1 int, err error) {
@@ -2821,3 +2841,35 @@ func (rr *X25) unpack(msg []byte, off int) (off1 int, err error) {
 	}
 	return off, nil
 }
+
+func (rr *ZONEMD) unpack(msg []byte, off int) (off1 int, err error) {
+	rdStart := off
+	_ = rdStart
+
+	rr.Serial, off, err = unpackUint32(msg, off)
+	if err != nil {
+		return off, err
+	}
+	if off == len(msg) {
+		return off, nil
+	}
+	rr.Scheme, off, err = unpackUint8(msg, off)
+	if err != nil {
+		return off, err
+	}
+	if off == len(msg) {
+		return off, nil
+	}
+	rr.Hash, off, err = unpackUint8(msg, off)
+	if err != nil {
+		return off, err
+	}
+	if off == len(msg) {
+		return off, nil
+	}
+	rr.Digest, off, err = unpackStringHex(msg, off, rdStart+int(rr.Hdr.Rdlength))
+	if err != nil {
+		return off, err
+	}
+	return off, nil
+}
diff --git a/ztypes.go b/ztypes.go
index 11b51bf2..5d060cfe 100644
--- a/ztypes.go
+++ b/ztypes.go
@@ -82,6 +82,7 @@ var TypeToRR = map[uint16]func() RR{
 	TypeUINFO:      func() RR { return new(UINFO) },
 	TypeURI:        func() RR { return new(URI) },
 	TypeX25:        func() RR { return new(X25) },
+	TypeZONEMD:     func() RR { return new(ZONEMD) },
 }
 
 // TypeToString is a map of strings for each RR type.
@@ -168,6 +169,7 @@ var TypeToString = map[uint16]string{
 	TypeUNSPEC:     "UNSPEC",
 	TypeURI:        "URI",
 	TypeX25:        "X25",
+	TypeZONEMD:     "ZONEMD",
 	TypeNSAPPTR:    "NSAP-PTR",
 }
 
@@ -245,6 +247,7 @@ func (rr *UID) Header() *RR_Header        { return &rr.Hdr }
 func (rr *UINFO) Header() *RR_Header      { return &rr.Hdr }
 func (rr *URI) Header() *RR_Header        { return &rr.Hdr }
 func (rr *X25) Header() *RR_Header        { return &rr.Hdr }
+func (rr *ZONEMD) Header() *RR_Header     { return &rr.Hdr }
 
 // len() functions
 func (rr *A) len(off int, compression map[string]struct{}) int {
@@ -684,6 +687,14 @@ func (rr *X25) len(off int, compression map[string]struct{}) int {
 	l += len(rr.PSDNAddress) + 1
 	return l
 }
+func (rr *ZONEMD) len(off int, compression map[string]struct{}) int {
+	l := rr.Hdr.len(off, compression)
+	l += 4 // Serial
+	l++    // Scheme
+	l++    // Hash
+	l += len(rr.Digest) / 2
+	return l
+}
 
 // copy() functions
 func (rr *A) copy() RR {
@@ -936,3 +947,6 @@ func (rr *URI) copy() RR {
 func (rr *X25) copy() RR {
 	return &X25{rr.Hdr, rr.PSDNAddress}
 }
+func (rr *ZONEMD) copy() RR {
+	return &ZONEMD{rr.Hdr, rr.Serial, rr.Scheme, rr.Hash, rr.Digest}
+}


Test patch:
diff --git a/parse_test.go b/parse_test.go
index 6ca007d6..1d5bf6f9 100644
--- a/parse_test.go
+++ b/parse_test.go
@@ -1921,3 +1921,74 @@ func TestUnpackRRWithHeaderInvalidLengths(t *testing.T) {
 		}
 	}
 }
+
+func TestParseZONEMD(t *testing.T) {
+	// Uses examples from https://tools.ietf.org/html/rfc8976
+	dt := map[string]string{
+		// Simple Zone
+		`example.	86400	IN	ZONEMD	2018031900 1 1 (
+										c68090d90a7aed71
+										6bc459f9340e3d7c
+										1370d4d24b7e2fc3
+										a1ddc0b9a87153b9
+										a9713b3c9ae5cc27
+										777f98b8e730044c )
+		`: "example.\t86400\tIN\tZONEMD\t2018031900 1 1 c68090d90a7aed716bc459f9340e3d7c1370d4d24b7e2fc3a1ddc0b9a87153b9a9713b3c9ae5cc27777f98b8e730044c",
+		// Complex Zone
+		`example.	86400	IN	ZONEMD	2018031900 1 1 (
+										a3b69bad980a3504
+										e1cffcb0fd6397f9
+										3848071c93151f55
+										2ae2f6b1711d4bd2
+										d8b39808226d7b9d
+										b71e34b72077f8fe )
+		`: "example.\t86400\tIN\tZONEMD\t2018031900 1 1 a3b69bad980a3504e1cffcb0fd6397f93848071c93151f552ae2f6b1711d4bd2d8b39808226d7b9db71e34b72077f8fe",
+		// Multiple Digests Zone
+		`example.	86400	IN	ZONEMD	2018031900 1 1 (
+										62e6cf51b02e54b9
+										b5f967d547ce4313
+										6792901f9f88e637
+										493daaf401c92c27
+										9dd10f0edb1c56f8
+										080211f8480ee306 )
+		`: "example.\t86400\tIN\tZONEMD\t2018031900 1 1 62e6cf51b02e54b9b5f967d547ce43136792901f9f88e637493daaf401c92c279dd10f0edb1c56f8080211f8480ee306",
+		`example.	86400	IN	ZONEMD	2018031900 1 2 (
+										08cfa1115c7b948c
+										4163a901270395ea
+										226a930cd2cbcf2f
+										a9a5e6eb85f37c8a
+										4e114d884e66f176
+										eab121cb02db7d65
+										2e0cc4827e7a3204
+										f166b47e5613fd27 )
+		`: "example.\t86400\tIN\tZONEMD\t2018031900 1 2 08cfa1115c7b948c4163a901270395ea226a930cd2cbcf2fa9a5e6eb85f37c8a4e114d884e66f176eab121cb02db7d652e0cc4827e7a3204f166b47e5613fd27",
+		`example.	86400	IN	ZONEMD	2018031900 1 240 (
+										e2d523f654b9422a
+										96c5a8f44607bbee )
+		`: "example.	86400	IN	ZONEMD	2018031900 1 240 e2d523f654b9422a96c5a8f44607bbee",
+		`example.	86400	IN	ZONEMD	2018031900 241 1 (
+										e1846540e33a9e41
+										89792d18d5d131f6
+										05fc283e )
+		`: "example.	86400	IN	ZONEMD	2018031900 241 1 e1846540e33a9e4189792d18d5d131f605fc283e",
+		// URI.ARPA zone
+		`uri.arpa.		3600	IN		ZONEMD	2018100702 1 1 (
+			0dbc3c4dbfd75777c12ca19c337854b1577799901307c482e9d91d5d15
+			cd934d16319d98e30c4201cf25a1d5a0254960 )`: "uri.arpa.\t3600\tIN\tZONEMD\t2018100702 1 1 0dbc3c4dbfd75777c12ca19c337854b1577799901307c482e9d91d5d15cd934d16319d98e30c4201cf25a1d5a0254960",
+		// ROOT-SERVERS.NET Zone
+		`root-servers.net.     3600000 IN  ZONEMD  2018091100 1 1 (
+			f1ca0ccd91bd5573d9f431c00ee0101b2545c97602be0a97
+			8a3b11dbfc1c776d5b3e86ae3d973d6b5349ba7f04340f79 )
+		`: "root-servers.net.\t3600000\tIN\tZONEMD\t2018091100 1 1 f1ca0ccd91bd5573d9f431c00ee0101b2545c97602be0a978a3b11dbfc1c776d5b3e86ae3d973d6b5349ba7f04340f79",
+	}
+	for i, o := range dt {
+		rr, err := NewRR(i)
+		if err != nil {
+			t.Error("failed to parse RR: ", err)
+			continue
+		}
+		if rr.String() != o {
+			t.Errorf("`%s' should be equal to\n`%s', but is     `%s'", i, o, rr.String())
+		}
+	}
+}