From ab1d7e1078dc8449366d28bc6f5d33121902d067 Mon Sep 17 00:00:00 2001 From: Brian Wellington Date: Wed, 21 Apr 2021 17:22:40 -0700 Subject: Update SVCB to the current spec. --- dns/rdtypes/svcbbase.py | 77 ++++++++++++++++------------ tests/example | 4 +- tests/example1.good | 4 +- tests/example2.good | 4 +- tests/example3.good | 4 +- tests/svcb_test_vectors.generic | 103 +++++++++++++++++++++++++++++++++++++ tests/svcb_test_vectors.text | 33 ++++++++++++ tests/test_svcb.py | 109 +++++++++++++++++++++++++++++++--------- 8 files changed, 274 insertions(+), 64 deletions(-) create mode 100644 tests/svcb_test_vectors.generic create mode 100644 tests/svcb_test_vectors.text diff --git a/dns/rdtypes/svcbbase.py b/dns/rdtypes/svcbbase.py index 80e67e0..49f35fe 100644 --- a/dns/rdtypes/svcbbase.py +++ b/dns/rdtypes/svcbbase.py @@ -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) diff --git a/tests/example b/tests/example index 86af9dd..7450933 100644 --- a/tests/example +++ b/tests/example @@ -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" diff --git a/tests/example1.good b/tests/example1.good index c1ddfd4..2fb2d0b 100644 --- a/tests/example1.good +++ b/tests/example1.good @@ -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/example2.good b/tests/example2.good index ac14e20..efd95e1 100644 --- a/tests/example2.good +++ b/tests/example2.good @@ -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 diff --git a/tests/example3.good b/tests/example3.good index c1ddfd4..2fb2d0b 100644 --- a/tests/example3.good +++ b/tests/example3.good @@ -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 index 0000000..104eb32 --- /dev/null +++ b/tests/svcb_test_vectors.generic @@ -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 index 0000000..4ebbfc8 --- /dev/null +++ b/tests/svcb_test_vectors.text @@ -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 diff --git a/tests/test_svcb.py b/tests/test_svcb.py index 7cd7768..34fc9ad 100644 --- a/tests/test_svcb.py +++ b/tests/test_svcb.py @@ -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) -- cgit v1.2.1