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: