Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions include/ccf/crypto/openssl/openssl_wrappers.h
Original file line number Diff line number Diff line change
Expand Up @@ -354,6 +354,14 @@ namespace ccf::crypto::OpenSSL
using Unique_SSL_OBJECT::Unique_SSL_OBJECT;
};

struct Unique_EVP_CIPHER
: public Unique_SSL_OBJECT<EVP_CIPHER, nullptr, EVP_CIPHER_free>
{
Unique_EVP_CIPHER(EVP_CIPHER* cipher) :
Unique_SSL_OBJECT(cipher, EVP_CIPHER_free)
{}
};

struct Unique_STACK_OF_X509
: public Unique_SSL_OBJECT<STACK_OF(X509), nullptr, nullptr>
{
Expand Down
178 changes: 155 additions & 23 deletions src/crypto/openssl/symmetric_key.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,12 @@
#include "ccf/crypto/symmetric_key.h"
#include "ds/internal_logger.h"

#include <array>
#include <climits>
#include <mutex>
#include <openssl/aes.h>
#include <openssl/evp.h>
#include <optional>

namespace ccf::crypto
{
Expand All @@ -19,30 +22,158 @@ namespace ccf::crypto
static constexpr size_t KEY_SIZE_192 = 192;
static constexpr size_t KEY_SIZE_128 = 128;

KeyAesGcm_OpenSSL::KeyAesGcm_OpenSSL(std::span<const uint8_t> rawKey) :
key(std::vector<uint8_t>(rawKey.data(), rawKey.data() + rawKey.size()))
namespace
{
const auto n = static_cast<unsigned int>(rawKey.size() * CHAR_BIT);
if (n >= KEY_SIZE_256)
static constexpr size_t MAX_CACHED_CONTEXTS = 16;

const char* get_gcm_cipher_name(std::span<const uint8_t> raw_key)
{
evp_cipher = EVP_aes_256_gcm();
evp_cipher_wrap_pad = EVP_aes_256_wrap_pad();
const auto n = static_cast<unsigned int>(raw_key.size() * CHAR_BIT);
if (n >= KEY_SIZE_256)
{
return "AES-256-GCM";
}
if (n >= KEY_SIZE_192)
{
return "AES-192-GCM";
}
if (n >= KEY_SIZE_128)
{
return "AES-128-GCM";
}
throw std::logic_error(
fmt::format("Need at least {} bits, only have {}", KEY_SIZE_128, n));
}
else if (n >= KEY_SIZE_192)

const EVP_CIPHER* get_wrap_pad_cipher(std::span<const uint8_t> raw_key)
{
evp_cipher = EVP_aes_192_gcm();
evp_cipher_wrap_pad = EVP_aes_192_wrap_pad();
const auto n = static_cast<unsigned int>(raw_key.size() * CHAR_BIT);
if (n >= KEY_SIZE_256)
{
return EVP_aes_256_wrap_pad();
}
if (n >= KEY_SIZE_192)
{
return EVP_aes_192_wrap_pad();
}
return EVP_aes_128_wrap_pad();
}
else if (n >= KEY_SIZE_128)

struct CachedContext
{
evp_cipher = EVP_aes_128_gcm();
evp_cipher_wrap_pad = EVP_aes_128_wrap_pad();
}
else
std::mutex lock;
std::optional<Unique_EVP_CIPHER_CTX> context = std::nullopt;
bool keyed = false;
};

class ContextLease
{
throw std::logic_error(
fmt::format("Need at least {} bits, only have {}", KEY_SIZE_128, n));
}
private:
CachedContext* cached = nullptr;
[[maybe_unused]] std::unique_lock<std::mutex> lock;
std::optional<Unique_EVP_CIPHER_CTX> uncached = std::nullopt;
EVP_CIPHER_CTX* context = nullptr;

public:
ContextLease(
CachedContext& cached_, std::unique_lock<std::mutex>&& lock_) :
cached(&cached_),
lock(std::move(lock_))
{
if (!cached->context.has_value())
{
cached->context.emplace();
}
context = cached->context.value();
}

ContextLease() : uncached(std::in_place), context(uncached.value()) {}

EVP_CIPHER_CTX* get()
{
return context;
}

void initialise(
bool encrypt, const EVP_CIPHER* cipher, std::span<const uint8_t> key)
{
if (cached != nullptr && cached->keyed)
{
return;
}

if (encrypt)
{
CHECK1(
EVP_EncryptInit_ex2(context, cipher, key.data(), nullptr, nullptr));
}
else
{
CHECK1(
EVP_DecryptInit_ex2(context, cipher, key.data(), nullptr, nullptr));
}

if (cached != nullptr)
{
cached->keyed = true;
}
}
};

class ContextPool
{
private:
const bool encrypt;
std::array<CachedContext, MAX_CACHED_CONTEXTS> contexts;

public:
ContextPool(bool encrypt_) : encrypt(encrypt_) {}

ContextLease acquire(
const EVP_CIPHER* cipher, std::span<const uint8_t> key)
{
for (auto& cached : contexts)
{
std::unique_lock<std::mutex> lock(cached.lock, std::try_to_lock);
if (lock.owns_lock())
{
ContextLease lease(cached, std::move(lock));
lease.initialise(encrypt, cipher, key);
return lease;
}
}

ContextLease lease;
lease.initialise(encrypt, cipher, key);
return lease;
}
};
}

