From c5556bd37b1030f7f65d783b40daf95adf2d624d Mon Sep 17 00:00:00 2001 From: Kamil Holubicki Date: Wed, 23 Sep 2026 13:43:40 +0200 Subject: [PATCH] PBS-49 feature: move SHA-256 digest into opensslpp::digest_context https://perconadev.atlassian.net/browse/PBS-49 Problem: The caching_sha2_password authenticator was the only translation unit in the PBS tree that still included OpenSSL headers directly and drove EVP_MD_CTX by hand. Every other cryptographic primitive already lives in the shared opensslpp module (cipher_context, crypto_rng, core_error). PBS-49 tracks pulling the remaining raw calls behind that same wrapper. Solution: Introduce opensslpp::digest_context, a minimal RAII wrapper around EVP_MD_CTX that exposes exactly the operations the plugin needs: select a digest by digest_code_type (only sha256 today), update() with a std::string_view of input, and finalize() to a std::string of raw digest bytes. A static one-shot calculate() mirrors PS opensslpp's free-standing calculate_digest. Co-Authored-By: Claude Opus 4.7 --- CMakeLists.txt | 4 + .../caching_sha2_password_authenticator.cpp | 108 ++---------------- src/opensslpp/digest_context.cpp | 97 ++++++++++++++++ src/opensslpp/digest_context.hpp | 57 +++++++++ src/opensslpp/digest_context_fwd.hpp | 25 ++++ tests/CMakeLists.txt | 14 +++ tests/digest_context_test.cpp | 95 +++++++++++++++ 7 files changed, 299 insertions(+), 101 deletions(-) create mode 100644 src/opensslpp/digest_context.cpp create mode 100644 src/opensslpp/digest_context.hpp create mode 100644 src/opensslpp/digest_context_fwd.hpp create mode 100644 tests/digest_context_test.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index a3fb6ed..1fe201a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -179,6 +179,10 @@ set(opensslpp_source_files src/opensslpp/crypto_rng.hpp src/opensslpp/crypto_rng.cpp + src/opensslpp/digest_context_fwd.hpp + src/opensslpp/digest_context.hpp + src/opensslpp/digest_context.cpp + src/opensslpp/core_error_fwd.hpp src/opensslpp/core_error.hpp src/opensslpp/core_error.cpp diff --git a/src/minimysql/caching_sha2_password_authenticator.cpp b/src/minimysql/caching_sha2_password_authenticator.cpp index bc7ae81..d858c80 100644 --- a/src/minimysql/caching_sha2_password_authenticator.cpp +++ b/src/minimysql/caching_sha2_password_authenticator.cpp @@ -17,108 +17,13 @@ #include #include -#include #include #include -#include -#include +#include #include #include -#include -#include - -namespace { - -enum class digest_code_type : std::uint8_t { - sha256, -}; - -class digest_context { -public: - // no std::string_view for 'type' as we need it to be nul-terminated - explicit digest_context(digest_code_type digest_code) - : impl_{EVP_MD_CTX_new(), digest_context_deleter{}} { - if (!impl_) { - throw std::runtime_error{"failed to create digest context"}; - } - if (EVP_DigestInit_ex(impl_.get(), get_md_by_digest_code(digest_code), - nullptr) == 0) { - throw std::runtime_error{"failed to initialize digest context"}; - } - } - - ~digest_context() noexcept = default; - - digest_context(const digest_context &obj) = delete; - digest_context(digest_context &&obj) noexcept = delete; - - digest_context &operator=(const digest_context &obj) = delete; - digest_context &operator=(digest_context &&obj) noexcept = delete; - - [[nodiscard]] std::size_t get_size_in_bytes() const noexcept { - assert(impl_); - auto native_result{EVP_MD_CTX_size(impl_.get())}; - assert(native_result != -1); - return static_cast(native_result); - } - - void update(std::string_view data) { - assert(impl_); - if (EVP_DigestUpdate(impl_.get(), std::data(data), std::size(data)) == 0) { - throw std::runtime_error{"failed to update digest context"}; - } - } - std::string finalize() { - assert(impl_); - std::string result(get_size_in_bytes(), '\0'); - - unsigned int result_size = 0; - if (EVP_DigestFinal_ex( - impl_.get(), - // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast) - reinterpret_cast(std::data(result)), - &result_size) == 0) { - throw std::runtime_error{"cannot finalize digest context"}; - } - assert(result_size == std::size(result)); - - impl_.reset(); - return result; - } - -private: - struct digest_context_deleter { - void operator()(EVP_MD_CTX *digest_context) const noexcept { - // null-ness is handled by EVP_MD_CTX_free - EVP_MD_CTX_free(digest_context); - } - }; - - using impl_ptr = std::unique_ptr; - impl_ptr impl_; - - [[nodiscard]] static const EVP_MD * - get_md_by_digest_code(digest_code_type digest_code) noexcept { - switch (digest_code) { - case digest_code_type::sha256: - return EVP_sha256(); - default: - // should never happen as we only construct digest_context with supported - // digest_code_type - return nullptr; - } - } -}; - -std::string calculate_digest(digest_code_type digest_code, - std::string_view data) { - digest_context ctx(digest_code); - ctx.update(data); - return ctx.finalize(); -} - -} // anonymous namespace +#include "opensslpp/digest_context.hpp" namespace minimysql { @@ -132,16 +37,17 @@ std::string caching_sha2_password_authenticator::scramble( // server, provided that it knows original password and server_auth_data // (salt), can verify client_auth_data by calculating the same way and // comparing the result with client_auth_data - const auto digest_code{digest_code_type::sha256}; + const std::string digest_name{"SHA256"}; // calculating hashed password - auto result{calculate_digest(digest_code, password)}; + auto result{opensslpp::digest_context::calculate(digest_name, password)}; // calculating double-hashed password - const auto double_hashed_password{calculate_digest(digest_code, result)}; + const auto double_hashed_password{ + opensslpp::digest_context::calculate(digest_name, result)}; // calculating salted triple-hashed password - digest_context ctx(digest_code); + opensslpp::digest_context ctx{digest_name}; ctx.update(double_hashed_password); ctx.update(salt); const auto salted_triple_hashed_password{ctx.finalize()}; diff --git a/src/opensslpp/digest_context.cpp b/src/opensslpp/digest_context.cpp new file mode 100644 index 0000000..8f5d7b5 --- /dev/null +++ b/src/opensslpp/digest_context.cpp @@ -0,0 +1,97 @@ +// Copyright (c) 2023-2026 Percona and/or its affiliates. +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License, version 2.0, +// as published by the Free Software Foundation. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License, version 2.0, for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +#include "opensslpp/digest_context.hpp" + +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include "opensslpp/core_error.hpp" + +#include "util/exception_location_helpers.hpp" + +namespace opensslpp { + +void digest_context::impl_deleter::operator()(void *digest_ctx) const noexcept { + if (digest_ctx != nullptr) { + EVP_MD_CTX_free(static_cast(digest_ctx)); + } +} + +digest_context::digest_context(const std::string &digest_name) { + const auto *digest{EVP_get_digestbyname(digest_name.c_str())}; + if (digest == nullptr) { + util::exception_location().raise("unknown digest name"); + } + + std::unique_ptr new_impl{EVP_MD_CTX_new(), + impl_deleter{}}; + if (!new_impl) { + util::exception_location().raise( + "cannot allocate digest context"); + } + if (EVP_DigestInit_ex(static_cast(new_impl.get()), digest, + nullptr) == 0) { + util::exception_location().raise( + "cannot initialize digest context"); + } + impl_ = std::move(new_impl); +} + +void digest_context::update(std::string_view data) { + assert(impl_); + if (std::empty(data)) { + return; + } + if (EVP_DigestUpdate(static_cast(impl_.get()), std::data(data), + std::size(data)) == 0) { + util::exception_location().raise( + "cannot update digest context"); + } +} + +std::string digest_context::finalize() { + assert(impl_); + auto *ctx{static_cast(impl_.get())}; + std::string result(static_cast(EVP_MD_CTX_size(ctx)), '\0'); + unsigned int size{0U}; + if (EVP_DigestFinal_ex( + ctx, + // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast) + reinterpret_cast(std::data(result)), &size) == 0) { + util::exception_location().raise( + "cannot finalize digest context"); + } + assert(static_cast(size) == std::size(result)); + impl_.reset(); + return result; +} + +std::string digest_context::calculate(const std::string &digest_name, + std::string_view data) { + digest_context ctx{digest_name}; + ctx.update(data); + return ctx.finalize(); +} + +} // namespace opensslpp diff --git a/src/opensslpp/digest_context.hpp b/src/opensslpp/digest_context.hpp new file mode 100644 index 0000000..6b10492 --- /dev/null +++ b/src/opensslpp/digest_context.hpp @@ -0,0 +1,57 @@ +// Copyright (c) 2023-2026 Percona and/or its affiliates. +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License, version 2.0, +// as published by the Free Software Foundation. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License, version 2.0, for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +#ifndef OPENSSLPP_DIGEST_CONTEXT_HPP +#define OPENSSLPP_DIGEST_CONTEXT_HPP + +#include "opensslpp/digest_context_fwd.hpp" // IWYU pragma: export + +#include +#include +#include + +namespace opensslpp { + +class digest_context { +public: + digest_context() noexcept = default; + // 'digest_name' must be a valid digest name supported by OpenSSL + // (e.g. "SHA256") + explicit digest_context(const std::string &digest_name); + ~digest_context() noexcept = default; + + digest_context(const digest_context &) = delete; + digest_context(digest_context &&) noexcept = default; + digest_context &operator=(const digest_context &) = delete; + digest_context &operator=(digest_context &&) noexcept = default; + + [[nodiscard]] bool is_empty() const noexcept { return !impl_; } + + void update(std::string_view data); + [[nodiscard]] std::string finalize(); + + [[nodiscard]] static std::string calculate(const std::string &digest_name, + std::string_view data); + +private: + struct impl_deleter { + void operator()(void *digest_ctx) const noexcept; + }; + std::unique_ptr impl_; +}; + +} // namespace opensslpp + +#endif // OPENSSLPP_DIGEST_CONTEXT_HPP diff --git a/src/opensslpp/digest_context_fwd.hpp b/src/opensslpp/digest_context_fwd.hpp new file mode 100644 index 0000000..bf2e81d --- /dev/null +++ b/src/opensslpp/digest_context_fwd.hpp @@ -0,0 +1,25 @@ +// Copyright (c) 2023-2026 Percona and/or its affiliates. +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License, version 2.0, +// as published by the Free Software Foundation. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License, version 2.0, for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +#ifndef OPENSSLPP_DIGEST_CONTEXT_FWD_HPP +#define OPENSSLPP_DIGEST_CONTEXT_FWD_HPP + +namespace opensslpp { + +class digest_context; + +} // namespace opensslpp + +#endif // OPENSSLPP_DIGEST_CONTEXT_FWD_HPP diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 10b3ca6..fb4d85c 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -121,6 +121,19 @@ set_target_properties(crypto_rnd_test PROPERTIES CXX_EXTENSIONS NO ) +add_executable(digest_context_test digest_context_test.cpp) +target_include_directories(digest_context_test PRIVATE "${PROJECT_SOURCE_DIR}/src") +target_link_libraries(digest_context_test + PRIVATE + binlog_server_compiler_flags + binsrv::lib_opensslpp + Boost::unit_test_framework +) +set_target_properties(digest_context_test PROPERTIES + CXX_STANDARD_REQUIRED YES + CXX_EXTENSIONS NO +) + set(test_run_options --no_color_output) add_test(NAME byte_span_encoding_test COMMAND byte_span_encoding_test ${test_run_options}) @@ -131,3 +144,4 @@ add_test(NAME gtid_set_test COMMAND gtid_set_test ${test_run_options}) add_test(NAME event_test COMMAND event_test ${test_run_options}) add_test(NAME cipher_context_test COMMAND cipher_context_test ${test_run_options}) add_test(NAME crypto_rnd_test COMMAND crypto_rnd_test ${test_run_options}) +add_test(NAME digest_context_test COMMAND digest_context_test ${test_run_options}) diff --git a/tests/digest_context_test.cpp b/tests/digest_context_test.cpp new file mode 100644 index 0000000..a464541 --- /dev/null +++ b/tests/digest_context_test.cpp @@ -0,0 +1,95 @@ +// Copyright (c) 2023-2026 Percona and/or its affiliates. +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License, version 2.0, +// as published by the Free Software Foundation. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License, version 2.0, for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +#include +#include +#include +#include +#include +#include + +#define BOOST_TEST_MODULE DigestContextTests +// this include is needed as it provides the 'main()' function +// NOLINTNEXTLINE(misc-include-cleaner) +#include + +#include + +#include + +#include "opensslpp/core_error.hpp" +#include "opensslpp/digest_context.hpp" + +namespace { + +// FIPS 180-4 short-message sample: SHA-256("abc") +constexpr std::string_view sha256_abc_hex{ + "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad"}; + +// FIPS 180-4 long-message sample input and its SHA-256 digest: +constexpr std::string_view long_input{ + "abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq"}; +constexpr std::string_view sha256_long_hex{ + "248d6a61d20638b8e5c026930c3e6039a33ce45964ff2167f6ecedd419db06c1"}; + +std::string to_hex(std::string_view raw) { + std::ostringstream oss; + oss << std::hex << std::setfill('0'); + for (const auto raw_char : raw) { + oss << std::setw(2) + << static_cast(static_cast(raw_char)); + } + return oss.str(); +} + +} // anonymous namespace + +BOOST_AUTO_TEST_CASE(DigestContextDefaultIsEmpty) { + const opensslpp::digest_context ctx{}; + BOOST_CHECK(ctx.is_empty()); +} + +BOOST_AUTO_TEST_CASE(DigestContextCalculateKnownVector) { + const auto digest{ + opensslpp::digest_context::calculate(std::string{"SHA256"}, "abc")}; + BOOST_CHECK_EQUAL(to_hex(digest), std::string{sha256_abc_hex}); +} + +BOOST_AUTO_TEST_CASE(DigestContextStreamedMatchesOneShot) { + constexpr std::size_t first_chunk_size{10U}; + constexpr std::size_t second_chunk_size{20U}; + + opensslpp::digest_context ctx{std::string{"SHA256"}}; + ctx.update(long_input.substr(0U, first_chunk_size)); + ctx.update(long_input.substr(first_chunk_size, second_chunk_size)); + ctx.update(long_input.substr(first_chunk_size + second_chunk_size)); + const auto streamed{ctx.finalize()}; + BOOST_CHECK_EQUAL(to_hex(streamed), std::string{sha256_long_hex}); + + const auto one_shot{ + opensslpp::digest_context::calculate(std::string{"SHA256"}, long_input)}; + BOOST_CHECK(streamed == one_shot); +} + +BOOST_AUTO_TEST_CASE(DigestContextFinalizeConsumesContext) { + opensslpp::digest_context ctx{std::string{"SHA256"}}; + static_cast(ctx.finalize()); + BOOST_CHECK(ctx.is_empty()); +} + +BOOST_AUTO_TEST_CASE(DigestContextInvalidNameThrows) { + BOOST_CHECK_THROW(opensslpp::digest_context{std::string{"INVALID-DIGEST"}}, + opensslpp::core_error); +}