1 # Copyright (C) Internet Systems Consortium, Inc. ("ISC") 2 # 3 # SPDX-License-Identifier: MPL-2.0 4 # 5 # This Source Code Form is subject to the terms of the Mozilla Public 6 # License, v. 2.0. If a copy of the MPL was not distributed with this 7 # file, you can obtain one at https://mozilla.org/MPL/2.0/. 8 # 9 # See the COPYRIGHT file distributed with this work for additional 10 # information regarding copyright ownership. 11 12 """Handler for the attack.delegationtrap. zone (DelegationTrap reproducer). 13 14 This is a Python port of the DelegationTrap vector from the ReTrap PoC 15 (gitlab.isc.org/bind-team/nankai-cve-reproducers). It reproduces the 16 "deep delegation hierarchy" algorithmic-complexity attack described in 17 issue #5347 (part of the #5341 meta-issue). 18 19 The server is authoritative for the whole attack.delegationtrap. subtree 20 and pretends that *every* label below the apex is its own secure zone, 21 all sharing a single reused key. For any name N under the apex it 22 answers: 23 24 N/DNSKEY -> DNSKEY(K) signed by N (self-signed) 25 N/DS -> DS(K, owner=N) signed by parent(N) 26 N/A -> A 10.53.0.4 signed by parent(N) 27 28 Because the A answer is returned directly (no referral) but its RRSIG 29 signer is the immediate parent, a validating resolver must build the 30 whole chain of trust label by label: for a query with a depth-D name it 31 fetches and validates DNSKEY + DS at each of the D levels. This is the 32 "chain-of-trust construction" cost the attack amplifies, and it is what 33 BIND's per-fetch validation quota (max-validations-per-fetch) is meant 34 to bound. 35 36 Key material is written by bootstrap() in tests_delegationtrap.py to 37 attack_delegationtrap.pem in this directory before any server starts. 38 """ 39 40 from collections.abc import AsyncGenerator 41 from pathlib import Path 42 43 import time 44 45 from cryptography.hazmat.primitives import serialization 46 from dns.rdtypes.dnskeybase import Flag 47 48 import dns.dnssec 49 import dns.name 50 import dns.rcode 51 import dns.rdata 52 import dns.rdataclass 53 import dns.rdatatype 54 import dns.rrset 55 56 from isctest.asyncserver import ( 57 DnsResponseSend, 58 DomainHandler, 59 QueryContext, 60 ResponseAction, 61 ) 62 63 ZONE_NAME = "attack.delegationtrap." 64 SERVER_IP = "10.53.0.4" 65 TTL = 300 66 PEM_PATH = Path("attack_delegationtrap.pem") 67 68 69 class DelegationTrapHandler(DomainHandler): 70 """Serve every label under attack.delegationtrap. as a secure zone cut.""" 71 72 domains = [ZONE_NAME] 73 74 def __init__(self) -> None: 75 super().__init__() 76 self._apex = dns.name.from_text(ZONE_NAME) 77 78 self._priv = serialization.load_pem_private_key( 79 PEM_PATH.read_bytes(), password=None 80 ) 81 self._dnskey = dns.dnssec.make_dnskey( 82 self._priv.public_key(), 83 dns.dnssec.Algorithm.ECDSAP256SHA256, 84 flags=Flag.ZONE | Flag.SEP, 85 ) 86 87 now = int(time.time()) 88 self._inception = now - 3600 89 self._expiration = now + 14 * 86400 90 91 def _sign(self, rrset: dns.rrset.RRset, signer: dns.name.Name) -> dns.rrset.RRset: 92 """Return an RRSIG RRset covering `rrset`, signed as zone `signer`.""" 93 rrsig = dns.dnssec.sign( 94 rrset, 95 self._priv, 96 signer=signer, 97 dnskey=self._dnskey, 98 inception=self._inception, 99 expiration=self._expiration, 100 lifetime=None, 101 deterministic=False, # for OpenSSL<3.2.0 compat 102 ) 103 rrsig_rrset = dns.rrset.RRset( 104 rrset.name, rrset.rdclass, dns.rdatatype.RRSIG, rrset.rdtype 105 ) 106 rrsig_rrset.update_ttl(TTL) 107 rrsig_rrset.add(rrsig) 108 return rrsig_rrset 109 110 def _dnskey_rrset(self, name: dns.name.Name) -> dns.rrset.RRset: 111 rrset = dns.rrset.RRset(name, dns.rdataclass.IN, dns.rdatatype.DNSKEY) 112 rrset.update_ttl(TTL) 113 rrset.add(self._dnskey) 114 return rrset 115 116 def _ds_rrset(self, name: dns.name.Name) -> dns.rrset.RRset: 117 ds = dns.dnssec.make_ds(name, self._dnskey, dns.dnssec.DSDigest.SHA256) 118 rrset = dns.rrset.RRset(name, dns.rdataclass.IN, dns.rdatatype.DS) 119 rrset.update_ttl(TTL) 120 rrset.add(ds) 121 return rrset 122 123 def _a_rrset(self, name: dns.name.Name) -> dns.rrset.RRset: 124 rrset = dns.rrset.RRset(name, dns.rdataclass.IN, dns.rdatatype.A) 125 rrset.update_ttl(TTL) 126 rrset.add(dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.A, SERVER_IP)) 127 return rrset 128 129 async def get_responses( 130 self, qctx: QueryContext 131 ) -> AsyncGenerator[ResponseAction, None]: 132 qname = qctx.qname 133 qtype = qctx.qtype 134 response = qctx.prepare_new_response(with_zone_data=False) 135 response.set_rcode(dns.rcode.NOERROR) 136 137 parent = qname.parent() if qname != dns.name.root else qname 138 139 if qtype == dns.rdatatype.DNSKEY: 140 # The DNSKEY RRset is signed by the zone itself (self-signed KSK). 141 rrset = self._dnskey_rrset(qname) 142 response.answer.extend([rrset, self._sign(rrset, qname)]) 143 elif qtype == dns.rdatatype.DS: 144 # A DS lives in the parent zone and is signed by the parent. 145 rrset = self._ds_rrset(qname) 146 response.answer.extend([rrset, self._sign(rrset, parent)]) 147 elif qtype == dns.rdatatype.A: 148 # The A answer is signed by the immediate parent, forcing the 149 # resolver to build the trust chain label by label. 150 rrset = self._a_rrset(qname) 151 response.answer.extend([rrset, self._sign(rrset, parent)]) 152 153 yield DnsResponseSend(response, authoritative=True) 154