Skip to content

Commit 8a42346

Browse files
committed
gh-160171: Fix ssl get_unverified_chain() on resumed server sessions
Include the client's certificate when the session was resumed from a ticket, and return [] instead of raising AttributeError when the client sent no certificate.
1 parent ab5187c commit 8a42346

4 files changed

Lines changed: 95 additions & 22 deletions

File tree

‎Doc/library/ssl.rst‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1296,13 +1296,21 @@ SSL sockets also have the following additional methods and attributes:
12961296
If certificate verification was disabled method acts the same as
12971297
:meth:`~SSLSocket.get_unverified_chain`.
12981298

1299+
The verified chain is not stored with a session, so the list is empty
1300+
when the session was resumed (see :attr:`~SSLSocket.session_reused`).
1301+
12991302
.. versionadded:: 3.13
13001303

13011304
.. method:: SSLSocket.get_unverified_chain()
13021305

13031306
Returns raw certificate chain provided by the other
13041307
end of the SSL channel as a list of DER-encoded bytes.
13051308

1309+
No certificates are exchanged when a session is resumed (see
1310+
:attr:`~SSLSocket.session_reused`); the list then holds what was stored
1311+
with the session, which for a session resumed from a ticket on the
1312+
server side is the peer's own certificate only.
1313+
13061314
.. versionadded:: 3.13
13071315

13081316
.. method:: SSLSocket.cipher()

‎Lib/test/test_ssl.py‎

Lines changed: 56 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3017,11 +3017,19 @@ def run(self):
30173017
cert = self.sslconn.getpeercert()
30183018
self.write(repr(cert).encode("us-ascii") + b"\n")
30193019
elif stripped == b'VERIFIEDCHAIN':
3020-
certs = self.sslconn._sslobj.get_verified_chain()
3021-
self.write(len(certs).to_bytes(1, "big") + b"\n")
3020+
try:
3021+
certs = self.sslconn.get_verified_chain()
3022+
except Exception as e:
3023+
self.write(repr(e).encode("us-ascii") + b"\n")
3024+
else:
3025+
self.write(len(certs).to_bytes(1, "big") + b"\n")
30223026
elif stripped == b'UNVERIFIEDCHAIN':
3023-
certs = self.sslconn._sslobj.get_unverified_chain()
3024-
self.write(len(certs).to_bytes(1, "big") + b"\n")
3027+
try:
3028+
certs = self.sslconn.get_unverified_chain()
3029+
except Exception as e:
3030+
self.write(repr(e).encode("us-ascii") + b"\n")
3031+
else:
3032+
self.write(len(certs).to_bytes(1, "big") + b"\n")
30253033
else:
30263034
if (support.verbose and
30273035
self.server.connectionchatty):
@@ -5701,6 +5709,50 @@ def test_internal_chain_server(self):
57015709
res = s.recv(1024)
57025710
self.assertEqual(res, b'\x02\n')
57035711

5712+
def test_internal_chain_server_resumed_session(self):
5713+
# A session resumed from a ticket stores the client's certificate
5714+
# but not the chain it was sent with.
5715+
client_context, server_context, hostname = testing_context()
5716+
client_context.load_cert_chain(SIGNED_CERTFILE)
5717+
server_context.verify_mode = ssl.CERT_REQUIRED
5718+
# TODO: sessions aren't compatible with TLSv1.3 yet
5719+
client_context.maximum_version = ssl.TLSVersion.TLSv1_2
5720+
5721+
server = ThreadedEchoServer(context=server_context, chatty=False)
5722+
with server:
5723+
with client_context.wrap_socket(
5724+
socket.socket(),
5725+
server_hostname=hostname
5726+
) as s:
5727+
s.connect((HOST, server.port))
5728+
session = s.session
5729+
self.assertTrue(session.has_ticket)
5730+
with client_context.wrap_socket(
5731+
socket.socket(),
5732+
server_hostname=hostname,
5733+
session=session
5734+
) as s:
5735+
s.connect((HOST, server.port))
5736+
self.assertTrue(s.session_reused)
5737+
s.write(b'UNVERIFIEDCHAIN\n')
5738+
res = s.recv(1024)
5739+
self.assertEqual(res, b'\x01\n')
5740+
5741+
def test_internal_chain_server_no_client_cert(self):
5742+
client_context, server_context, hostname = testing_context()
5743+
server_context.verify_mode = ssl.CERT_OPTIONAL
5744+
5745+
server = ThreadedEchoServer(context=server_context, chatty=False)
5746+
with server:
5747+
with client_context.wrap_socket(
5748+
socket.socket(),
5749+
server_hostname=hostname
5750+
) as s:
5751+
s.connect((HOST, server.port))
5752+
s.write(b'UNVERIFIEDCHAIN\n')
5753+
res = s.recv(1024)
5754+
self.assertEqual(res, b'\x00\n')
5755+
57045756

