]> git.ipfire.org Git - thirdparty/dnspython.git/commitdiff
Update SVCB to the current spec. 659/head
authorBrian Wellington <bwelling@xbill.org>
Thu, 22 Apr 2021 00:22:40 +0000 (17:22 -0700)
committerBrian Wellington <bwelling@xbill.org>
Thu, 22 Apr 2021 00:22:40 +0000 (17:22 -0700)
dns/rdtypes/svcbbase.py
tests/example
tests/example1.good
tests/example2.good
tests/example3.good
tests/svcb_test_vectors.generic [new file with mode: 0644]
tests/svcb_test_vectors.text [new file with mode: 0644]
tests/test_svcb.py

index 80e67e0a257015b5808f6bc26c9adc9547d63951..49f35fee141e8bf453bf65f42544b6c8653fff95 100644 (file)
@@ -32,7 +32,7 @@ class ParamKey(dns.enum.IntEnum):
     NO_DEFAULT_ALPN = 2
     PORT = 3
     IPV4HINT = 4
-    ECHCONFIG = 5
+    ECH = 5
     IPV6HINT = 6
 
     @classmethod
@@ -91,22 +91,15 @@ def _escapify(qstring):
             text += '\\%03d' % c
     return text
 
-def _unescape(value, list_mode=False):
+def _unescape(value):
     if value == '':
         return value
-    items = []
     unescaped = b''
     l = len(value)
     i = 0
     while i < l:
         c = value[i]
         i += 1
-        if c == ',' and list_mode:
-            if len(unescaped) == 0:
-                raise ValueError('list item cannot be empty')
-            items.append(unescaped)
-            unescaped = b''
-            continue
         if c == '\\':
             if i >= l:  # pragma: no cover   (can't happen via tokenizer get())
                 raise dns.exception.UnexpectedEnd
@@ -126,20 +119,33 @@ def _unescape(value, list_mode=False):
                 codepoint = int(c) * 100 + int(c2) * 10 + int(c3)
                 if codepoint > 255:
                     raise dns.exception.SyntaxError
-                c = chr(codepoint)
+                unescaped += b'%c' % (codepoint)
+                continue
         unescaped += c.encode()
-    if len(unescaped) > 0:
-        items.append(unescaped)
-    else:
-        # This can't happen outside of list_mode because that would
-        # require the value parameter to the function to be empty, but
-        # we special case that at the beginning.
-        assert list_mode
-        raise ValueError('trailing comma')
-    if list_mode:
-        return items
-    else:
-        return items[0]
+    return unescaped
+
+
+def _split(value):
+    l = len(value)
+    i = 0
+    items = []
+    unescaped = b''
+    while i < l:
+        c = value[i]
+        i += 1
+        if c == ord('\\'):
+            if i >= l:  # pragma: no cover   (can't happen via tokenizer get())
+                raise dns.exception.UnexpectedEnd
+            c = value[i]
+            i += 1
+            unescaped += b'%c' % (c)
+        elif c == ord(','):
+            items.append(unescaped)
+            unescaped = b''
+        else:
+            unescaped += b'%c' % (c)
+    items.append(unescaped)
+    return items
 
 
 @dns.immutable.immutable
@@ -170,7 +176,7 @@ class GenericParam(Param):
             return cls(_unescape(value))
 
     def to_text(self):
-        return '"' + _escapify(self.value) + '"'
+        return '"' + dns.rdata._escapify(self.value) + '"'
 
     @classmethod
     def from_wire_parser(cls, parser, origin=None):  # pylint: disable=W0613
@@ -231,10 +237,11 @@ class ALPNParam(Param):
 
     @classmethod
     def from_value(cls, value):
-        return cls(_unescape(value, True))
+        return cls(_split(_unescape(value)))
 
     def to_text(self):
-        return '"' + ','.join([_escapify(id) for id in self.ids]) + '"'
+        value = ','.join([_escapify(id) for id in self.ids])
+        return '"' + dns.rdata._escapify(value.encode()) + '"'
 
     @classmethod
     def from_wire_parser(cls, parser, origin=None):  # pylint: disable=W0613
