diff --git a/Doc/library/ssl.rst b/Doc/library/ssl.rst --- a/Doc/library/ssl.rst +++ b/Doc/library/ssl.rst @@ -428,9 +428,17 @@ Constants .. versionadded:: 3.3 +.. data:: OP_SINGLE_DH_USE + + Prevents re-use of the same DH key for distinct SSL sessions. This + improves forward secrecy but requires more computational resources. + This option only applies to server sockets. + + .. versionadded:: 3.3 + .. data:: OP_SINGLE_ECDH_USE - Prevents re-use of the same ECDH key for several SSL sessions. This + Prevents re-use of the same ECDH key for distinct SSL sessions. This improves forward secrecy but requires more computational resources. This option only applies to server sockets. @@ -680,12 +688,24 @@ to speed up repeated connections from th when connected, the :meth:`SSLSocket.cipher` method of SSL sockets will give the currently selected cipher. +.. method:: SSLContext.load_dh_params(dhfile) + + Load the key generation parameters for Diffie-Helman (DH) key exchange. + Using DH key exchange improves forward secrecy at the expense of + computational resources (both on the server and on the client). + The *dhfile* parameter should be the path to a file containing DH + parameters in PEM format. + + This setting doesn't apply to client sockets. You can also use the + :data:`OP_SINGLE_DH_USE` option to further improve security. + + .. versionadded:: 3.3 + .. method:: SSLContext.set_ecdh_curve(curve_name) - Set the curve name for Elliptic Curve-based Diffie-Hellman (abbreviated - ECDH) key exchange. Using Diffie-Hellman key exchange improves forward - secrecy at the expense of computational resources (both on the server and - on the client). The *curve_name* parameter should be a string describing + Set the curve name for Elliptic Curve-based Diffie-Hellman (ECDH) key + exchange. ECDH is significantly faster than regular DH while arguably + as secure. The *curve_name* parameter should be a string describing a well-known elliptic curve, for example ``prime256v1`` for a widely supported curve. diff --git a/Lib/ssl.py b/Lib/ssl.py --- a/Lib/ssl.py +++ b/Lib/ssl.py @@ -68,7 +68,7 @@ from _ssl import ( from _ssl import CERT_NONE, CERT_OPTIONAL, CERT_REQUIRED from _ssl import ( OP_ALL, OP_NO_SSLv2, OP_NO_SSLv3, OP_NO_TLSv1, - OP_CIPHER_SERVER_PREFERENCE, OP_SINGLE_ECDH_USE, + OP_CIPHER_SERVER_PREFERENCE, OP_SINGLE_DH_USE, OP_SINGLE_ECDH_USE, ) from _ssl import RAND_status, RAND_egd, RAND_add, RAND_bytes, RAND_pseudo_bytes from _ssl import ( diff --git a/Lib/test/dh512.pem b/Lib/test/dh512.pem new file mode 100644 --- /dev/null +++ b/Lib/test/dh512.pem @@ -0,0 +1,9 @@ +-----BEGIN DH PARAMETERS----- +MEYCQQD1Kv884bEpQBgRjXyEpwpy1obEAxnIByl6ypUM2Zafq9AKUJsCRtMIPWak +XUGfnHy9iUsiGSa6q6Jew1XpKgVfAgEC +-----END DH PARAMETERS----- + +These are the 512 bit DH parameters from "Assigned Number for SKIP Protocols" +(http://www.skip-vpn.org/spec/numbers.html). +See there for how they were generated. +Note that g is not a generator, but this is not a problem since p is a safe prime. diff --git a/Lib/test/ssl_servers.py b/Lib/test/ssl_servers.py --- a/Lib/test/ssl_servers.py +++ b/Lib/test/ssl_servers.py @@ -179,6 +179,8 @@ if __name__ == "__main__": parser.add_argument('--curve-name', dest='curve_name', type=str, action='store', help='curve name for EC-based Diffie-Hellman') + parser.add_argument('--dh', dest='dh_file', type=str, action='store', + help='PEM file containing DH parameters') args = parser.parse_args() support.verbose = args.verbose @@ -191,6 +193,8 @@ if __name__ == "__main__": context.load_cert_chain(CERTFILE) if args.curve_name: context.set_ecdh_curve(args.curve_name) + if args.dh_file: + context.load_dh_params(args.dh_file) server = HTTPSServer(("", args.port), handler_class, context) if args.verbose: diff --git a/Lib/test/test_ssl.py b/Lib/test/test_ssl.py --- a/Lib/test/test_ssl.py +++ b/Lib/test/test_ssl.py @@ -56,6 +56,8 @@ WRONGCERT = data_file("XXXnonexisting.pe BADKEY = data_file("badkey.pem") NOKIACERT = data_file("nokia.pem") +DHFILE = data_file("dh512.pem") +BYTES_DHFILE = os.fsencode(DHFILE) def handle_error(prefix): exc_format = ' '.join(traceback.format_exception(*sys.exc_info())) @@ -99,6 +101,7 @@ class BasicSocketTests(unittest.TestCase ssl.CERT_OPTIONAL ssl.CERT_REQUIRED ssl.OP_CIPHER_SERVER_PREFERENCE + ssl.OP_SINGLE_DH_USE ssl.OP_SINGLE_ECDH_USE self.assertIn(ssl.HAS_SNI, {True, False}) @@ -535,6 +538,18 @@ class ContextTests(unittest.TestCase): # Issue #10989: crash if the second argument type is invalid self.assertRaises(TypeError, ctx.load_verify_locations, None, True) + def test_load_dh_params(self): + ctx = ssl.SSLContext(ssl.PROTOCOL_TLSv1) + ctx.load_dh_params(DHFILE) + ctx.load_dh_params(BYTES_DHFILE) + self.assertRaises(TypeError, ctx.load_dh_params) + self.assertRaises(TypeError, ctx.load_dh_params, None) + with self.assertRaises(FileNotFoundError) as cm: + ctx.load_dh_params(WRONGCERT) + self.assertEqual(cm.exception.errno, errno.ENOENT) + with self.assertRaisesRegex(ssl.SSLError, "PEM routines"): + ctx.load_dh_params(CERTFILE) + @skip_if_broken_ubuntu_ssl def test_session_stats(self): for proto in PROTOCOLS: @@ -1185,7 +1200,11 @@ else: if connectionchatty: if support.verbose: sys.stdout.write(" client: closing connection.\n") + stats = { + 'cipher': s.cipher(), + } s.close() + return stats finally: server.stop() server.join() @@ -1814,6 +1833,19 @@ else: server.stop() server.join() + def test_dh_params(self): + # Check we can get a connection with ephemeral Diffie-Hellman + context = ssl.SSLContext(ssl.PROTOCOL_TLSv1) + context.load_cert_chain(CERTFILE) + context.load_dh_params(DHFILE) + context.set_ciphers("kEDH") + stats = server_params_test(context, context, + chatty=True, connectionchatty=True) + cipher = stats["cipher"][0] + parts = cipher.split("-") + if "ADH" not in parts and "EDH" not in parts and "DHE" not in parts: + self.fail("Non-DH cipher: " + cipher[0]) + def test_main(verbose=False): if support.verbose: plats = { diff --git a/Modules/_ssl.c b/Modules/_ssl.c --- a/Modules/_ssl.c +++ b/Modules/_ssl.c @@ -1902,6 +1902,38 @@ load_verify_locations(PySSLContext *self } static PyObject * +load_dh_params(PySSLContext *self, PyObject *filepath) +{ + FILE *f; + DH *dh; + + f = _Py_fopen(filepath, "rb"); + if (f == NULL) { + if (!PyErr_Occurred()) + PyErr_SetFromErrnoWithFilenameObject(PyExc_OSError, filepath); + return NULL; + } + errno = 0; + PySSL_BEGIN_ALLOW_THREADS + dh = PEM_read_DHparams(f, NULL, NULL, NULL); + PySSL_END_ALLOW_THREADS + if (dh == NULL) { + if (errno != 0) { + ERR_clear_error(); + PyErr_SetFromErrnoWithFilenameObject(PyExc_OSError, filepath); + } + else { + _setSSLError(NULL, 0, __FILE__, __LINE__); + } + return NULL; + } + if (SSL_CTX_set_tmp_dh(self->ctx, dh) == 0) + _setSSLError(NULL, 0, __FILE__, __LINE__); + DH_free(dh); + Py_RETURN_NONE; +} + +static PyObject * context_wrap_socket(PySSLContext *self, PyObject *args, PyObject *kwds) { char *kwlist[] = {"sock", "server_side", "server_hostname", NULL}; @@ -2028,6 +2060,8 @@ static struct PyMethodDef context_method METH_VARARGS, NULL}, {"load_cert_chain", (PyCFunction) load_cert_chain, METH_VARARGS | METH_KEYWORDS, NULL}, + {"load_dh_params", (PyCFunction) load_dh_params, + METH_O, NULL}, {"load_verify_locations", (PyCFunction) load_verify_locations, METH_VARARGS | METH_KEYWORDS, NULL}, {"session_stats", (PyCFunction) session_stats, @@ -2481,6 +2515,7 @@ PyInit__ssl(void) PyModule_AddIntConstant(m, "OP_NO_TLSv1", SSL_OP_NO_TLSv1); PyModule_AddIntConstant(m, "OP_CIPHER_SERVER_PREFERENCE", SSL_OP_CIPHER_SERVER_PREFERENCE); + PyModule_AddIntConstant(m, "OP_SINGLE_DH_USE", SSL_OP_SINGLE_DH_USE); PyModule_AddIntConstant(m, "OP_SINGLE_ECDH_USE", SSL_OP_SINGLE_ECDH_USE); #ifdef SSL_CTRL_SET_TLSEXT_HOSTNAME