10000 [3.7] bpo-34670: Add TLS 1.3 post handshake auth (GH-9460) by tiran · Pull Request #9505 · python/cpython · GitHub
[go: up one dir, main page]

Skip to content

[3.7] bpo-34670: Add TLS 1.3 post handshake auth (GH-9460) #9505

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Sep 23, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ cache:

env:
global:
- OPENSSL=1.1.0h
- OPENSSL=1.1.0i
- OPENSSL_DIR="$HOME/multissl/openssl/${OPENSSL}"
- PATH="${OPENSSL_DIR}/bin:$PATH"
# Use -O3 because we don't use debugger on Travis-CI
Expand Down
42 changes: 42 additions & 0 deletions Doc/library/ssl.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1314,6 +1314,26 @@ SSL sockets also have the following additional methods and attributes:
returned socket should always be used for further communication with the
other side of the connection, rather than the original socket.

.. method:: SSLSocket.verify_client_post_handshake()

Requests post-handshake authentication (PHA) from a TLS 1.3 client. PHA
can only be initiated for a TLS 1.3 connection from a server-side socket,
after the initial TLS handshake and with PHA enabled on both sides, see
:attr:`SSLContext.post_handshake_auth`.

The method does not perform a cert exchange immediately. The server-side
sends a CertificateRequest during the next write event and expects the
client to respond with a certificate on the next read event.

If any precondition isn't met (e.g. not TLS 1.3, PHA not enabled), an
:exc:`SSLError` is raised.

.. versionadded:: 3.7.1

.. note::
Only available with OpenSSL 1.1.1 and TLS 1.3 enabled. Without TLS 1.3
support, the method raises :exc:`NotImplementedError`.

.. method:: SSLSocket.version()

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

.. attribute:: SSLContext.post_handshake_auth

Enable TLS 1.3 post-handshake client authentication. Post-handshake auth
is disabled by default and a server can only request a TLS client
certificate during the initial handshake. When enabled, a server may
request a TLS client certificate at any time after the handshake.

When enabled on client-side sockets, the client signals the server that
it supports post-handshake authentication.

When enabled on server-side sockets, :attr:`SSLContext.verify_mode` must
be set to :data:`CERT_OPTIONAL` or :data:`CERT_REQUIRED`, too. The
actual client cert exchange is delayed until
:meth:`SSLSocket.verify_client_post_handshake` is called and some I/O is
performed.

.. versionadded:: 3.7.1

.. note::
Only available with OpenSSL 1.1.1 and TLS 1.3 enabled. Without TLS 1.3
support, the property value is None and can't be modified

.. attribute:: SSLContext.protocol

The protocol version chosen when constructing the context. This attribute
Expand Down
4 changes: 4 additions & 0 deletions Doc/whatsnew/3.7.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1350,6 +1350,10 @@ Supported protocols are indicated by serveral new flags, such as
:data:`~ssl.HAS_TLSv1_1`.
(Contributed by Christian Heimes in :issue:`32609`.)

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