struct KeyAesGcm_OpenSSL::ContextPools
{
ContextPool encrypt{true};
ContextPool decrypt{false};
};

KeyAesGcm_OpenSSL::KeyAesGcm_OpenSSL(std::span<const uint8_t> rawKey) :
key(std::vector<uint8_t>(rawKey.data(), rawKey.data() + rawKey.size())),
evp_cipher(EVP_CIPHER_fetch(nullptr, get_gcm_cipher_name(rawKey), nullptr)),
evp_cipher_wrap_pad(get_wrap_pad_cipher(rawKey)),
context_pools(std::make_unique<ContextPools>())
{}

KeyAesGcm_OpenSSL::KeyAesGcm_OpenSSL(KeyAesGcm_OpenSSL&& that) noexcept :
key(std::move(that.key)),
evp_cipher(std::move(that.evp_cipher)),
evp_cipher_wrap_pad(that.evp_cipher_wrap_pad),
context_pools(std::move(that.context_pools))
{}

KeyAesGcm_OpenSSL::~KeyAesGcm_OpenSSL()
{
context_pools.reset();
OPENSSL_cleanse(const_cast<uint8_t*>(key.data()), key.size());
}

size_t KeyAesGcm_OpenSSL::key_size() const
Expand All @@ -62,12 +193,12 @@ namespace ccf::crypto
throw std::logic_error("aad and plain cannot both be empty");
}

Unique_EVP_CIPHER_CTX ctx;
CHECK1(EVP_EncryptInit_ex(ctx, evp_cipher, nullptr, key.data(), nullptr));
auto lease = context_pools->encrypt.acquire(evp_cipher, key);
auto* ctx = lease.get();
Comment on lines +196 to +197

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So I've gone back and forth on whether this is the right approach. The subtle detail is that EVP_CIPHER_CTX is not thread-safe, but the existing API (std::shared_ptr<KeyAesGcm>->encrypt) is safe to call from multiple threads, because it creates this context locally (at great cost). If we want to reuse that context, we need to handle the thread-safety somewhere. Roughly, we can either do that way down here inside, and not touch the external APIs at all, or create some kind of AesGcmContext object at the external API, and make management (and thread-safe access to it) the caller's responsibility. Since the affinity is both thread- and instance- specific (we have multiple keys for old ledger secrets!), we don't get help from thread_local. We've actually provided both methods already - for SHA its a single global context that's expensive to lookup, so trivial to cache statically for everyone. For TLS, there are many contexts that are externally managed, and we ensure non-concurrent access to these. This ContextPool is local and well-contained, but uncomfortably complex.


