diff --git a/ChangeLog b/ChangeLog index 373f614..2b3fa50 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,3 +1,203 @@ +2017-04-03 Ray Brown + + Release 1.2.0 + + * README.md: Release updates + +2017-04-02 Ray Brown + + Release 1.2.0 Preparation + + * README.txt -> README.md: renamed + * dtls/sslconnection.py: Reduce the default MTU in effect while handshaking to 576, suitable for various path MTUs and PPPoE + * dtls/prebuilt/win32-x86[_64]: Rebuilt with Visual C++ 2008 to eliminate requirement to install a C++ redistributable package + * dtls/prebuilt/mingw-x86: mingw support is deprecated + * dtls/__init__.py: VERSION introduced + * setup.py: Version incremented to 1.2.0 + +2017-03-28 Björn Freise + + Workaround for Windows concerning the MTU size + + * dtls/sslconnection.py: Hardcoded setting of the MTU size only for Windows and in case it is not already configured + * dtls/test/unit_wrapper.py: No user config of the MTU size; using the hardcoded one from SSLConnection + +2017-03-28 Björn Freise + + Minor fixes and "hopefully" compatible to Ubuntu 16.04 + + * dtls/__init__.py: Removed wrapper import + * dtls/openssl.py: Fixed line endings to LF + * dtls/patch.py: Removed PROTOCOL_SSLv3 import and fixed line endings to LF + * dtls/sslconnection.py: Fixed line endings to LF + * dtls/test/certs/*_ec.pem: Fixed line endings to LF + * dtls/test/echo_seq.py: Fixed line endings to LF + * dtls/test/simple_client.py: Fixed line endings to LF + * dtls/test/unit.py: Fixed line endings to LF + * dtls/test/unit_wrapper.py: Corrected wrapper import and fixed line endings to LF + * dtls/util.py: Fixed line endings to LF + * dtls/wrapper.py: Corrected function naming to wrap_client() and wrap_server(); Fixed line endings to LF + * dtls/x509.py: Fixed line endings to LF + +2017-03-23 Björn Freise + + Patched ssl-Module with SSL_BUILD_*- and ERR_*- constants and added aliases for wrap_server() and wrap_client() + + * dtls/__init__.py: Added DtlsSocket() from wrapper and aliases for wrap_server() and wrap_client() + * dtls/err.py: Added patch_ssl_errors() to patch ssl-Module with ERR_* constants + * dtls/patch.py: Patched ssl-Module with SSL_BUILD_* constants and added call to patch_ssl_errors() + * dtls/wrapper.py: + - Added a server and client function to alias/wrap DtlsSocket() creation + - Cleanup of DtlsSocket.__init__() + - Cleanup of exception handling in all member methods + - Cleanup sendto() from client: no endless loop and first do a connect if not already connected + * dtls/test/unit_wrapper.py: Adopt the changes made described above + +2017-03-17 Björn Freise + + Added a wrapper for a DTLS-Socket either as client or server - including unit tests + + * dtls/__init__.py: Import SSLContext() and SSL() for external use + * dtls/wrapper.py: Added class DtlsSocket() to be used as client or server + * dtls/test/unit_wrapper.py: unit test for DtlsSocket() + +2017-03-17 Björn Freise + + Added more on error evaluation and a method to get the peer certificate chain + + * dtls/__init__.py: import error codes from err.py as error_codes for external access + * dtls/err.py: Added errors for ERR_WRONG_SSL_VERSION, ERR_CERTIFICATE_VERIFY_FAILED, ERR_NO_SHARED_CIPHER and ERR_SSL_HANDSHAKE_FAILURE + * dtls/openssl.py: + - Added constant SSL_BUILD_CHAIN_FLAG_NONE for SSL_CTX_build_cert_chain() + - Added method SSL_get_peer_cert_chain() + * dtls/patch.py: Added getpeercertchain() as method to ssl.SSLSocket() + * dtls/sslconnection.py: + - Bugfix SSLContext.set_ecdh_curve() returns 1 for success and 0 for failure + - SSLContext.build_cert_chain() changed default flags to SSL_BUILD_CHAIN_FLAG_NONE + - In SSLConnection() the mtu size gets only set if no user config function is given + - SSLConnection.listen() raises an exception for ERR_WRONG_VERSION_NUMBER, ERR_COOKIE_MISMATCH, ERR_NO_SHARED_CIPHER and all other unknown errors + - SSLConnection.read() and write() now can also raise ERR_PORT_UNREACHABLE + - If SSLConnection.write() successfully writes bytes to the peer, then the handshake is assumed to be okay + - Added method SSLConnection.getpeercertchain() + * dtls/test/unit.py: ThreadedEchoServer() with an extra exception branch for the newly raised exceptions in SSLConnection.listen() + +2017-03-17 Björn Freise + + Added certificate creation using ECDSA + + * dtls/test/makecerts_ec.bat: creates ca-cert_ec.pem, keycert_ec.pem and server-cert_ec.pem + * dtls/test/openssl_ca.cnf and openssl_server.cnf: Added HOME to be able to use the conf file under windows + +2017-03-17 Björn Freise + + Added an interface in SSLConnection() to access SSLContext() and SSL() for manipulating settings during creation + + * dtls/openssl.py: + - Added utility functions EC_curve_nist2nid() and EC_curve_nid2nist() + * dtls/patch.py: + - Extended wrap_socket() arguments with callbacks for user config functions of ssl context and ssl session values + - Extended SSLSocket() arguments with callbacks for user config functions of ssl context and ssl session values + * dtls/sslconnection.py: + - Extended SSLConnection() arguments with callbacks for user config functions of ssl context and ssl session values + - During the init of client and server the corresponding user config functions are called (if given) + - Added new classes SSLContext() [set_ciphers(), set_sigalgs(), set_curves(), set_ecdh_curve(), build_cert_chain(), + set_ssl_logging()] and SSL() [set_mtu(), set_link_mtu()] + +2017-03-17 Björn Freise + + Added methods getting the curves supported by the runtime openSSL lib + + * dtls/openssl.py: + - Added class _EllipticCurve() for easy handling of the builtin curves + - Added wrapper get_elliptic_curves() - which uses _EllipticCurve() + - Added EC_get_builtin_curves(), EC_KEY_new_by_curve_name() and EC_KEY_free() + - Added OBJ_nid2sn() for translating numeric ids to names + * dtls/util.py: Added _EC_KEY() derived from _Rsrc() with own free/del method + +2017-03-17 Björn Freise + + Added methods for setting and getting the curves used during negotiation and encryption + + * dtls/openssl.py: + - Added SSL_CTX_set1_curves() and SSL_CTX_set1_curves_list() + - Added SSL_CTX_set_ecdh_auto() and SSL_CTX_set_tmp_ecdh() + - Added SSL_get1_curves(), SSL_get_shared_curve(), SSL_set1_curves() and SSL_set1_curves_list() + +2017-03-17 Björn Freise + + Added methods for setting the signature algorithms + + * dtls/openssl.py: + - Added SSL_CTX_set1_client_sigalgs_list(), SSL_CTX_set1_client_sigalgs(), SSL_CTX_set1_sigalgs_list() and SSL_CTX_set1_sigalgs() + - Added SSL_set1_client_sigalgs_list(), SSL_set1_client_sigalgs(), SSL_set1_sigalgs_list() and SSL_set1_sigalgs() + +2017-03-17 Björn Freise + + Added method SSL_CTX_build_cert_chain() + + * dtls/openssl.py: Added SSL_CTX_build_cert_chain() and corresponding constants + +2017-03-17 Björn Freise + + Added methods *_clear_options() and *_get_options() + + * dtls/openssl.py: + - Added SSL_CTX_clear_options() and SSL_CTX_get_options() + - Added SSL_clear_options() and SSL_get_options() + +2017-03-17 Björn Freise + + Added new methods for DTLSv1.2 + + * dtls/err.py: Added error code ERR_WRONG_VERSION_NUMBER + * dtls/openssl.py: Added DTLS_server_method(), DTLSv1_2_server_method() and DTLSv1_2_client_method() + * dtls/patch.py: Default protocol DTLS for ssl.wrap_socket() and ssl.SSLSocket() + * dtls/sslconnection.py: + - Introduced PROTOCOL_DTLSv1_2 and PROTOCOL_DTLS (the latter one is a synonym for the "higher" version) + - Updated _init_client() and _init_server() with the new protocol methods + - Default protocol DTLS for SSLConnection() + - Return on ERR_WRONG_VERSION_NUMBER if client and server cannot agree on protocol version + * dtls/test/unit.py: + - Extended test_get_server_certificate() to iterate over the different protocol combinations + - Extended test_protocol_dtlsv1() to try the different protocol combinations between client and server + +2017-03-17 Björn Freise + + Updating openSSL libs to v1.0.2l-dev + + * dtls/openssl.py: Added mtu-functions SSL_set_mtu() and DTLS_set_link_mtu() + * dtls/prebuilt/win32-*: Updated libs for x86 and x86_64 to version 1.0.2l-dev + * dtls/sslconnection.py: mtu size set hardcoded to 1500 - otherwise the windows implementation has problems + +2017-03-17 Björn Freise + + Added interface for SSL_CTX_set_info_callback() + + * dtls/openssl.py: + - Added methods SSL_CTX_set_info_callback(), SSL_state_string_long(), SSL_alert_type_string_long() and SSL_alert_desc_string_long() + - Added constants for state and error evaluation during callback + * dtls/sslconnection.py: Added _ssl_logging_cb() as default callback function - only outputs messages when logger is active + +2017-03-17 Björn Freise + + SSL_write() extended to handle ctypes.Array as data + + * dtls/openssl.py: SSL_write() can handle ctypes.Array data + * dtls/sslconnection.py: Added missing import ERR_BOTH_KEY_CERT_FILES + * dtls/test/simple_client.py: Added basic test client to use with dtls/test/echo_seq.py + +2017-03-17 Björn Freise + + Beautified lists and maps, grouped imports for easy merges in the future - no changed functionality! + + * dtls/openssl.py: + - Ordered constants according to header file from openSSL + - Beautified __all__-list and map for _make_function() in order to easy merges in the future + - Added a few returns in order to evaluate the success of the called methods + * dtls/patch.py: Grouped imports in the following order - system, local + * dtls/sslconnection.py: ssl protocol not hardcoded anymore for forked objects + * dtls/x509.py: logger messages working again + 2017-02-27 Ray Brown * dtls/openssl.py: support reading directly into given buffer instead of forcing buffer copy (for ssl module compatibility) diff --git a/README.txt b/README.md similarity index 80% rename from README.txt rename to README.md index bb4316e..0942bee 100644 --- a/README.txt +++ b/README.md @@ -1,6 +1,4 @@ -============================================ -Datagram Transport Layer Security for Python -============================================ +# Datagram Transport Layer Security for Python PyDTLS brings Datagram Transport Layer Security (DTLS - RFC 6347: http://tools.ietf.org/html/rfc6347) to the Python environment. In a @@ -13,47 +11,51 @@ DTLS is now very easy to use in Python. If you're familiar with the ssl module in Python's standard library, you already know how. All it takes is passing a datagram/UDP socket to the *wrap_socket* function instead of a stream/TCP socket. Here's how one sets up the client side -of a connection:: +of a connection: - import ssl - from socket import socket, AF_INET, SOCK_DGRAM - from dtls import do_patch - do_patch() - sock = ssl.wrap_socket(socket(AF_INET, SOCK_DGRAM)) - sock.connect(('foo.bar.com', 1234)) - sock.send('Hi there') +``` +import ssl +from socket import socket, AF_INET, SOCK_DGRAM +from dtls import do_patch +do_patch() +sock = ssl.wrap_socket(socket(AF_INET, SOCK_DGRAM)) +sock.connect(('foo.bar.com', 1234)) +sock.send('Hi there') +``` -Design Goals -============ +As of version 1.2.0, PyDTLS supports DTLS version 1.2 in addition to +version 1.0. This version also introduces forward secrecy using +elliptic curve cryptography and more fine-grained configuration options. + +## Design Goals The primary design goal of PyDTLS is broad availability. It has therefore been built to be widely compatible with the following: - * Operating systems: apart from the Python standard library, PyDTLS - relies on the OpenSSL library only. OpenSSL is widely ported, and - in fact the Python standard library's *ssl* module also uses it. - * Python runtime environments: PyDTLS is a package consisting of - pure Python modules only. It should therefore be portable to many - interpreters and runtime environments. It interfaces with OpenSSL - strictly through the standard library's *ctypes* foreign function - library. - * The Python standard library: the standard library's *ssl* module is - Python's de facto interface to SSL/TLS. PyDTLS aims to be compatible - with the full public interface presented by this module. The ssl - module ought to behave identically with respect to all of its - functions and options when used in conjunction with PyDTLS and - datagram sockets as when used without PyDTLS and stream sockets. - * Connection-based protocols: as outlined below, layering security - on top of datagram sockets requires introducing certain - connection constructs normally absent from datagram - sockets. These constructs have been added in such a way as to be - compatible with code that expects to interoperate with - connection-oriented stream sockets. For example, code that - expects to go through server-side bind/listen/accept connection - establishment should be reusable with PyDTLS sockets. + * Operating systems: apart from the Python standard library, PyDTLS + relies on the OpenSSL library only. OpenSSL is widely ported, and + in fact the Python standard library's *ssl* module also uses it. + * Python runtime environments: PyDTLS is a package consisting of + pure Python modules only. It should therefore be portable to many + interpreters and runtime environments. It interfaces with OpenSSL + strictly through the standard library's *ctypes* foreign function + library. + * The Python standard library: the standard library's *ssl* module is + Python's de facto interface to SSL/TLS. PyDTLS aims to be compatible + with the full public interface presented by this module. The ssl + module ought to behave identically with respect to all of its + functions and options when used in conjunction with PyDTLS and + datagram sockets as when used without PyDTLS and stream sockets. + * Connection-based protocols: as outlined below, layering security + on top of datagram sockets requires introducing certain + connection constructs normally absent from datagram + sockets. These constructs have been added in such a way as to be + compatible with code that expects to interoperate with + connection-oriented stream sockets. For example, code that + expects to go through server-side bind/listen/accept connection + establishment should be reusable with PyDTLS sockets. -Distributions -============= +## Distributions PyDTLS requires version 1.0.0 or higher of the OpenSSL library. Earlier versions are reported not to offer stable DTLS @@ -67,14 +69,15 @@ no further installation steps. In comparison, installation of OpenSSL on Microsoft Windows operating systems is inconvenient. For this reason, source distributions of PyDTLS are available that include OpenSSL dll's for 32-bit and 64-bit -Windows. For 32-bit Windows, a version built with the MinGW toolchain -is also available. Its archive includes stripped as well as -non-stripped dll's. The latter can be debugged with gdb on Windows. All dll's have been linked with the Visual Studio 2008 version of the Microsoft C runtime library, msvcr90.dll, the version used by CPython 2.7. Installation of Microsoft redistributable runtime packages should therefore not be required on machines with CPython 2.7. The version of OpenSSL distributed with PyDTLS 0.1.0 is 1.0.1c. +The version distributed with PyDTLS 1.2.0 is commit +248cf959672041f38f4d80a4a09ee01d8ab04fe8 (branch OpenSSL_1_0_2-stable, +1.0.2l-dev, containing a desirable fix to DTLSv1_listen not present +in 1.0.2k, the stable version at the time of PyDTLS 1.2.0 release). The OpenSSL version used by PyDTLS can be determined from the values of *sslconnection's* DTLS_OPENSSL_VERSION_NUMBER, @@ -83,8 +86,7 @@ are available through the *ssl* module also if *do_patch* has been called (see below). Note that the OpenSSL version used by PyDTLS may differ from the one used by the *ssl* module. -Interfaces -========== +## Interfaces PyDTLS' top-level package, *dtls*, provides DTLS support through the **SSLConnection** class of its *sslconnection* @@ -111,8 +113,7 @@ exceptions of type **ssl.SSLError** instead of its default remain identical when interfacing with *ssl* across stream and datagram sockets. -Connection Handling -=================== +## Connection Handling The DTLS protocol implies a connection as an association between two network peers where the overall association state is characterized by the @@ -145,12 +146,11 @@ prohibited by *ssl*. *accept* returns peer address information, as expected. Note that when using the *ssl* interface to *dtls*, *listen* must be called before calling *accept*. -Demultiplexing -============== +## Demultiplexing At the network io layer, only datagrams from its connected peer must be passed to a **SSLConnection** or **SSLSocket** object (unless the object -is unconnected on the server-side, in which case in can be in listening +is unconnected on the server-side, in which case it can be in listening mode, the initial server-side socket whose role it is to listen for incoming client connection requests). @@ -203,8 +203,7 @@ socket might now be readable as a result of the forwarded datagram. *accept* must return so that the application can iterate on its asynchronous *select* loop. -Shutdown and Unwrapping -======================= +## Shutdown and Unwrapping PyDTLS implements the SSL/TLS shutdown protocol as it has been adapted for DTLS. **SSLConnection's** *shutdown* and **SSLSocket's** *unwrap* @@ -227,21 +226,19 @@ passing it to *ssl.wrap_socket* or the **SSLConnection** constructor. If *osnet* is used, an actual *socket.socket* instance is returned. -Framework Compatibility -======================= +## Framework Compatibility PyDTLS sockets have been tested under the following usage modes: - * Using blocking sockets and sockets with timeouts in - multi-threaded UDP servers - * Using non-blocking sockets, and in conjunction with the - asynchronous socket handler, asyncore - * Using blocking sockets, and in conjunction with the network - server framework SocketServer - ThreadingTCPServer (this works - because of PyDTLS's emulation of connection-related calls) + * Using blocking sockets and sockets with timeouts in + multi-threaded UDP servers + * Using non-blocking sockets, and in conjunction with the + asynchronous socket handler, asyncore + * Using blocking sockets, and in conjunction with the network + server framework SocketServer - ThreadingTCPServer (this works + because of PyDTLS's emulation of connection-related calls) -Multi-thread Support -==================== +## Multi-thread Support Using multiple threads with OpenSSL requires implementing a locking callback. PyDTLS does implement this, and therefore multi-threaded @@ -267,8 +264,16 @@ platforms where PyDTLS loads the same OpenSSL shared object as *ssl*. On Ubuntu 12.04, for example, this is the case, but on Microsoft Windows it is not. -Testing -======= +## Testing + +A simple echo server is available to be executed from the project root +directory with `python -m dtls.test.echo_seq`. The echo server is +reachable using the code snippet at the top of this document, using port +28000 at "localhost". + +Unit test suites can be executed from the project root directory with +`python -m dtls.test.unit [-v]` and `python -m dtls.test.unit_wrapper` +(for the client and server wrappers) Almost all of the Python standard library's *ssl* unit tests from the module *test_ssl.py* have been ported to *dtls.test.unit.py*. All tests @@ -311,18 +316,19 @@ benefits of DTLS over SSL). Nevertheless, some useful insights can be gained by observing the operation of test_perf.py, including software stack behavior in the presence of some amount of packet loss. -Logging -======= +## Logging The *dtls* package and its sub-packages log various occurrences, primarily events that can aid debugging. Especially *router* emits many messages when the logging level is set to at least *logging.DEBUG*. dtls/test/echo_seq.py activates this logging level during its operation. -Currently Supported Platforms -============================= +## Currently Supported Platforms At the time of initial release, PyDTLS 0.1.0 has been tested on Ubuntu 12.04.1 LTS 32-bit and 64-bit, as well as Microsoft Windows 7 32-bit and 64-bit, using CPython 2.7.3. Patches with additional platform ports are welcome. + +As of release 1.2.0, PyDTLS is tested on Ubuntu 16.04 LTS as well as +Microsoft Windows 10, using CPython 2.7.13. diff --git a/dtls/__init__.py b/dtls/__init__.py index 7912b1e..651737d 100644 --- a/dtls/__init__.py +++ b/dtls/__init__.py @@ -32,6 +32,8 @@ sockets. wrap_socket's parameters and their semantics have been maintained. """ +VERSION = 1, 2, 0 + def _prep_bins(): """ Support for running straight out of a cloned source directory instead @@ -59,5 +61,5 @@ def _prep_bins(): _prep_bins() # prepare before module imports from patch import do_patch -from sslconnection import SSLConnection +from sslconnection import SSLContext, SSL, SSLConnection from demux import force_routing_demux, reset_default_demux diff --git a/dtls/err.py b/dtls/err.py index 212554b..25e8e43 100644 --- a/dtls/err.py +++ b/dtls/err.py @@ -47,8 +47,21 @@ ERR_READ_TIMEOUT = 502 ERR_WRITE_TIMEOUT = 503 ERR_HANDSHAKE_TIMEOUT = 504 ERR_PORT_UNREACHABLE = 505 -ERR_COOKIE_MISMATCH = 0x1408A134 +ERR_WRONG_SSL_VERSION = 0x1409210A +ERR_WRONG_VERSION_NUMBER = 0x1408A10B +ERR_COOKIE_MISMATCH = 0x1408A134 +ERR_CERTIFICATE_VERIFY_FAILED = 0x14090086 +ERR_NO_SHARED_CIPHER = 0x1408A0C1 +ERR_SSL_HANDSHAKE_FAILURE = 0x1410C0E5 +ERR_TLSV1_ALERT_UNKNOWN_CA = 0x14102418 + +def patch_ssl_errors(): + import ssl + errors = [i for i in globals().iteritems() if type(i[1]) == int and str(i[0]).startswith('ERR_')] + for k, v in errors: + if not hasattr(ssl, k): + setattr(ssl, k, v) class SSLError(socket_error): """This exception is raised by modules in the dtls package.""" diff --git a/dtls/openssl.py b/dtls/openssl.py index 4099504..53667a1 100644 --- a/dtls/openssl.py +++ b/dtls/openssl.py @@ -40,7 +40,7 @@ from os import path from datetime import timedelta from err import openssl_error from err import SSL_ERROR_NONE -from util import _BIO +from util import _EC_KEY, _BIO import ctypes from ctypes import CDLL from ctypes import CFUNCTYPE @@ -60,19 +60,10 @@ _logger = getLogger(__name__) # if sys.platform.startswith('win'): dll_path = path.abspath(path.dirname(__file__)) - debug_cryptodll_path = path.join(dll_path, "cygcrypto-1.0.0.dll") - debug_ssldll_path = path.join(dll_path, "cygssl-1.0.0.dll") - release_cryptodll_path = path.join(dll_path, "libeay32.dll") - release_ssldll_path = path.join(dll_path, "ssleay32.dll") - if path.exists(path.join(dll_path, "use_debug_openssl")) and \ - path.exists(debug_cryptodll_path) and \ - path.exists(debug_ssldll_path): - libcrypto = CDLL(debug_cryptodll_path) - libssl = CDLL(debug_ssldll_path) - else: - # If these don't exist, then let the exception propagate - libcrypto = CDLL(release_cryptodll_path) - libssl = CDLL(release_ssldll_path) + cryptodll_path = path.join(dll_path, "libeay32.dll") + ssldll_path = path.join(dll_path, "ssleay32.dll") + libcrypto = CDLL(cryptodll_path) + libssl = CDLL(ssldll_path) else: libcrypto = CDLL("libcrypto.so.1.0.0") libssl = CDLL("libssl.so.1.0.0") @@ -83,6 +74,7 @@ else: BIO_NOCLOSE = 0x00 BIO_CLOSE = 0x01 SSLEAY_VERSION = 0 +SSL_OP_NO_QUERY_MTU = 0x00001000 SSL_OP_NO_COMPRESSION = 0x00020000 SSL_VERIFY_NONE = 0x00 SSL_VERIFY_PEER = 0x01 @@ -97,28 +89,128 @@ 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_BUILD_CHAIN_FLAG_NONE = 0x0 +SSL_BUILD_CHAIN_FLAG_UNTRUSTED = 0x1 +SSL_BUILD_CHAIN_FLAG_NO_ROOT = 0x2 +SSL_BUILD_CHAIN_FLAG_CHECK = 0x4 +SSL_BUILD_CHAIN_FLAG_IGNORE_ERROR = 0x8 +SSL_BUILD_CHAIN_FLAG_CLEAR_ERROR = 0x10 SSL_FILE_TYPE_PEM = 1 GEN_DIRNAME = 4 NID_subject_alt_name = 85 CRYPTO_LOCK = 1 +SSL_ST_MASK = 0x0FFF +SSL_ST_CONNECT = 0x1000 +SSL_ST_ACCEPT = 0x2000 +SSL_ST_INIT = (SSL_ST_CONNECT | SSL_ST_ACCEPT) +SSL_ST_BEFORE = 0x4000 + +SSL_ST_OK = 0x03 +SSL_ST_RENEGOTIATE = (0x04 | SSL_ST_INIT) +SSL_ST_ERR = 0x05 + +SSL_CB_LOOP = 0x01 +SSL_CB_EXIT = 0x02 +SSL_CB_READ = 0x04 +SSL_CB_WRITE = 0x08 + +SSL_CB_ALERT = 0x4000 +SSL_CB_READ_ALERT = (SSL_CB_ALERT | SSL_CB_READ) +SSL_CB_WRITE_ALERT = (SSL_CB_ALERT | SSL_CB_WRITE) +SSL_CB_ACCEPT_LOOP = (SSL_ST_ACCEPT | SSL_CB_LOOP) +SSL_CB_ACCEPT_EXIT = (SSL_ST_ACCEPT | SSL_CB_EXIT) +SSL_CB_CONNECT_LOOP = (SSL_ST_CONNECT | SSL_CB_LOOP) +SSL_CB_CONNECT_EXIT = (SSL_ST_CONNECT | SSL_CB_EXIT) +SSL_CB_HANDSHAKE_START = 0x10 +SSL_CB_HANDSHAKE_DONE = 0x20 + # # Integer constants - internal # -SSL_CTRL_SET_SESS_CACHE_MODE = 44 -SSL_CTRL_SET_READ_AHEAD = 41 +SSL_CTRL_SET_TMP_ECDH = 4 +SSL_CTRL_SET_MTU = 17 SSL_CTRL_OPTIONS = 32 +SSL_CTRL_SET_READ_AHEAD = 41 +SSL_CTRL_SET_SESS_CACHE_MODE = 44 +SSL_CTRL_CLEAR_OPTIONS = 77 +SSL_CTRL_GET_CURVES = 90 +SSL_CTRL_SET_CURVES = 91 +SSL_CTRL_SET_CURVES_LIST = 92 +SSL_CTRL_GET_SHARED_CURVE = 93 +SSL_CTRL_SET_ECDH_AUTO = 94 +SSL_CTRL_SET_SIGALGS = 97 +SSL_CTRL_SET_SIGALGS_LIST = 98 +SSL_CTRL_SET_CLIENT_SIGALGS = 101 +SSL_CTRL_SET_CLIENT_SIGALGS_LIST = 102 +SSL_CTRL_BUILD_CERT_CHAIN = 105 + BIO_CTRL_INFO = 3 BIO_CTRL_DGRAM_SET_CONNECTED = 32 -BIO_CTRL_DGRAM_GET_PEER = 46 BIO_CTRL_DGRAM_SET_PEER = 44 +BIO_CTRL_DGRAM_GET_PEER = 46 + BIO_C_SET_NBIO = 102 + DTLS_CTRL_GET_TIMEOUT = 73 DTLS_CTRL_HANDLE_TIMEOUT = 74 DTLS_CTRL_LISTEN = 75 +DTLS_CTRL_SET_LINK_MTU = 120 + X509_NAME_MAXLEN = 256 GETS_MAXLEN = 2048 + +class _EllipticCurve(object): + _curves = None + + @classmethod + def _get_elliptic_curves(cls): + if cls._curves is None: + # Load once + cls._curves = cls._load_elliptic_curves() + return cls._curves + + @classmethod + def _load_elliptic_curves(cls): + num_curves = EC_get_builtin_curves(None, 0) + if num_curves > 0: + builtin_curves = create_string_buffer(sizeof(EC_builtin_curve) * num_curves) + EC_get_builtin_curves(cast(builtin_curves, POINTER(EC_builtin_curve)), num_curves) + return [cls(c.nid, OBJ_nid2sn(c.nid)) for c in cast(builtin_curves, POINTER(EC_builtin_curve))[:num_curves]] + return [] + + def __init__(self, nid, name): + self.nid = nid + self.name = name + + def __repr__(self): + return "" % (self.nid, self.name) + + def to_EC_KEY(self): + key = _EC_KEY(EC_KEY_new_by_curve_name(self.nid)) + return key if bool(key.value) else None + + +def get_elliptic_curves(): + u''' Return the available curves. If not yet loaded, then load them once. + + :rtype: list + ''' + return _EllipticCurve._get_elliptic_curves() + + +def get_elliptic_curve(name): + u''' Return the curve from the given name. + + :rtype: _EllipticCurve + ''' + for curve in get_elliptic_curves(): + if curve.name == name: + return curve + raise ValueError("unknown curve name", name) + + # # Parameter data types # @@ -151,9 +243,9 @@ class FuncParam(object): return self._as_parameter.value -class DTLSv1Method(FuncParam): +class DTLS_Method(FuncParam): def __init__(self, value): - super(DTLSv1Method, self).__init__(value) + super(DTLS_Method, self).__init__(value) class BIO_METHOD(FuncParam): @@ -176,6 +268,11 @@ class BIO(FuncParam): super(BIO, self).__init__(value) +class EC_KEY(FuncParam): + def __init__(self, value): + super(EC_KEY, self).__init__(value) + + class X509(FuncParam): def __init__(self, value): super(X509, self).__init__(value) @@ -246,6 +343,13 @@ class GENERAL_NAMES(STACK): super(GENERAL_NAMES, self).__init__(value) +class STACK_OF_X509(STACK): + stack_element_type = X509 + + def __init__(self, value): + super(STACK_OF_X509, self).__init__(value) + + class X509_NAME_ENTRY(Structure): _fields_ = [("object", c_void_p), ("value", c_void_p), @@ -281,6 +385,11 @@ class TIMEVAL(Structure): ("tv_usec", c_long)] +class EC_builtin_curve(Structure): + _fields_ = [("nid", c_int), + ("comment", c_char_p)] + + # # Socket address conversions # @@ -470,85 +579,139 @@ def _make_function(name, lib, args, export=True, errcheck="default"): _subst = {c_long_parm: c_long} _sigs = {} -__all__ = ["BIO_NOCLOSE", "BIO_CLOSE", - "SSLEAY_VERSION", - "SSL_OP_NO_COMPRESSION", - "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", - "GEN_DIRNAME", "NID_subject_alt_name", - "CRYPTO_LOCK", - "CRYPTO_set_locking_callback", - "DTLSv1_get_timeout", "DTLSv1_handle_timeout", - "DTLSv1_listen", - "BIO_gets", "BIO_read", "BIO_get_mem_data", - "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_CTX_set_options", - "SSL_read", "SSL_write", - "SSL_CTX_set_cookie_cb", - "OBJ_obj2txt", "decode_ASN1_STRING", "ASN1_TIME_print", - "X509_get_notAfter", - "ASN1_item_d2i", "GENERAL_NAME_print", - "sk_value", - "sk_pop_free", - "i2d_X509"] # note: the following map adds to this list +__all__ = [ + # Constants + "BIO_NOCLOSE", "BIO_CLOSE", + "SSLEAY_VERSION", + "SSL_OP_NO_QUERY_MTU", "SSL_OP_NO_COMPRESSION", + "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_ST_MASK", "SSL_ST_CONNECT", "SSL_ST_ACCEPT", "SSL_ST_INIT", "SSL_ST_BEFORE", "SSL_ST_OK", + "SSL_ST_RENEGOTIATE", "SSL_ST_ERR", "SSL_CB_LOOP", "SSL_CB_EXIT", "SSL_CB_READ", "SSL_CB_WRITE", + "SSL_CB_ALERT", "SSL_CB_READ_ALERT", "SSL_CB_WRITE_ALERT", + "SSL_CB_ACCEPT_LOOP", "SSL_CB_ACCEPT_EXIT", + "SSL_CB_CONNECT_LOOP", "SSL_CB_CONNECT_EXIT", + "SSL_CB_HANDSHAKE_START", "SSL_CB_HANDSHAKE_DONE", + "SSL_BUILD_CHAIN_FLAG_NONE", "SSL_BUILD_CHAIN_FLAG_UNTRUSTED", "SSL_BUILD_CHAIN_FLAG_NO_ROOT", + "SSL_BUILD_CHAIN_FLAG_CHECK", "SSL_BUILD_CHAIN_FLAG_IGNORE_ERROR", "SSL_BUILD_CHAIN_FLAG_CLEAR_ERROR", + "SSL_FILE_TYPE_PEM", + "GEN_DIRNAME", "NID_subject_alt_name", + "CRYPTO_LOCK", + # Methods + "CRYPTO_set_locking_callback", + "DTLSv1_get_timeout", "DTLSv1_handle_timeout", + "DTLSv1_listen", + "DTLS_set_link_mtu", + "BIO_gets", "BIO_read", "BIO_get_mem_data", + "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_CTX_set_options", "SSL_CTX_clear_options", "SSL_CTX_get_options", + "SSL_CTX_set1_client_sigalgs_list", "SSL_CTX_set1_client_sigalgs", + "SSL_CTX_set1_sigalgs_list", "SSL_CTX_set1_sigalgs", + "SSL_CTX_set1_curves", "SSL_CTX_set1_curves_list", + "SSL_CTX_set_info_callback", + "SSL_CTX_build_cert_chain", + "SSL_CTX_set_ecdh_auto", + "SSL_CTX_set_tmp_ecdh", + "SSL_read", "SSL_write", + "SSL_set_options", "SSL_clear_options", "SSL_get_options", + "SSL_set1_client_sigalgs_list", "SSL_set1_client_sigalgs", + "SSL_set1_sigalgs_list", "SSL_set1_sigalgs", + "SSL_get1_curves", "SSL_get_shared_curve", + "SSL_set1_curves", "SSL_set1_curves_list", + "SSL_set_mtu", + "SSL_state_string_long", "SSL_alert_type_string_long", "SSL_alert_desc_string_long", + "SSL_get_peer_cert_chain", + "SSL_CTX_set_cookie_cb", + "OBJ_obj2txt", "decode_ASN1_STRING", "ASN1_TIME_print", + "OBJ_nid2sn", + "X509_get_notAfter", + "ASN1_item_d2i", "GENERAL_NAME_print", + "sk_value", + "sk_pop_free", + "i2d_X509", + "get_elliptic_curves", +] # note: the following map adds to this list map(lambda x: _make_function(*x), ( - ("SSL_library_init", libssl, ((c_int, "ret"),)), - ("SSL_load_error_strings", libssl, ((None, "ret"),)), - ("SSLeay", libcrypto, ((c_long_parm, "ret"),)), - ("SSLeay_version", libcrypto, ((c_char_p, "ret"), (c_int, "t"))), + ("SSL_library_init", libssl, + ((c_int, "ret"),)), + ("SSL_load_error_strings", libssl, + ((None, "ret"),)), + ("SSLeay", libcrypto, + ((c_long_parm, "ret"),)), + ("SSLeay_version", libcrypto, + ((c_char_p, "ret"), (c_int, "t"))), ("CRYPTO_set_locking_callback", libcrypto, ((None, "ret"), (c_void_p, "func")), False), - ("CRYPTO_get_id_callback", libcrypto, ((c_void_p, "ret"),), True, None), - ("CRYPTO_num_locks", libcrypto, ((c_int, "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"))), + ("CRYPTO_get_id_callback", libcrypto, + ((c_void_p, "ret"),), True, None), + ("CRYPTO_num_locks", libcrypto, + ((c_int, "ret"),)), + ("DTLS_server_method", libssl, + ((DTLS_Method, "ret"),)), + ("DTLSv1_server_method", libssl, + ((DTLS_Method, "ret"),)), + ("DTLSv1_2_server_method", libssl, + ((DTLS_Method, "ret"),)), + ("DTLSv1_client_method", libssl, + ((DTLS_Method, "ret"),)), + ("DTLSv1_2_client_method", libssl, + ((DTLS_Method, "ret"),)), + ("SSL_CTX_new", libssl, + ((SSLCTX, "ret"), (DTLS_Method, "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"))), + ((None, "ret"), (SSLCTX, "ctx"), (c_void_p, "app_verify_cookie_cb")), False), + ("SSL_CTX_set_info_callback", libssl, + ((None, "ret"), (SSLCTX, "ctx"), (c_void_p, "app_info_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", libcrypto, ((BIO, "ret"), (BIO_METHOD, "type"))), - ("BIO_s_mem", libcrypto, ((BIO_METHOD, "ret"),)), + ("BIO_new", libcrypto, + ((BIO, "ret"), (BIO_METHOD, "type"))), + ("BIO_s_mem", libcrypto, + ((BIO_METHOD, "ret"),)), ("BIO_new_file", libcrypto, ((BIO, "ret"), (c_char_p, "filename"), (c_char_p, "mode"))), ("BIO_new_dgram", libcrypto, ((BIO, "ret"), (c_int, "fd"), (c_int, "close_flag"))), - ("BIO_free", libcrypto, ((c_int, "ret"), (BIO, "a"))), + ("BIO_free", libcrypto, + ((c_int, "ret"), (BIO, "a"))), ("BIO_gets", libcrypto, - ((c_int, "ret"), (BIO, "b"), (POINTER(c_char), "buf"), (c_int, "size")), - False), + ((c_int, "ret"), (BIO, "b"), (POINTER(c_char), "buf"), (c_int, "size")), False), ("BIO_read", libcrypto, ((c_int, "ret"), (BIO, "b"), (c_void_p, "buf"), (c_int, "len")), False), ("SSL_CTX_ctrl", libssl, - ((c_long_parm, "ret"), (SSLCTX, "ctx"), (c_int, "cmd"), (c_long, "larg"), - (c_void_p, "parg")), False), + ((c_long_parm, "ret"), (SSLCTX, "ctx"), (c_int, "cmd"), (c_long, "larg"), (c_void_p, "parg")), False), ("BIO_ctrl", libcrypto, - ((c_long_parm, "ret"), (BIO, "bp"), (c_int, "cmd"), (c_long, "larg"), - (c_void_p, "parg")), False), + ((c_long_parm, "ret"), (BIO, "bp"), (c_int, "cmd"), (c_long, "larg"), (c_void_p, "parg")), False), ("SSL_ctrl", libssl, - ((c_long_parm, "ret"), (SSL, "ssl"), (c_int, "cmd"), (c_long, "larg"), - (c_void_p, "parg")), False), - ("ERR_get_error", libcrypto, ((c_long_parm, "ret"),), False), + ((c_long_parm, "ret"), (SSL, "ssl"), (c_int, "cmd"), (c_long, "larg"), (c_void_p, "parg")), False), + ("ERR_get_error", libcrypto, + ((c_long_parm, "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), + ((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_state_string_long", libssl, + ((c_char_p, "ret"), (SSL, "ssl")), False), + ("SSL_alert_type_string_long", libssl, + ((c_char_p, "ret"), (c_int, "value")), False), + ("SSL_alert_desc_string_long", libssl, + ((c_char_p, "ret"), (c_int, "value")), False), ("SSL_CTX_set_cipher_list", libssl, ((c_int, "ret"), (SSLCTX, "ctx"), (c_char_p, "str"))), ("SSL_CTX_use_certificate_file", libssl, @@ -558,36 +721,45 @@ map(lambda x: _make_function(*x), ( ("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"))), + ((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_get_peer_certificate", libssl, ((X509, "ret"), (SSL, "ssl"))), + ((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_get_peer_certificate", libssl, + ((X509, "ret"), (SSL, "ssl"))), + ("SSL_get_peer_cert_chain", libssl, + ((STACK_OF_X509, "ret"), (SSL, "ssl")), False), ("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_pending", libssl, ((c_int, "ret"), (SSL, "ssl")), True, None), - ("SSL_shutdown", libssl, ((c_int, "ret"), (SSL, "ssl"))), + ("SSL_pending", libssl, + ((c_int, "ret"), (SSL, "ssl")), True, None), + ("SSL_shutdown", libssl, + ((c_int, "ret"), (SSL, "ssl"))), ("SSL_set_read_ahead", libssl, ((None, "ret"), (SSL, "ssl"), (c_int, "yes"))), - ("X509_free", libcrypto, ((None, "ret"), (X509, "a"))), + ("X509_free", libcrypto, + ((None, "ret"), (X509, "a"))), ("PEM_read_bio_X509_AUX", libcrypto, - ((X509, "ret"), (BIO, "bp"), (c_void_p, "x", 1, None), - (c_void_p, "cb", 1, None), (c_void_p, "u", 1, None))), + ((X509, "ret"), (BIO, "bp"), (c_void_p, "x", 1, None), (c_void_p, "cb", 1, None), (c_void_p, "u", 1, None))), ("OBJ_obj2txt", libcrypto, - ((c_int, "ret"), (POINTER(c_char), "buf"), (c_int, "buf_len"), - (ASN1_OBJECT, "a"), (c_int, "no_name")), False), - ("CRYPTO_free", libcrypto, ((None, "ret"), (c_void_p, "ptr"))), + ((c_int, "ret"), (POINTER(c_char), "buf"), (c_int, "buf_len"), (ASN1_OBJECT, "a"), (c_int, "no_name")), False), + ("OBJ_nid2sn", libcrypto, + ((c_char_p, "ret"), (c_int, "n")), False), + ("CRYPTO_free", libcrypto, + ((None, "ret"), (c_void_p, "ptr"))), ("ASN1_STRING_to_UTF8", libcrypto, - ((c_int, "ret"), (POINTER(POINTER(c_ubyte)), "out"), (ASN1_STRING, "in")), - False), + ((c_int, "ret"), (POINTER(POINTER(c_ubyte)), "out"), (ASN1_STRING, "in")), False), ("X509_NAME_entry_count", libcrypto, ((c_int, "ret"), (POINTER(X509_name_st), "name")), True, None), ("X509_NAME_get_entry", libcrypto, @@ -602,34 +774,41 @@ map(lambda x: _make_function(*x), ( ("ASN1_TIME_print", libcrypto, ((c_int, "ret"), (BIO, "fp"), (ASN1_TIME, "a")), False), ("X509_get_ext_by_NID", libcrypto, - ((c_int, "ret"), (X509, "x"), (c_int, "nid"), (c_int, "lastpos")), - True, None), + ((c_int, "ret"), (X509, "x"), (c_int, "nid"), (c_int, "lastpos")), True, None), ("X509_get_ext", libcrypto, - ((POINTER(X509_EXTENSION), "ret"), (X509, "x"), (c_int, "loc")), - True, errcheck_p), + ((POINTER(X509_EXTENSION), "ret"), (X509, "x"), (c_int, "loc")), True, errcheck_p), ("X509V3_EXT_get", libcrypto, - ((POINTER(X509V3_EXT_METHOD), "ret"), (POINTER(X509_EXTENSION), "ext")), - True, errcheck_p), + ((POINTER(X509V3_EXT_METHOD), "ret"), (POINTER(X509_EXTENSION), "ext")), True, errcheck_p), ("ASN1_item_d2i", libcrypto, - ((c_void_p, "ret"), (c_void_p, "val"), (POINTER(POINTER(c_ubyte)), "in"), - (c_long, "len"), (c_void_p, "it")), False, None), - ("sk_num", libcrypto, ((c_int, "ret"), (STACK, "stack")), True, None), + ((c_void_p, "ret"), (c_void_p, "val"), (POINTER(POINTER(c_ubyte)), "in"), (c_long, "len"), (c_void_p, "it")), False, None), + ("sk_num", libcrypto, + ((c_int, "ret"), (STACK, "stack")), True, None), ("sk_value", libcrypto, ((c_void_p, "ret"), (STACK, "stack"), (c_int, "loc")), False), ("GENERAL_NAME_print", libcrypto, ((c_int, "ret"), (BIO, "out"), (POINTER(GENERAL_NAME), "gen")), False), ("sk_pop_free", libcrypto, ((None, "ret"), (STACK, "st"), (c_void_p, "func")), False), - ("i2d_X509_bio", libcrypto, ((c_int, "ret"), (BIO, "bp"), (X509, "x")), - False), - ("SSL_get_current_cipher", libssl, ((SSL_CIPHER, "ret"), (SSL, "ssl"))), + ("i2d_X509_bio", libcrypto, + ((c_int, "ret"), (BIO, "bp"), (X509, "x")), False), + ("SSL_get_current_cipher", libssl, + ((SSL_CIPHER, "ret"), (SSL, "ssl"))), ("SSL_CIPHER_get_name", libssl, ((c_char_p, "ret"), (SSL_CIPHER, "cipher"))), ("SSL_CIPHER_get_version", libssl, ((c_char_p, "ret"), (SSL_CIPHER, "cipher"))), ("SSL_CIPHER_get_bits", libssl, - ((c_int, "ret"), (SSL_CIPHER, "cipher"), - (POINTER(c_int), "alg_bits", 1, None)), True, None), + ((c_int, "ret"), (SSL_CIPHER, "cipher"), (POINTER(c_int), "alg_bits", 1, None)), True, None), + ("EC_get_builtin_curves", libcrypto, + ((c_int, "ret"), (POINTER(EC_builtin_curve), "r"), (c_int, "nitems"))), + ("EC_KEY_new_by_curve_name", libcrypto, + ((EC_KEY, "ret"), (c_int, "nid"))), + ("EC_KEY_free", libcrypto, + ((None, "ret"), (EC_KEY, "key"))), + ("EC_curve_nist2nid", libcrypto, + ((c_int, "ret"), (POINTER(c_char), "name")), True, None), + ("EC_curve_nid2nist", libcrypto, + ((c_char_p, "ret"), (c_int, "nid")), True, None), )) # @@ -650,15 +829,78 @@ def CRYPTO_set_locking_callback(locking_function): 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) + return _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) + return _SSL_CTX_ctrl(ctx, SSL_CTRL_SET_READ_AHEAD, m, None) def SSL_CTX_set_options(ctx, options): # Returns the new option bitmaks after adding the given options - _SSL_CTX_ctrl(ctx, SSL_CTRL_OPTIONS, options, None) + return _SSL_CTX_ctrl(ctx, SSL_CTRL_OPTIONS, options, None) + +def SSL_CTX_clear_options(ctx, options): + return _SSL_CTX_ctrl(ctx, SSL_CTRL_CLEAR_OPTIONS, options, None) + +def SSL_CTX_get_options(ctx): + return _SSL_CTX_ctrl(ctx, SSL_CTRL_OPTIONS, 0, None) + +def SSL_CTX_set1_client_sigalgs(ctx, slist, slistlen): + _slist = (c_int * len(slist))(*slist) + return _SSL_CTX_ctrl(ctx, SSL_CTRL_SET_CLIENT_SIGALGS, len(_slist), _slist) + +def SSL_CTX_set1_client_sigalgs_list(ctx, s): + _s = cast(s, POINTER(c_char)) + return _SSL_CTX_ctrl(ctx, SSL_CTRL_SET_CLIENT_SIGALGS_LIST, 0, _s) + +def SSL_CTX_set1_sigalgs(ctx, slist, slistlen): + _slist = (c_int * len(slist))(*slist) + return _SSL_CTX_ctrl(ctx, SSL_CTRL_SET_SIGALGS, len(_slist), _slist) + +def SSL_CTX_set1_sigalgs_list(ctx, s): + _s = cast(s, POINTER(c_char)) + return _SSL_CTX_ctrl(ctx, SSL_CTRL_SET_SIGALGS_LIST, 0, _s) + +def SSL_CTX_set1_curves(ctx, clist, clistlen): + _curves = (c_int * len(clist))(*clist) + return _SSL_CTX_ctrl(ctx, SSL_CTRL_SET_CURVES, len(_curves), _curves) + +def SSL_CTX_set1_curves_list(ctx, s): + _s = cast(s, POINTER(c_char)) + return _SSL_CTX_ctrl(ctx, SSL_CTRL_SET_CURVES_LIST, 0, _s) + +_rvoid_voidp_int_int = CFUNCTYPE(None, c_void_p, c_int, c_int) + +_info_callback = dict() + +def SSL_CTX_set_info_callback(ctx, app_info_cb): + """ + Set the info callback + + :param callback: The Python callback to use + :return: None + """ + def py_info_callback(ssl, where, ret): + try: + app_info_cb(SSL(ssl), where, ret) + except: + pass + return + + global _info_callback + _info_callback[ctx] = _rvoid_voidp_int_int(py_info_callback) + _SSL_CTX_set_info_callback(ctx, _info_callback[ctx]) + +def SSL_CTX_build_cert_chain(ctx, flags): + return _SSL_CTX_ctrl(ctx, SSL_CTRL_BUILD_CERT_CHAIN, flags, None) + +def SSL_CTX_set_ecdh_auto(ctx, onoff): + return _SSL_CTX_ctrl(ctx, SSL_CTRL_SET_ECDH_AUTO, onoff, None) + +def SSL_CTX_set_tmp_ecdh(ctx, ec_key): + # return 1 on success and 0 on failure + _ec_key_p = cast(ec_key.raw, c_void_p) + return _SSL_CTX_ctrl(ctx, SSL_CTRL_SET_TMP_ECDH, 0, _ec_key_p) _rint_voidp_ubytep_uintp = CFUNCTYPE(c_int, c_void_p, POINTER(c_ubyte), POINTER(c_uint)) @@ -693,7 +935,7 @@ def SSL_CTX_set_cookie_cb(ctx, generate, verify): 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)) + return _BIO_ctrl(bio, BIO_CTRL_DGRAM_SET_CONNECTED, 0, byref(su)) def BIO_dgram_get_peer(bio): su = sockaddr_u() @@ -702,10 +944,10 @@ def BIO_dgram_get_peer(bio): 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)) + return _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) + return _BIO_ctrl(bio, BIO_C_SET_NBIO, 1 if n else 0, None) def DTLSv1_get_timeout(ssl): tv = TIMEVAL() @@ -727,7 +969,7 @@ def DTLSv1_handle_timeout(ssl): assert ret < 0 if ret > 0: ret = -10 - errcheck_p(ret, _SSL_ctrl, (ssl, DTLS_CTRL_HANDLE_TIMEOUT, 0, None)) + return errcheck_p(ret, _SSL_ctrl, (ssl, DTLS_CTRL_HANDLE_TIMEOUT, 0, None)) def DTLSv1_listen(ssl): su = sockaddr_u() @@ -735,6 +977,9 @@ def DTLSv1_listen(ssl): errcheck_ord(ret, _SSL_ctrl, (ssl, DTLS_CTRL_LISTEN, 0, byref(su))) return addr_tuple_from_sockaddr_u(su) +def DTLS_set_link_mtu(ssl, mtu): + return _SSL_ctrl(ssl, DTLS_CTRL_SET_LINK_MTU, mtu, None) + def SSL_read(ssl, length, buffer): if buffer: length = min(length, len(buffer)) @@ -751,15 +996,94 @@ def SSL_write(ssl, data): str_data = data elif hasattr(data, "tobytes") and callable(data.tobytes): str_data = data.tobytes() + elif isinstance(data, ctypes.Array): + str_data = data.raw else: str_data = str(data) return _SSL_write(ssl, str_data, len(str_data)) +def SSL_set_options(ssl, op): + return _SSL_ctrl(ssl, SSL_CTRL_OPTIONS, op, None) + +def SSL_clear_options(ssl, op): + return _SSL_ctrl(ssl, SSL_CTRL_CLEAR_OPTIONS, op, None) + +def SSL_get_options(ssl): + return _SSL_ctrl(ssl, SSL_CTRL_OPTIONS, 0, None) + +def SSL_set1_client_sigalgs(ssl, slist, slistlen): + _slist = (c_int * len(slist))(*slist) + return _SSL_ctrl(ssl, SSL_CTRL_SET_CLIENT_SIGALGS, len(_slist), _slist) + +def SSL_set1_client_sigalgs_list(ssl, s): + _s = cast(s, POINTER(c_char)) + return _SSL_ctrl(ssl, SSL_CTRL_SET_CLIENT_SIGALGS_LIST, 0, _s) + +def SSL_set1_sigalgs(ssl, slist, slistlen): + _slist = (c_int * len(slist))(*slist) + return _SSL_ctrl(ssl, SSL_CTRL_SET_SIGALGS, len(_slist), _slist) + +def SSL_set1_sigalgs_list(ssl, s): + _s = cast(s, POINTER(c_char)) + return _SSL_ctrl(ssl, SSL_CTRL_SET_SIGALGS_LIST, 0, _s) + +def SSL_get1_curves(ssl, curves=None): + assert curves is None or isinstance(curves, list) + if curves is not None: + cnt = SSL_get1_curves(ssl, None) + if cnt: + mem = create_string_buffer(sizeof(POINTER(c_int)) * cnt) + _SSL_ctrl(ssl, SSL_CTRL_GET_CURVES, 0, mem) + for x in cast(mem, POINTER(c_int))[:cnt]: + curves.append(x) + return cnt + else: + return _SSL_ctrl(ssl, SSL_CTRL_GET_CURVES, 0, None) + +def SSL_get_shared_curve(ssl, n): + return _SSL_ctrl(ssl, SSL_CTRL_GET_SHARED_CURVE, n, 0) + +def SSL_set1_curves(ssl, clist, clistlen): + _curves = (c_int * len(clist))(*clist) + return _SSL_ctrl(ssl, SSL_CTRL_SET_CURVES, len(_curves), _curves) + +def SSL_set1_curves_list(ssl, s): + _s = cast(s, POINTER(c_char)) + return _SSL_ctrl(ssl, SSL_CTRL_SET_CURVES_LIST, 0, _s) + +def SSL_set_mtu(ssl, mtu): + return _SSL_ctrl(ssl, SSL_CTRL_SET_MTU, mtu, None) + +def SSL_state_string_long(ssl): + try: + ret = _SSL_state_string_long(ssl) + except: + pass + return ret + +def SSL_alert_type_string_long(value): + try: + ret = _SSL_alert_type_string_long(value) + except: + pass + return ret + +def SSL_alert_desc_string_long(value): + try: + ret = _SSL_alert_desc_string_long(value) + except: + pass + return ret + def OBJ_obj2txt(asn1_object, no_name): buf = create_string_buffer(X509_NAME_MAXLEN) res_len = _OBJ_obj2txt(buf, sizeof(buf), asn1_object, 1 if no_name else 0) return buf.raw[:res_len] +def OBJ_nid2sn(nid): + _name = _OBJ_nid2sn(nid) + return cast(_name, c_char_p).value.decode("ascii") + def decode_ASN1_STRING(asn1_string): utf8_buf_ptr = POINTER(c_ubyte)() res_len = _ASN1_STRING_to_UTF8(byref(utf8_buf_ptr), asn1_string) @@ -833,3 +1157,13 @@ def i2d_X509(x509): bio = _BIO(BIO_new(BIO_s_mem())) _i2d_X509_bio(bio.value, x509) return BIO_get_mem_data(bio.value) + +def SSL_get_peer_cert_chain(ssl): + stack = _SSL_get_peer_cert_chain(ssl) + num = sk_num(stack) + certs = [] + if num: + # why not use sk_value(): because it doesn't cast correct in this case?! + # certs = [(sk_value(stack, i)) for i in xrange(num)] + certs = [X509(_sk_value(stack, i)) for i in xrange(num)] + return stack, num, certs diff --git a/dtls/patch.py b/dtls/patch.py index acd54dd..2882c4f 100644 --- a/dtls/patch.py +++ b/dtls/patch.py @@ -34,16 +34,20 @@ has the following effects: PROTOCOL_DTLSv1 for the parameter ssl_version is supported """ -from socket import SOCK_DGRAM, socket, _delegate_methods, error as socket_error -from socket import AF_INET, SOCK_STREAM, SOCK_DGRAM, getaddrinfo -from sslconnection import SSLConnection, PROTOCOL_DTLSv1, CERT_NONE -from sslconnection import DTLS_OPENSSL_VERSION_NUMBER, DTLS_OPENSSL_VERSION -from sslconnection import DTLS_OPENSSL_VERSION_INFO -from err import raise_as_ssl_module_error +from socket import socket, getaddrinfo, _delegate_methods, error as socket_error +from socket import AF_INET, SOCK_STREAM, SOCK_DGRAM +from ssl import PROTOCOL_SSLv23, CERT_NONE from types import MethodType from weakref import proxy import errno +from sslconnection import SSLConnection, PROTOCOL_DTLS, PROTOCOL_DTLSv1, PROTOCOL_DTLSv1_2 +from sslconnection import DTLS_OPENSSL_VERSION_NUMBER, DTLS_OPENSSL_VERSION, DTLS_OPENSSL_VERSION_INFO +from sslconnection import SSL_BUILD_CHAIN_FLAG_NONE, SSL_BUILD_CHAIN_FLAG_UNTRUSTED, \ + SSL_BUILD_CHAIN_FLAG_NO_ROOT, SSL_BUILD_CHAIN_FLAG_CHECK, SSL_BUILD_CHAIN_FLAG_IGNORE_ERROR, SSL_BUILD_CHAIN_FLAG_CLEAR_ERROR +from err import raise_as_ssl_module_error, patch_ssl_errors + + def do_patch(): import ssl as _ssl # import to be avoided if ssl module is never patched global _orig_SSLSocket_init, _orig_get_server_certificate @@ -51,18 +55,47 @@ def do_patch(): ssl = _ssl if hasattr(ssl, "PROTOCOL_DTLSv1"): return + _orig_wrap_socket = ssl.wrap_socket + ssl.wrap_socket = _wrap_socket + ssl.PROTOCOL_DTLS = PROTOCOL_DTLS ssl.PROTOCOL_DTLSv1 = PROTOCOL_DTLSv1 + ssl.PROTOCOL_DTLSv1_2 = PROTOCOL_DTLSv1_2 + ssl._PROTOCOL_NAMES[PROTOCOL_DTLS] = "DTLS" ssl._PROTOCOL_NAMES[PROTOCOL_DTLSv1] = "DTLSv1" + ssl._PROTOCOL_NAMES[PROTOCOL_DTLSv1_2] = "DTLSv1.2" ssl.DTLS_OPENSSL_VERSION_NUMBER = DTLS_OPENSSL_VERSION_NUMBER ssl.DTLS_OPENSSL_VERSION = DTLS_OPENSSL_VERSION ssl.DTLS_OPENSSL_VERSION_INFO = DTLS_OPENSSL_VERSION_INFO + ssl.SSL_BUILD_CHAIN_FLAG_NONE = SSL_BUILD_CHAIN_FLAG_NONE + ssl.SSL_BUILD_CHAIN_FLAG_UNTRUSTED = SSL_BUILD_CHAIN_FLAG_UNTRUSTED + ssl.SSL_BUILD_CHAIN_FLAG_NO_ROOT = SSL_BUILD_CHAIN_FLAG_NO_ROOT + ssl.SSL_BUILD_CHAIN_FLAG_CHECK = SSL_BUILD_CHAIN_FLAG_CHECK + ssl.SSL_BUILD_CHAIN_FLAG_IGNORE_ERROR = SSL_BUILD_CHAIN_FLAG_IGNORE_ERROR + ssl.SSL_BUILD_CHAIN_FLAG_CLEAR_ERROR = SSL_BUILD_CHAIN_FLAG_CLEAR_ERROR _orig_SSLSocket_init = ssl.SSLSocket.__init__ _orig_get_server_certificate = ssl.get_server_certificate ssl.SSLSocket.__init__ = _SSLSocket_init ssl.get_server_certificate = _get_server_certificate + patch_ssl_errors() raise_as_ssl_module_error() -PROTOCOL_SSLv23 = 2 +def _wrap_socket(sock, keyfile=None, certfile=None, + server_side=False, cert_reqs=CERT_NONE, + ssl_version=PROTOCOL_DTLS, ca_certs=None, + do_handshake_on_connect=True, + suppress_ragged_eofs=True, + ciphers=None, + cb_user_config_ssl_ctx=None, + cb_user_config_ssl=None): + + return ssl.SSLSocket(sock, keyfile=keyfile, certfile=certfile, + server_side=server_side, cert_reqs=cert_reqs, + ssl_version=ssl_version, ca_certs=ca_certs, + do_handshake_on_connect=do_handshake_on_connect, + suppress_ragged_eofs=suppress_ragged_eofs, + ciphers=ciphers, + cb_user_config_ssl_ctx=cb_user_config_ssl_ctx, + cb_user_config_ssl=cb_user_config_ssl) def _get_server_certificate(addr, ssl_version=PROTOCOL_SSLv23, ca_certs=None): """Retrieve a server certificate @@ -73,10 +106,10 @@ def _get_server_certificate(addr, ssl_version=PROTOCOL_SSLv23, ca_certs=None): If 'ssl_version' is specified, use it in the connection attempt. """ - if ssl_version != PROTOCOL_DTLSv1: + if ssl_version not in (PROTOCOL_DTLS, PROTOCOL_DTLSv1, PROTOCOL_DTLSv1_2): return _orig_get_server_certificate(addr, ssl_version, ca_certs) - if (ca_certs is not None): + if ca_certs is not None: cert_reqs = ssl.CERT_REQUIRED else: cert_reqs = ssl.CERT_NONE @@ -91,12 +124,14 @@ def _get_server_certificate(addr, ssl_version=PROTOCOL_SSLv23, ca_certs=None): def _SSLSocket_init(self, sock=None, keyfile=None, certfile=None, server_side=False, cert_reqs=CERT_NONE, - ssl_version=PROTOCOL_SSLv23, ca_certs=None, + ssl_version=PROTOCOL_DTLS, ca_certs=None, do_handshake_on_connect=True, family=AF_INET, type=SOCK_STREAM, proto=0, fileno=None, suppress_ragged_eofs=True, npn_protocols=None, ciphers=None, server_hostname=None, - _context=None): + _context=None, + cb_user_config_ssl_ctx=None, + cb_user_config_ssl=None): is_connection = is_datagram = False if isinstance(sock, SSLConnection): is_connection = True @@ -148,7 +183,9 @@ def _SSLSocket_init(self, sock=None, keyfile=None, certfile=None, server_side, cert_reqs, ssl_version, ca_certs, do_handshake_on_connect, - suppress_ragged_eofs, ciphers) + suppress_ragged_eofs, ciphers, + cb_user_config_ssl_ctx=cb_user_config_ssl_ctx, + cb_user_config_ssl=cb_user_config_ssl) else: self._connected = True self._sslobj = sock @@ -166,6 +203,8 @@ def _SSLSocket_init(self, sock=None, keyfile=None, certfile=None, self.do_handshake_on_connect = do_handshake_on_connect self.suppress_ragged_eofs = suppress_ragged_eofs self._makefile_refs = 0 + self._user_config_ssl_ctx = cb_user_config_ssl_ctx + self._user_config_ssl = cb_user_config_ssl # Perform method substitution and addition (without reference cycle) self._real_connect = MethodType(_SSLSocket_real_connect, proxy(self)) @@ -174,6 +213,12 @@ def _SSLSocket_init(self, sock=None, keyfile=None, certfile=None, self.get_timeout = MethodType(_SSLSocket_get_timeout, proxy(self)) self.handle_timeout = MethodType(_SSLSocket_handle_timeout, proxy(self)) + # Extra + self.getpeercertchain = MethodType(_getpeercertchain, proxy(self)) + +def _getpeercertchain(self, binary_form=False): + return self._sslobj.getpeercertchain(binary_form) + def _SSLSocket_listen(self, ignored): if self._connected: raise ValueError("attempt to listen on connected SSLSocket!") @@ -184,7 +229,9 @@ def _SSLSocket_listen(self, ignored): self.cert_reqs, self.ssl_version, self.ca_certs, self.do_handshake_on_connect, - self.suppress_ragged_eofs, self.ciphers) + self.suppress_ragged_eofs, self.ciphers, + cb_user_config_ssl_ctx=self._user_config_ssl_ctx, + cb_user_config_ssl=self._user_config_ssl) def _SSLSocket_accept(self): if self._connected: @@ -199,7 +246,9 @@ def _SSLSocket_accept(self): self.cert_reqs, self.ssl_version, self.ca_certs, self.do_handshake_on_connect, - self.suppress_ragged_eofs, self.ciphers) + self.suppress_ragged_eofs, self.ciphers, + cb_user_config_ssl_ctx=self._user_config_ssl_ctx, + cb_user_config_ssl=self._user_config_ssl) return new_ssl_sock, addr def _SSLSocket_real_connect(self, addr, return_errno): @@ -210,7 +259,9 @@ def _SSLSocket_real_connect(self, addr, return_errno): self.cert_reqs, self.ssl_version, self.ca_certs, self.do_handshake_on_connect, - self.suppress_ragged_eofs, self.ciphers) + self.suppress_ragged_eofs, self.ciphers, + cb_user_config_ssl_ctx=self._user_config_ssl_ctx, + cb_user_config_ssl=self._user_config_ssl) try: self._sslobj.connect(addr) except socket_error as e: diff --git a/dtls/prebuilt/mingw-x86/cygcrypto-1.0.0.dll b/dtls/prebuilt/mingw-x86/cygcrypto-1.0.0.dll deleted file mode 100644 index 1b649e8..0000000 Binary files a/dtls/prebuilt/mingw-x86/cygcrypto-1.0.0.dll and /dev/null differ diff --git a/dtls/prebuilt/mingw-x86/cygssl-1.0.0.dll b/dtls/prebuilt/mingw-x86/cygssl-1.0.0.dll deleted file mode 100644 index 45815bd..0000000 Binary files a/dtls/prebuilt/mingw-x86/cygssl-1.0.0.dll and /dev/null differ diff --git a/dtls/prebuilt/mingw-x86/libeay32.dll b/dtls/prebuilt/mingw-x86/libeay32.dll deleted file mode 100644 index d0306d5..0000000 Binary files a/dtls/prebuilt/mingw-x86/libeay32.dll and /dev/null differ diff --git a/dtls/prebuilt/mingw-x86/manifest.pycfg b/dtls/prebuilt/mingw-x86/manifest.pycfg deleted file mode 100644 index 2b16159..0000000 --- a/dtls/prebuilt/mingw-x86/manifest.pycfg +++ /dev/null @@ -1,33 +0,0 @@ -# Prebuilt directory manifest. - -# Copyright 2012 Ray Brown -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# The License is also distributed with this work in the file named "LICENSE." -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# This file is executed by the distribution builder, as well as the dtls -# package startup code; the purpose of the latter is being able to run -# from a cloned source directory without executing any sort of installation -# procedure. This file provides the definitions required to create -# a distribution including this directory's prebuilts. - -from os import path -from glob import glob - -assert MANIFEST_DIR - -ARCHITECTURE = "mingw-win32" -FORMATS = "gztar" -FILES = map(lambda x: path.basename(x), - glob(path.join(MANIFEST_DIR, "*.dll"))) diff --git a/dtls/prebuilt/mingw-x86/ssleay32.dll b/dtls/prebuilt/mingw-x86/ssleay32.dll deleted file mode 100644 index a61548e..0000000 Binary files a/dtls/prebuilt/mingw-x86/ssleay32.dll and /dev/null differ diff --git a/dtls/prebuilt/win32-x86/libeay32.dll b/dtls/prebuilt/win32-x86/libeay32.dll index 8f8a254..df0a5cb 100644 Binary files a/dtls/prebuilt/win32-x86/libeay32.dll and b/dtls/prebuilt/win32-x86/libeay32.dll differ diff --git a/dtls/prebuilt/win32-x86/ssleay32.dll b/dtls/prebuilt/win32-x86/ssleay32.dll index 1a70063..175bb06 100644 Binary files a/dtls/prebuilt/win32-x86/ssleay32.dll and b/dtls/prebuilt/win32-x86/ssleay32.dll differ diff --git a/dtls/prebuilt/win32-x86_64/libeay32.dll b/dtls/prebuilt/win32-x86_64/libeay32.dll index 9724d58..57aea15 100644 Binary files a/dtls/prebuilt/win32-x86_64/libeay32.dll and b/dtls/prebuilt/win32-x86_64/libeay32.dll differ diff --git a/dtls/prebuilt/win32-x86_64/ssleay32.dll b/dtls/prebuilt/win32-x86_64/ssleay32.dll index 648f388..2759d6c 100644 Binary files a/dtls/prebuilt/win32-x86_64/ssleay32.dll and b/dtls/prebuilt/win32-x86_64/ssleay32.dll differ diff --git a/dtls/sslconnection.py b/dtls/sslconnection.py index 09fc3dc..5b69b06 100644 --- a/dtls/sslconnection.py +++ b/dtls/sslconnection.py @@ -40,6 +40,7 @@ library's ssl module, since its values can be passed to this module. CERT_REQUIRED """ +import sys import errno import socket import hmac @@ -48,13 +49,14 @@ from logging import getLogger from os import urandom from select import select from weakref import proxy + from err import openssl_error, InvalidSocketError from err import raise_ssl_error from err import SSL_ERROR_WANT_READ, SSL_ERROR_SYSCALL -from err import ERR_COOKIE_MISMATCH, ERR_NO_CERTS +from err import ERR_WRONG_VERSION_NUMBER, ERR_COOKIE_MISMATCH, ERR_NO_SHARED_CIPHER from err import ERR_NO_CIPHER, ERR_HANDSHAKE_TIMEOUT, ERR_PORT_UNREACHABLE from err import ERR_READ_TIMEOUT, ERR_WRITE_TIMEOUT -from err import ERR_BOTH_KEY_CERT_FILES_SVR +from err import ERR_BOTH_KEY_CERT_FILES, ERR_BOTH_KEY_CERT_FILES_SVR, ERR_NO_CERTS from x509 import _X509, decode_cert from tlock import tlock_init from openssl import * @@ -63,6 +65,8 @@ from util import _Rsrc, _BIO _logger = getLogger(__name__) PROTOCOL_DTLSv1 = 256 +PROTOCOL_DTLSv1_2 = 258 +PROTOCOL_DTLS = 259 CERT_NONE = 0 CERT_OPTIONAL = 1 CERT_REQUIRED = 2 @@ -83,6 +87,58 @@ DTLS_OPENSSL_VERSION_INFO = ( DTLS_OPENSSL_VERSION_NUMBER & 0xF) # status +def _ssl_logging_cb(conn, where, return_code): + _state = where & ~SSL_ST_MASK + state = "SSL" + if _state & SSL_ST_INIT == SSL_ST_INIT: + if _state & SSL_ST_RENEGOTIATE == SSL_ST_RENEGOTIATE: + state += "_renew" + else: + state += "_init" + elif _state & SSL_ST_CONNECT: + state += "_connect" + elif _state & SSL_ST_ACCEPT: + state += "_accept" + elif _state == 0: + if where & SSL_CB_HANDSHAKE_START: + state += "_handshake_start" + elif where & SSL_CB_HANDSHAKE_DONE: + state += "_handshake_done" + + if where & SSL_CB_LOOP: + state += '_loop' + _logger.debug("%s:%s:%d" % (state, + SSL_state_string_long(conn), + return_code)) + + elif where & SSL_CB_ALERT: + state += '_alert' + state += "_read" if where & SSL_CB_READ else "_write" + _logger.debug("%s:%s:%s" % (state, + SSL_alert_type_string_long(return_code), + SSL_alert_desc_string_long(return_code))) + + elif where & SSL_CB_EXIT: + state += '_exit' + if return_code == 0: + _logger.debug("%s:%s:%d(failed)" % (state, + SSL_state_string_long(conn), + return_code)) + elif return_code < 0: + _logger.debug("%s:%s:%d(error)" % (state, + SSL_state_string_long(conn), + return_code)) + else: + _logger.debug("%s:%s:%d" % (state, + SSL_state_string_long(conn), + return_code)) + + else: + _logger.debug("%s:%s:%d" % (state, + SSL_state_string_long(conn), + return_code)) + + class _CTX(_Rsrc): """SSL_CTX wrapper""" def __init__(self, value): @@ -122,6 +178,124 @@ class _CallbackProxy(object): return self.ssl_func(self.ssl_connection, *args, **kwargs) +class SSLContext(object): + + def __init__(self, ctx): + self._ctx = ctx + + def set_ciphers(self, ciphers): + u''' + s.a. https://www.openssl.org/docs/man1.1.0/apps/ciphers.html + + :param str ciphers: Example "AES256-SHA:ECDHE-ECDSA-AES256-SHA", ... + :return: 1 for success and 0 for failure + ''' + retVal = SSL_CTX_set_cipher_list(self._ctx, ciphers) + return retVal + + def set_sigalgs(self, sigalgs): + u''' + s.a. https://www.openssl.org/docs/man1.1.0/ssl/SSL_CTX_set1_sigalgs_list.html + + :param str sigalgs: Example "RSA+SHA256", "ECDSA+SHA256", ... + :return: 1 for success and 0 for failure + ''' + retVal = SSL_CTX_set1_sigalgs_list(self._ctx, sigalgs) + return retVal + + def set_curves(self, curves): + u''' Set supported curves by name, nid or nist. + + :param str | tuple(int) curves: Example "secp384r1:secp256k1", (715, 714), "P-384", "K-409:B-409:K-571", ... + :return: 1 for success and 0 for failure + ''' + retVal = None + if isinstance(curves, str): + retVal = SSL_CTX_set1_curves_list(self._ctx, curves) + elif isinstance(curves, tuple): + retVal = SSL_CTX_set1_curves(self._ctx, curves, len(curves)) + return retVal + + @staticmethod + def get_ec_nist2nid(nist): + if not isinstance(nist, tuple): + nist = nist.split(":") + nid = tuple(EC_curve_nist2nid(x) for x in nist) + return nid + + @staticmethod + def get_ec_nid2nist(nid): + if not isinstance(nid, tuple): + nid = (nid, ) + nist = ":".join([EC_curve_nid2nist(x) for x in nid]) + return nist + + @staticmethod + def get_ec_available(bAsName=True): + curves = get_elliptic_curves() + return sorted([x.name for x in curves] if bAsName else [x.nid for x in curves]) + + def set_ecdh_curve(self, curve_name=None): + u''' Select a curve to use for ECDH(E) key exchange or set it to auto mode + + Used for server only! + + s.a. openssl.exe ecparam -list_curves + + :param None | str curve_name: None = Auto-mode, "secp256k1", "secp384r1", ... + :return: 1 for success and 0 for failure + ''' + if curve_name: + retVal = SSL_CTX_set_ecdh_auto(self._ctx, 0) + avail_curves = get_elliptic_curves() + key = [curve for curve in avail_curves if curve.name == curve_name][0].to_EC_KEY() + retVal &= SSL_CTX_set_tmp_ecdh(self._ctx, key) + else: + retVal = SSL_CTX_set_ecdh_auto(self._ctx, 1) + return retVal + + def build_cert_chain(self, flags=SSL_BUILD_CHAIN_FLAG_NONE): + u''' + Used for server side only! + + :param flags: + :return: 1 for success and 0 for failure + ''' + retVal = SSL_CTX_build_cert_chain(self._ctx, flags) + return retVal + + def set_ssl_logging(self, enable=False, func=_ssl_logging_cb): + u''' Enable or disable SSL logging + + :param True | False enable: Enable or disable SSL logging + :param func: Callback function for logging + ''' + if enable: + SSL_CTX_set_info_callback(self._ctx, func) + else: + SSL_CTX_set_info_callback(self._ctx, 0) + + +class SSL(object): + + def __init__(self, ssl): + self._ssl = ssl + + def set_mtu(self, mtu=None): + if mtu: + SSL_set_options(self._ssl, SSL_OP_NO_QUERY_MTU) + SSL_set_mtu(self._ssl, mtu) + else: + SSL_clear_options(self._ssl, SSL_OP_NO_QUERY_MTU) + + def set_link_mtu(self, mtu=None): + if mtu: + SSL_set_options(self._ssl, SSL_OP_NO_QUERY_MTU) + DTLS_set_link_mtu(self._ssl, mtu) + else: + SSL_clear_options(self._ssl, SSL_OP_NO_QUERY_MTU) + + class SSLConnection(object): """DTLS peer association @@ -149,7 +323,13 @@ class SSLConnection(object): else: self._rsock = rsock self._rbio = _BIO(BIO_new_dgram(self._rsock.fileno(), BIO_NOCLOSE)) - self._ctx = _CTX(SSL_CTX_new(DTLSv1_server_method())) + server_method = DTLS_server_method + if self._ssl_version == PROTOCOL_DTLSv1_2: + server_method = DTLSv1_2_server_method + elif self._ssl_version == PROTOCOL_DTLSv1: + server_method = DTLSv1_server_method + self._ctx = _CTX(SSL_CTX_new(server_method())) + self._intf_ssl_ctx = SSLContext(self._ctx.value) SSL_CTX_set_session_cache_mode(self._ctx.value, SSL_SESS_CACHE_OFF) if self._cert_reqs == CERT_NONE: verify_mode = SSL_VERIFY_NONE @@ -169,6 +349,7 @@ class SSLConnection(object): _CallbackProxy(self._generate_cookie_cb), _CallbackProxy(self._verify_cookie_cb)) self._ssl = _SSL(SSL_new(self._ctx.value)) + self._intf_ssl = SSL(self._ssl.value) SSL_set_accept_state(self._ssl.value) if peer_address and self._do_handshake_on_connect: return lambda: self.do_handshake() @@ -179,13 +360,20 @@ class SSLConnection(object): self._wbio = _BIO(BIO_new_dgram(self._sock.fileno(), BIO_NOCLOSE)) self._rbio = self._wbio - self._ctx = _CTX(SSL_CTX_new(DTLSv1_client_method())) + client_method = DTLSv1_2_client_method # no "any" exists, therefore use v1_2 (highest possible) + if self._ssl_version == PROTOCOL_DTLSv1_2: + client_method = DTLSv1_2_client_method + elif self._ssl_version == PROTOCOL_DTLSv1: + client_method = DTLSv1_client_method + self._ctx = _CTX(SSL_CTX_new(client_method())) + self._intf_ssl_ctx = SSLContext(self._ctx.value) 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)) + self._intf_ssl = SSL(self._ssl.value) SSL_set_connect_state(self._ssl.value) if peer_address: return lambda: self.connect(peer_address) @@ -208,6 +396,8 @@ class SSLConnection(object): SSL_CTX_set_cipher_list(self._ctx.value, self._ciphers) except openssl_error() as err: raise_ssl_error(ERR_NO_CIPHER, err) + if self._user_config_ssl_ctx: + self._user_config_ssl_ctx(self._intf_ssl_ctx) def _copy_server(self): source = self._sock @@ -233,7 +423,10 @@ class SSLConnection(object): BIO_dgram_set_connected(self._wbio.value, source._pending_peer_address) source._ssl = _SSL(SSL_new(self._ctx.value)) + self._intf_ssl = SSL(source._ssl.value) SSL_set_accept_state(source._ssl.value) + if self._user_config_ssl: + self._user_config_ssl(self._intf_ssl) source._rbio = new_source_rbio source._wbio = new_source_wbio SSL_set_bio(source._ssl.value, @@ -252,7 +445,10 @@ class SSLConnection(object): self._rbio = _BIO(BIO_new_dgram(self._rsock.fileno(), BIO_NOCLOSE)) BIO_dgram_set_peer(self._wbio.value, source._peer_address) self._ssl = _SSL(SSL_new(self._ctx.value)) + self._intf_ssl = SSL(self._ssl.value) SSL_set_accept_state(self._ssl.value) + if self._user_config_ssl: + self._user_config_ssl(self._intf_ssl) if self._do_handshake_on_connect: return lambda: self.do_handshake() @@ -310,9 +506,11 @@ class SSLConnection(object): def __init__(self, sock, keyfile=None, certfile=None, server_side=False, cert_reqs=CERT_NONE, - ssl_version=PROTOCOL_DTLSv1, ca_certs=None, + ssl_version=PROTOCOL_DTLS, ca_certs=None, do_handshake_on_connect=True, - suppress_ragged_eofs=True, ciphers=None): + suppress_ragged_eofs=True, ciphers=None, + cb_user_config_ssl_ctx=None, + cb_user_config_ssl=None): """Constructor Arguments: @@ -334,6 +532,7 @@ class SSLConnection(object): self._keyfile = keyfile self._certfile = certfile self._cert_reqs = cert_reqs + self._ssl_version = ssl_version self._ca_certs = ca_certs self._do_handshake_on_connect = do_handshake_on_connect self._suppress_ragged_eofs = suppress_ragged_eofs @@ -341,6 +540,11 @@ class SSLConnection(object): self._handshake_done = False self._wbio_nb = self._rbio_nb = False + self._user_config_ssl_ctx = cb_user_config_ssl_ctx + self._intf_ssl_ctx = None + self._user_config_ssl = cb_user_config_ssl + self._intf_ssl = None + if isinstance(sock, SSLConnection): post_init = self._copy_server() elif isinstance(sock, _UnwrappedSocket): @@ -355,12 +559,19 @@ class SSLConnection(object): else: post_init = self._init_client(peer_address) + if self._user_config_ssl: + self._user_config_ssl(self._intf_ssl) + + if sys.platform.startswith('win') and \ + not (SSL_get_options(self._ssl.value) & SSL_OP_NO_QUERY_MTU): + SSL_set_options(self._ssl.value, SSL_OP_NO_QUERY_MTU) + DTLS_set_link_mtu(self._ssl.value, 576) + SSL_set_bio(self._ssl.value, self._rbio.value, self._wbio.value) self._rbio.disown() self._wbio.disown() if post_init: post_init() - def get_socket(self, inbound): """Retrieve a socket used by this connection @@ -430,9 +641,15 @@ class SSLConnection(object): # 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_WRONG_VERSION_NUMBER: + _logger.debug("Wrong version number; aborting handshake") + raise elif err.errqueue and err.errqueue[0][0] == ERR_COOKIE_MISMATCH: _logger.debug("Mismatching cookie received; aborting handshake") - return + raise + elif err.errqueue and err.errqueue[0][0] == ERR_NO_SHARED_CIPHER: + _logger.debug("No shared cipher; aborting handshake") + raise _logger.exception("Unexpected error in DTLSv1_listen") raise finally: @@ -462,9 +679,11 @@ class SSLConnection(object): _logger.debug("Accept returning without connection") return new_conn = SSLConnection(self, self._keyfile, self._certfile, True, - self._cert_reqs, PROTOCOL_DTLSv1, + self._cert_reqs, self._ssl_version, self._ca_certs, self._do_handshake_on_connect, - self._suppress_ragged_eofs, self._ciphers) + self._suppress_ragged_eofs, self._ciphers, + cb_user_config_ssl_ctx=self._user_config_ssl_ctx, + cb_user_config_ssl=self._user_config_ssl) new_peer = self._pending_peer_address self._pending_peer_address = None if self._do_handshake_on_connect: @@ -525,8 +744,13 @@ class SSLConnection(object): string containing read bytes """ - return self._wrap_socket_library_call( - lambda: SSL_read(self._ssl.value, len, buffer), ERR_READ_TIMEOUT) + try: + return self._wrap_socket_library_call( + lambda: SSL_read(self._ssl.value, len, buffer), ERR_READ_TIMEOUT) + except openssl_error() as err: + if err.ssl_error == SSL_ERROR_SYSCALL and err.result == -1: + raise_ssl_error(ERR_PORT_UNREACHABLE, err) + raise def write(self, data): """Write data to connection @@ -540,8 +764,16 @@ class SSLConnection(object): number of bytes actually transmitted """ - return self._wrap_socket_library_call( - lambda: SSL_write(self._ssl.value, data), ERR_WRITE_TIMEOUT) + try: + ret = self._wrap_socket_library_call( + lambda: SSL_write(self._ssl.value, data), ERR_WRITE_TIMEOUT) + except openssl_error() as err: + if err.ssl_error == SSL_ERROR_SYSCALL and err.result == -1: + raise_ssl_error(ERR_PORT_UNREACHABLE, err) + raise + if ret: + self._handshake_done = True + return ret def shutdown(self): """Shut down the DTLS connection @@ -605,6 +837,21 @@ class SSLConnection(object): peer_certificate = getpeercert # compatibility with _ssl call interface + def getpeercertchain(self, binary_form=False): + try: + stack, num, certs = SSL_get_peer_cert_chain(self._ssl.value) + except openssl_error(): + return + + peer_cert_chain = [_Rsrc(cert) for cert in certs] + ret = [] + if binary_form: + ret = [i2d_X509(x.value) for x in peer_cert_chain] + elif len(peer_cert_chain): + ret = [decode_cert(x) for x in peer_cert_chain] + + return ret + def cipher(self): """Retrieve information about the current cipher diff --git a/dtls/test/certs/ca-cert_ec.pem b/dtls/test/certs/ca-cert_ec.pem new file mode 100644 index 0000000..b215e54 --- /dev/null +++ b/dtls/test/certs/ca-cert_ec.pem @@ -0,0 +1,11 @@ +-----BEGIN CERTIFICATE----- +MIIBgzCCASoCCQDdMwvUA/R3lzAKBggqhkjOPQQDAzBKMQswCQYDVQQGEwJVUzET +MBEGA1UECAwKV2FzaGluZ3RvbjETMBEGA1UECgwKUmF5IENBIEluYzERMA8GA1UE +AwwIUmF5Q0FJbmMwHhcNMTcwMzA3MDgzNjU3WhcNMjcwMzA1MDgzNjU3WjBKMQsw +CQYDVQQGEwJVUzETMBEGA1UECAwKV2FzaGluZ3RvbjETMBEGA1UECgwKUmF5IENB +IEluYzERMA8GA1UEAwwIUmF5Q0FJbmMwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNC +AASD4xiQkPryjEwUl/GYeGu1CSA3UC6BUY3TiGED3zrC5Bn/POaVVn9GGOQMZUFi +rCkuTgfg/qeIzTrTFndiR5C/MAoGCCqGSM49BAMDA0cAMEQCIHpd9qMvZZV6iaB5 +HrmlyfmhIuLBxDQra20Uxl2Y8N64AiAmPKqwPPp7z6IT2AzAXyHCPoVxwWA0NfGx +nmXoYpDFlw== +-----END CERTIFICATE----- diff --git a/dtls/test/certs/keycert_ec.pem b/dtls/test/certs/keycert_ec.pem new file mode 100644 index 0000000..e76947d --- /dev/null +++ b/dtls/test/certs/keycert_ec.pem @@ -0,0 +1,19 @@ +-----BEGIN EC PARAMETERS----- +BggqhkjOPQMBBw== +-----END EC PARAMETERS----- +-----BEGIN EC PRIVATE KEY----- +MHcCAQEEIEMWCku4TqKwrQdeECm5LQPCBnr7+cqE4InlRYeObLOxoAoGCCqGSM49 +AwEHoUQDQgAEgroFe2fym1V7E3zr/zjuJixpyAjwfig+UTsxxm/04IvXzk2jQCQC +TgbDVohJ8dgh4iEENZv2axWye7XCBzbftQ== +-----END EC PRIVATE KEY----- +-----BEGIN CERTIFICATE----- +MIIBhjCCASwCCQCZ3L2TA/e93zAKBggqhkjOPQQDAzBKMQswCQYDVQQGEwJVUzET +MBEGA1UECAwKV2FzaGluZ3RvbjETMBEGA1UECgwKUmF5IENBIEluYzERMA8GA1UE +AwwIUmF5Q0FJbmMwHhcNMTcwMzA3MDgzNjU4WhcNMjcwMzA1MDgzNjU4WjBMMQsw +CQYDVQQGEwJVUzETMBEGA1UECAwKV2FzaGluZ3RvbjEUMBIGA1UECgwLUmF5IFNy +diBJbmMxEjAQBgNVBAMMCVJheVNydkluYzBZMBMGByqGSM49AgEGCCqGSM49AwEH +A0IABIK6BXtn8ptVexN86/847iYsacgI8H4oPlE7McZv9OCL185No0AkAk4Gw1aI +SfHYIeIhBDWb9msVsnu1wgc237UwCgYIKoZIzj0EAwMDSAAwRQIhAK4caAt0QSTz +A1WYlrEAA2AH181P7USiXkqQ5qRyoWQNAiBm3vKaoB+0p4B98HeI+h5V/7loomQg +sW3uB0zEuJyqIQ== +-----END CERTIFICATE----- diff --git a/dtls/test/certs/server-cert_ec.pem b/dtls/test/certs/server-cert_ec.pem new file mode 100644 index 0000000..a9a76e5 --- /dev/null +++ b/dtls/test/certs/server-cert_ec.pem @@ -0,0 +1,11 @@ +-----BEGIN CERTIFICATE----- +MIIBhjCCASwCCQCZ3L2TA/e93zAKBggqhkjOPQQDAzBKMQswCQYDVQQGEwJVUzET +MBEGA1UECAwKV2FzaGluZ3RvbjETMBEGA1UECgwKUmF5IENBIEluYzERMA8GA1UE +AwwIUmF5Q0FJbmMwHhcNMTcwMzA3MDgzNjU4WhcNMjcwMzA1MDgzNjU4WjBMMQsw +CQYDVQQGEwJVUzETMBEGA1UECAwKV2FzaGluZ3RvbjEUMBIGA1UECgwLUmF5IFNy +diBJbmMxEjAQBgNVBAMMCVJheVNydkluYzBZMBMGByqGSM49AgEGCCqGSM49AwEH +A0IABIK6BXtn8ptVexN86/847iYsacgI8H4oPlE7McZv9OCL185No0AkAk4Gw1aI +SfHYIeIhBDWb9msVsnu1wgc237UwCgYIKoZIzj0EAwMDSAAwRQIhAK4caAt0QSTz +A1WYlrEAA2AH181P7USiXkqQ5qRyoWQNAiBm3vKaoB+0p4B98HeI+h5V/7loomQg +sW3uB0zEuJyqIQ== +-----END CERTIFICATE----- diff --git a/dtls/test/echo_seq.py b/dtls/test/echo_seq.py index 52e4bce..3be2e35 100644 --- a/dtls/test/echo_seq.py +++ b/dtls/test/echo_seq.py @@ -39,6 +39,7 @@ 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)) @@ -46,8 +47,8 @@ def main(): 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"), + keyfile=path.join(cert_path, "keycert.pem"), + certfile=path.join(cert_path, "keycert.pem"), server_side=True, ca_certs=path.join(cert_path, "ca-cert.pem"), do_handshake_on_connect=False) @@ -76,7 +77,7 @@ def main(): try: conn.do_handshake() except SSLError as err: - if str(err).startswith("504:"): + if err.errno == 504: continue raise print "Completed handshaking with peer" @@ -92,7 +93,7 @@ def main(): try: message = conn.read() except SSLError as err: - if str(err).startswith("502:"): + if err.errno == 502: continue if err.args[0] == SSL_ERROR_ZERO_RETURN: break @@ -111,7 +112,7 @@ def main(): s = conn.shutdown() s.shutdown(socket.SHUT_RDWR) except SSLError as err: - if str(err).startswith("502:"): + if err.errno == 502: continue raise break diff --git a/dtls/test/makecerts_ec.bat b/dtls/test/makecerts_ec.bat new file mode 100644 index 0000000..5d5eb8b --- /dev/null +++ b/dtls/test/makecerts_ec.bat @@ -0,0 +1,24 @@ +@echo off +set RANDFILE=.rnd + + +rem # Generate self-signed certificate for the certificate authority +echo Generating CA... +openssl ecparam -name prime256v1 -genkey -out tmp_ca_ec.key +openssl req -config "openssl_ca.cnf" -x509 -new -SHA384 -nodes -key tmp_ca_ec.key -days 3650 -out ca-cert_ec.pem + +rem # Generate a certificate request +echo Generating certificate request... +openssl ecparam -name prime256v1 -genkey -out tmp_server_ec.key +openssl req -config "openssl_server.cnf" -new -SHA384 -nodes -key tmp_server_ec.key -out tmp_server_ec.req + +rem # Sign the request with the certificate authority's certificate created above +echo Signing certificate request... +openssl req -in tmp_server_ec.req -noout -text +openssl x509 -req -SHA384 -days 3650 -in tmp_server_ec.req -CA ca-cert_ec.pem -CAkey tmp_ca_ec.key -CAcreateserial -out server-cert_ec.pem + +rem # Build pem file with private and public keys, ready for unprompted server use +cat tmp_server_ec.key server-cert_ec.pem > keycert_ec.pem + +rem # Clean up +rm tmp_ca_ec.key tmp_server_ec.key tmp_server_ec.req ca-cert_ec.srl diff --git a/dtls/test/openssl_ca.cnf b/dtls/test/openssl_ca.cnf index 365ab27..3ded7e5 100644 --- a/dtls/test/openssl_ca.cnf +++ b/dtls/test/openssl_ca.cnf @@ -1,3 +1,4 @@ +HOME = . RANDFILE = $ENV::HOME/.rnd [ req ] diff --git a/dtls/test/openssl_server.cnf b/dtls/test/openssl_server.cnf index 2d2e749..33aac27 100644 --- a/dtls/test/openssl_server.cnf +++ b/dtls/test/openssl_server.cnf @@ -1,3 +1,4 @@ +HOME = . RANDFILE = $ENV::HOME/.rnd [ req ] diff --git a/dtls/test/simple_client.py b/dtls/test/simple_client.py new file mode 100644 index 0000000..c243f20 --- /dev/null +++ b/dtls/test/simple_client.py @@ -0,0 +1,15 @@ +from os import path +import ssl +from socket import socket, AF_INET, SOCK_DGRAM, SHUT_RDWR +from logging import basicConfig, DEBUG +basicConfig(level=DEBUG) # set now for dtls import code +from dtls import do_patch +do_patch() + +cert_path = path.join(path.abspath(path.dirname(__file__)), "certs") +sock = ssl.wrap_socket(socket(AF_INET, SOCK_DGRAM), cert_reqs=ssl.CERT_REQUIRED, ca_certs=path.join(cert_path, "ca-cert.pem")) +sock.connect(('localhost', 28000)) +sock.send('Hi there') +print sock.recv() +sock.unwrap() +sock.shutdown(SHUT_RDWR) diff --git a/dtls/test/unit.py b/dtls/test/unit.py index b7e28a5..3e21701 100644 --- a/dtls/test/unit.py +++ b/dtls/test/unit.py @@ -80,6 +80,8 @@ class BasicSocketTests(unittest.TestCase): ssl.PROTOCOL_SSLv23 ssl.PROTOCOL_TLSv1 ssl.PROTOCOL_DTLSv1 # added + ssl.PROTOCOL_DTLSv1_2 # added + ssl.PROTOCOL_DTLS # added ssl.CERT_NONE ssl.CERT_OPTIONAL ssl.CERT_REQUIRED @@ -92,8 +94,8 @@ class BasicSocketTests(unittest.TestCase): self.assertIsInstance(t, tuple) self.assertIsInstance(s, str) # Some sanity checks follow - # >= 1.0 - self.assertGreaterEqual(n, 0x10000000) + # >= 1.0.2 + self.assertGreaterEqual(n, 0x10002000) # < 2.0 self.assertLess(n, 0x20000000) major, minor, fix, patch, status = t @@ -101,7 +103,7 @@ class BasicSocketTests(unittest.TestCase): self.assertLess(major, 2) self.assertGreaterEqual(minor, 0) self.assertLess(minor, 256) - self.assertGreaterEqual(fix, 0) + self.assertGreaterEqual(fix, 2) self.assertLess(fix, 256) self.assertGreaterEqual(patch, 0) self.assertLessEqual(patch, 26) @@ -300,28 +302,30 @@ class NetworkedTests(unittest.TestCase): "to establish session.\n") % count) def test_get_server_certificate(self): - with test_support.transient_internet() as remote: - pem = ssl.get_server_certificate(remote, ssl.PROTOCOL_DTLSv1) - if not pem: - self.fail("No server certificate!") - - try: + for prot in (ssl.PROTOCOL_DTLSv1, ssl.PROTOCOL_DTLSv1_2, ssl.PROTOCOL_DTLS): + with test_support.transient_internet() as remote: pem = ssl.get_server_certificate(remote, - ssl.PROTOCOL_DTLSv1, - ca_certs=OTHER_CERTFILE) - except ssl.SSLError: - #should fail - pass - else: - self.fail("Got server certificate %s!" % pem) + prot) + if not pem: + self.fail("No server certificate!") - pem = ssl.get_server_certificate(remote, - ssl.PROTOCOL_DTLSv1, - ca_certs=ISSUER_CERTFILE) - if not pem: - self.fail("No server certificate!") - if test_support.verbose: - sys.stdout.write("\nVerified certificate is\n%s\n" % pem) + try: + pem = ssl.get_server_certificate(remote, + prot, + ca_certs=OTHER_CERTFILE) + except ssl.SSLError: + # should fail + pass + else: + self.fail("Got server certificate %s!" % pem) + + pem = ssl.get_server_certificate(remote, + prot, + ca_certs=ISSUER_CERTFILE) + if not pem: + self.fail("No server certificate!") + if test_support.verbose: + sys.stdout.write("\nVerified certificate is\n%s\n" % pem) class ThreadedEchoServer(threading.Thread): @@ -534,6 +538,8 @@ class ThreadedEchoServer(threading.Thread): handler.start() except socket.timeout: pass + except ssl.SSLError: + pass except KeyboardInterrupt: self.stop() self.sock.close() @@ -1039,11 +1045,34 @@ class ThreadedTests(unittest.TestCase): """Connecting to a DTLSv1 server with various client options""" if test_support.verbose: sys.stdout.write("\n") + # server: 1.0 - client: 1.0 -> ok try_protocol_combo(ssl.PROTOCOL_DTLSv1, ssl.PROTOCOL_DTLSv1, True) try_protocol_combo(ssl.PROTOCOL_DTLSv1, ssl.PROTOCOL_DTLSv1, True, ssl.CERT_OPTIONAL) try_protocol_combo(ssl.PROTOCOL_DTLSv1, ssl.PROTOCOL_DTLSv1, True, ssl.CERT_REQUIRED) + # server: any - client: 1.0 and 1.2(any) -> ok + try_protocol_combo(ssl.PROTOCOL_DTLS, ssl.PROTOCOL_DTLSv1, True) + try_protocol_combo(ssl.PROTOCOL_DTLS, ssl.PROTOCOL_DTLSv1, True, + ssl.CERT_REQUIRED) + try_protocol_combo(ssl.PROTOCOL_DTLS, ssl.PROTOCOL_DTLSv1_2, True) + try_protocol_combo(ssl.PROTOCOL_DTLS, ssl.PROTOCOL_DTLSv1_2, True, + ssl.CERT_REQUIRED) + try_protocol_combo(ssl.PROTOCOL_DTLS, ssl.PROTOCOL_DTLS, True) + try_protocol_combo(ssl.PROTOCOL_DTLS, ssl.PROTOCOL_DTLS, True, + ssl.CERT_REQUIRED) + # server: 1.0 - client: 1.2 -> fail + try_protocol_combo(ssl.PROTOCOL_DTLSv1, ssl.PROTOCOL_DTLSv1_2, False) + try_protocol_combo(ssl.PROTOCOL_DTLSv1, ssl.PROTOCOL_DTLSv1_2, False, + ssl.CERT_REQUIRED) + # server: 1.2 - client: 1.0 -> fail + try_protocol_combo(ssl.PROTOCOL_DTLSv1_2, ssl.PROTOCOL_DTLSv1, False) + try_protocol_combo(ssl.PROTOCOL_DTLSv1_2, ssl.PROTOCOL_DTLSv1, False, + ssl.CERT_REQUIRED) + # server: 1.2 - client: 1.2 -> ok + try_protocol_combo(ssl.PROTOCOL_DTLSv1_2, ssl.PROTOCOL_DTLSv1_2, True) + try_protocol_combo(ssl.PROTOCOL_DTLSv1_2, ssl.PROTOCOL_DTLSv1_2, True, + ssl.CERT_REQUIRED) def test_starttls(self): """Switching from clear text to encrypted and back again.""" @@ -1062,7 +1091,7 @@ class ThreadedTests(unittest.TestCase): # try to connect wrapped = False try: - s = ssl.wrap_socket(socket.socket(AF_INET4_6, socket.SOCK_DGRAM)) + s = ssl.wrap_socket(socket.socket(AF_INET4_6, socket.SOCK_DGRAM), ssl_version=ssl.PROTOCOL_DTLSv1) s.connect((HOST, server.port)) s = s.unwrap() if test_support.verbose: diff --git a/dtls/test/unit_wrapper.py b/dtls/test/unit_wrapper.py new file mode 100644 index 0000000..ff55500 --- /dev/null +++ b/dtls/test/unit_wrapper.py @@ -0,0 +1,648 @@ +# -*- coding: utf-8 -*- + +# Test the support for DTLS through the SSL module. Adapted from the Python +# standard library's test_ssl.py regression test module by Björn Freise. + +import unittest +import threading +import sys +import socket +import os +import pprint + +from logging import basicConfig, DEBUG, getLogger +# basicConfig(level=DEBUG, format="%(asctime)s - %(threadName)-10s - %(name)s - %(levelname)s - %(message)s") +_logger = getLogger(__name__) + +import ssl +from dtls.wrapper import DtlsSocket + + +HOST = "localhost" +CHATTY = True +CHATTY_CLIENT = True + + +class ThreadedEchoServer(threading.Thread): + + def __init__(self, certificate, ssl_version=None, certreqs=None, cacerts=None, + ciphers=None, curves=None, sigalgs=None, + mtu=None, server_key_exchange_curve=None, server_cert_options=None, + chatty=True): + + if ssl_version is None: + ssl_version = ssl.PROTOCOL_DTLSv1 + if certreqs is None: + certreqs = ssl.CERT_NONE + + self.certificate = certificate + self.protocol = ssl_version + self.certreqs = certreqs + self.cacerts = cacerts + self.ciphers = ciphers + self.curves = curves + self.sigalgs = sigalgs + self.mtu = mtu + self.server_key_exchange_curve = server_key_exchange_curve + self.server_cert_options = server_cert_options + self.chatty = chatty + + self.flag = None + + self.sock = DtlsSocket(socket.socket(socket.AF_INET, socket.SOCK_DGRAM), + keyfile=self.certificate, + certfile=self.certificate, + server_side=True, + cert_reqs=self.certreqs, + ssl_version=self.protocol, + ca_certs=self.cacerts, + ciphers=self.ciphers, + curves=self.curves, + sigalgs=self.sigalgs, + user_mtu=self.mtu, + server_key_exchange_curve=self.server_key_exchange_curve, + server_cert_options=self.server_cert_options) + + if self.chatty: + sys.stdout.write(' server: wrapped server socket as %s\n' % str(self.sock)) + self.sock.bind((HOST, 0)) + self.port = self.sock.getsockname()[1] + self.active = False + threading.Thread.__init__(self) + self.daemon = True + + def start(self, flag=None): + self.flag = flag + self.starter = threading.current_thread().ident + threading.Thread.start(self) + + def run(self): + self.sock.settimeout(0.05) + self.sock.listen(0) + self.active = True + if self.flag: + # signal an event + self.flag.set() + while self.active: + try: + acc_ret = self.sock.recvfrom(4096) + if acc_ret: + newdata, connaddr = acc_ret + if self.chatty: + sys.stdout.write(' server: new data from ' + str(connaddr) + '\n') + self.sock.sendto(newdata.lower(), connaddr) + except socket.timeout: + pass + except KeyboardInterrupt: + self.stop() + except Exception as e: + if self.chatty: + sys.stdout.write(' server: error ' + str(e) + '\n') + pass + if self.chatty: + sys.stdout.write(' server: closing socket as %s\n' % str(self.sock)) + self.sock.close() + + def stop(self): + self.active = False + if self.starter != threading.current_thread().ident: + return + self.join() # don't allow spawning new handlers after we've checked + + +CERTFILE = os.path.join(os.path.dirname(__file__) or os.curdir, "certs", "keycert.pem") +CERTFILE_EC = os.path.join(os.path.dirname(__file__) or os.curdir, "certs", "keycert_ec.pem") +ISSUER_CERTFILE = os.path.join(os.path.dirname(__file__) or os.curdir, "certs", "ca-cert.pem") +ISSUER_CERTFILE_EC = os.path.join(os.path.dirname(__file__) or os.curdir, "certs", "ca-cert_ec.pem") + +# certfile, protocol, certreqs, cacertsfile, +# ciphers=None, curves=None, sigalgs=None, +tests = [ + {'testcase': + {'name': 'standard dtls v1', + 'desc': 'Standard DTLS v1 test with out-of-the box configuration and RSA certificate', + 'start_server': True}, + 'input': + {'certfile': CERTFILE, + 'protocol': ssl.PROTOCOL_DTLSv1, + 'certreqs': None, + 'cacertsfile': ISSUER_CERTFILE, + 'ciphers': None, + 'curves': None, + 'sigalgs': None, + 'client_certfile': None, + 'client_protocol': ssl.PROTOCOL_DTLSv1, + 'client_certreqs': ssl.CERT_REQUIRED, + 'client_cacertsfile': ISSUER_CERTFILE, + 'client_ciphers': None, + 'client_curves': None, + 'client_sigalgs': None}, + 'result': + {'ret_success': True, + 'error_code': None, + 'exception': None}}, + {'testcase': + {'name': 'standard dtls v1_2', + 'desc': 'Standard DTLS v1_2 test with out-of-the box configuration and ECDSA certificate', + 'start_server': True}, + 'input': + {'certfile': CERTFILE_EC, + 'protocol': ssl.PROTOCOL_DTLSv1_2, + 'certreqs': None, + 'cacertsfile': ISSUER_CERTFILE_EC, + 'ciphers': None, + 'curves': None, + 'sigalgs': None, + 'client_certfile': None, + 'client_protocol': ssl.PROTOCOL_DTLSv1_2, + 'client_certreqs': ssl.CERT_REQUIRED, + 'client_cacertsfile': ISSUER_CERTFILE_EC, + 'client_ciphers': None, + 'client_curves': None, + 'client_sigalgs': None}, + 'result': + {'ret_success': True, + 'error_code': None, + 'exception': None}}, + {'testcase': + {'name': 'protocol version mismatch', + 'desc': 'Client and server have different protocol versions', + 'start_server': True}, + 'input': + {'certfile': CERTFILE, + 'protocol': ssl.PROTOCOL_DTLSv1, + 'certreqs': None, + 'cacertsfile': ISSUER_CERTFILE, + 'ciphers': None, + 'curves': None, + 'sigalgs': None, + 'client_certfile': None, + 'client_protocol': ssl.PROTOCOL_DTLSv1_2, + 'client_certreqs': ssl.CERT_REQUIRED, + 'client_cacertsfile': ISSUER_CERTFILE, + 'client_ciphers': None, + 'client_curves': None, + 'client_sigalgs': None}, + 'result': + {'ret_success': False, + 'error_code': ssl.ERR_WRONG_SSL_VERSION, + 'exception': None}}, + {'testcase': + {'name': 'certificate verify fails', + 'desc': 'Server certificate cannot be verified by client', + 'start_server': True}, + 'input': + {'certfile': CERTFILE_EC, + 'protocol': ssl.PROTOCOL_DTLSv1_2, + 'certreqs': None, + 'cacertsfile': ISSUER_CERTFILE_EC, + 'ciphers': None, + 'curves': None, + 'sigalgs': None, + 'client_certfile': None, + 'client_protocol': ssl.PROTOCOL_DTLSv1_2, + 'client_certreqs': ssl.CERT_REQUIRED, + 'client_cacertsfile': ISSUER_CERTFILE, + 'client_ciphers': None, + 'client_curves': None, + 'client_sigalgs': None}, + 'result': + {'ret_success': False, + 'error_code': ssl.ERR_CERTIFICATE_VERIFY_FAILED, + 'exception': None}}, + {'testcase': + {'name': 'no matching curve', + 'desc': 'Client doesn\'t support curve used by server ECDSA certificate', + 'start_server': True}, + 'input': + {'certfile': CERTFILE_EC, + 'protocol': ssl.PROTOCOL_DTLSv1_2, + 'certreqs': None, + 'cacertsfile': ISSUER_CERTFILE_EC, + 'ciphers': None, + 'curves': None, + 'sigalgs': None, + 'client_certfile': None, + 'client_protocol': ssl.PROTOCOL_DTLSv1_2, + 'client_certreqs': ssl.CERT_REQUIRED, + 'client_cacertsfile': ISSUER_CERTFILE_EC, + 'client_ciphers': None, + 'client_curves': 'secp384r1', + 'client_sigalgs': None}, + 'result': + {'ret_success': False, + 'error_code': ssl.ERR_SSL_HANDSHAKE_FAILURE, + 'exception': None}}, + {'testcase': + {'name': 'matching curve', + 'desc': '', + 'start_server': True}, + 'input': + {'certfile': CERTFILE_EC, + 'protocol': ssl.PROTOCOL_DTLSv1_2, + 'certreqs': None, + 'cacertsfile': ISSUER_CERTFILE_EC, + 'ciphers': None, + 'curves': None, + 'sigalgs': None, + 'client_certfile': None, + 'client_protocol': ssl.PROTOCOL_DTLSv1_2, + 'client_certreqs': ssl.CERT_REQUIRED, + 'client_cacertsfile': ISSUER_CERTFILE_EC, + 'client_ciphers': None, + 'client_curves': 'prime256v1', + 'client_sigalgs': None}, + 'result': + {'ret_success': True, + 'error_code': None, + 'exception': None}}, + {'testcase': + {'name': 'no host', + 'desc': 'No server port is listening', + 'start_server': False}, + 'input': + {'certfile': CERTFILE_EC, + 'protocol': ssl.PROTOCOL_DTLSv1_2, + 'certreqs': None, + 'cacertsfile': ISSUER_CERTFILE_EC, + 'ciphers': None, + 'curves': None, + 'sigalgs': None, + 'client_certfile': None, + 'client_protocol': ssl.PROTOCOL_DTLSv1_2, + 'client_certreqs': ssl.CERT_REQUIRED, + 'client_cacertsfile': ISSUER_CERTFILE_EC, + 'client_ciphers': None, + 'client_curves': None, + 'client_sigalgs': None}, + 'result': + {'ret_success': False, + 'error_code': ssl.ERR_PORT_UNREACHABLE, + 'exception': None}}, + {'testcase': + {'name': 'no matching sigalgs', + 'desc': '', + 'start_server': True}, + 'input': + {'certfile': CERTFILE_EC, + 'protocol': ssl.PROTOCOL_DTLSv1_2, + 'certreqs': None, + 'cacertsfile': ISSUER_CERTFILE_EC, + 'ciphers': None, + 'curves': None, + 'sigalgs': None, + 'client_certfile': None, + 'client_protocol': ssl.PROTOCOL_DTLSv1_2, + 'client_certreqs': ssl.CERT_REQUIRED, + 'client_cacertsfile': ISSUER_CERTFILE_EC, + 'client_ciphers': None, + 'client_curves': None, + 'client_sigalgs': "RSA+SHA256"}, + 'result': + {'ret_success': False, + 'error_code': ssl.ERR_SSL_HANDSHAKE_FAILURE, + 'exception': None}}, + {'testcase': + {'name': 'matching sigalgs', + 'desc': '', + 'start_server': True}, + 'input': + {'certfile': CERTFILE_EC, + 'protocol': ssl.PROTOCOL_DTLSv1_2, + 'certreqs': None, + 'cacertsfile': ISSUER_CERTFILE_EC, + 'ciphers': None, + 'curves': None, + 'sigalgs': None, + 'client_certfile': None, + 'client_protocol': ssl.PROTOCOL_DTLSv1_2, + 'client_certreqs': ssl.CERT_REQUIRED, + 'client_cacertsfile': ISSUER_CERTFILE_EC, + 'client_ciphers': None, + 'client_curves': None, + 'client_sigalgs': "ECDSA+SHA256"}, + 'result': + {'ret_success': True, + 'error_code': None, + 'exception': None}}, + {'testcase': + {'name': 'no matching cipher', + 'desc': 'Server using a ECDSA certificate while client is only able to use RSA encryption', + 'start_server': True}, + 'input': + {'certfile': CERTFILE_EC, + 'protocol': ssl.PROTOCOL_DTLSv1_2, + 'certreqs': None, + 'cacertsfile': ISSUER_CERTFILE_EC, + 'ciphers': None, + 'curves': None, + 'sigalgs': None, + 'client_certfile': None, + 'client_protocol': ssl.PROTOCOL_DTLSv1_2, + 'client_certreqs': ssl.CERT_REQUIRED, + 'client_cacertsfile': ISSUER_CERTFILE_EC, + 'client_ciphers': "AES256-SHA", + 'client_curves': None, + 'client_sigalgs': None}, + 'result': + {'ret_success': False, + 'error_code': ssl.ERR_SSL_HANDSHAKE_FAILURE, + 'exception': None}}, + {'testcase': + {'name': 'matching cipher', + 'desc': '', + 'start_server': True}, + 'input': + {'certfile': CERTFILE_EC, + 'protocol': ssl.PROTOCOL_DTLSv1_2, + 'certreqs': None, + 'cacertsfile': ISSUER_CERTFILE_EC, + 'ciphers': None, + 'curves': None, + 'sigalgs': None, + 'client_certfile': None, + 'client_protocol': ssl.PROTOCOL_DTLSv1_2, + 'client_certreqs': ssl.CERT_REQUIRED, + 'client_cacertsfile': ISSUER_CERTFILE_EC, + 'client_ciphers': "ECDHE-ECDSA-AES256-SHA", + 'client_curves': None, + 'client_sigalgs': None}, + 'result': + {'ret_success': True, + 'error_code': None, + 'exception': None}}, +] + + +def params_test(start_server, certfile, protocol, certreqs, cacertsfile, + client_certfile=None, client_protocol=None, client_certreqs=None, client_cacertsfile=None, + ciphers=None, curves=None, sigalgs=None, + client_ciphers=None, client_curves=None, client_sigalgs=None, + mtu=None, server_key_exchange_curve=None, server_cert_options=None, + indata="FOO\n", chatty=False, connectionchatty=False): + """ + Launch a server, connect a client to it and try various reads + and writes. + """ + server = ThreadedEchoServer(certfile, + ssl_version=protocol, + certreqs=certreqs, + cacerts=cacertsfile, + ciphers=ciphers, + curves=curves, + sigalgs=sigalgs, + mtu=mtu, + server_key_exchange_curve=server_key_exchange_curve, + server_cert_options=server_cert_options, + chatty=chatty) + # should we really run the server? + if start_server: + flag = threading.Event() + server.start(flag) + # wait for it to start + flag.wait() + else: + server.sock.close() + # try to connect + if client_protocol is None: + client_protocol = protocol + if client_ciphers is None: + client_ciphers = ciphers + if client_curves is None: + client_curves = curves + if client_sigalgs is None: + client_sigalgs = sigalgs + try: + s = DtlsSocket(socket.socket(socket.AF_INET, socket.SOCK_DGRAM), + keyfile=client_certfile, + certfile=client_certfile, + cert_reqs=client_certreqs, + ssl_version=client_protocol, + ca_certs=client_cacertsfile, + ciphers=client_ciphers, + curves=client_curves, + sigalgs=client_sigalgs, + user_mtu=mtu) + s.connect((HOST, server.port)) + if connectionchatty: + sys.stdout.write(" client: sending %s...\n" % (repr(indata))) + s.write(indata) + outdata = s.read() + if connectionchatty: + sys.stdout.write(" client: read %s\n" % repr(outdata)) + if outdata != indata.lower(): + raise AssertionError("bad data <<%s>> (%d) received; expected <<%s>> (%d)\n" + % (outdata[:min(len(outdata), 20)], len(outdata), + indata[:min(len(indata), 20)].lower(), len(indata))) + cert = s.getpeercert() + cipher = s.cipher() + if connectionchatty: + sys.stdout.write("cert:\n" + pprint.pformat(cert) + "\n") + sys.stdout.write("cipher:\n" + pprint.pformat(cipher) + "\n") + if connectionchatty: + sys.stdout.write(" client: closing connection.\n") + try: + s.close() + except Exception as e: + if connectionchatty: + sys.stdout.write(" client: error closing connection %s...\n" % (repr(e))) + pass + except Exception as e: + if connectionchatty: + sys.stdout.write(" client: aborting with exception %s...\n" % (repr(e))) + return False, e + finally: + if start_server: + server.stop() + return True, None + + +class TestSequenceMeta(type): + def __new__(mcs, name, bases, dict): + + def gen_test(_case, _input, _result): + def test(self): + try: + if CHATTY or CHATTY_CLIENT: + sys.stdout.write("\nTestcase: %s\n" % _case['name']) + ret, e = params_test(_case['start_server'], chatty=CHATTY, connectionchatty=CHATTY_CLIENT, **_input) + if _result['ret_success']: + self.assertEqual(ret, _result['ret_success']) + else: + try: + last_error = e.errqueue[-1][0] + except: + try: + last_error = e.errno + except: + last_error = None + self.assertEqual(last_error, _result['error_code']) + except Exception as e: + raise + return test + + for testcase in tests: + _case, _input, _result = testcase.itervalues() + test_name = "test_%s" % _case['name'].lower().replace(' ', '_') + dict[test_name] = gen_test(_case, _input, _result) + + return type.__new__(mcs, name, bases, dict) + + +class WrapperTests(unittest.TestCase): + __metaclass__ = TestSequenceMeta + + def test_build_cert_chain(self): + steps = [ssl.SSL_BUILD_CHAIN_FLAG_NONE, ssl.SSL_BUILD_CHAIN_FLAG_NO_ROOT] + chatty, connectionchatty = CHATTY, CHATTY_CLIENT + indata = 'FOO' + certs = dict() + + if chatty or connectionchatty: + sys.stdout.write("\nTestcase: test_build_cert_chain\n") + for step in steps: + server = ThreadedEchoServer(certificate=CERTFILE, + ssl_version=ssl.PROTOCOL_DTLSv1_2, + certreqs=ssl.CERT_NONE, + cacerts=ISSUER_CERTFILE, + ciphers=None, + curves=None, + sigalgs=None, + mtu=None, + server_key_exchange_curve=None, + server_cert_options=step, + chatty=chatty) + flag = threading.Event() + server.start(flag) + # wait for it to start + flag.wait() + try: + s = DtlsSocket(socket.socket(socket.AF_INET, socket.SOCK_DGRAM), + keyfile=None, + certfile=None, + cert_reqs=ssl.CERT_REQUIRED, + ssl_version=ssl.PROTOCOL_DTLSv1_2, + ca_certs=ISSUER_CERTFILE, + ciphers=None, + curves=None, + sigalgs=None, + user_mtu=None) + s.connect((HOST, server.port)) + if connectionchatty: + sys.stdout.write(" client: sending %s...\n" % (repr(indata))) + s.write(indata) + outdata = s.read() + if connectionchatty: + sys.stdout.write(" client: read %s\n" % repr(outdata)) + if outdata != indata.lower(): + raise AssertionError("bad data <<%s>> (%d) received; expected <<%s>> (%d)\n" + % (outdata[:min(len(outdata), 20)], len(outdata), + indata[:min(len(indata), 20)].lower(), len(indata))) + # cert = s.getpeercert() + # cipher = s.cipher() + # if connectionchatty: + # sys.stdout.write("cert:\n" + pprint.pformat(cert) + "\n") + # sys.stdout.write("cipher:\n" + pprint.pformat(cipher) + "\n") + certs[step] = s.getpeercertchain() + if connectionchatty: + sys.stdout.write(" client: closing connection.\n") + try: + s.close() + except Exception as e: + if connectionchatty: + sys.stdout.write(" client: error closing connection %s...\n" % (repr(e))) + pass + except Exception as e: + if connectionchatty: + sys.stdout.write(" client: aborting with exception %s...\n" % (repr(e))) + raise + finally: + server.stop() + + if chatty: + sys.stdout.write("certs:\n") + for step in steps: + sys.stdout.write("SSL_CTX_build_cert_chain: %s\n%s\n" % (step, pprint.pformat(certs[step]))) + self.assertNotEqual(certs[steps[0]], certs[steps[1]]) + self.assertEqual(len(certs[steps[0]]) - len(certs[steps[1]]), 1) + + def test_set_ecdh_curve(self): + steps = { + # server, client, result + 'all auto': (None, None, True), # Auto + 'client restricted': (None, "secp256k1:prime256v1", True), # client can handle key curve + 'client too restricted': (None, "secp256k1", False), # client _cannot_ handle key curve + 'client minimum': (None, "prime256v1", True), # client can only handle key curve + 'server restricted': ("secp384r1", None, True), # client can handle key curve + 'server one, client two': ("secp384r1", "prime256v1:secp384r1", True), # client can handle key curve + 'server one, client one': ("secp384r1", "secp384r1", False), # client _cannot_ handle key curve + } + + chatty, connectionchatty = CHATTY, CHATTY_CLIENT + indata = 'FOO' + certs = dict() + + if chatty or connectionchatty: + sys.stdout.write("\nTestcase: test_ecdh_curve\n") + for step, tmp in steps.iteritems(): + if chatty or connectionchatty: + sys.stdout.write("\n Subcase: %s\n" % step) + server_curve, client_curve, result = tmp + server = ThreadedEchoServer(certificate=CERTFILE_EC, + ssl_version=ssl.PROTOCOL_DTLSv1_2, + certreqs=ssl.CERT_NONE, + cacerts=ISSUER_CERTFILE_EC, + ciphers=None, + curves=None, + sigalgs=None, + mtu=None, + server_key_exchange_curve=server_curve, + server_cert_options=None, + chatty=chatty) + flag = threading.Event() + server.start(flag) + # wait for it to start + flag.wait() + try: + s = DtlsSocket(socket.socket(socket.AF_INET, socket.SOCK_DGRAM), + keyfile=None, + certfile=None, + cert_reqs=ssl.CERT_REQUIRED, + ssl_version=ssl.PROTOCOL_DTLSv1_2, + ca_certs=ISSUER_CERTFILE_EC, + ciphers=None, + curves=client_curve, + sigalgs=None, + user_mtu=None) + s.connect((HOST, server.port)) + if connectionchatty: + sys.stdout.write(" client: sending %s...\n" % (repr(indata))) + s.write(indata) + outdata = s.read() + if connectionchatty: + sys.stdout.write(" client: read %s\n" % repr(outdata)) + if outdata != indata.lower(): + raise AssertionError("bad data <<%s>> (%d) received; expected <<%s>> (%d)\n" + % (outdata[:min(len(outdata), 20)], len(outdata), + indata[:min(len(indata), 20)].lower(), len(indata))) + if connectionchatty: + sys.stdout.write(" client: closing connection.\n") + try: + s.close() + except Exception as e: + if connectionchatty: + sys.stdout.write(" client: error closing connection %s...\n" % (repr(e))) + pass + except Exception as e: + if connectionchatty: + sys.stdout.write(" client: aborting with exception %s...\n" % (repr(e))) + if result: + raise + finally: + server.stop() + + pass + + +if __name__ == '__main__': + unittest.main() diff --git a/dtls/util.py b/dtls/util.py index 13e20f5..534b77e 100644 --- a/dtls/util.py +++ b/dtls/util.py @@ -57,3 +57,15 @@ class _BIO(_Rsrc): BIO_free(self._value) self.owned = False self._value = None + + +class _EC_KEY(_Rsrc): + """EC KEY wrapper""" + def __init__(self, value): + super(_EC_KEY, self).__init__(value) + + def __del__(self): + _logger.debug("Freeing EC_KEY: %d", self.raw) + from openssl import EC_KEY_free + EC_KEY_free(self._value) + self._value = None diff --git a/dtls/wrapper.py b/dtls/wrapper.py new file mode 100644 index 0000000..3a53d26 --- /dev/null +++ b/dtls/wrapper.py @@ -0,0 +1,370 @@ +# -*- coding: utf-8 -*- + +# DTLS Socket: A wrapper for a server and client using a DTLS connection. + +# Copyright 2017 Björn Freise +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# The License is also distributed with this work in the file named "LICENSE." +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""DTLS Socket + +This wrapper encapsulates the state and behavior associated with the connection +between the OpenSSL library and an individual peer when using the DTLS +protocol. + +Classes: + + DtlsSocket -- DTLS Socket wrapper for use as a client or server +""" + +import select + +from logging import getLogger + +import ssl +import socket +from patch import do_patch +do_patch() +from sslconnection import SSLContext, SSL +import err as err_codes + +_logger = getLogger(__name__) + + +def wrap_client(sock, keyfile=None, certfile=None, + cert_reqs=ssl.CERT_NONE, ssl_version=ssl.PROTOCOL_DTLSv1_2, ca_certs=None, + do_handshake_on_connect=True, suppress_ragged_eofs=True, + ciphers=None, curves=None, sigalgs=None, user_mtu=None): + + return DtlsSocket(sock=sock, keyfile=keyfile, certfile=certfile, server_side=False, + cert_reqs=cert_reqs, ssl_version=ssl_version, ca_certs=ca_certs, + do_handshake_on_connect=do_handshake_on_connect, suppress_ragged_eofs=suppress_ragged_eofs, + ciphers=ciphers, curves=curves, sigalgs=sigalgs, user_mtu=user_mtu, + server_key_exchange_curve=None, server_cert_options=ssl.SSL_BUILD_CHAIN_FLAG_NONE) + + +def wrap_server(sock, keyfile=None, certfile=None, + cert_reqs=ssl.CERT_NONE, ssl_version=ssl.PROTOCOL_DTLS, ca_certs=None, + do_handshake_on_connect=False, suppress_ragged_eofs=True, + ciphers=None, curves=None, sigalgs=None, user_mtu=None, + server_key_exchange_curve=None, server_cert_options=ssl.SSL_BUILD_CHAIN_FLAG_NONE): + + return DtlsSocket(sock=sock, keyfile=keyfile, certfile=certfile, server_side=True, + cert_reqs=cert_reqs, ssl_version=ssl_version, ca_certs=ca_certs, + do_handshake_on_connect=do_handshake_on_connect, suppress_ragged_eofs=suppress_ragged_eofs, + ciphers=ciphers, curves=curves, sigalgs=sigalgs, user_mtu=user_mtu, + server_key_exchange_curve=server_key_exchange_curve, server_cert_options=server_cert_options) + + +class DtlsSocket(object): + + class _ClientSession(object): + + def __init__(self, host, port, handshake_done=False): + self.host = host + self.port = int(port) + self.handshake_done = handshake_done + + def getAddr(self): + return self.host, self.port + + def __init__(self, + sock=None, + keyfile=None, + certfile=None, + server_side=False, + cert_reqs=ssl.CERT_NONE, + ssl_version=ssl.PROTOCOL_DTLSv1_2, + ca_certs=None, + do_handshake_on_connect=False, + suppress_ragged_eofs=True, + ciphers=None, + curves=None, + sigalgs=None, + user_mtu=None, + server_key_exchange_curve=None, + server_cert_options=ssl.SSL_BUILD_CHAIN_FLAG_NONE): + + if server_cert_options is None: + server_cert_options = ssl.SSL_BUILD_CHAIN_FLAG_NONE + + self._ssl_logging = False + self._server_side = server_side + self._ciphers = ciphers + self._curves = curves + self._sigalgs = sigalgs + self._user_mtu = user_mtu + self._server_key_exchange_curve = server_key_exchange_curve + self._server_cert_options = server_cert_options + + # Default socket creation + _sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + if isinstance(sock, socket.socket): + _sock = sock + + self._sock = ssl.wrap_socket(_sock, + keyfile=keyfile, + certfile=certfile, + server_side=self._server_side, + cert_reqs=cert_reqs, + ssl_version=ssl_version, + ca_certs=ca_certs, + do_handshake_on_connect=do_handshake_on_connect, + suppress_ragged_eofs=suppress_ragged_eofs, + ciphers=self._ciphers, + cb_user_config_ssl_ctx=self.user_config_ssl_ctx, + cb_user_config_ssl=self.user_config_ssl) + + if self._server_side: + self._clients = {} + self._timeout = None + + def __getattr__(self, item): + if hasattr(self, "_sock") and hasattr(self._sock, item): + return getattr(self._sock, item) + raise AttributeError + + def user_config_ssl_ctx(self, _ctx): + """ + + :param SSLContext _ctx: + """ + _ctx.set_ssl_logging(self._ssl_logging) + if self._ciphers: + _ctx.set_ciphers(self._ciphers) + if self._curves: + _ctx.set_curves(self._curves) + if self._sigalgs: + _ctx.set_sigalgs(self._sigalgs) + if self._server_side: + _ctx.build_cert_chain(flags=self._server_cert_options) + _ctx.set_ecdh_curve(curve_name=self._server_key_exchange_curve) + + def user_config_ssl(self, _ssl): + """ + + :param SSL _ssl: + """ + if self._user_mtu: + _ssl.set_link_mtu(self._user_mtu) + + def settimeout(self, t): + if self._server_side: + self._timeout = t + else: + self._sock.settimeout(t) + + def close(self): + if self._server_side: + for cli in self._clients.keys(): + cli.close() + else: + try: + self._sock.unwrap() + except: + pass + self._sock.close() + + def recvfrom(self, bufsize, flags=0): + if self._server_side: + return self._recvfrom_on_server_side(bufsize, flags=flags) + else: + return self._recvfrom_on_client_side(bufsize, flags=flags) + + def _recvfrom_on_server_side(self, bufsize, flags): + try: + r, _, _ = select.select(self._getAllReadingSockets(), [], [], self._timeout) + + except socket.timeout: + # __Nothing__ received from any client + raise socket.timeout + + try: + for conn in r: + _last_peer = conn.getpeername() if conn._connected else None + if self._sockIsServerSock(conn): + # Connect + self._clientAccept(conn) + else: + # Handshake + if not self._clientHandshakeDone(conn): + self._clientDoHandshake(conn) + # Normal read + else: + buf = self._clientRead(conn, bufsize) + if buf: + if conn in self._clients: + return buf, self._clients[conn].getAddr() + else: + _logger.debug('Received data from an already disconnected client!') + + except Exception as e: + setattr(e, 'peer', _last_peer) + raise e + + try: + for conn in self._getClientReadingSockets(): + if conn.get_timeout(): + ret = conn.handle_timeout() + _logger.debug('Retransmission triggered for %s: %d' % (str(self._clients[conn].getAddr()), ret)) + + except Exception as e: + raise e + + # __No_data__ received from any client + raise socket.timeout + + def _recvfrom_on_client_side(self, bufsize, flags): + try: + buf = self._sock.recv(bufsize, flags) + + except ssl.SSLError as e: + if e.errno == ssl.ERR_READ_TIMEOUT or e.args[0] == ssl.SSL_ERROR_WANT_READ: + pass + else: + raise e + + else: + if buf: + return buf, self._sock.getpeername() + + # __No_data__ received from any client + raise socket.timeout + + def sendto(self, buf, address): + if self._server_side: + return self._sendto_from_server_side(buf, address) + else: + return self._sendto_from_client_side(buf, address) + + def _sendto_from_server_side(self, buf, address): + for conn, client in self._clients.iteritems(): + if client.getAddr() == address: + return self._clientWrite(conn, buf) + return 0 + + def _sendto_from_client_side(self, buf, address): + try: + if not self._sock._connected: + self._sock.connect(address) + bytes_sent = self._sock.send(buf) + + except ssl.SSLError as e: + raise e + + return bytes_sent + + def _getClientReadingSockets(self): + return [x for x in self._clients.keys()] + + def _getAllReadingSockets(self): + return [self._sock] + self._getClientReadingSockets() + + def _sockIsServerSock(self, conn): + return conn is self._sock + + def _clientHandshakeDone(self, conn): + return conn in self._clients and self._clients[conn].handshake_done is True + + def _clientAccept(self, conn): + _logger.debug('+' * 60) + ret = None + + try: + ret = conn.accept() + _logger.debug('Accept returned with ... %s' % (str(ret))) + + except Exception as e: + raise e + + else: + if ret: + client, addr = ret + host, port = addr + if client in self._clients: + _logger.debug('Client already connected %s' % str(client)) + raise ValueError + self._clients[client] = self._ClientSession(host=host, port=port) + + self._clientDoHandshake(client) + + def _clientDoHandshake(self, conn): + _logger.debug('-' * 60) + conn.setblocking(False) + + try: + conn.do_handshake() + _logger.debug('Connection from %s successful' % (str(self._clients[conn].getAddr()))) + + self._clients[conn].handshake_done = True + + except ssl.SSLError as e: + if e.errno == err_codes.ERR_HANDSHAKE_TIMEOUT or e.args[0] == ssl.SSL_ERROR_WANT_READ: + pass + else: + self._clientDrop(conn, error=e) + raise e + + def _clientRead(self, conn, bufsize=4096): + _logger.debug('*' * 60) + ret = None + + try: + ret = conn.recv(bufsize) + _logger.debug('From client %s ... bytes received %s' % (str(self._clients[conn].getAddr()), str(len(ret)))) + + except ssl.SSLError as e: + if e.args[0] == ssl.SSL_ERROR_WANT_READ: + pass + else: + self._clientDrop(conn, error=e) + + return ret + + def _clientWrite(self, conn, data): + _logger.debug('#' * 60) + ret = None + + try: + _data = data + if False: + _data = data.raw + ret = conn.send(_data) + _logger.debug('To client %s ... bytes sent %s' % (str(self._clients[conn].getAddr()), str(ret))) + + except Exception as e: + raise e + + return ret + + def _clientDrop(self, conn, error=None): + _logger.debug('$' * 60) + + try: + if error: + _logger.debug('Drop client %s ... with error: %s' % (self._clients[conn].getAddr(), error)) + else: + _logger.debug('Drop client %s' % str(self._clients[conn].getAddr())) + + if conn in self._clients: + del self._clients[conn] + try: + conn.unwrap() + except: + pass + conn.close() + + except Exception as e: + pass diff --git a/dtls/x509.py b/dtls/x509.py index bb194ae..f6e03f7 100644 --- a/dtls/x509.py +++ b/dtls/x509.py @@ -43,7 +43,7 @@ class _X509(_Rsrc): super(_X509, self).__init__(value) def __del__(self): - _logger.debug("Freeing X509: %d", self._value._as_parameter) + _logger.debug("Freeing X509: %d", self.raw) X509_free(self._value) self._value = None @@ -54,11 +54,10 @@ class _STACK(_Rsrc): super(_STACK, self).__init__(value) def __del__(self): - _logger.debug("Freeing stack: %d", self._value._as_parameter) + _logger.debug("Freeing stack: %d", self.raw) sk_pop_free(self._value) self._value = None - def decode_cert(cert): """Convert an X509 certificate into a Python dictionary diff --git a/setup.py b/setup.py index 62302aa..a2fcc52 100644 --- a/setup.py +++ b/setup.py @@ -33,7 +33,7 @@ for scheme in INSTALL_SCHEMES.values(): scheme['data'] = scheme['purelib'] NAME = "Dtls" -VERSION = "1.0.2" +VERSION = "1.2.0" DIST_DIR = "dist" FORMAT_TO_SUFFIX = { "zip": ".zip",