diff options
-rw-r--r-- | cryptography/hazmat/backends/openssl/backend.py | 34 | ||||
-rw-r--r-- | cryptography/hazmat/bindings/openssl/binding.py | 5 | ||||
-rw-r--r-- | cryptography/hazmat/bindings/openssl/osrandom_engine.py | 198 | ||||
-rw-r--r-- | docs/changelog.rst | 1 | ||||
-rw-r--r-- | docs/hazmat/backends/openssl.rst | 50 | ||||
-rw-r--r-- | tests/hazmat/backends/test_openssl.py | 69 |
6 files changed, 355 insertions, 2 deletions
diff --git a/cryptography/hazmat/backends/openssl/backend.py b/cryptography/hazmat/backends/openssl/backend.py index 74faee57..6da90cef 100644 --- a/cryptography/hazmat/backends/openssl/backend.py +++ b/cryptography/hazmat/backends/openssl/backend.py @@ -58,6 +58,40 @@ class Backend(object): self._cipher_registry = {} self._register_default_ciphers() + self.activate_osrandom_engine() + + def activate_builtin_random(self): + # Obtain a new structural reference. + e = self._lib.ENGINE_get_default_RAND() + if e != self._ffi.NULL: + self._lib.ENGINE_unregister_RAND(e) + # Reset the RNG to use the new engine. + self._lib.RAND_cleanup() + # decrement the structural reference from get_default_RAND + res = self._lib.ENGINE_finish(e) + assert res == 1 + + def activate_osrandom_engine(self): + # Unregister and free the current engine. + self.activate_builtin_random() + # Fetches an engine by id and returns it. This creates a structural + # reference. + e = self._lib.ENGINE_by_id(self._lib.Cryptography_osrandom_engine_id) + assert e != self._ffi.NULL + # Initialize the engine for use. This adds a functional reference. + res = self._lib.ENGINE_init(e) + assert res == 1 + # Set the engine as the default RAND provider. + res = self._lib.ENGINE_set_default_RAND(e) + assert res == 1 + # Decrement the structural ref incremented by ENGINE_by_id. + res = self._lib.ENGINE_free(e) + assert res == 1 + # Decrement the functional ref incremented by ENGINE_init. + res = self._lib.ENGINE_finish(e) + assert res == 1 + # Reset the RNG to use the new engine. + self._lib.RAND_cleanup() def openssl_version_text(self): """ diff --git a/cryptography/hazmat/bindings/openssl/binding.py b/cryptography/hazmat/bindings/openssl/binding.py index 3adcc887..714ecc07 100644 --- a/cryptography/hazmat/bindings/openssl/binding.py +++ b/cryptography/hazmat/bindings/openssl/binding.py @@ -60,6 +60,7 @@ class Binding(object): "nid", "objects", "opensslv", + "osrandom_engine", "pem", "pkcs7", "pkcs12", @@ -91,11 +92,13 @@ class Binding(object): if sys.platform != "win32": libraries = ["crypto", "ssl"] else: # pragma: no cover - libraries = ["libeay32", "ssleay32"] + libraries = ["libeay32", "ssleay32", "advapi32"] cls.ffi, cls.lib = build_ffi(cls._module_prefix, cls._modules, _OSX_PRE_INCLUDE, _OSX_POST_INCLUDE, libraries) + res = cls.lib.Cryptography_add_osrandom_engine() + assert res == 1 @classmethod def is_available(cls): diff --git a/cryptography/hazmat/bindings/openssl/osrandom_engine.py b/cryptography/hazmat/bindings/openssl/osrandom_engine.py new file mode 100644 index 00000000..6e7e172e --- /dev/null +++ b/cryptography/hazmat/bindings/openssl/osrandom_engine.py @@ -0,0 +1,198 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +INCLUDES = """ +#ifdef _WIN32 +#include <Wincrypt.h> +#else +#include <fcntl.h> +#include <unistd.h> +#endif +""" + +TYPES = """ +static const char *const Cryptography_osrandom_engine_name; +static const char *const Cryptography_osrandom_engine_id; +""" + +FUNCTIONS = """ +int Cryptography_add_osrandom_engine(void); +""" + +MACROS = """ +""" + +WIN32_CUSTOMIZATIONS = """ +static HCRYPTPROV hCryptProv = 0; + +static int osrandom_init(ENGINE *e) { + if (hCryptProv > 0) { + return 1; + } + if (CryptAcquireContext(&hCryptProv, NULL, NULL, + PROV_RSA_FULL, CRYPT_VERIFYCONTEXT)) { + return 1; + } else { + return 0; + } +} + +static int osrandom_rand_bytes(unsigned char *buffer, int size) { + if (hCryptProv == 0) { + return 0; + } + + if (!CryptGenRandom(hCryptProv, (DWORD)size, buffer)) { + ERR_put_error(ERR_LIB_RAND, 0, ERR_R_RAND_LIB, "osrandom.py", 0); + return 0; + } + return 1; +} + +static int osrandom_finish(ENGINE *e) { + if (CryptReleaseContext(hCryptProv, 0)) { + hCryptProv = 0; + return 1; + } else { + return 0; + } +} + +static int osrandom_rand_status(void) { + if (hCryptProv == 0) { + return 0; + } else { + return 1; + } +} +""" + +POSIX_CUSTOMIZATIONS = """ +static int urandom_fd = -1; + +static int osrandom_finish(ENGINE *e); + +static int osrandom_init(ENGINE *e) { + if (urandom_fd > -1) { + return 1; + } + urandom_fd = open("/dev/urandom", O_RDONLY); + if (urandom_fd > -1) { + int flags = fcntl(urandom_fd, F_GETFD); + if (flags == -1) { + osrandom_finish(e); + return 0; + } else if (fcntl(urandom_fd, F_SETFD, flags | FD_CLOEXEC) == -1) { + osrandom_finish(e); + return 0; + } + return 1; + } else { + return 0; + } +} + +static int osrandom_rand_bytes(unsigned char *buffer, int size) { + ssize_t n; + while (size > 0) { + do { + n = read(urandom_fd, buffer, (size_t)size); + } while (n < 0 && errno == EINTR); + if (n <= 0) { + ERR_put_error(ERR_LIB_RAND, 0, ERR_R_RAND_LIB, "osrandom.py", 0); + return 0; + } + buffer += n; + size -= n; + } + return 1; +} + +static int osrandom_finish(ENGINE *e) { + int n; + do { + n = close(urandom_fd); + } while (n < 0 && errno == EINTR); + urandom_fd = -1; + if (n < 0) { + return 0; + } else { + return 1; + } +} + +static int osrandom_rand_status(void) { + if (urandom_fd == -1) { + return 0; + } else { + return 1; + } +} +""" + +CUSTOMIZATIONS = """ +static const char *Cryptography_osrandom_engine_id = "osrandom"; +static const char *Cryptography_osrandom_engine_name = "osrandom_engine"; + +#if defined(_WIN32) +""" + WIN32_CUSTOMIZATIONS + """ +#else +""" + POSIX_CUSTOMIZATIONS + """ +#endif + +/* This replicates the behavior of the OpenSSL FIPS RNG, which returns a + -1 in the event that there is an error when calling RAND_pseudo_bytes. */ +static int osrandom_pseudo_rand_bytes(unsigned char *buffer, int size) { + int res = osrandom_rand_bytes(buffer, size); + if (res == 0) { + return -1; + } else { + return res; + } +} + +static RAND_METHOD osrandom_rand = { + NULL, + osrandom_rand_bytes, + NULL, + NULL, + osrandom_pseudo_rand_bytes, + osrandom_rand_status, +}; + +int Cryptography_add_osrandom_engine(void) { + ENGINE *e = ENGINE_new(); + if (e == NULL) { + return 0; + } + if(!ENGINE_set_id(e, Cryptography_osrandom_engine_id) || + !ENGINE_set_name(e, Cryptography_osrandom_engine_name) || + !ENGINE_set_RAND(e, &osrandom_rand) || + !ENGINE_set_init_function(e, osrandom_init) || + !ENGINE_set_finish_function(e, osrandom_finish)) { + ENGINE_free(e); + return 0; + } + if (!ENGINE_add(e)) { + ENGINE_free(e); + return 0; + } + if (!ENGINE_free(e)) { + return 0; + } + + return 1; +} +""" + +CONDITIONAL_NAMES = {} diff --git a/docs/changelog.rst b/docs/changelog.rst index e322b145..4d459bd9 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -17,6 +17,7 @@ Changelog * Added :class:`~cryptography.hazmat.primitives.kdf.pbkdf2.PBKDF2HMAC`. * Added :class:`~cryptography.hazmat.primitives.kdf.hkdf.HKDF`. * Added :doc:`/hazmat/backends/multibackend`. +* Set default random for the :doc:`/hazmat/backends/openssl` to the OS random engine. 0.1 - 2014-01-08 ~~~~~~~~~~~~~~~~ diff --git a/docs/hazmat/backends/openssl.rst b/docs/hazmat/backends/openssl.rst index 4db3972d..ea72af96 100644 --- a/docs/hazmat/backends/openssl.rst +++ b/docs/hazmat/backends/openssl.rst @@ -16,10 +16,58 @@ The `OpenSSL`_ C library. * :class:`~cryptography.hazmat.backends.interfaces.HMACBackend` * :class:`~cryptography.hazmat.backends.interfaces.PBKDF2HMACBackend` - It has one additional public attribute. + It also exposes the following: .. attribute:: name The string name of this backend: ``"openssl"`` + .. method:: activate_osrandom_engine() + + Activates the OS random engine. This will effectively disable OpenSSL's + default CSPRNG. + + .. method:: activate_builtin_random() + + This will activate the default OpenSSL CSPRNG. + +OS Random Engine +---------------- + +OpenSSL uses a user-space CSPRNG that is seeded from system random ( +``/dev/urandom`` or ``CryptGenRandom``). This CSPRNG is not reseeded +automatically when a process calls ``fork()``. This can result in situations +where two different processes can return similar or identical keys and +compromise the security of the system. + +The approach this project has chosen to mitigate this vulnerability is to +include an engine that replaces the OpenSSL default CSPRNG with one that sources +its entropy from ``/dev/urandom`` on UNIX-like operating systems and uses +``CryptGenRandom`` on Windows. This method of pulling from the system pool +allows us to avoid potential issues with `initializing the RNG`_ as well as +protecting us from the ``fork()`` weakness. + +This engine is **active** by default when importing the OpenSSL backend. When +active this engine will be used to generate all the random data OpenSSL +requests. + +When importing only the binding it is added to the engine list but +**not activated**. + + +OS Random Sources +----------------- + +On OS X and FreeBSD ``/dev/urandom`` is an alias for ``/dev/random`` and +utilizes the `Yarrow`_ algorithm. + +On Windows ``CryptGenRandom`` is backed by `Fortuna`_. + +Linux uses its own PRNG design. ``/dev/urandom`` is a non-blocking source seeded +from the same pool as ``/dev/random``. + + .. _`OpenSSL`: https://www.openssl.org/ +.. _`initializing the RNG`: http://en.wikipedia.org/wiki/OpenSSL#Vulnerability_in_the_Debian_implementation +.. _`Yarrow`: http://en.wikipedia.org/wiki/Yarrow_algorithm +.. _`Fortuna`: http://en.wikipedia.org/wiki/Fortuna_(PRNG) diff --git a/tests/hazmat/backends/test_openssl.py b/tests/hazmat/backends/test_openssl.py index ea04c133..b24808df 100644 --- a/tests/hazmat/backends/test_openssl.py +++ b/tests/hazmat/backends/test_openssl.py @@ -152,3 +152,72 @@ class TestOpenSSL(object): pytest.skip("Requires an older OpenSSL") with pytest.raises(UnsupportedAlgorithm): backend.derive_pbkdf2_hmac(hashes.SHA256(), 10, b"", 1000, b"") + + # This test is not in the next class because to check if it's really + # default we don't want to run the setup_method before it + def test_osrandom_engine_is_default(self): + e = backend._lib.ENGINE_get_default_RAND() + name = backend._lib.ENGINE_get_name(e) + assert name == backend._lib.Cryptography_osrandom_engine_name + res = backend._lib.ENGINE_free(e) + assert res == 1 + + +class TestOpenSSLRandomEngine(object): + def teardown_method(self, method): + # we need to reset state to being default. backend is a shared global + # for all these tests. + backend.activate_osrandom_engine() + current_default = backend._lib.ENGINE_get_default_RAND() + name = backend._lib.ENGINE_get_name(current_default) + assert name == backend._lib.Cryptography_osrandom_engine_name + + def test_osrandom_sanity_check(self): + # This test serves as a check against catastrophic failure. + buf = backend._ffi.new("char[]", 500) + res = backend._lib.RAND_bytes(buf, 500) + assert res == 1 + assert backend._ffi.buffer(buf)[:] != "\x00" * 500 + + def test_activate_osrandom_already_default(self): + e = backend._lib.ENGINE_get_default_RAND() + name = backend._lib.ENGINE_get_name(e) + assert name == backend._lib.Cryptography_osrandom_engine_name + res = backend._lib.ENGINE_free(e) + assert res == 1 + backend.activate_osrandom_engine() + e = backend._lib.ENGINE_get_default_RAND() + name = backend._lib.ENGINE_get_name(e) + assert name == backend._lib.Cryptography_osrandom_engine_name + res = backend._lib.ENGINE_free(e) + assert res == 1 + + def test_activate_osrandom_no_default(self): + backend.activate_builtin_random() + e = backend._lib.ENGINE_get_default_RAND() + assert e == backend._ffi.NULL + backend.activate_osrandom_engine() + e = backend._lib.ENGINE_get_default_RAND() + name = backend._lib.ENGINE_get_name(e) + assert name == backend._lib.Cryptography_osrandom_engine_name + res = backend._lib.ENGINE_free(e) + assert res == 1 + + def test_activate_builtin_random(self): + e = backend._lib.ENGINE_get_default_RAND() + assert e != backend._ffi.NULL + name = backend._lib.ENGINE_get_name(e) + assert name == backend._lib.Cryptography_osrandom_engine_name + res = backend._lib.ENGINE_free(e) + assert res == 1 + backend.activate_builtin_random() + e = backend._lib.ENGINE_get_default_RAND() + assert e == backend._ffi.NULL + + def test_activate_builtin_random_already_active(self): + backend.activate_builtin_random() + e = backend._lib.ENGINE_get_default_RAND() + assert e == backend._ffi.NULL + backend.activate_builtin_random() + e = backend._lib.ENGINE_get_default_RAND() + assert e == backend._ffi.NULL |