# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license # Copyright (C) 2003-2017 Nominum, Inc. # # Permission to use, copy, modify, and distribute this software and its # documentation for any purpose with or without fee is hereby granted, # provided that the above copyright notice and this permission notice # appear in all copies. # # THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES # WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF # MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR # ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES # WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN # ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT # OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. """Common DNSSEC-related functions and constants.""" from io import BytesIO import struct import time import dns.exception import dns.name import dns.node import dns.rdataset import dns.rdata import dns.rdatatype import dns.rdataclass from ._compat import string_types class UnsupportedAlgorithm(dns.exception.DNSException): """The DNSSEC algorithm is not supported.""" class ValidationFailure(dns.exception.DNSException): """The DNSSEC signature is invalid.""" #: RSAMD5 RSAMD5 = 1 #: DH DH = 2 #: DSA DSA = 3 #: ECC ECC = 4 #: RSASHA1 RSASHA1 = 5 #: DSANSEC3SHA1 DSANSEC3SHA1 = 6 #: RSASHA1NSEC3SHA1 RSASHA1NSEC3SHA1 = 7 #: RSASHA256 RSASHA256 = 8 #: RSASHA512 RSASHA512 = 10 #: ECDSAP256SHA256 ECDSAP256SHA256 = 13 #: ECDSAP384SHA384 ECDSAP384SHA384 = 14 #: INDIRECT INDIRECT = 252 #: PRIVATEDNS PRIVATEDNS = 253 #: PRIVATEOID PRIVATEOID = 254 _algorithm_by_text = { 'RSAMD5': RSAMD5, 'DH': DH, 'DSA': DSA, 'ECC': ECC, 'RSASHA1': RSASHA1, 'DSANSEC3SHA1': DSANSEC3SHA1, 'RSASHA1NSEC3SHA1': RSASHA1NSEC3SHA1, 'RSASHA256': RSASHA256, 'RSASHA512': RSASHA512, 'INDIRECT': INDIRECT, 'ECDSAP256SHA256': ECDSAP256SHA256, 'ECDSAP384SHA384': ECDSAP384SHA384, 'PRIVATEDNS': PRIVATEDNS, 'PRIVATEOID': PRIVATEOID, } # We construct the inverse mapping programmatically to ensure that we # cannot make any mistakes (e.g. omissions, cut-and-paste errors) that # would cause the mapping not to be true inverse. _algorithm_by_value = {y: x for x, y in _algorithm_by_text.items()} def algorithm_from_text(text): """Convert text into a DNSSEC algorithm value. Returns an ``int``. """ value = _algorithm_by_text.get(text.upper()) if value is None: value = int(text) return value def algorithm_to_text(value): """Convert a DNSSEC algorithm value to text Returns a ``str``. """ text = _algorithm_by_value.get(value) if text is None: text = str(value) return text def _to_rdata(record, origin): s = BytesIO() record.to_wire(s, origin=origin) return s.getvalue() def key_id(key, origin=None): """Return the key id (a 16-bit number) for the specified key. Note the *origin* parameter of this function is historical and is not needed. Returns an ``int`` between 0 and 65535. """ rdata = _to_rdata(key, origin) rdata = bytearray(rdata) if key.algorithm == RSAMD5: return (rdata[-3] << 8) + rdata[-2] else: total = 0 for i in range(len(rdata) // 2): total += (rdata[2 * i] << 8) + \ rdata[2 * i + 1] if len(rdata) % 2 != 0: total += rdata[len(rdata) - 1] << 8 total += ((total >> 16) & 0xffff) return total & 0xffff def make_ds(name, key, algorithm, origin=None): """Create a DS record for a DNSSEC key. *name* is the owner name of the DS record. *key* is a ``dns.rdtypes.ANY.DNSKEY``. *algorithm* is a string describing which hash algorithm to use. The currently supported hashes are "SHA1" and "SHA256". Case does not matter for these strings. *origin* is a ``dns.name.Name`` and will be used as the origin if *key* is a relative name. Returns a ``dns.rdtypes.ANY.DS``. """ if algorithm.upper() == 'SHA1': dsalg = 1 hash = SHA1.new() elif algorithm.upper() == 'SHA256': dsalg = 2 hash = SHA256.new() else: raise UnsupportedAlgorithm('unsupported algorithm "%s"' % algorithm) if isinstance(name, string_types): name = dns.name.from_text(name, origin) hash.update(name.canonicalize().to_wire()) hash.update(_to_rdata(key, origin)) digest = hash.digest() dsrdata = struct.pack("!HBB", key_id(key), key.algorithm, dsalg) + digest return dns.rdata.from_wire(dns.rdataclass.IN, dns.rdatatype.DS, dsrdata, 0, len(dsrdata)) def _find_candidate_keys(keys, rrsig): candidate_keys = [] value = keys.get(rrsig.signer) if value is None: return None if isinstance(value, dns.node.Node): try: rdataset = value.find_rdataset(dns.rdataclass.IN, dns.rdatatype.DNSKEY) except KeyError: return None else: rdataset = value for rdata in rdataset: if rdata.algorithm == rrsig.algorithm and \ key_id(rdata) == rrsig.key_tag: candidate_keys.append(rdata) return candidate_keys def _is_rsa(algorithm): return algorithm in (RSAMD5, RSASHA1, RSASHA1NSEC3SHA1, RSASHA256, RSASHA512) def _is_dsa(algorithm): return algorithm in (DSA, DSANSEC3SHA1) def _is_ecdsa(algorithm): return _have_ecdsa and (algorithm in (ECDSAP256SHA256, ECDSAP384SHA384)) def _is_md5(algorithm): return algorithm == RSAMD5 def _is_sha1(algorithm): return algorithm in (DSA, RSASHA1, DSANSEC3SHA1, RSASHA1NSEC3SHA1) def _is_sha256(algorithm): return algorithm in (RSASHA256, ECDSAP256SHA256) def _is_sha384(algorithm): return algorithm == ECDSAP384SHA384 def _is_sha512(algorithm): return algorithm == RSASHA512 def _make_hash(algorithm): if _is_md5(algorithm): return MD5.new() if _is_sha1(algorithm): return SHA1.new() if _is_sha256(algorithm): return SHA256.new() if _is_sha384(algorithm): return SHA384.new() if _is_sha512(algorithm): return SHA512.new() raise ValidationFailure('unknown hash for algorithm %u' % algorithm) def _make_algorithm_id(algorithm): if _is_md5(algorithm): oid = [0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x02, 0x05] elif _is_sha1(algorithm): oid = [0x2b, 0x0e, 0x03, 0x02, 0x1a] elif _is_sha256(algorithm): oid = [0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x01] elif _is_sha512(algorithm): oid = [0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x03] else: raise ValidationFailure('unknown algorithm %u' % algorithm) olen = len(oid) dlen = _make_hash(algorithm).digest_size idbytes = [0x30] + [8 + olen + dlen] + \ [0x30, olen + 4] + [0x06, olen] + oid + \ [0x05, 0x00] + [0x04, dlen] return struct.pack('!%dB' % len(idbytes), *idbytes) def _validate_rrsig(rrset, rrsig, keys, origin=None, now=None): """Validate an RRset against a single signature rdata The owner name of *rrsig* is assumed to be the same as the owner name of *rrset*. *rrset* is the RRset to validate. It can be a ``dns.rrset.RRset`` or a ``(dns.name.Name, dns.rdataset.Rdataset)`` tuple. *rrsig* is a ``dns.rdata.Rdata``, the signature to validate. *keys* is the key dictionary, used to find the DNSKEY associated with a given name. The dictionary is keyed by a ``dns.name.Name``, and has ``dns.node.Node`` or ``dns.rdataset.Rdataset`` values. *origin* is a ``dns.name.Name``, the origin to use for relative names. *now* is an ``int``, the time to use when validating the signatures, in seconds since the UNIX epoch. The default is the current time. """ if isinstance(origin, string_types): origin = dns.name.from_text(origin, dns.name.root) candidate_keys = _find_candidate_keys(keys, rrsig) if candidate_keys is None: raise ValidationFailure('unknown key') for candidate_key in candidate_keys: # For convenience, allow the rrset to be specified as a (name, # rdataset) tuple as well as a proper rrset if isinstance(rrset, tuple): rrname = rrset[0] rdataset = rrset[1] else: rrname = rrset.name rdataset = rrset if now is None: now = time.time() if rrsig.expiration < now: raise ValidationFailure('expired') if rrsig.inception > now: raise ValidationFailure('not yet valid') hash = _make_hash(rrsig.algorithm) if _is_rsa(rrsig.algorithm): keyptr = candidate_key.key (bytes_,) = struct.unpack('!B', keyptr[0:1]) keyptr = keyptr[1:] if bytes_ == 0: (bytes_,) = struct.unpack('!H', keyptr[0:2]) keyptr = keyptr[2:] rsa_e = keyptr[0:bytes_] rsa_n = keyptr[bytes_:] try: pubkey = CryptoRSA.construct( (number.bytes_to_long(rsa_n), number.bytes_to_long(rsa_e))) except ValueError: raise ValidationFailure('invalid public key') sig = rrsig.signature elif _is_dsa(rrsig.algorithm): keyptr = candidate_key.key (t,) = struct.unpack('!B', keyptr[0:1]) keyptr = keyptr[1:] octets = 64 + t * 8 dsa_q = keyptr[0:20] keyptr = keyptr[20:] dsa_p = keyptr[0:octets] keyptr = keyptr[octets:] dsa_g = keyptr[0:octets] keyptr = keyptr[octets:] dsa_y = keyptr[0:octets] pubkey = CryptoDSA.construct( (number.bytes_to_long(dsa_y), number.bytes_to_long(dsa_g), number.bytes_to_long(dsa_p), number.bytes_to_long(dsa_q))) sig = rrsig.signature[1:] elif _is_ecdsa(rrsig.algorithm): # use ecdsa for NIST-384p -- not currently supported by pycryptodome keyptr = candidate_key.key if rrsig.algorithm == ECDSAP256SHA256: curve = ecdsa.curves.NIST256p key_len = 32 elif rrsig.algorithm == ECDSAP384SHA384: curve = ecdsa.curves.NIST384p key_len = 48 x = number.bytes_to_long(keyptr[0:key_len]) y = number.bytes_to_long(keyptr[key_len:key_len * 2]) if not ecdsa.ecdsa.point_is_valid(curve.generator, x, y): raise ValidationFailure('invalid ECDSA key') point = ecdsa.ellipticcurve.Point(curve.curve, x, y, curve.order) verifying_key = ecdsa.keys.VerifyingKey.from_public_point(point, curve) pubkey = ECKeyWrapper(verifying_key, key_len) r = rrsig.signature[:key_len] s = rrsig.signature[key_len:] sig = ecdsa.ecdsa.Signature(number.bytes_to_long(r), number.bytes_to_long(s)) else: raise ValidationFailure('unknown algorithm %u' % rrsig.algorithm) hash.update(_to_rdata(rrsig, origin)[:18]) hash.update(rrsig.signer.to_digestable(origin)) if rrsig.labels < len(rrname) - 1: suffix = rrname.split(rrsig.labels + 1)[1] rrname = dns.name.from_text('*', suffix) rrnamebuf = rrname.to_digestable(origin) rrfixed = struct.pack('!HHI', rdataset.rdtype, rdataset.rdclass, rrsig.original_ttl) rrlist = sorted(rdataset) for rr in rrlist: hash.update(rrnamebuf) hash.update(rrfixed) rrdata = rr.to_digestable(origin) rrlen = struct.pack('!H', len(rrdata)) hash.update(rrlen) hash.update(rrdata) try: if _is_rsa(rrsig.algorithm): verifier = pkcs1_15.new(pubkey) # will raise ValueError if verify fails: verifier.verify(hash, sig) elif _is_dsa(rrsig.algorithm): verifier = DSS.new(pubkey, 'fips-186-3') verifier.verify(hash, sig) elif _is_ecdsa(rrsig.algorithm): digest = hash.digest() if not pubkey.verify(digest, sig): raise ValueError else: # Raise here for code clarity; this won't actually ever happen # since if the algorithm is really unknown we'd already have # raised an exception above raise ValidationFailure('unknown algorithm %u' % rrsig.algorithm) # If we got here, we successfully verified so we can return without error return except ValueError: # this happens on an individual validation failure continue # nothing verified -- raise failure: raise ValidationFailure('verify failure') def _validate(rrset, rrsigset, keys, origin=None, now=None): """Validate an RRset. *rrset* is the RRset to validate. It can be a ``dns.rrset.RRset`` or a ``(dns.name.Name, dns.rdataset.Rdataset)`` tuple. *rrsigset* is the signature RRset to be validated. It can be a ``dns.rrset.RRset`` or a ``(dns.name.Name, dns.rdataset.Rdataset)`` tuple. *keys* is the key dictionary, used to find the DNSKEY associated with a given name. The dictionary is keyed by a ``dns.name.Name``, and has ``dns.node.Node`` or ``dns.rdataset.Rdataset`` values. *origin* is a ``dns.name.Name``, the origin to use for relative names. *now* is an ``int``, the time to use when validating the signatures, in seconds since the UNIX epoch. The default is the current time. """ if isinstance(origin, string_types): origin = dns.name.from_text(origin, dns.name.root) if isinstance(rrset, tuple): rrname = rrset[0] else: rrname = rrset.name if isinstance(rrsigset, tuple): rrsigname = rrsigset[0] rrsigrdataset = rrsigset[1] else: rrsigname = rrsigset.name rrsigrdataset = rrsigset rrname = rrname.choose_relativity(origin) rrsigname = rrsigname.choose_relativity(origin) if rrname != rrsigname: raise ValidationFailure("owner names do not match") for rrsig in rrsigrdataset: try: _validate_rrsig(rrset, rrsig, keys, origin, now) return except ValidationFailure: pass raise ValidationFailure("no RRSIGs validated") def _need_pycrypto(*args, **kwargs): raise NotImplementedError("DNSSEC validation requires pycryptodome/pycryptodomex") try: try: # test we're using pycryptodome, not pycrypto (which misses SHA1 for example) from Crypto.Hash import MD5, SHA1, SHA256, SHA384, SHA512 from Crypto.PublicKey import RSA as CryptoRSA, DSA as CryptoDSA from Crypto.Signature import pkcs1_15, DSS from Crypto.Util import number except ImportError: from Cryptodome.Hash import MD5, SHA1, SHA256, SHA384, SHA512 from Cryptodome.PublicKey import RSA as CryptoRSA, DSA as CryptoDSA from Cryptodome.Signature import pkcs1_15, DSS from Cryptodome.Util import number except ImportError: validate = _need_pycrypto validate_rrsig = _need_pycrypto _have_pycrypto = False _have_ecdsa = False else: validate = _validate validate_rrsig = _validate_rrsig _have_pycrypto = True try: import ecdsa import ecdsa.ecdsa import ecdsa.ellipticcurve import ecdsa.keys except ImportError: _have_ecdsa = False else: _have_ecdsa = True class ECKeyWrapper(object): def __init__(self, key, key_len): self.key = key self.key_len = key_len def verify(self, digest, sig): diglong = number.bytes_to_long(digest) return self.key.pubkey.verifies(diglong, sig)