From 3b36fb1f537a771829c9c7c9720dc040d3c87885 Mon Sep 17 00:00:00 2001 From: Antoine Pitrou Date: Fri, 22 Jun 2012 21:11:52 +0200 Subject: [PATCH] Issue #14837: SSL errors now have `library` and `reason` attributes describing precisely what happened and in which OpenSSL submodule. The str() of a SSLError is also enhanced accordingly. NOTE: this commit creates a reference leak. The leak seems tied to the use of PyType_FromSpec() to create the SSLError type. The leak is on the type object when it is instantiated: >>> e = ssl.SSLError() >>> sys.getrefcount(ssl.SSLError) 35 >>> e = ssl.SSLError() >>> sys.getrefcount(ssl.SSLError) 36 >>> e = ssl.SSLError() >>> sys.getrefcount(ssl.SSLError) 37 --- Doc/library/ssl.rst | 16 +++ Lib/test/test_ssl.py | 45 +++++++- Misc/NEWS | 4 + Modules/_ssl.c | 262 ++++++++++++++++++++++++++++++++----------- 4 files changed, 262 insertions(+), 65 deletions(-) diff --git a/Doc/library/ssl.rst b/Doc/library/ssl.rst index 8092581ece4..e08c2b9e9c9 100644 --- a/Doc/library/ssl.rst +++ b/Doc/library/ssl.rst @@ -59,6 +59,22 @@ Functions, Constants, and Exceptions .. versionchanged:: 3.3 :exc:`SSLError` used to be a subtype of :exc:`socket.error`. + .. attribute:: library + + A string mnemonic designating the OpenSSL submodule in which the error + occurred, such as ``SSL``, ``PEM`` or ``X509``. The range of possible + values depends on the OpenSSL version. + + .. versionadded:: 3.3 + + .. attribute:: reason + + A string mnemonic designating the reason this error occurred, for + example ``CERTIFICATE_VERIFY_FAILED``. The range of possible + values depends on the OpenSSL version. + + .. versionadded:: 3.3 + .. exception:: SSLZeroReturnError A subclass of :exc:`SSLError` raised when trying to read or write and diff --git a/Lib/test/test_ssl.py b/Lib/test/test_ssl.py index d2839dd7f83..51762cffc8c 100644 --- a/Lib/test/test_ssl.py +++ b/Lib/test/test_ssl.py @@ -552,7 +552,7 @@ class ContextTests(unittest.TestCase): with self.assertRaises(FileNotFoundError) as cm: ctx.load_dh_params(WRONGCERT) self.assertEqual(cm.exception.errno, errno.ENOENT) - with self.assertRaisesRegex(ssl.SSLError, "PEM routines"): + with self.assertRaises(ssl.SSLError) as cm: ctx.load_dh_params(CERTFILE) @skip_if_broken_ubuntu_ssl @@ -590,6 +590,47 @@ class ContextTests(unittest.TestCase): self.assertRaises(ValueError, ctx.set_ecdh_curve, b"foo") +class SSLErrorTests(unittest.TestCase): + + def test_str(self): + # The str() of a SSLError doesn't include the errno + e = ssl.SSLError(1, "foo") + self.assertEqual(str(e), "foo") + self.assertEqual(e.errno, 1) + # Same for a subclass + e = ssl.SSLZeroReturnError(1, "foo") + self.assertEqual(str(e), "foo") + self.assertEqual(e.errno, 1) + + def test_lib_reason(self): + # Test the library and reason attributes + ctx = ssl.SSLContext(ssl.PROTOCOL_TLSv1) + with self.assertRaises(ssl.SSLError) as cm: + ctx.load_dh_params(CERTFILE) + self.assertEqual(cm.exception.library, 'PEM') + self.assertEqual(cm.exception.reason, 'NO_START_LINE') + s = str(cm.exception) + self.assertTrue(s.startswith("[PEM: NO_START_LINE] no start line"), s) + + def test_subclass(self): + # Check that the appropriate SSLError subclass is raised + # (this only tests one of them) + ctx = ssl.SSLContext(ssl.PROTOCOL_TLSv1) + with socket.socket() as s: + s.bind(("127.0.0.1", 0)) + s.listen(5) + with socket.socket() as c: + c.connect(s.getsockname()) + c.setblocking(False) + c = ctx.wrap_socket(c, False, do_handshake_on_connect=False) + with self.assertRaises(ssl.SSLWantReadError) as cm: + c.do_handshake() + s = str(cm.exception) + self.assertTrue(s.startswith("The operation did not complete (read)"), s) + # For compatibility + self.assertEqual(cm.exception.errno, ssl.SSL_ERROR_WANT_READ) + + class NetworkedTests(unittest.TestCase): def test_connect(self): @@ -1931,7 +1972,7 @@ def test_main(verbose=False): if not os.path.exists(filename): raise support.TestFailed("Can't read certificate file %r" % filename) - tests = [ContextTests, BasicSocketTests] + tests = [ContextTests, BasicSocketTests, SSLErrorTests] if support.is_resource_enabled('network'): tests.append(NetworkedTests) diff --git a/Misc/NEWS b/Misc/NEWS index af1e26c3dbe..a733354ecaa 100644 --- a/Misc/NEWS +++ b/Misc/NEWS @@ -40,6 +40,10 @@ Core and Builtins Library ------- +- Issue #14837: SSL errors now have ``library`` and ``reason`` attributes + describing precisely what happened and in which OpenSSL submodule. The + str() of a SSLError is also enhanced accordingly. + - Issue #9527: datetime.astimezone() method will now supply a class timezone instance corresponding to the system local timezone when called with no arguments. diff --git a/Modules/_ssl.c b/Modules/_ssl.c index d7554110203..b5d15306d6a 100644 --- a/Modules/_ssl.c +++ b/Modules/_ssl.c @@ -76,6 +76,16 @@ enum py_ssl_version { PY_SSL_VERSION_TLS1 }; +struct py_ssl_error_code { + const char *mnemonic; + int library, reason; +}; + +struct py_ssl_library_code { + const char *library; + int code; +}; + /* Include symbols from _socket module */ #include "socketmodule.h" @@ -97,6 +107,9 @@ static PySocketModule_APIObject PySocketModule; #include "openssl/err.h" #include "openssl/rand.h" +/* Include generated data (error codes) */ +#include "_ssl_data.h" + /* SSL error object */ static PyObject *PySSLErrorObject; static PyObject *PySSLZeroReturnErrorObject; @@ -105,6 +118,11 @@ static PyObject *PySSLWantWriteErrorObject; static PyObject *PySSLSyscallErrorObject; static PyObject *PySSLEOFErrorObject; +/* Error mappings */ +static PyObject *err_codes_to_names; +static PyObject *err_names_to_codes; +static PyObject *lib_codes_to_names; + #ifdef WITH_THREAD /* serves as a flag to see whether we've initialized the SSL thread support. */ @@ -202,22 +220,134 @@ typedef enum { #define ERRSTR1(x,y,z) (x ":" y ": " z) #define ERRSTR(x) ERRSTR1("_ssl.c", STRINGIFY2(__LINE__), x) -/* XXX It might be helpful to augment the error message generated - below with the name of the SSL function that generated the error. - I expect it's obvious most of the time. -*/ + +/* + * SSL errors. + */ + +PyDoc_STRVAR(SSLError_doc, +"An error occurred in the SSL implementation."); + +PyDoc_STRVAR(SSLZeroReturnError_doc, +"SSL/TLS session closed cleanly."); + +PyDoc_STRVAR(SSLWantReadError_doc, +"Non-blocking SSL socket needs to read more data\n" +"before the requested operation can be completed."); + +PyDoc_STRVAR(SSLWantWriteError_doc, +"Non-blocking SSL socket needs to write more data\n" +"before the requested operation can be completed."); + +PyDoc_STRVAR(SSLSyscallError_doc, +"System error when attempting SSL operation."); + +PyDoc_STRVAR(SSLEOFError_doc, +"SSL/TLS connection terminated abruptly."); + +static PyObject * +SSLError_str(PyOSErrorObject *self) +{ + if (self->strerror != NULL && PyUnicode_Check(self->strerror)) { + Py_INCREF(self->strerror); + return self->strerror; + } + else + return PyObject_Str(self->args); +} + +static PyType_Slot sslerror_type_slots[] = { + {Py_tp_base, NULL}, /* Filled out in module init as it's not a constant */ + {Py_tp_doc, SSLError_doc}, + {Py_tp_str, SSLError_str}, + {0, 0}, +}; + +static PyType_Spec sslerror_type_spec = { + "ssl.SSLError", + sizeof(PyOSErrorObject), + 0, + Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE, + sslerror_type_slots +}; + +static void +fill_and_set_sslerror(PyObject *type, int ssl_errno, const char *errstr, + int lineno, unsigned long errcode) +{ + PyObject *err_value = NULL, *reason_obj = NULL, *lib_obj = NULL; + PyObject *init_value, *msg, *key; + _Py_IDENTIFIER(reason); + _Py_IDENTIFIER(library); + + if (errcode != 0) { + int lib, reason; + + lib = ERR_GET_LIB(errcode); + reason = ERR_GET_REASON(errcode); + key = Py_BuildValue("ii", lib, reason); + if (key == NULL) + goto fail; + reason_obj = PyDict_GetItem(err_codes_to_names, key); + Py_DECREF(key); + if (reason_obj == NULL) { + /* XXX if reason < 100, it might reflect a library number (!!) */ + PyErr_Clear(); + } + key = PyLong_FromLong(lib); + if (key == NULL) + goto fail; + lib_obj = PyDict_GetItem(lib_codes_to_names, key); + Py_DECREF(key); + if (lib_obj == NULL) { + PyErr_Clear(); + } + if (errstr == NULL) + errstr = ERR_reason_error_string(errcode); + } + if (errstr == NULL) + errstr = "unknown error"; + + if (reason_obj && lib_obj) + msg = PyUnicode_FromFormat("[%S: %S] %s (_ssl.c:%d)", + lib_obj, reason_obj, errstr, lineno); + else if (lib_obj) + msg = PyUnicode_FromFormat("[%S] %s (_ssl.c:%d)", + lib_obj, errstr, lineno); + else + msg = PyUnicode_FromFormat("%s (_ssl.c:%d)", errstr, lineno); + + if (msg == NULL) + goto fail; + init_value = Py_BuildValue("iN", ssl_errno, msg); + err_value = PyObject_CallObject(type, init_value); + Py_DECREF(init_value); + if (err_value == NULL) + goto fail; + if (reason_obj == NULL) + reason_obj = Py_None; + if (_PyObject_SetAttrId(err_value, &PyId_reason, reason_obj)) + goto fail; + if (lib_obj == NULL) + lib_obj = Py_None; + if (_PyObject_SetAttrId(err_value, &PyId_library, lib_obj)) + goto fail; + PyErr_SetObject(type, err_value); +fail: + Py_XDECREF(err_value); +} static PyObject * PySSL_SetError(PySSLSocket *obj, int ret, char *filename, int lineno) { - PyObject *v; PyObject *type = PySSLErrorObject; - char buf[2048]; - char *errstr; + char *errstr = NULL; int err; enum py_ssl_error p = PY_SSL_ERROR_NONE; + unsigned long e = 0; assert(ret <= 0); + e = ERR_peek_last_error(); if (obj->ssl != NULL) { err = SSL_get_error(obj->ssl, ret); @@ -248,7 +378,6 @@ PySSL_SetError(PySSLSocket *obj, int ret, char *filename, int lineno) break; case SSL_ERROR_SYSCALL: { - unsigned long e = ERR_get_error(); if (e == 0) { PySocketSockObject *s = (PySocketSockObject *) PyWeakref_GetObject(obj->Socket); @@ -260,9 +389,9 @@ PySSL_SetError(PySSLSocket *obj, int ret, char *filename, int lineno) /* underlying BIO reported an I/O error */ Py_INCREF(s); ERR_clear_error(); - v = s->errorhandler(); + s->errorhandler(); Py_DECREF(s); - return v; + return NULL; } else { /* possible? */ p = PY_SSL_ERROR_SYSCALL; type = PySSLSyscallErrorObject; @@ -270,60 +399,43 @@ PySSL_SetError(PySSLSocket *obj, int ret, char *filename, int lineno) } } else { p = PY_SSL_ERROR_SYSCALL; - /* XXX Protected by global interpreter lock */ - errstr = ERR_error_string(e, NULL); } break; } case SSL_ERROR_SSL: { - unsigned long e = ERR_get_error(); p = PY_SSL_ERROR_SSL; - if (e != 0) - /* XXX Protected by global interpreter lock */ - errstr = ERR_error_string(e, NULL); - else { /* possible? */ + if (e == 0) + /* possible? */ errstr = "A failure in the SSL library occurred"; - } break; } default: p = PY_SSL_ERROR_INVALID_ERROR_CODE; errstr = "Invalid error code"; } - } else { - errstr = ERR_error_string(ERR_peek_last_error(), NULL); } - PyOS_snprintf(buf, sizeof(buf), "_ssl.c:%d: %s", lineno, errstr); + fill_and_set_sslerror(type, p, errstr, lineno, e); ERR_clear_error(); - v = Py_BuildValue("(is)", p, buf); - if (v != NULL) { - PyErr_SetObject(type, v); - Py_DECREF(v); - } return NULL; } static PyObject * _setSSLError (char *errstr, int errcode, char *filename, int lineno) { - char buf[2048]; - PyObject *v; - - if (errstr == NULL) { + if (errstr == NULL) errcode = ERR_peek_last_error(); - errstr = ERR_error_string(errcode, NULL); - } - PyOS_snprintf(buf, sizeof(buf), "_ssl.c:%d: %s", lineno, errstr); + else + errcode = 0; + fill_and_set_sslerror(PySSLErrorObject, errcode, errstr, lineno, errcode); ERR_clear_error(); - v = Py_BuildValue("(is)", errcode, buf); - if (v != NULL) { - PyErr_SetObject(PySSLErrorObject, v); - Py_DECREF(v); - } return NULL; } +/* + * SSL objects + */ + static PySSLSocket * newPySSLSocket(SSL_CTX *ctx, PySocketSockObject *sock, enum py_ssl_server_or_client socket_type, @@ -2520,27 +2632,6 @@ parse_openssl_version(unsigned long libver, *major = libver & 0xFF; } -PyDoc_STRVAR(SSLError_doc, -"An error occurred in the SSL implementation."); - -PyDoc_STRVAR(SSLZeroReturnError_doc, -"SSL/TLS session closed cleanly."); - -PyDoc_STRVAR(SSLWantReadError_doc, -"Non-blocking SSL socket needs to read more data\n" -"before the requested operation can be completed."); - -PyDoc_STRVAR(SSLWantWriteError_doc, -"Non-blocking SSL socket needs to write more data\n" -"before the requested operation can be completed."); - -PyDoc_STRVAR(SSLSyscallError_doc, -"System error when attempting SSL operation."); - -PyDoc_STRVAR(SSLEOFError_doc, -"SSL/TLS connection terminated abruptly."); - - PyMODINIT_FUNC PyInit__ssl(void) { @@ -2548,6 +2639,8 @@ PyInit__ssl(void) unsigned long libver; unsigned int major, minor, fix, patch, status; PySocketModule_APIObject *socket_api; + struct py_ssl_error_code *errcode; + struct py_ssl_library_code *libcode; if (PyType_Ready(&PySSLContext_Type) < 0) return NULL; @@ -2577,12 +2670,11 @@ PyInit__ssl(void) OpenSSL_add_all_algorithms(); /* Add symbols to module dict */ - PySSLErrorObject = PyErr_NewExceptionWithDoc("ssl.SSLError", - SSLError_doc, - PyExc_OSError, - NULL); + sslerror_type_slots[0].pfunc = PyExc_OSError; + PySSLErrorObject = PyType_FromSpec(&sslerror_type_spec); if (PySSLErrorObject == NULL) return NULL; + PySSLZeroReturnErrorObject = PyErr_NewExceptionWithDoc( "ssl.SSLZeroReturnError", SSLZeroReturnError_doc, PySSLErrorObject, NULL); @@ -2705,6 +2797,50 @@ PyInit__ssl(void) Py_INCREF(r); PyModule_AddObject(m, "HAS_NPN", r); + /* Mappings for error codes */ + err_codes_to_names = PyDict_New(); + err_names_to_codes = PyDict_New(); + if (err_codes_to_names == NULL || err_names_to_codes == NULL) + return NULL; + errcode = error_codes; + while (errcode->mnemonic != NULL) { + PyObject *mnemo, *key; + mnemo = PyUnicode_FromString(errcode->mnemonic); + key = Py_BuildValue("ii", errcode->library, errcode->reason); + if (mnemo == NULL || key == NULL) + return NULL; + if (PyDict_SetItem(err_codes_to_names, key, mnemo)) + return NULL; + if (PyDict_SetItem(err_names_to_codes, mnemo, key)) + return NULL; + Py_DECREF(key); + Py_DECREF(mnemo); + errcode++; + } + if (PyModule_AddObject(m, "err_codes_to_names", err_codes_to_names)) + return NULL; + if (PyModule_AddObject(m, "err_names_to_codes", err_names_to_codes)) + return NULL; + + lib_codes_to_names = PyDict_New(); + if (lib_codes_to_names == NULL) + return NULL; + libcode = library_codes; + while (libcode->library != NULL) { + PyObject *mnemo, *key; + key = PyLong_FromLong(libcode->code); + mnemo = PyUnicode_FromString(libcode->library); + if (key == NULL || mnemo == NULL) + return NULL; + if (PyDict_SetItem(lib_codes_to_names, key, mnemo)) + return NULL; + Py_DECREF(key); + Py_DECREF(mnemo); + libcode++; + } + if (PyModule_AddObject(m, "lib_codes_to_names", lib_codes_to_names)) + return NULL; + /* OpenSSL version */ /* SSLeay() gives us the version of the library linked against, which could be different from the headers version.