From f2aa3723ca88e6708722a3d93699dbffd18d4aaa Mon Sep 17 00:00:00 2001 From: Markus Theil Date: Wed, 14 Aug 2024 22:45:13 +0200 Subject: [PATCH] Add Entropy Source and DRNG Manager (ESDM) RNG support ESDM is a Linux-based user-space PRNG daemon, with configurable entropy sources. See: https://github.com/smuellerDD/esdm It currently gets used, when a high level of control over entropy sources is desirable, e.g. for VPN appliance solutions. In order to use this source, the ESDM server daemon has to be running and botan must be configured --with-esdm. ESDM currently works only on Linux. Signed-off-by: Markus Theil --- configure.py | 2 +- readme.rst | 2 +- src/cli/cli_rng.cpp | 20 +++++- src/lib/ffi/ffi.h | 2 + src/lib/ffi/ffi_rng.cpp | 11 +++ src/lib/rng/esdm_rng/esdm_rng.cpp | 87 ++++++++++++++++++++++ src/lib/rng/esdm_rng/esdm_rng.h | 111 +++++++++++++++++++++++++++++ src/lib/rng/esdm_rng/info.txt | 18 +++++ src/scripts/ci/setup_gh_actions.sh | 16 ++++- src/scripts/ci_build.py | 17 ++++- src/scripts/config_for_oss_fuzz.py | 2 +- src/scripts/test_cli.py | 7 +- src/scripts/test_python.py | 22 ++++++ 13 files changed, 308 insertions(+), 9 deletions(-) create mode 100644 src/lib/rng/esdm_rng/esdm_rng.cpp create mode 100644 src/lib/rng/esdm_rng/esdm_rng.h create mode 100644 src/lib/rng/esdm_rng/info.txt diff --git a/configure.py b/configure.py index 04a50fc2f1e..ab873b01087 100755 --- a/configure.py +++ b/configure.py @@ -582,7 +582,7 @@ def add_enable_disable_pair(group, what, default, msg=optparse.SUPPRESS_HELP): 'disable building of deprecated features and modules') # Should be derived from info.txt but this runs too early - third_party = ['boost', 'bzip2', 'lzma', 'commoncrypto', 'sqlite3', 'zlib', 'tpm'] + third_party = ['boost', 'bzip2', 'esdm_rng', 'lzma', 'commoncrypto', 'sqlite3', 'zlib', 'tpm'] for mod in third_party: mods_group.add_option('--with-%s' % (mod), diff --git a/readme.rst b/readme.rst index 3ee1fe5af46..bca1ee3eb58 100644 --- a/readme.rst +++ b/readme.rst @@ -127,7 +127,7 @@ Other Useful Things * Full C++ PKCS #11 API wrapper * Interfaces for TPM v1.2 device access * Simple compression API wrapping zlib, bzip2, and lzma libraries -* RNG wrappers for system RNG and hardware RNGs +* RNG wrappers for system RNG, ESDM and hardware RNGs * HMAC_DRBG and entropy collection system for userspace RNGs * SRP-6a password authenticated key exchange * Key derivation functions including HKDF, KDF2, SP 800-108, SP 800-56A, SP 800-56C diff --git a/src/cli/cli_rng.cpp b/src/cli/cli_rng.cpp index af6a00b13a0..4d375025a21 100644 --- a/src/cli/cli_rng.cpp +++ b/src/cli/cli_rng.cpp @@ -14,6 +14,10 @@ #include #endif +#if defined(BOTAN_HAS_ESDM_RNG) + #include +#endif + #if defined(BOTAN_HAS_SYSTEM_RNG) #include #endif @@ -36,6 +40,15 @@ std::shared_ptr cli_make_rng(const std::string& rn } #endif +#if defined(BOTAN_HAS_ESDM_RNG) + if(rng_type == "esdm-full") { + return std::make_shared(false); + } + if(rng_type == "esdm-pr") { + return std::make_shared(true); + } +#endif + const std::vector drbg_seed = Botan::hex_decode(hex_drbg_seed); #if defined(BOTAN_HAS_AUTO_SEEDING_RNG) @@ -89,7 +102,10 @@ std::shared_ptr cli_make_rng(const std::string& rn class RNG final : public Command { public: - RNG() : Command("rng --format=hex --system --rdrand --auto --entropy --drbg --drbg-seed= *bytes") {} + RNG() : + Command( + "rng --format=hex --system --esdm-full --esdm-pr --rdrand --auto --entropy --drbg --drbg-seed= *bytes") { + } std::string group() const override { return "misc"; } @@ -100,7 +116,7 @@ class RNG final : public Command { std::string type = get_arg("rng-type"); if(type.empty()) { - for(std::string flag : {"system", "rdrand", "auto", "entropy", "drbg"}) { + for(std::string flag : {"system", "rdrand", "auto", "entropy", "drbg", "esdm-full", "esdm-pr"}) { if(flag_set(flag)) { type = flag; break; diff --git a/src/lib/ffi/ffi.h b/src/lib/ffi/ffi.h index c0fd79f8b1a..acde50c9f90 100644 --- a/src/lib/ffi/ffi.h +++ b/src/lib/ffi/ffi.h @@ -264,6 +264,8 @@ typedef struct botan_rng_struct* botan_rng_t; * @param rng rng object * @param rng_type type of the rng, possible values: * "system": system RNG +* "esdm-full": ESDM RNG (fully seeded) +* "esdm-pr": ESDM RNG (w. prediction resistance) * "user": userspace RNG * "user-threadsafe": userspace RNG, with internal locking * "rdrand": directly read RDRAND diff --git a/src/lib/ffi/ffi_rng.cpp b/src/lib/ffi/ffi_rng.cpp index 9a23cde3672..46dd166105e 100644 --- a/src/lib/ffi/ffi_rng.cpp +++ b/src/lib/ffi/ffi_rng.cpp @@ -19,6 +19,10 @@ #include #endif +#if defined(BOTAN_HAS_ESDM_RNG) + #include +#endif + extern "C" { using namespace Botan_FFI; @@ -45,6 +49,13 @@ int botan_rng_init(botan_rng_t* rng_out, const char* rng_type) { rng = std::make_unique(); } #endif +#if defined(BOTAN_HAS_ESDM_RNG) + else if(rng_type_s == "esdm-full") { + rng = std::make_unique(false); + } else if(rng_type_s == "esdm-pr") { + rng = std::make_unique(true); + } +#endif if(!rng) { return BOTAN_FFI_ERROR_NOT_IMPLEMENTED; diff --git a/src/lib/rng/esdm_rng/esdm_rng.cpp b/src/lib/rng/esdm_rng/esdm_rng.cpp new file mode 100644 index 00000000000..9f9555e9714 --- /dev/null +++ b/src/lib/rng/esdm_rng/esdm_rng.cpp @@ -0,0 +1,87 @@ +/* +* ESDM RNG +* (C) 2024, Markus Theil +* +* Botan is released under the Simplified BSD License (see license.txt) +*/ + +#include + +#include + +namespace Botan { + +namespace { +/** +* This helper makes sure that the ESDM service is initialized and +* finalized as needed in a threadsafe fashion. Finalization happens +* as soon as all instances of ESDM_RNG are destructed. This may +* happen multiple times in the lifetime of the process. +*/ +class ESDM_Context { + public: + [[nodiscard]] static std::shared_ptr instance() { + static ESDM_Context g_instance; + return g_instance.acquire(); + } + + private: + ESDM_Context() = default; + + [[nodiscard]] std::shared_ptr acquire() { + std::scoped_lock lk(m_mutex); + if(m_refs++ == 0) { + if(esdm_rpcc_init_unpriv_service(nullptr) != 0) { + throw Botan::System_Error("unable to initialize ESDM unprivileged service"); + } + } + return std::shared_ptr{nullptr, [this](void*) { this->release(); }}; + } + + void release() { + std::scoped_lock lk(m_mutex); + if(m_refs-- == 1) { + esdm_rpcc_fini_unpriv_service(); + } + } + + private: + std::mutex m_mutex; + size_t m_refs = 0; +}; +} // namespace + +ESDM_RNG::ESDM_RNG(bool prediction_resistance) : + m_prediction_resistance(prediction_resistance), m_ctx(ESDM_Context::instance()) {} + +void ESDM_RNG::fill_bytes_with_input(std::span out, std::span in) { + if(!in.empty()) { + ssize_t ret = 0; + /* + * take additional input, but do not account entropy for it, + * as this information is not included in the API + */ + esdm_invoke(esdm_rpcc_write_data(in.data(), in.size())); + if(ret != 0) { + throw Botan::System_Error("Writing additional input to ESDM failed"); + } + } + if(!out.empty()) { + ssize_t ret = 0; + if(m_prediction_resistance) { + esdm_invoke(esdm_rpcc_get_random_bytes_pr(out.data(), out.size())); + } else { + esdm_invoke(esdm_rpcc_get_random_bytes_full(out.data(), out.size())); + } + if(ret != static_cast(out.size())) { + throw Botan::System_Error("Fetching random bytes from ESDM failed"); + } + } +} + +RandomNumberGenerator& esdm_rng() { + static ESDM_RNG g_esdm_rng; + return g_esdm_rng; +} + +} // namespace Botan diff --git a/src/lib/rng/esdm_rng/esdm_rng.h b/src/lib/rng/esdm_rng/esdm_rng.h new file mode 100644 index 00000000000..6aeb9265c2d --- /dev/null +++ b/src/lib/rng/esdm_rng/esdm_rng.h @@ -0,0 +1,111 @@ +/* +* ESDM RNG +* (C) 2024, Markus Theil +* +* Botan is released under the Simplified BSD License (see license.txt) +*/ + +#ifndef BOTAN_ESDM_RNG_H_ +#define BOTAN_ESDM_RNG_H_ + +#include +#include + +namespace Botan { + +/** +* Return a shared reference to a global PRNG instance provided by ESDM +*/ +BOTAN_PUBLIC_API(3, 6) RandomNumberGenerator& esdm_rng(); + +/** +* Entropy Source and DRNG Manager (ESDM) is a Linux/Unix based +* PRNG manager, which can be seeded from NTG.1/SP800-90B sources. +* +* See: +* - Repository: https://github.com/smuellerDD/esdm +* - Further Docs: https://www.chronox.de/esdm/index.html +* +* Different entropy sources can be configured in respect of beeing +* active and how much entropy is accounted for each of them. +* +* ESDM tracks its seed and reseed status and blocks, when not fully seeded. +* For this functionality, the esdm_rpcc_get_random_bytes_pr (prediction resistant) or +* esdm_rpcc_get_random_bytes_full (fully seeded) calls have to be used. +* +* Configurable modes: +* - fully seeded (-> fast): provide entropy from a DRBG/PRNG after beeing fully seeded, +* block until this point is reached, reseed from after a time +* and/or invocation limit, block again if reseeding is not possible +* - prediction resistance (-> slow): reseed ESDM with fresh entropy after each invocation +* +* You typically want to use the fast fully seeded mode, which is the default. +* +* Instances of this class communicate over RPC with ESDM. The esdm_rpc_client +* library, provided by ESDM, is leveraged for this. +* +* Thread safety: +* It is fine to construct, destruct and use objects of this class concurrently. +* The communication with ESDM is thread-safe, as handled by esdm_rpc_client. +* The initialization of esdm_rpc_client is not thread safe, therefore this class +* takes care of it, with its embedded ESDM_Context. +*/ +class BOTAN_PUBLIC_API(3, 6) ESDM_RNG final : public Botan::RandomNumberGenerator { + public: + /** + * Default constructor for ESDM, fully seeded mode + */ + ESDM_RNG() : ESDM_RNG(false) {} + + /** + * Construct ESDM instance with configurable mode + */ + explicit ESDM_RNG(bool prediction_resistance); + + std::string name() const override { + if(m_prediction_resistance) { + return "esdm-pr"; + } else { + return "esdm-full"; + } + } + + /** + * ESDM blocks, if it is not seeded, + * + * @return true + */ + bool is_seeded() const override { return true; } + + /** + * ESDM can inject additional inputs + * but we do not account entropy for it + * + * @return true + */ + bool accepts_input() const override { return true; } + + /** + * the ESDM RNG does not hold any state outside ESDM, that should be cleared + * here + */ + void clear() override {} + + protected: + void fill_bytes_with_input(std::span out, std::span in) override; + + private: + /** + * tracks if predicition resistant or fully seeded interface should be queried + */ + bool m_prediction_resistance; + + /** + * takes care of thread-safe esdm_rpc_client initialization + */ + std::shared_ptr m_ctx; +}; + +} // namespace Botan + +#endif /* BOTAN_ESDM_RNG_H_ */ diff --git a/src/lib/rng/esdm_rng/info.txt b/src/lib/rng/esdm_rng/info.txt new file mode 100644 index 00000000000..f32a936ccc0 --- /dev/null +++ b/src/lib/rng/esdm_rng/info.txt @@ -0,0 +1,18 @@ + +ESDM_RNG -> 20240814 + + + +name -> "ESDM RNG" +brief -> "RNG based on ESDM - Entropy Source and DRNG Manager" + + +load_on vendor + + +esdm_rng.h + + + +linux -> esdm_rpc_client + \ No newline at end of file diff --git a/src/scripts/ci/setup_gh_actions.sh b/src/scripts/ci/setup_gh_actions.sh index 6c221f529d6..d09b68a49e2 100755 --- a/src/scripts/ci/setup_gh_actions.sh +++ b/src/scripts/ci/setup_gh_actions.sh @@ -34,7 +34,7 @@ if type -p "apt-get"; then sudo NEEDRESTART_MODE=l apt-get -qq install valgrind elif [ "$TARGET" = "shared" ] || [ "$TARGET" = "examples" ] || [ "$TARGET" = "tlsanvil" ] || [ "$TARGET" = "clang-tidy" ] ; then - sudo apt-get -qq install libboost-dev + sudo apt-get -qq install libboost-dev elif [ "$TARGET" = "clang" ]; then sudo apt-get -qq install clang @@ -133,6 +133,20 @@ if type -p "apt-get"; then sudo apt-get -qq install doxygen python3-docutils python3-sphinx fi + + # ESDM for shared, coverage and sanitizer target + if [ "$TARGET" = "shared" ] || [ "$TARGET" = "coverage" ] || [ "$TARGET" = "sanitizer" ]; then + sudo apt-get -qq install libprotobuf-c-dev meson + + # install ESDM 1.2.0 + wget -O esdm.tar.gz https://github.com/smuellerDD/esdm/archive/refs/tags/v1.2.0.tar.gz + tar xvfz esdm.tar.gz + pushd esdm-* + meson setup build -Dselinux=disabled -Dais2031=false -Dlinux-devfiles=disabled -Des_jent=disabled --prefix=/usr --libdir=lib + meson compile -C build + sudo meson install -C build + popd + fi else export HOMEBREW_NO_AUTO_UPDATE=1 export HOMEBREW_NO_INSTALLED_DEPENDENTS_CHECK=1 diff --git a/src/scripts/ci_build.py b/src/scripts/ci_build.py index cf4f90406db..9d2ad557323 100755 --- a/src/scripts/ci_build.py +++ b/src/scripts/ci_build.py @@ -205,6 +205,9 @@ def sanitize_kv(some_string): # Workaround for https://github.com/actions/runner-images/issues/10004 flags += ['--extra-cxxflags=/D_DISABLE_CONSTEXPR_MUTEX_CONSTRUCTOR'] + if target_os == 'linux' and target in ['shared', 'coverage', 'sanitizer']: + flags += ['--with-esdm_rng'] + if target in ['minimized']: flags += ['--minimized-build', '--enable-modules=system_rng,sha2_32,sha2_64,aes'] @@ -714,8 +717,6 @@ def main(args=None): py_scripts = [ 'configure.py', - 'src/python/botan3.py', - 'src/scripts/ci_build.py', 'src/scripts/install.py', 'src/scripts/ci_check_headers.py', 'src/scripts/ci_check_install.py', @@ -878,12 +879,24 @@ def main(args=None): cmds.append(make_cmd + ['clean']) cmds.append(make_cmd + ['distclean']) + # start ESDM in background, if on Linux + if target in ['shared', 'coverage', 'sanitizer'] and platform.system() == "Linux": + print('Starting esdm-server for this target') + esdm_process = subprocess.Popen(f'sudo /usr/bin/esdm-server -f', shell=True) + assert esdm_process.poll() is None, f"esdm-server did not start for target {target}" + else: + print('Not starting esdm-server for this target') + for cmd in cmds: if options.dry_run: print('$ ' + ' '.join(cmd)) else: run_cmd(cmd, root_dir, build_dir) + if target in ['shared', 'coverage', 'sanitizer'] and platform.system() == "Linux": + print('Stopping esdm-server') + esdm_process.kill() + return 0 if __name__ == '__main__': diff --git a/src/scripts/config_for_oss_fuzz.py b/src/scripts/config_for_oss_fuzz.py index e9967a166df..3c574dd3991 100755 --- a/src/scripts/config_for_oss_fuzz.py +++ b/src/scripts/config_for_oss_fuzz.py @@ -29,7 +29,7 @@ "--build-fuzzers=libfuzzer", "--build-targets=static", "--without-os-features=getrandom,getentropy", - "--disable-modules=system_rng,processor_rng", + "--disable-modules=system_rng,processor_rng,esdm_rng", "--with-fuzzer-lib=FuzzingEngine", ] diff --git a/src/scripts/test_cli.py b/src/scripts/test_cli.py index db9fffe1408..ca641c63738 100755 --- a/src/scripts/test_cli.py +++ b/src/scripts/test_cli.py @@ -705,7 +705,12 @@ def cli_rng_tests(_tmp_dir): hex_10 = re.compile('[A-F0-9]{20}') - for rng in ['system', 'auto', 'entropy']: + rngs = ['system', 'auto', 'entropy'] + # execute ESDM tests only on Linux + if platform.system() == "Linux": + rngs += ['esdm-full', 'esdm-pr'] + + for rng in rngs: output = test_cli("rng", ["10", '--%s' % (rng)], use_drbg=False) if output == "D80F88F6ADBE65ACB10C": logging.error('RNG produced DRBG output') diff --git a/src/scripts/test_python.py b/src/scripts/test_python.py index e13155051ec..07679666148 100644 --- a/src/scripts/test_python.py +++ b/src/scripts/test_python.py @@ -176,6 +176,28 @@ def test_rng(self): user_rng.add_entropy('seed material...') + # execute ESDM tests only on Linux + if platform.system() != "Linux": + return + + esdm_rng = botan.RandomNumberGenerator("esdm-full") + + output1 = esdm_rng.get(32) + output2 = esdm_rng.get(32) + + self.assertEqual(len(output1), 32) + self.assertEqual(len(output2), 32) + self.assertNotEqual(output1, output2) + + esdm_rng = botan.RandomNumberGenerator("esdm-pr") + + output1 = esdm_rng.get(32) + output2 = esdm_rng.get(32) + + self.assertEqual(len(output1), 32) + self.assertEqual(len(output2), 32) + self.assertNotEqual(output1, output2) + def test_hash(self): try: