From 3323a3612d5b3c34e21dcd105ffac2a5b3be4889 Mon Sep 17 00:00:00 2001 From: Bob Halley Date: Fri, 13 Nov 2009 04:06:54 +0900 Subject: [PATCH] add support for more TSIG algorithms --- ChangeLog | 8 ++++--- dns/message.py | 15 +++++++++--- dns/query.py | 7 ++++-- dns/renderer.py | 6 +++-- dns/resolver.py | 15 +++++++++--- dns/tsig.py | 64 +++++++++++++++++++++++++++++++++++++++++++------ dns/update.py | 7 ++++-- 7 files changed, 100 insertions(+), 22 deletions(-) diff --git a/ChangeLog b/ChangeLog index 6b9cc500..496d21d0 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,12 +1,14 @@ -2009-11-13 Bob Halley +2009-11-13 Bob Halley + + * Support has been added for hmac-sha1, hmac-sha224, hmac-sha256, + hmac-sha384 and hmac-sha512. Thanks to Kevin Chen for a + thoughtful, high quality patch. * dns/update.py (Update::present): A zero TTL was not added if present() was called with a single rdata, causing _add() to be unhappy. Thanks to Eugene Kim for reporting the problem and submitting a patch. -2009-11-13 Bob Halley - * dns/entropy.py: Use os.urandom() if present. Don't seed until someone wants randomness. diff --git a/dns/message.py b/dns/message.py index eec9acb5..20769d48 100644 --- a/dns/message.py +++ b/dns/message.py @@ -92,6 +92,9 @@ 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. + @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 more information on TSIG fields. @@ -149,6 +152,7 @@ class Message(object): self.request_payload = 0 self.keyring = None self.keyname = None + self.keyalgorithm = dns.tsig.default_algorithm self.request_mac = '' self.other_data = '' self.tsig_error = 0 @@ -410,12 +414,14 @@ class Message(object): if not self.keyname is None: r.add_tsig(self.keyname, self.keyring[self.keyname], self.fudge, self.original_id, self.tsig_error, - self.other_data, self.request_mac) + self.other_data, self.request_mac, + self.keyalgorithm) self.mac = r.mac return r.get_wire() - def use_tsig(self, keyring, keyname=None, fudge=300, original_id=None, - tsig_error=0, other_data=''): + def use_tsig(self, keyring, keyname=None, fudge=300, + original_id=None, tsig_error=0, other_data='', + algorithm=dns.tsig.default_algorithm): """When sending, a TSIG signature using the specified keyring and keyname should be added. @@ -436,6 +442,8 @@ class Message(object): @type tsig_error: int @param other_data: TSIG other data. @type other_data: string + @param algorithm: The TSIG algorithm to use; defaults to + dns.tsig.default_algorithm """ self.keyring = keyring @@ -445,6 +453,7 @@ class Message(object): if isinstance(keyname, (str, unicode)): keyname = dns.name.from_text(keyname) self.keyname = keyname + self.keyalgorithm = algorithm self.fudge = fudge if original_id is None: self.original_id = self.id diff --git a/dns/query.py b/dns/query.py index 09e39d58..ff353747 100644 --- a/dns/query.py +++ b/dns/query.py @@ -258,7 +258,7 @@ def tcp(q, where, timeout=None, port=53, af=None, source=None, source_port=0, def xfr(where, zone, rdtype=dns.rdatatype.AXFR, rdclass=dns.rdataclass.IN, timeout=None, port=53, keyring=None, keyname=None, relativize=True, af=None, lifetime=None, source=None, source_port=0, serial=0, - use_udp=False): + use_udp=False, keyalgorithm=dns.tsig.default_algorithm): """Return a generator for the responses to a zone transfer. @param where: where to send the message @@ -303,6 +303,9 @@ def xfr(where, zone, rdtype=dns.rdatatype.AXFR, rdclass=dns.rdataclass.IN, @type serial: int @param use_udp: Use UDP (only meaningful for IXFR) @type use_udp: bool + @param keyalgorithm: The TSIG algorithm to use; defaults to + dns.tsig.default_algorithm + @type keyalgorithm: string """ if isinstance(zone, (str, unicode)): @@ -315,7 +318,7 @@ def xfr(where, zone, rdtype=dns.rdatatype.AXFR, rdclass=dns.rdataclass.IN, '. . %u 0 0 0 0' % serial) q.authority.append(rrset) if not keyring is None: - q.use_tsig(keyring, keyname) + q.use_tsig(keyring, keyname, keyalgorithm) wire = q.to_wire() if af is None: try: diff --git a/dns/renderer.py b/dns/renderer.py index db827f1f..c4453d07 100644 --- a/dns/renderer.py +++ b/dns/renderer.py @@ -250,7 +250,7 @@ class Renderer(object): self.counts[ADDITIONAL] += 1 def add_tsig(self, keyname, secret, fudge, id, tsig_error, other_data, - request_mac): + request_mac, algorithm=dns.tsig.default_algorithm): """Add a TSIG signature to the message. @param keyname: the TSIG key name @@ -267,6 +267,7 @@ class Renderer(object): @type other_data: string @param request_mac: This message is a response to the request which had the specified MAC. + @param algorithm: the TSIG algorithm to use @type request_mac: string """ @@ -281,7 +282,8 @@ class Renderer(object): id, tsig_error, other_data, - request_mac) + request_mac, + algorithm=algorithm) keyname.to_wire(self.output, self.compress, self.origin) self.output.write(struct.pack('!HHIH', dns.rdatatype.TSIG, dns.rdataclass.ANY, 0, 0)) diff --git a/dns/resolver.py b/dns/resolver.py index 70156c1d..2da94cc1 100644 --- a/dns/resolver.py +++ b/dns/resolver.py @@ -265,6 +265,9 @@ class Resolver(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. + @type keyalgorithm: string @ivar edns: The EDNS level to use. The default is -1, no Edns. @type edns: int @ivar ednsflags: The EDNS flags @@ -307,6 +310,7 @@ class Resolver(object): self.lifetime = 30.0 self.keyring = None self.keyname = None + self.keyalgorithm = dns.tsig.default_algorithm self.edns = -1 self.ednsflags = 0 self.payload = 0 @@ -589,7 +593,7 @@ class Resolver(object): return answer request = dns.message.make_query(qname, rdtype, rdclass) if not self.keyname is None: - request.use_tsig(self.keyring, self.keyname) + request.use_tsig(self.keyring, self.keyname, self.keyalgorithm) request.use_edns(self.edns, self.ednsflags, self.payload) response = None # @@ -670,7 +674,8 @@ class Resolver(object): self.cache.put((qname, rdtype, rdclass), answer) return answer - def use_tsig(self, keyring, keyname=None): + def use_tsig(self, keyring, keyname=None, + algorithm=dns.tsig.default_algorithm): """Add a TSIG signature to the query. @param keyring: The TSIG keyring to use; defaults to None. @@ -680,12 +685,16 @@ class Resolver(object): but a keyname is not, then the key used will be the first key in the keyring. Note that the order of keys in a dictionary is not defined, so applications should supply a keyname when a keyring is used, unless - they know the keyring contains only one key.""" + they know the keyring contains only one key. + @param algorithm: The TSIG key algorithm to use. The default + is dns.tsig.default_algorithm. + @type algorithm: string""" self.keyring = keyring if keyname is None: self.keyname = self.keyring.keys()[0] else: self.keyname = keyname + self.keyalgorithm = algorithm def use_edns(self, edns, ednsflags, payload): """Configure Edns. diff --git a/dns/tsig.py b/dns/tsig.py index 3f579d10..a94cae09 100644 --- a/dns/tsig.py +++ b/dns/tsig.py @@ -50,7 +50,7 @@ class PeerBadTruncation(PeerError): """Raised if the peer didn't like amount of truncation in the TSIG we sent""" pass -_alg_name = dns.name.from_text('HMAC-MD5.SIG-ALG.REG.INT.').to_digestable() +default_algorithm = "HMAC-MD5.SIG-ALG.REG.INT" BADSIG = 16 BADKEY = 17 @@ -58,16 +58,19 @@ BADTIME = 18 BADTRUNC = 22 def hmac_md5(wire, keyname, secret, time, fudge, original_id, error, - other_data, request_mac, ctx=None, multi=False, first=True): - """Return a (tsig_rdata, mac, ctx) tuple containing the HMAC-MD5 TSIG rdata - for the input parameters, the HMAC-MD5 MAC calculated by applying the + other_data, request_mac, ctx=None, multi=False, first=True, + algorithm=default_algorithm): + """Return a (tsig_rdata, mac, ctx) tuple containing the HMAC TSIG rdata + for the input parameters, the HMAC MAC calculated by applying the TSIG signature algorithm, and the TSIG digest context. @rtype: (string, string, hmac.HMAC object) @raises ValueError: I{other_data} is too long + @raises NotImplementedError: I{algorithm} is not supported """ + (algorithm_name, digestmod) = get_algorithm(algorithm) if first: - ctx = hmac.new(secret) + ctx = hmac.new(secret, digestmod=digestmod) ml = len(request_mac) if ml > 0: ctx.update(struct.pack('!H', ml)) @@ -83,7 +86,7 @@ def hmac_md5(wire, keyname, secret, time, fudge, original_id, error, upper_time = (long_time >> 32) & 0xffffL lower_time = long_time & 0xffffffffL time_mac = struct.pack('!HIH', upper_time, lower_time, fudge) - pre_mac = _alg_name + time_mac + pre_mac = algorithm_name + time_mac ol = len(other_data) if ol > 65535: raise ValueError, 'TSIG Other Data is > 65535 bytes' @@ -153,7 +156,54 @@ def validate(wire, keyname, secret, now, request_mac, tsig_start, tsig_rdata, raise BadTime (junk, our_mac, ctx) = hmac_md5(new_wire, keyname, secret, time, fudge, original_id, error, other_data, - request_mac, ctx, multi, first) + request_mac, ctx, multi, first, aname) if (our_mac != mac): raise BadSignature return ctx + +def get_algorithm(algorithm): + """Returns the wire format string and the hash module to use for the + specified TSIG algorithm" + @rtype: (string, hash module) + @raises NotImplementedError: I{algorithm} is not supported + """ + + hashes = {} + try: + import hashlib + 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 + + import sys + if sys.hexversion < 0x02050000: + # hashlib doesn't conform to PEP 247: API for + # Cryptographic Hash Functions, which hmac before python + # 2.5 requires, so add the necessary items. + class HashlibWrapper: + def __init__(self, basehash): + self.basehash = basehash + self.digest_size = self.basehash().digest_size + + def new(self, *args, **kwargs): + return self.basehash(*args, **kwargs) + + for name in hashes: + hashes[name] = HashlibWrapper(hashes[name]) + + except ImportError: + pass + + import md5, sha + hashes[dns.name.from_text('HMAC-MD5.SIG-ALG.REG.INT')] = md5 + hashes[dns.name.from_text('hmac-sha1')] = sha + + if isinstance(algorithm, (str, unicode)): + 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" diff --git a/dns/update.py b/dns/update.py index 5bd289f7..7cf13f68 100644 --- a/dns/update.py +++ b/dns/update.py @@ -24,7 +24,7 @@ import dns.rdataset class Update(dns.message.Message): def __init__(self, zone, rdclass=dns.rdataclass.IN, keyring=None, - keyname=None): + keyname=None, keyalgorithm=dns.tsig.default_algorithm): """Initialize a new DNS Update object. @param zone: The zone which is being updated. @@ -41,6 +41,9 @@ class Update(dns.message.Message): so applications should supply a keyname when a keyring is used, unless 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 + @type keyalgorithm: string """ super(Update, self).__init__() self.flags |= dns.opcode.to_flags(dns.opcode.UPDATE) @@ -53,7 +56,7 @@ class Update(dns.message.Message): self.find_rrset(self.question, self.origin, rdclass, dns.rdatatype.SOA, create=True, force_unique=True) if not keyring is None: - self.use_tsig(keyring, keyname) + self.use_tsig(keyring, keyname, keyalgorithm) def _add_rr(self, name, ttl, rd, deleting=None, section=None): """Add a single RR to the update section.""" -- 2.47.3