@@ -357,19 +364,19 @@ class IPv6HintParam(Param):
 
 
 @dns.immutable.immutable
-class ECHConfigParam(Param):
-    def __init__(self, echconfig):
-        self.echconfig = dns.rdata.Rdata._as_bytes(echconfig, True)
+class ECHParam(Param):
+    def __init__(self, ech):
+        self.ech = dns.rdata.Rdata._as_bytes(ech, True)
 
     @classmethod
     def from_value(cls, value):
         if '\\' in value:
-            raise ValueError('escape in ECHConfig value')
+            raise ValueError('escape in ECH value')
         value = base64.b64decode(value.encode())
         return cls(value)
 
     def to_text(self):
-        b64 = base64.b64encode(self.echconfig).decode('ascii')
+        b64 = base64.b64encode(self.ech).decode('ascii')
         return f'"{b64}"'
 
     @classmethod
@@ -378,7 +385,7 @@ class ECHConfigParam(Param):
         return cls(value)
 
     def to_wire(self, file, origin=None):  # pylint: disable=W0613
-        file.write(self.echconfig)
+        file.write(self.ech)
 
 
 _class_for_key = {
@@ -387,7 +394,7 @@ _class_for_key = {
     ParamKey.NO_DEFAULT_ALPN: NoDefaultALPNParam,
     ParamKey.PORT: PortParam,
     ParamKey.IPV4HINT: IPv4HintParam,
-    ParamKey.ECHCONFIG: ECHConfigParam,
+    ParamKey.ECH: ECHParam,
     ParamKey.IPV6HINT: IPv6HintParam,
 }
 
@@ -436,8 +443,12 @@ class SVCBBase(dns.rdata.Rdata):
                 # Note we have to say "not in" as we have None as a value
                 # so a get() and a not None test would be wrong.
                 if key not in params:
-                    raise ValueError(f'key {key} declared mandatory but not'
+                    raise ValueError(f'key {key} declared mandatory but not '
                                      'present')
+        # The no-default-alpn parameter requires the alpn parameter.
+        if ParamKey.NO_DEFAULT_ALPN in params:
+            if ParamKey.ALPN not in params:
+                raise ValueError(f'no-default-alpn present, but alpn missing')
 
     def to_text(self, origin=None, relativize=True, **kw):
         target = self.target.choose_relativity(origin, relativize)
index 86af9dd8c12368bd6dca4afb4b77cf8bc21a5df7..745093344ed4ddfacdcb47e1a316ccf598d7d0e6 100644 (file)
@@ -242,7 +242,7 @@ zonemd03                ZONEMD 2018031900 1 240 e2d523f654b9422a 96c5a8f44607bbe
 zonemd04                ZONEMD 2018031900 241 1 e1846540e33a9e41 89792d18d5d131f6 05fc283e aaaaaaaa aaaaaaaaaaaaaaaaa aaaaaaaaaaaaaaaa aaaaaaaaaaaaaaa
 svcb01                  SVCB (
 100 foo.com. mandatory="alpn,port" alpn="h2,h3" no-default-alpn port="12345"
-echconfig="abcd" ipv4hint=1.2.3.4,4.3.2.1 ipv6hint=1::2,3::4 key12345="foo"
+ech="abcd" ipv4hint=1.2.3.4,4.3.2.1 ipv6hint=1::2,3::4 key12345="foo"
 )
 https01                 HTTPS 0 svc
-https02                 HTTPS 1 . port=8002 echconfig="abcd"
+https02                 HTTPS 1 . port=8002 ech="abcd"
index c1ddfd49b6160e57095e2a8c98eb5dab98649a00..2fb2d0bb3176e570489651a1a55d6361865ca209 100644 (file)
@@ -62,7 +62,7 @@ hip01 3600 IN HIP 2 200100107b1a74df365639cc39f1d578 AwEAAbdxyhNuSutc5EMzxTs9LBP
 hip02 3600 IN HIP 2 200100107b1a74df365639cc39f1d578 AwEAAbdxyhNuSutc5EMzxTs9LBPCIkOFH8cIvM4p9+LrV4e19WzK00+CI6zBCQTdtWsuxKbWIy87UOoJTwkUs7lBu+Upr1gsNrut79ryra+bSRGQb1slImA8YVJyuIDsj7kwzG7jnERNqnWxZ48AWkskmdHaVDP4BcelrTI3rMXdXF5D rvs.example.com.
 hip03 3600 IN HIP 2 200100107b1a74df365639cc39f1d578 AwEAAbdxyhNuSutc5EMzxTs9LBPCIkOFH8cIvM4p9+LrV4e19WzK00+CI6zBCQTdtWsuxKbWIy87UOoJTwkUs7lBu+Upr1gsNrut79ryra+bSRGQb1slImA8YVJyuIDsj7kwzG7jnERNqnWxZ48AWkskmdHaVDP4BcelrTI3rMXdXF5D rvs1.example.com. rvs2.example.com.
 https01 3600 IN HTTPS 0 svc
-https02 3600 IN HTTPS 1 . port="8002" echconfig="abcd"
+https02 3600 IN HTTPS 1 . port="8002" ech="abcd"
 ipseckey01 3600 IN IPSECKEY 10 1 2 192.0.2.38 AQNRU3mG7TVTO2BkR47usntb102uFJtu gbo6BSGvgqt4AQ==
 ipseckey02 3600 IN IPSECKEY 10 0 2 . AQNRU3mG7TVTO2BkR47usntb102uFJtu gbo6BSGvgqt4AQ==
 ipseckey03 3600 IN IPSECKEY 10 3 2 mygateway.example.com. AQNRU3mG7TVTO2BkR47usntb102uFJtu gbo6BSGvgqt4AQ==
@@ -116,7 +116,7 @@ spf 3600 IN SPF "v=spf1 mx -all"
 srv01 3600 IN SRV 0 0 0 .
 srv02 3600 IN SRV 65535 65535 65535 old-slow-box.example.com.
 sshfp1 3600 IN SSHFP 1 1 aa549bfe898489c02d1715d97d79c57ba2fa76ab
-svcb01 3600 IN SVCB 100 foo.com. mandatory="alpn,port" alpn="h2,h3" no-default-alpn port="12345" ipv4hint="1.2.3.4,4.3.2.1" echconfig="abcd" ipv6hint="1::2,3::4" key12345="foo"
+svcb01 3600 IN SVCB 100 foo.com. mandatory="alpn,port" alpn="h2,h3" no-default-alpn port="12345" ipv4hint="1.2.3.4,4.3.2.1" ech="abcd" ipv6hint="1::2,3::4" key12345="foo"
 t 301 IN A 73.80.65.49
 tlsa1 3600 IN TLSA 3 1 1 a9cdf989b504fe5dca90c0d2167b6550570734f7c763e09fdf88904e06157065
 tlsa2 3600 IN TLSA 1 0 1 efddf0d915c7bdc5782c0881e1b2a95ad099fbdd06d7b1f77982d9364338d955
index ac14e202c3eb5a9925bb74bd07809cc0e7aa603b..efd95e128ff8e508a6b67167c75114b4dc855885 100644 (file)
@@ -62,7 +62,7 @@ hip01.example. 3600 IN HIP 2 200100107b1a74df365639cc39f1d578 AwEAAbdxyhNuSutc5E
 hip02.example. 3600 IN HIP 2 200100107b1a74df365639cc39f1d578 AwEAAbdxyhNuSutc5EMzxTs9LBPCIkOFH8cIvM4p9+LrV4e19WzK00+CI6zBCQTdtWsuxKbWIy87UOoJTwkUs7lBu+Upr1gsNrut79ryra+bSRGQb1slImA8YVJyuIDsj7kwzG7jnERNqnWxZ48AWkskmdHaVDP4BcelrTI3rMXdXF5D rvs.example.com.
 hip03.example. 3600 IN HIP 2 200100107b1a74df365639cc39f1d578 AwEAAbdxyhNuSutc5EMzxTs9LBPCIkOFH8cIvM4p9+LrV4e19WzK00+CI6zBCQTdtWsuxKbWIy87UOoJTwkUs7lBu+Upr1gsNrut79ryra+bSRGQb1slImA8YVJyuIDsj7kwzG7jnERNqnWxZ48AWkskmdHaVDP4BcelrTI3rMXdXF5D rvs1.example.com. rvs2.example.com.
 https01.example. 3600 IN HTTPS 0 svc.example.
-https02.example. 3600 IN HTTPS 1 . port="8002" echconfig="abcd"
+https02.example. 3600 IN HTTPS 1 . port="8002" ech="abcd"
 ipseckey01.example. 3600 IN IPSECKEY 10 1 2 192.0.2.38 AQNRU3mG7TVTO2BkR47usntb102uFJtu gbo6BSGvgqt4AQ==
 ipseckey02.example. 3600 IN IPSECKEY 10 0 2 . AQNRU3mG7TVTO2BkR47usntb102uFJtu gbo6BSGvgqt4AQ==
 ipseckey03.example. 3600 IN IPSECKEY 10 3 2 mygateway.example.com. AQNRU3mG7TVTO2BkR47usntb102uFJtu gbo6BSGvgqt4AQ==
@@ -116,7 +116,7 @@ spf.example. 3600 IN SPF "v=spf1 mx -all"
 srv01.example. 3600 IN SRV 0 0 0 .
 srv02.example. 3600 IN SRV 65535 65535 65535 old-slow-box.example.com.
 sshfp1.example. 3600 IN SSHFP 1 1 aa549bfe898489c02d1715d97d79c57ba2fa76ab
-svcb01.example. 3600 IN SVCB 100 foo.com. mandatory="alpn,port" alpn="h2,h3" no-default-alpn port="12345" ipv4hint="1.2.3.4,4.3.2.1" echconfig="abcd" ipv6hint="1::2,3::4" key12345="foo"
+svcb01.example. 3600 IN SVCB 100 foo.com. mandatory="alpn,port" alpn="h2,h3" no-default-alpn port="12345" ipv4hint="1.2.3.4,4.3.2.1" ech="abcd" ipv6hint="1::2,3::4" key12345="foo"
 t.example. 301 IN A 73.80.65.49
 tlsa1.example. 3600 IN TLSA 3 1 1 a9cdf989b504fe5dca90c0d2167b6550570734f7c763e09fdf88904e06157065
 tlsa2.example. 3600 IN TLSA 1 0 1 efddf0d915c7bdc5782c0881e1b2a95ad099fbdd06d7b1f77982d9364338d955
index c1ddfd49b6160e57095e2a8c98eb5dab98649a00..2fb2d0bb3176e570489651a1a55d6361865ca209 100644 (file)
@@ -62,7 +62,7 @@ hip01 3600 IN HIP 2 200100107b1a74df365639cc39f1d578 AwEAAbdxyhNuSutc5EMzxTs9LBP
 hip02 3600 IN HIP 2 200100107b1a74df365639cc39f1d578 AwEAAbdxyhNuSutc5EMzxTs9LBPCIkOFH8cIvM4p9+LrV4e19WzK00+CI6zBCQTdtWsuxKbWIy87UOoJTwkUs7lBu+Upr1gsNrut79ryra+bSRGQb1slImA8YVJyuIDsj7kwzG7jnERNqnWxZ48AWkskmdHaVDP4BcelrTI3rMXdXF5D rvs.example.com.
 hip03 3600 IN HIP 2 200100107b1a74df365639cc39f1d578 AwEAAbdxyhNuSutc5EMzxTs9LBPCIkOFH8cIvM4p9+LrV4e19WzK00+CI6zBCQTdtWsuxKbWIy87UOoJTwkUs7lBu+Upr1gsNrut79ryra+bSRGQb1slImA8YVJyuIDsj7kwzG7jnERNqnWxZ48AWkskmdHaVDP4BcelrTI3rMXdXF5D rvs1.example.com. rvs2.example.com.
 https01 3600 IN HTTPS 0 svc
-https02 3600 IN HTTPS 1 . port="8002" echconfig="abcd"
+https02 3600 IN HTTPS 1 . port="8002" ech="abcd"
 ipseckey01 3600 IN IPSECKEY 10 1 2 192.0.2.38 AQNRU3mG7TVTO2BkR47usntb102uFJtu gbo6BSGvgqt4AQ==
 ipseckey02 3600 IN IPSECKEY 10 0 2 . AQNRU3mG7TVTO2BkR47usntb102uFJtu gbo6BSGvgqt4AQ==
 ipseckey03 3600 IN IPSECKEY 10 3 2 mygateway.example.com. AQNRU3mG7TVTO2BkR47usntb102uFJtu gbo6BSGvgqt4AQ==
@@ -116,7 +116,7 @@ spf 3600 IN SPF "v=spf1 mx -all"
 srv01 3600 IN SRV 0 0 0 .
 srv02 3600 IN SRV 65535 65535 65535 old-slow-box.example.com.
 sshfp1 3600 IN SSHFP 1 1 aa549bfe898489c02d1715d97d79c57ba2fa76ab
-svcb01 3600 IN SVCB 100 foo.com. mandatory="alpn,port" alpn="h2,h3" no-default-alpn port="12345" ipv4hint="1.2.3.4,4.3.2.1" echconfig="abcd" ipv6hint="1::2,3::4" key12345="foo"
+svcb01 3600 IN SVCB 100 foo.com. mandatory="alpn,port" alpn="h2,h3" no-default-alpn port="12345" ipv4hint="1.2.3.4,4.3.2.1" ech="abcd" ipv6hint="1::2,3::4" key12345="foo"
 t 301 IN A 73.80.65.49
 tlsa1 3600 IN TLSA 3 1 1 a9cdf989b504fe5dca90c0d2167b6550570734f7c763e09fdf88904e06157065
 tlsa2 3600 IN TLSA 1 0 1 efddf0d915c7bdc5782c0881e1b2a95ad099fbdd06d7b1f77982d9364338d955
diff --git a/tests/svcb_test_vectors.generic b/tests/svcb_test_vectors.generic
new file mode 100644 (file)
index 0000000..104eb32
--- /dev/null
@@ -0,0 +1,103 @@
+; Alias form
+\# 19 (
+00 00                                              ; priority
+03 66 6f 6f 07 65 78 61 6d 70 6c 65 03 63 6f 6d 00 ; target
+)
+
+; Service form
+
+; The first form is the simple "use the ownername".
+\# 3 (
+00 01      ; priority
+00         ; target (root label)
+)
+
+; This vector only has a port.
+\# 25 (
+00 10                                              ; priority
+03 66 6f 6f 07 65 78 61 6d 70 6c 65 03 63 6f 6d 00 ; target
+00 03                                              ; key 3
+00 02                                              ; length 2
+00 35                                              ; value
+)
+
+; This example has a key that is not registered, its value is unquoted.
+\# 28 (
+00 01                                              ; priority
+03 66 6f 6f 07 65 78 61 6d 70 6c 65 03 63 6f 6d 00 ; target
+02 9b                                              ; key 667
+00 05                                              ; length 5
+68 65 6c 6c 6f                                     ; value
+)
+
+; This example has a key that is not registered, its value is quoted and
+; contains a decimal-escaped character.
+\# 32 (
+00 01                                              ; priority
+03 66 6f 6f 07 65 78 61 6d 70 6c 65 03 63 6f 6d 00 ; target
+02 9b                                              ; key 667
+00 09                                              ; length 9
+68 65 6c 6c 6f d2 71 6f 6f                         ; value
+)
+
+; Here, two IPv6 hints are quoted in the presentation format.
+\# 55 (
+00 01                                              ; priority
+03 66 6f 6f 07 65 78 61 6d 70 6c 65 03 63 6f 6d 00 ; target
+00 06                                              ; key 6
+00 20                                              ; length 32
+20 01 0d b8 00 00 00 00 00 00 00 00 00 00 00 01    ; first address
+20 01 0d b8 00 00 00 00 00 00 00 00 00 53 00 01    ; second address
+)
+
+; This example shows a single IPv6 hint in IPv4 mapped IPv6 presentation format.
+\# 35 (
+00 01                                              ; priority
+07 65 78 61 6d 70 6c 65 03 63 6f 6d 00             ; target
+00 06                                              ; key 6
+00 10                                              ; length 16
+20 01 0d b8 ff ff ff ff ff ff ff ff c6 33 64 64    ; address
+)
+
+; In the next vector, neither the SvcParamValues nor the mandatory keys are
+; sorted in presentation format, but are correctly sorted in the wire-format.
+\# 48 (
+00 10                                              ; priority
+03 66 6f 6f 07 65 78 61 6d 70 6c 65 03 6f 72 67 00 ; target
+00 00                                              ; key 0
+00 04                                              ; param length 4
+00 01                                              ; value: key 1
+00 04                                              ; value: key 4
+00 01                                              ; key 1
+00 09                                              ; param length 9
+02                                                 ; alpn length 2
+68 32                                              ; alpn value
+05                                                 ; alpn length 5
+68 33 2d 31 39                                     ; alpn value
+00 04                                              ; key 4
+00 04                                              ; param length 4
+c0 00 02 01                                        ; param value
+)
+
+; This last vector has an alpn value with an escaped comma and an escaped
+; backslash in two presentation formats.
+\# 35 (
+00 10                                              ; priority
+03 66 6f 6f 07 65 78 61 6d 70 6c 65 03 6f 72 67 00 ; target
+00 01                                              ; key 1
+00 0c                                              ; param length 12
+08                                                 ; alpn length 8
+66 5c 6f 6f 2c 62 61 72                            ; alpn value
+02                                                 ; alpn length 2
+68 32                                              ; alpn value
+)
+\# 35 (
+00 10                                              ; priority
+03 66 6f 6f 07 65 78 61 6d 70 6c 65 03 6f 72 67 00 ; target
+00 01                                              ; key 1
+00 0c                                              ; param length 12
+08                                                 ; alpn length 8
+66 5c 6f 6f 2c 62 61 72                            ; alpn value
+02                                                 ; alpn length 2
+68 32                                              ; alpn value
+)
diff --git a/tests/svcb_test_vectors.text b/tests/svcb_test_vectors.text
new file mode 100644 (file)
index 0000000..4ebbfc8
--- /dev/null
@@ -0,0 +1,33 @@
+; Alias form
+0 foo.example.com.
+
+; Service form
+
+; The first form is the simple "use the ownername".
+1 .
+
+; This vector only has a port.
+16 foo.example.com. port=53
+
+; This example has a key that is not registered, its value is unquoted.
+1 foo.example.com. key667=hello
+
+; This example has a key that is not registered, its value is quoted and
+; contains a decimal-escaped character.
+1 foo.example.com. key667="hello\210qoo"
+
+; Here, two IPv6 hints are quoted in the presentation format.
+1 foo.example.com. ipv6hint="2001:db8::1,2001:db8::53:1"
+
+; This example shows a single IPv6 hint in IPv4 mapped IPv6 presentation format.
+1 example.com. ipv6hint="2001:db8:ffff:ffff:ffff:ffff:198.51.100.100"
+
+; In the next vector, neither the SvcParamValues nor the mandatory keys are
+; sorted in presentation format, but are correctly sorted in the wire-format.
+16 foo.example.org. (alpn=h2,h3-19 mandatory=ipv4hint,alpn
+                    ipv4hint=192.0.2.1)
+
+; This last vector has an alpn value with an escaped comma and an escaped
+; backslash in two presentation formats.
+16 foo.example.org. alpn="f\\\\oo\\,bar,h2"
+16 foo.example.org. alpn=f\\\092oo\092,bar,h2
index 7cd776881af3fa138c327339f517a5ce98733d58..34fc9ad380e504a23186996ab7f0459e8dac6149 100644 (file)
@@ -6,6 +6,9 @@ import unittest
 import dns.rdata
 import dns.rdtypes.svcbbase
 import dns.rrset