string
------
Expand Down
9 changes: 9 additions & 0 deletions Lib/ssl.py
Original file line number Diff line number Diff line change
Expand Up @@ -777,6 +777,9 @@ def version(self):
current SSL channel. """
return self._sslobj.version()

def verify_client_post_handshake(self):
return self._sslobj.verify_client_post_handshake()


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

def verify_client_post_handshake(self):
if self._sslobj:
return self._sslobj.verify_client_post_handshake()
else:
raise ValueError("No SSL wrapper around " + str(self))

def _real_close(self):
self._sslobj = None
super()._real_close()
Expand Down
193 changes: 192 additions & 1 deletion Lib/test/test_ssl.py
Original file line number Diff line number Diff line change
Expand Up @@ -234,7 +234,7 @@ def testing_context(server_cert=SIGNED_CERTFILE):

server_context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
server_context.load_cert_chain(server_cert)
client_context.load_verify_locations(SIGNING_CA)
server_context.load_verify_locations(SIGNING_CA)

return client_context, server_context, hostname

Expand Down Expand Up @@ -2282,6 +2282,23 @@ def run(self):
sys.stdout.write(" server: read CB tls-unique from client, sending our CB data...\n")
data = self.sslconn.get_channel_binding("tls-unique")
self.write(repr(data).encode("us-ascii") + b"\n")
elif stripped == b'PHA':
if support.verbose and self.server.connectionchatty:
sys.stdout.write(" server: initiating post handshake auth\n")
try:
self.sslconn.verify_client_post_handshake()
except ssl.SSLError as e:
self.write(repr(e).encode("us-ascii") + b"\n")
else:
self.write(b"OK\n")
elif stripped == b'HASCERT':
if self.sslconn.getpeercert() is not None:
self.write(b'TRUE\n')
else:
self.write(b'FALSE\n')
elif stripped == b'GETCERT':
cert = self.sslconn.getpeercert()
self.write(repr(cert).encode("us-ascii") + b"\n")
else:
if (support.verbose and
self.server.connectionchatty):
Expand Down Expand Up @@ -4175,6 +4192,179 @@ def test_session_handling(self):
'Session refers to a different SSLContext.')


@unittest.skipUnless(ssl.HAS_TLSv1_3, "Test needs TLS 1.3")
class TestPostHandshakeAuth(unittest.TestCase):
def test_pha_setter(self):
protocols = [
ssl.PROTOCOL_TLS, ssl.PROTOCOL_TLS_SERVER, ssl.PROTOCOL_TLS_CLIENT
]
for protocol in protocols:
ctx = ssl.SSLContext(protocol)
self.assertEqual(ctx.post_handshake_auth, False)

ctx.post_handshake_auth = True
self.assertEqual(ctx.post_handshake_auth, True)

ctx.verify_mode = ssl.CERT_REQUIRED
self.assertEqual(ctx.verify_mode, ssl.CERT_REQUIRED)
self.assertEqual(ctx.post_handshake_auth, True)

ctx.post_handshake_auth = False
self.assertEqual(ctx.verify_mode, ssl.CERT_REQUIRED)
self.assertEqual(ctx.post_handshake_auth, False)

ctx.verify_mode = ssl.CERT_OPTIONAL
ctx.post_handshake_auth = True
self.assertEqual(ctx.verify_mode, ssl.CERT_OPTIONAL)
self.assertEqual(ctx.post_handshake_auth, True)

def test_pha_required(self):
client_context, server_context, hostname = testing_context()
server_context.post_handshake_auth = True
server_context.verify_mode = ssl.CERT_REQUIRED
client_context.post_handshake_auth = True
client_context.load_cert_chain(SIGNED_CERTFILE)

server = ThreadedEchoServer(context=server_context, chatty=False)
with server:
with client_context.wrap_socket(socket.socket(),
server_hostname=hostname) as s:
s.connect((HOST, server.port))
s.write(b'HASCERT')
self.assertEqual(s.recv(1024), b'FALSE\n')
s.write(b'PHA')
self.assertEqual(s.recv(1024), b'OK\n')
s.write(b'HASCERT')
self.assertEqual(s.recv(1024), b'TRUE\n')
# PHA method just returns true when cert is already available
s.write(b'PHA')
self.assertEqual(s.recv(1024), b'OK\n')
s.write(b'GETCERT')
cert_text = s.recv(4096).decode('us-ascii')
self.assertIn('Python Software Foundation CA', cert_text)

def test_pha_required_nocert(self):
client_context, server_context, hostname = testing_context()
server_context.post_handshake_auth = True
server_context.verify_mode = ssl.CERT_REQUIRED
client_context.post_handshake_auth = True

server = ThreadedEchoServer(context=server_context, chatty=False)
with server:
with client_context.wrap_socket(socket.socket(),
server_hostname=hostname) as s:
s.connect((HOST, server.port))
s.write(b'PHA')
# receive CertificateRequest
self.assertEqual(s.recv(1024), b'OK\n')
# send empty Certificate + Finish
s.write(b'HASCERT')
# receive alert
with self.assertRaisesRegex(
ssl.SSLError,
'tlsv13 alert certificate required'):
s.recv(1024)

def test_pha_optional(self):
if support.verbose:
sys.stdout.write("\n")

client_context, server_context, hostname = testing_context()
server_context.post_handshake_auth = True
server_context.verify_mode = ssl.CERT_REQUIRED
client_context.post_handshake_auth = True
client_context.load_cert_chain(SIGNED_CERTFILE)

# check CERT_OPTIONAL
server_context.verify_mode = ssl.CERT_OPTIONAL
server = ThreadedEchoServer(context=server_context, chatty=False)
with server:
with client_context.wrap_socket(socket.socket(),
server_hostname=hostname) as s:
s.connect((HOST, server.port))
s.write(b'HASCERT')
self.assertEqual(s.recv(1024), b'FALSE\n')
s.write(b'PHA')
self.assertEqual(s.recv(1024), b'OK\n')
s.write(b'HASCERT')
self.assertEqual(s.recv(1024), b'TRUE\n')

def test_pha_optional_nocert(self):
if support.verbose:
sys.stdout.write("\n")

client_context, server_context, hostname = testing_context()
server_context.post_handshake_auth = True
server_context.verify_mode = ssl.CERT_OPTIONAL
client_context.post_handshake_auth = True

server = ThreadedEchoServer(context=server_context, chatty=False)
with server:
with client_context.wrap_socket(socket.socket(),
server_hostname=hostname) as s:
s.connect((HOST, server.port))
s.write(b'HASCERT')
self.assertEqual(s.recv(1024), b'FALSE\n')
s.write(b'PHA')
self.assertEqual(s.recv(1024), b'OK\n')
# optional doens't fail when client does not have a cert
s.write(b'HASCERT')
self.assertEqual(s.recv(1024), b'FALSE\n')

def test_pha_no_pha_client(self):
client_context, server_context, hostname = testing_context()
server_context.post_handshake_auth = True
server_context.verify_mode = ssl.CERT_REQUIRED
client_context.load_cert_chain(SIGNED_CERTFILE)

server = ThreadedEchoServer(context=server_context, chatty=False)
with server:
with client_context.wrap_socket(socket.socket(),
server_hostname=hostname) as s:
s.connect((HOST, server.port))
with self.assertRaisesRegex(ssl.SSLError, 'not server'):
s.verify_client_post_handshake()
s.write(b'PHA')
self.assertIn(b'extension not received', s.recv(1024))

def test_pha_no_pha_server(self):
# server doesn't have PHA enabled, cert is requested in handshake
client_context, server_context, hostname = testing_context()
server_context.verify_mode = ssl.CERT_REQUIRED
client_context.post_handshake_auth = True
client_context.load_cert_chain(SIGNED_CERTFILE)

server = ThreadedEchoServer(context=server_context, chatty=False)
with server:
with client_context.wrap_socket(socket.socket(),
server_hostname=hostname) as s:
s.connect((HOST, server.port))
s.write(b'HASCERT')
self.assertEqual(s.recv(1024), b'TRUE\n')
# PHA doesn't fail if there is already a cert
s.write(b'PHA')
self.assertEqual(s.recv(1024), b'OK\n')
s.write(b'HASCERT')
self.assertEqual(s.recv(1024), b'TRUE\n')

def test_pha_not_tls13(self):
# TLS 1.2
client_context, server_context, hostname = testing_context()
server_context.verify_mode = ssl.CERT_REQUIRED
client_context.maximum_version = ssl.TLSVersion.TLSv1_2
client_context.post_handshake_auth = True
client_context.load_cert_chain(SIGNED_CERTFILE)

server = ThreadedEchoServer(context=server_context, chatty=False)
with server:
with client_context.wrap_socket(socket.socket(),
server_hostname=hostname) as s:
s.connect((HOST, server.port))
# PHA fails for TLS != 1.3
s.write(b'PHA')
self.assertIn(b'WRONG_SSL_VERSION', s.recv(1024))


def test_main(verbose=False):
if support.verbose:
import warnings
Expand Down Expand Up @@ -4218,6 +4408,7 @@ def test_main(verbose=False):
tests = [
ContextTests, BasicSocketTests, SSLErrorTests, MemoryBIOTests,
SSLObjectTests, SimpleBackgroundTests, ThreadedTests,
TestPostHandshakeAuth
]

if support.is_resource_enabled('network'):
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Add SSLContext.post_handshake_auth and
SSLSocket.verify_client_post_handshake for TLS 1.3's post
handshake authentication feature.
Loading
0