From: Bob Halley Date: Sun, 12 Dec 2010 17:25:58 +0000 (-0800) Subject: merge 1.9.2 master X-Git-Tag: v1.10.0-py3~41 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=8cb9e80abcf6818872096275c1ffb0dbe2fea7f8;p=thirdparty%2Fdnspython.git merge 1.9.2 master --- diff --git a/ChangeLog b/ChangeLog index 73a66ede..6a07424d 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,3 +1,88 @@ +2010-12-10 Bob Halley + + * dns/resolver.py (Resolver.query): disallow metaqueries. + + * dns/rdata.py (Rdata.__hash__): Added a __hash__ method for rdata. + +2010-11-23 Bob Halley + + * (Version 1.9.2 released) + +2010-11-23 Bob Halley + + * dns/dnssec.py (_need_pycrypto): DSA and RSA are modules, not + functions, and I didn't notice because the test suite masked + the bug! *sigh* + +2010-11-22 Bob Halley + + * (Version 1.9.1 released) + +2010-11-22 Bob Halley + + * dns/dnssec.py: the "from" style import used to get DSA from + PyCrypto trashed a DSA constant. Now a normal import is used + to avoid namespace contamination. + +2010-11-20 Bob Halley + + * (Version 1.9.0 released) + +2010-11-07 Bob Halley + + * dns/dnssec.py: Added validate() to do basic DNSSEC validation + (requires PyCrypto). Thanks to Brian Wellington for the patch. + + * dns/hash.py: Hash compatibility handling is now its own module. + +2010-10-31 Bob Halley + + * dns/resolver.py (zone_for_name): A query name resulting in a + CNAME or DNAME response to a node which had an SOA was incorrectly + treated as a zone origin. In these cases, we should just look + higher. Thanks to Gert Berger for reporting this problem. + + * Added zonediff.py to examples. This program compares two zones + and shows the differences either in diff-like plain text, or + HTML. Thanks to Dennis Kaarsemaker for contributing this + useful program. + +2010-10-27 Bob Halley + + * Incorporate a patch to use poll() instead of select() by + default on platforms which support it. Thanks to + Peter Schüller and Spotify for the contribution. + +2010-10-17 Bob Halley + + * Python prior to 2.5.2 doesn't compute the correct values for + HMAC-SHA384 and HMAC-SHA512. We now detect attempts to use + them and raise NotImplemented if the Python version is too old. + Thanks to Kevin Chen for reporting the problem. + + * Various routines that took the string forms of rdata types and + classes did not permit the strings to be Unicode strings. + Thanks to Ryan Workman for reporting the issue. + + * dns/tsig.py: Added symbolic constants for the algorithm strings. + E.g. you can now say dns.tsig.HMAC_MD5 instead of + "HMAC-MD5.SIG-ALG.REG.INT". Thanks to Cillian Sharkey for + suggesting this improvement. + + * dns/tsig.py (get_algorithm): fix hashlib compatibility; thanks to + Kevin Chen for the patch. + + * dns/dnssec.py: Added key_id() and make_ds(). + + * dns/message.py: message.py needs to import dns.edns since it uses + it. + +2010-05-04 Bob Halley + + * dns/rrset.py (RRset.__init__): "covers" was not passed to the + superclass __init__(). Thanks to Shanmuga Rajan for reporting + the problem. + 2010-03-10 Bob Halley * The TSIG algorithm value was passed to use_tsig() incorrectly diff --git a/README b/README index b313d1c1..bfc75b22 100644 --- a/README +++ b/README @@ -22,7 +22,62 @@ development by continuing to employ the author :). ABOUT THIS RELEASE -This is dnspython 1.8.0 +This is dnspython 1.9.2 + +New since 1.9.1: + + Nothing. + +Bugs fixed since 1.9.1: + + The dns.dnssec module didn't work at all due to missing + imports that escaped detection in testing because the test + suite also did the imports. The third time is the charm! + +New since 1.9.0: + + Nothing. + +Bugs fixed since 1.9.0: + + The dns.dnssec module didn't work with DSA due to namespace + contamination from a "from"-style import. + +New since 1.8.0: + + dnspython now uses poll() instead of select() when available. + + Basic DNSSEC validation can be done using dns.dnsec.validate() + and dns.dnssec.validate_rrsig() if you have PyCrypto 2.3 or + later installed. Complete secure resolution is not yet + available. + + Added key_id() to the DNSSEC module, which computes the DNSSEC + key id of a DNSKEY rdata. + + Added make_ds() to the DNSSEC module, which returns the DS RR + for a given DNSKEY rdata. + + dnspython now raises an exception if HMAC-SHA284 or + HMAC-SHA512 are used with a Python older than 2.5.2. (Older + Pythons do not compute the correct value.) + + Symbolic constants are now available for TSIG algorithm names. + +Bugs fixed since 1.8.0 + + dns.resolver.zone_for_name() didn't handle a query response + with a CNAME or DNAME correctly in some cases. + + When specifying rdata types and classes as text, Unicode + strings may now be used. + + Hashlib compatibility issues have been fixed. + + dns.message now imports dns.edns. + + The TSIG algorithm value was passed incorrectly to use_tsig() + in some cases. New since 1.7.1: @@ -310,7 +365,7 @@ the prior release. REQUIREMENTS -Python 2.2 or later. +Python 3.1 or later. INSTALLATION diff --git a/dns/__init__.py b/dns/__init__.py index aef737e7..87370581 100644 --- a/dns/__init__.py +++ b/dns/__init__.py @@ -22,6 +22,7 @@ __all__ = [ 'entropy', 'exception', 'flags', + 'hash', 'inet', 'ipv4', 'ipv6', diff --git a/dns/dnssec.py b/dns/dnssec.py index 5e3d4987..9c1d5f3d 100644 --- a/dns/dnssec.py +++ b/dns/dnssec.py @@ -15,6 +15,27 @@ """Common DNSSEC-related functions and constants.""" +import io +import struct +import time + +import dns.exception +import dns.hash +import dns.name +import dns.node +import dns.rdataset +import dns.rdata +import dns.rdatatype +import dns.rdataclass + +class UnsupportedAlgorithm(dns.exception.DNSException): + """Raised if an algorithm is not supported.""" + pass + +class ValidationFailure(dns.exception.DNSException): + """The DNSSEC signature is invalid.""" + pass + RSAMD5 = 1 DH = 2 DSA = 3 @@ -49,14 +70,10 @@ _algorithm_by_text = { _algorithm_by_value = dict([(y, x) for x, y in _algorithm_by_text.items()]) -class UnknownAlgorithm(Exception): - """Raised if an algorithm is unknown.""" - pass - def algorithm_from_text(text): """Convert text into a DNSSEC algorithm value @rtype: int""" - + value = _algorithm_by_text.get(text.upper()) if value is None: value = int(text) @@ -65,8 +82,292 @@ def algorithm_from_text(text): def algorithm_to_text(value): """Convert a DNSSEC algorithm value to text @rtype: string""" - + text = _algorithm_by_value.get(value) if text is None: text = str(value) return text + +def _to_rdata(record, origin): + s = io.StringIO() + record.to_wire(s, origin=origin) + return s.getvalue() + +def key_id(key, origin=None): + rdata = _to_rdata(key, origin) + 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): + if algorithm.upper() == 'SHA1': + dsalg = 1 + hash = dns.hash.get('SHA1')() + elif algorithm.upper() == 'SHA256': + dsalg = 2 + hash = dns.hash.get('SHA256')() + else: + raise UnsupportedAlgorithm('unsupported algorithm "%s"' % algorithm) + + if isinstance(name, (str, unicode)): + 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_key(keys, rrsig): + value = keys.get(rrsig.signer) + if value is None: + return None + if isinstance(value, dns.node.Node): + try: + rdataset = node.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: + return rdata + return None + +def _is_rsa(algorithm): + return algorithm in (RSAMD5, RSASHA1, + RSASHA1NSEC3SHA1, RSASHA256, + RSASHA512) + +def _is_dsa(algorithm): + return algorithm in (DSA, DSANSEC3SHA1) + +def _is_md5(algorithm): + return algorithm == RSAMD5 + +def _is_sha1(algorithm): + return algorithm in (DSA, RSASHA1, + DSANSEC3SHA1, RSASHA1NSEC3SHA1) + +def _is_sha256(algorithm): + return algorithm == RSASHA256 + +def _is_sha512(algorithm): + return algorithm == RSASHA512 + +def _make_hash(algorithm): + if _is_md5(algorithm): + return dns.hash.get('MD5')() + if _is_sha1(algorithm): + return dns.hash.get('SHA1')() + if _is_sha256(algorithm): + return dns.hash.get('SHA256')() + if _is_sha512(algorithm): + return dns.hash.get('SHA512')() + 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 bytes(idbytes) + +def _validate_rrsig(rrset, rrsig, keys, origin=None, now=None): + """Validate an RRset against a single signature rdata + + The owner name of the rrsig is assumed to be the same as the owner name + of the rrset. + + @param rrset: The RRset to validate + @type rrset: dns.rrset.RRset or (dns.name.Name, dns.rdataset.Rdataset) + tuple + @param rrsig: The signature rdata + @type rrsig: dns.rrset.Rdata + @param keys: The key dictionary. + @type keys: a dictionary keyed by dns.name.Name with node or rdataset values + @param origin: The origin to use for relative names + @type origin: dns.name.Name or None + @param now: The time to use when validating the signatures. The default + is the current time. + @type now: int + """ + + if isinstance(origin, (str, unicode)): + origin = dns.name.from_text(origin, dns.name.root) + + key = _find_key(keys, rrsig) + if not key: + raise ValidationFailure('unknown key') + + # 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 = 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:] + keylen = len(rsa_n) * 8 + pubkey = Crypto.PublicKey.RSA.construct( + (Crypto.Util.number.bytes_to_long(rsa_n), + Crypto.Util.number.bytes_to_long(rsa_e))) + sig = (Crypto.Util.number.bytes_to_long(rrsig.signature),) + elif _is_dsa(rrsig.algorithm): + keyptr = 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 = Crypto.PublicKey.DSA.construct( + (Crypto.Util.number.bytes_to_long(dsa_y), + Crypto.Util.number.bytes_to_long(dsa_g), + Crypto.Util.number.bytes_to_long(dsa_p), + Crypto.Util.number.bytes_to_long(dsa_q))) + (dsa_r, dsa_s) = struct.unpack('!20s20s', rrsig.signature[1:]) + sig = (Crypto.Util.number.bytes_to_long(dsa_r), + Crypto.Util.number.bytes_to_long(dsa_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) + + digest = hash.digest() + + if _is_rsa(rrsig.algorithm): + # PKCS1 algorithm identifier goop + digest = _make_algorithm_id(rrsig.algorithm) + digest + padlen = keylen / 8 - len(digest) - 3 + digest = bytes(0) + bytes(1) + bytes(0xFF) * padlen + bytes(0) + \ + digest + elif _is_dsa(rrsig.algorithm): + pass + 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 not pubkey.verify(digest, sig): + raise ValidationFailure('verify failure') + +def _validate(rrset, rrsigset, keys, origin=None, now=None): + """Validate an RRset + + @param rrset: The RRset to validate + @type rrset: dns.rrset.RRset or (dns.name.Name, dns.rdataset.Rdataset) + tuple + @param rrsigset: The signature RRset + @type rrsigset: dns.rrset.RRset or (dns.name.Name, dns.rdataset.Rdataset) + tuple + @param keys: The key dictionary. + @type keys: a dictionary keyed by dns.name.Name with node or rdataset values + @param origin: The origin to use for relative names + @type origin: dns.name.Name or None + @param now: The time to use when validating the signatures. The default + is the current time. + @type now: int + """ + + if isinstance(origin, (str, unicode)): + 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 = rrname.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 pycrypto") + +try: + import Crypto.PublicKey.RSA + import Crypto.PublicKey.DSA + import Crypto.Util.number + validate = _validate + validate_rrsig = _validate_rrsig +except ImportError: + validate = _need_pycrypto + validate_rrsig = _need_pycrypto diff --git a/dns/hash.py b/dns/hash.py new file mode 100644 index 00000000..00f979ba --- /dev/null +++ b/dns/hash.py @@ -0,0 +1,30 @@ +# Copyright (C) 2010 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. + +"""Hashing backwards compatibility wrapper""" + +import sys +import hashlib + +_hashes = {'MD5' : hashlib.md5, + 'SHA1' : hashlib.sha1, + 'SHA224' : hashlib.sha224, + 'SHA256' : hashlib.sha256, + 'SHA384' : hashlib.sha384, + 'SHA512' : hashlib.sha512, + } + +def get(algorithm): + return _hashes[algorithm.upper()] diff --git a/dns/message.py b/dns/message.py index 54803e14..ca17e5de 100644 --- a/dns/message.py +++ b/dns/message.py @@ -21,6 +21,7 @@ import struct import sys import time +import dns.edns import dns.exception import dns.flags import dns.name @@ -92,8 +93,11 @@ class Message(object): @type keyring: dict @ivar keyname: The TSIG keyname to use. The default is None. @type keyname: dns.name.Name object - @ivar keyalgorithm: The TSIG key algorithm to use. The default is - dns.tsig.default_algorithm. + @ivar keyalgorithm: The TSIG algorithm to use; defaults to + dns.tsig.default_algorithm. Constants for TSIG algorithms are defined + in dns.tsig, and the currently implemented algorithms are + HMAC_MD5, HMAC_SHA1, HMAC_SHA224, HMAC_SHA256, HMAC_SHA384, and + HMAC_SHA512. @type keyalgorithm: string @ivar request_mac: The TSIG MAC of the request message associated with this message; used when validating TSIG signatures. @see: RFC 2845 for diff --git a/dns/query.py b/dns/query.py index cda2309e..05651984 100644 --- a/dns/query.py +++ b/dns/query.py @@ -45,7 +45,59 @@ def _compute_expiration(timeout): else: return time.time() + timeout -def _wait_for(ir, iw, ix, expiration): +def _poll_for(fd, readable, writable, error, timeout): + """ + @param fd: File descriptor (int). + @param readable: Whether to wait for readability (bool). + @param writable: Whether to wait for writability (bool). + @param expiration: Deadline timeout (expiration time, in seconds (float)). + + @return True on success, False on timeout + """ + event_mask = 0 + if readable: + event_mask |= select.POLLIN + if writable: + event_mask |= select.POLLOUT + if error: + event_mask |= select.POLLERR + + pollable = select.poll() + pollable.register(fd, event_mask) + + if timeout: + event_list = pollable.poll(long(timeout * 1000)) + else: + event_list = pollable.poll() + + return bool(event_list) + +def _select_for(fd, readable, writable, error, timeout): + """ + @param fd: File descriptor (int). + @param readable: Whether to wait for readability (bool). + @param writable: Whether to wait for writability (bool). + @param expiration: Deadline timeout (expiration time, in seconds (float)). + + @return True on success, False on timeout + """ + rset, wset, xset = [], [], [] + + if readable: + rset = [fd] + if writable: + wset = [fd] + if error: + xset = [fd] + + if timeout is None: + (rcount, wcount, xcount) = select.select(rset, wset, xset) + else: + (rcount, wcount, xcount) = select.select(rset, wset, xset, timeout) + + return bool((rcount or wcount or xcount)) + +def _wait_for(fd, readable, writable, error, expiration): done = False while not done: if expiration is None: @@ -55,22 +107,34 @@ def _wait_for(ir, iw, ix, expiration): if timeout <= 0.0: raise dns.exception.Timeout try: - if timeout is None: - (r, w, x) = select.select(ir, iw, ix) - else: - (r, w, x) = select.select(ir, iw, ix, timeout) + if not _polling_backend(fd, readable, writable, error, timeout): + raise dns.exception.Timeout except select.error as e: if e.args[0] != errno.EINTR: raise e done = True - if len(r) == 0 and len(w) == 0 and len(x) == 0: - raise dns.exception.Timeout + +def _set_polling_backend(fn): + """ + Internal API. Do not use. + """ + global _polling_backend + + _polling_backend = fn + +if hasattr(select, 'poll'): + # Prefer poll() on platforms that support it because it has no + # limits on the maximum value of a file descriptor (plus it will + # be more efficient for high values). + _polling_backend = _poll_for +else: + _polling_backend = _select_for def _wait_for_readable(s, expiration): - _wait_for([s], [], [s], expiration) + _wait_for(s, True, False, True, expiration) def _wait_for_writable(s, expiration): - _wait_for([], [s], [s], expiration) + _wait_for(s, False, True, True, expiration) def _addresses_equal(af, a1, a2): # Convert the first value of the tuple, which is a textual format diff --git a/dns/rdata.py b/dns/rdata.py index fe4547df..1feec760 100644 --- a/dns/rdata.py +++ b/dns/rdata.py @@ -29,6 +29,7 @@ import base64 import io import dns.exception +import dns.name import dns.rdataclass import dns.rdatatype import dns.tokenizer @@ -256,6 +257,9 @@ class Rdata(object): return dns.util.cmp((self.rdclass, self.rdtype), (other.rdclass, other.rdtype)) return self._cmp(other) > 0 + def __hash__(self): + return hash(self.to_digestable(dns.name.root)) + @classmethod def from_text(cls, rdclass, rdtype, tok, origin = None, relativize = True): """Build an rdata object from text format. diff --git a/dns/resolver.py b/dns/resolver.py index 3512b4b8..db8a1b5d 100644 --- a/dns/resolver.py +++ b/dns/resolver.py @@ -61,6 +61,9 @@ class NoRootSOA(dns.exception.DNSException): This should never happen!""" pass +class NoMetaqueries(dns.exception.DNSException): + """Metaqueries are not allowed.""" + pass class Answer(object): """DNS stub resolver answer @@ -566,8 +569,12 @@ class Resolver(object): qname = dns.name.from_text(qname, None) if isinstance(rdtype, str): rdtype = dns.rdatatype.from_text(rdtype) + if dns.rdatatype.is_metatype(rdtype): + raise NoMetaqueries if isinstance(rdclass, str): rdclass = dns.rdataclass.from_text(rdclass) + if dns.rdataclass.is_metaclass(rdclass): + raise NoMetaqueries qnames_to_try = [] if qname.is_absolute(): qnames_to_try.append(qname) @@ -749,9 +756,12 @@ def zone_for_name(name, rdclass=dns.rdataclass.IN, tcp=False, resolver=None): while 1: try: answer = resolver.query(name, dns.rdatatype.SOA, rdclass, tcp) - return name + if answer.rrset.name == name: + return name + # otherwise we were CNAMEd or DNAMEd and need to look higher except (dns.resolver.NXDOMAIN, dns.resolver.NoAnswer): - try: - name = name.parent() - except dns.name.NoParent: - raise NoRootSOA + pass + try: + name = name.parent() + except dns.name.NoParent: + raise NoRootSOA diff --git a/dns/rrset.py b/dns/rrset.py index 31711924..0ba7a3e2 100644 --- a/dns/rrset.py +++ b/dns/rrset.py @@ -36,7 +36,7 @@ class RRset(dns.rdataset.Rdataset): deleting=None): """Create a new RRset.""" - super(RRset, self).__init__(rdclass, rdtype) + super(RRset, self).__init__(rdclass, rdtype, covers) self.name = name self.deleting = deleting diff --git a/dns/tsig.py b/dns/tsig.py index 2479d09b..e1f706a6 100644 --- a/dns/tsig.py +++ b/dns/tsig.py @@ -18,6 +18,7 @@ import hashlib import hmac import struct +import sys import dns.exception import dns.rdataclass @@ -51,7 +52,16 @@ class PeerBadTruncation(PeerError): """Raised if the peer didn't like amount of truncation in the TSIG we sent""" pass -default_algorithm = "HMAC-MD5.SIG-ALG.REG.INT" +# TSIG Algorithms + +HMAC_MD5 = dns.name.from_text("HMAC-MD5.SIG-ALG.REG.INT") +HMAC_SHA1 = dns.name.from_text("hmac-sha1") +HMAC_SHA224 = dns.name.from_text("hmac-sha224") +HMAC_SHA256 = dns.name.from_text("hmac-sha256") +HMAC_SHA384 = dns.name.from_text("hmac-sha384") +HMAC_SHA512 = dns.name.from_text("hmac-sha512") + +default_algorithm = HMAC_MD5 BADSIG = 16 BADKEY = 17 @@ -167,6 +177,24 @@ def validate(wire, keyname, secret, now, request_mac, tsig_start, tsig_rdata, raise BadSignature return ctx +_hashes = None + +def _maybe_add_hash(tsig_alg, hash_alg): + try: + _hashes[tsig_alg] = dns.hash.get(hash_alg) + except KeyError: + pass + +def _setup_hashes(): + global _hashes + _hashes = {} + _maybe_add_hash(HMAC_SHA224, 'SHA224') + _maybe_add_hash(HMAC_SHA256, 'SHA256') + _maybe_add_hash(HMAC_SHA384, 'SHA384') + _maybe_add_hash(HMAC_SHA512, 'SHA512') + _maybe_add_hash(HMAC_SHA1, 'SHA1') + _maybe_add_hash(HMAC_MD5, 'MD5') + def get_algorithm(algorithm): """Returns the wire format string and the hash module to use for the specified TSIG algorithm @@ -175,19 +203,15 @@ def get_algorithm(algorithm): @raises NotImplementedError: I{algorithm} is not supported """ - hashes = {} - hashes[dns.name.from_text('hmac-sha224')] = hashlib.sha224 - hashes[dns.name.from_text('hmac-sha256')] = hashlib.sha256 - hashes[dns.name.from_text('hmac-sha384')] = hashlib.sha384 - hashes[dns.name.from_text('hmac-sha512')] = hashlib.sha512 - hashes[dns.name.from_text('hmac-sha1')] = hashlib.sha1 - hashes[dns.name.from_text('HMAC-MD5.SIG-ALG.REG.INT')] = hashlib.md5 + global _hashes + if _hashes is None: + _setup_hashes() - if isinstance(algorithm, (str, unicode)): + if isinstance(algorithm, str): algorithm = dns.name.from_text(algorithm) - if algorithm in hashes: - return (algorithm.to_digestable(), hashes[algorithm]) - - raise NotImplementedError("TSIG algorithm " + str(algorithm) + - " is not supported") + try: + return (algorithm.to_digestable(), _hashes[algorithm]) + except KeyError: + raise NotImplementedError("TSIG algorithm " + str(algorithm) + + " is not supported") diff --git a/dns/update.py b/dns/update.py index 4a1a3739..baad00e9 100644 --- a/dns/update.py +++ b/dns/update.py @@ -21,6 +21,7 @@ import dns.opcode import dns.rdata import dns.rdataclass import dns.rdataset +import dns.tsig class Update(dns.message.Message): def __init__(self, zone, rdclass=dns.rdataclass.IN, keyring=None, @@ -42,7 +43,10 @@ class Update(dns.message.Message): they know the keyring contains only one key. @type keyname: dns.name.Name or string @param keyalgorithm: The TSIG algorithm to use; defaults to - dns.tsig.default_algorithm + dns.tsig.default_algorithm. Constants for TSIG algorithms are defined + in dns.tsig, and the currently implemented algorithms are + HMAC_MD5, HMAC_SHA1, HMAC_SHA224, HMAC_SHA256, HMAC_SHA384, and + HMAC_SHA512. @type keyalgorithm: string """ super(Update, self).__init__() diff --git a/dns/version.py b/dns/version.py index dd135a13..46799a77 100644 --- a/dns/version.py +++ b/dns/version.py @@ -16,8 +16,8 @@ """dnspython release version information.""" MAJOR = 1 -MINOR = 8 -MICRO = 1 +MINOR = 9 +MICRO = 3 RELEASELEVEL = 0x0f SERIAL = 0 diff --git a/examples/zonediff.py b/examples/zonediff.py new file mode 100755 index 00000000..ad81fb1d --- /dev/null +++ b/examples/zonediff.py @@ -0,0 +1,270 @@ +#!/usr/bin/env python +# +# Small library and commandline tool to do logical diffs of zonefiles +# ./zonediff -h gives you help output +# +# Requires dnspython to do all the heavy lifting +# +# (c)2009 Dennis Kaarsemaker +# +# 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 THE AUTHOR DISCLAIMS ALL WARRANTIES +# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR 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. +"""See diff_zones.__doc__ for more information""" + +__all__ = ['diff_zones', 'format_changes_plain', 'format_changes_html'] + +try: + import dns.zone +except ImportError: + import sys + sys.stderr.write("Please install dnspython") + sys.exit(1) + +def diff_zones(zone1, zone2, ignore_ttl=False, ignore_soa=False): + """diff_zones(zone1, zone2, ignore_ttl=False, ignore_soa=False) -> changes + Compares two dns.zone.Zone objects and returns a list of all changes + in the format (name, oldnode, newnode). + + If ignore_ttl is true, a node will not be added to this list if the + only change is its TTL. + + If ignore_soa is true, a node will not be added to this list if the + only changes is a change in a SOA Rdata set. + + The returned nodes do include all Rdata sets, including unchanged ones. + """ + + changes = [] + for name in zone1: + name = str(name) + n1 = zone1.get_node(name) + n2 = zone2.get_node(name) + if not n2: + changes.append((str(name), n1, n2)) + elif _nodes_differ(n1, n2, ignore_ttl, ignore_soa): + changes.append((str(name), n1, n2)) + + for name in zone2: + n1 = zone1.get_node(name) + if not n1: + n2 = zone2.get_node(name) + changes.append((str(name), n1, n2)) + return changes + +def _nodes_differ(n1, n2, ignore_ttl, ignore_soa): + if ignore_soa or not ignore_ttl: + # Compare datasets directly + for r in n1.rdatasets: + if ignore_soa and r.rdtype == dns.rdatatype.SOA: + continue + if r not in n2.rdatasets: + return True + if not ignore_ttl: + return r.ttl != n2.find_rdataset(r.rdclass, r.rdtype).ttl + + for r in n2.rdatasets: + if ignore_soa and r.rdtype == dns.rdatatype.SOA: + continue + if r not in n1.rdatasets: + return True + else: + return n1 != n2 + +def format_changes_plain(oldf, newf, changes, ignore_ttl=False): + """format_changes(oldfile, newfile, changes, ignore_ttl=False) -> str + Given 2 filenames and a list of changes from diff_zones, produce diff-like + output. If ignore_ttl is True, TTL-only changes are not displayed""" + + ret = "--- %s\n+++ %s\n" % (oldf, newf) + for name, old, new in changes: + ret += "@ %s\n" % name + if not old: + for r in new.rdatasets: + ret += "+ %s\n" % str(r).replace('\n','\n+ ') + elif not new: + for r in old.rdatasets: + ret += "- %s\n" % str(r).replace('\n','\n+ ') + else: + for r in old.rdatasets: + if r not in new.rdatasets or (r.ttl != new.find_rdataset(r.rdclass, r.rdtype).ttl and not ignore_ttl): + ret += "- %s\n" % str(r).replace('\n','\n+ ') + for r in new.rdatasets: + if r not in old.rdatasets or (r.ttl != old.find_rdataset(r.rdclass, r.rdtype).ttl and not ignore_ttl): + ret += "+ %s\n" % str(r).replace('\n','\n+ ') + return ret + +def format_changes_html(oldf, newf, changes, ignore_ttl=False): + """format_changes(oldfile, newfile, changes, ignore_ttl=False) -> str + Given 2 filenames and a list of changes from diff_zones, produce nice html + output. If ignore_ttl is True, TTL-only changes are not displayed""" + + ret = ''' + + + + + + + + \n''' % (oldf, newf) + + for name, old, new in changes: + ret += ' \n \n' % name + if not old: + for r in new.rdatasets: + ret += ' \n \n' % str(r).replace('\n','
') + elif not new: + for r in old.rdatasets: + ret += ' \n \n' % str(r).replace('\n','
') + else: + ret += ' \n' + ret += ' \n' + ret += ' \n' + return ret + ' \n
 %s%s
