diff options
-rw-r--r-- | cryptography/hazmat/backends/openssl/backend.py | 41 | ||||
-rw-r--r-- | cryptography/hazmat/bindings/openssl/binding.py | 3 | ||||
-rw-r--r-- | cryptography/hazmat/bindings/openssl/osrand_engine.py | 179 | ||||
-rw-r--r-- | docs/hazmat/backends/openssl.rst | 28 | ||||
-rw-r--r-- | docs/spelling_wordlist.txt | 1 | ||||
-rw-r--r-- | tests/hazmat/backends/test_openssl.py | 189 |
6 files changed, 440 insertions, 1 deletions
diff --git a/cryptography/hazmat/backends/openssl/backend.py b/cryptography/hazmat/backends/openssl/backend.py index cf931dab..99d97b7f 100644 --- a/cryptography/hazmat/backends/openssl/backend.py +++ b/cryptography/hazmat/backends/openssl/backend.py @@ -58,6 +58,47 @@ class Backend(object): self._cipher_registry = {} self._register_default_ciphers() + self.register_osrandom_engine() + + def unregister_osrandom_engine(self): + e = self._lib.ENGINE_get_default_RAND() + if e != self._ffi.NULL: + name = self._lib.ENGINE_get_name(e) + assert name != self._ffi.NULL + if name == self._lib.Cryptography_osrandom_engine_name: + self._lib.ENGINE_unregister_RAND(e) + # this resets the RNG to use the new engine + self._lib.RAND_cleanup() + res = self._lib.ENGINE_finish(e) + assert res == 1 + + def register_osrandom_engine(self): + current_rand = self._lib.ENGINE_get_default_RAND() + if current_rand != self._ffi.NULL: + name = self._lib.ENGINE_get_name(current_rand) + assert name != self._ffi.NULL + if name != self._lib.Cryptography_osrandom_engine_name: + self._register_osrandom_engine() + res = self._lib.ENGINE_finish(current_rand) + assert res == 1 + else: + self._register_osrandom_engine() + + def _register_osrandom_engine(self): + e = self._lib.ENGINE_by_id(self._lib.Cryptography_osrandom_engine_id) + assert e != self._ffi.NULL + res = self._lib.ENGINE_init(e) + assert res == 1 + 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 + # this resets 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 cde3bdbd..9df26cde 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", + "osrand_engine", "pem", "pkcs7", "pkcs12", @@ -95,6 +96,8 @@ class Binding(object): 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/osrand_engine.py b/cryptography/hazmat/bindings/openssl/osrand_engine.py new file mode 100644 index 00000000..bca2d79d --- /dev/null +++ b/cryptography/hazmat/bindings/openssl/osrand_engine.py @@ -0,0 +1,179 @@ +# 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 = """ +""" + +CUSTOMIZATIONS = """ +static const char *Cryptography_osrandom_engine_id= "osrandom"; +static const char *Cryptography_osrandom_engine_name = "osrandom_engine"; + +#ifndef _WIN32 +static int urandom_fd = -1; + +static int osrandom_rand_bytes(unsigned char *buffer, int size) { + ssize_t n; + while (0 < size) { + do { + n = read(urandom_fd, buffer, (size_t)size); + } while (n < 0 && errno == EINTR); + if (n <= 0) { + return 0; + } + buffer += n; + size -= n; + } + return 1; +} + +static int osrandom_rand_status(void) { + if (urandom_fd == -1) { + return 0; + } else { + return 1; + } +} + +static int osrandom_init(ENGINE *e) { + if (urandom_fd > -1) { + return 1; + } + urandom_fd = open("/dev/urandom", O_RDONLY); + if (urandom_fd > -1) { + if (fcntl(urandom_fd, F_SETFD, FD_CLOEXEC) == -1) { + return 0; + } + return 1; + } else { + return 0; + } +} + +static int osrandom_finish(ENGINE *e) { + int n; + do { + n = close(urandom_fd); + } while (n < 0 && errno == EINTR); + if (n < 0) { + urandom_fd = -1; + return 0; + } else { + urandom_fd = -1; + return 1; + } +} +#endif + +#ifdef _WIN32 +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) { + size_t chunk; + + if (hCryptProv == 0) { + return 0; + } + + while (size > 0) { + chunk = size; + if (!CryptGenRandom(hCryptProv, (DWORD)chunk, buffer)) { + return 0; + } + buffer += chunk; + size -= chunk; + } + 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; + } +} +#endif /* MS_WINDOWS */ + +static RAND_METHOD osrandom_rand = { + NULL, + osrandom_rand_bytes, + NULL, + NULL, + osrandom_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)) { + 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/hazmat/backends/openssl.rst b/docs/hazmat/backends/openssl.rst index 12d2d9f6..5ad00d03 100644 --- a/docs/hazmat/backends/openssl.rst +++ b/docs/hazmat/backends/openssl.rst @@ -7,10 +7,36 @@ The `OpenSSL`_ C library. .. data:: cryptography.hazmat.backends.openssl.backend - This is the exposed API for the OpenSSL backend. It has one public attribute. + This is the exposed API for the OpenSSL backend. .. attribute:: name The string name of this backend: ``"openssl"`` + .. method:: register_osrandom_engine() + + Registers the OS random engine as default. This will effectively + disable OpenSSL's default CSPRNG. + + .. method:: unregister_osrandom_engine() + + Unregisters the OS random engine if it is default. This will restore + the default OpenSSL CSPRNG. If the OS random engine is not the default + engine (e.g. if another engine is set as default) nothing will be + changed. + +OS Random Engine +---------------- + +OpenSSL has a CSPRNG that it seeds when starting up. Unfortunately, its state +is replicated when the process is forked and child processes can deliver +similar or identical random values. OpenSSL has landed a patch to mitigate this +issue, but this project can't rely on users having recent versions. + +To work around this cryptography uses a custom OpenSSL engine that replaces the +standard random source with one that fetches entropy from ``/dev/urandom`` (or +CryptGenRandom on Windows). This engine is **active** by default when importing +the OpenSSL backend. It is added to the engine list but not activated if you +only import the binding. + .. _`OpenSSL`: https://www.openssl.org/ diff --git a/docs/spelling_wordlist.txt b/docs/spelling_wordlist.txt index 75628ba5..e05efc6c 100644 --- a/docs/spelling_wordlist.txt +++ b/docs/spelling_wordlist.txt @@ -27,3 +27,4 @@ Changelog Docstrings Fernet Schneier +Unregisters diff --git a/tests/hazmat/backends/test_openssl.py b/tests/hazmat/backends/test_openssl.py index f01c3f64..e527ed19 100644 --- a/tests/hazmat/backends/test_openssl.py +++ b/tests/hazmat/backends/test_openssl.py @@ -11,6 +11,8 @@ # See the License for the specific language governing permissions and # limitations under the License. +import cffi + import pytest from cryptography import utils @@ -23,6 +25,96 @@ from cryptography.hazmat.primitives.ciphers.algorithms import AES from cryptography.hazmat.primitives.ciphers.modes import CBC +ffi = cffi.FFI() + +ffi.cdef(""" +static const char *const Cryptography_faux_engine_name; +static const char *const Cryptography_faux_engine_id; +int Cryptography_add_faux_engine(void); +""") +dummy_engine = ffi.verify( + source=""" + #include <openssl/engine.h> + #include <string.h> + static const char *const Cryptography_faux_engine_name="faux_engine"; + static const char *const Cryptography_faux_engine_id="faux"; + static int faux_bytes(unsigned char *buffer, int size) { + memset(buffer, 1, size); + return 1; + } + static int faux_status(void) { return 1; } + static int faux_init(ENGINE *e) { return 1; } + static int faux_finish(ENGINE *e) { return 1; } + static RAND_METHOD faux_rand = { + NULL, + faux_bytes, + NULL, + NULL, + faux_bytes, + faux_status, + }; + + int Cryptography_add_faux_engine(void) { + ENGINE *e = ENGINE_new(); + if (e == NULL) { + return 0; + } + if(!ENGINE_set_id(e, Cryptography_faux_engine_id) || + !ENGINE_set_name(e, Cryptography_faux_engine_name) || + !ENGINE_set_RAND(e, &faux_rand) || + !ENGINE_set_init_function(e, faux_init) || + !ENGINE_set_finish_function(e, faux_finish)) { + return 0; + } + if (!ENGINE_add(e)) { + ENGINE_free(e); + return 0; + } + if (!ENGINE_free(e)) { + return 0; + } + + return 1; + } + """, + libraries=["crypto", "ssl"], +) + + +def register_dummy_engine(): + current_rand = backend._lib.ENGINE_get_default_RAND() + assert current_rand != backend._ffi.NULL + name = backend._lib.ENGINE_get_name(current_rand) + assert name != backend._ffi.NULL + assert name != dummy_engine.Cryptography_faux_engine_id + res = backend._lib.ENGINE_finish(current_rand) + assert res == 1 + e = backend._lib.ENGINE_by_id(dummy_engine.Cryptography_faux_engine_id) + assert e != backend._ffi.NULL + res = backend._lib.ENGINE_init(e) + assert res == 1 + res = backend._lib.ENGINE_set_default_RAND(e) + assert res == 1 + res = backend._lib.ENGINE_finish(e) + assert res == 1 + res = backend._lib.ENGINE_free(e) + assert res == 1 + # this resets the RNG to use the new engine + backend._lib.RAND_cleanup() + + +def unregister_dummy_engine(): + e = backend._lib.ENGINE_get_default_RAND() + if e != backend._ffi.NULL: + name = backend._lib.ENGINE_get_name(e) + assert name != backend._ffi.NULL + if name == dummy_engine.Cryptography_faux_engine_name: + backend._lib.ENGINE_unregister_RAND(e) + backend._lib.RAND_cleanup() + res = backend._lib.ENGINE_finish(e) + assert res == 1 + + @utils.register_interface(interfaces.Mode) class DummyMode(object): name = "dummy-mode" @@ -113,3 +205,100 @@ class TestOpenSSL(object): b"error:0607F08A:digital envelope routines:EVP_EncryptFinal_ex:" b"data not multiple of block length" ) + + # 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): + @classmethod + def setup_class(cls): + # add the faux engine to the list of available engines + res = dummy_engine.Cryptography_add_faux_engine() + assert res == 1 + + def teardown_method(self, method): + # we need to reset state to being default. backend is a shared global + # for all these tests. + unregister_dummy_engine() + backend.register_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_register_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.register_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_unregister_osrandom_engine_nothing_registered(self): + backend.unregister_osrandom_engine() + e = backend._lib.ENGINE_get_default_RAND() + assert e == backend._ffi.NULL + backend.unregister_osrandom_engine() + e = backend._lib.ENGINE_get_default_RAND() + assert e == backend._ffi.NULL + + def test_unregister_osrandom_engine(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.unregister_osrandom_engine() + e = backend._lib.ENGINE_get_default_RAND() + assert e == backend._ffi.NULL + + def test_register_osrandom_no_default(self): + backend.unregister_osrandom_engine() + e = backend._lib.ENGINE_get_default_RAND() + assert e == backend._ffi.NULL + backend.register_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_unregister_osrandom_other_engine_default(self): + register_dummy_engine() + default = backend._lib.ENGINE_get_default_RAND() + default_name = backend._lib.ENGINE_get_name(default) + assert default_name == dummy_engine.Cryptography_faux_engine_name + res = backend._lib.ENGINE_finish(default) + assert res == 1 + backend.unregister_osrandom_engine() + current_default = backend._lib.ENGINE_get_default_RAND() + name = backend._lib.ENGINE_get_name(current_default) + assert name == dummy_engine.Cryptography_faux_engine_name + res = backend._lib.ENGINE_finish(current_default) + assert res == 1 + + def test_register_osrandom_other_engine_default(self): + register_dummy_engine() + default = backend._lib.ENGINE_get_default_RAND() + default_name = backend._lib.ENGINE_get_name(default) + assert default_name == dummy_engine.Cryptography_faux_engine_name + res = backend._lib.ENGINE_finish(default) + assert res == 1 + backend.register_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 + res = backend._lib.ENGINE_finish(current_default) + assert res == 1 |