diff options
-rw-r--r-- | .travis.yml | 2 | ||||
-rw-r--r-- | Doc/library/ssl.rst | 42 | ||||
-rw-r--r-- | Doc/whatsnew/3.8.rst | 9 | ||||
-rw-r--r-- | Lib/ssl.py | 9 | ||||
-rw-r--r-- | Lib/test/test_ssl.py | 193 | ||||
-rw-r--r-- | Misc/NEWS.d/next/Library/2018-09-14-14-29-45.bpo-34670.17XwGB.rst | 3 | ||||
-rw-r--r-- | Modules/_ssl.c | 100 | ||||
-rw-r--r-- | Modules/clinic/_ssl.c.h | 20 | ||||
-rwxr-xr-x | Tools/ssl/multissltests.py | 8 |
9 files changed, 370 insertions, 16 deletions
diff --git a/.travis.yml b/.travis.yml index 90989f5..d1a6da7 100644 --- a/.travis.yml +++ b/.travis.yml @@ -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 diff --git a/Doc/library/ssl.rst b/Doc/library/ssl.rst index 99d1d77..a8cbe23 100644 --- a/Doc/library/ssl.rst +++ b/Doc/library/ssl.rst @@ -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.8 + + .. 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 @@ -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.8 + + .. 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 diff --git a/Doc/whatsnew/3.8.rst b/Doc/whatsnew/3.8.rst index c464346..9aaaa76 100644 --- a/Doc/whatsnew/3.8.rst +++ b/Doc/whatsnew/3.8.rst @@ -140,6 +140,14 @@ pathlib contain characters unrepresentable at the OS level. (Contributed by Serhiy Storchaka in :issue:`33721`.) +ssl +--- + +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`.) + venv ---- @@ -147,7 +155,6 @@ venv activating virtual environments under PowerShell Core 6.1. (Contributed by Brett Cannon in :issue:`32718`.) - Optimizations ============= @@ -777,6 +777,9 @@ class SSLObject: 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 @@ -1094,6 +1097,12 @@ class SSLSocket(socket): 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() diff --git a/Lib/test/test_ssl.py b/Lib/test/test_ssl.py index b4cafc1..6fd2002 100644 --- a/Lib/test/test_ssl.py +++ b/Lib/test/test_ssl.py @@ -218,7 +218,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 @@ -2262,6 +2262,23 @@ class ThreadedEchoServer(threading.Thread): 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): @@ -4148,6 +4165,179 @@ class ThreadedTests(unittest.TestCase): '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 @@ -4183,6 +4373,7 @@ def test_main(verbose=False): tests = [ ContextTests, BasicSocketTests, SSLErrorTests, MemoryBIOTests, SSLObjectTests, SimpleBackgroundTests, ThreadedTests, + TestPostHandshakeAuth ] if support.is_resource_enabled('network'): diff --git a/Misc/NEWS.d/next/Library/2018-09-14-14-29-45.bpo-34670.17XwGB.rst b/Misc/NEWS.d/next/Library/2018-09-14-14-29-45.bpo-34670.17XwGB.rst new file mode 100644 index 0000000..c1a6129 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2018-09-14-14-29-45.bpo-34670.17XwGB.rst @@ -0,0 +1,3 @@ +Add SSLContext.post_handshake_auth and +SSLSocket.verify_client_post_handshake for TLS 1.3's post +handshake authentication feature. diff --git a/Modules/_ssl.c b/Modules/_ssl.c index 5b5d7dd..99d4ecc 100644 --- a/Modules/_ssl.c +++ b/Modules/_ssl.c @@ -421,6 +421,9 @@ typedef struct { */ unsigned int hostflags; int protocol; +#ifdef TLS1_3_VERSION + int post_handshake_auth; +#endif } PySSLContext; typedef struct { @@ -2643,6 +2646,30 @@ _ssl__SSLSocket_get_channel_binding_impl(PySSLSocket *self, return PyBytes_FromStringAndSize(buf, len); } +/*[clinic input] +_ssl._SSLSocket.verify_client_post_handshake + +Initiate TLS 1.3 post-handshake authentication +[clinic start generated code]*/ + +static PyObject * +_ssl__SSLSocket_verify_client_post_handshake_impl(PySSLSocket *self) +/*[clinic end generated code: output=532147f3b1341425 input=6bfa874810a3d889]*/ +{ +#ifdef TLS1_3_VERSION + int err = SSL_verify_client_post_handshake(self->ssl); + if (err == 0) + return _setSSLError(NULL, 0, __FILE__, __LINE__); + else + Py_RETURN_NONE; +#else + PyErr_SetString(PyExc_NotImplementedError, + "Post-handshake auth is not supported by your " + "OpenSSL version."); + return NULL; +#endif +} + #ifdef OPENSSL_VERSION_1_1 static SSL_SESSION* @@ -2819,6 +2846,7 @@ static PyMethodDef PySSLMethods[] = { _SSL__SSLSOCKET_SELECTED_ALPN_PROTOCOL_METHODDEF _SSL__SSLSOCKET_COMPRESSION_METHODDEF _SSL__SSLSOCKET_SHUTDOWN_METHODDEF + _SSL__SSLSOCKET_VERIFY_CLIENT_POST_HANDSHAKE_METHODDEF {NULL, NULL} }; @@ -2862,7 +2890,7 @@ static PyTypeObject PySSLSocket_Type = { */ static int -_set_verify_mode(SSL_CTX *ctx, enum py_ssl_cert_requirements n) +_set_verify_mode(PySSLContext *self, enum py_ssl_cert_requirements n) { int mode; int (*verify_cb)(int, X509_STORE_CTX *) = NULL; @@ -2882,9 +2910,13 @@ _set_verify_mode(SSL_CTX *ctx, enum py_ssl_cert_requirements n) "invalid value for verify_mode"); return -1; } +#ifdef TLS1_3_VERSION + if (self->post_handshake_auth) + mode |= SSL_VERIFY_POST_HANDSHAKE; +#endif /* keep current verify cb */ - verify_cb = SSL_CTX_get_verify_callback(ctx); - SSL_CTX_set_verify(ctx, mode, verify_cb); + verify_cb = SSL_CTX_get_verify_callback(self->ctx); + SSL_CTX_set_verify(self->ctx, mode, verify_cb); return 0; } @@ -2966,13 +2998,13 @@ _ssl__SSLContext_impl(PyTypeObject *type, int proto_version) /* Don't check host name by default */ if (proto_version == PY_SSL_VERSION_TLS_CLIENT) { self->check_hostname = 1; - if (_set_verify_mode(self->ctx, PY_SSL_CERT_REQUIRED) == -1) { + if (_set_verify_mode(self, PY_SSL_CERT_REQUIRED) == -1) { Py_DECREF(self); return NULL; } } else { self->check_hostname = 0; - if (_set_verify_mode(self->ctx, PY_SSL_CERT_NONE) == -1) { + if (_set_verify_mode(self, PY_SSL_CERT_NONE) == -1) { Py_DECREF(self); return NULL; } @@ -3065,6 +3097,11 @@ _ssl__SSLContext_impl(PyTypeObject *type, int proto_version) #endif X509_VERIFY_PARAM_set_hostflags(params, self->hostflags); +#ifdef TLS1_3_VERSION + self->post_handshake_auth = 0; + SSL_CTX_set_post_handshake_auth(self->ctx, self->post_handshake_auth); +#endif + return (PyObject *)self; } @@ -3319,7 +3356,10 @@ _ssl__SSLContext__set_alpn_protocols_impl(PySSLContext *self, static PyObject * get_verify_mode(PySSLContext *self, void *c) { - switch (SSL_CTX_get_verify_mode(self->ctx)) { + /* ignore SSL_VERIFY_CLIENT_ONCE and SSL_VERIFY_POST_HANDSHAKE */ + int mask = (SSL_VERIFY_NONE | SSL_VERIFY_PEER | + SSL_VERIFY_FAIL_IF_NO_PEER_CERT); + switch (SSL_CTX_get_verify_mode(self->ctx) & mask) { case SSL_VERIFY_NONE: return PyLong_FromLong(PY_SSL_CERT_NONE); case SSL_VERIFY_PEER: @@ -3344,7 +3384,7 @@ set_verify_mode(PySSLContext *self, PyObject *arg, void *c) "check_hostname is enabled."); return -1; } - return _set_verify_mode(self->ctx, n); + return _set_verify_mode(self, n); } static PyObject * @@ -3550,7 +3590,7 @@ set_check_hostname(PySSLContext *self, PyObject *arg, void *c) if (check_hostname && SSL_CTX_get_verify_mode(self->ctx) == SSL_VERIFY_NONE) { /* check_hostname = True sets verify_mode = CERT_REQUIRED */ - if (_set_verify_mode(self->ctx, PY_SSL_CERT_REQUIRED) == -1) { + if (_set_verify_mode(self, PY_SSL_CERT_REQUIRED) == -1) { return -1; } } @@ -3559,6 +3599,43 @@ set_check_hostname(PySSLContext *self, PyObject *arg, void *c) } static PyObject * +get_post_handshake_auth(PySSLContext *self, void *c) { +#if TLS1_3_VERSION + return PyBool_FromLong(self->post_handshake_auth); +#else + Py_RETURN_NONE; +#endif +} + +#if TLS1_3_VERSION +static int +set_post_handshake_auth(PySSLContext *self, PyObject *arg, void *c) { + int (*verify_cb)(int, X509_STORE_CTX *) = NULL; + int mode = SSL_CTX_get_verify_mode(self->ctx); + int pha = PyObject_IsTrue(arg); + + if (pha == -1) { + return -1; + } + self->post_handshake_auth = pha; + + /* client-side socket setting, ignored by server-side */ + SSL_CTX_set_post_handshake_auth(self->ctx, pha); + + /* server-side socket setting, ignored by client-side */ + verify_cb = SSL_CTX_get_verify_callback(self->ctx); + if (pha) { + mode |= SSL_VERIFY_POST_HANDSHAKE; + } else { + mode ^= SSL_VERIFY_POST_HANDSHAKE; + } + SSL_CTX_set_verify(self->ctx, mode, verify_cb); + + return 0; +} +#endif + +static PyObject * get_protocol(PySSLContext *self, void *c) { return PyLong_FromLong(self->protocol); } @@ -4461,6 +4538,13 @@ static PyGetSetDef context_getsetlist[] = { (setter) set_sni_callback, PySSLContext_sni_callback_doc}, {"options", (getter) get_options, (setter) set_options, NULL}, + {"post_handshake_auth", (getter) get_post_handshake_auth, +#ifdef TLS1_3_VERSION + (setter) set_post_handshake_auth, +#else + NULL, +#endif + NULL}, {"protocol", (getter) get_protocol, NULL, NULL}, {"verify_flags", (getter) get_verify_flags, diff --git a/Modules/clinic/_ssl.c.h b/Modules/clinic/_ssl.c.h index 5ba34ec..186e643 100644 --- a/Modules/clinic/_ssl.c.h +++ b/Modules/clinic/_ssl.c.h @@ -342,6 +342,24 @@ exit: return return_value; } +PyDoc_STRVAR(_ssl__SSLSocket_verify_client_post_handshake__doc__, +"verify_client_post_handshake($self, /)\n" +"--\n" +"\n" +"Initiate TLS 1.3 post-handshake authentication"); + +#define _SSL__SSLSOCKET_VERIFY_CLIENT_POST_HANDSHAKE_METHODDEF \ + {"verify_client_post_handshake", (PyCFunction)_ssl__SSLSocket_verify_client_post_handshake, METH_NOARGS, _ssl__SSLSocket_verify_client_post_handshake__doc__}, + +static PyObject * +_ssl__SSLSocket_verify_client_post_handshake_impl(PySSLSocket *self); + +static PyObject * +_ssl__SSLSocket_verify_client_post_handshake(PySSLSocket *self, PyObject *Py_UNUSED(ignored)) +{ + return _ssl__SSLSocket_verify_client_post_handshake_impl(self); +} + static PyObject * _ssl__SSLContext_impl(PyTypeObject *type, int proto_version); @@ -1175,4 +1193,4 @@ exit: #ifndef _SSL_ENUM_CRLS_METHODDEF #define _SSL_ENUM_CRLS_METHODDEF #endif /* !defined(_SSL_ENUM_CRLS_METHODDEF) */ -/*[clinic end generated code: output=e2417fee28666f7c input=a9049054013a1b77]*/ +/*[clinic end generated code: output=c4e73b70ac3618ba input=a9049054013a1b77]*/ diff --git a/Tools/ssl/multissltests.py b/Tools/ssl/multissltests.py index c4ebe31..759f5f4 100755 --- a/Tools/ssl/multissltests.py +++ b/Tools/ssl/multissltests.py @@ -45,16 +45,16 @@ OPENSSL_OLD_VERSIONS = [ ] OPENSSL_RECENT_VERSIONS = [ - "1.0.2o", - "1.1.0h", - # "1.1.1-pre7", + "1.0.2p", + "1.1.0i", + "1.1.1", ] LIBRESSL_OLD_VERSIONS = [ ] LIBRESSL_RECENT_VERSIONS = [ - "2.7.3", + "2.7.4", ] # store files in ../multissl |