Skip to content

Commit 29eba9c

Browse files
miss-islingtongpsheadpicnixz
authored
[3.14] gh-160171: Fix ssl get_unverified_chain() on resumed server sessions (GH-160172) (#160419)
gh-160171: Fix ssl get_unverified_chain() on resumed server sessions (GH-160172) 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. ANT-2026-2RMA0IPV (cherry picked from commit a4ac244) Co-authored-by: Gregory P. Smith <68491+gpshead@users.noreply.github.com> Co-authored-by: Bénédikt Tran <10796600+picnixz@users.noreply.github.com>
1 parent 2ae9141 commit 29eba9c

4 files changed

Lines changed: 108 additions & 22 deletions

File tree

‎Doc/library/ssl.rst‎

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

1259+
The verified chain is not stored with a session, so the list is empty
1260+
when the session is resumed (see :attr:`~SSLSocket.session_reused`).
1261+
12591262
.. versionadded:: 3.13
12601263

12611264
.. method:: SSLSocket.get_unverified_chain()
12621265

12631266
Returns raw certificate chain provided by the other
12641267
end of the SSL channel as a list of DER-encoded bytes.
12651268

1269+
No certificates are exchanged when a session is resumed (see
1270+
:attr:`~SSLSocket.session_reused`); the list then holds what was stored
1271+
with the session, which for a session resumed from a ticket on the
1272+
server side is the peer's own certificate only.
1273+
12661274
.. versionadded:: 3.13
12671275

12681276
.. method:: SSLSocket.cipher()

‎Lib/test/test_ssl.py‎

Lines changed: 72 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2862,11 +2862,19 @@ def run(self):
28622862
cert = self.sslconn.getpeercert()
28632863
self.write(repr(cert).encode("us-ascii") + b"\n")
28642864
elif stripped == b'VERIFIEDCHAIN':
2865-
certs = self.sslconn._sslobj.get_verified_chain()
2866-
self.write(len(certs).to_bytes(1, "big") + b"\n")
2865+
try:
2866+
certs = self.sslconn.get_verified_chain()
2867+
except Exception as e:
2868+
self.write(repr(e).encode("us-ascii") + b"\n")
2869+
else:
2870+
self.write(len(certs).to_bytes(1, "big") + b"\n")
28672871
elif stripped == b'UNVERIFIEDCHAIN':
2868-
certs = self.sslconn._sslobj.get_unverified_chain()
2869-
self.write(len(certs).to_bytes(1, "big") + b"\n")
2872+
try:
2873+
certs = self.sslconn.get_unverified_chain()
2874+
except Exception as e:
2875+
self.write(repr(e).encode("us-ascii") + b"\n")
2876+
else:
2877+
self.write(len(certs).to_bytes(1, "big") + b"\n")
28702878
else:
28712879
if (support.verbose and
28722880
self.server.connectionchatty):
@@ -5345,6 +5353,66 @@ def test_internal_chain_server(self):
53455353
res = s.recv(1024)
53465354
self.assertEqual(res, b'\x02\n')
53475355

5356+
def test_internal_chain_server_resumed_session(self):
5357+
# A session resumed from a ticket stores the client's certificate
5358+
# but not the chain it was sent with.
5359+
for version in ssl.TLSVersion.TLSv1_2, ssl.TLSVersion.TLSv1_3:
5360+
if not has_tls_version(version):
5361+
continue
5362+
with self.subTest(version=version.name):
5363+
self.check_chain_server_resumed_session(version)
5364+
5365+
def check_chain_server_resumed_session(self, version):
5366+
client_context, server_context, hostname = testing_context()
5367+
client_context.load_cert_chain(SIGNED_CERTFILE)
5368+
server_context.verify_mode = ssl.CERT_REQUIRED
5369+
client_context.minimum_version = version
5370+
client_context.maximum_version = version
5371+
5372+
server = ThreadedEchoServer(context=server_context, chatty=False)
5373+
with server:
5374+
with client_context.wrap_socket(
5375+
socket.socket(),
5376+
server_hostname=hostname
5377+
) as s:
5378+
s.connect((HOST, server.port))
5379+
s.write(b'UNVERIFIEDCHAIN\n')
5380+
res = s.recv(1024)
5381+
self.assertEqual(res, b'\x02\n')
5382+
# TLS 1.3 sends the ticket after the handshake, so only
5383+
# get the session once data was received.
5384+
session = s.session
5385+
self.assertTrue(session.has_ticket)
5386+
with client_context.wrap_socket(
5387+
socket.socket(),
5388+
server_hostname=hostname,
5389+
session=session
5390+
) as s:
5391+
s.connect((HOST, server.port))
5392+
self.assertTrue(s.session_reused)
5393+
s.write(b'UNVERIFIEDCHAIN\n')
5394+
res = s.recv(1024)
5395+
self.assertEqual(res, b'\x01\n')
5396+
# The verified chain is not stored with the session at all.
5397+
s.write(b'VERIFIEDCHAIN\n')
5398+
res = s.recv(1024)
5399+
self.assertEqual(res, b'\x00\n')
5400+
5401+
def test_internal_chain_server_no_client_cert(self):
5402+
client_context, server_context, hostname = testing_context()
5403+
server_context.verify_mode = ssl.CERT_OPTIONAL
5404+
5405+
server = ThreadedEchoServer(context=server_context, chatty=False)
5406+
with server:
5407+
with client_context.wrap_socket(
5408+
socket.socket(),
5409+
server_hostname=hostname
5410+
) as s:
5411+
s.connect((HOST, server.port))
5412+
s.write(b'UNVERIFIEDCHAIN\n')
5413+
res = s.recv(1024)
5414+
self.assertEqual(res, b'\x00\n')
5415+
53485416

53495417
class TestSSLDebug(unittest.TestCase):
53505418

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: 24 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1954,31 +1954,37 @@ _ssl__SSLSocket_get_unverified_chain_impl(PySSLSocket *self)
19541954
/*[clinic end generated code: output=5acdae414e13f913 input=079f8ff5c205cb3b]*/
19551955
{
19561956
PyObject *retval;
1957+
X509 *peer = NULL;
19571958
/* borrowed reference */
1958-
/* TODO: include SSL_get_peer_certificate() for server-side sockets */
19591959
STACK_OF(X509) *chain = SSL_get_peer_cert_chain(self->ssl);
1960-
if (chain == NULL) {
1960+
/* OpenSSL does not include the peer cert in the chain for server side
1961+
* connections, so the chain alone does not tell whether the client sent
1962+
* a certificate: the chain is empty when it did not, and NULL when the
1963+
* session was resumed from a ticket (which stores the peer cert only). */
1964+
if (self->socket_type == PY_SSL_SERVER) {
1965+
peer = SSL_get_peer_certificate(self->ssl);
1966+
if (peer == NULL) {
1967+
Py_RETURN_NONE;
1968+
}
1969+
}
1970+
else if (chain == NULL) {
19611971
Py_RETURN_NONE;
19621972
}
1963-
retval = _PySSL_CertificateFromX509Stack(self->ctx->state, chain, 1);
1973+
retval = chain == NULL
1974+
? PyList_New(0)
1975+
: _PySSL_CertificateFromX509Stack(self->ctx->state, chain, 1);
19641976
if (retval == NULL) {
1977+
X509_free(peer);
19651978
return NULL;
19661979
}
1967-
/* OpenSSL does not include peer cert for server side connections */
1968-
if (self->socket_type == PY_SSL_SERVER) {
1969-
PyObject *peerobj = NULL;
1970-
X509 *peer = SSL_get_peer_certificate(self->ssl);
1971-
1972-
if (peer == NULL) {
1973-
peerobj = Py_NewRef(Py_None);
1974-
} else {
1975-
/* consume X509 reference on success */
1976-
peerobj = _PySSL_CertificateFromX509(self->ctx->state, peer, 0);
1977-
if (peerobj == NULL) {
1978-
X509_free(peer);
1979-
Py_DECREF(retval);
1980-
return NULL;
1981-
}
1980+
if (peer != NULL) {
1981+
/* consume X509 reference on success */
1982+
PyObject *peerobj = _PySSL_CertificateFromX509(self->ctx->state,
1983+
peer, 0);
1984+
if (peerobj == NULL) {
1985+
X509_free(peer);
1986+
Py_DECREF(retval);
1987+
return NULL;
19821988
}
19831989
int res = PyList_Insert(retval, 0, peerobj);
19841990
Py_DECREF(peerobj);

0 commit comments

Comments
 (0)