+from dns.tokenizer import Tokenizer
+
+from tests.util import here
 
 class SVCBTestCase(unittest.TestCase):
     def check_valid_inputs(self, inputs):
@@ -17,7 +20,7 @@ class SVCBTestCase(unittest.TestCase):
 
     def check_invalid_inputs(self, inputs):
         for text in inputs:
-            with self.assertRaises(dns.exception.SyntaxError):
+            with self.assertRaises((dns.exception.SyntaxError, ValueError)):
                 dns.rdata.from_text('IN', 'SVCB', text)
 
     def test_svcb_general_invalid(self):
@@ -83,14 +86,18 @@ class SVCBTestCase(unittest.TestCase):
             "1 . alpn=h\\050,h3",
             "1 . alpn=\"h\\050,h3\"",
             "1 . alpn=\\h2,h3",
+            "1 . alpn=\"h2\\,h3\"",
+            "1 . alpn=h2\\,h3",
+            "1 . alpn=h2\\044h3",
             "1 . key1=\\002h2\\002h3",
         )
         self.check_valid_inputs(valid_inputs_two_items)
 
         valid_inputs_one_item = (
-            "1 . alpn=\"h2\\,h3\"",
-            "1 . alpn=h2\\,h3",
-            "1 . alpn=h2\\044h3",
+            "1 . alpn=\"h2\\\\,h3\"",
+            "1 . alpn=h2\\\\,h3",
+            "1 . alpn=h2\\092\\044h3",
+            "1 . key1=\\005h2,h3",
         )
         self.check_valid_inputs(valid_inputs_one_item)
 
