Skip to content

Commit 5c0de5f

Browse files
committed
gh-150101 Expose OpenSSL's error queue through SSLError.error_queue
1 parent 29415c0 commit 5c0de5f

4 files changed

Lines changed: 65 additions & 1 deletion

File tree

Doc/library/ssl.rst

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -262,7 +262,15 @@ Exceptions
262262
example ``CERTIFICATE_VERIFY_FAILED``. The range of possible
263263
values depends on the OpenSSL version.
264264

265-
.. versionadded:: 3.3
265+
.. attribute:: error_queue
266+
267+
The full OpenSSL error queue, as a list of strings, where the last
268+
item of the list is the top of the queue. If :attr:`reason` is not enough
269+
to diagnose a problem, OpenSSL may report more detail through this queue.
270+
The format of the strings follows that of OpenSSL's
271+
`ERR_error_string <https://docs.openssl.org/master/man3/ERR_error_string/>`_.
272+
273+
.. versionadded:: 3.16
266274

267275
.. exception:: SSLZeroReturnError
268276

Lib/test/test_ssl.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1909,6 +1909,7 @@ def test_lib_reason(self):
19091909
self.assertEqual(cm.exception.library, 'PEM')
19101910
regex = "(NO_START_LINE|UNSUPPORTED_PUBLIC_KEY_TYPE)"
19111911
self.assertRegex(cm.exception.reason, regex)
1912+
self.assertTrue(len(cm.exception.error_queue >= 1))
19121913
s = str(cm.exception)
19131914
self.assertIn("NO_START_LINE", s)
19141915

@@ -4688,6 +4689,7 @@ def cb_returning_alert(ssl_sock, server_name, initial_context):
46884689
chatty=False,
46894690
sni_name='supermessage')
46904691
self.assertEqual(cm.exception.reason, 'TLSV1_ALERT_ACCESS_DENIED')
4692+
self.assertTrue(len(cm.exception.error_queue >= 1))
46914693

46924694
def test_sni_callback_raising(self):
46934695
# Raising fails the connection with a TLS handshake failure alert.
@@ -4708,6 +4710,7 @@ def cb_raising(ssl_sock, server_name, initial_context):
47084710
"|SSLV3_ALERT_HANDSHAKE_FAILURE"
47094711
"|NO_PRIVATE_VALUE)")
47104712
self.assertRegex(cm.exception.reason, regex)
4713+
self.assertTrue(len(cm.exception.error_queue >= 1))
47114714
self.assertEqual(catch.unraisable.exc_type, ZeroDivisionError)
47124715

47134716
def test_sni_callback_wrong_return_type(self):
@@ -4727,6 +4730,7 @@ def cb_wrong_return_type(ssl_sock, server_name, initial_context):
47274730

47284731

47294732
self.assertEqual(cm.exception.reason, 'TLSV1_ALERT_INTERNAL_ERROR')
4733+
self.assertTrue(len(cm.exception.error_queue >= 1))
47304734
self.assertEqual(catch.unraisable.exc_type, TypeError)
47314735

47324736
def test_shared_ciphers(self):

Modules/_ssl.c

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -513,6 +513,8 @@ fill_and_set_sslerror(_sslmodulestate *state,
513513
PyObject *verify_obj = NULL, *verify_code_obj = NULL;
514514
PyObject *init_value, *msg, *key;
515515
PyUnicodeWriter *writer = NULL;
516+
PyObject *error_queue = PyList_New(0);
517+
if (!error_queue) goto fail;
516518

517519
if (ssl_errno == PY_SSL_ERROR_EOF && sslsock != NULL) {
518520
sslsock->got_eof_error = 1;
@@ -542,6 +544,45 @@ fill_and_set_sslerror(_sslmodulestate *state,
542544
if (errstr == NULL) {
543545
errstr = ERR_reason_error_string(errcode);
544546
}
547+
548+
/* populate error_list from OpenSSL's error queue */
549+
unsigned int q_pos = 0; /* Error queue position */
550+
const char *eq_filename, *eq_func, *eq_data;
551+
char eq_msg[256];
552+
int eq_lineno;
553+
int flags; /* Flags (discarded) */
554+
unsigned long openssl_errorcode;
555+
556+
/* Presumably, we no longer need the OpenSSL error queue after this, so
557+
we can call ERR_get_error (destructive) instead of ERR_peek_error */
558+
while ((openssl_errorcode = ERR_get_error_all(&eq_filename, &eq_lineno, &eq_func,
559+
&eq_data, &flags))) {
560+
if (q_pos == 0) {
561+
/* errcode should have come from a caller, and should have been
562+
returned from ERR_peek_last_error() */
563+
assert(openssl_errorcode == errcode);
564+
}
565+
566+
ERR_error_string_n(openssl_errorcode, eq_msg, 256);
567+
568+
// Follows [lib reason] error_string extra_data (OpenSSL file:func:line)
569+
PyObject *current_eq_msg = NULL;
570+
if (eq_data != NULL) {
571+
current_eq_msg = PyUnicode_FromFormat(
572+
"%s %s (%s:%s:%d)",
573+
eq_msg, eq_data, eq_filename, eq_func, eq_lineno
574+
);
575+
} else {
576+
current_eq_msg = PyUnicode_FromFormat(
577+
"%s (%s:%s:%d)",
578+
eq_msg, eq_filename, eq_func, eq_lineno
579+
);
580+
}
581+
if (PyList_Append(error_queue, current_eq_msg) != 0) {
582+
goto fail;
583+
}
584+
q_pos++;
585+
}
545586
}
546587

547588
/* verify code for cert validation error */
@@ -655,6 +696,12 @@ fill_and_set_sslerror(_sslmodulestate *state,
655696
goto fail;
656697
}
657698

699+
/* Add the full OpenSSL error queue to exception */
700+
if (PyObject_SetAttr(err_value, state->str_error_queue, error_queue) != 0) {
701+
goto fail;
702+
}
703+
Py_DECREF(error_queue);
704+
658705
PyErr_SetObject(type, err_value);
659706
fail:
660707
Py_XDECREF(err_value);
@@ -7359,6 +7406,10 @@ sslmodule_init_strings(PyObject *module)
73597406
if (state->str_verify_code == NULL) {
73607407
return -1;
73617408
}
7409+
state->str_error_queue = PyUnicode_InternFromString("error_queue");
7410+
if (state->str_error_queue == NULL) {
7411+
return -1;
7412+
}
73627413
return 0;
73637414
}
73647415

Modules/_ssl.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ typedef struct {
3333
PyObject *str_reason;
3434
PyObject *str_verify_code;
3535
PyObject *str_verify_message;
36+
PyObject *str_error_queue;
3637
/* keylog lock */
3738
PyThread_type_lock keylog_lock;
3839
} _sslmodulestate;

0 commit comments

Comments
 (0)