diff -r 87858e0b757d Lib/ftplib.py --- a/Lib/ftplib.py Thu Nov 28 15:12:15 2013 +0100 +++ b/Lib/ftplib.py Thu Nov 28 18:05:20 2013 +0100 @@ -748,7 +748,8 @@ resp = self.voidcmd('AUTH TLS') else: resp = self.voidcmd('AUTH SSL') - self.sock = self.context.wrap_socket(self.sock) + self.sock = self.context.wrap_socket(self.sock, + server_hostname=self.host) self.file = self.sock.makefile(mode='r', encoding=self.encoding) return resp @@ -787,7 +788,8 @@ def ntransfercmd(self, cmd, rest=None): conn, size = FTP.ntransfercmd(self, cmd, rest) if self._prot_p: - conn = self.context.wrap_socket(conn) + conn = self.context.wrap_socket(conn, + server_hostname=self.host) return conn, size def abort(self): diff -r 87858e0b757d Lib/ssl.py --- a/Lib/ssl.py Thu Nov 28 15:12:15 2013 +0100 +++ b/Lib/ssl.py Thu Nov 28 18:05:20 2013 +0100 @@ -148,6 +148,7 @@ from _ssl import enum_certificates, enum_crls from socket import getnameinfo as _getnameinfo +from socket import SHUT_RDWR as _SHUT_RDWR from socket import socket, AF_INET, SOCK_STREAM, create_connection import base64 # for DER-to-PEM translation import traceback @@ -235,7 +236,9 @@ returns nothing. """ if not cert: - raise ValueError("empty or no certificate") + raise ValueError("empty or no certificate, match_hostname needs a " + "SSL socket or SSL context with either " + "CERT_OPTIONAL or CERT_REQUIRED") dnsnames = [] san = cert.get('subjectAltName', ()) for key, value in san: @@ -387,9 +390,10 @@ context.options |= getattr(_ssl, "OP_NO_COMPRESSION", 0) # disallow ciphers with known vulnerabilities context.set_ciphers(_RESTRICTED_CIPHERS) - # verify certs in client mode + # verify certs and host name in client mode if purpose == Purpose.SERVER_AUTH: context.verify_mode = CERT_REQUIRED + context.check_hostname = True if cafile or capath or cadata: context.load_verify_locations(cafile, capath, cadata) elif context.verify_mode != CERT_NONE: @@ -522,9 +526,9 @@ raise ValueError("do_handshake_on_connect should not be specified for non-blocking sockets") self.do_handshake() - except OSError as x: + except (OSError, ValueError): self.close() - raise x + raise @property def context(self): @@ -751,6 +755,17 @@ finally: self.settimeout(timeout) + if self.context.check_hostname: + try: + if not self.server_hostname: + raise ValueError("check_hostname needs server_hostname " + "argument") + match_hostname(self.getpeercert(), self.server_hostname) + except Exception: + self.shutdown(_SHUT_RDWR) + self.close() + raise + def _real_connect(self, addr, connect_ex): if self.server_side: raise ValueError("can't connect in server-side mode") @@ -770,7 +785,7 @@ if self.do_handshake_on_connect: self.do_handshake() return rc - except OSError: + except (OSError, ValueError): self._sslobj = None raise diff -r 87858e0b757d Lib/test/test_ftplib.py --- a/Lib/test/test_ftplib.py Thu Nov 28 15:12:15 2013 +0100 +++ b/Lib/test/test_ftplib.py Thu Nov 28 18:05:20 2013 +0100 @@ -301,7 +301,8 @@ if ssl is not None: - CERTFILE = os.path.join(os.path.dirname(__file__), "keycert.pem") + CERTFILE = os.path.join(os.path.dirname(__file__), "keycert3.pem") + CAFILE = os.path.join(os.path.dirname(__file__), "pycacert.pem") class SSLConnection(asyncore.dispatcher): """An asyncore.dispatcher subclass supporting TLS/SSL.""" @@ -923,6 +924,36 @@ self.client.ccc() self.assertRaises(ValueError, self.client.sock.unwrap) + def test_check_hostname(self): + self.client.quit() + ctx = ssl.SSLContext(ssl.PROTOCOL_TLSv1) + ctx.verify_mode = ssl.CERT_REQUIRED + ctx.check_hostname = True + ctx.load_verify_locations(CAFILE) + self.client = ftplib.FTP_TLS(context=ctx, timeout=TIMEOUT) + + # 127.0.0.1 doesn't match SAN + self.client.connect(self.server.host, self.server.port) + with self.assertRaises(ssl.CertificateError): + self.client.auth() + # exception quits connection + + self.client.connect(self.server.host, self.server.port) + self.client.prot_p() + with self.assertRaises(ssl.CertificateError): + with self.client.transfercmd("list") as sock: + pass + self.client.quit() + + self.client.connect("localhost", self.server.port) + self.client.auth() + self.client.quit() + + self.client.connect("localhost", self.server.port) + self.client.prot_p() + with self.client.transfercmd("list") as sock: + pass + class TestTimeouts(TestCase): diff -r 87858e0b757d Modules/_ssl.c --- a/Modules/_ssl.c Thu Nov 28 15:12:15 2013 +0100 +++ b/Modules/_ssl.c Thu Nov 28 18:05:20 2013 +0100 @@ -214,6 +214,7 @@ #ifndef OPENSSL_NO_TLSEXT PyObject *set_hostname; #endif + int check_hostname; } PySSLContext; typedef struct { @@ -2050,6 +2051,8 @@ #ifndef OPENSSL_NO_TLSEXT self->set_hostname = NULL; #endif + /* Don't check host name by default */ + self->check_hostname = 0; /* Defaults */ SSL_CTX_set_verify(self->ctx, SSL_VERIFY_NONE, NULL); SSL_CTX_set_options(self->ctx, @@ -2231,6 +2234,12 @@ "invalid value for verify_mode"); return -1; } + if (mode == SSL_VERIFY_NONE && self->check_hostname) { + PyErr_SetString(PyExc_ValueError, + "Cannot set verify_mode to CERT_NONE when " + "check_hostname is enabled."); + return -1; + } SSL_CTX_set_verify(self->ctx, mode, NULL); return 0; } @@ -2304,6 +2313,30 @@ return 0; } +static PyObject * +get_check_hostname(PySSLContext *self, void *c) +{ + return PyBool_FromLong(self->check_hostname); +} + +static int +set_check_hostname(PySSLContext *self, PyObject *arg, void *c) +{ + int check_hostname; + if (!PyArg_Parse(arg, "p", &check_hostname)) + return -1; + if (check_hostname && + SSL_CTX_get_verify_mode(self->ctx) == SSL_VERIFY_NONE) { + PyErr_SetString(PyExc_ValueError, + "check_hostname needs a SSL context with either " + "CERT_OPTIONAL or CERT_REQUIRED"); + return -1; + } + self->check_hostname = check_hostname; + return 0; +} + + typedef struct { PyThreadState *thread_state; PyObject *callable; @@ -3093,6 +3126,8 @@ static PyGetSetDef context_getsetlist[] = { + {"check_hostname", (getter) get_check_hostname, + (setter) set_check_hostname, NULL}, {"options", (getter) get_options, (setter) set_options, NULL}, #ifdef HAVE_OPENSSL_VERIFY_PARAM