@@ -115,18 +122,22 @@ class SVCBTestCase(unittest.TestCase):
 
     def test_svcb_no_default_alpn(self):
         valid_inputs = (
-            "1 . no-default-alpn",
-            "1 . no-default-alpn=\"\"",
-            "1 . key2",
-            "1 . key2=\"\"",
+            "1 . alpn=\"h2\" no-default-alpn",
+            "1 . alpn=\"h2\" no-default-alpn=\"\"",
+            "1 . alpn=\"h2\" key2",
+            "1 . alpn=\"h2\" key2=\"\"",
         )
         self.check_valid_inputs(valid_inputs)
 
         invalid_inputs = (
-            "1 . no-default-alpn=foo",
-            "1 . no-default-alpn=",
-            "1 . key2=foo",
-            "1 . key2=",
+            "1 . no-default-alpn",
+            "1 . no-default-alpn=\"\"",
+            "1 . key2",
+            "1 . key2=\"\"",
+            "1 . alpn=h2 no-default-alpn=foo",
+            "1 . alpn=h2 no-default-alpn=",
+            "1 . alpn=h2 key2=foo",
+            "1 . alpn=h2 key2=",
         )
         self.check_invalid_inputs(invalid_inputs)
 
@@ -171,20 +182,20 @@ class SVCBTestCase(unittest.TestCase):
         )
         self.check_invalid_inputs(invalid_inputs)
 
