diff --git a/CHANGES b/CHANGES index 44170ef4..a5f05121 100644 --- a/CHANGES +++ b/CHANGES @@ -1,4 +1,9 @@ - +dpkg-1.8: + - fix a typo in vrrp.py + - fix IPv4 and IPv6 packet to correctly handle zero payload length + - store cipher_suite as int in TLSServerHello to allow app-specific messages + - improve SSL parsing + dpkt-1.7: - handle dynamic imports from py2exe/freeze.py/zipped egg packages, from plotnikoff diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 00000000..e3762cee --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,3 @@ +include AUTHORS CHANGES README LICENSE HACKING +recursive-include examples * +recursive-include tests * diff --git a/dpkt/__init__.py b/dpkt/__init__.py index b29a4cce..3a4e4a22 100644 --- a/dpkt/__init__.py +++ b/dpkt/__init__.py @@ -1,4 +1,4 @@ -# $Id: __init__.py 82 2011-01-10 03:43:38Z timur.alperovich@gmail.com $ +# $Id: __init__.py 89 2013-05-23 01:31:22Z andrewflnr@gmail.com $ """fast, simple packet creation and parsing.""" @@ -6,7 +6,7 @@ __copyright__ = 'Copyright (c) 2004 Dug Song' __license__ = 'BSD' __url__ = 'http://dpkt.googlecode.com/' -__version__ = '1.7' +__version__ = '1.8' from dpkt import * diff --git a/dpkt/bgp.py b/dpkt/bgp.py index 9c6e3083..b9fb26a0 100644 --- a/dpkt/bgp.py +++ b/dpkt/bgp.py @@ -14,7 +14,6 @@ # Cease Subcodes - RFC 4486 # NOPEER Community - RFC 3765 # Multiprotocol Extensions - 2858 -# Support for Four-octet AS Number Space - RFC 4893 # Message Types OPEN = 1 @@ -36,8 +35,6 @@ CLUSTER_LIST = 10 MP_REACH_NLRI = 14 MP_UNREACH_NLRI = 15 -AS4_PATH = 17 -AS4_AGGREGATOR = 18 # Origin Types ORIGIN_IGP = 0 @@ -72,13 +69,6 @@ # Capability Types CAP_MULTIPROTOCOL = 1 CAP_ROUTE_REFRESH = 2 -CAP_OUTBOUND_ROUTE_FILTER = 3 -CAP_MULTI_ROUTE_TO_DEST = 4 -CAP_GRACEFUL_RESTART = 64 -CAP_SUPPORT_AS4 = 65 -CAP_SUPPORT_DYN_CAP = 67 -CAP_MULTISESSION_BGP = 68 -CAP_ROUTE_REFRESH_OLD = 128 # NOTIFICATION Error Codes MESSAGE_HEADER_ERROR = 1 @@ -209,15 +199,6 @@ def unpack(self, buf): dpkt.Packet.unpack(self, buf) self.data = self.data[:self.len] - if self.code == CAP_MULTIPROTOCOL: - self.data = self.multiprotocol = self.MultiProtocol(self.data) - - class MultiProtocol(dpkt.Packet): - __hdr__ = ( - ('afi', 'H', 1), - ('res', 'B', 0), - ('safi', 'B', 1) - ) class Update(dpkt.Packet): __hdr_defaults__ = { @@ -316,12 +297,7 @@ def unpack(self, buf): if self.type == ORIGIN: self.data = self.origin = self.Origin(self.data) elif self.type == AS_PATH: - preserved=self.data - try: - self.data = self.as_path = self.AS4Path(self.data) - except dpkt.UnpackError, (errno): - self.data=preserved - self.data = self.as_path = self.ASPath(self.data) + self.data = self.as_path = self.ASPath(self.data) elif self.type == NEXT_HOP: self.data = self.next_hop = self.NextHop(self.data) elif self.type == MULTI_EXIT_DISC: @@ -331,12 +307,7 @@ def unpack(self, buf): elif self.type == ATOMIC_AGGREGATE: self.data = self.atomic_aggregate = self.AtomicAggregate(self.data) elif self.type == AGGREGATOR: - preserved=self.data - try: - self.data = self.as4_aggregator = self.AS4Aggregator(self.data) - except dpkt.UnpackError, (errno): - self.data=preserved - self.data = self.aggregator = self.Aggregator(self.data) + self.data = self.aggregator = self.Aggregator(self.data) elif self.type == COMMUNITIES: self.data = self.communities = self.Communities(self.data) elif self.type == ORIGINATOR_ID: @@ -347,10 +318,6 @@ def unpack(self, buf): self.data = self.mp_reach_nlri = self.MPReachNLRI(self.data) elif self.type == MP_UNREACH_NLRI: self.data = self.mp_unreach_nlri = self.MPUnreachNLRI(self.data) - elif self.type == AS4_PATH: - self.data = self.as4_path = self.AS4Path(self.data) - elif self.type == AS4_AGGREGATOR: - self.data = self.as4_aggregator = self.AS4Aggregator(self.data) def __len__(self): if self.extended_length: @@ -376,7 +343,6 @@ class Origin(dpkt.Packet): ) class ASPath(dpkt.Packet): - #__hdr__=() __hdr_defaults__ = { 'segments': [] } @@ -402,10 +368,6 @@ class ASPathSegment(dpkt.Packet): ('len', 'B', 0) ) - __hdr_defaults__ = { - 'path': [] - } - def unpack(self, buf): dpkt.Packet.unpack(self, buf) l = [] @@ -458,7 +420,6 @@ class Aggregator(dpkt.Packet): ) class Communities(dpkt.Packet): - #__hdr__ = () __hdr_defaults__ = { 'list': [] } @@ -617,59 +578,6 @@ def __str__(self): return self.pack_hdr() + \ ''.join(map(str, self.data)) - class AS4Path(dpkt.Packet): - #__hdr__ = () - __hdr_defaults__ = { - 'segments': [] - } - - def unpack(self, buf): - self.data = buf - l = [] - while self.data: - seg = self.AS4PathSegment(self.data) - self.data = self.data[len(seg):] - l.append(seg) - self.data = self.segments = l - - def __len__(self): - return sum(map(len, self.data)) - - def __str__(self): - return ''.join(map(str, self.data)) - - class AS4PathSegment(dpkt.Packet): - __hdr__ = ( - ('type', 'B', 0), - ('len', 'B', 0) - ) - - def unpack(self, buf): - dpkt.Packet.unpack(self, buf) - l = [] - for i in range(self.len): - AS = struct.unpack('>L', self.data[:4])[0] - self.data = self.data[4:] - l.append(AS) - self.data = self.path = l - - def __len__(self): - return self.__hdr_len__ + \ - 4 * len(self.path) - - def __str__(self): - as_str = '' - for AS in self.path: - as_str += struct.pack('>L', AS) - return self.pack_hdr() + \ - as_str - - class AS4Aggregator(dpkt.Packet): - __hdr__ = ( - ('asn', 'L', 0), - ('ip', 'I', 0) - ) - class Notification(dpkt.Packet): __hdr__ = ( diff --git a/dpkt/ethernet.py b/dpkt/ethernet.py index cbf3a5a5..ab523ce9 100644 --- a/dpkt/ethernet.py +++ b/dpkt/ethernet.py @@ -30,7 +30,6 @@ ETH_TYPE_MPLS_MCAST = 0x8848 # MPLS Multicast ETH_TYPE_PPPoE_DISC = 0x8863 # PPP Over Ethernet Discovery Stage ETH_TYPE_PPPoE = 0x8864 # PPP Over Ethernet Session Stage -ETH_TYPE_LLDP = 0x88CC # Link Layer Discovery Protocol # MPLS label stack fields MPLS_LABEL_MASK = 0xfffff000 diff --git a/dpkt/http.py b/dpkt/http.py index 705ec57b..ce0ddc64 100644 --- a/dpkt/http.py +++ b/dpkt/http.py @@ -1,4 +1,4 @@ -# $Id: http.py 80 2011-01-06 16:50:42Z jon.oberheide $ +# $Id: http.py 86 2013-03-05 19:25:19Z andrewflnr@gmail.com $ """Hypertext Transfer Protocol.""" @@ -193,9 +193,13 @@ def test_format_request(self): r.headers['content-type'] = 'text/plain' r.headers['content-length'] = '5' r.body = 'hello' - assert str(r) == 'POST /foo/bar/baz.html HTTP/1.0\r\ncontent-length: 5\r\ncontent-type: text/plain\r\n\r\nhello' + s = str(r) + assert s.startswith('POST /foo/bar/baz.html HTTP/1.0\r\n') + assert s.endswith('\r\n\r\nhello') + assert '\r\ncontent-length: 5\r\n' in s + assert '\r\ncontent-type: text/plain\r\n' in s r = Request(str(r)) - assert str(r) == 'POST /foo/bar/baz.html HTTP/1.0\r\ncontent-length: 5\r\ncontent-type: text/plain\r\n\r\nhello' + assert str(r) == s def test_chunked_response(self): s = """HTTP/1.1 200 OK\r\nCache-control: no-cache\r\nPragma: no-cache\r\nContent-Type: text/javascript; charset=utf-8\r\nContent-Encoding: gzip\r\nTransfer-Encoding: chunked\r\nSet-Cookie: S=gmail=agg:gmail_yj=v2s:gmproxy=JkU; Domain=.google.com; Path=/\r\nServer: GFE/1.3\r\nDate: Mon, 12 Dec 2005 22:33:23 GMT\r\n\r\na\r\n\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\x00\r\n152\r\nm\x91MO\xc4 \x10\x86\xef\xfe\n\x82\xc9\x9eXJK\xe9\xb6\xee\xc1\xe8\x1e6\x9e4\xf1\xe0a5\x86R\xda\x12Yh\x80\xba\xfa\xef\x85\xee\x1a/\xf21\x99\x0c\xef0<\xc3\x81\xa0\xc3\x01\xe6\x10\xc1<\xa7eYT5\xa1\xa4\xac\xe1\xdb\x15:\xa4\x9d\x0c\xfa5K\x00\xf6.\xaa\xeb\x86\xd5y\xcdHY\x954\x8e\xbc*h\x8c\x8e!L7Y\xe6\'\xeb\x82WZ\xcf>8\x1ed\x87\x851X\xd8c\xe6\xbc\x17Z\x89\x8f\xac \x84e\xde\n!]\x96\x17i\xb5\x02{{\xc2z0\x1e\x0f#7\x9cw3v\x992\x9d\xfc\xc2c8\xea[/EP\xd6\xbc\xce\x84\xd0\xce\xab\xf7`\'\x1f\xacS\xd2\xc7\xd2\xfb\x94\x02N\xdc\x04\x0f\xee\xba\x19X\x03TtW\xd7\xb4\xd9\x92\n\xbcX\xa7;\xb0\x9b\'\x10$?F\xfd\xf3CzPt\x8aU\xef\xb8\xc8\x8b-\x18\xed\xec<\xe0\x83\x85\x08!\xf8"[\xb0\xd3j\x82h\x93\xb8\xcf\xd8\x9b\xba\xda\xd0\x92\x14\xa4a\rc\reM\xfd\x87=X;h\xd9j;\xe0db\x17\xc2\x02\xbd\xb0F\xc2in#\xfb:\xb6\xc4x\x15\xd6\x9f\x8a\xaf\xcf)\x0b^\xbc\xe7i\x11\x80\x8b\x00D\x01\xd8/\x82x\xf6\xd8\xf7J(\xae/\x11p\x1f+\xc4p\t:\xfe\xfd\xdf\xa3Y\xfa\xae4\x7f\x00\xc5\xa5\x95\xa1\xe2\x01\x00\x00\r\n0\r\n\r\n""" diff --git a/dpkt/ip.py b/dpkt/ip.py index 4f3427d9..a8f9bd96 100644 --- a/dpkt/ip.py +++ b/dpkt/ip.py @@ -1,4 +1,4 @@ -# $Id: ip.py 65 2010-03-26 02:53:51Z dugsong $ +# $Id: ip.py 87 2013-03-05 19:41:04Z andrewflnr@gmail.com $ """Internet Protocol.""" @@ -55,7 +55,10 @@ def unpack(self, buf): if ol < 0: raise dpkt.UnpackError, 'invalid header length' self.opts = buf[self.__hdr_len__:self.__hdr_len__ + ol] - buf = buf[self.__hdr_len__ + ol:self.len] + if self.len: + buf = buf[self.__hdr_len__ + ol:self.len] + else: # very likely due to TCP segmentation offload + buf = buf[self.__hdr_len__ + ol:] try: self.data = self._protosw[self.p](buf) setattr(self, self.data.__class__.__name__.lower(), self.data) @@ -285,6 +288,14 @@ def test_opt(self): s = '\x4f\x00\x00\x50\xae\x08\x00\x00\x40\x06\x17\xfc\xc0\xa8\x0a\x26\xc0\xa8\x0a\x01\x07\x27\x08\x01\x02\x03\x04\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' ip = IP(s) ip.sum = 0 - self.failUnless(str(ip) == s) + self.failUnless(str(ip) == s) + + def test_zerolen(self): + import tcp + d = 'X' * 2048 + s = 'E\x00\x00\x004\xce@\x00\x80\x06\x00\x00\x7f\x00\x00\x01\x7f\x00\x00\x01\xccN\x0c8`\xff\xc6N_\x8a\x12\x98P\x18@):\xa3\x00\x00' + d + ip = IP(s) + self.failUnless(isinstance(ip.data, tcp.TCP)) + self.failUnless(ip.tcp.data == d) unittest.main() diff --git a/dpkt/ip6.py b/dpkt/ip6.py index 5bcf45d4..38002fa6 100644 --- a/dpkt/ip6.py +++ b/dpkt/ip6.py @@ -1,4 +1,4 @@ -# $Id: ip6.py 58 2010-03-06 00:06:14Z dugsong $ +# $Id: ip6.py 87 2013-03-05 19:41:04Z andrewflnr@gmail.com $ """Internet Protocol, version 6.""" @@ -13,7 +13,11 @@ class IP6(dpkt.Packet): ('src', '16s', ''), ('dst', '16s', '') ) - _protosw = {} # XXX - shared with IP + + # XXX - to be shared with IP. We cannot refer to the ip module + # right now because ip.__load_protos() expects the IP6 class to be + # defined. + _protosw = None def _get_v(self): return self.v_fc_flow >> 28 @@ -37,7 +41,10 @@ def unpack(self, buf): dpkt.Packet.unpack(self, buf) self.extension_hdrs = dict(((i, None) for i in ext_hdrs)) - buf = self.data[:self.plen] + if self.plen: + buf = self.data[:self.plen] + else: # due to jumbo payload or TSO + buf = self.data next = self.nxt @@ -91,9 +98,14 @@ def get_proto(cls, p): return cls._protosw[p] get_proto = classmethod(get_proto) -# XXX - auto-load IP6 dispatch table from IP dispatch table import ip -IP6._protosw.update(ip.IP._protosw) +# We are most likely still in the middle of ip.__load_protos() which +# implicitly loads this module through __import__(), so the content of +# ip.IP._protosw is still incomplete at the moment. By sharing the +# same dictionary by reference as opposed to making a copy, when +# ip.__load_protos() finishes, we will also automatically get the most +# up-to-date dictionary. +IP6._protosw = ip.IP._protosw class IP6ExtensionHeader(dpkt.Packet): """ diff --git a/dpkt/mrt.py b/dpkt/mrt.py index d3809c0f..9f3c719c 100644 --- a/dpkt/mrt.py +++ b/dpkt/mrt.py @@ -1,13 +1,9 @@ # $Id: mrt.py 29 2007-01-26 02:29:07Z jon.oberheide $ -# Patched with Mattia Rossi's code from MDFMT: -# http://caia.swin.edu.au/urp/bgp/tools.html """Multi-threaded Routing Toolkit.""" import dpkt import bgp -import struct -import socket # Multi-threaded Routing Toolkit # http://www.ietf.org/internet-drafts/draft-ietf-grow-mrt-03.txt @@ -26,7 +22,6 @@ BGP4PLUS_01 = 10 # Deprecated by BGP4MP OSPF = 11 TABLE_DUMP = 12 -TABLE_DUMP_V2 = 13 BGP4MP = 16 BGP4MP_ET = 17 ISIS = 32 @@ -44,14 +39,6 @@ AFI_IPv4 = 1 AFI_IPv6 = 2 -# TableDump v2 Subtypes -TABLE_DUMP_V2_PEER_INDEX_TABLE = 1 -TABLE_DUMP_V2_RIB_IPV4_UNICAST = 2 -TABLE_DUMP_V2_RIB_IPV4_MULTICAST = 3 -TABLE_DUMP_V2_RIB_IPV6_UNICAST = 4 -TABLE_DUMP_V2_RIB_IPV6_MULTICAST = 5 -TABLE_DUMP_V2_RIB_GENERIC = 6 - class MRTHeader(dpkt.Packet): __hdr__ = ( ('ts', 'I', 0), @@ -84,154 +71,6 @@ def unpack(self, buf): l.append(attr) self.attributes = l -class TableDump2_PeerIndex(dpkt.Packet): - __hdr__ = ( - ('id', 'I', 0), - ('viewname_len', 'H', 0), - ) - - __hdr_defaults__ = { - 'view_name' : 0, - 'peer_count' : 0, - 'peers' : [] - } - - def unpack(self, buf): - dpkt.Packet.unpack(self, buf) - vlen = self.viewname_len - self.view_name = self.data[:vlen] - pcount = self.peer_count = struct.unpack('>H', \ - self.data[vlen:vlen + 2])[0] - l = [] - for i in range(pcount): - peer = self.Peer(self.data[vlen + 2:]) - self.data = self.data[len(peer):] - l.append(peer) - self.data = self.peers = l - - class Peer(dpkt.Packet): - - __hdr__ = ( - ('type', 'B', 0), - ('id', 'I', 0) - ) - - __hdr_defaults__ = { - 'address': 0, - 'asn': 0, - } - - def unpack(self, buf): - self.len=0 - dpkt.Packet.unpack(self, buf) - if (self.type >> 0) & 0x01 : - self.address = self.data[:16] - self.data = self.data[16:] - self.len += 16 - else: - self.address = socket.inet_ntoa(self.data[:4]) - self.data = self.data[4:] - self.len += 4 - if (self.type >> 1) & 0x01 : - self.asn = struct.unpack('>L', \ - self.data[:4])[0] - self.len += 4 - else : - self.asn = struct.unpack('>H', \ - self.data[:2])[0] - self.len += 2 - self.data='' - - def __len__(self): - return self.__hdr_len__ + \ - self.len - -class TableDump2_IPV4(dpkt.Packet): - __hdr__ = ( - ('seq','I',0), - ) - - __hdr_defaults__ = { - 'ribentry' : [] - } - - def unpack(self,buf): - dpkt.Packet.unpack(self, buf) - pre = bgp.RouteIPV4(self.data) - self.prefix_len = pre.len - self.prefix = pre.prefix - self.entry_count = struct.unpack('>H', \ - self.data[len(pre):len(pre)+2])[0] - self.data = self.data[len(pre)+2:] - l = [] - for i in range(self.entry_count): - entry = TableDump2_RIBEntry(self.data) - self.data = self.data[len(entry):] - l.append(entry) - self.data = self.ribentry = l - -class TableDump2_IPV6(dpkt.Packet): - __hdr__ = ( - ('seq','I',0), - ) - - def unpack(self,buf): - dpkt.Packet.unpack(self, buf) - pre = bgp.RouteIPV6(self.data) - self.prefix_len = pre.len - self.prefix = pre.prefix - self.entry_count = struct.unpack('>H', \ - self.data[len(pre):len(pre)+2])[0] - self.data = self.data[len(pre)+2:] - l = [] - for i in range(self.entry_count): - entry = TableDump2_RIBEntry(self.data) - self.data = self.data[len(entry):] - l.append(entry) - self.data = self.ribentry = l - -class TableDump2_RIBGeneric(dpkt.Packet): - __hdr__ = ( - ('seq', 'I', 0), - ('afi', 'H', 0), - ('safi', 'B', 0) - ) - - def unpack(self,buf): - dpkt.Packet.unpack(self, buf) - route = bgp.RouteGeneric(self.data) - self.entry_count = struct.unpack('>H', \ - self.data[len(route):len(route)+2])[0] - self.data = self.data[len(route)+2:] - l = [] - for i in range(self.entry_count): - entry = TableDump2_RIBEntry(self.data) - self.data = self.data[len(entry):] - l.append(entry) - self.data = self.ribentry = l - -class TableDump2_RIBEntry(dpkt.Packet): - __hdr__ = ( - ('peer_index', 'H', 0), - ('originated_ts', 'I', 0), - ('attr_len', 'H', 0) - ) - - def unpack(self,buf): - dpkt.Packet.unpack(self,buf) - plen = self.attr_len - l = [] - while plen > 0: - attr = bgp.BGP.Update.Attribute(self.data) - self.data = self.data[len(attr):] - plen -= len(attr) - l.append(attr) - self.attributes = l - - def __len__(self): - return self.__hdr_len__ + self.attr_len - - class BGP4MPMessage(dpkt.Packet): __hdr__ = ( ('src_as', 'H', 0), diff --git a/dpkt/ssl.py b/dpkt/ssl.py index 3a94a5c9..80c43cdc 100644 --- a/dpkt/ssl.py +++ b/dpkt/ssl.py @@ -1,8 +1,21 @@ -# $Id: ssl.py 46 2008-05-27 02:08:12Z jon.oberheide $ +# $Id: ssl.py 84 2012-08-24 18:44:00Z andrewflnr@gmail.com $ +# Portion Copyright 2012 Google Inc. All rights reserved. """Secure Sockets Layer / Transport Layer Security.""" import dpkt +import ssl_ciphersuites +import struct +import binascii +import traceback +import datetime + +# +# Note from April 2011: cde...@gmail.com added code that parses SSL3/TLS messages more in depth. +# +# Jul 2012: afleenor@google.com modified and extended SSL support further. +# + class SSL2(dpkt.Packet): __hdr__ = ( @@ -22,52 +35,522 @@ def unpack(self, buf): self.pad = self.data[1+n:1+n+padlen] self.data = self.data[1+n+padlen:] -# SSLv3/TLS version -SSL3_VERSION = 0x0300 -TLS1_VERSION = 0x0301 - -# Record type -SSL3_RT_CHANGE_CIPHER_SPEC = 20 -SSL3_RT_ALERT = 21 -SSL3_RT_HANDSHAKE = 22 -SSL3_RT_APPLICATION_DATA = 23 - -# Handshake message type -SSL3_MT_HELLO_REQUEST = 0 -SSL3_MT_CLIENT_HELLO = 1 -SSL3_MT_SERVER_HELLO = 2 -SSL3_MT_CERTIFICATE = 11 -SSL3_MT_SERVER_KEY_EXCHANGE = 12 -SSL3_MT_CERTIFICATE_REQUEST = 13 -SSL3_MT_SERVER_DONE = 14 -SSL3_MT_CERTIFICATE_VERIFY = 15 -SSL3_MT_CLIENT_KEY_EXCHANGE = 16 -SSL3_MT_FINISHED = 20 - -class SSL3(dpkt.Packet): + +# SSLv3/TLS versions +SSL3_V = 0x0300 +TLS1_V = 0x0301 +TLS11_V = 0x0302 +TLS12_V = 0x0303 + +ssl3_versions_str = { + SSL3_V: 'SSL3', + TLS1_V: 'TLS 1.0', + TLS11_V: 'TLS 1.1', + TLS12_V: 'TLS 1.2' +} + +SSL3_VERSION_BYTES = set(('\x03\x00', '\x03\x01', '\x03\x02', '\x03\x03')) + + +# Alert levels +SSL3_AD_WARNING = 1 +SSL3_AD_FATAL = 2 +alert_level_str = { + SSL3_AD_WARNING: 'SSL3_AD_WARNING', + SSL3_AD_FATAL: 'SSL3_AD_FATAL' +} + +# SSL3 alert descriptions +SSL3_AD_CLOSE_NOTIFY = 0 +SSL3_AD_UNEXPECTED_MESSAGE = 10 # fatal +SSL3_AD_BAD_RECORD_MAC = 20 # fatal +SSL3_AD_DECOMPRESSION_FAILURE = 30 # fatal +SSL3_AD_HANDSHAKE_FAILURE = 40 # fatal +SSL3_AD_NO_CERTIFICATE = 41 +SSL3_AD_BAD_CERTIFICATE = 42 +SSL3_AD_UNSUPPORTED_CERTIFICATE = 43 +SSL3_AD_CERTIFICATE_REVOKED = 44 +SSL3_AD_CERTIFICATE_EXPIRED = 45 +SSL3_AD_CERTIFICATE_UNKNOWN = 46 +SSL3_AD_ILLEGAL_PARAMETER = 47 # fatal + +# TLS1 alert descriptions +TLS1_AD_DECRYPTION_FAILED = 21 +TLS1_AD_RECORD_OVERFLOW = 22 +TLS1_AD_UNKNOWN_CA = 48 # fatal +TLS1_AD_ACCESS_DENIED = 49 # fatal +TLS1_AD_DECODE_ERROR = 50 # fatal +TLS1_AD_DECRYPT_ERROR = 51 +TLS1_AD_EXPORT_RESTRICTION = 60 # fatal +TLS1_AD_PROTOCOL_VERSION = 70 # fatal +TLS1_AD_INSUFFICIENT_SECURITY = 71 # fatal +TLS1_AD_INTERNAL_ERROR = 80 # fatal +TLS1_AD_USER_CANCELLED = 90 +TLS1_AD_NO_RENEGOTIATION = 100 +#/* codes 110-114 are from RFC3546 */ +TLS1_AD_UNSUPPORTED_EXTENSION = 110 +TLS1_AD_CERTIFICATE_UNOBTAINABLE = 111 +TLS1_AD_UNRECOGNIZED_NAME = 112 +TLS1_AD_BAD_CERTIFICATE_STATUS_RESPONSE = 113 +TLS1_AD_BAD_CERTIFICATE_HASH_VALUE = 114 +TLS1_AD_UNKNOWN_PSK_IDENTITY = 115 # fatal + + +# Mapping alert types to strings +alert_description_str = { + SSL3_AD_CLOSE_NOTIFY: 'SSL3_AD_CLOSE_NOTIFY', + SSL3_AD_UNEXPECTED_MESSAGE: 'SSL3_AD_UNEXPECTED_MESSAGE', + SSL3_AD_BAD_RECORD_MAC: 'SSL3_AD_BAD_RECORD_MAC', + SSL3_AD_DECOMPRESSION_FAILURE: 'SSL3_AD_DECOMPRESSION_FAILURE', + SSL3_AD_HANDSHAKE_FAILURE: 'SSL3_AD_HANDSHAKE_FAILURE', + SSL3_AD_NO_CERTIFICATE: 'SSL3_AD_NO_CERTIFICATE', + SSL3_AD_BAD_CERTIFICATE: 'SSL3_AD_BAD_CERTIFICATE', + SSL3_AD_UNSUPPORTED_CERTIFICATE: 'SSL3_AD_UNSUPPORTED_CERTIFICATE', + SSL3_AD_CERTIFICATE_REVOKED: 'SSL3_AD_CERTIFICATE_REVOKED', + SSL3_AD_CERTIFICATE_EXPIRED: 'SSL3_AD_CERTIFICATE_EXPIRED', + SSL3_AD_CERTIFICATE_UNKNOWN: 'SSL3_AD_CERTIFICATE_UNKNOWN', + SSL3_AD_ILLEGAL_PARAMETER: 'SSL3_AD_ILLEGAL_PARAMETER', + TLS1_AD_DECRYPTION_FAILED: 'TLS1_AD_DECRYPTION_FAILED', + TLS1_AD_RECORD_OVERFLOW: 'TLS1_AD_RECORD_OVERFLOW', + TLS1_AD_UNKNOWN_CA: 'TLS1_AD_UNKNOWN_CA', + TLS1_AD_ACCESS_DENIED: 'TLS1_AD_ACCESS_DENIED', + TLS1_AD_DECODE_ERROR: 'TLS1_AD_DECODE_ERROR', + TLS1_AD_DECRYPT_ERROR: 'TLS1_AD_DECRYPT_ERROR', + TLS1_AD_EXPORT_RESTRICTION: 'TLS1_AD_EXPORT_RESTRICTION', + TLS1_AD_PROTOCOL_VERSION: 'TLS1_AD_PROTOCOL_VERSION', + TLS1_AD_INSUFFICIENT_SECURITY: 'TLS1_AD_INSUFFICIENT_SECURITY', + TLS1_AD_INTERNAL_ERROR: 'TLS1_AD_INTERNAL_ERROR', + TLS1_AD_USER_CANCELLED: 'TLS1_AD_USER_CANCELLED', + TLS1_AD_NO_RENEGOTIATION: 'TLS1_AD_NO_RENEGOTIATION', + TLS1_AD_UNSUPPORTED_EXTENSION: 'TLS1_AD_UNSUPPORTED_EXTENSION', + TLS1_AD_CERTIFICATE_UNOBTAINABLE: 'TLS1_AD_CERTIFICATE_UNOBTAINABLE', + TLS1_AD_UNRECOGNIZED_NAME: 'TLS1_AD_UNRECOGNIZED_NAME', + TLS1_AD_BAD_CERTIFICATE_STATUS_RESPONSE: 'TLS1_AD_BAD_CERTIFICATE_STATUS_RESPONSE', + TLS1_AD_BAD_CERTIFICATE_HASH_VALUE: 'TLS1_AD_BAD_CERTIFICATE_HASH_VALUE', + TLS1_AD_UNKNOWN_PSK_IDENTITY: 'TLS1_AD_UNKNOWN_PSK_IDENTITY' +} + + +# struct format strings for parsing buffer lengths +# don't forget, you have to pad a 3-byte value with \x00 +_SIZE_FORMATS = ['!B', '!H', '!I', '!I'] + +def parse_variable_array(buf, lenbytes): + """ + Parse an array described using the 'Type name' syntax from the spec + + Read a length at the start of buf, and returns that many bytes + after, in a tuple with the TOTAL bytes consumed (including the size). This + does not check that the array is the right length for any given datatype. + """ + # first have to figure out how to parse length + assert lenbytes <= 4 # pretty sure 4 is impossible, too + size_format = _SIZE_FORMATS[lenbytes - 1] + padding = '\x00' if lenbytes == 3 else '' + # read off the length + size = struct.unpack(size_format, padding + buf[:lenbytes])[0] + # read the actual data + data = buf[lenbytes:lenbytes + size] + # if len(data) != size: insufficient data + return data, size + lenbytes + + +class SSL3Exception(Exception): + pass + + +class TLSRecord(dpkt.Packet): + """ + SSLv3 or TLSv1+ packet. + + In addition to the fields specified in the header, there are + compressed and decrypted fields, indicating whether, in the language + of the spec, this is a TLSPlaintext, TLSCompressed, or + TLSCiphertext. The application will have to figure out when it's + appropriate to change these values. + """ + __hdr__ = ( ('type', 'B', 0), ('version', 'H', 0), - ('len', 'H', 0), + ('length', 'H', 0), ) + + def __init__(self, *args, **kwargs): + # assume plaintext unless specified otherwise in arguments + self.compressed = kwargs.pop('compressed', False) + self.encrypted = kwargs.pop('encrypted', False) + # parent constructor + dpkt.Packet.__init__(self, *args, **kwargs) + # make sure length and data are consistent + self.length = len(self.data) + + def unpack(self, buf): + dpkt.Packet.unpack(self, buf) + header_length = self.__hdr_len__ + self.data = buf[header_length:header_length+self.length] + # make sure buffer was long enough + if len(self.data) != self.length: + raise dpkt.NeedData('TLSRecord data was too short.') + # assume compressed and encrypted when it's been parsed from + # raw data + self.compressed = True + self.encrypted = True + + +class TLSChangeCipherSpec(dpkt.Packet): + """ + ChangeCipherSpec message is just a single byte with value 1 + """ + __hdr__ = (('type', 'B', 1),) + + +class TLSAppData(str): + """ + As far as TLSRecord is concerned, AppData is just an opaque blob. + """ + pass + + +class TLSAlert(dpkt.Packet): + + __hdr__ = ( + ('level', 'B', 1), + ('description', 'B', 0), + ) + + +class TLSHelloRequest(dpkt.Packet): + __hdr__ = tuple() + + +class TLSClientHello(dpkt.Packet): + __hdr__ = ( + ('version', 'H', 0x0301), + ('random', '32s', '\x00'*32), + ) # the rest is variable-length and has to be done manually + def unpack(self, buf): dpkt.Packet.unpack(self, buf) - if self.len <= len(self.data): - self.msg, self.data = self.data[:self.len], self.data[self.len:] - -""" -Byte 0 = SSL record type = 22 (SSL3_RT_HANDSHAKE) -Bytes 1-2 = SSL version (major/minor) -Bytes 3-4 = Length of data in the record (excluding the header itself). -Byte 5 = Handshake type -Bytes 6-8 = Length of data to follow in this record -Bytes 9-n = Command-specific data -""" - + # now session, cipher suites, extensions are in self.data + self.session_id, pointer = parse_variable_array(self.data, 1) +# print 'pointer',pointer + # handle ciphersuites + ciphersuites, parsed = parse_variable_array(self.data[pointer:], 2) + pointer += parsed + self.num_ciphersuites = len(ciphersuites) / 2 + # check len(ciphersuites) % 2 == 0 ? + # compression methods + compression_methods, parsed = parse_variable_array( + self.data[pointer:], 1) + pointer += parsed + self.num_compression_methods = parsed - 1 + self.compression_methods = map(ord, compression_methods) + # extensions + + +class TLSServerHello(dpkt.Packet): + __hdr__ = ( + ('version', 'H', '0x0301'), + ('random', '32s', '\x00'*32), + ) # session is variable, forcing rest to be manual + + def unpack(self, buf): + try: + dpkt.Packet.unpack(self, buf) + self.session_id, pointer = parse_variable_array(self.data, 1) + # single cipher suite + self.cipher_suite = struct.unpack('!H', self.data[pointer:pointer+2])[0] + pointer += 2 + # single compression method + self.compression = struct.unpack('!B', self.data[pointer:pointer+1])[0] + pointer += 1 + # ignore extensions for now + except struct.error: + # probably data too short + raise dpkt.NeedData + + +class TLSUnknownHandshake(dpkt.Packet): + __hdr__ = tuple() + +TLSCertificate = TLSUnknownHandshake +TLSServerKeyExchange = TLSUnknownHandshake +TLSCertificateRequest = TLSUnknownHandshake +TLSServerHelloDone = TLSUnknownHandshake +TLSCertificateVerify = TLSUnknownHandshake +TLSClientKeyExchange = TLSUnknownHandshake +TLSFinished = TLSUnknownHandshake + + +# mapping of handshake type ids to their names +# and the classes that implement them +HANDSHAKE_TYPES = { + 0: ('HelloRequest', TLSHelloRequest), + 1: ('ClientHello', TLSClientHello), + 2: ('ServerHello', TLSServerHello), + 11: ('Certificate', TLSCertificate), + 12: ('ServerKeyExchange', TLSServerKeyExchange), + 13: ('CertificateRequest', TLSCertificateRequest), + 14: ('ServerHelloDone', TLSServerHelloDone), + 15: ('CertificateVerify', TLSCertificateVerify), + 16: ('ClientKeyExchange', TLSClientKeyExchange), + 20: ('Finished', TLSFinished), +} + + +class TLSHandshake(dpkt.Packet): + ''' + A TLS Handshake message + + This goes for all messages encapsulated in the Record layer, but especially + important for handshakes and app data: A message may be spread across a + number of TLSRecords, in addition to the possibility of there being more + than one in a given Record. You have to put together the contents of + TLSRecord's yourself. + ''' + + # struct.unpack can't handle the 3-byte int, so we parse it as bytes + # (and store it as bytes so dpkt doesn't get confused), and turn it into + # an int in a user-facing property + __hdr__ = ( + ('type', 'B', 0), + ('length_bytes', '3s', 0), + ) + + def unpack(self, buf): + dpkt.Packet.unpack(self, buf) + # Wait, might there be more than one message of self.type? + embedded_type = HANDSHAKE_TYPES.get(self.type, None) + if embedded_type is None: + raise SSL3Exception('Unknown or invalid handshake type %d' % + self.type) + # only take the right number of bytes + self.data = self.data[:self.length] + if len(self.data) != self.length: + raise dpkt.NeedData + # get class out of embedded_type tuple + self.data = embedded_type[1](self.data) + + @property + def length(self): + return struct.unpack('!I', '\x00' + self.length_bytes)[0] + + +RECORD_TYPES = { + 20: TLSChangeCipherSpec, + 21: TLSAlert, + 22: TLSHandshake, + 23: TLSAppData, +} + class SSLFactory(object): def __new__(cls, buf): v = buf[1:3] - if v == '\x03\x01' or v == '\x03\x00': + if v in [ '\x03\x00', '\x03\x01', '\x03\x02' ]: return SSL3(buf) + # SSL2 has no characteristic header or magic bytes, so we just assume + # that the msg is an SSL2 msg if it is not detected as SSL3+ return SSL2(buf) + + +def TLSMultiFactory(buf): + ''' + Attempt to parse one or more TLSRecord's out of buf + + Args: + buf: string containing SSL/TLS messages. May have an incomplete record + on the end + + Returns: + [TLSRecord] + int, total bytes consumed, != len(buf) if an incomplete record was left at + the end. + + Raises ...? + ''' + if not buf: + return [], 0 + v = buf[1:3] + if v in SSL3_VERSION_BYTES: + try: + msg = TLSRecord(buf) + parsed_bytes = len(msg) # len fn includes header length + except dpkt.NeedData: + return [], 0 # tell caller we parsed nothing + else: + raise SSL3Exception('Bad TLS version in buf: %r' % buf[:5]) + later_messages, later_bytes = TLSMultiFactory(buf[len(msg):]) + return [msg] + later_messages, parsed_bytes + later_bytes + +import unittest + + +_hexdecode = binascii.a2b_hex + + +class TLSRecordTest(unittest.TestCase): + """ + Test basic TLSRecord functionality + + For this test, the contents of the record doesn't matter, since we're not + parsing the next layer. + """ + def setUp(self): + # add some extra data, to make sure length is parsed correctly + self.p = TLSRecord('\x17\x03\x01\x00\x08abcdefghzzzzzzzzzzz') + def testContentType(self): + self.assertEqual(self.p.type, 23) + def testVersion(self): + self.assertEqual(self.p.version, 0x0301) + def testLength(self): + self.assertEqual(self.p.length, 8) + def testData(self): + self.assertEqual(self.p.data, 'abcdefgh') + def testInitialFlags(self): + self.assertTrue(self.p.compressed) + self.assertTrue(self.p.encrypted) + def testRepack(self): + p2 = TLSRecord(type=23, version=0x0301, data='abcdefgh') + self.assertEqual(p2.type, 23) + self.assertEqual(p2.version, 0x0301) + self.assertEqual(p2.length, 8) + self.assertEqual(p2.data, 'abcdefgh') + self.assertEqual(p2.pack(), self.p.pack()) + def testTotalLength(self): + # that len(p) includes header + self.assertEqual(len(self.p), 13) + def testRaisesNeedDataWhenBufIsShort(self): + self.assertRaises( + dpkt.NeedData, + TLSRecord, + '\x16\x03\x01\x00\x10abc') + + +class TLSChangeCipherSpecTest(unittest.TestCase): + "It's just a byte. This will be quick, I promise" + def setUp(self): + self.p = TLSChangeCipherSpec('\x01') + def testParses(self): + self.assertEqual(self.p.type, 1) + def testTotalLength(self): + self.assertEqual(len(self.p), 1) + + +class TLSAppDataTest(unittest.TestCase): + "AppData is basically just a string" + def testValue(self): + d = TLSAppData('abcdefgh') + self.assertEqual(d, 'abcdefgh') + + +class TLSHandshakeTest(unittest.TestCase): + def setUp(self): + self.h = TLSHandshake('\x00\x00\x00\x01\xff') + def testCreatedInsideMessage(self): + self.assertTrue(isinstance(self.h.data, TLSHelloRequest)) + def testLength(self): + self.assertEqual(self.h.length, 0x01) + def testRaisesNeedData(self): + self.assertRaises(dpkt.NeedData, TLSHandshake, '\x00\x00\x01\x01') + + +class ClientHelloTest(unittest.TestCase): + 'This data is extracted from and verified by Wireshark' + + def setUp(self): + self.data = _hexdecode( + "01000199" # handshake header + "0301" # version + "5008220ce5e0e78b6891afe204498c9363feffbe03235a2d9e05b7d990eb708d" # rand + "2009bc0192e008e6fa8fe47998fca91311ba30ddde14a9587dc674b11c3d3e5ed1" # session id + # cipher suites + "005400ffc00ac0140088008700390038c00fc00500840035c007c009c011c0130045004400330032c00cc00ec002c0040096004100050004002fc008c01200160013c00dc003feff000ac006c010c00bc00100020001" + "0100" # compresssion methods + # extensions + "00fc0000000e000c0000096c6f63616c686f7374000a00080006001700180019000b00020100002300d0a50b2e9f618a9ea9bf493ef49b421835cd2f6b05bbe1179d8edf70d58c33d656e8696d36d7e7e0b9d3ecc0e4de339552fa06c64c0fcb550a334bc43944e2739ca342d15a9ebbe981ac87a0d38160507d47af09bdc16c5f0ee4cdceea551539382333226048a026d3a90a0535f4a64236467db8fee22b041af986ad0f253bc369137cd8d8cd061925461d7f4d7895ca9a4181ab554dad50360ac31860e971483877c9335ac1300c5e78f3e56f3b8e0fc16358fcaceefd5c8d8aaae7b35be116f8832856ca61144fcdd95e071b94d0cf7233740000" + "FFFFFFFFFFFFFFFF") # random garbage + self.p = TLSHandshake(self.data) + + def testClientHelloConstructed(self): + 'Make sure the correct class was constructed' + #print self.p + self.assertTrue(isinstance(self.p.data, TLSClientHello)) + +# def testClientDateCorrect(self): +# self.assertEqual(self.p.random_unixtime, 1342710284) + + def testClientRandomCorrect(self): + self.assertEqual(self.p.data.random, + _hexdecode('5008220ce5e0e78b6891afe204498c9363feffbe03235a2d9e05b7d990eb708d')) + + def testCipherSuiteLength(self): + # we won't bother testing the identity of each cipher suite in the list. + self.assertEqual(self.p.data.num_ciphersuites, 42) + #self.assertEqual(len(self.p.ciphersuites), 42) + + def testSessionId(self): + self.assertEqual(self.p.data.session_id, + _hexdecode('09bc0192e008e6fa8fe47998fca91311ba30ddde14a9587dc674b11c3d3e5ed1')) + + def testCompressionMethods(self): + self.assertEqual(self.p.data.num_compression_methods, 1) + + def testTotalLength(self): + self.assertEqual(len(self.p), 413) + + +class ServerHelloTest(unittest.TestCase): + 'Again, from Wireshark' + + def setUp(self): + self.data = _hexdecode('0200004d03015008220c8ec43c5462315a7c99f5d5b6bff009ad285b51dc18485f352e9fdecd2009bc0192e008e6fa8fe47998fca91311ba30ddde14a9587dc674b11c3d3e5ed10002000005ff01000100') + self.p = TLSHandshake(self.data) + + def testConstructed(self): + self.assertTrue(isinstance(self.p.data, TLSServerHello)) + +# def testDateCorrect(self): +# self.assertEqual(self.p.random_unixtime, 1342710284) + + def testRandomCorrect(self): + self.assertEqual(self.p.data.random, + _hexdecode('5008220c8ec43c5462315a7c99f5d5b6bff009ad285b51dc18485f352e9fdecd')) + + def testCipherSuite(self): + self.assertEqual(self.p.data.cipher_suite.name, 'TLS_RSA_WITH_NULL_SHA') + + def testTotalLength(self): + self.assertEqual(len(self.p), 81) + + +class TLSMultiFactoryTest(unittest.TestCase): + "Made up test data" + + def setUp(self): + self.data = _hexdecode('1703010010' # header 1 + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' # data 1 + '1703010010' # header 2 + 'BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB' # data 2 + '1703010010' # header 3 + 'CCCCCCCC') # data 3 (incomplete) + self.msgs, self.bytes_parsed = TLSMultiFactory(self.data) + + def testNumMessages(self): + # only complete messages should be parsed, incomplete ones left + # in buffer + self.assertEqual(len(self.msgs), 2) + + def testBytesParsed(self): + self.assertEqual(self.bytes_parsed, (5 + 16) * 2) + + def testFirstMsgData(self): + self.assertEqual(self.msgs[0].data, _hexdecode('AA' * 16)) + + def testSecondMsgData(self): + self.assertEqual(self.msgs[1].data, _hexdecode('BB' * 16)) + + +if __name__ == '__main__': + unittest.main() diff --git a/dpkt/ssl_ciphersuites.py b/dpkt/ssl_ciphersuites.py new file mode 100644 index 00000000..49148a34 --- /dev/null +++ b/dpkt/ssl_ciphersuites.py @@ -0,0 +1,76 @@ +# Copyright 2012 Google Inc. All rights reserved. + +""" +Nicely formatted cipher suite definitions for TLS + +A list of cipher suites in the form of CipherSuite objects. +These are supposed to be immutable; don't mess with them. +""" + + +class CipherSuite(object): + """ + Encapsulates a cipher suite. + + Members/args: + * code: two-byte ID code, as int + * name: as in 'TLS_RSA_WITH_RC4_40_MD5' + * kx: key exchange algorithm, string + * auth: authentication algorithm, string + * encoding: encoding algorithm + * mac: message authentication code algorithm + """ + + def __init__(self, code, name, kx, auth, encoding, mac): + self.code = code + self.name = name + self.kx = kx + self.auth = auth + self.encoding = encoding + self.mac = mac + + def __repr__(self): + return 'CipherSuite(%s)' % self.name + + MAC_SIZES = { + 'MD5': 16, + 'SHA': 20, + 'SHA256': 32, # I guess + } + + BLOCK_SIZES = { + 'AES_256_CBC': 16, + } + + @property + def mac_size(self): + """In bytes. Default to 0.""" + return self.MAC_SIZES.get(self.mac, 0) + + @property + def block_size(self): + """In bytes. Default to 1.""" + return self.BLOCK_SIZES.get(self.encoding, 1) + + +# master list of CipherSuite Objects +CIPHERSUITES = [ + # not a real cipher suite, can be ignored, see RFC5746 + CipherSuite(0xff, 'TLS_EMPTY_RENEGOTIATION_INFO', + 'NULL', 'NULL', 'NULL', 'NULL'), + CipherSuite(0x00, 'TLS_NULL_WITH_NULL_NULL', + 'NULL', 'NULL', 'NULL', 'NULL'), + CipherSuite(0x01, 'TLS_RSA_WITH_NULL_MD5', 'RSA', 'RSA', 'NULL', 'MD5'), + CipherSuite(0x02, 'TLS_RSA_WITH_NULL_SHA', 'RSA', 'RSA', 'NULL', 'SHA'), + CipherSuite(0x0039, 'TLS_DHE_RSA_WITH_AES_256_CBC_SHA', + 'DHE', 'RSA', 'AES_256_CBC', 'SHA'), # not sure I got the kx/auth thing right. + CipherSuite(0xffff, 'UNKNOWN_CIPHER', '', '', '', '') +] + +BY_CODE = dict( + (cipher.code, cipher) for cipher in CIPHERSUITES) + +BY_NAME = dict( + (suite.name, suite) for suite in CIPHERSUITES) + +NULL_SUITE = BY_CODE[0x00] diff --git a/dpkt/vrrp.py b/dpkt/vrrp.py index 4c7af84c..271e3ecd 100644 --- a/dpkt/vrrp.py +++ b/dpkt/vrrp.py @@ -1,4 +1,4 @@ -# $Id: vrrp.py 23 2006-11-08 15:45:33Z dugsong $ +# $Id: vrrp.py 88 2013-03-05 19:43:17Z andrewflnr@gmail.com $ """Virtual Router Redundancy Protocol.""" @@ -26,7 +26,7 @@ def _get_type(self): return self.vtype & 0xf def _set_type(self, v): self.vtype = (self.vtype & ~0xf0) | (v & 0xf) - type = property(_get_v, _set_v) + type = property(_get_type, _set_type) def unpack(self, buf): dpkt.Packet.unpack(self, buf)