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()
incoming
mcfreis 2017-03-20 16:39:50 +01:00
parent 13edf48fdf
commit ff509e0724
7 changed files with 207 additions and 121 deletions

View File

@ -1,3 +1,23 @@
2017-03-17 Björn Freise <mcfreis@gmx.net>
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 <mcfreis@gmx.net> 2017-03-17 Björn Freise <mcfreis@gmx.net>
Added certificate creation using ECDSA Added certificate creation using ECDSA

View File

@ -61,3 +61,4 @@ _prep_bins() # prepare before module imports
from patch import do_patch from patch import do_patch
from sslconnection import SSLConnection from sslconnection import SSLConnection
from demux import force_routing_demux, reset_default_demux from demux import force_routing_demux, reset_default_demux
import err as error_codes

View File

@ -48,8 +48,12 @@ ERR_WRITE_TIMEOUT = 503
ERR_HANDSHAKE_TIMEOUT = 504 ERR_HANDSHAKE_TIMEOUT = 504
ERR_PORT_UNREACHABLE = 505 ERR_PORT_UNREACHABLE = 505
ERR_WRONG_SSL_VERSION = 0x1409210A
ERR_WRONG_VERSION_NUMBER = 0x1408A10B ERR_WRONG_VERSION_NUMBER = 0x1408A10B
ERR_COOKIE_MISMATCH = 0x1408A134 ERR_COOKIE_MISMATCH = 0x1408A134
ERR_CERTIFICATE_VERIFY_FAILED = 0x14090086
ERR_NO_SHARED_CIPHER = 0x1408A0C1
ERR_SSL_HANDSHAKE_FAILURE = 0x1410C0E5
class SSLError(socket_error): class SSLError(socket_error):

View File

