8000 bpo-34670: Add TLS 1.3 post handshake auth · python/cpython@8418bf5 · GitHub
[go: up one dir, main page]

Skip to content

Commit 8418bf5

Browse files
committed
bpo-34670: Add TLS 1.3 post handshake auth
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>
1 parent c0da582 commit 8418bf5

File tree

9 files changed

+370
-16
lines changed

9 files changed

+370
-16
lines changed

.travis.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ cache:
1212

1313
env:
1414
global:
15-
- OPENSSL=1.1.0h
15+
- OPENSSL=1.1.0i
1616
- OPENSSL_DIR="$HOME/multissl/openssl/${OPENSSL}"
1717
- PATH="${OPENSSL_DIR}/bin:$PATH"
1818
# Use -O3 because we don't use debugger on Travis-CI

Doc/library/ssl.rst

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

1317+
.. method:: SSLSocket.verify_client_post_handshake()
1318+
1319+
Requests post-handshake authentication (PHA) from a TLS 1.3 client. PHA
1320+
can only be initiated for a TLS 1.3 connection from a server-side socket,
1321+
after the initial TLS handshake and with PHA enabled on both sides, see
1322+
:attr:`SSLContext.post_handshake_auth`.
1323+
1324+
The method does not perform a cert exchange immediately. The server-side
1325+
sends a CertificateRequest during the next write event and expects the
1326+
client to respond with a certificate on the next read event.
1327+
1328+
If any precondition isn't met (e.g. not TLS 1.3, PHA not enabled), an
1329+
:exc:`SSLError` is raised.
1330+
1331+
.. versionadded:: 3.8
1332+
1333+
.. note::
1334+
Only available with OpenSSL 1.1.1 and TLS 1.3 enabled. Without TLS 1.3
1335+
support, the method raises :exc:`NotImplementedError`.
1336+
13171337
.. method:: SSLSocket.version()
13181338

13191339
Return the actual SSL protocol version negotiated by the connection
@@ -1929,6 +1949,28 @@ to speed up repeated connections from the same clients.
19291949
>>> ssl.create_default_context().options # doctest: +SKIP
19301950
<Options.OP_ALL|OP_NO_SSLv3|OP_NO_SSLv2|OP_NO_COMPRESSION: 2197947391>
19311951

1952+
.. attribute:: SSLContext.post_handshake_auth
1953+
1954+
Enable TLS 1.3 post-handshake client authentication. Post-handshake auth
1955+
is disabled by default and a server can only request a TLS client
1956+
certificate during the initial handshake. When enabled, a server may
1957+
request a TLS client certificate at any time after the handshake.
1958+
1959+
When enabled on client-side sockets, the client signals the server that
1960+
it supports post-handshake authentication.
1961+
1962+
When enabled on server-side sockets, :attr:`SSLContext.verify_mode` must
1963+
be set to :data:`CERT_OPTIONAL` or :data:`CERT_REQUIRED`, too. The
1964+
actual client cert exchange is delayed until
1965+
:meth:`SSLSocket.verify_client_post_handshake` is called and some I/O is
1966+
performed.
1967+
1968+
.. versionadded:: 3.8
1969+
1970+
.. note::
1971+
Only available with OpenSSL 1.1.1 and TLS 1.3 enabled. Without TLS 1.3
1972+
support, the property value is None and can't be modified
1973+
19321974
.. attribute:: SSLContext.protocol
19331975

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

Doc/whatsnew/3.8.rst

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -136,14 +136,21 @@ pathlib
136136
contain characters unrepresentable at the OS level.
137137
(Contributed by Serhiy Storchaka in :issue:`33721`.)
138138

139+
ssl
140+
---
141+
10000
142+
Added :attr:`SSLContext.post_handshake_auth` to enable and
143+
:meth:`ssl.SSLSocket.verify_client_post_handshake` to initiate TLS 1.3
144+
post-handshake authentication.
145+
(Contributed by Christian Heimes in :issue:`34670`.)
146+
139147
venv
140148
----
141149

