Home | History | Annotate | Line # | Download | only in ans1
      1 #!/usr/bin/python3
      2 
      3 # Copyright (C) Internet Systems Consortium, Inc. ("ISC")
      4 #
      5 # SPDX-License-Identifier: MPL-2.0
      6 
      7 from collections.abc import AsyncGenerator
      8 from dataclasses import dataclass
      9 from pathlib import Path
     10 
     11 import json
     12 
     13 from cryptography.hazmat.primitives import serialization
     14 
     15 import dns.dnssec
     16 import dns.flags
     17 import dns.message
     18 import dns.name
     19 import dns.rcode
     20 import dns.rdata
     21 import dns.rdataclass
     22 import dns.rdatatype
     23 import dns.rrset
     24 
     25 from isctest.asyncserver import (
     26     AsyncDnsServer,
     27     DnsResponseSend,
     28     QueryContext,
     29     ResponseHandler,
     30 )
     31 
     32 TTL = 300
     33 PARENT = "p031.test."
     34 CHILD = f"c.{PARENT}"
     35 GRANDCHILD = f"grand.{CHILD}"
     36 GRANDCHILD3 = f"grand3.{CHILD}"
     37 ATTACK = f"www-bind.{GRANDCHILD}"
     38 ATTACK3 = f"www-bind.{GRANDCHILD3}"
     39 FORGED_A = "6.6.6.60"
     40 CHILD_DS = "12345 13 2 abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789"
     41 
     42 
     43 @dataclass(frozen=True)
     44 class Key:
     45     zone: dns.name.Name
     46     private_key: object
     47     dnskey: dns.rdata.Rdata
     48 
     49 
     50 def name(text: str) -> dns.name.Name:
     51     return dns.name.from_text(text)
     52 
     53 
     54 def load_key() -> Key:
     55     path = Path(__file__).resolve().parent / "keys.json"
     56     with path.open(encoding="utf-8") as keys_file:
     57         raw_key = json.load(keys_file)[PARENT]
     58 
     59     private_key = serialization.load_pem_private_key(
     60         raw_key["private_pem"].encode("ascii"),
     61         password=None,
     62     )
     63     dnskey = dns.rdata.from_text(
     64         dns.rdataclass.IN, dns.rdatatype.DNSKEY, raw_key["dnskey"]
     65     )
     66     return Key(name(PARENT), private_key, dnskey)
     67 
     68 
     69 def rrset(owner: str, rdtype: dns.rdatatype.RdataType, *rdatas: str) -> dns.rrset.RRset:
     70     return dns.rrset.from_text(owner, TTL, dns.rdataclass.IN, rdtype, *rdatas)
     71 
     72 
     73 def rrset_from_rdata(owner: str, rdata: dns.rdata.Rdata) -> dns.rrset.RRset:
     74     return dns.rrset.from_rdata(name(owner), TTL, rdata)
     75 
     76 
     77 def add_signed(
     78     section: list[dns.rrset.RRset], covered: dns.rrset.RRset, signer: Key
     79 ) -> None:
     80     rrsig = dns.dnssec.sign(
     81         covered,
     82         signer.private_key,
     83         signer.zone,
     84         signer.dnskey,
     85         lifetime=86400,
     86         verify=True,
     87     )
     88     section.append(covered)
     89     section.append(dns.rrset.from_rdata(covered.name, covered.ttl, rrsig))
     90 
     91 
     92 def soa_rrset() -> dns.rrset.RRset:
     93     return rrset(
     94         PARENT,
     95         dns.rdatatype.SOA,
     96         f"ns.{PARENT} hostmaster.{PARENT} 1 3600 600 86400 300",
     97     )
     98 
     99 
    100 def nsec_rrset(owner: str, next_name: str, *types: str) -> dns.rrset.RRset:
    101     return rrset(owner, dns.rdatatype.NSEC, f"{next_name} {' '.join(types)}")
    102 
    103 
    104 def child_ds_rrset() -> dns.rrset.RRset:
    105     return rrset(CHILD, dns.rdatatype.DS, CHILD_DS)
    106 
    107 
    108 def grandchild_nsec_lie() -> dns.rrset.RRset:
    109     return nsec_rrset(GRANDCHILD, f"grandz.{CHILD}", "NS", "RRSIG", "NSEC")
    110 
    111 
    112 def grandchild3_nsec3_lie() -> dns.rrset.RRset:
    113     # An NSEC3 owned by the grandparent zone P that matches the hash of the
    114     # grandchild name and shows an (insecure) delegation: NS bit set, DS bit
    115     # clear.  Same forgery as grandchild_nsec_lie(), but expressed as NSEC3 so
    116     # that the resolver reaches is_insecure_referral()'s trynsec3 arm.
    117     digest = dns.dnssec.nsec3_hash(name(GRANDCHILD3), None, 0, 1).lower()
    118     owner = f"{digest}.{PARENT}"
    119     return rrset(owner, dns.rdatatype.NSEC3, f"1 0 0 - {digest} NS")
    120 
    121 
    122 def add_parent_nodata(
    123     response: dns.message.Message, parent_key: Key, nsec: dns.rrset.RRset
    124 ) -> None:
    125     add_signed(response.authority, soa_rrset(), parent_key)
    126     add_signed(response.authority, nsec, parent_key)
    127 
    128 
    129 def prepare_response(qctx: QueryContext) -> dns.message.Message:
    130     qctx.prepare_new_response(with_zone_data=False)
    131     qctx.response.flags |= dns.flags.AA
    132     qctx.response.set_rcode(dns.rcode.NOERROR)
    133     return qctx.response
    134 
    135 
    136 class GrandparentNsecHandler(ResponseHandler):
    137     def __init__(self, parent_key: Key) -> None:
    138         self.parent_key = parent_key
    139         self.parent = name(PARENT)
    140         self.child = name(CHILD)
    141         self.grandchild = name(GRANDCHILD)
    142         self.grandchild3 = name(GRANDCHILD3)
    143 
    144     def match(self, qctx: QueryContext) -> bool:
    145         return qctx.qname.is_subdomain(self.parent)
    146 
    147     async def get_responses(
    148         self, qctx: QueryContext
    149     ) -> AsyncGenerator[DnsResponseSend, None]:
    150         response = prepare_response(qctx)
    151 
    152         if qctx.qname == self.parent and qctx.qtype == dns.rdatatype.DNSKEY:
    153             # Priming, parent DNSKEY
    154             add_signed(
    155                 response.answer,
    156                 rrset_from_rdata(PARENT, self.parent_key.dnskey),
    157                 self.parent_key,
    158             )
    159         elif qctx.qname == self.parent and qctx.qtype == dns.rdatatype.SOA:
    160             # Priming, parent SOA
    161             add_signed(response.answer, soa_rrset(), self.parent_key)
    162         elif qctx.qname == self.child and qctx.qtype == dns.rdatatype.DS:
    163             # Priming, child DS
    164             add_signed(response.answer, child_ds_rrset(), self.parent_key)
    165         elif qctx.qname == self.grandchild and qctx.qtype == dns.rdatatype.DS:
    166             # Forge no data for grand child DS (NSEC variant)
    167             add_parent_nodata(response, self.parent_key, grandchild_nsec_lie())
    168         elif qctx.qname == self.grandchild3 and qctx.qtype == dns.rdatatype.DS:
    169             # Forge no data for grand child DS (NSEC3 variant)
    170             add_parent_nodata(response, self.parent_key, grandchild3_nsec3_lie())
    171         elif (
    172             qctx.qname.is_subdomain(self.grandchild)
    173             or qctx.qname.is_subdomain(self.grandchild3)
    174         ) and qctx.qtype == dns.rdatatype.A:
    175             # Attack query
    176             response.answer.append(
    177                 rrset(qctx.qname.to_text(), dns.rdatatype.A, FORGED_A)
    178             )
    179         else:
    180             response.set_rcode(dns.rcode.NXDOMAIN)
    181 
    182         yield DnsResponseSend(response, authoritative=True)
    183 
    184 
    185 def main() -> None:
    186     server = AsyncDnsServer(default_aa=True)
    187     server.install_response_handlers(GrandparentNsecHandler(load_key()))
    188     server.run()
    189 
    190 
    191 if __name__ == "__main__":
    192     main()
    193