8000 [3.6] bpo-34670: Add TLS 1.3 post handshake auth (GH-9460) (GH-9507) · python/cpython@94812f7 · GitHub
[go: up one dir, main page]

Skip to content

Commit 94812f7

Browse files
tiranmiss-islington
authored andcommitted
[3.6] bpo-34670: Add TLS 1.3 post handshake auth (GH-9460) (GH-9507)
Add SSLContext.post_handshake_auth and SSLSocket.verify_client_post_handshake for TLS 1.3 post-handshake authentication. Signed-off-by: Christian Heimes <christian@python.org>q https://bugs.python.org/issue34670. (cherry picked from commit 9fb051f) Co-authored-by: Christian Heimes <christian@python.org> https://bugs.python.org/issue34670
1 parent ed21919 commit 94812f7

File tree

8 files changed

+378
-12
lines changed

8 files changed

+378
-12
lines changed

Doc/library/ssl.rst

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1210,6 +1210,26 @@ SSL sockets also have the following additional methods and attributes:
12101210
returned socket should always be used for further communication with the
12111211
other side of the connection, rather than the original socket.
12121212

1213+
.. method:: SSLSocket.verify_client_post_handshake()
1214+
1215+
Requests post-handshake authentication (PHA) from a TLS 1.3 client. PHA
1216+
can only be initiated for a TLS 1.3 connection from a server-side socket,
1217+
after the initial TLS handshake and with PHA enabled on both sides, see
1218+
:attr:`SSLContext.post_handshake_auth`.
1219+
1220+
The method does not perform a cert exchange immediately. The server-side
1221+
sends a CertificateRequest during the next write event and expects the
1222+
client to respond with a certificate on the next read event.
1223+
1224+
If any precondition isn't met (e.g. not TLS 1.3, PHA not enabled), an
1225+
:exc:`SSLError` is raised.
1226+
1227+
.. versionadded:: 3.6.7
1228+
1229+
.. note::
1230+
Only available with OpenSSL 1.1.1 and TLS 1.3 enabled. Without TLS 1.3
1231+
support, the method raises :exc:`NotImplementedError`.
1232+
12131233
.. method:: SSLSocket.version()
12141234

12151235
Return the actual SSL protocol version negotiated by the connection
@@ -1693,6 +1713,28 @@ to speed up repeated connections from the same clients.
16931713
>>> ssl.create_default_context().options
16941714
<Options.OP_ALL|OP_NO_SSLv3|OP_NO_SSLv2|OP_NO_COMPRESSION: 2197947391>
16951715

1716+
.. attribute:: SSLContext.post_handshake_auth
1717+
1718+
Enable TLS 1.3 post-handshake client authentication. Post-handshake auth
1719+
is disabled by default and a server can only request a TLS client
1720+
certificate during the initial handshake. When enabled, a server may
1721+
request a TLS client certificate at any time after the handshake.
1722+
1723+
When enabled on client-side sockets, the client signals the server that
1724+
it supports post-handshake authentication.
1725+
1726+
When enabled on server-side sockets, :attr:`SSLContext.verify_mode` must
1727+
be set to :data:`CERT_OPTIONAL` or :data:`CERT_REQUIRED`, too. The
1728+
actual client cert exchange is delayed until
1729+
:meth:`SSLSocket.verify_client_post_handshake` is called and some I/O is
1730+
performed.
1731+
1732+
.. versionadded:: 3.6.7
1733+
1734+
.. note::
1735+
Only available with OpenSSL 1.1.1 and TLS 1.3 enabled. Without TLS 1.3
1736+
support, the property value is None and can't be modified
1737+
16961738
.. attribute:: SSLContext.protocol
16971739

16981740
The protocol version chosen when constructing the context. This attribute

Doc/whatsnew/3.6.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1462,6 +1462,10 @@ Server and client-side specific TLS protocols for :class:`~ssl.SSLContext`
14621462
were added.
14631463
(Contributed by Christian Heimes in :issue:`28085`.)
14641464