57055757
class TestSSLDebug(unittest.TestCase):
57065758

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
On the server side, :meth:`ssl.SSLSocket.get_unverified_chain` now returns
2+
the client's certificate when the session was resumed from a session ticket,
3+
instead of an empty list, and returns an empty list instead of raising
4+
:exc:`AttributeError` when the client sent no certificate.

‎Modules/_ssl.c‎

Lines changed: 27 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1991,31 +1991,40 @@ _ssl__SSLSocket_get_unverified_chain_impl(PySSLSocket *self)
19911991
/*[clinic end generated code: output=5acdae414e13f913 input=079f8ff5c205cb3b]*/
19921992
{
19931993
PyObject *retval;
1994+
X509 *peer = NULL;
19941995
/* borrowed reference */
1995-
/* TODO: include SSL_get_peer_certificate() for server-side sockets */
19961996
STACK_OF(X509) *chain = SSL_get_peer_cert_chain(self->ssl);
1997-
if (chain == NULL) {
1997+
/* OpenSSL does not include the peer cert in the chain for server side
1998+
* connections, so the chain alone does not tell whether the client sent
1999+
* a certificate: the chain is empty when it did not, and NULL when the
2000+
* session was resumed from a ticket (which stores the peer cert only). */
2001+
if (self->socket_type == PY_SSL_SERVER) {
2002+
peer = SSL_get_peer_certificate(self->ssl);
2003+
if (peer == NULL) {
2004+
Py_RETURN_NONE;
2005+
}
2006+
}
2007+
else if (chain == NULL) {
19982008
Py_RETURN_NONE;
19992009
}
2000-
retval = _PySSL_CertificateFromX509Stack(self->ctx->state, chain, 1);
2010+
if (chain == NULL) {
2011+
retval = PyList_New(0);
2012+
}
2013+
else {
2014+
retval = _PySSL_CertificateFromX509Stack(self->ctx->state, chain, 1);
2015+
}
20012016
if (retval == NULL) {
2017+
X509_free(peer);
20022018
return NULL;
20032019
}
2004-
/* OpenSSL does not include peer cert for server side connections */
2005-
if (self->socket_type == PY_SSL_SERVER) {
2006-
PyObject *peerobj = NULL;
2007-
X509 *peer = SSL_get_peer_certificate(self->ssl);
2008-
2009-
if (peer == NULL) {
2010-
peerobj = Py_NewRef(Py_None);
2011-
} else {
2012-
/* consume X509 reference on success */
2013-
peerobj = _PySSL_CertificateFromX509(self->ctx->state, peer, 0);
2014-
if (peerobj == NULL) {
2015-
X509_free(peer);
2016-
Py_DECREF(retval);
2017-
return NULL;
2018-
}
2020+
if (peer != NULL) {
2021+
/* consume X509 reference on success */
2022+
PyObject *peerobj = _PySSL_CertificateFromX509(self->ctx->state,
2023+
peer, 0);
2024+
if (peerobj == NULL) {
2025+
X509_free(peer);
2026+
Py_DECREF(retval);
2027+
return NULL;
20192028
}
20202029
int res = PyList_Insert(retval, 0, peerobj);
20212030
Py_DECREF(peerobj);

0 commit comments

Comments
 (0)