Kaydet (Commit) b3ad0e51 authored tarafından Christian Heimes's avatar Christian Heimes Kaydeden (comit) GitHub

bpo-28182: Expose OpenSSL verification results (#3412)

The SSL module now raises SSLCertVerificationError when OpenSSL fails to
verify the peer's certificate. The exception contains more information about
the error.

Original patch by Chi Hsuan Yen
Signed-off-by: 's avatarChristian Heimes <christian@python.org>
üst af8d6b90
...@@ -129,11 +129,26 @@ Functions, Constants, and Exceptions ...@@ -129,11 +129,26 @@ Functions, Constants, and Exceptions
.. versionadded:: 3.3 .. versionadded:: 3.3
.. exception:: SSLCertVerificationError
A subclass of :exc:`SSLError` raised when certificate validation has
failed.
.. versionadded:: 3.7
.. attribute:: verify_code
A numeric error number that denotes the verification error.
.. attribute:: verify_message
A human readable string of the verification error.
.. exception:: CertificateError .. exception:: CertificateError
Raised to signal an error with a certificate (such as mismatching Raised to signal an error with a certificate (such as mismatching
hostname). Certificate errors detected by OpenSSL, though, raise hostname). Certificate errors detected by OpenSSL, though, raise
an :exc:`SSLError`. an :exc:`SSLCertVerificationError`.
Socket creation Socket creation
......
...@@ -104,7 +104,7 @@ from _ssl import OPENSSL_VERSION_NUMBER, OPENSSL_VERSION_INFO, OPENSSL_VERSION ...@@ -104,7 +104,7 @@ from _ssl import OPENSSL_VERSION_NUMBER, OPENSSL_VERSION_INFO, OPENSSL_VERSION
from _ssl import _SSLContext, MemoryBIO, SSLSession from _ssl import _SSLContext, MemoryBIO, SSLSession
from _ssl import ( from _ssl import (
SSLError, SSLZeroReturnError, SSLWantReadError, SSLWantWriteError, SSLError, SSLZeroReturnError, SSLWantReadError, SSLWantWriteError,
SSLSyscallError, SSLEOFError, SSLSyscallError, SSLEOFError, SSLCertVerificationError
) )
from _ssl import txt2obj as _txt2obj, nid2obj as _nid2obj from _ssl import txt2obj as _txt2obj, nid2obj as _nid2obj
from _ssl import RAND_status, RAND_add, RAND_bytes, RAND_pseudo_bytes from _ssl import RAND_status, RAND_add, RAND_bytes, RAND_pseudo_bytes
......
...@@ -2530,6 +2530,29 @@ class ThreadedTests(unittest.TestCase): ...@@ -2530,6 +2530,29 @@ class ThreadedTests(unittest.TestCase):
finally: finally:
t.join() t.join()
def test_ssl_cert_verify_error(self):
if support.verbose:
sys.stdout.write("\n")
server_context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
server_context.load_cert_chain(SIGNED_CERTFILE)
context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
server = ThreadedEchoServer(context=server_context, chatty=True)
with server:
with context.wrap_socket(socket.socket(),
server_hostname="localhost") as s:
try:
s.connect((HOST, server.port))
except ssl.SSLError as e:
msg = 'unable to get local issuer certificate'
self.assertIsInstance(e, ssl.SSLCertVerificationError)
self.assertEqual(e.verify_code, 20)
self.assertEqual(e.verify_message, msg)
self.assertIn(msg, repr(e))
self.assertIn('certificate verify failed', repr(e))
@skip_if_broken_ubuntu_ssl @skip_if_broken_ubuntu_ssl
@unittest.skipUnless(hasattr(ssl, 'PROTOCOL_SSLv2'), @unittest.skipUnless(hasattr(ssl, 'PROTOCOL_SSLv2'),
"OpenSSL is compiled without SSLv2 support") "OpenSSL is compiled without SSLv2 support")
......
The SSL module now raises SSLCertVerificationError when OpenSSL fails to
verify the peer's certificate. The exception contains more information about
the error.
...@@ -66,6 +66,7 @@ static PySocketModule_APIObject PySocketModule; ...@@ -66,6 +66,7 @@ static PySocketModule_APIObject PySocketModule;
/* SSL error object */ /* SSL error object */
static PyObject *PySSLErrorObject; static PyObject *PySSLErrorObject;
static PyObject *PySSLCertVerificationErrorObject;
static PyObject *PySSLZeroReturnErrorObject; static PyObject *PySSLZeroReturnErrorObject;
static PyObject *PySSLWantReadErrorObject; static PyObject *PySSLWantReadErrorObject;
static PyObject *PySSLWantWriteErrorObject; static PyObject *PySSLWantWriteErrorObject;
...@@ -386,6 +387,9 @@ typedef enum { ...@@ -386,6 +387,9 @@ typedef enum {
PyDoc_STRVAR(SSLError_doc, PyDoc_STRVAR(SSLError_doc,
"An error occurred in the SSL implementation."); "An error occurred in the SSL implementation.");
PyDoc_STRVAR(SSLCertVerificationError_doc,
"A certificate could not be verified.");
PyDoc_STRVAR(SSLZeroReturnError_doc, PyDoc_STRVAR(SSLZeroReturnError_doc,
"SSL/TLS session closed cleanly."); "SSL/TLS session closed cleanly.");
...@@ -430,13 +434,16 @@ static PyType_Spec sslerror_type_spec = { ...@@ -430,13 +434,16 @@ static PyType_Spec sslerror_type_spec = {
}; };
static void static void
fill_and_set_sslerror(PyObject *type, int ssl_errno, const char *errstr, fill_and_set_sslerror(PySSLSocket *sslsock, PyObject *type, int ssl_errno,
int lineno, unsigned long errcode) const char *errstr, int lineno, unsigned long errcode)
{ {
PyObject *err_value = NULL, *reason_obj = NULL, *lib_obj = NULL; PyObject *err_value = NULL, *reason_obj = NULL, *lib_obj = NULL;
PyObject *verify_obj = NULL, *verify_code_obj = NULL;
PyObject *init_value, *msg, *key; PyObject *init_value, *msg, *key;
_Py_IDENTIFIER(reason); _Py_IDENTIFIER(reason);
_Py_IDENTIFIER(library); _Py_IDENTIFIER(library);
_Py_IDENTIFIER(verify_message);
_Py_IDENTIFIER(verify_code);
if (errcode != 0) { if (errcode != 0) {
int lib, reason; int lib, reason;
...@@ -466,7 +473,50 @@ fill_and_set_sslerror(PyObject *type, int ssl_errno, const char *errstr, ...@@ -466,7 +473,50 @@ fill_and_set_sslerror(PyObject *type, int ssl_errno, const char *errstr,
if (errstr == NULL) if (errstr == NULL)
errstr = "unknown error"; errstr = "unknown error";
if (reason_obj && lib_obj) /* verify code for cert validation error */
if ((sslsock != NULL) && (type == PySSLCertVerificationErrorObject)) {
const char *verify_str = NULL;
long verify_code;
verify_code = SSL_get_verify_result(sslsock->ssl);
verify_code_obj = PyLong_FromLong(verify_code);
if (verify_code_obj == NULL) {
goto fail;
}
switch (verify_code) {
case X509_V_ERR_HOSTNAME_MISMATCH:
verify_obj = PyUnicode_FromFormat(
"Hostname mismatch, certificate is not valid for '%S'.",
sslsock->server_hostname
);
break;
case X509_V_ERR_IP_ADDRESS_MISMATCH:
verify_obj = PyUnicode_FromFormat(
"IP address mismatch, certificate is not valid for '%S'.",
sslsock->server_hostname
);
break;
default:
verify_str = X509_verify_cert_error_string(verify_code);
if (verify_str != NULL) {
verify_obj = PyUnicode_FromString(verify_str);
} else {
verify_obj = Py_None;
Py_INCREF(verify_obj);
}
break;
}
if (verify_obj == NULL) {
goto fail;
}
}
if (verify_obj && reason_obj && lib_obj)
msg = PyUnicode_FromFormat("[%S: %S] %s: %S (_ssl.c:%d)",
lib_obj, reason_obj, errstr, verify_obj,
lineno);
else if (reason_obj && lib_obj)
msg = PyUnicode_FromFormat("[%S: %S] %s (_ssl.c:%d)", msg = PyUnicode_FromFormat("[%S: %S] %s (_ssl.c:%d)",
lib_obj, reason_obj, errstr, lineno); lib_obj, reason_obj, errstr, lineno);
else if (lib_obj) else if (lib_obj)
...@@ -490,17 +540,30 @@ fill_and_set_sslerror(PyObject *type, int ssl_errno, const char *errstr, ...@@ -490,17 +540,30 @@ fill_and_set_sslerror(PyObject *type, int ssl_errno, const char *errstr,
reason_obj = Py_None; reason_obj = Py_None;
if (_PyObject_SetAttrId(err_value, &PyId_reason, reason_obj)) if (_PyObject_SetAttrId(err_value, &PyId_reason, reason_obj))
goto fail; goto fail;
if (lib_obj == NULL) if (lib_obj == NULL)
lib_obj = Py_None; lib_obj = Py_None;
if (_PyObject_SetAttrId(err_value, &PyId_library, lib_obj)) if (_PyObject_SetAttrId(err_value, &PyId_library, lib_obj))
goto fail; goto fail;
if ((sslsock != NULL) && (type == PySSLCertVerificationErrorObject)) {
/* Only set verify code / message for SSLCertVerificationError */
if (_PyObject_SetAttrId(err_value, &PyId_verify_code,
verify_code_obj))
goto fail;
if (_PyObject_SetAttrId(err_value, &PyId_verify_message, verify_obj))
goto fail;
}
PyErr_SetObject(type, err_value); PyErr_SetObject(type, err_value);
fail: fail:
Py_XDECREF(err_value); Py_XDECREF(err_value);
Py_XDECREF(verify_code_obj);
Py_XDECREF(verify_obj);
} }
static PyObject * static PyObject *
PySSL_SetError(PySSLSocket *obj, int ret, const char *filename, int lineno) PySSL_SetError(PySSLSocket *sslsock, int ret, const char *filename, int lineno)
{ {
PyObject *type = PySSLErrorObject; PyObject *type = PySSLErrorObject;
char *errstr = NULL; char *errstr = NULL;
...@@ -511,8 +574,8 @@ PySSL_SetError(PySSLSocket *obj, int ret, const char *filename, int lineno) ...@@ -511,8 +574,8 @@ PySSL_SetError(PySSLSocket *obj, int ret, const char *filename, int lineno)
assert(ret <= 0); assert(ret <= 0);
e = ERR_peek_last_error(); e = ERR_peek_last_error();
if (obj->ssl != NULL) { if (sslsock->ssl != NULL) {
err = SSL_get_error(obj->ssl, ret); err = SSL_get_error(sslsock->ssl, ret);
switch (err) { switch (err) {
case SSL_ERROR_ZERO_RETURN: case SSL_ERROR_ZERO_RETURN:
...@@ -541,7 +604,7 @@ PySSL_SetError(PySSLSocket *obj, int ret, const char *filename, int lineno) ...@@ -541,7 +604,7 @@ PySSL_SetError(PySSLSocket *obj, int ret, const char *filename, int lineno)
case SSL_ERROR_SYSCALL: case SSL_ERROR_SYSCALL:
{ {
if (e == 0) { if (e == 0) {
PySocketSockObject *s = GET_SOCKET(obj); PySocketSockObject *s = GET_SOCKET(sslsock);
if (ret == 0 || (((PyObject *)s) == Py_None)) { if (ret == 0 || (((PyObject *)s) == Py_None)) {
p = PY_SSL_ERROR_EOF; p = PY_SSL_ERROR_EOF;
type = PySSLEOFErrorObject; type = PySSLEOFErrorObject;
...@@ -566,9 +629,14 @@ PySSL_SetError(PySSLSocket *obj, int ret, const char *filename, int lineno) ...@@ -566,9 +629,14 @@ PySSL_SetError(PySSLSocket *obj, int ret, const char *filename, int lineno)
case SSL_ERROR_SSL: case SSL_ERROR_SSL:
{ {
p = PY_SSL_ERROR_SSL; p = PY_SSL_ERROR_SSL;
if (e == 0) if (e == 0) {
/* possible? */ /* possible? */
errstr = "A failure in the SSL library occurred"; errstr = "A failure in the SSL library occurred";
}
if (ERR_GET_LIB(e) == ERR_LIB_SSL &&
ERR_GET_REASON(e) == SSL_R_CERTIFICATE_VERIFY_FAILED) {
type = PySSLCertVerificationErrorObject;
}
break; break;
} }
default: default:
...@@ -576,7 +644,7 @@ PySSL_SetError(PySSLSocket *obj, int ret, const char *filename, int lineno) ...@@ -576,7 +644,7 @@ PySSL_SetError(PySSLSocket *obj, int ret, const char *filename, int lineno)
errstr = "Invalid error code"; errstr = "Invalid error code";
} }
} }
fill_and_set_sslerror(type, p, errstr, lineno, e); fill_and_set_sslerror(sslsock, type, p, errstr, lineno, e);
ERR_clear_error(); ERR_clear_error();
return NULL; return NULL;
} }
...@@ -588,15 +656,11 @@ _setSSLError (const char *errstr, int errcode, const char *filename, int lineno) ...@@ -588,15 +656,11 @@ _setSSLError (const char *errstr, int errcode, const char *filename, int lineno)
errcode = ERR_peek_last_error(); errcode = ERR_peek_last_error();
else else
errcode = 0; errcode = 0;
fill_and_set_sslerror(PySSLErrorObject, errcode, errstr, lineno, errcode); fill_and_set_sslerror(NULL, PySSLErrorObject, errcode, errstr, lineno, errcode);
ERR_clear_error(); ERR_clear_error();
return NULL; return NULL;
} }
/*
* SSL objects
*/
static PySSLSocket * static PySSLSocket *
newPySSLSocket(PySSLContext *sslctx, PySocketSockObject *sock, newPySSLSocket(PySSLContext *sslctx, PySocketSockObject *sock,
enum py_ssl_server_or_client socket_type, enum py_ssl_server_or_client socket_type,
...@@ -656,7 +720,6 @@ newPySSLSocket(PySSLContext *sslctx, PySocketSockObject *sock, ...@@ -656,7 +720,6 @@ newPySSLSocket(PySSLContext *sslctx, PySocketSockObject *sock,
if (server_hostname != NULL) if (server_hostname != NULL)
SSL_set_tlsext_host_name(self->ssl, server_hostname); SSL_set_tlsext_host_name(self->ssl, server_hostname);
#endif #endif
/* If the socket is in non-blocking mode or timeout mode, set the BIO /* If the socket is in non-blocking mode or timeout mode, set the BIO
* to non-blocking mode (blocking is the default) * to non-blocking mode (blocking is the default)
*/ */
...@@ -5130,7 +5193,7 @@ parse_openssl_version(unsigned long libver, ...@@ -5130,7 +5193,7 @@ parse_openssl_version(unsigned long libver,
PyMODINIT_FUNC PyMODINIT_FUNC
PyInit__ssl(void) PyInit__ssl(void)
{ {
PyObject *m, *d, *r; PyObject *m, *d, *r, *bases;
unsigned long libver; unsigned long libver;
unsigned int major, minor, fix, patch, status; unsigned int major, minor, fix, patch, status;
PySocketModule_APIObject *socket_api; PySocketModule_APIObject *socket_api;
...@@ -5182,6 +5245,14 @@ PyInit__ssl(void) ...@@ -5182,6 +5245,14 @@ PyInit__ssl(void)
if (PySSLErrorObject == NULL) if (PySSLErrorObject == NULL)
return NULL; return NULL;
/* ssl.CertificateError used to be a subclass of ValueError */
bases = Py_BuildValue("OO", PySSLErrorObject, PyExc_ValueError);
if (bases == NULL)
return NULL;
PySSLCertVerificationErrorObject = PyErr_NewExceptionWithDoc(
"ssl.SSLCertVerificationError", SSLCertVerificationError_doc,
bases, NULL);
Py_DECREF(bases);
PySSLZeroReturnErrorObject = PyErr_NewExceptionWithDoc( PySSLZeroReturnErrorObject = PyErr_NewExceptionWithDoc(
"ssl.SSLZeroReturnError", SSLZeroReturnError_doc, "ssl.SSLZeroReturnError", SSLZeroReturnError_doc,
PySSLErrorObject, NULL); PySSLErrorObject, NULL);
...@@ -5197,13 +5268,16 @@ PyInit__ssl(void) ...@@ -5197,13 +5268,16 @@ PyInit__ssl(void)
PySSLEOFErrorObject = PyErr_NewExceptionWithDoc( PySSLEOFErrorObject = PyErr_NewExceptionWithDoc(
"ssl.SSLEOFError", SSLEOFError_doc, "ssl.SSLEOFError", SSLEOFError_doc,
PySSLErrorObject, NULL); PySSLErrorObject, NULL);
if (PySSLZeroReturnErrorObject == NULL if (PySSLCertVerificationErrorObject == NULL
|| PySSLZeroReturnErrorObject == NULL
|| PySSLWantReadErrorObject == NULL || PySSLWantReadErrorObject == NULL
|| PySSLWantWriteErrorObject == NULL || PySSLWantWriteErrorObject == NULL
|| PySSLSyscallErrorObject == NULL || PySSLSyscallErrorObject == NULL
|| PySSLEOFErrorObject == NULL) || PySSLEOFErrorObject == NULL)
return NULL; return NULL;
if (PyDict_SetItemString(d, "SSLError", PySSLErrorObject) != 0 if (PyDict_SetItemString(d, "SSLError", PySSLErrorObject) != 0
|| PyDict_SetItemString(d, "SSLCertVerificationError",
PySSLCertVerificationErrorObject) != 0
|| PyDict_SetItemString(d, "SSLZeroReturnError", PySSLZeroReturnErrorObject) != 0 || PyDict_SetItemString(d, "SSLZeroReturnError", PySSLZeroReturnErrorObject) != 0
|| PyDict_SetItemString(d, "SSLWantReadError", PySSLWantReadErrorObject) != 0 || PyDict_SetItemString(d, "SSLWantReadError", PySSLWantReadErrorObject) != 0
|| PyDict_SetItemString(d, "SSLWantWriteError", PySSLWantWriteErrorObject) != 0 || PyDict_SetItemString(d, "SSLWantWriteError", PySSLWantWriteErrorObject) != 0
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment