Home | History | Annotate | Line # | Download | only in checkds
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