CHECK1(
EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_SET_IVLEN, iv.size(), nullptr));
CHECK1(EVP_EncryptInit_ex(ctx, nullptr, nullptr, key.data(), iv.data()));
CHECK1(EVP_EncryptInit_ex2(ctx, nullptr, nullptr, iv.data(), nullptr));

if (!aad.empty())
{
Expand Down Expand Up @@ -113,12 +244,13 @@ namespace ccf::crypto
std::span<const uint8_t> aad,
std::vector<uint8_t>& plain) const
{
Unique_EVP_CIPHER_CTX ctx;
CHECK1(EVP_DecryptInit_ex(ctx, evp_cipher, nullptr, nullptr, nullptr));
auto lease = context_pools->decrypt.acquire(evp_cipher, key);
auto* ctx = lease.get();

CHECK1(
EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_SET_IVLEN, iv.size(), nullptr));

CHECK1(EVP_DecryptInit_ex(ctx, nullptr, nullptr, key.data(), iv.data()));
CHECK1(EVP_DecryptInit_ex2(ctx, nullptr, nullptr, iv.data(), nullptr));
if (!aad.empty())
{
int aad_outl{0};
Expand Down
12 changes: 6 additions & 6 deletions src/crypto/openssl/symmetric_key.h
Original file line number Diff line number Diff line change
Expand Up @@ -12,18 +12,18 @@ namespace ccf::crypto
class KeyAesGcm_OpenSSL : public KeyAesGcm
{
private:
const std::vector<uint8_t> key;
const EVP_CIPHER* evp_cipher = nullptr;
struct ContextPools;

std::vector<uint8_t> key;
OpenSSL::Unique_EVP_CIPHER evp_cipher;
const EVP_CIPHER* evp_cipher_wrap_pad;
std::unique_ptr<ContextPools> context_pools;

public:
KeyAesGcm_OpenSSL(std::span<const uint8_t> rawKey);
KeyAesGcm_OpenSSL(const KeyAesGcm_OpenSSL& that) = delete;
KeyAesGcm_OpenSSL(KeyAesGcm_OpenSSL&& that) noexcept;
~KeyAesGcm_OpenSSL() override
{
OPENSSL_cleanse(const_cast<uint8_t*>(key.data()), key.size());
}
~KeyAesGcm_OpenSSL() override;

[[nodiscard]] size_t key_size() const override;

Expand Down
33 changes: 33 additions & 0 deletions src/crypto/test/bench.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,29 @@ static void benchmark_hmac(picobench::state& s)
s.stop_timer();
}

template <size_t NContents>
static void benchmark_aes_gcm_encrypt(picobench::state& s)
{
const std::vector<uint8_t> key(GCM_DEFAULT_KEY_SIZE, 0x42);
const auto contents = make_contents<NContents>();
auto aes_gcm_key = make_key_aes_gcm(key);
StandardGcmHeader header;
std::vector<uint8_t> cipher;
uint64_t iv = 0;

s.start_timer();
for (auto _ : s)
{
(void)_;
memcpy(header.iv.data(), &iv, sizeof(iv));
++iv;
aes_gcm_key->encrypt(header.get_iv(), contents, {}, cipher, header.tag);
do_not_optimize(cipher);
clobber_memory();
}
s.stop_timer();
}

template <typename P, MDType M, size_t NContents>
static void benchmark_hash(picobench::state& s)
{
Expand Down Expand Up @@ -465,6 +488,16 @@ namespace HMAC_bench
PICOBENCH(openssl_hmac_sha256_64).PICO_HASH_SUFFIX();
}

PICOBENCH_SUITE("aes gcm");
namespace AES_GCM_bench
{
auto aes_gcm_encrypt_64 = benchmark_aes_gcm_encrypt<64>;
PICOBENCH(aes_gcm_encrypt_64).iterations({100000});

auto aes_gcm_encrypt_1024 = benchmark_aes_gcm_encrypt<1024>;
PICOBENCH(aes_gcm_encrypt_1024).iterations({100000});
}

std::vector<ccf::crypto::sharing::Share> shares;

PICOBENCH_SUITE("share");
Expand Down
Loading
Loading