bpo-34670: Add TLS 1.3 post handshake auth (GH-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
This commit is contained in:
Christian Heimes 2018-09-23 08:32:31 +02:00 committed by Miss Islington (bot)
parent 4b860fd777
commit 9fb051f032
9 changed files with 370 additions and 16 deletions

View File

@ -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

View File

@ -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

View File

@ -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
=============

View File

@ -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()

View File

@ -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'):

View File

@ -0,0 +1,3 @@
Add SSLContext.post_handshake_auth and
SSLSocket.verify_client_post_handshake for TLS 1.3's post
handshake authentication feature.

View File

@ -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;
}
}
@ -3558,6 +3598,43 @@ set_check_hostname(PySSLContext *self, PyObject *arg, void *c)
return 0;
}
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,

View File

@ -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]*/

View File

@ -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