@ -98,6 +98,7 @@ SSL_SESS_CACHE_NO_INTERNAL_LOOKUP = 0x0100
SSL_SESS_CACHE_NO_INTERNAL_STORE = 0x0200 SSL_SESS_CACHE_NO_INTERNAL_STORE = 0x0200
SSL_SESS_CACHE_NO_INTERNAL = \ SSL_SESS_CACHE_NO_INTERNAL = \
SSL_SESS_CACHE_NO_INTERNAL_LOOKUP | SSL_SESS_CACHE_NO_INTERNAL_STORE 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_UNTRUSTED = 0x1
SSL_BUILD_CHAIN_FLAG_NO_ROOT = 0x2 SSL_BUILD_CHAIN_FLAG_NO_ROOT = 0x2
SSL_BUILD_CHAIN_FLAG_CHECK = 0x4 SSL_BUILD_CHAIN_FLAG_CHECK = 0x4
@ -351,6 +352,13 @@ class GENERAL_NAMES(STACK):
super(GENERAL_NAMES, self).__init__(value) 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): class X509_NAME_ENTRY(Structure):
_fields_ = [("object", c_void_p), _fields_ = [("object", c_void_p),
("value", c_void_p), ("value", c_void_p),
@ -597,8 +605,8 @@ __all__ = [
"SSL_CB_ACCEPT_LOOP", "SSL_CB_ACCEPT_EXIT", "SSL_CB_ACCEPT_LOOP", "SSL_CB_ACCEPT_EXIT",
"SSL_CB_CONNECT_LOOP", "SSL_CB_CONNECT_EXIT", "SSL_CB_CONNECT_LOOP", "SSL_CB_CONNECT_EXIT",
"SSL_CB_HANDSHAKE_START", "SSL_CB_HANDSHAKE_DONE", "SSL_CB_HANDSHAKE_START", "SSL_CB_HANDSHAKE_DONE",
"SSL_BUILD_CHAIN_FLAG_UNTRUSTED", "SSL_BUILD_CHAIN_FLAG_NO_ROOT", "SSL_BUILD_CHAIN_FLAG_CHECK", "SSL_BUILD_CHAIN_FLAG_NONE", "SSL_BUILD_CHAIN_FLAG_UNTRUSTED", "SSL_BUILD_CHAIN_FLAG_NO_ROOT",
"SSL_BUILD_CHAIN_FLAG_IGNORE_ERROR", "SSL_BUILD_CHAIN_FLAG_CLEAR_ERROR", "SSL_BUILD_CHAIN_FLAG_CHECK", "SSL_BUILD_CHAIN_FLAG_IGNORE_ERROR", "SSL_BUILD_CHAIN_FLAG_CLEAR_ERROR",
"SSL_FILE_TYPE_PEM", "SSL_FILE_TYPE_PEM",
"GEN_DIRNAME", "NID_subject_alt_name", "GEN_DIRNAME", "NID_subject_alt_name",
"CRYPTO_LOCK", "CRYPTO_LOCK",
@ -628,6 +636,7 @@ __all__ = [
"SSL_set1_curves", "SSL_set1_curves_list", "SSL_set1_curves", "SSL_set1_curves_list",
"SSL_set_mtu", "SSL_set_mtu",
"SSL_state_string_long", "SSL_alert_type_string_long", "SSL_alert_desc_string_long", "SSL_state_string_long", "SSL_alert_type_string_long", "SSL_alert_desc_string_long",
"SSL_get_peer_cert_chain",
"SSL_CTX_set_cookie_cb", "SSL_CTX_set_cookie_cb",
"OBJ_obj2txt", "decode_ASN1_STRING", "ASN1_TIME_print", "OBJ_obj2txt", "decode_ASN1_STRING", "ASN1_TIME_print",
"OBJ_nid2sn", "OBJ_nid2sn",
@ -736,6 +745,8 @@ map(lambda x: _make_function(*x), (
((c_int, "ret"), (SSL, "ssl"))), ((c_int, "ret"), (SSL, "ssl"))),
("SSL_get_peer_certificate", libssl, ("SSL_get_peer_certificate", libssl,
((X509, "ret"), (SSL, "ssl"))), ((X509, "ret"), (SSL, "ssl"))),
("SSL_get_peer_cert_chain", libssl,
((STACK_OF_X509, "ret"), (SSL, "ssl")), False),
("SSL_read", libssl, ("SSL_read", libssl,
((c_int, "ret"), (SSL, "ssl"), (c_void_p, "buf"), (c_int, "num")), False), ((c_int, "ret"), (SSL, "ssl"), (c_void_p, "buf"), (c_int, "num")), False),
("SSL_write", libssl, ("SSL_write", libssl,
@ -1155,3 +1166,13 @@ def i2d_X509(x509):
bio = _BIO(BIO_new(BIO_s_mem())) bio = _BIO(BIO_new(BIO_s_mem()))
_i2d_X509_bio(bio.value, x509) _i2d_X509_bio(bio.value, x509)
return BIO_get_mem_data(bio.value) 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

View File

@ -204,6 +204,12 @@ def _SSLSocket_init(self, sock=None, keyfile=None, certfile=None,
self.get_timeout = MethodType(_SSLSocket_get_timeout, proxy(self)) self.get_timeout = MethodType(_SSLSocket_get_timeout, proxy(self))
self.handle_timeout = MethodType(_SSLSocket_handle_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): def _SSLSocket_listen(self, ignored):
if self._connected: if self._connected:
raise ValueError("attempt to listen on connected SSLSocket!") raise ValueError("attempt to listen on connected SSLSocket!")

View File

@ -52,10 +52,10 @@ from weakref import proxy
from err import openssl_error, InvalidSocketError from err import openssl_error, InvalidSocketError
from err import raise_ssl_error from err import raise_ssl_error
from err import SSL_ERROR_WANT_READ, SSL_ERROR_SYSCALL from err import SSL_ERROR_WANT_READ, SSL_ERROR_SYSCALL
from err import ERR_WRONG_VERSION_NUMBER, 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_NO_CIPHER, ERR_HANDSHAKE_TIMEOUT, ERR_PORT_UNREACHABLE
from err import ERR_READ_TIMEOUT, ERR_WRITE_TIMEOUT from err import ERR_READ_TIMEOUT, ERR_WRITE_TIMEOUT
from err import ERR_BOTH_KEY_CERT_FILES, 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 x509 import _X509, decode_cert
from tlock import tlock_init from tlock import tlock_init
from openssl import * from openssl import *
@ -235,7 +235,8 @@ class SSLContext(object):
return sorted([x.name for x in curves] if bAsName else [x.nid for x in 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): def set_ecdh_curve(self, curve_name=None):
u''' u''' Select a curve to use for ECDH(E) key exchange or set it to auto mode
Used for server only! Used for server only!
s.a. openssl.exe ecparam -list_curves s.a. openssl.exe ecparam -list_curves
@ -246,13 +247,13 @@ class SSLContext(object):
if curve_name: if curve_name:
retVal = SSL_CTX_set_ecdh_auto(self._ctx, 0) retVal = SSL_CTX_set_ecdh_auto(self._ctx, 0)
avail_curves = get_elliptic_curves() avail_curves = get_elliptic_curves()
self._ctx.key = [curve for curve in avail_curves if curve.name == curve_name][0].to_EC_KEY() key = [curve for curve in avail_curves if curve.name == curve_name][0].to_EC_KEY()
retVal = SSL_CTX_set_tmp_ecdh(self._ctx, self._ctx.key) retVal &= SSL_CTX_set_tmp_ecdh(self._ctx, key)
else: else:
retVal = SSL_CTX_set_ecdh_auto(self._ctx, 1) retVal = SSL_CTX_set_ecdh_auto(self._ctx, 1)
return retVal return retVal
def build_cert_chain(self, flags=SSL_BUILD_CHAIN_FLAG_NO_ROOT): def build_cert_chain(self, flags=SSL_BUILD_CHAIN_FLAG_NONE):
u''' u'''
Used for server side only! Used for server side only!
@ -557,11 +558,11 @@ class SSLConnection(object):
else: else:
post_init = self._init_client(peer_address) post_init = self._init_client(peer_address)
SSL_set_options(self._ssl.value, SSL_OP_NO_QUERY_MTU)
DTLS_set_link_mtu(self._ssl.value, 1500)
if self._user_config_ssl: if self._user_config_ssl:
self._user_config_ssl(self._intf_ssl) self._user_config_ssl(self._intf_ssl)
else:
SSL_set_options(self._ssl.value, SSL_OP_NO_QUERY_MTU)
DTLS_set_link_mtu(self._ssl.value, 1500)
SSL_set_bio(self._ssl.value, self._rbio.value, self._wbio.value) SSL_set_bio(self._ssl.value, self._rbio.value, self._wbio.value)
self._rbio.disown() self._rbio.disown()
self._wbio.disown() self._wbio.disown()
@ -638,10 +639,13 @@ class SSLConnection(object):
return return
elif err.errqueue and err.errqueue[0][0] == ERR_WRONG_VERSION_NUMBER: elif err.errqueue and err.errqueue[0][0] == ERR_WRONG_VERSION_NUMBER:
_logger.debug("Wrong version number; aborting handshake") _logger.debug("Wrong version number; aborting handshake")
return raise
elif err.errqueue and err.errqueue[0][0] == ERR_COOKIE_MISMATCH: elif err.errqueue and err.errqueue[0][0] == ERR_COOKIE_MISMATCH:
_logger.debug("Mismatching cookie received; aborting handshake") _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") _logger.exception("Unexpected error in DTLSv1_listen")
raise raise
finally: finally:
@ -736,8 +740,13 @@ class SSLConnection(object):
string containing read bytes string containing read bytes
""" """
return self._wrap_socket_library_call( try:
lambda: SSL_read(self._ssl.value, len, buffer), ERR_READ_TIMEOUT) 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): def write(self, data):
"""Write data to connection """Write data to connection
@ -751,8 +760,16 @@ class SSLConnection(object):
number of bytes actually transmitted number of bytes actually transmitted
""" """
return self._wrap_socket_library_call( try:
lambda: SSL_write(self._ssl.value, data), ERR_WRITE_TIMEOUT) 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): def shutdown(self):
"""Shut down the DTLS connection """Shut down the DTLS connection
@ -816,6 +833,21 @@ class SSLConnection(object):
peer_certificate = getpeercert # compatibility with _ssl call interface 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): def cipher(self):
"""Retrieve information about the current cipher """Retrieve information about the current cipher

View File

@ -538,6 +538,8 @@ class ThreadedEchoServer(threading.Thread):
handler.start() handler.start()
except socket.timeout: except socket.timeout:
pass pass
except ssl.SSLError:
pass
except KeyboardInterrupt: except KeyboardInterrupt:
self.stop() self.stop()
self.sock.close() self.sock.close()