142150
* :mod:`venv` now includes an ``Activate.ps1`` script on all platforms for
143151
activating virtual environments under PowerShell Core 6.1.
144152
(Contributed by Brett Cannon in :issue:`32718`.)
145153

146-
147154
Optimizations
148155
=============
149156

Lib/ssl.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -777,6 +777,9 @@ def version(self):
777777
current SSL channel. """
778778
return self._sslobj.version()
779779

780+
def verify_client_post_handshake(self):
781+
return self._sslobj.verify_client_post_handshake()
782+
780783

781784
class SSLSocket(socket):
782785
"""This class implements a subtype of socket.socket that wraps
@@ -1094,6 +1097,12 @@ def unwrap(self):
10941097
else:
10951098
raise ValueError("No SSL wrapper around " + str(self))
10961099

1100+
def verify_client_post_handshake(self):
1101+
if self._sslobj:
1102+
return self._sslobj.verify_client_post_handshake()
1103+
else:
1104+
raise ValueError("No SSL wrapper around " + str(self))
1105+
10971106
def _real_close(self):
10981107
self._sslobj = None
10991108
super()._real_close()

Lib/test/test_ssl.py

Lines changed: 192 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -218,7 +218,7 @@ def testing_context(server_cert=SIGNED_CERTFILE):
218218

219219
server_context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
220220
server_context.load_cert_chain(server_cert)
221-
client_context.load_verify_locations(SIGNING_CA)
221+
server_context.load_verify_locations(SIGNING_CA)
222222

223223
return client_context, server_context, hostname
224224

@@ -2262,6 +2262,23 @@ def run(self):
22622262
sys.stdout.write(" server: read CB tls-unique from client, sending our CB data...\n")
22632263
data = self.sslconn.get_channel_binding("tls-unique")
22642264
self.write(repr(data).encode("us-ascii") + b"\n")
2265+
elif stripped == b'PHA':
2266+
if support.verbose and self.server.connectionchatty:
2267+
sys.stdout.write(" server: initiating post handshake auth\n")
2268+
try:
2269+
self.sslconn.verify_client_post_handshake()
2270+
except ssl.SSLError as e:
2271+
self.write(repr(e).encode("us-ascii") + b"\n")
2272+
else:
2273+
self.write(b"OK\n")
2274+
elif stripped == b'HASCERT':
2275+
if self.sslconn.getpeercert() is not None:
2276+
self.write(b'TRUE\n')
2277+
else:
2278+
self.write(b'FALSE\n')
2279+
elif stripped == b'GETCERT':
2280+
cert = self.sslconn.getpeercert()
2281+
self.write(repr(cert).encode("us-ascii") + b"\n")
22652282
else:
22662283
if (support.verbose and
22672284
self.server.connectionchatty):
@@ -4148,6 +4165,179 @@ def test_session_handling(self):
41484165
'Session refers to a different SSLContext.')
41494166

41504167

