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: Christian Heimes <christian@python.org>
This commit is contained in:
parent
af8d6b9072
commit
b3ad0e5127
|
@ -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
|
||||||
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):
|
||||||
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")
|
||||||
|
|
|
@ -0,0 +1,3 @@
|
||||||
|
The SSL module now raises SSLCertVerificationError when OpenSSL fails to
|
||||||
|
verify the peer's certificate. The exception contains more information about
|
||||||
|
the error.
|
108
Modules/_ssl.c
108
Modules/_ssl.c
|
@ -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 {
|
||||||
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 = {
|
||||||
};
|
};
|
||||||
|
|
||||||
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,
|
||||||
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,
|
||||||
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)
|
||||||
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)
|
||||||
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)
|
||||||
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)
|
||||||
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)
|
||||||
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,
|
||||||
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,
|
||||||
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)
|
||||||
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)
|
||||||
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
|
||||||
|
|
Loading…
Reference in New Issue