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 10 import base64 11 12 import dns.dnssec 13 import dns.flags 14 import dns.message 15 import dns.rcode 16 import dns.rdatatype 17 import dns.rrset 18 19 from dnssec_nsec3.ans1.common import ( 20 Key, 21 add_signed, 22 name, 23 nsec3_hash, 24 nsec3_rrset, 25 rrset, 26 rrset_from_rdata, 27 soa_rrset, 28 ) 29 from isctest.asyncserver import DnsResponseSend, DomainHandler, QueryContext 30 31 TTL = 300 32 PARENT = "f025.test." 33 CHILD = f"evil.{PARENT}" 34 PARENT_NS = f"ns.{PARENT}" 35 CHILD_NS = f"ns.{CHILD}" 36 CLOSEST = f"victim2.{CHILD}" 37 ATTACK = f"b.{CLOSEST}" 38 LEGIT = f"legit.{CHILD}" 39 WILDCARD = f"*.{CHILD}" 40 FORGED_A = "6.6.6.6" 41 42 43 @dataclass(frozen=True) 44 class Nsec3Entry: 45 owner: str 46 owner_hash: str 47 types: tuple[str, ...] 48 49 50 def base32hex_add(hash_text: str, delta: int) -> str: 51 raw = bytearray(base64.b32hexdecode(hash_text.upper())) 52 value = int.from_bytes(raw, "big") + delta 53 value %= 1 << (8 * len(raw)) 54 return base64.b32hexencode(value.to_bytes(len(raw), "big")).decode("ascii") 55 56 57 class Nsec3Chain: 58 def __init__(self, zone: str, entries: list[tuple[str, tuple[str, ...]]]) -> None: 59 self.zone = zone 60 self.entries = sorted( 61 [Nsec3Entry(owner, nsec3_hash(owner), types) for owner, types in entries], 62 key=lambda entry: entry.owner_hash, 63 ) 64 65 def rrset_for_entry(self, entry: Nsec3Entry) -> dns.rrset.RRset: 66 index = self.entries.index(entry) 67 next_hash = self.entries[(index + 1) % len(self.entries)].owner_hash 68 return nsec3_rrset(self.zone, entry.owner_hash, next_hash, 0, *entry.types) 69 70 def rrsets(self) -> list[dns.rrset.RRset]: 71 return [self.rrset_for_entry(entry) for entry in self.entries] 72 73 74 def add_nsec3_chain( 75 section: list[dns.rrset.RRset], chain: Nsec3Chain, signer: Key 76 ) -> None: 77 for covered in chain.rrsets(): 78 add_signed(section, covered, signer) 79 80 81 def add_tight_parent_nsec3(section: list[dns.rrset.RRset], parent: Key) -> None: 82 target_hash = nsec3_hash(f"{CLOSEST}") 83 covered = nsec3_rrset( 84 PARENT, 85 base32hex_add(target_hash, -1), 86 base32hex_add(target_hash, 1), 87 0, 88 "TXT", 89 "RRSIG", 90 ) 91 add_signed(section, covered, parent) 92 93 94 def wildcard_rrsig(owner: str, child: Key) -> dns.rrset.RRset: 95 wildcard = rrset(WILDCARD, dns.rdatatype.A, FORGED_A) 96 rrsig = dns.dnssec.sign( 97 wildcard, 98 child.private_key, 99 child.zone, 100 child.dnskey, 101 lifetime=86400, 102 verify=True, 103 ) 104 return dns.rrset.from_rdata(name(owner), wildcard.ttl, rrsig) 105 106 107 def add_wildcard_answer(response: dns.message.Message, owner: str, child: Key) -> None: 108 response.answer.append(rrset(owner, dns.rdatatype.A, FORGED_A)) 109 response.answer.append(wildcard_rrsig(owner, child)) 110 111 112 class F025Handler(DomainHandler): 113 domains = [PARENT, CHILD] 114 115 def __init__(self, keys: dict[str, Key]) -> None: 116 super().__init__() 117 self.keys = keys 118 119 self.parent = name(PARENT) 120 self.child = name(CHILD) 121 self.parent_ns = name(PARENT_NS) 122 self.child_ns = name(CHILD_NS) 123 self.child_nsec3 = Nsec3Chain( 124 CHILD, 125 [ 126 (CHILD, ("NS", "SOA", "RRSIG", "DNSKEY", "NSEC3PARAM")), 127 (WILDCARD, ("A", "RRSIG")), 128 (CHILD_NS, ("A", "RRSIG")), 129 ], 130 ) 131 132 def _add_extra_nsec3(self, response: dns.message.Message, qname: str) -> None: 133 parent_key = self.keys[PARENT] 134 child_key = self.keys[CHILD] 135 if "victim2." in qname: 136 add_tight_parent_nsec3(response.authority, parent_key) 137 else: 138 add_nsec3_chain(response.authority, self.child_nsec3, child_key) 139 140 async def get_responses( 141 self, qctx: QueryContext 142 ) -> AsyncGenerator[DnsResponseSend, None]: 143 qctx.prepare_new_response(with_zone_data=False) 144 qctx.response.flags |= dns.flags.AA 145 qctx.response.set_rcode(dns.rcode.NOERROR) 146 147 parent_key = self.keys[PARENT] 148 child_key = self.keys[CHILD] 149 qname = qctx.qname.to_text() 150 151 if qctx.qname == self.parent and qctx.qtype == dns.rdatatype.DNSKEY: 152 # Priming, parent DNSKEY 153 add_signed( 154 qctx.response.answer, 155 rrset_from_rdata(PARENT, parent_key.dnskey), 156 parent_key, 157 ) 158 elif qctx.qname == self.parent and qctx.qtype == dns.rdatatype.SOA: 159 # Priming, parent SOA 160 add_signed(qctx.response.answer, soa_rrset(PARENT), parent_key) 161 elif qctx.qname == self.parent and qctx.qtype == dns.rdatatype.NS: 162 # Priming, parent NS 163 add_signed( 164 qctx.response.answer, 165 rrset(PARENT, dns.rdatatype.NS, PARENT_NS), 166 parent_key, 167 ) 168 elif qctx.qname == self.parent_ns and qctx.qtype == dns.rdatatype.A: 169 # Priming, parent glue 170 add_signed( 171 qctx.response.answer, 172 rrset(PARENT_NS, dns.rdatatype.A, "10.53.0.1"), 173 parent_key, 174 ) 175 elif qctx.qname == self.child and qctx.qtype == dns.rdatatype.DS: 176 # Priming, child DS 177 add_signed( 178 qctx.response.answer, 179 rrset_from_rdata(CHILD, child_key.ds), 180 parent_key, 181 ) 182 elif qctx.qname == self.child and qctx.qtype == dns.rdatatype.DNSKEY: 183 # Priming, child DNSKEY 184 add_signed( 185 qctx.response.answer, 186 rrset_from_rdata(CHILD, child_key.dnskey), 187 child_key, 188 ) 189 elif qctx.qname == self.child and qctx.qtype == dns.rdatatype.SOA: 190 # Priming, child SOA 191 add_signed(qctx.response.answer, soa_rrset(CHILD), child_key) 192 elif qctx.qname == self.child and qctx.qtype == dns.rdatatype.NS: 193 # Priming, child NS 194 add_signed( 195 qctx.response.answer, 196 rrset(CHILD, dns.rdatatype.NS, CHILD_NS), 197 child_key, 198 ) 199 elif qctx.qname == self.child_ns and qctx.qtype == dns.rdatatype.A: 200 # Priming, child glue 201 add_signed( 202 qctx.response.answer, 203 rrset(CHILD_NS, dns.rdatatype.A, "10.53.0.1"), 204 child_key, 205 ) 206 elif qctx.qname.is_subdomain(self.child): 207 if qctx.qtype == dns.rdatatype.A: 208 add_wildcard_answer(qctx.response, qname, child_key) 209 else: 210 add_signed(qctx.response.authority, soa_rrset(CHILD), child_key) 211 # Adding malicious NSEC3 212 self._add_extra_nsec3(qctx.response, qname) 213 else: 214 # Everything else is NODATA 215 add_signed(qctx.response.authority, soa_rrset(PARENT), parent_key) 216 217 yield DnsResponseSend(qctx.response, authoritative=True) 218