Skip to content

Commit

Permalink
crypto: Added HMAC support
Browse files Browse the repository at this point in the history
Signed-off-by: Michael Boquard <[email protected]>
  • Loading branch information
michael-redpanda committed Mar 5, 2024
1 parent 433123c commit d1eceeb
Show file tree
Hide file tree
Showing 7 changed files with 420 additions and 0 deletions.
1 change: 1 addition & 0 deletions src/v/crypto/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ v_cc_library(
SRCS
crypto.cc
digest.cc
hmac.cc
ssl_utils.cc
DEPS
absl::node_hash_set
Expand Down
28 changes: 28 additions & 0 deletions src/v/crypto/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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();
```
174 changes: 174 additions & 0 deletions src/v/crypto/hmac.cc
Original file line number Diff line number Diff line change
@@ -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 <openssl/evp.h>
#include <openssl/params.h>

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>,
"hmac_ctx should be nothrow move constructible");
static_assert(
std::is_nothrow_move_assignable_v<hmac_ctx>,
"hmac_ctx should be nothrow move assignable");

class hmac_ctx::impl {
public:
impl(digest_type type, bytes_view key) {
std::array<OSSL_PARAM, 2> params{
OSSL_PARAM_construct_utf8_string(
"digest",
// NOLINTNEXTLINE
const_cast<char*>(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<impl>(type, key)) {}

hmac_ctx::hmac_ctx(digest_type type, std::string_view key)
: _impl(
std::make_unique<impl>(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<char> hmac_ctx::final(std::span<char> 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<char> hmac_ctx::reset(std::span<char> 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
99 changes: 99 additions & 0 deletions src/v/crypto/include/crypto/crypto.h
Original file line number Diff line number Diff line change
Expand Up @@ -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<char> final(std::span<char> 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<char> reset(std::span<char> signature);

private:
class impl;
std::unique_ptr<impl> _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
2 changes: 2 additions & 0 deletions src/v/crypto/ssl_utils.h
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ struct deleter {
template<typename T, void (*fn)(T*)>
using handle = std::unique_ptr<T, deleter<T, fn>>;

using EVP_MAC_ptr = handle<EVP_MAC, EVP_MAC_free>;
using EVP_MAC_CTX_ptr = handle<EVP_MAC_CTX, EVP_MAC_CTX_free>;
using EVP_MD_CTX_ptr = handle<EVP_MD_CTX, EVP_MD_CTX_free>;

/// Exception class used to extract the error from OpenSSL
Expand Down
1 change: 1 addition & 0 deletions src/v/crypto/test/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ rp_test(
BINARY_NAME gtest_crypto
SOURCES
digest_tests.cc
hmac_tests.cc
LIBRARIES
v::crypto
v::gtest_main
Expand Down
Loading

0 comments on commit d1eceeb

Please sign in to comment.