1465+
Added :attr:`SSLContext.post_handshake_auth` to enable and
1466+
:meth:`ssl.SSLSocket.verify_client_post_handshake` to initiate TLS 1.3
1467+
post-handshake authentication.
1468+
(Contributed by Christian Heimes in :issue:`34670`.)
14651469

14661470
statistics
14671471
----------

Lib/ssl.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -714,6 +714,9 @@ def version(self):
714714
current SSL channel. """
715715
return self._sslobj.version()
716716

717+
def verify_client_post_handshake(self):
718+
return self._sslobj.verify_client_post_handshake()
719+
717720

718721
class SSLSocket(socket):
719722
"""This class implements a subtype of socket.socket that wraps
@@ -1054,6 +1057,12 @@ def unwrap(self):
10541057
else:
10551058
raise ValueError("No SSL wrapper around " + str(self))
10561059

1060+
def verify_client_post_handshake(self):
1061+
if self._sslobj:
1062+
return self._sslobj.verify_client_post_handshake()
1063+
else:
1064+
raise ValueError("No SSL wrapper around " + str(self))
1065+
10571066
def _real_close(self):
10581067
self._sslobj = None
10591068
socket._real_close(self)

Lib/test/test_ssl.py

Lines changed: 207 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1976,6 +1976,24 @@ def run(self):
19761976
sys.stdout.write(" server: read CB tls-unique from client, sending our CB data...\n")
19771977
data = self.sslconn.get_channel_binding("tls-unique")
19781978
self.write(repr(data).encode("us-ascii") + b"\n")
1979+
elif stripped == b'PHA':
1980+
if support.verbose and self.server.connectionchatty:
1981+
sys.stdout.write(
1982+
" server: initiating post handshake auth\n")
1983+
try:
1984+
self.sslconn.verify_client_post_handshake()
1985+
except ssl.SSLError as e:
1986+
self.write(repr(e).encode("us-ascii") + b"\n")
1987+
else:
1988+
self.write(b"OK\n")
1989+
elif stripped == b'HASCERT':
1990+
if self.sslconn.getpeercert() is not None:
1991+
self.write(b'TRUE\n')
1992+
else:
1993+
self.write(b'FALSE\n')
1994+
elif stripped == b'GETCERT':
1995+
cert = self.sslconn.getpeercert()
1996+
self.write(repr(cert).encode("us-ascii") + b"\n")
19791997
else:
19801998
if (support.verbose and
19811999
self.server.connectionchatty):
@@ -3629,6 +3647,194 @@ def test_session_handling(self):
36293647
'Session refers to a different SSLContext.')
36303648

36313649

