diff --git a/src/v/crypto/CMakeLists.txt b/src/v/crypto/CMakeLists.txt index 424d686c5a58e..1b3ac76817563 100644 --- a/src/v/crypto/CMakeLists.txt +++ b/src/v/crypto/CMakeLists.txt @@ -5,6 +5,7 @@ v_cc_library( SRCS crypto.cc digest.cc + hmac.cc ssl_utils.cc DEPS absl::node_hash_set diff --git a/src/v/crypto/README.md b/src/v/crypto/README.md index 4268d9202f9c2..e652805595724 100644 --- a/src/v/crypto/README.md +++ b/src/v/crypto/README.md @@ -41,3 +41,31 @@ crypto::digest_ctx ctx(crypto::digest_type::SHA256); ctx.update(msg); auto digest = std::move(ctx).final(); ``` + +## HMAC + +Like digests, this library can perform HMAC two ways: one-shot and multi-part. + +For one-shot: + +```c++ +#include "crypto/crypto.h" + +bytes key = {...}; +bytes msg = {...}; + +auto sig = crypto::hmac(crypto::digest_type::SHA256, key, msg); +``` + +For multi-part: + +```c++ +#include "crypto/crypto.h" + +bytes key = {...}; +bytes msg = {...}; + +crypto::hmac_ctx ctx(crypto::digest_type::SHA256, key); +ctx.update(msg); +auto sig = std::move(ctx).final(); +``` diff --git a/src/v/crypto/hmac.cc b/src/v/crypto/hmac.cc new file mode 100644 index 0000000000000..f1496f240fc25 --- /dev/null +++ b/src/v/crypto/hmac.cc @@ -0,0 +1,174 @@ +/* + * Copyright 2024 Redpanda Data, Inc. + * + * Use of this software is governed by the Business Source License + * included in the file licenses/BSL.md + * + * As of the Change Date specified in that file, in accordance with + * the Business Source License, use of this software will be governed + * by the Apache License, Version 2.0 + */ + +#include "crypto/crypto.h" +#include "internal.h" +#include "ssl_utils.h" + +#include +#include + +namespace crypto { +hmac_ctx::~hmac_ctx() noexcept = default; +hmac_ctx::hmac_ctx(hmac_ctx&&) noexcept = default; +hmac_ctx& hmac_ctx::operator=(hmac_ctx&&) noexcept = default; + +static_assert( + std::is_nothrow_move_constructible_v, + "hmac_ctx should be nothrow move constructible"); +static_assert( + std::is_nothrow_move_assignable_v, + "hmac_ctx should be nothrow move assignable"); + +class hmac_ctx::impl { +public: + impl(digest_type type, bytes_view key) { + std::array params{ + OSSL_PARAM_construct_utf8_string( + "digest", + // NOLINTNEXTLINE + const_cast(get_digest_str(type)), + 0), + OSSL_PARAM_construct_end()}; + _mac = internal::EVP_MAC_ptr(EVP_MAC_fetch(nullptr, "HMAC", nullptr)); + if (!_mac) { + throw internal::ossl_error( + "Failed to fetch HMAC for MAC operation"); + } + + _mac_ctx = internal::EVP_MAC_CTX_ptr(EVP_MAC_CTX_new(_mac.get())); + if ( + 1 + != EVP_MAC_init( + _mac_ctx.get(), key.data(), key.size(), params.data())) { + throw internal::ossl_error( + fmt::format("Failed to initialize HMAC-{}", type)); + } + } + + void update(bytes_view msg) { + if (1 != EVP_MAC_update(_mac_ctx.get(), msg.data(), msg.size())) { + throw internal::ossl_error("Failed to update MAC operation"); + } + } + + bytes finish() { + bytes sig(bytes::initialized_later(), size()); + finish_no_check(sig); + return sig; + } + + bytes_span<> finish(bytes_span<> sig) { + auto len = sig.size(); + if (len != size()) { + throw exception(fmt::format( + "Invalid signature buffer length: {} != {}", len, size())); + } + return finish_no_check(sig); + } + + bytes reset() { + bytes sig(bytes::initialized_later(), size()); + reset(sig); + return sig; + } + + bytes_span<> reset(bytes_span<> sig) { + auto len = sig.size(); + if (len != size()) { + throw exception(fmt::format( + "Invalid signature buffer length: {} != {}", len, size())); + } + finish_no_check(sig); + if (1 != EVP_MAC_init(_mac_ctx.get(), nullptr, 0, nullptr)) { + throw internal::ossl_error("Failed to re-initialize HMAC"); + } + + return sig; + } + + size_t size() const { return EVP_MAC_CTX_get_mac_size(_mac_ctx.get()); } + static size_t size(digest_type type) { return digest_ctx::size(type); } + +private: + internal::EVP_MAC_ptr _mac; + internal::EVP_MAC_CTX_ptr _mac_ctx; + + static const char* get_digest_str(digest_type type) { + switch (type) { + case digest_type::MD5: + return "MD5"; + case digest_type::SHA256: + return "SHA256"; + case digest_type::SHA512: + return "SHA512"; + } + } + + bytes_span<> finish_no_check(bytes_span<> sig) { + size_t outl = sig.size(); + if (1 != EVP_MAC_final(_mac_ctx.get(), sig.data(), &outl, sig.size())) { + throw internal::ossl_error("Failed to finalize MAC operation"); + } + return sig; + } +}; + +hmac_ctx::hmac_ctx(digest_type type, bytes_view key) + : _impl(std::make_unique(type, key)) {} + +hmac_ctx::hmac_ctx(digest_type type, std::string_view key) + : _impl( + std::make_unique(type, internal::string_view_to_bytes_view(key))) {} + +size_t hmac_ctx::size() const { return _impl->size(); } +size_t hmac_ctx::size(digest_type type) { return impl::size(type); } + +hmac_ctx& hmac_ctx::update(bytes_view msg) { + _impl->update(msg); + return *this; +} + +hmac_ctx& hmac_ctx::update(std::string_view msg) { + return update(internal::string_view_to_bytes_view(msg)); +} + +bytes hmac_ctx::final() && { return _impl->finish(); } +bytes_span<> hmac_ctx::final(bytes_span<> signature) && { + return _impl->finish(signature); +} + +std::span hmac_ctx::final(std::span signature) && { + _impl->finish(internal::char_span_to_bytes_span(signature)); + return signature; +} + +bytes hmac_ctx::reset() { return _impl->reset(); }; +bytes_span<> hmac_ctx::reset(bytes_span<> sig) { return _impl->reset(sig); } +std::span hmac_ctx::reset(std::span sig) { + _impl->reset(internal::char_span_to_bytes_span(sig)); + return sig; +} + +// NOLINTNEXTLINE +bytes hmac(digest_type type, bytes_view key, bytes_view msg) { + hmac_ctx ctx(type, key); + ctx.update(msg); + return std::move(ctx).final(); +} + +// NOLINTNEXTLINE +bytes hmac(digest_type type, std::string_view key, std::string_view msg) { + hmac_ctx ctx(type, key); + ctx.update(msg); + return std::move(ctx).final(); +} +} // namespace crypto diff --git a/src/v/crypto/include/crypto/crypto.h b/src/v/crypto/include/crypto/crypto.h index ada8529aa71d2..4d876656af023 100644 --- a/src/v/crypto/include/crypto/crypto.h +++ b/src/v/crypto/include/crypto/crypto.h @@ -109,4 +109,103 @@ struct digest_ctx final { */ bytes digest(digest_type type, bytes_view msg); bytes digest(digest_type type, std::string_view msg); + +/////////////////////////////////////////////////////////////////////////////// +/// MAC operations +/////////////////////////////////////////////////////////////////////////////// + +/** + * Context structure used to perform HMAC operations + */ +struct hmac_ctx final { +public: + /** + * Construct a new hmac ctx object + * + * @param type The type of HMAC to generate + * @param key The key to use + * @throws crypto::exception On internal error + */ + hmac_ctx(digest_type type, bytes_view key); + hmac_ctx(digest_type type, std::string_view key); + ~hmac_ctx() noexcept; + hmac_ctx(const hmac_ctx&) = delete; + hmac_ctx& operator=(const hmac_ctx&) = delete; + hmac_ctx(hmac_ctx&&) noexcept; + hmac_ctx& operator=(hmac_ctx&&) noexcept; + + /** + * @return size_t Size of the resulting HMAC signature + */ + size_t size() const; + static size_t size(digest_type type); + + /** + * Updates HMAC operation + * + * @return hmac_ctx& Itself for update chaining + * @throws crypto::exception On internal error + */ + hmac_ctx& update(bytes_view msg); + hmac_ctx& update(std::string_view msg); + + /** + * Finalizes HMAC operation and returns signature + * + * @return bytes The signature + * @throws crypto::exception On internal error + */ + bytes final() &&; + + /** + * Finalizes digest operation and returns the signature in the provided + * buffer + * + * @param signature The buffer to place the signature into. It must be + * exactly the size of the signature + * @return The provided buffer + * @throws crypto::exception On internal error or if @p signature is an + * invalid size + */ + bytes_span<> final(bytes_span<> signature) &&; + std::span final(std::span signature) &&; + + /** + * Finalizes HMAC operation and returns signature and resets context so it + * can be used again + * + * @return bytes The signature + * @throws crypto::exception On internal error + */ + bytes reset(); + + /** + * Finalizes digest operation and returns the signature in the provided + * buffer and resets the context so it can be used again + * + * @param signature The buffer to place the signature into. It must be + * exactly the size of the signature + * @return The provided buffer + * @throws crypto::exception On internal error or if @p signature is an + * invalid size + */ + bytes_span<> reset(bytes_span<> signature); + std::span reset(std::span signature); + +private: + class impl; + std::unique_ptr _impl; +}; + +/** + * Performs one-shot digest operation + * + * @param type The type of digest to create + * @param key The key to use + * @param msg The message + * @return bytes The signature + * @throws crypto::exception On internal error + */ +bytes hmac(digest_type type, bytes_view key, bytes_view msg); +bytes hmac(digest_type type, std::string_view key, std::string_view msg); } // namespace crypto diff --git a/src/v/crypto/ssl_utils.h b/src/v/crypto/ssl_utils.h index 593992a2cb46d..c338ec576ff0b 100644 --- a/src/v/crypto/ssl_utils.h +++ b/src/v/crypto/ssl_utils.h @@ -43,6 +43,8 @@ struct deleter { template using handle = std::unique_ptr>; +using EVP_MAC_ptr = handle; +using EVP_MAC_CTX_ptr = handle; using EVP_MD_CTX_ptr = handle; /// Exception class used to extract the error from OpenSSL diff --git a/src/v/crypto/test/CMakeLists.txt b/src/v/crypto/test/CMakeLists.txt index 3b2205723ab59..8e751fa0649b0 100644 --- a/src/v/crypto/test/CMakeLists.txt +++ b/src/v/crypto/test/CMakeLists.txt @@ -4,6 +4,7 @@ rp_test( BINARY_NAME gtest_crypto SOURCES digest_tests.cc + hmac_tests.cc LIBRARIES v::crypto v::gtest_main diff --git a/src/v/crypto/test/hmac_tests.cc b/src/v/crypto/test/hmac_tests.cc new file mode 100644 index 0000000000000..8ccbee26b69bd --- /dev/null +++ b/src/v/crypto/test/hmac_tests.cc @@ -0,0 +1,115 @@ +/* + * Copyright 2024 Redpanda Data, Inc. + * + * Use of this software is governed by the Business Source License + * included in the file licenses/BSL.md + * + * As of the Change Date specified in that file, in accordance with + * the Business Source License, use of this software will be governed + * by the Apache License, Version 2.0 + */ + +#include "crypto/crypto.h" +#include "crypto/types.h" +#include "crypto_test_utils.h" +#include "test_utils/test.h" + +#include +#include + +// NOLINTBEGIN +const auto hmac_sha256_key = convert_from_hex( + "9779d9120642797f1747025d5b22b7ac607cab08e1758f2f3a46c8be1e25c53b8c6a8f58ffef" + "a176"); + +const auto hmac_sha256_msg = convert_from_hex( + "b1689c2591eaf3c9e66070f8a77954ffb81749f1b00346f9dfe0b2ee905dcc288baf4a92de3f" + "4001dd9f44c468c3d07d6c6ee82faceafc97c2fc0fc0601719d2dcd0aa2aec92d1b0ae933c65" + "eb06a03c9c935c2bad0459810241347ab87e9f11adb30415424c6c7f5f22a003b8ab8de54f6d" + "ed0e3ab9245fa79568451dfa258e"); + +const auto hmac_sha256_expected = convert_from_hex( + "769f00d3e6a6cc1fb426a14a4f76c6462e6149726e0dee0ec0cf97a16605ac8b"); + +const auto hmac_sha512_key = convert_from_hex( + "57c2eb677b5093b9e829ea4babb50bde55d0ad59fec34a618973802b2ad9b78e26b2045dda78" + "4df3ff90ae0f2cc51ce39cf54867320ac6f3ba2c6f0d72360480c96614ae66581f266c35fb79" + "fd28774afd113fa5187eff9206d7cbe90dd8bf67c844e202"); + +const auto hmac_sha512_msg = convert_from_hex( + "2423dff48b312be864cb3490641f793d2b9fb68a7763b8e298c86f42245e4540eb01ae4d2d45" + "00370b1886f23ca2cf9701704cad5bd21ba87b811daf7a854ea24a56565ced425b35e40e1acb" + "ebe03603e35dcf4a100e57218408a1d8dbcc3b99296cfea931efe3ebd8f719a6d9a15487b9ad" + "67eafedf15559ca42445b0f9b42e"); + +const auto hmac_sha512_expected = convert_from_hex( + "33c511e9bc2307c62758df61125a980ee64cefebd90931cb91c13742d4714c06de4003faf3c4" + "1c06aefc638ad47b21906e6b104816b72de6269e045a1f4429d4"); + +static const absl::flat_hash_map expected_sizes = { + {crypto::digest_type::SHA256, 32}, {crypto::digest_type::SHA512, 64}}; +// NOLINTEND + +TEST(crypto_hmac, length) { + // NOLINTNEXTLINE + bytes fake_key('k', 16); + for (auto&& [k, v] : expected_sizes) { + ASSERT_EQ(crypto::hmac_ctx::size(k), v) + << "Mismatch on HMAC size for digest type " << k; + crypto::hmac_ctx ctx(k, fake_key); + ASSERT_EQ(ctx.size(), v) + << "Mismatch on HMAC size for context using digest type " << k; + } +} + +TEST(crypto_hmac, hmac_sha256_one_shot) { + ASSERT_EQ( + crypto::hmac( + crypto::digest_type::SHA256, hmac_sha256_key, hmac_sha256_msg), + hmac_sha256_expected); +} + +TEST(crypto_hmac, hmac_sha512_one_shot) { + ASSERT_EQ( + crypto::hmac( + crypto::digest_type::SHA512, + bytes_view_to_string_view(hmac_sha512_key), + bytes_view_to_string_view(hmac_sha512_msg)), + hmac_sha512_expected); +} + +TEST(crypto_hmac, hmac_sha256_multipart_bytes) { + crypto::hmac_ctx ctx(crypto::digest_type::SHA256, hmac_sha256_key); + ctx.update(hmac_sha256_msg); + bytes sig(bytes::initialized_later{}, ctx.size()); + ASSERT_NO_THROW(std::move(ctx).final(sig)); + ASSERT_EQ(sig, hmac_sha256_expected); +} + +TEST(crypto_hmac, hmac_sha256_multipart_string) { + crypto::hmac_ctx ctx( + crypto::digest_type::SHA256, bytes_view_to_string_view(hmac_sha256_key)); + ctx.update(bytes_view_to_string_view(hmac_sha256_msg)); + bytes sig(bytes::initialized_later{}, ctx.size()); + ASSERT_NO_THROW(std::move(ctx).final(bytes_span_to_char_span(sig))); + ASSERT_EQ(sig, hmac_sha256_expected); +} + +TEST(crypto_hmac, hmac_sha256_multipart_bad_len) { + crypto::hmac_ctx ctx(crypto::digest_type::SHA256, hmac_sha256_key); + ctx.update(hmac_sha256_msg); + bytes sig(bytes::initialized_later{}, ctx.size() - 1); + ASSERT_THROW(std::move(ctx).final(sig), crypto::exception); +} + +TEST(crypto_hmac, hmac_sha256_reset) { + crypto::hmac_ctx ctx(crypto::digest_type::SHA256, hmac_sha256_key); + ctx.update(bytes_view_to_string_view(hmac_sha256_msg)); + bytes sig(bytes::initialized_later{}, ctx.size()); + ctx.reset(bytes_span_to_char_span(sig)); + ASSERT_EQ(sig, hmac_sha256_expected); + ctx.update(hmac_sha256_msg); + bytes sig2(bytes::initialized_later{}, ctx.size()); + ctx.reset(sig2); + ASSERT_EQ(sig2, hmac_sha256_expected); +}