-    def test_svcb_echconfig(self):
+    def test_svcb_ech(self):
         valid_inputs = (
-            "1 . echconfig=\"Zm9vMA==\"",
-            "1 . echconfig=Zm9vMA==",
+            "1 . ech=\"Zm9vMA==\"",
+            "1 . ech=Zm9vMA==",
             "1 . key5=foo0",
             "1 . key5=\\102\\111\\111\\048",
         )
         self.check_valid_inputs(valid_inputs)
 
         invalid_inputs = (
-            "1 . echconfig",
-            "1 . echconfig=",
-            "1 . echconfig=Zm9vMA",
-            "1 . echconfig=\\090m9vMA==",
+            "1 . ech",
+            "1 . ech=",
+            "1 . ech=Zm9vMA",
+            "1 . ech=\\090m9vMA==",
             "1 . key5",
             "1 . key5=",
         )
@@ -251,7 +262,7 @@ class SVCBTestCase(unittest.TestCase):
 
         everything = \
             "100 foo.com. mandatory=\"alpn,port\" alpn=\"h2,h3\" " \
-            "             no-default-alpn port=\"12345\" echconfig=\"abcd\" " \
+            "             no-default-alpn port=\"12345\" ech=\"abcd\" " \
             "             ipv4hint=1.2.3.4,4.3.2.1 ipv6hint=1::2,3::4" \
             "             key12345=\"foo\""
         rr = dns.rdata.from_text('IN', 'SVCB', everything)
