Initial commit: up to and including data exchange functionality
This initial commit for the PyDTLS package includes the following functionality: * DTLS cookie exchange, using secure hmac cookies * A platform-independent routing UDP demultiplexer * SSL handshaking over UDP using the DTLS protocol * Datagram exchange using the DTLS protocol * SSL shutdown over UDP The package is structured as follows: * dtls: top-level package * dtls.demux: demultiplexer package; automatically loads a demultiplexer appropriate for the currently executing platform * dtls.demux.router: a routing demux for platforms whose network stacks cannot assign incoming UDP packets to sockets based on the sockets' connection information * dtls.demux.osnet: a demux that uses the operating system's UDP packet routing functionality * dtls.err: package-wide error handling and error definitions * dtls.sslconnection: a client and server-side connection class for UDP network connections secured with the DTLS protocol * dtls.openssl: a ctypes-based wrapper for the OpenSSL library * dtls.test: test scripts, utilities, and unit tests The following binaries are provided: * libeay32.dll: cryptographic portion of the OpenSSL library * ssleay32.dll: protocol portion of the OpenSSL library (depends on former) * cygcrypto-1.0.0.dll: as libeay32.dll, but with debugging symbols * cygssl-1.0.0.dll: as ssleay32.dll, but with debugging symbols All binaries have been built with the MinGW tool chain, targeted for msvcr90. The unstripped dll's can be debugged on Windows with gdb. Cygwin is not used.incoming
parent
91323b475b
commit
9bb24c5d29
|
@ -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.
|
||||
"""
|
Binary file not shown.
Binary file not shown.
|
@ -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"]
|
|
@ -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)
|
|
@ -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
|
|
@ -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"
|
||||
}
|
Binary file not shown.
|
@ -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))
|
|
@ -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
|
Binary file not shown.
|
@ -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.
|
||||
"""
|
|
@ -0,0 +1,13 @@
|
|||
-----BEGIN CERTIFICATE-----
|
||||
MIIB3TCCAYegAwIBAgIJAJdD48tCuQ4ZMA0GCSqGSIb3DQEBBQUAMEoxCzAJBgNV
|
||||
BAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRMwEQYDVQQKEwpSYXkgQ0EgSW5j
|
||||
MREwDwYDVQQDEwhSYXlDQUluYzAeFw0xMjA5MjEyMTE0MTZaFw0xMzA5MjEyMTE0
|
||||
MTZaMEoxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRMwEQYDVQQK
|
||||
EwpSYXkgQ0EgSW5jMREwDwYDVQQDEwhSYXlDQUluYzBcMA0GCSqGSIb3DQEBAQUA
|
||||
A0sAMEgCQQC33ThS1uvx6c9/jdQgPrLnVepv9NJdtyRMIDH3ZVfIKwwC6Nde3CJh
|
||||
bdo3j2njxlY7pw0P6J/F6mQpGtsRGaX1AgMBAAGjUDBOMB0GA1UdDgQWBBQBj0cB
|
||||
lkz531jiz4oLP0osGlVR3zAfBgNVHSMEGDAWgBQBj0cBlkz531jiz4oLP0osGlVR
|
||||
3zAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBBQUAA0EAUyS5rT6LFjhhPeoW1Gk1
|
||||
sibwzgPSKdEzllt0vGZtWESekkoJ0UxnDvRzKv8OEVSclt+2YuzJXuZGteFABxDA
|
||||
Cw==
|
||||
-----END CERTIFICATE-----
|
|
@ -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-----
|
|
@ -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-----
|
|
@ -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()
|
|
@ -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()
|
Loading…
Reference in New Issue