4168+
@unittest.skipUnless(ssl.HAS_TLSv1_3, "Test needs TLS 1.3")
4169+
class TestPostHandshakeAuth(unittest.TestCase):
4170+
def test_pha_setter(self):
4171+
protocols = [
4172+
ssl.PROTOCOL_TLS, ssl.PROTOCOL_TLS_SERVER, ssl.PROTOCOL_TLS_CLIENT
4173+
]
4174+
for protocol in protocols:
4175+
ctx = ssl.SSLContext(protocol)
4176+
self.assertEqual(ctx.post_handshake_auth, False)
4177+
4178+
ctx.post_handshake_auth = True
4179+
self.assertEqual(ctx.post_handshake_auth, True)
4180+
4181+
ctx.verify_mode = ssl.CERT_REQUIRED
4182+
self.assertEqual(ctx.verify_mode, ssl.CERT_REQUIRED)
4183+
self.assertEqual(ctx.post_handshake_auth, True)
4184+
4185+
ctx.post_handshake_auth = False
4186+
self.assertEqual(ctx.verify_mode, ssl.CERT_REQUIRED)
4187+
self.assertEqual(ctx.post_handshake_auth, False)
4188+
4189+
ctx.verify_mode = ssl.CERT_OPTIONAL
4190+
ctx.post_handshake_auth = True
4191+
self.assertEqual(ctx.verify_mode, ssl.CERT_OPTIONAL)
4192+
self.assertEqual(ctx.post_handshake_auth, True)
4193+
4194+
def test_pha_required(self):
4195+
client_context, server_context, hostname = testing_context()
4196+
server_context.post_handshake_auth = True
4197+
server_context.verify_mode = ssl.CERT_REQUIRED
4198+
client_context.post_handshake_auth = True
4199+
client_context.load_cert_chain(SIGNED_CERTFILE)
4200+
4201+
server = ThreadedEchoServer(context=server_context, chatty=False)
4202+
with server:
4203+
with client_context.wrap_socket(socket.socket(),
4204+
server_hostname=hostname) as s:
4205+
s.connect((HOST, server.port))
4206+
s.write(b'HASCERT')
4207+
self.assertEqual(s.recv(1024), b'FALSE\n')
4208+
s.write(b'PHA')
4209+
self.assertEqual(s.recv(1024), b'OK\n')
4210+
s.write(b'HASCERT')
4211+
self.assertEqual(s.recv(1024), b'TRUE\n')
4212+
# PHA method just returns true when cert is already available
4213+
s.write(b'PHA')
4214+
self.assertEqual(s.recv(1024), b'OK\n')
4215+
s.write(b'GETCERT')
4216+
cert_text = s.recv(4096).decode('us-ascii')
4217+
self.assertIn('Python Software Foundation CA', cert_text)
4218+
4219+
def test_pha_required_nocert(self):
4220+
client_context, server_context, hostname = testing_context()
4221+
server_context.post_handshake_auth = True
4222+
server_context.verify_mode = ssl.CERT_REQUIRED
4223+
client_context.post_handshake_auth = True
4224+
4225+
server = ThreadedEchoServer(context=server_context, chatty=False)
4226+
with server:
4227+
with client_context.wrap_socket(socket.socket(),
4228+
server_hostname=hostname) as s:
4229+
s.connect((HOST, server.port))
4230+
s.write(b'PHA')
4231+
# receive CertificateRequest
4232+
self.assertEqual(s.recv(1024), b'OK\n')
4233+
# send empty Certificate + Finish
4234+
s.write(b'HASCERT')
4235+
# receive alert
4236+
with self.assertRaisesRegex(
4237+
ssl.SSLError,
4238+
'tlsv13 alert certificate required'):
4239+
s.recv(1024)
4240+
4241+
def test_pha_optional(self):
4242+
if support.verbose:
4243+
sys.stdout.write("\n")
4244+
4245+
client_context, server_context, hostname = testing_context()
4246+
server_context.post_handshake_auth = True
4247+
server_context.verify_mode = ssl.CERT_REQUIRED
4248+
client_context.post_handshake_auth = True
4249+
client_context.load_cert_chain(SIGNED_CERTFILE)
4250+
4251+
# check CERT_OPTIONAL
4252+
server_context.verify_mode = ssl.CERT_OPTIONAL
4253+
server = ThreadedEchoServer(context=server_context, chatty=False)
4254+
with server:
4255+
with client_context.wrap_socket(socket.socket(),
4256+
server_hostname=hostname) as s:
4257+
s.connect((HOST, server.port))
4258+
s.write(b'HASCERT')
4259+
self.assertEqual(s.recv(1024), b'FALSE\n')
4260+
s.write(b'PHA')
4261+
self.assertEqual(s.recv(1024), b'OK\n')
4262+
s.write(b'HASCERT')
4263+
self.assertEqual(s.recv(1024), b'TRUE\n')
4264+
4265+
def test_pha_optional_nocert(self):
4266+
if support.verbose:
4267+
sys.stdout.write("\n")
4268+
4269+
client_context, server_context, hostname = testing_context()
4270+
server_context.post_handshake_auth = True
4271+
server_context.verify_mode = ssl.CERT_OPTIONAL
4272+
client_context.post_handshake_auth = True
4273+
4274+
server = ThreadedEchoServer(context=server_context, chatty=False)
4275+
with server:
4276+
with client_context.wrap_socket(socket.socket(),
4277+
server_hostname=hostname) as s:
4278+
s.connect((HOST, server.port))
4279+
s.write(b'HASCERT')
4280+
self.assertEqual(s.recv(1024), b'FALSE\n')
4281+
s.write(b'PHA')
4282+
self.assertEqual(s.recv(1024), b'OK\n')
4283+
# optional doens't fail when client does not have a cert
4284+
s.write(b'HASCERT')
4285+
self.assertEqual(s.recv(1024), b'FALSE\n')
4286+
4287+
def test_pha_no_pha_client(self):
4288+
client_context, server_context, hostname = testing_context()
4289+
server_context.post_handshake_auth = True
4290+
server_context.verify_mode = ssl.CERT_REQUIRED
4291+
client_context.load_cert_chain(SIGNED_CERTFILE)
4292+
4293+
server = ThreadedEchoServer(context=server_context, chatty=False)
4294+
with server:
4295+
with client_context.wrap_socket(socket.socket(),
4296+
server_hostname=hostname) as s:
4297+
s.connect((HOST, server.port))
4298+
with self.assertRaisesRegex(ssl.SSLError, 'not server'):
4299+
s.verify_client_post_handshake()
4300+
s.write(b'PHA')
4301+
self.assertIn(b'extension not received', s.recv(1024))
4302+
4303+
def test_pha_no_pha_server(self):
4304+
# server doesn't have PHA enabled, cert is requested in handshake
4305+
client_context, server_context, hostname = testing_context()
4306+
server_context.verify_mode = ssl.CERT_REQUIRED
4307+
client_context.post_handshake_auth = True
4308+
client_context.load_cert_chain(SIGNED_CERTFILE)
4309+
4310+
server = ThreadedEchoServer(context=server_context, chatty=False)
4311+
with server:
4312+
with client_context.wrap_socket(socket.socket(),
4313+
server_hostname=hostname) as s:
4314+
s.connect((HOST, server.port))
4315+
s.write(b'HASCERT')
4316+
self.assertEqual(s.recv(1024), b'TRUE\n')
4317+
# PHA doesn't fail if there is already a cert
4318+
s.write(b'PHA')
4319+
self.assertEqual(s.recv(1024), b'OK\n')
4320+
s.write(b'HASCERT')
4321+
self.assertEqual(s.recv(1024), b'TRUE\n')
4322+
4323+
def test_pha_not_tls13(self):
4324+
# TLS 1.2
4325+
client_context, server_context, hostname = testing_context()
4326+
server_context.verify_mode = ssl.CERT_REQUIRED
4327+
client_context.maximum_version = ssl.TLSVersion.TLSv1_2
4328+
client_context.post_handshake_auth = True
4329+
client_context.load_cert_chain(SIGNED_CERTFILE)
4330+
4331+
server = ThreadedEchoServer(context=server_context, chatty=False)
4332+
with server:
4333+
with client_context.wrap_socket(socket.socket(),
4334+
server_hostname=hostname) as s:
4335+
s.connect((HOST, server.port))
4336+
# PHA fails for TLS != 1.3
4337+
s.write(b'PHA')
4338+
self.assertIn(b'WRONG_SSL_VERSION', s.recv(1024))
4339+
4340+
41514341
def test_main(verbose=False):
41524342
if support.verbose:
41534343
import warnings
@@ -4183,6 +4373,7 @@ def test_main(verbose=False):
41834373
tests = [
41844374
ContextTests, BasicSocketTests, SSLErrorTests, MemoryBIOTests,
41854375
SSLObjectTests, SimpleBackgroundTests, ThreadedTests,
4376+
TestPostHandshakeAuth
41864377
]
41874378

41884379
if support.is_resource_enabled('network'):
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