@@ -262,16 +273,18 @@ class SVCBTestCase(unittest.TestCase):
             # As above, but the keys are out of order.
             "\\# 24 0001 00 0000000400010003 000300020101 00010003026832",
             # As above, but the mandatory keys don't match
-            "\\# 24 0001 00 0000000400010002 000300020101 00010003026832",
-            "\\# 24 0001 00 0000000400010004 000300020101 00010003026832",
+            "\\# 24 0001 00 0000000400010002 00010003026832 000300020101",
+            "\\# 24 0001 00 0000000400010004 00010003026832 000300020101",
             # Alias form shouldn't have parameters.
             "\\# 08 0000 000300020101",
+            # no-default-alpn requires alpn
+            "\\# 07 0001 00 00020000",
         )
         self.check_invalid_inputs(invalid_inputs)
 
     def test_misc_escape(self):
         rdata = dns.rdata.from_text('in', 'svcb', '1 . alpn=\\010\\010')
-        expected = '1 . alpn="\\010\\010"'
+        expected = '1 . alpn="\\\\010\\\\010"'
         self.assertEqual(rdata.to_text(), expected)
         with self.assertRaises(dns.exception.SyntaxError):
             dns.rdata.from_text('in', 'svcb', '1 . alpn=\\0')
@@ -286,6 +299,56 @@ class SVCBTestCase(unittest.TestCase):
         expected = '"\\001\\002"'
         self.assertEqual(gp.to_text(), expected)
 
