1#!/usr/local/bin/python3.8 2# 3# Electrum - lightweight Bitcoin client 4# Copyright (C) 2015 Thomas Voegtlin 5# 6# Permission is hereby granted, free of charge, to any person 7# obtaining a copy of this software and associated documentation files 8# (the "Software"), to deal in the Software without restriction, 9# including without limitation the rights to use, copy, modify, merge, 10# publish, distribute, sublicense, and/or sell copies of the Software, 11# and to permit persons to whom the Software is furnished to do so, 12# subject to the following conditions: 13# 14# The above copyright notice and this permission notice shall be 15# included in all copies or substantial portions of the Software. 16# 17# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 18# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 19# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 20# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS 21# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN 22# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 23# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24# SOFTWARE. 25 26# Check DNSSEC trust chain. 27# Todo: verify expiration dates 28# 29# Based on 30# http://backreference.org/2010/11/17/dnssec-verification-with-dig/ 31# https://github.com/rthalley/dnspython/blob/master/tests/test_dnssec.py 32 33 34import dns 35import dns.name 36import dns.query 37import dns.dnssec 38import dns.message 39import dns.resolver 40import dns.rdatatype 41import dns.rdtypes.ANY.NS 42import dns.rdtypes.ANY.CNAME 43import dns.rdtypes.ANY.DLV 44import dns.rdtypes.ANY.DNSKEY 45import dns.rdtypes.ANY.DS 46import dns.rdtypes.ANY.NSEC 47import dns.rdtypes.ANY.NSEC3 48import dns.rdtypes.ANY.NSEC3PARAM 49import dns.rdtypes.ANY.RRSIG 50import dns.rdtypes.ANY.SOA 51import dns.rdtypes.ANY.TXT 52import dns.rdtypes.IN.A 53import dns.rdtypes.IN.AAAA 54 55from .logging import get_logger 56 57 58_logger = get_logger(__name__) 59 60 61# hard-coded trust anchors (root KSKs) 62trust_anchors = [ 63 # KSK-2017: 64 dns.rrset.from_text('.', 1 , 'IN', 'DNSKEY', '257 3 8 AwEAAaz/tAm8yTn4Mfeh5eyI96WSVexTBAvkMgJzkKTOiW1vkIbzxeF3+/4RgWOq7HrxRixHlFlExOLAJr5emLvN7SWXgnLh4+B5xQlNVz8Og8kvArMtNROxVQuCaSnIDdD5LKyWbRd2n9WGe2R8PzgCmr3EgVLrjyBxWezF0jLHwVN8efS3rCj/EWgvIWgb9tarpVUDK/b58Da+sqqls3eNbuv7pr+eoZG+SrDK6nWeL3c6H5Apxz7LjVc1uTIdsIXxuOLYA4/ilBmSVIzuDWfdRUfhHdY6+cn8HFRm+2hM8AnXGXws9555KrUB5qihylGa8subX2Nn6UwNR1AkUTV74bU='), 65 # KSK-2010: 66 dns.rrset.from_text('.', 15202, 'IN', 'DNSKEY', '257 3 8 AwEAAagAIKlVZrpC6Ia7gEzahOR+9W29euxhJhVVLOyQbSEW0O8gcCjF FVQUTf6v58fLjwBd0YI0EzrAcQqBGCzh/RStIoO8g0NfnfL2MTJRkxoX bfDaUeVPQuYEhg37NZWAJQ9VnMVDxP/VHL496M/QZxkjf5/Efucp2gaD X6RS6CXpoY68LsvPVjR0ZSwzz1apAzvN9dlzEheX7ICJBBtuA6G3LQpz W5hOA2hzCTMjJPJ8LbqF6dsV6DoBQzgul0sGIcGOYl7OyQdXfZ57relS Qageu+ipAdTTJ25AsRTAoub8ONGcLmqrAmRLKBP1dfwhYB4N7knNnulq QxA+Uk1ihz0='), 67] 68 69 70def _check_query(ns, sub, _type, keys): 71 q = dns.message.make_query(sub, _type, want_dnssec=True) 72 response = dns.query.tcp(q, ns, timeout=5) 73 assert response.rcode() == 0, 'No answer' 74 answer = response.answer 75 assert len(answer) != 0, ('No DNS record found', sub, _type) 76 assert len(answer) != 1, ('No DNSSEC record found', sub, _type) 77 if answer[0].rdtype == dns.rdatatype.RRSIG: 78 rrsig, rrset = answer 79 elif answer[1].rdtype == dns.rdatatype.RRSIG: 80 rrset, rrsig = answer 81 else: 82 raise Exception('No signature set in record') 83 if keys is None: 84 keys = {dns.name.from_text(sub):rrset} 85 dns.dnssec.validate(rrset, rrsig, keys) 86 return rrset 87 88 89def _get_and_validate(ns, url, _type): 90 # get trusted root key 91 root_rrset = None 92 for dnskey_rr in trust_anchors: 93 try: 94 # Check if there is a valid signature for the root dnskey 95 root_rrset = _check_query(ns, '', dns.rdatatype.DNSKEY, {dns.name.root: dnskey_rr}) 96 break 97 except dns.dnssec.ValidationFailure: 98 # It's OK as long as one key validates 99 continue 100 if not root_rrset: 101 raise dns.dnssec.ValidationFailure('None of the trust anchors found in DNS') 102 keys = {dns.name.root: root_rrset} 103 # top-down verification 104 parts = url.split('.') 105 for i in range(len(parts), 0, -1): 106 sub = '.'.join(parts[i-1:]) 107 name = dns.name.from_text(sub) 108 # If server is authoritative, don't fetch DNSKEY 109 query = dns.message.make_query(sub, dns.rdatatype.NS) 110 response = dns.query.udp(query, ns, 3) 111 assert response.rcode() == dns.rcode.NOERROR, "query error" 112 rrset = response.authority[0] if len(response.authority) > 0 else response.answer[0] 113 rr = rrset[0] 114 if rr.rdtype == dns.rdatatype.SOA: 115 continue 116 # get DNSKEY (self-signed) 117 rrset = _check_query(ns, sub, dns.rdatatype.DNSKEY, None) 118 # get DS (signed by parent) 119 ds_rrset = _check_query(ns, sub, dns.rdatatype.DS, keys) 120 # verify that a signed DS validates DNSKEY 121 for ds in ds_rrset: 122 for dnskey in rrset: 123 htype = 'SHA256' if ds.digest_type == 2 else 'SHA1' 124 good_ds = dns.dnssec.make_ds(name, dnskey, htype) 125 if ds == good_ds: 126 break 127 else: 128 continue 129 break 130 else: 131 raise Exception("DS does not match DNSKEY") 132 # set key for next iteration 133 keys = {name: rrset} 134 # get TXT record (signed by zone) 135 rrset = _check_query(ns, url, _type, keys) 136 return rrset 137 138 139def query(url, rtype): 140 # 8.8.8.8 is Google's public DNS server 141 nameservers = ['8.8.8.8'] 142 ns = nameservers[0] 143 try: 144 out = _get_and_validate(ns, url, rtype) 145 validated = True 146 except Exception as e: 147 _logger.info(f"DNSSEC error: {repr(e)}") 148 out = dns.resolver.resolve(url, rtype) 149 validated = False 150 return out, validated 151