Skip to content

Commit 69f6f4e

Browse files
committed
[3.7] bpo-34670: Add TLS 1.3 post handshake auth (pythonGH-9460)
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>
1 parent 44989bc commit 69f6f4e

9 files changed

Lines changed: 366 additions & 15 deletions

File tree

‎.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.7.1
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.7.1
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.7.rst‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1350,6 +1350,10 @@ Supported protocols are indicated by serveral new flags, such as
13501350
:data:`~ssl.HAS_TLSv1_1`.
13511351
(Contributed by Christian Heimes in :issue:`32609`.)
13521352

1353+
Added :attr:`SSLContext.post_handshake_auth` to enable and
1354+
:meth:`ssl.SSLSocket.verify_client_post_handshake` to initiate TLS 1.3
1355+
post-handshake authentication.
1356+
(Contributed by Christian Heimes in :issue:`34670`.)
13531357

13541358
string
13551359
------

‎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
@@ -234,7 +234,7 @@ def testing_context(server_cert=SIGNED_CERTFILE):
234234

235235
server_context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
236236
server_context.load_cert_chain(server_cert)
237-
client_context.load_verify_locations(SIGNING_CA)
237+
server_context.load_verify_locations(SIGNING_CA)
238238

239239
return client_context, server_context, hostname
240240

@@ -2282,6 +2282,23 @@ def run(self):
22822282
sys.stdout.write(" server: read CB tls-unique from client, sending our CB data...\n")
22832283
data = self.sslconn.get_channel_binding("tls-unique")
22842284
self.write(repr(data).encode("us-ascii") + b"\n")
2285+
elif stripped == b'PHA':
2286+
if support.verbose and self.server.connectionchatty:
2287+
sys.stdout.write(" server: initiating post handshake auth\n")
2288+
try:
2289+
self.sslconn.verify_client_post_handshake()
2290+
except ssl.SSLError as e:
2291+
self.write(repr(e).encode("us-ascii") + b"\n")
2292+
else:
2293+
self.write(b"OK\n")
2294+
elif stripped == b'HASCERT':
2295+
if self.sslconn.getpeercert() is not None:
2296+
self.write(b'TRUE\n')
2297+
else:
2298+
self.write(b'FALSE\n')
2299+
elif stripped == b'GETCERT':
2300+
cert = self.sslconn.getpeercert()
2301+
self.write(repr(cert).encode("us-ascii") + b"\n")
22852302
else:
22862303
if (support.verbose and
22872304
self.server.connectionchatty):
@@ -4175,6 +4192,179 @@ def test_session_handling(self):
41754192
'Session refers to a different SSLContext.')
41764193

41774194

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

42234414
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)