+    def test_svcb_spec_test_vectors(self):
+        text_file = here("svcb_test_vectors.text")
+        text_tokenizer = Tokenizer(open(text_file), filename=text_file)
+        generic_file = here("svcb_test_vectors.generic")
+        generic_tokenizer = Tokenizer(open(generic_file), filename=generic_file)
+
+        while True:
+            while True:
+                text_token = text_tokenizer.get()
+                if text_token.is_eol():
+                    continue
+                break
+            while True:
+                generic_token = generic_tokenizer.get()
+                if generic_token.is_eol():
+                    continue
+                break
+            self.assertEqual(text_token.ttype, generic_token.ttype)
+            if text_token.is_eof():
+                break
+            self.assertTrue(text_token.is_identifier)
+            text_tokenizer.unget(text_token)
+            generic_tokenizer.unget(generic_token)
+            text_rdata = dns.rdata.from_text('IN', 'SVCB', text_tokenizer)
+            generic_rdata = dns.rdata.from_text('IN', 'SVCB', generic_tokenizer)
+            self.assertEqual(text_rdata, generic_rdata)
+
+    def test_svcb_spec_failure_cases(self):
+        failure_cases = (
+            # This example has multiple instances of the same SvcParamKey
+            "1 foo.example.com. key123=abc key123=def",
+            # In the next examples the SvcParamKeys are missing their values.
+            "1 foo.example.com. mandatory",
+            "1 foo.example.com. alpn",
+            "1 foo.example.com. port",
+            "1 foo.example.com. ipv4hint",
+            "1 foo.example.com. ipv6hint",
+            # The "no-default-alpn" SvcParamKey value MUST be empty (Section 6.1).
+            "1 foo.example.com. no-default-alpn=abc",
+            # In this record a mandatory SvcParam is missing (Section 7).
+            "1 foo.example.com. mandatory=key123",
+            # The "mandatory" SvcParamKey MUST not be included in mandatory list
+            # (Section 7).
+            "1 foo.example.com. mandatory=mandatory",
+            # Here there are multiple instances of the same SvcParamKey in the
+            # mandatory list (Section 7).
+            "1 foo.example.com. mandatory=key123,key123 key123=abc",
+        )
+        self.check_invalid_inputs(failure_cases);
+
     def test_alias_mode(self):
         rd = dns.rdata.from_text('in', 'svcb', '0 .')
         self.assertEqual(len(rd.params), 0)