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 from collections.abc import AsyncGenerator 15 from datetime import datetime, timedelta, timezone 16 17 import base64 18 19 import dns.flags 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 DnsResponseSend, DomainHandler, QueryContext 27 from nsec_synthesis.ans1.common import ( 28 Key, 29 add_signed, 30 name, 31 prepare_response, 32 rrset, 33 rrset_from_rdata, 34 soa_rrset, 35 ) 36 37 TTL = 300 38 F004_ZONE = "f004.test." 39 ATTACKER = "attacker.f004.test." 40 VICTIM = "victim.f004.test." 41 VICTIM_A = "203.0.113.1" 42 POISON_NEXT = f"b.{VICTIM}" 43 VICTIM_NODATA_NEXT = f"z.{F004_ZONE}" 44 45 46 def attacker_nsec_rrset() -> dns.rrset.RRset: 47 return rrset( 48 ATTACKER, 49 dns.rdatatype.NSEC, 50 f"{POISON_NEXT} NS SOA RRSIG NSEC DNSKEY", 51 ) 52 53 54 def victim_nodata_nsec_rrset() -> dns.rrset.RRset: 55 # An NSEC owned by the victim name itself, whose type bitmap omits A but 56 # includes the NSEC and RRSIG types query_coveringnsec requires. If it 57 # were trusted, it would prove a NODATA for victim/A and hide the real 58 # A record below. 59 return rrset( 60 VICTIM, 61 dns.rdatatype.NSEC, 62 f"{VICTIM_NODATA_NEXT} TXT RRSIG NSEC", 63 ) 64 65 66 def garbage_rrsig(covered: dns.rrset.RRset, signer: Key) -> dns.rrset.RRset: 67 now = datetime.now(timezone.utc) 68 inception = (now - timedelta(hours=1)).strftime("%Y%m%d%H%M%S") 69 expiration = (now + timedelta(days=1)).strftime("%Y%m%d%H%M%S") 70 signature = base64.b64encode(bytes(64)).decode("ascii") 71 text = ( 72 f"{dns.rdatatype.to_text(covered.rdtype)} " 73 f"{signer.dnskey.algorithm} 3 {covered.ttl} " 74 f"{expiration} {inception} 9999 {F004_ZONE} {signature}" 75 ) 76 rdata = dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.RRSIG, text) 77 return dns.rrset.from_rdata(covered.name, covered.ttl, rdata) 78 79 80 class F004Handler(DomainHandler): 81 domains = [F004_ZONE] 82 83 def __init__(self, keys: dict[str, Key]) -> None: 84 super().__init__() 85 self.keys = keys 86 87 if F004_ZONE not in keys: 88 return 89 90 self.key = keys[F004_ZONE] 91 self.parent = name(F004_ZONE) 92 self.attacker = name(ATTACKER) 93 self.victim = name(VICTIM) 94 95 async def get_responses( 96 self, qctx: QueryContext 97 ) -> AsyncGenerator[DnsResponseSend, None]: 98 response = prepare_response(qctx) 99 100 if qctx.qname == self.parent and qctx.qtype == dns.rdatatype.DNSKEY: 101 add_signed( 102 response.answer, 103 rrset_from_rdata(F004_ZONE, self.key.dnskey), 104 self.key, 105 ) 106 elif qctx.qname == self.parent and qctx.qtype == dns.rdatatype.SOA: 107 add_signed(response.answer, soa_rrset(F004_ZONE), self.key) 108 elif qctx.qname == self.attacker and qctx.qtype == dns.rdatatype.NSEC: 109 if qctx.query.flags & dns.flags.CD: 110 nsec = attacker_nsec_rrset() 111 response.answer.append(nsec) 112 response.answer.append(garbage_rrsig(nsec, self.key)) 113 else: 114 response.set_rcode(dns.rcode.REFUSED) 115 elif qctx.qname == self.victim and qctx.qtype == dns.rdatatype.NSEC: 116 if qctx.query.flags & dns.flags.CD: 117 nsec = victim_nodata_nsec_rrset() 118 response.answer.append(nsec) 119 response.answer.append(garbage_rrsig(nsec, self.key)) 120 else: 121 response.set_rcode(dns.rcode.REFUSED) 122 elif qctx.qname == self.victim and qctx.qtype == dns.rdatatype.A: 123 add_signed( 124 response.answer, 125 rrset(VICTIM, dns.rdatatype.A, VICTIM_A), 126 self.key, 127 ) 128 else: 129 response.set_rcode(dns.rcode.NXDOMAIN) 130 add_signed(response.authority, soa_rrset(F004_ZONE), self.key) 131 132 yield DnsResponseSend(response, authoritative=True) 133