3650+
def testing_context():
3651+
"""Create context
3652+
3653+
client_context, server_context, hostname = testing_context()
3654+
"""
3655+
client_context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
3656+
client_context.load_verify_locations(SIGNING_CA)
3657+
3658+
server_context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
3659+
server_context.load_cert_chain(SIGNED_CERTFILE)
3660+
server_context.load_verify_locations(SIGNING_CA)
3661+
3662+
return client_context, server_context, 'localhost'
3663+
3664+
3665+
@unittest.skipUnless(ssl.HAS_TLSv1_3, "Test needs TLS 1.3")
3666+
class TestPostHandshakeAuth(unittest.TestCase):
3667+
def test_pha_setter(self):
3668+
protocols = [
3669+
ssl.PROTOCOL_TLS, ssl.PROTOCOL_TLS_SERVER, ssl.PROTOCOL_TLS_CLIENT
3670+
]
3671+
for protocol in protocols:
3672+
ctx = ssl.SSLContext(protocol)
3673+
self.assertEqual(ctx.post_handshake_auth, False)
3674+
3675+
ctx.post_handshake_auth = True
3676+
self.assertEqual(ctx.post_handshake_auth, True)
3677+
3678+
ctx.verify_mode = ssl.CERT_REQUIRED
3679+
self.assertEqual(ctx.verify_mode, ssl.CERT_REQUIRED)
3680+
self.assertEqual(ctx.post_handshake_auth, True)
3681+
3682+
ctx.post_handshake_auth = False
3683+
self.assertEqual(ctx.verify_mode, ssl.CERT_REQUIRED)
3684+
self.assertEqual(ctx.post_handshake_auth, False)
3685+
3686+
ctx.verify_mode = ssl.CERT_OPTIONAL
3687+
ctx.post_handshake_auth = True
3688+
self.assertEqual(ctx.verify_mode, ssl.CERT_OPTIONAL)
3689+
self.assertEqual(ctx.post_handshake_auth, True)
3690+
3691+
def test_pha_required(self):
3692+
client_context, server_context, hostname = testing_context()
3693+
server_context.post_handshake_auth = True
3694+
server_context.verify_mode = ssl.CERT_REQUIRED
3695+
client_context.post_handshake_auth = True
3696+
client_context.load_cert_chain(SIGNED_CERTFILE)
3697+
3698+
server = ThreadedEchoServer(context=server_context, chatty=False)
3699+
with server:
3700+
with client_context.wrap_socket(socket.socket(),
3701+
server_hostname=hostname) as s:
3702+
s.connect((HOST, server.port))
3703+
s.write(b'HASCERT')
3704+
self.assertEqual(s.recv(1024), b'FALSE\n')
3705+
s.write(b'PHA')
3706+
self.assertEqual(s.recv(1024), b'OK\n')
3707+
s.write(b'HASCERT')
3708+
self.assertEqual(s.recv(1024), b'TRUE\n')
3709+
# PHA method just returns true when cert is already available
3710+
s.write(b'PHA')
3711+
self.assertEqual(s.recv(1024), b'OK\n')
3712+
s.write(b'GETCERT')
3713+
cert_text = s.recv(4096).decode('us-ascii')
3714+
self.assertIn('Python Software Foundation CA', cert_text)
3715+
3716+
def test_pha_required_nocert(self):
3717+
client_context, server_context, hostname = testing_context()
3718+
server_context.post_handshake_auth = True
3719+
server_context.verify_mode = ssl.CERT_REQUIRED
3720+
client_context.post_handshake_auth = True
3721+
3722+
server = ThreadedEchoServer(context=server_context, chatty=False)
3723+
with server:
3724+
with client_context.wrap_socket(socket.socket(),
3725+
server_hostname=hostname) as s:
3726+
s.connect((HOST, server.port))
3727+
s.write(b'PHA')
3728+
# receive CertificateRequest
3729+
self.assertEqual(s.recv(1024), b'OK\n')
3730+
# send empty Certificate + Finish
3731+
s.write(b'HASCERT')
3732+
# receive alert
3733+
with self.assertRaisesRegex(
3734+
ssl.SSLError,
3735+
'tlsv13 alert certificate required'):
3736+
s.recv(1024)
3737+
3738+
def test_pha_optional(self):
3739+
if support.verbose:
3740+
sys.stdout.write("\n")
3741+
3742+
client_context, server_context, hostname = testing_context()
3743+
server_context.post_handshake_auth = True
3744+
server_context.verify_mode = ssl.CERT_REQUIRED
3745+
client_context.post_handshake_auth = True
3746+
client_context.load_cert_chain(SIGNED_CERTFILE)
3747+
3748+
# check CERT_OPTIONAL
3749+
server_context.verify_mode = ssl.CERT_OPTIONAL
3750+
server = ThreadedEchoServer(context=server_context, chatty=False)
3751+
with server:
3752+
with client_context.wrap_socket(socket.socket(),
3753+
server_hostname=hostname) as s:
3754+
s.connect((HOST, server.port))
3755+
s.write(b'HASCERT')
3756+
self.assertEqual(s.recv(1024), b'FALSE\n')
3757+
s.write(b'PHA')
3758+
self.assertEqual(s.recv(1024), b'OK\n')
3759+
s.write(b'HASCERT')
3760+
self.assertEqual(s.recv(1024), b'TRUE\n')
3761+
3762+
def test_pha_optional_nocert(self):
3763+
if support.verbose:
3764+
sys.stdout.write("\n")
3765+
3766+
client_context, server_context, hostname = testing_context()
3767+
server_context.post_handshake_auth = True
3768+
server_context.verify_mode = ssl.CERT_OPTIONAL
3769+
client_context.post_handshake_auth = True
3770+
3771+
server = ThreadedEchoServer(context=server_context, chatty=False)
3772+
with server:
3773+
with client_context.wrap_socket(socket.socket(),
3774+
server_hostname=hostname) as s:
3775+
s.connect((HOST, server.port))
3776+
s.write(b'HASCERT')
3777+
self.assertEqual(s.recv(1024), b'FALSE\n')
3778+
s.write(b'PHA')
3779+
self.assertEqual(s.recv(1024), b'OK\n')
3780+
# optional doens't fail when client does not have a cert
3781+
s.write(b'HASCERT')
3782+
self.assertEqual(s.recv(1024), b'FALSE\n')
3783+
3784+
def test_pha_no_pha_client(self):
3785+
client_context, server_context, hostname = testing_context()
3786+
server_context.post_handshake_auth = True
3787+
server_context.verify_mode = ssl.CERT_REQUIRED
3788+
client_context.load_cert_chain(SIGNED_CERTFILE)
3789+
3790+
server = ThreadedEchoServer(context=server_context, chatty=False)
3791+
with server:
3792+
with client_context.wrap_socket(socket.socket(),
3793+
server_hostname=hostname) as s:
3794+
s.connect((HOST, server.port))
3795+
with self.assertRaisesRegex(ssl.SSLError, 'not server'):
3796+
s.verify_client_post_handshake()
3797+
s.write(b'PHA')
3798+
self.assertIn(b'extension not received', s.recv(1024))
3799+
3800+
def test_pha_no_pha_server(self):
3801+
# server doesn't have PHA enabled, cert is requested in handshake
3802+
client_context, server_context, hostname = testing_context()
3803+
server_context.verify_mode = ssl.CERT_REQUIRED
3804+
client_context.post_handshake_auth = True
3805+
client_context.load_cert_chain(SIGNED_CERTFILE)
3806+
3807+
server = ThreadedEchoServer(context=server_context, chatty=False)
3808+
with server:
3809+
with client_context.wrap_socket(socket.socket(),
3810+
server_hostname=hostname) as s:
3811+
s.connect((HOST, server.port))
3812+
s.write(b'HASCERT')
3813+
self.assertEqual(s.recv(1024), b'TRUE\n')
3814+
# PHA doesn't fail if there is already a cert
3815+
s.write(b'PHA')
3816+
self.assertEqual(s.recv(1024), b'OK\n')
3817+
s.write(b'HASCERT')
3818+
self.assertEqual(s.recv(1024), b'TRUE\n')
3819+
3820+
def test_pha_not_tls13(self):
3821+
# TLS 1.2
3822+
client_context, server_context, hostname = testing_context()
3823+
server_context.verify_mode = ssl.CERT_REQUIRED
3824+
client_context.options |= ssl.OP_NO_TLSv1_3
3825+
client_context.post_handshake_auth = True
3826+
client_context.load_cert_chain(SIGNED_CERTFILE)
3827+
3828+
server = ThreadedEchoServer(context=server_context, chatty=False)
3829+
with server:
3830+
with client_context.wrap_socket(socket.socket(),
3831+
server_hostname=hostname) as s:
3832+
s.connect((HOST, server.port))
3833+
# PHA fails for TLS != 1.3
3834+
s.write(b'PHA')
3835+
self.assertIn(b'WRONG_SSL_VERSION', s.recv(1024))
3836+
3837+
36323838
def test_main(verbose=False):
36333839
if support.verbose:
36343840
import warnings
@@ -3681,6 +3887,7 @@ def test_main(verbose=False):
36813887
thread_info = support.threading_setup()
36823888
if thread_info:
36833889
tests.append(ThreadedTests)
3890+
tests.append(TestPostHandshakeAuth)
36843891

36853892
try:
36863893
support.run_unittest(*tests)
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Add SSLContext.post_handshake_auth and
2+
SSLSocket.verify_client_post_handshake for TLS 1.3's post
3+
handshake authentication feature.

0 commit comments

Comments
 (0)
0