diff --git a/dtls/__init__.py b/dtls/__init__.py new file mode 100644 index 0000000..d110877 --- /dev/null +++ b/dtls/__init__.py @@ -0,0 +1,11 @@ +# PyDTLS: datagram TLS for Python. Written by Ray Brown. +"""PyDTLS package + +This package exports OpenSSL's DTLS support to Python. Importing it will add +the constant PROTOCOL_DTLSv1 to the Python standard library's ssl module. +Passing a datagram socket to that module's wrap_socket function (or +instantiating its SSLSocket class with a datagram socket) will activate this +module's DTLS implementation for the returned SSLSocket instance. + +wrap_socket's parameters and their semantics have been maintained. +""" diff --git a/dtls/cygcrypto-1.0.0.dll b/dtls/cygcrypto-1.0.0.dll new file mode 100644 index 0000000..1b649e8 Binary files /dev/null and b/dtls/cygcrypto-1.0.0.dll differ diff --git a/dtls/cygssl-1.0.0.dll b/dtls/cygssl-1.0.0.dll new file mode 100644 index 0000000..45815bd Binary files /dev/null and b/dtls/cygssl-1.0.0.dll differ diff --git a/dtls/demux/__init__.py b/dtls/demux/__init__.py new file mode 100644 index 0000000..4c88f1a --- /dev/null +++ b/dtls/demux/__init__.py @@ -0,0 +1,26 @@ +# Demux loader: imports a demux module appropriate for this platform. +# Written by Ray Brown. +"""UDP Demux + +A UDP demux is a wrapper for a datagram socket. The demux must be initialized +with an unconnected datagram socket, referred to as the root socket. Once +initialized, the demux will create new connections to peer endpoints upon +arrival of datagrams from a new endpoint. Such a connection is of a +socket-derived type, and will receive datagrams only from the peer endpoint for +which it was created, and that are sent to the root socket. + +Connections must be used for receiving datagrams only. Outgoing traffic should +be sent through the root socket. + +Varying implementations of this functionality are provided for different +platforms. +""" + +import sys + +if sys.platform.startswith('win') or sys.platform.startswith('cygwin'): + from router import UDPDemux +else: + from osnet import UDPDemux + +__all__ = ["UDPDemux"] diff --git a/dtls/demux/osnet.py b/dtls/demux/osnet.py new file mode 100644 index 0000000..cb7e607 --- /dev/null +++ b/dtls/demux/osnet.py @@ -0,0 +1,62 @@ +# OSNet demux: uses the OS network stack to demultiplex incoming datagrams +# among sockets bound to the same ports. Written by Ray Brown. +"""OS Network UDP Demux + +This module implements a demux that uses the OS network stack to demultiplex +datagrams coming from different peers among datagram sockets that are all bound +to the port at which these datagrams are being received. The network stack is +instructed as to which socket an incoming datagram should be sent to by +connecting the destination socket to the peer endpoint. + +The OSNet demux requires operating system functionality that exists in the +Linux kernel, but not in the Windows network stack. + +Classes: + + UDPDemux -- a network stack configuring UDP demux + +Exceptions: + + KeyError -- raised for unknown peer addresses +""" + + +class UDPDemux(object): + """OS network stack configuring demux + + This class implements a demux that creates sockets connected to peer + network endpoints, configuring the network stack to demultiplex + incoming datagrams from these endpoints among these sockets. + + Methods: + + get_connection -- create a new connection or retrieve an existing one + remove_connection -- remove an existing connection + service -- this method does nothing for this type of demux + """ + + def get_connection(self, address): + """Create or retrieve a muxed connection + + Arguments: + address -- a peer endpoint in IPv4/v6 address format; None refers + to the connection for unknown peers + + Return: + a bound, connected datagram socket instance, or the root socket + in case address was None + """ + + def remove_connection(self, address): + """Remove a muxed connection + + Arguments: + address -- an address for which a muxed connection was previously + retrieved through get_connection, which has not yet + been removed + + Return: + the socket object whose connection has been removed + """ + + return self.connections.pop(address) diff --git a/dtls/demux/router.py b/dtls/demux/router.py new file mode 100644 index 0000000..6d00565 --- /dev/null +++ b/dtls/demux/router.py @@ -0,0 +1,172 @@ +# Routing demux: forwards datagrams from the root socket to connected +# sockets bound to ephemeral ports. Written by Ray Brown. +"""Routing UDP Demux + +This module implements an explicitly routing UDP demux. New connections create +datagram sockets bound to ephemeral ports on the loopback interface and +connected to a forwarding socket. The demux services incoming datagrams by +receiving them from the root socket and sending them to the socket belonging to +the connection that is associated with the sending peer. + +A routing UDP demux can be used on any platform. + +Classes: + + UDPDemux -- an explicitly routing UDP demux + +Exceptions: + + InvalidSocketError -- exception raised for improper socket objects + KeyError -- raised for unknown peer addresses +""" + +import socket +from logging import getLogger +from weakref import WeakValueDictionary +from ..err import InvalidSocketError + +_logger = getLogger(__name__) + +UDP_MAX_DGRAM_LENGTH = 65527 + + +class UDPDemux(object): + """Explicitly routing UDP demux + + This class implements a demux that forwards packets from the root + socket to sockets belonging to connections. It does this whenever its + service method is invoked. + + Methods: + + remove_connection -- remove an existing connection + service -- distribute datagrams from the root socket to connections + forward -- forward a stored datagram to a connection + """ + + _forwarding_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + _forwarding_socket.bind(('127.0.0.1', 0)) + + def __init__(self, datagram_socket): + """Constructor + + Arguments: + datagram_socket -- the root socket; this must be a bound, unconnected + datagram socket + """ + + if datagram_socket.type != socket.SOCK_DGRAM: + raise InvalidSocketError("datagram_socket is not of " + + "type SOCK_DGRAM") + try: + datagram_socket.getsockname() + except: + raise InvalidSocketError("datagram_socket is unbound") + try: + datagram_socket.getpeername() + except: + pass + else: + raise InvalidSocketError("datagram_socket is connected") + + self.datagram_socket = datagram_socket + self.payload = "" + self.payload_peer_address = None + self.connections = WeakValueDictionary() + + def get_connection(self, address): + """Create or retrieve a muxed connection + + Arguments: + address -- a peer endpoint in IPv4/v6 address format; None refers + to the connection for unknown peers + + Return: + a bound, connected datagram socket instance + """ + + if self.connections.has_key(address): + return self.connections[address] + + # We need a new datagram socket on a dynamically assigned ephemeral port + conn = socket.socket(self._forwarding_socket.family, + self._forwarding_socket.type, + self._forwarding_socket.proto) + conn.bind((self._forwarding_socket.getsockname()[0], 0)) + conn.connect(self._forwarding_socket.getsockname()) + if not address: + conn.setblocking(0) + self.connections[address] = conn + _logger.debug("Created new connection for address: %s", address) + return conn + + def remove_connection(self, address): + """Remove a muxed connection + + Arguments: + address -- an address that was previously returned by the service + method and whose connection has not yet been removed + + Return: + the socket object whose connection has been removed + """ + + return self.connections.pop(address) + + def service(self): + """Service the root socket + + Read from the root socket and forward one datagram to a + connection. The call will return without forwarding data + if any of the following occurs: + + * An error is encountered while reading from the root socket + * Reading from the root socket times out + * The root socket is non-blocking and has no data available + * An empty payload is received + * A non-empty payload is received from an unknown peer (a peer + for which get_connection has not yet been called); in this case, + the payload is held by this instance and will be forwarded when + the forward method is called + + Return: + if the datagram received was from a new peer, then the peer's + address; otherwise None + """ + + self.payload, self.payload_peer_address = \ + self.datagram_socket.recvfrom(UDP_MAX_DGRAM_LENGTH) + _logger.debug("Received datagram from peer: %s", + self.payload_peer_address) + if not self.payload: + self.payload_peer_address = None + return + if self.connections.has_key(self.payload_peer_address): + self.forward() + else: + return self.payload_peer_address + + def forward(self): + """Forward a stored datagram + + When the service method returns the address of a new peer, it holds + the datagram from that peer in this instance. In this case, this + method will perform the forwarding step. The target connection is the + one associated with address None if get_connection has not been called + since the service method returned the new peer's address, and the + connection associated with the new peer's address if it has. + """ + + assert self.payload + assert self.payload_peer_address + if self.connections.has_key(self.payload_peer_address): + conn = self.connections[self.payload_peer_address] + default = False + else: + conn = self.connections[None] # propagate exception if not created + default = True + _logger.debug("Forwarding datagram from peer: %s, default: %s", + self.payload_peer_address, default) + self._forwarding_socket.sendto(self.payload, conn.getsockname()) + self.payload = "" + self.payload_peer_address = None diff --git a/dtls/err.py b/dtls/err.py new file mode 100644 index 0000000..2e3fcb5 --- /dev/null +++ b/dtls/err.py @@ -0,0 +1,67 @@ +# DTLS exceptions. Written by Ray Brown +"""DTLS Errors + +This module defines error functionality and exception types for the dtls +package. + +Classes: + + SSLError -- exception raised for I/O errors + InvalidSocketError -- exception raised for improper socket objects +""" + +from socket import error as socket_error + +SSL_ERROR_NONE = 0 +SSL_ERROR_SSL = 1 +SSL_ERROR_WANT_READ = 2 +SSL_ERROR_WANT_WRITE = 3 +SSL_ERROR_WANT_X509_LOOKUP = 4 +SSL_ERROR_SYSCALL = 5 +SSL_ERROR_ZERO_RETURN = 6 +SSL_ERROR_WANT_CONNECT = 7 +SSL_ERROR_WANT_ACCEPT = 8 + +ERR_BOTH_KEY_CERT_FILES = 500 +ERR_BOTH_KEY_CERT_FILES_SVR = 298 +ERR_NO_CERTS = 331 + +ERR_COOKIE_MISMATCH = 0x1408A134 + + +class SSLError(socket_error): + """This exception is raised by modules in the dtls package.""" + def __init__(self, *args): + super(SSLError, self).__init__(*args) + + +class OpenSSLError(SSLError): + """This exception is raised when an error occurs in the OpenSSL library""" + def __init__(self, ssl_error, errqueue, result, func, args): + self.ssl_error = ssl_error + self.errqueue = errqueue + self.result = result + self.func = func + self.args = args + super(OpenSSLError, self).__init__(ssl_error, errqueue, + result, func, args) + + +class InvalidSocketError(Exception): + """There is a problem with a socket passed to the dtls package.""" + def __init__(self, *args): + super(InvalidSocketError, self).__init__(*args) + + +def raise_ssl_error(code): + """Raise an SSL error with the given error code""" + raise SSLError(str(code) + ": " + _ssl_errors[code]) + +_ssl_errors = { + ERR_NO_CERTS: "No root certificates specified for verification " + \ + "of other-side certificates", + ERR_BOTH_KEY_CERT_FILES: "Both the key & certificate files " + \ + "must be specified", + ERR_BOTH_KEY_CERT_FILES_SVR: "Both the key & certificate files must be " + \ + "specified for server-side operation" + } diff --git a/dtls/libeay32.dll b/dtls/libeay32.dll new file mode 100644 index 0000000..d0306d5 Binary files /dev/null and b/dtls/libeay32.dll differ diff --git a/dtls/openssl.py b/dtls/openssl.py new file mode 100644 index 0000000..2192d52 --- /dev/null +++ b/dtls/openssl.py @@ -0,0 +1,455 @@ +# OpenSSL library wrapper: provide access to both OpenSSL dynamic libraries +# through ctypes. Wrtten by Ray Brown. +"""OpenSSL Wrapper + +This module provides run-time access to the OpenSSL cryptographic and +protocols libraries. + +Exceptions: + + OpenSSLError -- exception raised when errors occur in the OpenSSL library + +Functions: + +Integer constants: + + BIO_NOCLOSE -- don't destroy encapsulated resource when closing BIO + BIO_CLOSE -- do destroy encapsulated resource when closing BIO + +""" + +import sys +import array +import socket +from logging import getLogger +from os import path +from err import OpenSSLError +from err import SSL_ERROR_NONE +import ctypes +from ctypes import CDLL +from ctypes import CFUNCTYPE +from ctypes import c_void_p, c_int, c_uint, c_ulong, c_char_p, c_size_t +from ctypes import c_short, c_ushort, c_ubyte, c_char +from ctypes import byref, POINTER +from ctypes import Structure, Union +from ctypes import create_string_buffer, sizeof, memmove + +# +# Module initialization +# +_logger = getLogger(__name__) + +# +# Library loading +# +if sys.platform.startswith('win'): + dll_path = path.abspath(path.dirname(__file__)) + #libcrypto = CDLL(path.join(dll_path, "libeay32.dll")) + #libssl = CDLL(path.join(dll_path, "ssleay32.dll")) + libcrypto = CDLL(path.join(dll_path, "cygcrypto-1.0.0.dll")) + libssl = CDLL(path.join(dll_path, "cygssl-1.0.0.dll")) +else: + libcrypto = CDLL("libcrypto.so.1.0.0") + libssl = CDLL("libssl.so.1.0.0") + +# +# Integer constants - exported +# +BIO_NOCLOSE = 0x00 +BIO_CLOSE = 0x01 +SSL_VERIFY_NONE = 0x00 +SSL_VERIFY_PEER = 0x01 +SSL_VERIFY_FAIL_IF_NO_PEER_CERT = 0x02 +SSL_VERIFY_CLIENT_ONCE = 0x04 +SSL_SESS_CACHE_OFF = 0x0000 +SSL_SESS_CACHE_CLIENT = 0x0001 +SSL_SESS_CACHE_SERVER = 0x0002 +SSL_SESS_CACHE_BOTH = SSL_SESS_CACHE_CLIENT | SSL_SESS_CACHE_SERVER +SSL_SESS_CACHE_NO_AUTO_CLEAR = 0x0080 +SSL_SESS_CACHE_NO_INTERNAL_LOOKUP = 0x0100 +SSL_SESS_CACHE_NO_INTERNAL_STORE = 0x0200 +SSL_SESS_CACHE_NO_INTERNAL = \ + SSL_SESS_CACHE_NO_INTERNAL_LOOKUP | SSL_SESS_CACHE_NO_INTERNAL_STORE +SSL_FILE_TYPE_PEM = 1 + +# +# Integer constants - internal +# +SSL_CTRL_SET_SESS_CACHE_MODE = 44 +SSL_CTRL_SET_READ_AHEAD = 41 +BIO_CTRL_DGRAM_SET_CONNECTED = 32 +BIO_CTRL_DGRAM_GET_PEER = 46 +BIO_CTRL_DGRAM_SET_PEER = 44 +BIO_C_SET_NBIO = 102 +DTLS_CTRL_LISTEN = 75 + +# +# Parameter data types +# +class c_long(object): + """Long integer paramter class + + c_long must be distinguishable from c_int, as the latter is associated + with a default error checking routine, while the former is not. + """ + + +class FuncParam(object): + """Function parameter or return type""" + @classmethod + def from_param(cls, value): + if not isinstance(value, cls): + _logger.error("Parameter type mismatch: %s not of type %s", + value, cls) + raise TypeError(repr(value) + " is not of type " + repr(cls)) + return value._as_parameter + + def __init__(self, value): + self._as_parameter = value + + +class DTLSv1Method(FuncParam): + def __init__(self, value): + super(DTLSv1Method, self).__init__(value) + + +class SSLCTX(FuncParam): + def __init__(self, value): + super(SSLCTX, self).__init__(value) + + +class SSL(FuncParam): + def __init__(self, value): + super(SSL, self).__init__(value) + + +class BIO(FuncParam): + def __init__(self, value): + super(BIO, self).__init__(value) + + +# +# Socket address conversions +# +class sockaddr_storage(Structure): + _fields_ = [("ss_family", c_short), + ("pad", c_char * 126)] + +class sockaddr_in(Structure): + _fields_ = [("sin_family", c_short), + ("sin_port", c_ushort), + ("sin_addr", c_ulong * 1), + ("sin_zero", c_char * 8)] + +class sockaddr_in6(Structure): + _fields_ = [("sin6_family", c_short), + ("sin6_port", c_ushort), + ("sin6_flowinfo", c_ulong), + ("sin6_addr", c_ulong * 4), + ("sin6_scope_id", c_ulong)] + +class sockaddr_u(Union): + _fields_ = [("ss", sockaddr_storage), + ("s4", sockaddr_in), + ("s6", sockaddr_in6)] + +py_inet_ntop = getattr(socket, "inet_ntop", None) +if not py_inet_ntop: + windll = getattr(ctypes, "windll", None) + if windll: + wsa_inet_ntop = getattr(windll.ws2_32, "inet_ntop", None) + else: + wsa_inet_ntop = None + +py_inet_pton = getattr(socket, "inet_pton", None) +if not py_inet_pton: + windll = getattr(ctypes, "windll", None) + if windll: + wsa_inet_pton = getattr(windll.ws2_32, "inet_pton", None) + else: + wsa_inet_pton = None + +def inet_ntop(address_family, packed_ip): + if py_inet_ntop: + return py_inet_ntop(address_family, + array.array('L', packed_ip).tostring()) + if wsa_inet_ntop: + string_buf = create_string_buffer(47) + wsa_inet_ntop(address_family, packed_ip, + string_buf, sizeof(string_buf)) + if not string_buf.value: + raise ValueError("wsa_inet_ntop failed with: %s" % + array.array('L', packed_ip).tostring()) + return string_buf.value + if address_family == socket.AF_INET6: + raise ValueError("Platform does not support IPv6") + return socket.inet_ntoa(array.array('L', packed_ip).tostring()) + +def inet_pton(address_family, string_ip): + if address_family == socket.AF_INET6: + ret_packed_ip = (c_ulong * 4)() + else: + ret_packed_ip = (c_ulong * 1)() + if py_inet_pton: + ret_string = py_inet_pton(address_family, string_ip) + ret_packed_ip[:] = array.array('L', ret_string) + elif wsa_inet_pton: + if wsa_inet_pton(address_family, string_ip, ret_packed_ip) != 1: + raise ValueError("wsa_inet_pton failed with: %s" % string_ip) + else: + if address_family == socket.AF_INET6: + raise ValueError("Platform does not support IPv6") + ret_string = socket.inet_aton(string_ip) + ret_packed_ip[:] = array.array('L', ret_string) + return ret_packed_ip + +def addr_tuple_from_sockaddr_u(su): + if su.ss.ss_family == socket.AF_INET6: + return (inet_ntop(socket.AF_INET6, su.s6.sin6_addr), + socket.ntohs(su.s6.sin6_port), + socket.ntohl(su.s6.sin6_flowinfo), + socket.ntohl(su.s6.sin6_scope_id)) + assert su.ss.ss_family == socket.AF_INET + return inet_ntop(socket.AF_INET, su.s4.sin_addr), \ + socket.ntohs(su.s4.sin_port) + +def sockaddr_u_from_addr_tuple(address): + su = sockaddr_u() + if len(address) > 2: + su.ss.ss_family = socket.AF_INET6 + su.s6.sin6_addr[:] = inet_pton(socket.AF_INET6, address[0]) + su.s6.sin6_port = socket.htons(address[1]) + su.s6.sin6_flowinfo = socket.htonl(address[2]) + su.s6.sin6_scope_id = socket.htonl(address[3]) + else: + su.ss.ss_family = socket.AF_INET + su.s4.sin_addr[:] = inet_pton(socket.AF_INET, address[0]) + su.s4.sin_port = socket.htons(address[1]) + return su + +# +# Error handling +# +def raise_ssl_error(result, func, args, ssl): + if not ssl: + ssl_error = SSL_ERROR_NONE + else: + ssl_error = _SSL_get_error(ssl, result) + errqueue = [] + while True: + err = _ERR_get_error() + if not err: + break + buf = create_string_buffer(512) + _ERR_error_string_n(err, buf, sizeof(buf)) + errqueue.append((err, buf.value)) + _logger.debug("SSL error raised: ssl_error: %d, result: %d, " + + "errqueue: %s, func_name: %s", + ssl_error, result, errqueue, func.func_name) + raise OpenSSLError(ssl_error, errqueue, result, func, args) + +def find_ssl_arg(args): + for arg in args: + if isinstance(arg, SSL): + return arg + +def errcheck_ord(result, func, args): + if result <= 0: + raise_ssl_error(result, func, args, find_ssl_arg(args)) + return args + +def errcheck_p(result, func, args): + if not result: + raise_ssl_error(result, func, args, None) + return args + +# +# Function prototypes +# +def _make_function(name, lib, args, export=True, errcheck="default"): + assert len(args) + + def type_subst(map_type): + if _subst.has_key(map_type): + return _subst[map_type] + return map_type + + sig = tuple(type_subst(i[0]) for i in args) + if not _sigs.has_key(sig): + _sigs[sig] = CFUNCTYPE(*sig) + if export: + glbl_name = name + __all__.append(name) + else: + glbl_name = "_" + name + func = _sigs[sig]((name, lib), tuple((i[2] if len(i) > 2 else 1, + i[1], + i[3] if len(i) > 3 else None) + [:3 if len(i) > 3 else 2] + for i in args[1:])) + func.func_name = name + if errcheck == "default": + # Assign error checker based on return type + if args[0][0] in (c_int,): + errcheck = errcheck_ord + elif args[0][0] in (c_void_p, c_char_p) or \ + isinstance(args[0][0], FuncParam): + errcheck = errcheck_p + else: + errcheck = None + if errcheck: + func.errcheck = errcheck + globals()[glbl_name] = func + +_subst = {c_long: ctypes.c_long} +_sigs = {} +__all__ = ["BIO_NOCLOSE", "BIO_CLOSE", + "SSL_VERIFY_NONE", "SSL_VERIFY_PEER", + "SSL_VERIFY_FAIL_IF_NO_PEER_CERT", "SSL_VERIFY_CLIENT_ONCE", + "SSL_SESS_CACHE_OFF", "SSL_SESS_CACHE_CLIENT", + "SSL_SESS_CACHE_SERVER", "SSL_SESS_CACHE_BOTH", + "SSL_SESS_CACHE_NO_AUTO_CLEAR", "SSL_SESS_CACHE_NO_INTERNAL_LOOKUP", + "SSL_SESS_CACHE_NO_INTERNAL_STORE", "SSL_SESS_CACHE_NO_INTERNAL", + "SSL_FILE_TYPE_PEM", + "DTLSv1_listen", + "BIO_dgram_set_connected", + "BIO_dgram_get_peer", "BIO_dgram_set_peer", + "BIO_set_nbio", + "SSL_CTX_set_session_cache_mode", "SSL_CTX_set_read_ahead", + "SSL_read", "SSL_write", + "SSL_CTX_set_cookie_cb"] + +map(lambda x: _make_function(*x), ( + ("SSL_library_init", libssl, ((c_int, "ret"),)), + ("SSL_load_error_strings", libssl, ((None, "ret"),)), + ("DTLSv1_server_method", libssl, ((DTLSv1Method, "ret"),)), + ("DTLSv1_client_method", libssl, ((DTLSv1Method, "ret"),)), + ("SSL_CTX_new", libssl, ((SSLCTX, "ret"), (DTLSv1Method, "meth"))), + ("SSL_CTX_free", libssl, ((None, "ret"), (SSLCTX, "ctx"))), + ("SSL_CTX_set_cookie_generate_cb", libssl, + ((None, "ret"), (SSLCTX, "ctx"), (c_void_p, "app_gen_cookie_cb")), False), + ("SSL_CTX_set_cookie_verify_cb", libssl, + ((None, "ret"), (SSLCTX, "ctx"), (c_void_p, "app_verify_cookie_cb")), + False), + ("SSL_new", libssl, ((SSL, "ret"), (SSLCTX, "ctx"))), + ("SSL_free", libssl, ((None, "ret"), (SSL, "ssl"))), + ("SSL_set_bio", libssl, + ((None, "ret"), (SSL, "ssl"), (BIO, "rbio"), (BIO, "wbio"))), + ("BIO_new_dgram", libcrypto, + ((BIO, "ret"), (c_int, "fd"), (c_int, "close_flag"))), + ("BIO_free", libcrypto, ((c_int, "ret"), (BIO, "a"))), + ("SSL_CTX_ctrl", libssl, + ((c_long, "ret"), (SSLCTX, "ctx"), (c_int, "cmd"), (c_long, "larg"), + (c_void_p, "parg")), False), + ("BIO_ctrl", libcrypto, + ((c_long, "ret"), (BIO, "bp"), (c_int, "cmd"), (c_long, "larg"), + (c_void_p, "parg")), False), + ("SSL_ctrl", libssl, + ((c_long, "ret"), (SSL, "ssl"), (c_int, "cmd"), (c_long, "larg"), + (c_void_p, "parg")), False), + ("ERR_get_error", libcrypto, ((c_long, "ret"),), False), + ("ERR_error_string_n", libcrypto, + ((None, "ret"), (c_ulong, "e"), (c_char_p, "buf"), (c_size_t, "len")), + False), + ("SSL_get_error", libssl, ((c_int, "ret"), (SSL, "ssl"), (c_int, "ret")), + False, None), + ("SSL_CTX_set_cipher_list", libssl, + ((c_int, "ret"), (SSLCTX, "ctx"), (c_char_p, "str"))), + ("SSL_CTX_use_certificate_file", libssl, + ((c_int, "ret"), (SSLCTX, "ctx"), (c_char_p, "file"), (c_int, "type"))), + ("SSL_CTX_use_certificate_chain_file", libssl, + ((c_int, "ret"), (SSLCTX, "ctx"), (c_char_p, "file"))), + ("SSL_CTX_use_PrivateKey_file", libssl, + ((c_int, "ret"), (SSLCTX, "ctx"), (c_char_p, "file"), (c_int, "type"))), + ("SSL_CTX_load_verify_locations", libssl, + ((c_int, "ret"), (SSLCTX, "ctx"), (c_char_p, "CAfile"), + (c_char_p, "CApath"))), + ("SSL_CTX_set_verify", libssl, + ((None, "ret"), (SSLCTX, "ctx"), (c_int, "mode"), + (c_void_p, "verify_callback", 1, None))), + ("SSL_accept", libssl, ((c_int, "ret"), (SSL, "ssl"))), + ("SSL_connect", libssl, ((c_int, "ret"), (SSL, "ssl"))), + ("SSL_set_connect_state", libssl, ((None, "ret"), (SSL, "ssl"))), + ("SSL_set_accept_state", libssl, ((None, "ret"), (SSL, "ssl"))), + ("SSL_do_handshake", libssl, ((c_int, "ret"), (SSL, "ssl"))), + ("SSL_read", libssl, + ((c_int, "ret"), (SSL, "ssl"), (c_void_p, "buf"), (c_int, "num")), False), + ("SSL_write", libssl, + ((c_int, "ret"), (SSL, "ssl"), (c_void_p, "buf"), (c_int, "num")), False), + ("SSL_shutdown", libssl, ((c_int, "ret"), (SSL, "ssl"))), + ("SSL_set_read_ahead", libssl, + ((None, "ret"), (SSL, "ssl"), (c_int, "yes"))), + )) + +# +# Wrappers - functions generally equivalent to OpenSSL library macros +# +_rint_voidp_ubytep_uintp = CFUNCTYPE(c_int, c_void_p, POINTER(c_ubyte), + POINTER(c_uint)) +_rint_voidp_ubytep_uint = CFUNCTYPE(c_int, c_void_p, POINTER(c_ubyte), c_uint) + +def SSL_CTX_set_session_cache_mode(ctx, mode): + # Returns the previous value of mode + _SSL_CTX_ctrl(ctx, SSL_CTRL_SET_SESS_CACHE_MODE, mode, None) + +def SSL_CTX_set_read_ahead(ctx, m): + # Returns the previous value of m + _SSL_CTX_ctrl(ctx, SSL_CTRL_SET_READ_AHEAD, m, None) + +def SSL_CTX_set_cookie_cb(ctx, generate, verify): + def py_generate_cookie_cb(ssl, cookie, cookie_len): + try: + ret_cookie = generate(SSL(ssl)) + except: + _logger.exception("Cookie generation failed") + return 0 + cookie_len[0] = len(ret_cookie) + memmove(cookie, ret_cookie, cookie_len[0]) + _logger.debug("Returning cookie: %s", cookie[:cookie_len[0]]) + return 1 + + def py_verify_cookie_cb(ssl, cookie, cookie_len): + _logger.debug("Verifying cookie: %s", cookie[:cookie_len]) + try: + verify(SSL(ssl), ''.join([chr(i) for i in cookie[:cookie_len]])) + except: + _logger.debug("Cookie verification failed") + return 0 + return 1 + + gen_cb = _rint_voidp_ubytep_uintp(py_generate_cookie_cb) + ver_cb = _rint_voidp_ubytep_uint(py_verify_cookie_cb) + _SSL_CTX_set_cookie_generate_cb(ctx, gen_cb) + _SSL_CTX_set_cookie_verify_cb(ctx, ver_cb) + return gen_cb, ver_cb + +def BIO_dgram_set_connected(bio, peer_address): + su = sockaddr_u_from_addr_tuple(peer_address) + _BIO_ctrl(bio, BIO_CTRL_DGRAM_SET_CONNECTED, 0, byref(su)) + +def BIO_dgram_get_peer(bio): + su = sockaddr_u() + _BIO_ctrl(bio, BIO_CTRL_DGRAM_GET_PEER, 0, byref(su)) + return addr_tuple_from_sockaddr_u(su) + +def BIO_dgram_set_peer(bio, peer_address): + su = sockaddr_u_from_addr_tuple(peer_address) + _BIO_ctrl(bio, BIO_CTRL_DGRAM_SET_PEER, 0, byref(su)) + +def BIO_set_nbio(bio, n): + _BIO_ctrl(bio, BIO_C_SET_NBIO, 1 if n else 0, None) + +def DTLSv1_listen(ssl): + su = sockaddr_u() + ret = _SSL_ctrl(ssl, DTLS_CTRL_LISTEN, 0, byref(su)) + errcheck_ord(ret, _SSL_ctrl, (ssl, DTLS_CTRL_LISTEN, 0, byref(su))) + return addr_tuple_from_sockaddr_u(su) + +def SSL_read(ssl, length): + buf = create_string_buffer(length) + res_len = _SSL_read(ssl, buf, length) + return buf.raw[:res_len] + +def SSL_write(ssl, data): + str_data = str(data) + return _SSL_write(ssl, str_data, len(str_data)) diff --git a/dtls/sslconnection.py b/dtls/sslconnection.py new file mode 100644 index 0000000..c1b909f --- /dev/null +++ b/dtls/sslconnection.py @@ -0,0 +1,445 @@ +# SSL connection: state and behavior associated with the connection between +# the OpenSSL library and an individual peer. Written by Ray Brown. +"""SSL Connection + +This module encapsulates the state and behavior associated with the connection +between the OpenSSL library and an individual peer when using the DTLS +protocol. It defines the application side of the interface of a client with a +DTLS server, and of a server with a DTLS client. + +Classes: + + SSLConnection -- DTLS peer association + +Integer constants: + + PROTOCOL_DTLSv1 + +The cert group must coincide in meaning and value with the one of the standard +library's ssl module, since its values can be passed to this module. + + CERT_NONE + CERT_OPTIONAL + CERT_REQUIRED +""" + +import errno +import socket +import hmac +from logging import getLogger +from os import urandom +from weakref import proxy +from err import OpenSSLError, InvalidSocketError +from err import raise_ssl_error +from err import SSL_ERROR_WANT_READ, ERR_COOKIE_MISMATCH, ERR_NO_CERTS +from openssl import * + +_logger = getLogger(__name__) + +PROTOCOL_DTLSv1 = 256 +CERT_NONE = 0 +CERT_OPTIONAL = 1 +CERT_REQUIRED = 2 + +# +# One-time global OpenSSL library initialization +# +SSL_library_init() +SSL_load_error_strings() + + +class _Rsrc(object): + """Wrapper base for library-owned resources""" + def __init__(self, value): + self._value = value + + @property + def value(self): + return self._value + + +class _CTX(_Rsrc): + """SSL_CTX wrapper""" + def __init__(self, value): + super(_CTX, self).__init__(value) + + def __del__(self): + _logger.debug("Freeing SSL CTX: %d", self._value._as_parameter) + SSL_CTX_free(self._value) + self._value = None + + +class _BIO(_Rsrc): + """BIO wrapper""" + def __init__(self, value): + super(_BIO, self).__init__(value) + self.owned = True + + def disown(self): + self.owned = False + + def __del__(self): + if self.owned: + _logger.debug("Freeing BIO: %d", self._value._as_parameter) + BIO_free(self._value) + self.owned = False + self._value = None + + +class _SSL(_Rsrc): + """SSL structure wrapper""" + def __init__(self, value): + super(_SSL, self).__init__(value) + + def __del__(self): + _logger.debug("Freeing SSL: %d", self._value._as_parameter) + SSL_free(self._value) + self._value = None + + +class _CallbackProxy(object): + """Callback gateway to an SSLConnection object + + This class forms a weak connection between a callback method and + an SSLConnection object. It can be passed as a callback callable + without creating a strong reference through bound methods of + the SSLConnection. + """ + + def __init__(self, cbm): + self.ssl_connection = proxy(cbm.im_self) + self.ssl_func = cbm.im_func + + def __call__(self, *args, **kwargs): + return self.ssl_func(self.ssl_connection, *args, **kwargs) + + +class SSLConnection(object): + """DTLS peer association + + This class associates two DTLS peer instances, wrapping OpenSSL library + state including SSL (struct ssl_st), SSL_CTX, and BIO instances. + """ + + _rnd_key = urandom(16) + + def _init_server(self): + if self.sock.type != socket.SOCK_DGRAM: + raise InvalidSocketError("sock must be of type SOCK_DGRAM") + + from demux import UDPDemux + self.udp_demux = UDPDemux(self.sock) + self.rsock = self.udp_demux.get_connection(None) + self.wbio = _BIO(BIO_new_dgram(self.sock.fileno(), BIO_NOCLOSE)) + self.rbio = _BIO(BIO_new_dgram(self.rsock.fileno(), BIO_NOCLOSE)) + self.ctx = _CTX(SSL_CTX_new(DTLSv1_server_method())) + SSL_CTX_set_session_cache_mode(self.ctx.value, SSL_SESS_CACHE_OFF) + if self.cert_reqs == CERT_NONE: + verify_mode = SSL_VERIFY_NONE + elif self.cert_reqs == CERT_OPTIONAL: + verify_mode = SSL_VERIFY_PEER | SSL_VERIFY_CLIENT_ONCE + else: + verify_mode = SSL_VERIFY_PEER | SSL_VERIFY_CLIENT_ONCE | \ + SSL_VERIFY_FAIL_IF_NO_PEER_CERT + self.listening = False + self.listening_peer_address = None + self.pending_peer_address = None + self._config_ssl_ctx(verify_mode) + self.cb_keepalive = SSL_CTX_set_cookie_cb( + self.ctx.value, + _CallbackProxy(self._generate_cookie_cb), + _CallbackProxy(self._verify_cookie_cb)) + self.ssl = _SSL(SSL_new(self.ctx.value)) + SSL_set_accept_state(self.ssl.value) + + def _init_client(self): + if self.sock.type != socket.SOCK_DGRAM: + raise InvalidSocketError("sock must be of type SOCK_DGRAM") + + self.wbio = _BIO(BIO_new_dgram(self.sock.fileno(), BIO_NOCLOSE)) + self.rbio = self.wbio + self.ctx = _CTX(SSL_CTX_new(DTLSv1_client_method())) + if self.cert_reqs == CERT_NONE: + verify_mode = SSL_VERIFY_NONE + else: + verify_mode = SSL_VERIFY_PEER + self._config_ssl_ctx(verify_mode) + self.ssl = _SSL(SSL_new(self.ctx.value)) + SSL_set_connect_state(self.ssl.value) + + def _config_ssl_ctx(self, verify_mode): + SSL_CTX_set_verify(self.ctx.value, verify_mode) + SSL_CTX_set_read_ahead(self.ctx.value, 1) + if self.certfile: + SSL_CTX_use_certificate_chain_file(self.ctx.value, self.certfile) + if self.keyfile: + SSL_CTX_use_PrivateKey_file(self.ctx.value, self.keyfile, + SSL_FILE_TYPE_PEM) + if self.ca_certs: + SSL_CTX_load_verify_locations(self.ctx.value, self.ca_certs, None) + if self.ciphers: + SSL_CTX_set_cipher_list(self.ctx.value, self.ciphers) + + def _copy_server(self): + source = self.sock + self.sock = source.sock + self.udp_demux = source.udp_demux + self.rsock = self.udp_demux.get_connection(source.pending_peer_address) + self.wbio = _BIO(BIO_new_dgram(self.sock.fileno(), BIO_NOCLOSE)) + self.rbio = _BIO(BIO_new_dgram(self.rsock.fileno(), BIO_NOCLOSE)) + BIO_dgram_set_peer(self.wbio.value, source.pending_peer_address) + self.ctx = source.ctx + self.ssl = source.ssl + new_source_wbio = _BIO(BIO_new_dgram(source.sock.fileno(), + BIO_NOCLOSE)) + new_source_rbio = _BIO(BIO_new_dgram(source.rsock.fileno(), + BIO_NOCLOSE)) + source.ssl = _SSL(SSL_new(self.ctx.value)) + source.rbio = new_source_rbio + source.wbio = new_source_wbio + SSL_set_bio(source.ssl.value, + new_source_rbio.value, + new_source_wbio.value) + new_source_rbio.disown() + new_source_wbio.disown() + + def _check_nbio(self): + BIO_set_nbio(self.wbio.value, self.sock.gettimeout() is not None) + if self.wbio is not self.rbio: + BIO_set_nbio(self.rbio.value, self.rsock.gettimeout() is not None) + + def _get_cookie(self, ssl): + assert self.listening + assert self.ssl.value._as_parameter == ssl._as_parameter + if self.listening_peer_address: + peer_address = self.listening_peer_address + else: + peer_address = BIO_dgram_get_peer(self.rbio.value) + cookie_hmac = hmac.new(self._rnd_key, str(peer_address)) + return cookie_hmac.digest() + + def _generate_cookie_cb(self, ssl): + return self._get_cookie(ssl) + + def _verify_cookie_cb(self, ssl, cookie): + if self._get_cookie(ssl) != cookie: + raise Exception("DTLS cookie mismatch") + + def __init__(self, sock, keyfile=None, certfile=None, + server_side=False, cert_reqs=CERT_NONE, + ssl_version=PROTOCOL_DTLSv1, ca_certs=None, + do_handshake_on_connect=True, + suppress_ragged_eofs=True, ciphers=None): + """Constructor + + Arguments: + these arguments match the ones of the SSLSocket class in the + standard library's ssl module + """ + + if keyfile and not certfile or certfile and not keyfile: + raise_ssl_error(ERR_BOTH_KEY_CERT_FILES) + if server_side and not keyfile: + raise_ssl_error(ERR_BOTH_KEY_CERT_FILES_SVR) + if cert_reqs != CERT_NONE and not ca_certs: + raise_ssl_error(ERR_NO_CERTS) + + if not ciphers: + ciphers = "DEFAULT" + + self.sock = sock + self.keyfile = keyfile + self.certfile = certfile + self.cert_reqs = cert_reqs + self.ca_certs = ca_certs + self.do_handshake_on_connect = do_handshake_on_connect + self.suppress_ragged_eofs = suppress_ragged_eofs + self.ciphers = ciphers + + if isinstance(sock, SSLConnection): + self._copy_server() + elif server_side: + self._init_server() + else: + self._init_client() + + SSL_set_bio(self.ssl.value, self.rbio.value, self.wbio.value) + self.rbio.disown() + self.wbio.disown() + + def listen(self): + """Server-side cookie exchange + + This method reads datagrams from the socket and initiates cookie + exchange, upon whose successful conclusion one can then proceed to + the accept method. Alternatively, accept can be called directly, in + which case it will call this method. In order to prevent denial-of- + service attacks, only a small, constant set of computing resources + are used during the listen phase. + + On some platforms, listen must be called so that packets will be + forwarded to accepted connections. Doing so is therefore recommened + in all cases for portable code. + + Return value: a peer address if a datagram from a new peer was + encountered, None if a datagram for a known peer was forwarded + """ + + self.pending_peer_address = None + try: + peer_address = self.udp_demux.service() + except socket.timeout: + peer_address = None + except socket.error as sock_err: + if sock_err.errno != errno.EWOULDBLOCK: + _logger.exception("Unexpected socket error in listen") + raise + peer_address = None + + if not peer_address: + _logger.debug("Listen returning without peer") + return + + # The demux advises that a datagram from a new peer may have arrived + if type(peer_address) is tuple: + # For this type of demux, the write BIO must be pointed at the peer + BIO_dgram_set_peer(self.wbio.value, peer_address) + self.udp_demux.forward() + self.listening_peer_address = peer_address + + self._check_nbio() + self.listening = True + try: + _logger.debug("Invoking DTLSv1_listen for ssl: %d", + self.ssl.value._as_parameter) + dtls_peer_address = DTLSv1_listen(self.ssl.value) + except OpenSSLError as err: + if err.ssl_error == SSL_ERROR_WANT_READ: + # This method must be called again to forward the next datagram + _logger.debug("DTLSv1_listen must be resumed") + return + elif err.errqueue and err.errqueue[0][0] == ERR_COOKIE_MISMATCH: + _logger.debug("Mismatching cookie received; aborting handshake") + return + _logger.exception("Unexpected error in DTLSv1_listen") + raise + finally: + self.listening = False + self.listening_peer_address = None + if type(peer_address) is tuple: + _logger.debug("New local peer: %s", dtls_peer_address) + self.pending_peer_address = peer_address + else: + self.pending_peer_address = dtls_peer_address + _logger.debug("New peer: %s", self.pending_peer_address) + return self.pending_peer_address + + def accept(self): + """Server-side UDP connection establishment + + This method returns a server-side SSLConnection object, connected to + that peer most recently returned from the listen method and not yet + connected. If there is no such peer, then the listen method is invoked. + + Return value: SSLConnection connected to a new peer, None if packet + forwarding only to an existing peer occurred. + """ + + if not self.pending_peer_address: + if not self.listen(): + _logger.debug("Accept returning without connection") + return + new_conn = SSLConnection(self, self.keyfile, self.certfile, True, + self.cert_reqs, PROTOCOL_DTLSv1, + self.ca_certs, self.do_handshake_on_connect, + self.suppress_ragged_eofs, self.ciphers) + self.pending_peer_address = None + if self.do_handshake_on_connect: + # Note that since that connection's socket was just created in its + # constructor, the following operation must be blocking; hence + # handshake-on-connect can only be used with a routing demux if + # listen is serviced by a separate application thread, or else we + # will hang in this call + new_conn.do_handshake() + _logger.debug("Accept returning new connection for new peer") + return new_conn + + def connect(self, peer_address): + """Client-side UDP connection establishment + + This method connects this object's underlying socket. It subsequently + performs a handshake if do_handshake_on_connect was set during + initialization. + + Arguments: + peer_address - address tuple of server peer + """ + + self.sock.connect(peer_address) + BIO_dgram_set_connected(self.wbio.value, peer_address) + assert self.wbio is self.rbio + if self.do_handshake_on_connect: + self.do_handshake() + + def do_handshake(self): + """Perform a handshake with the peer + + This method forces an explicit handshake to be performed with either + the client or server peer. + """ + + _logger.debug("Initiating handshake...") + self._check_nbio() + SSL_do_handshake(self.ssl.value) + _logger.debug("...completed handshake") + + def read(self, len=1024): + """Read data from connection + + Read up to len bytes and return them. + Arguments: + len -- maximum number of bytes to read + + Return value: + string containing read bytes + """ + + self._check_nbio() + return SSL_read(self.ssl.value, len) + + def write(self, data): + """Write data to connection + + Write data as string of bytes. + + Arguments: + data -- buffer containing data to be written + + Return value: + number of bytes actually transmitted + """ + + self._check_nbio() + return SSL_write(self.ssl.value, data) + + def shutdown(self): + """Shut down the DTLS connection + + This method attemps to complete a bidirectional shutdown between + peers. For non-blocking sockets, it should be called repeatedly until + it no longer raises continuation request exceptions. + """ + + self._check_nbio() + try: + SSL_shutdown(self.ssl.value) + except OpenSSLError as err: + if err.result == 0: + # close-notify alert was just sent; wait for same from peer + # Note: while it might seem wise to suppress further read-aheads + # with SSL_set_read_ahead here, doing so causes a shutdown + # failure (ret: -1, SSL_ERROR_SYSCALL) on the DTLS shutdown + # initiator side. + SSL_shutdown(self.ssl.value) + else: + raise diff --git a/dtls/ssleay32.dll b/dtls/ssleay32.dll new file mode 100644 index 0000000..a61548e Binary files /dev/null and b/dtls/ssleay32.dll differ diff --git a/dtls/test/__init__.py b/dtls/test/__init__.py new file mode 100644 index 0000000..e21ffef --- /dev/null +++ b/dtls/test/__init__.py @@ -0,0 +1,5 @@ +# Test: unit tests for PyDTLS. Written by Ray Brown. +"""PyDTLs unit tests + +This package contains unit tests and other test scripts and resources. +""" diff --git a/dtls/test/certs/ca-cert.pem b/dtls/test/certs/ca-cert.pem new file mode 100644 index 0000000..c51b49b --- /dev/null +++ b/dtls/test/certs/ca-cert.pem @@ -0,0 +1,13 @@ +-----BEGIN CERTIFICATE----- +MIIB3TCCAYegAwIBAgIJAJdD48tCuQ4ZMA0GCSqGSIb3DQEBBQUAMEoxCzAJBgNV +BAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRMwEQYDVQQKEwpSYXkgQ0EgSW5j +MREwDwYDVQQDEwhSYXlDQUluYzAeFw0xMjA5MjEyMTE0MTZaFw0xMzA5MjEyMTE0 +MTZaMEoxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRMwEQYDVQQK +EwpSYXkgQ0EgSW5jMREwDwYDVQQDEwhSYXlDQUluYzBcMA0GCSqGSIb3DQEBAQUA +A0sAMEgCQQC33ThS1uvx6c9/jdQgPrLnVepv9NJdtyRMIDH3ZVfIKwwC6Nde3CJh +bdo3j2njxlY7pw0P6J/F6mQpGtsRGaX1AgMBAAGjUDBOMB0GA1UdDgQWBBQBj0cB +lkz531jiz4oLP0osGlVR3zAfBgNVHSMEGDAWgBQBj0cBlkz531jiz4oLP0osGlVR +3zAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBBQUAA0EAUyS5rT6LFjhhPeoW1Gk1 +sibwzgPSKdEzllt0vGZtWESekkoJ0UxnDvRzKv8OEVSclt+2YuzJXuZGteFABxDA +Cw== +-----END CERTIFICATE----- diff --git a/dtls/test/certs/server-cert.pem b/dtls/test/certs/server-cert.pem new file mode 100644 index 0000000..e93f97b --- /dev/null +++ b/dtls/test/certs/server-cert.pem @@ -0,0 +1,36 @@ +Certificate: + Data: + Version: 1 (0x0) + Serial Number: 1 (0x1) + Signature Algorithm: md5WithRSAEncryption + Issuer: C=US, ST=Washington, O=Ray CA Inc, CN=RayCAInc + Validity + Not Before: Sep 21 21:16:18 2012 GMT + Not After : Sep 21 21:16:18 2013 GMT + Subject: C=US, ST=Washington, O=Ray Srv Inc, CN=RaySrvInc + Subject Public Key Info: + Public Key Algorithm: rsaEncryption + Public-Key: (512 bit) + Modulus: + 00:b8:f7:77:26:6c:9d:25:f5:e1:ca:28:b4:6a:0b: + 15:81:13:0e:51:c3:b2:ba:57:5b:69:ff:cb:bb:86: + d9:f9:4d:33:1f:43:92:a1:89:2f:d0:08:5b:cf:b7: + a1:2b:ea:06:31:e5:32:fd:2b:86:54:09:fb:24:bc: + a3:ce:56:22:f1 + Exponent: 65537 (0x10001) + Signature Algorithm: md5WithRSAEncryption + 12:4c:55:17:c1:c4:19:5f:0d:e2:66:00:84:37:22:62:9b:6d: + 9e:76:fd:a5:82:f5:a2:41:c6:ae:16:88:16:ab:bd:0f:5d:0b: + 7c:fd:52:2a:9b:3f:ea:cf:ea:35:29:37:61:a2:15:8f:27:56: + 38:71:1f:eb:1a:f8:cc:99:22:17 +-----BEGIN CERTIFICATE----- +MIIBgDCCASoCAQEwDQYJKoZIhvcNAQEEBQAwSjELMAkGA1UEBhMCVVMxEzARBgNV +BAgTCldhc2hpbmd0b24xEzARBgNVBAoTClJheSBDQSBJbmMxETAPBgNVBAMTCFJh +eUNBSW5jMB4XDTEyMDkyMTIxMTYxOFoXDTEzMDkyMTIxMTYxOFowTDELMAkGA1UE +BhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24xFDASBgNVBAoTC1JheSBTcnYgSW5j +MRIwEAYDVQQDEwlSYXlTcnZJbmMwXDANBgkqhkiG9w0BAQEFAANLADBIAkEAuPd3 +JmydJfXhyii0agsVgRMOUcOyuldbaf/Lu4bZ+U0zH0OSoYkv0Ahbz7ehK+oGMeUy +/SuGVAn7JLyjzlYi8QIDAQABMA0GCSqGSIb3DQEBBAUAA0EAEkxVF8HEGV8N4mYA +hDciYpttnnb9pYL1okHGrhaIFqu9D10LfP1SKps/6s/qNSk3YaIVjydWOHEf6xr4 +zJkiFw== +-----END CERTIFICATE----- diff --git a/dtls/test/certs/server-key.pem b/dtls/test/certs/server-key.pem new file mode 100644 index 0000000..51908d6 --- /dev/null +++ b/dtls/test/certs/server-key.pem @@ -0,0 +1,10 @@ +-----BEGIN PRIVATE KEY----- +MIIBVAIBADANBgkqhkiG9w0BAQEFAASCAT4wggE6AgEAAkEAuPd3JmydJfXhyii0 +agsVgRMOUcOyuldbaf/Lu4bZ+U0zH0OSoYkv0Ahbz7ehK+oGMeUy/SuGVAn7JLyj +zlYi8QIDAQABAkAygtnV82lC2Y/Mbis+nkJEGlkZuRCQ1JRRMRqI3n2eF6CviqF3 +PiBXIEEExzKihC9bvbHKTAkYDLr+/4YpbiQBAiEA7JLS5Lp7KI/ayWwEzl2r5XXu +k/cbH++A4zZz6A9XIsECIQDIJ8ciDa5/VGyQnYMzBNgKnwaFDDBOiEUFDaU/9ZN8 +MQIgCG3Gw819G9ncQrbtiOi/eiJ0iKMSPVYMMow7HvaE9UECIQCLyQwPwlJd5s4z +aW4ZkYZ4VHuvK8YI8q6RSuhf9Nhd4QIgFbRNdEeehgrzGzGug2yVCMzVzS3MQNBJ +6LqBZaPlFsM= +-----END PRIVATE KEY----- diff --git a/dtls/test/echo_seq.py b/dtls/test/echo_seq.py new file mode 100644 index 0000000..c93b78f --- /dev/null +++ b/dtls/test/echo_seq.py @@ -0,0 +1,102 @@ +# PyDTLS sequential echo. Written by Ray Brown. +"""PyDTLS sequential echo + +This script runs a sequential echo server. It is sequential in that it will +respond without error only to a single sclient that invokes the following steps +in order: + * DTLS cookie exchange on port 28000 of localhost + * DTLS handshake (application-default ciphers) + * Write and receive echo back for an arbitrary number of datagrams + * Isue shutdown notification and receive the shutdown notification response + +Note that this script's operation is slow and inefficient on purpose: it +invokes the demux without socket select, but with 5-second timeouts after +the cookie exchange; this is done so that one can follow the debug logs when +operating this server from a client shell interactively. +""" + +import socket +from os import path +from logging import basicConfig, DEBUG +basicConfig(level=DEBUG) # set now for dtls import code +from dtls.sslconnection import SSLConnection +from dtls.err import SSLError, SSL_ERROR_WANT_READ, SSL_ERROR_ZERO_RETURN + +def main(): + sck = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + sck.bind(("127.0.0.1", 28000)) + sck.settimeout(30) + cert_path = path.join(path.abspath(path.dirname(__file__)), "certs") + scn = SSLConnection( + sck, + keyfile=path.join(cert_path, "server-key.pem"), + certfile=path.join(cert_path, "server-cert.pem"), + server_side=True, + ca_certs=path.join(cert_path, "ca-cert.pem"), + do_handshake_on_connect=False) + cnt = 0 + + while True: + cnt += 1 + print "Listen invocation: %d" % cnt + peer_address = scn.listen() + if peer_address: + print "Completed listening for peer: %s" % str(peer_address) + break + + print "Accepting..." + conn = scn.accept() + sck.settimeout(5) + conn.rsock.settimeout(5) + + cnt = 0 + while True: + cnt += 1 + print "Listen invocation: %d" % cnt + peer_address = scn.listen() + assert not peer_address + print "Handshake invocation: %d" % cnt + try: + conn.do_handshake() + except SSLError as err: + if err.args[0] == SSL_ERROR_WANT_READ: + continue + raise + print "Completed handshaking with peer" + break + + cnt = 0 + while True: + cnt += 1 + print "Listen invocation: %d" % cnt + peer_address = scn.listen() + assert not peer_address + print "Read invocation: %d" % cnt + try: + message = conn.read() + except SSLError as err: + if err.args[0] == SSL_ERROR_WANT_READ: + continue + if err.args[0] == SSL_ERROR_ZERO_RETURN: + break + raise + print message + conn.write("Back to you: " + message) + + cnt = 0 + while True: + cnt += 1 + print "Listen invocation: %d" % cnt + peer_address = scn.listen() + assert not peer_address + print "Shutdown invocation: %d" % cnt + try: + conn.shutdown() + except SSLError as err: + if err.args[0] == SSL_ERROR_WANT_READ: + continue + raise + break + +if __name__ == "__main__": + main() diff --git a/dtls/test/rl.py b/dtls/test/rl.py new file mode 100644 index 0000000..05e5ff6 --- /dev/null +++ b/dtls/test/rl.py @@ -0,0 +1,26 @@ +# PyDTLS reloader. Written by Ray Brown. +"""PyDTLS package reloader + +This script reloads all modules of the DTLS package. This can be useful in +runtime environments that usually persist across package file edits, such as +the IPython shell. +""" + +import dtls +import dtls.err +import dtls.sslconnection +import dtls.openssl +import dtls.demux +import dtls.demux.router + +def main(): + reload(dtls) + reload(dtls.err) + reload(dtls.sslconnection) + reload(dtls.openssl) + reload(dtls.demux) + reload(dtls.demux.router) + reload(dtls.sslconnection) + +if __name__ == "__main__": + main()