.. exception:: CertificateError
- Raised to signal an error with a certificate (such as mismatching
- hostname). Certificate errors detected by OpenSSL, though, raise
- an :exc:`SSLCertVerificationError`.
+ An alias for :exc:`SSLCertVerificationError`.
+
+ .. versionchanged:: 3.7
+ The exception is now an alias for :exc:`SSLCertVerificationError`.
Socket creation
of the certificate, is now supported.
.. versionchanged:: 3.7
+ The function is no longer used to TLS connections. Hostname matching
+ is now performed by OpenSSL.
+
Allow wildcard when it is the leftmost and the only character
- in that segment.
+ in that segment. Partial wildcards like ``www*.example.com`` are no
+ longer supported.
+
+ .. deprecated:: 3.7
.. function:: cert_time_to_seconds(cert_time)
.. versionadded:: 3.5
+.. data:: HAS_NEVER_CHECK_COMMON_NAME
+
+ Whether the OpenSSL library has built-in support not checking subject
+ common name and :attr:`SSLContext.hostname_checks_common_name` is
+ writeable.
+
+ .. versionadded:: 3.7
+
.. data:: HAS_ECDH
Whether the OpenSSL library has built-in support for Elliptic Curve-based
The socket timeout is no more reset each time bytes are received or sent.
The socket timeout is now to maximum total duration of the handshake.
+ .. versionchanged:: 3.7
+ Hostname or IP address is matched by OpenSSL during handshake. The
+ function :func:`match_hostname` is no longer used. In case OpenSSL
+ refuses a hostname or IP address, the handshake is aborted early and
+ a TLS alert message is send to the peer.
+
.. method:: SSLSocket.getpeercert(binary_form=False)
If there is no certificate for the peer on the other end of the connection,
The protocol version chosen when constructing the context. This attribute
is read-only.
+.. attribute:: SSLContext.hostname_checks_common_name
+
+ Whether :attr:`~SSLContext.check_hostname` falls back to verify the cert's
+ subject common name in the absence of a subject alternative name
+ extension (default: true).
+
+ .. versionadded:: 3.7
+
+ .. note::
+ Only writeable with OpenSSL 1.1.0 or higher.
+
.. attribute:: SSLContext.verify_flags
The flags for certificate verification operations. You can set flags like
check is automatically performed when :attr:`SSLContext.check_hostname` is
enabled.
+.. versionchanged:: 3.7
+ Hostname matchings is now performed by OpenSSL. Python no longer uses
+ :func:`match_hostname`.
+
In server mode, if you want to authenticate your clients using the SSL layer
(rather than using a higher-level authentication mechanism), you'll also have
to specify :const:`CERT_REQUIRED` and similarly check the client certificate.
``'^$'`` or ``(?=-)`` that matches an empty string.
(Contributed by Serhiy Storchaka in :issue:`25054`.)
+ssl
+---
+
+The ssl module now uses OpenSSL's builtin API instead of
+:func:`~ssl.match_hostname` to check host name or IP address. Values
+are validated during TLS handshake. Any cert validation error including
+a failing host name match now raises :exc:`~ssl.SSLCertVerificationError` and
+aborts the handshake with a proper TLS Alert message. The new exception
+contains additional information. Host name validation can be customized
+with :attr:`~ssl.SSLContext.host_flags`.
+(Contributed by Christian Heimes in :issue:`31399`.)
+
+.. note::
+ The improved host name check requires an OpenSSL 1.0.2 or 1.1 compatible
+ libssl. OpenSSL 0.9.8 and 1.0.1 are no longer supported. LibreSSL is
+ temporarily not supported until it gains the necessary OpenSSL 1.0.2 APIs.
+
+The ssl module no longer sends IP addresses in SNI TLS extension.
+(Contributed by Christian Heimes in :issue:`32185`.)
+
+:func:`~ssl.match_hostname` no longer supports partial wildcards like
+``www*.example.org``. :attr:`~ssl.SSLContext.host_flags` has partial
+wildcard matching disabled by default.
+(Contributed by Mandeep Singh in :issue:`23033` and Christian Heimes in
+:issue:`31399`.)
+
string
------
emitted in the first place), and an explicit ``error::BytesWarning``
warnings filter added to convert them to exceptions.
+* CPython' :mod:`ssl` module requires OpenSSL 1.0.2 or 1.1 compatible libssl.
+ OpenSSL 1.0.1 has reached end of lifetime on 2016-12-31 and is no longer
+ supported. LibreSSL is temporarily not supported as well. LibreSSL releases
+ up to version 2.6.4 are missing required OpenSSL 1.0.2 APIs.
+
+
Documentation
=============
raise handshake_exc
peercert = sslobj.getpeercert()
- if not hasattr(self._sslcontext, 'check_hostname'):
- # Verify hostname if requested, Python 3.4+ uses check_hostname
- # and checks the hostname in do_handshake()
- if (self._server_hostname and
- self._sslcontext.verify_mode != ssl.CERT_NONE):
- ssl.match_hostname(peercert, self._server_hostname)
except BaseException as exc:
if self._loop.get_debug():
if isinstance(exc, ssl.CertificateError):
if key_file or cert_file:
context.load_cert_chain(cert_file, key_file)
self._context = context
- self._check_hostname = check_hostname
+ if check_hostname is not None:
+ self._context.check_hostname = check_hostname
def connect(self):
"Connect to a host on a given (SSL) port."
self.sock = self._context.wrap_socket(self.sock,
server_hostname=server_hostname)
- if not self._context.check_hostname and self._check_hostname:
- try:
- ssl.match_hostname(self.sock.getpeercert(), server_hostname)
- except Exception:
- self.sock.shutdown(socket.SHUT_RDWR)
- self.sock.close()
- raise
__all__.append("HTTPSConnection")
lambda name: name.startswith('CERT_'),
source=_ssl)
-
PROTOCOL_SSLv23 = _SSLMethod.PROTOCOL_SSLv23 = _SSLMethod.PROTOCOL_TLS
_PROTOCOL_NAMES = {value: name for name, value in _SSLMethod.__members__.items()}
else:
CHANNEL_BINDING_TYPES = []
+HAS_NEVER_CHECK_COMMON_NAME = hasattr(_ssl, 'HOSTFLAG_NEVER_CHECK_SUBJECT')
+
# Disable weak or insecure ciphers by default
# (OpenSSL's default setting is 'DEFAULT:!aNULL:!eNULL')
'!aNULL:!eNULL:!MD5:!DSS:!RC4:!3DES'
)
-
-class CertificateError(ValueError):
- pass
+CertificateError = SSLCertVerificationError
def _dnsname_match(dn, hostname):
def options(self, value):
super(SSLContext, SSLContext).options.__set__(self, value)
+ if hasattr(_ssl, 'HOSTFLAG_NEVER_CHECK_SUBJECT'):
+ @property
+ def hostname_checks_common_name(self):
+ ncs = self._host_flags & _ssl.HOSTFLAG_NEVER_CHECK_SUBJECT
+ return ncs != _ssl.HOSTFLAG_NEVER_CHECK_SUBJECT
+
+ @hostname_checks_common_name.setter
+ def hostname_checks_common_name(self, value):
+ if value:
+ self._host_flags &= ~_ssl.HOSTFLAG_NEVER_CHECK_SUBJECT
+ else:
+ self._host_flags |= _ssl.HOSTFLAG_NEVER_CHECK_SUBJECT
+ else:
+ @property
+ def hostname_checks_common_name(self):
+ return True
+
@property
def verify_flags(self):
return VerifyFlags(super().verify_flags)
def do_handshake(self):
"""Start the SSL/TLS handshake."""
self._sslobj.do_handshake()
- if self.context.check_hostname:
- if not self.server_hostname:
- raise ValueError("check_hostname needs server_hostname "
- "argument")
- match_hostname(self.getpeercert(), self.server_hostname)
def unwrap(self):
"""Start the SSL shutdown handshake."""
with test_utils.disable_logger():
with self.assertRaisesRegex(
ssl.CertificateError,
- "hostname '127.0.0.1' doesn't match 'localhost'"):
+ "IP address mismatch, certificate is not valid for "
+ "'127.0.0.1'"):
self.loop.run_until_complete(f_c)
# close connection
- proto.transport.close()
+ # transport is None because TLS ALERT aborted the handshake
+ self.assertIsNone(proto.transport)
server.close()
@support.skip_unless_bind_unix_socket
return
elif err.args[0] == ssl.SSL_ERROR_EOF:
return self.handle_close()
+ # TODO: SSLError does not expose alert information
+ elif "SSLV3_ALERT_BAD_CERTIFICATE" in err.args[1]:
+ return self.handle_close()
raise
except OSError as err:
if err.args[0] == errno.ECONNABORTED:
ssl_context.load_verify_locations(CAFILE)
with self.assertRaisesRegex(ssl.CertificateError,
- "hostname '127.0.0.1' doesn't match 'localhost'"):
+ "IP address mismatch, certificate is not valid for "
+ "'127.0.0.1'"):
_, server = self._setup(SimpleIMAPHandler)
client = self.imap_class(*server.server_address,
ssl_context=ssl_context)
with self.assertRaisesRegex(
ssl.CertificateError,
- "hostname '127.0.0.1' doesn't match 'localhost'"):
+ "IP address mismatch, certificate is not valid for "
+ "'127.0.0.1'"):
with self.reaped_server(SimpleIMAPHandler) as server:
client = self.imap_class(*server.server_address,
ssl_context=ssl_context)
return
elif err.args[0] == ssl.SSL_ERROR_EOF:
return self.handle_close()
+ # TODO: SSLError does not expose alert information
+ elif "SSLV3_ALERT_BAD_CERTIFICATE" in err.args[1]:
+ return self.handle_close()
raise
except OSError as err:
if err.args[0] == errno.ECONNABORTED:
self.assertEqual(ctx.verify_mode, ssl.CERT_REQUIRED)
self.assertTrue(ctx.check_hostname)
+ def test_hostname_checks_common_name(self):
+ ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
+ self.assertTrue(ctx.hostname_checks_common_name)
+ if ssl.HAS_NEVER_CHECK_COMMON_NAME:
+ ctx.hostname_checks_common_name = True
+ self.assertTrue(ctx.hostname_checks_common_name)
+ ctx.hostname_checks_common_name = False
+ self.assertFalse(ctx.hostname_checks_common_name)
+ ctx.hostname_checks_common_name = True
+ self.assertTrue(ctx.hostname_checks_common_name)
+ else:
+ with self.assertRaises(AttributeError):
+ ctx.hostname_checks_common_name = True
@unittest.skipUnless(have_verify_flags(),
"verify_flags need OpenSSL > 0.9.8")
ctx.wrap_bio(ssl.MemoryBIO(), ssl.MemoryBIO(),
server_hostname="xn--.com")
+ def test_bad_server_hostname(self):
+ ctx = ssl.create_default_context()
+ with self.assertRaises(ValueError):
+ ctx.wrap_bio(ssl.MemoryBIO(), ssl.MemoryBIO(),
+ server_hostname="")
+ with self.assertRaises(ValueError):
+ ctx.wrap_bio(ssl.MemoryBIO(), ssl.MemoryBIO(),
+ server_hostname=".example.org")
+
+
class MemoryBIOTests(unittest.TestCase):
def test_read_write(self):
with server:
with client_context.wrap_socket(socket.socket(),
server_hostname="invalid") as s:
- with self.assertRaisesRegex(ssl.CertificateError,
- "hostname 'invalid' doesn't match 'localhost'"):
+ with self.assertRaisesRegex(
+ ssl.CertificateError,
+ "Hostname mismatch, certificate is not valid for 'invalid'."):
s.connect((HOST, server.port))
# missing server_hostname arg should cause an exception, too
cafile=CERT_fakehostname)
# Good cert, but mismatching hostname
handler = self.start_https_server(certfile=CERT_fakehostname)
- with self.assertRaises(ssl.CertificateError) as cm:
+ with self.assertRaises(urllib.error.URLError) as cm:
self.urlopen("https://localhost:%s/bizarre" % handler.port,
cafile=CERT_fakehostname)
--- /dev/null
+The ssl module now uses OpenSSL's X509_VERIFY_PARAM_set1_host() and
+X509_VERIFY_PARAM_set1_ip() API to verify hostname and IP addresses. Subject
+common name fallback can be disabled with
+SSLContext.hostname_checks_common_name.
#include "openssl/rand.h"
#include "openssl/bio.h"
-/* Set HAVE_X509_VERIFY_PARAM_SET1_HOST for non-autoconf builds */
#ifndef HAVE_X509_VERIFY_PARAM_SET1_HOST
-# if !defined(LIBRESSL_VERSION_NUMBER) && OPENSSL_VERSION_NUMBER > 0x1000200fL
+# ifdef LIBRESSL_VERSION_NUMBER
+# error "LibreSSL is missing X509_VERIFY_PARAM_set1_host(), see https://github.com/libressl-portable/portable/issues/381"
+# elif OPENSSL_VERSION_NUMBER > 0x1000200fL
# define HAVE_X509_VERIFY_PARAM_SET1_HOST
+# else
+# error "libssl is too old and does not support X509_VERIFY_PARAM_set1_host()"
# endif
#endif
return store->objs;
}
-static X509_VERIFY_PARAM *X509_STORE_get0_param(X509_STORE *store)
-{
- return store->param;
-}
-
static int
SSL_SESSION_has_ticket(const SSL_SESSION *s)
{
PyObject *set_hostname;
#endif
int check_hostname;
+ /* OpenSSL has no API to get hostflags from X509_VERIFY_PARAM* struct.
+ * We have to maintain our own copy. OpenSSL's hostflags default to 0.
+ */
+ unsigned int hostflags;
} PySSLContext;
typedef struct {
return NULL;
}
+/*
+ * SSL objects
+ */
+
+static int
+_ssl_configure_hostname(PySSLSocket *self, const char* server_hostname)
+{
+ int retval = -1;
+ ASN1_OCTET_STRING *ip;
+ PyObject *hostname;
+ size_t len;
+
+ assert(server_hostname);
+
+ /* Disable OpenSSL's special mode with leading dot in hostname:
+ * When name starts with a dot (e.g ".example.com"), it will be
+ * matched by a certificate valid for any sub-domain of name.
+ */
+ len = strlen(server_hostname);
+ if (len == 0 || *server_hostname == '.') {
+ PyErr_SetString(
+ PyExc_ValueError,
+ "server_hostname cannot be an empty string or start with a "
+ "leading dot.");
+ return retval;
+ }
+
+ /* inet_pton is not available on all platforms. */
+ ip = a2i_IPADDRESS(server_hostname);
+ if (ip == NULL) {
+ ERR_clear_error();
+ }
+
+ hostname = PyUnicode_Decode(server_hostname, len, "idna", "strict");
+ if (hostname == NULL) {
+ goto error;
+ }
+ self->server_hostname = hostname;
+
+ /* Only send SNI extension for non-IP hostnames */
+ if (ip == NULL) {
+ if (!SSL_set_tlsext_host_name(self->ssl, server_hostname)) {
+ _setSSLError(NULL, 0, __FILE__, __LINE__);
+ }
+ }
+ if (self->ctx->check_hostname) {
+ X509_VERIFY_PARAM *param = SSL_get0_param(self->ssl);
+ if (ip == NULL) {
+ if (!X509_VERIFY_PARAM_set1_host(param, server_hostname, 0)) {
+ _setSSLError(NULL, 0, __FILE__, __LINE__);
+ goto error;
+ }
+ } else {
+ if (!X509_VERIFY_PARAM_set1_ip(param, ASN1_STRING_data(ip),
+ ASN1_STRING_length(ip))) {
+ _setSSLError(NULL, 0, __FILE__, __LINE__);
+ goto error;
+ }
+ }
+ }
+ retval = 0;
+ error:
+ if (ip != NULL) {
+ ASN1_OCTET_STRING_free(ip);
+ }
+ return retval;
+}
+
static PySSLSocket *
newPySSLSocket(PySSLContext *sslctx, PySocketSockObject *sock,
enum py_ssl_server_or_client socket_type,
self->shutdown_seen_zero = 0;
self->owner = NULL;
self->server_hostname = NULL;
- if (server_hostname != NULL) {
- PyObject *hostname = PyUnicode_Decode(server_hostname, strlen(server_hostname),
- "idna", "strict");
- if (hostname == NULL) {
- Py_DECREF(self);
- return NULL;
- }
- self->server_hostname = hostname;
- }
self->ssl_errno = 0;
self->c_errno = 0;
#ifdef MS_WINDOWS
#endif
SSL_set_mode(self->ssl, mode);
-#if HAVE_SNI
- if (server_hostname != NULL)
- SSL_set_tlsext_host_name(self->ssl, server_hostname);
-#endif
+ if (server_hostname != NULL) {
+ if (_ssl_configure_hostname(self, server_hostname) < 0) {
+ Py_DECREF(self);
+ return NULL;
+ }
+ }
/* If the socket is in non-blocking mode or timeout mode, set the BIO
* to non-blocking mode (blocking is the default)
*/
PySSLContext *self;
long options;
SSL_CTX *ctx = NULL;
+ X509_VERIFY_PARAM *params;
int result;
#if defined(SSL_MODE_RELEASE_BUFFERS)
unsigned long libver;
return NULL;
}
self->ctx = ctx;
+ self->hostflags = X509_CHECK_FLAG_NO_PARTIAL_WILDCARDS;
#if defined(OPENSSL_NPN_NEGOTIATED) && !defined(OPENSSL_NO_NEXTPROTONEG)
self->npn_protocols = NULL;
#endif
sizeof(SID_CTX));
#undef SID_CTX
+ params = SSL_CTX_get0_param(self->ctx);
#ifdef X509_V_FLAG_TRUSTED_FIRST
- {
- /* Improve trust chain building when cross-signed intermediate
- certificates are present. See https://bugs.python.org/issue23476. */
- X509_STORE *store = SSL_CTX_get_cert_store(self->ctx);
- X509_STORE_set_flags(store, X509_V_FLAG_TRUSTED_FIRST);
- }
+ /* Improve trust chain building when cross-signed intermediate
+ certificates are present. See https://bugs.python.org/issue23476. */
+ X509_VERIFY_PARAM_set_flags(params, X509_V_FLAG_TRUSTED_FIRST);
#endif
+ X509_VERIFY_PARAM_set_hostflags(params, self->hostflags);
return (PyObject *)self;
}
static PyObject *
get_verify_flags(PySSLContext *self, void *c)
{
- X509_STORE *store;
X509_VERIFY_PARAM *param;
unsigned long flags;
- store = SSL_CTX_get_cert_store(self->ctx);
- param = X509_STORE_get0_param(store);
+ param = SSL_CTX_get0_param(self->ctx);
flags = X509_VERIFY_PARAM_get_flags(param);
return PyLong_FromUnsignedLong(flags);
}
static int
set_verify_flags(PySSLContext *self, PyObject *arg, void *c)
{
- X509_STORE *store;
X509_VERIFY_PARAM *param;
unsigned long new_flags, flags, set, clear;
if (!PyArg_Parse(arg, "k", &new_flags))
return -1;
- store = SSL_CTX_get_cert_store(self->ctx);
- param = X509_STORE_get0_param(store);
+ param = SSL_CTX_get0_param(self->ctx);
flags = X509_VERIFY_PARAM_get_flags(param);
clear = flags & ~new_flags;
set = ~flags & new_flags;
return 0;
}
+static PyObject *
+get_host_flags(PySSLContext *self, void *c)
+{
+ return PyLong_FromUnsignedLong(self->hostflags);
+}
+
+static int
+set_host_flags(PySSLContext *self, PyObject *arg, void *c)
+{
+ X509_VERIFY_PARAM *param;
+ unsigned int new_flags = 0;
+
+ if (!PyArg_Parse(arg, "I", &new_flags))
+ return -1;
+
+ param = SSL_CTX_get0_param(self->ctx);
+ self->hostflags = new_flags;
+ X509_VERIFY_PARAM_set_hostflags(param, new_flags);
+ return 0;
+}
+
static PyObject *
get_check_hostname(PySSLContext *self, void *c)
{
static PyGetSetDef context_getsetlist[] = {
{"check_hostname", (getter) get_check_hostname,
(setter) set_check_hostname, NULL},
+ {"_host_flags", (getter) get_host_flags,
+ (setter) set_host_flags, NULL},
{"options", (getter) get_options,
(setter) set_options, NULL},
{"verify_flags", (getter) get_verify_flags,
SSL_OP_NO_COMPRESSION);
#endif
+#ifdef X509_CHECK_FLAG_ALWAYS_CHECK_SUBJECT
+ PyModule_AddIntConstant(m, "HOSTFLAG_ALWAYS_CHECK_SUBJECT",
+ X509_CHECK_FLAG_ALWAYS_CHECK_SUBJECT);
+#endif
+#ifdef X509_CHECK_FLAG_NEVER_CHECK_SUBJECT
+ PyModule_AddIntConstant(m, "HOSTFLAG_NEVER_CHECK_SUBJECT",
+ X509_CHECK_FLAG_NEVER_CHECK_SUBJECT);
+#endif
+#ifdef X509_CHECK_FLAG_NO_WILDCARDS
+ PyModule_AddIntConstant(m, "HOSTFLAG_NO_WILDCARDS",
+ X509_CHECK_FLAG_NO_WILDCARDS);
+#endif
+#ifdef X509_CHECK_FLAG_NO_PARTIAL_WILDCARDS
+ PyModule_AddIntConstant(m, "HOSTFLAG_NO_PARTIAL_WILDCARDS",
+ X509_CHECK_FLAG_NO_PARTIAL_WILDCARDS);
+#endif
+#ifdef X509_CHECK_FLAG_MULTI_LABEL_WILDCARDS
+ PyModule_AddIntConstant(m, "HOSTFLAG_MULTI_LABEL_WILDCARDS",
+ X509_CHECK_FLAG_MULTI_LABEL_WILDCARDS);
+#endif
+#ifdef X509_CHECK_FLAG_SINGLE_LABEL_SUBDOMAINS
+ PyModule_AddIntConstant(m, "HOSTFLAG_SINGLE_LABEL_SUBDOMAINS",
+ X509_CHECK_FLAG_SINGLE_LABEL_SUBDOMAINS);
+#endif
+
#if HAVE_SNI
r = Py_True;
#else
/* framework name */
#define _PYTHONFRAMEWORK ""
+/* Define if libssl has X509_VERIFY_PARAM_set1_host and related function */
+#define HAVE_X509_VERIFY_PARAM_SET1_HOST 1
+
#endif /* !Py_CONFIG_H */
print_three_column(failed)
print()
+ if any('_ssl' in l
+ for l in (missing, self.failed, self.failed_on_import)):
+ print()
+ print("Could not build the ssl module!")
+ print("Python requires an OpenSSL 1.0.2 or 1.1 compatible "
+ "libssl with X509_VERIFY_PARAM_set1_host().")
+ print("LibreSSL 2.6.4 and earlier do not provide the necessary "
+ "APIs, https://github.com/libressl-portable/portable/issues/381")
+ print()
+
def build_extension(self, ext):
if ext.name == '_ctypes':
if krb5_h:
ssl_incs.extend(krb5_h)
- ssl_ext = Extension(
- '_ssl', ['_ssl.c'],
- include_dirs=openssl_includes,
- library_dirs=openssl_libdirs,
- libraries=openssl_libs,
- depends=['socketmodule.h']
- )
+ if config_vars.get("HAVE_X509_VERIFY_PARAM_SET1_HOST"):
+ ssl_ext = Extension(
+ '_ssl', ['_ssl.c'],
+ include_dirs=openssl_includes,
+ library_dirs=openssl_libdirs,
+ libraries=openssl_libs,
+ depends=['socketmodule.h']
+ )
+ else:
+ ssl_ext = None
hashlib_ext = Extension(
'_hashlib', ['_hashopenssl.c'],