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