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 datetime import datetime, timedelta, timezone 10 from pathlib import Path 11 12 import base64 13 import json 14 15 from cryptography.hazmat.primitives import serialization 16 17 import dns.dnssec 18 import dns.message 19 import dns.name 20 import dns.rcode 21 import dns.rdata 22 import dns.rdataclass 23 import dns.rdatatype 24 import dns.rrset 25 26 from isctest.asyncserver import ( 27 AsyncDnsServer, 28 DnsResponseSend, 29 QueryContext, 30 ResponseHandler, 31 ) 32 33 TTL = 300 34 PARENT = "p22.hack." 35 CHILD = f"c.{PARENT}" 36 CHILD_NS = f"ns.{CHILD}" 37 VICTIM = f"victim.{PARENT}" 38 AAC = f"aac.{PARENT}" 39 CHILD_NEXT = f"ns1.{PARENT}" 40 TRIGGER = f"www.{CHILD}" 41 PRIME_NX = f"0.{PARENT}" 42 43 CHILD_A = "192.0.2.50" 44 CHILD_NS_A = "10.53.0.2" 45 VICTIM_A = "192.0.2.99" 46 AAC_A = "192.0.2.77" 47 48 49 @dataclass(frozen=True) 50 class Key: 51 zone: dns.name.Name 52 private_key: object 53 dnskey: dns.rdata.Rdata 54 55 56 def name(text: str) -> dns.name.Name: 57 return dns.name.from_text(text) 58 59 60 def load_keys() -> dict[str, Key]: 61 path = Path("keys.json") 62 with path.open(encoding="utf-8") as keys_file: 63 raw_keys = json.load(keys_file) 64 65 keys = {} 66 for zone, raw_key in raw_keys.items(): 67 private_key = serialization.load_pem_private_key( 68 raw_key["private_pem"].encode("ascii"), 69 password=None, 70 ) 71 dnskey = dns.rdata.from_text( 72 dns.rdataclass.IN, dns.rdatatype.DNSKEY, raw_key["dnskey"] 73 ) 74 keys[zone] = Key(name(zone), private_key, dnskey) 75 76 return keys 77 78 79 def rrset(owner: str, rdtype: dns.rdatatype.RdataType, *rdatas: str) -> dns.rrset.RRset: 80 return dns.rrset.from_text(owner, TTL, dns.rdataclass.IN, rdtype, *rdatas) 81 82 83 def rrset_from_rdata(owner: str, rdata: dns.rdata.Rdata) -> dns.rrset.RRset: 84 return dns.rrset.from_rdata(name(owner), TTL, rdata) 85 86 87 def add_signed( 88 section: list[dns.rrset.RRset], covered: dns.rrset.RRset, signer: Key 89 ) -> None: 90 rrsig = dns.dnssec.sign( 91 covered, 92 signer.private_key, 93 signer.zone, 94 signer.dnskey, 95 lifetime=86400, 96 verify=True, 97 ) 98 section.append(covered) 99 section.append(dns.rrset.from_rdata(covered.name, covered.ttl, rrsig)) 100 101 102 def add_dnskey(response: dns.message.Message, zone: str, key: Key) -> None: 103 add_signed(response.answer, rrset_from_rdata(zone, key.dnskey), key) 104 105 106 def soa_rrset(zone: str) -> dns.rrset.RRset: 107 return rrset( 108 zone, 109 dns.rdatatype.SOA, 110 f"ns.{zone} hostmaster.{zone} 1 3600 600 86400 300", 111 ) 112 113 114 def nsec_rrset(owner: str, next_name: str, *types: str) -> dns.rrset.RRset: 115 return rrset(owner, dns.rdatatype.NSEC, f"{next_name} {' '.join(types)}") 116 117 118 def nsec_apex() -> dns.rrset.RRset: 119 return nsec_rrset(PARENT, AAC, "NS", "SOA", "RRSIG", "NSEC", "DNSKEY") 120 121 122 def nsec_deleg_child() -> dns.rrset.RRset: 123 return nsec_rrset(CHILD, CHILD_NEXT, "NS", "RRSIG", "NSEC") 124 125 126 def stuffed_ent_nsec() -> dns.rrset.RRset: 127 return nsec_rrset(f"t.{PARENT}", f"sub.{VICTIM}", "A", "RRSIG", "NSEC") 128 129 130 def stuffed_range_nsec() -> dns.rrset.RRset: 131 return nsec_rrset(f"aab.{PARENT}", f"az.{PARENT}", "A", "RRSIG", "NSEC") 132 133 134 def garbage_rrsig(covered: dns.rrset.RRset, signer: Key) -> dns.rrset.RRset: 135 now = datetime.now(timezone.utc) 136 inception = (now - timedelta(hours=1)).strftime("%Y%m%d%H%M%S") 137 expiration = (now + timedelta(days=1)).strftime("%Y%m%d%H%M%S") 138 signer_name = signer.zone.to_text() 139 labels = len(covered.name.labels) - 1 140 key_tag = dns.dnssec.key_id(signer.dnskey) 141 signature = base64.b64encode(bytes(64)).decode("ascii") 142 text = ( 143 f"{dns.rdatatype.to_text(covered.rdtype)} " 144 f"{signer.dnskey.algorithm} {labels} {covered.ttl} " 145 f"{expiration} {inception} {key_tag} {signer_name} {signature}" 146 ) 147 rdata = dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.RRSIG, text) 148 return dns.rrset.from_rdata(covered.name, covered.ttl, rdata) 149 150 151 def add_garbage_signed_nsec( 152 section: list[dns.rrset.RRset], covered: dns.rrset.RRset, signer: Key 153 ) -> None: 154 section.append(covered) 155 section.append(garbage_rrsig(covered, signer)) 156 157 158 def prepare_response(qctx: QueryContext) -> dns.message.Message: 159 qctx.prepare_new_response(with_zone_data=False) 160 qctx.response.set_rcode(dns.rcode.NOERROR) 161 return qctx.response 162 163 164 def add_parent_negative( 165 response: dns.message.Message, signer: Key, nsec: dns.rrset.RRset 166 ) -> None: 167 add_signed(response.authority, soa_rrset(PARENT), signer) 168 add_signed(response.authority, nsec, signer) 169 170 171 class ParentHandler(ResponseHandler): 172 def __init__(self, keys: dict[str, Key]) -> None: 173 self.keys = keys 174 self.parent = name(PARENT) 175 self.child = name(CHILD) 176 self.victim = name(VICTIM) 177 self.aac = name(AAC) 178 self.prime_nx = name(PRIME_NX) 179 180 def match(self, qctx: QueryContext) -> bool: 181 return qctx.qname.is_subdomain(self.parent) 182 183 async def get_responses( 184 self, qctx: QueryContext 185 ) -> AsyncGenerator[DnsResponseSend, None]: 186 response = prepare_response(qctx) 187 parent_key = self.keys[PARENT] 188 189 if qctx.qname == self.parent and qctx.qtype == dns.rdatatype.DNSKEY: 190 add_dnskey(response, PARENT, parent_key) 191 elif qctx.qname == self.parent and qctx.qtype == dns.rdatatype.SOA: 192 add_signed(response.answer, soa_rrset(PARENT), parent_key) 193 elif qctx.qname == self.parent: 194 add_parent_negative(response, parent_key, nsec_apex()) 195 elif qctx.qname == self.child and qctx.qtype == dns.rdatatype.DS: 196 add_parent_negative(response, parent_key, nsec_deleg_child()) 197 elif qctx.qname == self.prime_nx: 198 response.set_rcode(dns.rcode.NXDOMAIN) 199 add_parent_negative(response, parent_key, nsec_apex()) 200 elif qctx.qname == self.child or qctx.qname.is_subdomain(self.child): 201 response.authority.append(rrset(CHILD, dns.rdatatype.NS, CHILD_NS)) 202 add_signed(response.authority, nsec_deleg_child(), parent_key) 203 add_garbage_signed_nsec(response.authority, stuffed_ent_nsec(), parent_key) 204 add_garbage_signed_nsec( 205 response.authority, stuffed_range_nsec(), parent_key 206 ) 207 response.additional.append(rrset(CHILD_NS, dns.rdatatype.A, CHILD_NS_A)) 208 elif qctx.qname == self.victim and qctx.qtype == dns.rdatatype.A: 209 add_signed( 210 response.answer, 211 rrset(VICTIM, dns.rdatatype.A, VICTIM_A), 212 parent_key, 213 ) 214 elif qctx.qname == self.aac and qctx.qtype == dns.rdatatype.A: 215 add_signed(response.answer, rrset(AAC, dns.rdatatype.A, AAC_A), parent_key) 216 else: 217 response.set_rcode(dns.rcode.NXDOMAIN) 218 add_parent_negative(response, parent_key, nsec_apex()) 219 220 yield DnsResponseSend(response, authoritative=True) 221 222 223 def main() -> None: 224 server = AsyncDnsServer(default_aa=True) 225 server.install_response_handlers(ParentHandler(load_keys())) 226 server.run() 227 228 229 if __name__ == "__main__": 230 main() 231