diff -r 67d9595a833c Doc/library/ssl.rst --- a/Doc/library/ssl.rst Fri Mar 02 22:54:03 2012 +0100 +++ b/Doc/library/ssl.rst Sat Mar 10 12:13:48 2012 -0500 @@ -115,7 +115,7 @@ Python 3.2, it can be more flexible to use :meth:`SSLContext.wrap_socket` instead. -.. function:: wrap_socket(sock, keyfile=None, certfile=None, server_side=False, cert_reqs=CERT_NONE, ssl_version={see docs}, ca_certs=None, do_handshake_on_connect=True, suppress_ragged_eofs=True, ciphers=None) +.. function:: wrap_socket(sock, keyfile=None, certfile=None, server_side=False, cert_reqs=CERT_NONE, ssl_version={see docs}, ca_certs=None, do_handshake_on_connect=True, suppress_ragged_eofs=True, npn_protocols=None, ciphers=None) Takes an instance ``sock`` of :class:`socket.socket`, and returns an instance of :class:`ssl.SSLSocket`, a subtype of :class:`socket.socket`, which wraps @@ -181,6 +181,18 @@ might specify ``"ALL"`` or ``"SSLv2"`` as the *ciphers* parameter to enable them. + The ``npn_protocols`` parameter specifies which protocols the socket should + avertise during the SSL/TLS handshake. It should be a list, like + ``['http/1.1', 'spdy/2']``, ordered by preference. The selection of a + protocol will happen during the handshake, and will play out according to + the `NPN draft specification + `_. After a + successful handshake, the :meth:`SSLSocket.selected_protocol` method will + return the agreed-upon protocol. + + .. note:: The NPN extension may not be available, depending on the system version + of OpenSSL. + The *ciphers* parameter sets the available ciphers for this SSL object. It should be a string in the `OpenSSL cipher list format `_. @@ -201,6 +213,8 @@ .. versionchanged:: 3.2 New optional argument *ciphers*. + .. versionchanged:: 3.3 + New optional argument ``npn_protocols`` Random generation ^^^^^^^^^^^^^^^^^ @@ -470,6 +484,16 @@ .. versionadded:: 3.2 +.. data:: HAS_NPN + + Whether the OpenSSL library has built-in support for *Next Protocol + Negotiation* as described in the `NPN draft specification + `_. When true, + you can use the :meth:`SSLContext.set_npn_protocols` method to advertise + which protocols you want to support. + + .. versionadded:: 3.3 + .. data:: CHANNEL_BINDING_TYPES List of supported TLS channel binding types. Strings in this list @@ -586,6 +610,14 @@ version of the SSL protocol that defines its use, and the number of secret bits being used. If no connection has been established, returns ``None``. +.. method:: SSLSocket.selected_protocol() + + Returns the protocol that was selected during the TLS/SSL handshake. If + ``npn_protocols`` was not specified, or if the other party does not support + NPN, or if the handshake has not yet happened, this will return ``None``. + + .. versionadded:: 3.3 + .. method:: SSLSocket.compression() Return the compression algorithm being used as a string, or ``None`` @@ -617,7 +649,6 @@ returned socket should always be used for further communication with the other side of the connection, rather than the original socket. - .. attribute:: SSLSocket.context The :class:`SSLContext` object this SSL socket is tied to. If the SSL @@ -715,6 +746,19 @@ when connected, the :meth:`SSLSocket.cipher` method of SSL sockets will give the currently selected cipher. +.. method:: SSLContext.set_npn_protocols(protocols) + + Specify which protocols the socket should avertise during the SSL/TLS + handshake. It should be a list, like ``['http/1.1', 'spdy/2']``, ordered by + preference. The selection of a protocol will happen during the handshake, + and will play out according to the `NPN draft specification + `_. After a + successful handshake, the :meth:`SSLSocket.selected_protocol` method will + return the agreed-upon protocol. + + .. note:: The NPN extension may not be available, depending on the system version + of OpenSSL. + .. method:: SSLContext.load_dh_params(dhfile) Load the key generation parameters for Diffie-Helman (DH) key exchange. diff -r 67d9595a833c Lib/ssl.py --- a/Lib/ssl.py Fri Mar 02 22:54:03 2012 +0100 +++ b/Lib/ssl.py Sat Mar 10 12:13:48 2012 -0500 @@ -220,7 +220,7 @@ ssl_version=PROTOCOL_SSLv23, ca_certs=None, do_handshake_on_connect=True, family=AF_INET, type=SOCK_STREAM, proto=0, fileno=None, - suppress_ragged_eofs=True, ciphers=None, + suppress_ragged_eofs=True, npn_protocols=None, ciphers=None, server_hostname=None, _context=None): @@ -240,6 +240,8 @@ self.context.load_verify_locations(ca_certs) if certfile: self.context.load_cert_chain(certfile, keyfile) + if npn_protocols: + self.context.set_npn_protocols(npn_protocols) if ciphers: self.context.set_ciphers(ciphers) self.keyfile = keyfile @@ -340,6 +342,13 @@ self._checkClosed() return self._sslobj.peer_certificate(binary_form) + def selected_protocol(self): + self._checkClosed() + if not self._sslobj or not _ssl.HAS_NPN: + return None + else: + return self._sslobj.selected_protocol() + def cipher(self): self._checkClosed() if not self._sslobj: @@ -568,14 +577,15 @@ server_side=False, cert_reqs=CERT_NONE, ssl_version=PROTOCOL_SSLv23, ca_certs=None, do_handshake_on_connect=True, - suppress_ragged_eofs=True, ciphers=None): + suppress_ragged_eofs=True, + npn_protocols=None, ciphers=None): return SSLSocket(sock=sock, keyfile=keyfile, certfile=certfile, server_side=server_side, cert_reqs=cert_reqs, ssl_version=ssl_version, ca_certs=ca_certs, do_handshake_on_connect=do_handshake_on_connect, suppress_ragged_eofs=suppress_ragged_eofs, - ciphers=ciphers) + npn_protocols=npn_protocols, ciphers=ciphers) # some utility functions diff -r 67d9595a833c Lib/test/test_ssl.py --- a/Lib/test/test_ssl.py Fri Mar 02 22:54:03 2012 +0100 +++ b/Lib/test/test_ssl.py Sat Mar 10 12:13:48 2012 -0500 @@ -879,6 +879,7 @@ try: self.sslconn = self.server.context.wrap_socket( self.sock, server_side=True) + self.server.selected_protocols.append(self.sslconn.selected_protocol()) except ssl.SSLError as e: # XXX Various errors can have happened here, for example # a mismatching protocol version, an invalid certificate, @@ -901,6 +902,8 @@ cipher = self.sslconn.cipher() if support.verbose and self.server.chatty: sys.stdout.write(" server: connection cipher is now " + str(cipher) + "\n") + sys.stdout.write(" server: selected protocol is now " + + str(self.sslconn.selected_protocol()) + "\n") return True def read(self): @@ -979,7 +982,7 @@ def __init__(self, certificate=None, ssl_version=None, certreqs=None, cacerts=None, chatty=True, connectionchatty=False, starttls_server=False, - ciphers=None, context=None): + npn_protocols=None, ciphers=None, context=None): if context: self.context = context else: @@ -992,6 +995,8 @@ self.context.load_verify_locations(cacerts) if certificate: self.context.load_cert_chain(certificate) + if npn_protocols: + self.context.set_npn_protocols(npn_protocols) if ciphers: self.context.set_ciphers(ciphers) self.chatty = chatty @@ -1001,6 +1006,7 @@ self.port = support.bind_port(self.sock) self.flag = None self.active = False + self.selected_protocols = [] self.conn_errors = [] threading.Thread.__init__(self) self.daemon = True @@ -1195,6 +1201,7 @@ Launch a server, connect a client to it and try various reads and writes. """ + stats = {} server = ThreadedEchoServer(context=server_context, chatty=chatty, connectionchatty=False) @@ -1220,12 +1227,14 @@ if connectionchatty: if support.verbose: sys.stdout.write(" client: closing connection.\n") - stats = { + stats.update({ 'compression': s.compression(), 'cipher': s.cipher(), - } + 'client_npn_protocol': s.selected_protocol() + }) s.close() - return stats + stats['server_npn_protocols'] = server.selected_protocols + return stats def try_protocol_combo(server_protocol, client_protocol, expect_success, certsreqs=None, server_options=0, client_options=0): @@ -1853,6 +1862,33 @@ if "ADH" not in parts and "EDH" not in parts and "DHE" not in parts: self.fail("Non-DH cipher: " + cipher[0]) + def test_npn_ext(self): + server_protocols = ['http/1.1', 'spdy/2'] + protocol_tests = [ + (['http/1.1', 'spdy/2'], 'http/1.1'), + (['spdy/2', 'http/1.1'], 'http/1.1'), + (['spdy/2', 'test'], 'spdy/2'), + (['abc', 'def'], 'abc') + ] + for client_protocols, expected in protocol_tests: + server_context = ssl.SSLContext(ssl.PROTOCOL_TLSv1) + server_context.load_cert_chain(CERTFILE) + server_context.set_npn_protocols(server_protocols) + client_context = ssl.SSLContext(ssl.PROTOCOL_TLSv1) + client_context.load_cert_chain(CERTFILE) + client_context.set_npn_protocols(client_protocols) + stats = server_params_test(client_context, server_context, + chatty=True, connectionchatty=True) + + msg = "failed trying %s (s) and %s (c).\n" \ + "was expecting %s, but got %%s from the %%s" \ + % (str(server_protocols), str(client_protocols), + str(expected)) + client_result = stats['client_npn_protocol'] + self.assertEqual(client_result, expected, msg % (client_result, "client")) + server_result = stats['server_npn_protocols'][-1] \ + if len(stats['server_npn_protocols']) else 'nothing' + self.assertEqual(server_result, expected, msg % (server_result, "server")) def test_main(verbose=False): if support.verbose: diff -r 67d9595a833c Modules/_ssl.c --- a/Modules/_ssl.c Fri Mar 02 22:54:03 2012 +0100 +++ b/Modules/_ssl.c Sat Mar 10 12:13:48 2012 -0500 @@ -159,6 +159,9 @@ typedef struct { PyObject_HEAD SSL_CTX *ctx; +#ifdef OPENSSL_NPN_NEGOTIATED + char* npn_protocols; +#endif } PySSLContext; typedef struct { @@ -1015,6 +1018,27 @@ return NULL; } +static PyObject *PySSL_selectedproto(PySSLSocket *self) { + PyObject *retval; +#ifdef OPENSSL_NPN_NEGOTIATED + const unsigned char *out; + unsigned int outlen; + + SSL_get0_next_proto_negotiated(self->ssl, + &out, &outlen); + + if (out == NULL) + retval = Py_None; + else { + retval = PyUnicode_FromStringAndSize((char *) out, outlen); + } +#else + retval = Py_None; +#endif + Py_INCREF(retval); + return retval; +} + static PyObject *PySSL_compression(PySSLSocket *self) { #ifdef OPENSSL_NO_COMP Py_RETURN_NONE; @@ -1487,6 +1511,9 @@ {"peer_certificate", (PyCFunction)PySSL_peercert, METH_VARARGS, PySSL_peercert_doc}, {"cipher", (PyCFunction)PySSL_cipher, METH_NOARGS}, +#ifdef OPENSSL_NPN_NEGOTIATED + {"selected_protocol", (PyCFunction)PySSL_selectedproto, METH_NOARGS}, +#endif {"compression", (PyCFunction)PySSL_compression, METH_NOARGS}, {"shutdown", (PyCFunction)PySSL_SSLshutdown, METH_NOARGS, PySSL_SSLshutdown_doc}, @@ -1621,6 +1648,117 @@ Py_RETURN_NONE; } +#ifdef OPENSSL_NPN_NEGOTIATED +/* this callback gets passed to SSL_CTX_set_next_protos_advertise_cb */ +static int +_advertiseNPN_cb(SSL *s, + const unsigned char **data, unsigned int *len, + void *args) +{ + PySSLContext *ssl_ctx = (PySSLContext *) args; + + if (ssl_ctx->npn_protocols == NULL) { + *data = (unsigned char *) ""; + *len = 0; + } else { + *data = (unsigned char *) ssl_ctx->npn_protocols; + *len = strlen(ssl_ctx->npn_protocols); + } + + return SSL_TLSEXT_ERR_OK; +} +/* this callback gets passed to SSL_CTX_set_next_proto_select_cb */ +static int +_selectNPN_cb(SSL *s, + unsigned char **out, unsigned char *outlen, + const unsigned char *server, unsigned int server_len, + void *args) +{ + PySSLContext *ssl_ctx = (PySSLContext *) args; + + unsigned char *client = (unsigned char *) ssl_ctx->npn_protocols; + int client_len; + + if (client == NULL) { + client = (unsigned char *) ""; + client_len = 0; + } else { + client_len = (unsigned int) strlen(ssl_ctx->npn_protocols); + } + + SSL_select_next_proto(out, outlen, + server, server_len, + client, client_len); + + return SSL_TLSEXT_ERR_OK; +} + +#endif +static PyObject * +set_npn_protocols(PySSLContext *self, PyObject *args) +{ +#ifdef OPENSSL_NPN_NEGOTIATED + + PyObject *proto_list; + PyObject *seq; + PyObject *item; + char *item_ascii; + char *protos; + int i, len, item_len, protos_len; + + if (!PyArg_ParseTuple(args, "O:set_npn_protocols", &proto_list)) + return NULL; + + seq = PySequence_Fast(proto_list, "npn_protocols must be iterable"); + len = PySequence_Length(proto_list); + if (len == 0) { + PyErr_SetString(PySSLErrorObject, + "npn_protocols cannot be an empty list"); + return NULL; + } + + protos_len = 0; + for (i = 0; i < len; i++) { + item = PyUnicode_AsASCIIString(PySequence_GetItem(seq, i)); + if (item == NULL) { + PyErr_SetString(PySSLErrorObject, + "NPN protocols must be strings"); + return NULL; + } + PyBytes_AsStringAndSize(item, &item_ascii, &item_len); + if (item_len > 255) { + PyErr_SetString(PySSLErrorObject, + "NPN protocols must be 255 characters or less"); + return NULL; + } + protos_len += 1 + item_len; + } + + protos = malloc(protos_len + 1); + sprintf(protos, ""); + for (i = 0; i < len; i++) { + item = PyUnicode_AsASCIIString(PySequence_GetItem(seq, i)); + PyBytes_AsStringAndSize(item, &item_ascii, &item_len); + sprintf(protos, "%s%c%s", protos, item_len, item_ascii); + } + self->npn_protocols = protos; + + /* set both server and client callbacks, because the context can + * be used to create both types of sockets */ + SSL_CTX_set_next_protos_advertised_cb(self->ctx, + _advertiseNPN_cb, + self); + SSL_CTX_set_next_proto_select_cb(self->ctx, + _selectNPN_cb, + self); + Py_RETURN_NONE; +#else + PyErr_SetString(PySSLErrorObject, + "The NPN extension requires OpenSSL 1.0.1 or later."); + return NULL; +#endif +} + static PyObject * get_verify_mode(PySSLContext *self, void *c) { @@ -2097,6 +2235,8 @@ METH_VARARGS | METH_KEYWORDS, NULL}, {"set_ciphers", (PyCFunction) set_ciphers, METH_VARARGS, NULL}, + {"set_npn_protocols", (PyCFunction) set_npn_protocols, + METH_VARARGS, NULL}, {"load_cert_chain", (PyCFunction) load_cert_chain, METH_VARARGS | METH_KEYWORDS, NULL}, {"load_dh_params", (PyCFunction) load_dh_params, @@ -2590,6 +2730,14 @@ Py_INCREF(r); PyModule_AddObject(m, "HAS_ECDH", r); +#ifdef OPENSSL_NPN_NEGOTIATED + r = Py_True; +#else + r = Py_False; +#endif + Py_INCREF(r); + PyModule_AddObject(m, "HAS_NPN", r); + /* OpenSSL version */ /* SSLeay() gives us the version of the library linked against, which could be different from the headers version.