tests_checkds.py revision 1.1 1 #!/usr/bin/python3
2
3 # Copyright (C) Internet Systems Consortium, Inc. ("ISC")
4 #
5 # SPDX-License-Identifier: MPL-2.0
6 #
7 # This Source Code Form is subject to the terms of the Mozilla Public
8 # License, v. 2.0. If a copy of the MPL was not distributed with this
9 # file, you can obtain one at https://mozilla.org/MPL/2.0/.
10 #
11 # See the COPYRIGHT file distributed with this work for additional
12 # information regarding copyright ownership.
13
14 import mmap
15 import os
16 import subprocess
17 import sys
18 import time
19
20 import pytest
21
22 pytest.importorskip("dns", minversion="2.0.0")
23 import dns.exception
24 import dns.message
25 import dns.name
26 import dns.query
27 import dns.rcode
28 import dns.rdataclass
29 import dns.rdatatype
30 import dns.resolver
31
32
33 def has_signed_apex_nsec(zone, response):
34 has_nsec = False
35 has_rrsig = False
36
37 ttl = 300
38 nextname = "a."
39 types = "NS SOA RRSIG NSEC DNSKEY CDS CDNSKEY"
40 match = "{0} {1} IN NSEC {2}{0} {3}".format(zone, ttl, nextname, types)
41 sig = "{0} {1} IN RRSIG NSEC 13 2 300".format(zone, ttl)
42
43 for rr in response.answer:
44 if match in rr.to_text():
45 has_nsec = True
46 if sig in rr.to_text():
47 has_rrsig = True
48
49 if not has_nsec:
50 print("error: missing apex NSEC record in response")
51 if not has_rrsig:
52 print("error: missing NSEC signature in response")
53
54 return has_nsec and has_rrsig
55
56
57 def do_query(server, qname, qtype, tcp=False):
58 query = dns.message.make_query(qname, qtype, use_edns=True, want_dnssec=True)
59 try:
60 if tcp:
61 response = dns.query.tcp(
62 query, server.nameservers[0], timeout=3, port=server.port
63 )
64 else:
65 response = dns.query.udp(
66 query, server.nameservers[0], timeout=3, port=server.port
67 )
68 except dns.exception.Timeout:
69 print(
70 "error: query timeout for query {} {} to {}".format(
71 qname, qtype, server.nameservers[0]
72 )
73 )
74 return None
75
76 return response
77
78
79 def verify_zone(zone, transfer):
80 verify = os.getenv("VERIFY")
81 assert verify is not None
82
83 filename = "{}out".format(zone)
84 with open(filename, "w", encoding="utf-8") as file:
85 for rr in transfer.answer:
86 file.write(rr.to_text())
87 file.write("\n")
88
89 # dnssec-verify command with default arguments.
90 verify_cmd = [verify, "-z", "-o", zone, filename]
91
92 verifier = subprocess.run(verify_cmd, capture_output=True, check=True)
93
94 if verifier.returncode != 0:
95 print("error: dnssec-verify {} failed".format(zone))
96 sys.stderr.buffer.write(verifier.stderr)
97
98 return verifier.returncode == 0
99
100
101 def read_statefile(server, zone):
102 addr = server.nameservers[0]
103 count = 0
104 keyid = 0
105 state = {}
106
107 response = do_query(server, zone, "DS", tcp=True)
108 if not isinstance(response, dns.message.Message):
109 print("error: no response for {} DS from {}".format(zone, addr))
110 return {}
111
112 if response.rcode() == dns.rcode.NOERROR:
113 # fetch key id from response.
114 for rr in response.answer:
115 if rr.match(
116 dns.name.from_text(zone),
117 dns.rdataclass.IN,
118 dns.rdatatype.DS,
119 dns.rdatatype.NONE,
120 ):
121 if count == 0:
122 keyid = list(dict(rr.items).items())[0][0].key_tag
123 count += 1
124
125 if count != 1:
126 print(
127 "error: expected a single DS in response for {} from {},"
128 "got {}".format(zone, addr, count)
129 )
130 return {}
131 else:
132 print(
133 "error: {} response for {} DNSKEY from {}".format(
134 dns.rcode.to_text(response.rcode()), zone, addr
135 )
136 )
137 return {}
138
139 filename = "ns9/K{}+013+{:05d}.state".format(zone, keyid)
140 print("read state file {}".format(filename))
141
142 try:
143 with open(filename, "r", encoding="utf-8") as file:
144 for line in file:
145 if line.startswith(";"):
146 continue
147 key, val = line.strip().split(":", 1)
148 state[key.strip()] = val.strip()
149
150 except FileNotFoundError:
151 # file may not be written just yet.
152 return {}
153
154 return state
155
156
157 def zone_check(server, zone):
158 addr = server.nameservers[0]
159
160 # wait until zone is fully signed.
161 signed = False
162 for _ in range(10):
163 response = do_query(server, zone, "NSEC")
164 if not isinstance(response, dns.message.Message):
165 print("error: no response for {} NSEC from {}".format(zone, addr))
166 elif response.rcode() == dns.rcode.NOERROR:
167 signed = has_signed_apex_nsec(zone, response)
168 else:
169 print(
170 "error: {} response for {} NSEC from {}".format(
171 dns.rcode.to_text(response.rcode()), zone, addr
172 )
173 )
174
175 if signed:
176 break
177
178 time.sleep(1)
179
180 assert signed
181
182 # check if zone if DNSSEC valid.
183 verified = False
184 transfer = do_query(server, zone, "AXFR", tcp=True)
185 if not isinstance(transfer, dns.message.Message):
186 print("error: no response for {} AXFR from {}".format(zone, addr))
187 elif transfer.rcode() == dns.rcode.NOERROR:
188 verified = verify_zone(zone, transfer)
189 else:
190 print(
191 "error: {} response for {} AXFR from {}".format(
192 dns.rcode.to_text(transfer.rcode()), zone, addr
193 )
194 )
195
196 assert verified
197
198
199 def keystate_check(server, zone, key):
200 val = 0
201 deny = False
202
203 search = key
204 if key.startswith("!"):
205 deny = True
206 search = key[1:]
207
208 for _ in range(10):
209 state = read_statefile(server, zone)
210 try:
211 val = state[search]
212 except KeyError:
213 pass
214
215 if not deny and val != 0:
216 break
217 if deny and val == 0:
218 break
219
220 time.sleep(1)
221
222 if deny:
223 assert val == 0
224 else:
225 assert val != 0
226
227
228 def wait_for_log(filename, log):
229 found = False
230
231 for _ in range(10):
232 print("read log file {}".format(filename))
233
234 try:
235 with open(filename, "r", encoding="utf-8") as file:
236 s = mmap.mmap(file.fileno(), 0, access=mmap.ACCESS_READ)
237 if s.find(bytes(log, "ascii")) != -1:
238 found = True
239 except FileNotFoundError:
240 print("file not found {}".format(filename))
241
242 if found:
243 break
244
245 print("sleep")
246 time.sleep(1)
247
248 assert found
249
250
251 def test_checkds_dspublished(named_port):
252 # We create resolver instances that will be used to send queries.
253 server = dns.resolver.Resolver()
254 server.nameservers = ["10.53.0.9"]
255 server.port = named_port
256
257 parent = dns.resolver.Resolver()
258 parent.nameservers = ["10.53.0.2"]
259 parent.port = named_port
260
261 # DS correctly published in parent.
262 zone_check(server, "dspublished.checkds.")
263 wait_for_log(
264 "ns9/named.run",
265 "zone dspublished.checkds/IN (signed): checkds: DS response from 10.53.0.2",
266 )
267 keystate_check(parent, "dspublished.checkds.", "DSPublish")
268
269 # DS correctly published in parent (reference to parental-agent).
270 zone_check(server, "reference.checkds.")
271 wait_for_log(
272 "ns9/named.run",
273 "zone reference.checkds/IN (signed): checkds: DS response from 10.53.0.2",
274 )
275 keystate_check(parent, "reference.checkds.", "DSPublish")
276
277 # DS not published in parent.
278 zone_check(server, "missing-dspublished.checkds.")
279 wait_for_log(
280 "ns9/named.run",
281 "zone missing-dspublished.checkds/IN (signed): checkds: "
282 "empty DS response from 10.53.0.5",
283 )
284 keystate_check(parent, "missing-dspublished.checkds.", "!DSPublish")
285
286 # Badly configured parent.
287 zone_check(server, "bad-dspublished.checkds.")
288 wait_for_log(
289 "ns9/named.run",
290 "zone bad-dspublished.checkds/IN (signed): checkds: "
291 "bad DS response from 10.53.0.6",
292 )
293 keystate_check(parent, "bad-dspublished.checkds.", "!DSPublish")
294
295 # TBD: DS published in parent, but bogus signature.
296
297 # DS correctly published in all parents.
298 zone_check(server, "multiple-dspublished.checkds.")
299 wait_for_log(
300 "ns9/named.run",
301 "zone multiple-dspublished.checkds/IN (signed): checkds: "
302 "DS response from 10.53.0.2",
303 )
304 wait_for_log(
305 "ns9/named.run",
306 "zone multiple-dspublished.checkds/IN (signed): checkds: "
307 "DS response from 10.53.0.4",
308 )
309 keystate_check(parent, "multiple-dspublished.checkds.", "DSPublish")
310
311 # DS published in only one of multiple parents.
312 zone_check(server, "incomplete-dspublished.checkds.")
313 wait_for_log(
314 "ns9/named.run",
315 "zone incomplete-dspublished.checkds/IN (signed): checkds: "
316 "DS response from 10.53.0.2",
317 )
318 wait_for_log(
319 "ns9/named.run",
320 "zone incomplete-dspublished.checkds/IN (signed): checkds: "
321 "DS response from 10.53.0.4",
322 )
323 wait_for_log(
324 "ns9/named.run",
325 "zone incomplete-dspublished.checkds/IN (signed): checkds: "
326 "empty DS response from 10.53.0.5",
327 )
328 keystate_check(parent, "incomplete-dspublished.checkds.", "!DSPublish")
329
330 # One of the parents is badly configured.
331 zone_check(server, "bad2-dswithdrawn.checkds.")
332 wait_for_log(
333 "ns9/named.run",
334 "zone bad2-dspublished.checkds/IN (signed): checkds: "
335 "DS response from 10.53.0.2",
336 )
337 wait_for_log(
338 "ns9/named.run",
339 "zone bad2-dspublished.checkds/IN (signed): checkds: "
340 "DS response from 10.53.0.4",
341 )
342 wait_for_log(
343 "ns9/named.run",
344 "zone bad2-dspublished.checkds/IN (signed): checkds: "
345 "bad DS response from 10.53.0.6",
346 )
347 keystate_check(parent, "bad2-dspublished.checkds.", "!DSPublish")
348
349 # TBD: DS published in all parents, but one has bogus signature.
350
351 # TBD: Check with TSIG
352
353
354 def test_checkds_dswithdrawn(named_port):
355 # We create resolver instances that will be used to send queries.
356 server = dns.resolver.Resolver()
357 server.nameservers = ["10.53.0.9"]
358 server.port = named_port
359
360 parent = dns.resolver.Resolver()
361 parent.nameservers = ["10.53.0.2"]
362 parent.port = named_port
363
364 # DS correctly published in single parent.
365 zone_check(server, "dswithdrawn.checkds.")
366 wait_for_log(
367 "ns9/named.run",
368 "zone dswithdrawn.checkds/IN (signed): checkds: "
369 "empty DS response from 10.53.0.5",
370 )
371 keystate_check(parent, "dswithdrawn.checkds.", "DSRemoved")
372
373 # DS not withdrawn from parent.
374 zone_check(server, "missing-dswithdrawn.checkds.")
375 wait_for_log(
376 "ns9/named.run",
377 "zone missing-dswithdrawn.checkds/IN (signed): checkds: "
378 "DS response from 10.53.0.2",
379 )
380 keystate_check(parent, "missing-dswithdrawn.checkds.", "!DSRemoved")
381
382 # Badly configured parent.
383 zone_check(server, "bad-dswithdrawn.checkds.")
384 wait_for_log(
385 "ns9/named.run",
386 "zone bad-dswithdrawn.checkds/IN (signed): checkds: "
387 "bad DS response from 10.53.0.6",
388 )
389 keystate_check(parent, "bad-dswithdrawn.checkds.", "!DSRemoved")
390
391 # TBD: DS published in parent, but bogus signature.
392
393 # DS correctly withdrawn from all parents.
394 zone_check(server, "multiple-dswithdrawn.checkds.")
395 wait_for_log(
396 "ns9/named.run",
397 "zone multiple-dswithdrawn.checkds/IN (signed): checkds: "
398 "empty DS response from 10.53.0.5",
399 )
400 wait_for_log(
401 "ns9/named.run",
402 "zone multiple-dswithdrawn.checkds/IN (signed): checkds: "
403 "empty DS response from 10.53.0.7",
404 )
405 keystate_check(parent, "multiple-dswithdrawn.checkds.", "DSRemoved")
406
407 # DS withdrawn from only one of multiple parents.
408 zone_check(server, "incomplete-dswithdrawn.checkds.")
409 wait_for_log(
410 "ns9/named.run",
411 "zone incomplete-dswithdrawn.checkds/IN (signed): checkds: "
412 "DS response from 10.53.0.2",
413 )
414 wait_for_log(
415 "ns9/named.run",
416 "zone incomplete-dswithdrawn.checkds/IN (signed): checkds: "
417 "empty DS response from 10.53.0.5",
418 )
419 wait_for_log(
420 "ns9/named.run",
421 "zone incomplete-dswithdrawn.checkds/IN (signed): checkds: "
422 "empty DS response from 10.53.0.7",
423 )
424 keystate_check(parent, "incomplete-dswithdrawn.checkds.", "!DSRemoved")
425
426 # One of the parents is badly configured.
427 zone_check(server, "bad2-dswithdrawn.checkds.")
428 wait_for_log(
429 "ns9/named.run",
430 "zone bad2-dswithdrawn.checkds/IN (signed): checkds: "
431 "empty DS response from 10.53.0.5",
432 )
433 wait_for_log(
434 "ns9/named.run",
435 "zone bad2-dswithdrawn.checkds/IN (signed): checkds: "
436 "empty DS response from 10.53.0.7",
437 )
438 wait_for_log(
439 "ns9/named.run",
440 "zone bad2-dswithdrawn.checkds/IN (signed): checkds: "
441 "bad DS response from 10.53.0.6",
442 )
443 keystate_check(parent, "bad2-dswithdrawn.checkds.", "!DSRemoved")
444
445 # TBD: DS withdrawn from all parents, but one has bogus signature.
446