%s %s%s ' + for r in old.rdatasets: + if r not in new.rdatasets or (r.ttl != new.find_rdataset(r.rdclass, r.rdtype).ttl and not ignore_ttl): + ret += str(r).replace('\n','
') + ret += '
' + for r in new.rdatasets: + if r not in old.rdatasets or (r.ttl != old.find_rdataset(r.rdclass, r.rdtype).ttl and not ignore_ttl): + ret += str(r).replace('\n','
') + ret += '
' + +# Make this module usable as a script too. +if __name__ == '__main__': + import optparse + import subprocess + import sys + import traceback + + usage = """%prog zonefile1 zonefile2 - Show differences between zones in a diff-like format +%prog [--git|--bzr|--rcs] zonefile rev1 [rev2] - Show differences between two revisions of a zonefile + +The differences shown will be logical differences, not textual differences. +""" + p = optparse.OptionParser(usage=usage) + p.add_option('-s', '--ignore-soa', action="store_true", default=False, dest="ignore_soa", + help="Ignore SOA-only changes to records") + p.add_option('-t', '--ignore-ttl', action="store_true", default=False, dest="ignore_ttl", + help="Ignore TTL-only changes to Rdata") + p.add_option('-T', '--traceback', action="store_true", default=False, dest="tracebacks", + help="Show python tracebacks when errors occur") + p.add_option('-H', '--html', action="store_true", default=False, dest="html", + help="Print HTML output") + p.add_option('-g', '--git', action="store_true", default=False, dest="use_git", + help="Use git revisions instead of real files") + p.add_option('-b', '--bzr', action="store_true", default=False, dest="use_bzr", + help="Use bzr revisions instead of real files") + p.add_option('-r', '--rcs', action="store_true", default=False, dest="use_rcs", + help="Use rcs revisions instead of real files") + opts, args = p.parse_args() + opts.use_vc = opts.use_git or opts.use_bzr or opts.use_rcs + + def _open(what, err): + if isinstance(what, basestring): + # Open as normal file + try: + return open(what, 'rb') + except: + sys.stderr.write(err + "\n") + if opts.tracebacks: + traceback.print_exc() + else: + # Must be a list, open subprocess + try: + proc = subprocess.Popen(what, stdout=subprocess.PIPE) + proc.wait() + if proc.returncode == 0: + return proc.stdout + sys.stderr.write(err + "\n") + except: + sys.stderr.write(err + "\n") + if opts.tracebacks: + traceback.print_exc() + + if not opts.use_vc and len(args) != 2: + p.print_help() + sys.exit(64) + if opts.use_vc and len(args) not in (2,3): + p.print_help() + sys.exit(64) + + # Open file desriptors + if not opts.use_vc: + oldn, newn = args + else: + if len(args) == 3: + filename, oldr, newr = args + oldn = "%s:%s" % (oldr, filename) + newn = "%s:%s" % (newr, filename) + else: + filename, oldr = args + newr = None + oldn = "%s:%s" % (oldr, filename) + newn = filename + + + old, new = None, None + oldz, newz = None, None + if opts.use_bzr: + old = _open(["bzr", "cat", "-r" + oldr, filename], + "Unable to retrieve revision %s of %s" % (oldr, filename)) + if newr != None: + new = _open(["bzr", "cat", "-r" + newr, filename], + "Unable to retrieve revision %s of %s" % (newr, filename)) + elif opts.use_git: + old = _open(["git", "show", oldn], + "Unable to retrieve revision %s of %s" % (oldr, filename)) + if newr != None: + new = _open(["git", "show", newn], + "Unable to retrieve revision %s of %s" % (newr, filename)) + elif opts.use_rcs: + old = _open(["co", "-q", "-p", "-r" + oldr, filename], + "Unable to retrieve revision %s of %s" % (oldr, filename)) + if newr != None: + new = _open(["co", "-q", "-p", "-r" + newr, filename], + "Unable to retrieve revision %s of %s" % (newr, filename)) + if not opts.use_vc: + old = _open(oldn, "Unable to open %s" % oldn) + if not opts.use_vc or newr == None: + new = _open(newn, "Unable to open %s" % newn) + + if not old or not new: + sys.exit(65) + + # Parse the zones + try: + oldz = dns.zone.from_file(old, origin = '.', check_origin=False) + except dns.exception.DNSException: + sys.stderr.write("Incorrect zonefile: %s\n", old) + if opts.tracebacks: + traceback.print_exc() + try: + newz = dns.zone.from_file(new, origin = '.', check_origin=False) + except dns.exception.DNSException: + sys.stderr.write("Incorrect zonefile: %s\n" % new) + if opts.tracebacks: + traceback.print_exc() + if not oldz or not newz: + sys.exit(65) + + changes = diff_zones(oldz, newz, opts.ignore_ttl, opts.ignore_soa) + changes.sort() + + if not changes: + sys.exit(0) + if opts.html: + print format_changes_html(oldn, newn, changes, opts.ignore_ttl) + else: + print format_changes_plain(oldn, newn, changes, opts.ignore_ttl) + sys.exit(1) diff --git a/setup.py b/setup.py index 21ebddfb..f84711f7 100755 --- a/setup.py +++ b/setup.py @@ -18,7 +18,7 @@ import sys from distutils.core import setup -version = '1.8.1' +version = '1.9.3' kwargs = { 'name' : 'dnspython', diff --git a/tests/dnssec.py b/tests/dnssec.py new file mode 100644 index 00000000..371a30c7 --- /dev/null +++ b/tests/dnssec.py @@ -0,0 +1,146 @@ +# Copyright (C) 2010 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. + +import unittest + +import dns.dnssec +import dns.name +import dns.rdata +import dns.rdataclass +import dns.rdatatype +import dns.rrset + +abs_dnspython_org = dns.name.from_text('dnspython.org') + +abs_keys = { abs_dnspython_org : + dns.rrset.from_text('dnspython.org.', 3600, 'IN', 'DNSKEY', + '257 3 5 AwEAAenVTr9L1OMlL1/N2ta0Qj9LLLnnmFWIr1dJoAsWM9BQfsbV7kFZ XbAkER/FY9Ji2o7cELxBwAsVBuWn6IUUAJXLH74YbC1anY0lifjgt29z SwDzuB7zmC7yVYZzUunBulVW4zT0tg1aePbpVL2EtTL8VzREqbJbE25R KuQYHZtFwG8S4iBxJUmT2Bbd0921LLxSQgVoFXlQx/gFV2+UERXcJ5ce iX6A6wc02M/pdg/YbJd2rBa0MYL3/Fz/Xltre0tqsImZGxzi6YtYDs45 NC8gH+44egz82e2DATCVM1ICPmRDjXYTLldQiWA2ZXIWnK0iitl5ue24 7EsWJefrIhE=', + '256 3 5 AwEAAdSSghOGjU33IQZgwZM2Hh771VGXX05olJK49FxpSyuEAjDBXY58 LGU9R2Zgeecnk/b9EAhFu/vCV9oECtiTCvwuVAkt9YEweqYDluQInmgP NGMJCKdSLlnX93DkjDw8rMYv5dqXCuSGPlKChfTJOLQxIAxGloS7lL+c 0CTZydAF') + } + +rel_keys = { dns.name.empty : + dns.rrset.from_text('@', 3600, 'IN', 'DNSKEY', + '257 3 5 AwEAAenVTr9L1OMlL1/N2ta0Qj9LLLnnmFWIr1dJoAsWM9BQfsbV7kFZ XbAkER/FY9Ji2o7cELxBwAsVBuWn6IUUAJXLH74YbC1anY0lifjgt29z SwDzuB7zmC7yVYZzUunBulVW4zT0tg1aePbpVL2EtTL8VzREqbJbE25R KuQYHZtFwG8S4iBxJUmT2Bbd0921LLxSQgVoFXlQx/gFV2+UERXcJ5ce iX6A6wc02M/pdg/YbJd2rBa0MYL3/Fz/Xltre0tqsImZGxzi6YtYDs45 NC8gH+44egz82e2DATCVM1ICPmRDjXYTLldQiWA2ZXIWnK0iitl5ue24 7EsWJefrIhE=', + '256 3 5 AwEAAdSSghOGjU33IQZgwZM2Hh771VGXX05olJK49FxpSyuEAjDBXY58 LGU9R2Zgeecnk/b9EAhFu/vCV9oECtiTCvwuVAkt9YEweqYDluQInmgP NGMJCKdSLlnX93DkjDw8rMYv5dqXCuSGPlKChfTJOLQxIAxGloS7lL+c 0CTZydAF') + } + +when = 1290250287 + +abs_soa = dns.rrset.from_text('dnspython.org.', 3600, 'IN', 'SOA', + 'howl.dnspython.org. hostmaster.dnspython.org. 2010020047 3600 1800 604800 3600') + +abs_other_soa = dns.rrset.from_text('dnspython.org.', 3600, 'IN', 'SOA', + 'foo.dnspython.org. hostmaster.dnspython.org. 2010020047 3600 1800 604800 3600') + +abs_soa_rrsig = dns.rrset.from_text('dnspython.org.', 3600, 'IN', 'RRSIG', + 'SOA 5 2 3600 20101127004331 20101119213831 61695 dnspython.org. sDUlltRlFTQw5ITFxOXW3TgmrHeMeNpdqcZ4EXxM9FHhIlte6V9YCnDw t6dvM9jAXdIEi03l9H/RAd9xNNW6gvGMHsBGzpvvqFQxIBR2PoiZA1mX /SWHZFdbt4xjYTtXqpyYvrMK0Dt7bUYPadyhPFCJ1B+I8Zi7B5WJEOd0 8vs=') + +rel_soa = dns.rrset.from_text('@', 3600, 'IN', 'SOA', + 'howl hostmaster 2010020047 3600 1800 604800 3600') + +rel_other_soa = dns.rrset.from_text('@', 3600, 'IN', 'SOA', + 'foo hostmaster 2010020047 3600 1800 604800 3600') + +rel_soa_rrsig = dns.rrset.from_text('@', 3600, 'IN', 'RRSIG', + 'SOA 5 2 3600 20101127004331 20101119213831 61695 @ sDUlltRlFTQw5ITFxOXW3TgmrHeMeNpdqcZ4EXxM9FHhIlte6V9YCnDw t6dvM9jAXdIEi03l9H/RAd9xNNW6gvGMHsBGzpvvqFQxIBR2PoiZA1mX /SWHZFdbt4xjYTtXqpyYvrMK0Dt7bUYPadyhPFCJ1B+I8Zi7B5WJEOd0 8vs=') + +sep_key = dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.DNSKEY, + '257 3 5 AwEAAenVTr9L1OMlL1/N2ta0Qj9LLLnnmFWIr1dJoAsWM9BQfsbV7kFZ XbAkER/FY9Ji2o7cELxBwAsVBuWn6IUUAJXLH74YbC1anY0lifjgt29z SwDzuB7zmC7yVYZzUunBulVW4zT0tg1aePbpVL2EtTL8VzREqbJbE25R KuQYHZtFwG8S4iBxJUmT2Bbd0921LLxSQgVoFXlQx/gFV2+UERXcJ5ce iX6A6wc02M/pdg/YbJd2rBa0MYL3/Fz/Xltre0tqsImZGxzi6YtYDs45 NC8gH+44egz82e2DATCVM1ICPmRDjXYTLldQiWA2ZXIWnK0iitl5ue24 7EsWJefrIhE=') + +good_ds = dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.DS, + '57349 5 2 53A79A3E7488AB44FFC56B2D1109F0699D1796DD977E72108B841F96 E47D7013') + +when2 = 1290425644 + +abs_example = dns.name.from_text('example') + +abs_dsa_keys = { abs_example : + dns.rrset.from_text('example.', 86400, 'IN', 'DNSKEY', + '257 3 3 CI3nCqyJsiCJHTjrNsJOT4RaszetzcJPYuoH3F9ZTVt3KJXncCVR3bwn 1w0iavKljb9hDlAYSfHbFCp4ic/rvg4p1L8vh5s8ToMjqDNl40A0hUGQ Ybx5hsECyK+qHoajilUX1phYSAD8d9WAGO3fDWzUPBuzR7o85NiZCDxz yXuNVfni0uhj9n1KYhEO5yAbbruDGN89wIZcxMKuQsdUY2GYD93ssnBv a55W6XRABYWayKZ90WkRVODLVYLSn53Pj/wwxGH+XdhIAZJXimrZL4yl My7rtBsLMqq8Ihs4Tows7LqYwY7cp6y/50tw6pj8tFqMYcPUjKZV36l1 M/2t5BVg3i7IK61Aidt6aoC3TDJtzAxg3ZxfjZWJfhHjMJqzQIfbW5b9 q1mjFsW5EUv39RaNnX+3JWPRLyDqD4pIwDyqfutMsdk/Py3paHn82FGp CaOg+nicqZ9TiMZURN/XXy5JoXUNQ3RNvbHCUiPUe18KUkY6mTfnyHld 1l9YCWmzXQVClkx/hOYxjJ4j8Ife58+Obu5X', + '256 3 3 CJE1yb9YRQiw5d2xZrMUMR+cGCTt1bp1KDCefmYKmS+Z1+q9f42ETVhx JRiQwXclYwmxborzIkSZegTNYIV6mrYwbNB27Q44c3UGcspb3PiOw5TC jNPRYEcdwGvDZ2wWy+vkSV/S9tHXY8O6ODiE6abZJDDg/RnITyi+eoDL R3KZ5n/V1f1T1b90rrV6EewhBGQJpQGDogaXb2oHww9Tm6NfXyo7SoMM pbwbzOckXv+GxRPJIQNSF4D4A9E8XCksuzVVdE/0lr37+uoiAiPia38U 5W2QWe/FJAEPLjIp2eTzf0TrADc1pKP1wrA2ASpdzpm/aX3IB5RPp8Ew S9U72eBFZJAUwg635HxJVxH1maG6atzorR566E+e0OZSaxXS9o1o6QqN 3oPlYLGPORDiExilKfez3C/x/yioOupW9K5eKF0gmtaqrHX0oq9s67f/ RIM2xVaKHgG9Vf2cgJIZkhv7sntujr+E4htnRmy9P9BxyFxsItYxPI6Z bzygHAZpGhlI/7ltEGlIwKxyTK3ZKBm67q7B') + } + +abs_dsa_soa = dns.rrset.from_text('example.', 86400, 'IN', 'SOA', + 'ns1.example. hostmaster.example. 2 10800 3600 604800 86400') + +abs_other_dsa_soa = dns.rrset.from_text('example.', 86400, 'IN', 'SOA', + 'ns1.example. hostmaster.example. 2 10800 3600 604800 86401') + +abs_dsa_soa_rrsig = dns.rrset.from_text('example.', 86400, 'IN', 'RRSIG', + 'SOA 3 1 86400 20101129143231 20101122112731 42088 example. CGul9SuBofsktunV8cJs4eRs6u+3NCS3yaPKvBbD+pB2C76OUXDZq9U=') + +example_sep_key = dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.DNSKEY, + '257 3 3 CI3nCqyJsiCJHTjrNsJOT4RaszetzcJPYuoH3F9ZTVt3KJXncCVR3bwn 1w0iavKljb9hDlAYSfHbFCp4ic/rvg4p1L8vh5s8ToMjqDNl40A0hUGQ Ybx5hsECyK+qHoajilUX1phYSAD8d9WAGO3fDWzUPBuzR7o85NiZCDxz yXuNVfni0uhj9n1KYhEO5yAbbruDGN89wIZcxMKuQsdUY2GYD93ssnBv a55W6XRABYWayKZ90WkRVODLVYLSn53Pj/wwxGH+XdhIAZJXimrZL4yl My7rtBsLMqq8Ihs4Tows7LqYwY7cp6y/50tw6pj8tFqMYcPUjKZV36l1 M/2t5BVg3i7IK61Aidt6aoC3TDJtzAxg3ZxfjZWJfhHjMJqzQIfbW5b9 q1mjFsW5EUv39RaNnX+3JWPRLyDqD4pIwDyqfutMsdk/Py3paHn82FGp CaOg+nicqZ9TiMZURN/XXy5JoXUNQ3RNvbHCUiPUe18KUkY6mTfnyHld 1l9YCWmzXQVClkx/hOYxjJ4j8Ife58+Obu5X') + +example_ds_sha1 = dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.DS, + '18673 3 1 71b71d4f3e11bbd71b4eff12cde69f7f9215bbe7') + +example_ds_sha256 = dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.DS, + '18673 3 2 eb8344cbbf07c9d3d3d6c81d10c76653e28d8611a65e639ef8f716e4e4e5d913') + +class DNSSECValidatorTestCase(unittest.TestCase): + + def testAbsoluteRSAGood(self): + dns.dnssec.validate(abs_soa, abs_soa_rrsig, abs_keys, None, when) + + def testAbsoluteRSABad(self): + def bad(): + dns.dnssec.validate(abs_other_soa, abs_soa_rrsig, abs_keys, None, + when) + self.failUnlessRaises(dns.dnssec.ValidationFailure, bad) + + def testRelativeRSAGood(self): + dns.dnssec.validate(rel_soa, rel_soa_rrsig, rel_keys, + abs_dnspython_org, when) + + def testRelativeRSABad(self): + def bad(): + dns.dnssec.validate(rel_other_soa, rel_soa_rrsig, rel_keys, + abs_dnspython_org, when) + self.failUnlessRaises(dns.dnssec.ValidationFailure, bad) + + def testMakeSHA256DS(self): + ds = dns.dnssec.make_ds(abs_dnspython_org, sep_key, 'SHA256') + self.failUnless(ds == good_ds) + + def testAbsoluteDSAGood(self): + dns.dnssec.validate(abs_dsa_soa, abs_dsa_soa_rrsig, abs_dsa_keys, None, + when2) + + def testAbsoluteDSABad(self): + def bad(): + dns.dnssec.validate(abs_other_dsa_soa, abs_dsa_soa_rrsig, + abs_dsa_keys, None, when2) + self.failUnlessRaises(dns.dnssec.ValidationFailure, bad) + + def testMakeExampleSHA1DS(self): + ds = dns.dnssec.make_ds(abs_example, example_sep_key, 'SHA1') + self.failUnless(ds == example_ds_sha1) + + def testMakeExampleSHA256DS(self): + ds = dns.dnssec.make_ds(abs_example, example_sep_key, 'SHA256') + self.failUnless(ds == example_ds_sha256) + +if __name__ == '__main__': + import_ok = False + try: + import Crypto.Util.number + import_ok = True + except: + pass + if import_ok: + unittest.main() + else: + print('skipping DNSSEC tests because pycrypto is not installed') diff --git a/tests/resolver.py b/tests/resolver.py index f29fa28c..b88be8c3 100644 --- a/tests/resolver.py +++ b/tests/resolver.py @@ -14,6 +14,7 @@ # OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. import io +import select import sys import time import unittest @@ -46,7 +47,7 @@ example. 1 IN A 10.0.0.1 ;ADDITIONAL """ -class ResolverTestCase(unittest.TestCase): +class BaseResolverTests(object): if sys.platform != 'win32': def testRead(self): @@ -101,5 +102,26 @@ class ResolverTestCase(unittest.TestCase): zname = dns.resolver.zone_for_name(name) self.failUnlessRaises(dns.resolver.NotAbsolute, bad) +class PollingMonkeyPatchMixin(object): + def setUp(self): + self.__native_polling_backend = dns.query._polling_backend + dns.query._set_polling_backend(self.polling_backend()) + + unittest.TestCase.setUp(self) + + def tearDown(self): + dns.query._set_polling_backend(self.__native_polling_backend) + + unittest.TestCase.tearDown(self) + +class SelectResolverTestCase(PollingMonkeyPatchMixin, BaseResolverTests, unittest.TestCase): + def polling_backend(self): + return dns.query._select_for + +if hasattr(select, 'poll'): + class PollResolverTestCase(PollingMonkeyPatchMixin, BaseResolverTests, unittest.TestCase): + def polling_backend(self): + return dns.query._poll_for + if __name__ == '__main__': unittest.main()