From 85736370a15bbb44cbe8e7746fce2a0777398074 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 18 Aug 2026 15:17:07 +0000 Subject: [PATCH 1/2] Initial plan From 6686bf494db5985d947290ceedb7e87e374ef169 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 18 Aug 2026 15:57:33 +0000 Subject: [PATCH 2/2] Reuse pre-keyed AES-GCM contexts Co-authored-by: eddyashton <6000239+eddyashton@users.noreply.github.com> --- include/ccf/crypto/openssl/openssl_wrappers.h | 8 + src/crypto/openssl/symmetric_key.cpp | 178 +++++++++++++++--- src/crypto/openssl/symmetric_key.h | 12 +- src/crypto/test/bench.cpp | 33 ++++ src/crypto/test/crypto.cpp | 160 ++++++++++++++++ src/node/test/encryptor.cpp | 6 +- 6 files changed, 367 insertions(+), 30 deletions(-) diff --git a/include/ccf/crypto/openssl/openssl_wrappers.h b/include/ccf/crypto/openssl/openssl_wrappers.h index ff375f54b32b..cafd51b1e62a 100644 --- a/include/ccf/crypto/openssl/openssl_wrappers.h +++ b/include/ccf/crypto/openssl/openssl_wrappers.h @@ -354,6 +354,14 @@ namespace ccf::crypto::OpenSSL using Unique_SSL_OBJECT::Unique_SSL_OBJECT; }; + struct Unique_EVP_CIPHER + : public Unique_SSL_OBJECT + { + Unique_EVP_CIPHER(EVP_CIPHER* cipher) : + Unique_SSL_OBJECT(cipher, EVP_CIPHER_free) + {} + }; + struct Unique_STACK_OF_X509 : public Unique_SSL_OBJECT { diff --git a/src/crypto/openssl/symmetric_key.cpp b/src/crypto/openssl/symmetric_key.cpp index 14d8d997fc0d..479615499091 100644 --- a/src/crypto/openssl/symmetric_key.cpp +++ b/src/crypto/openssl/symmetric_key.cpp @@ -7,9 +7,12 @@ #include "ccf/crypto/symmetric_key.h" #include "ds/internal_logger.h" +#include #include +#include #include #include +#include namespace ccf::crypto { @@ -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 rawKey) : - key(std::vector(rawKey.data(), rawKey.data() + rawKey.size())) + namespace { - const auto n = static_cast(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 raw_key) { - evp_cipher = EVP_aes_256_gcm(); - evp_cipher_wrap_pad = EVP_aes_256_wrap_pad(); + const auto n = static_cast(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 raw_key) { - evp_cipher = EVP_aes_192_gcm(); - evp_cipher_wrap_pad = EVP_aes_192_wrap_pad(); + const auto n = static_cast(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 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 lock; + std::optional uncached = std::nullopt; + EVP_CIPHER_CTX* context = nullptr; + + public: + ContextLease( + CachedContext& cached_, std::unique_lock&& 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 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 contexts; + + public: + ContextPool(bool encrypt_) : encrypt(encrypt_) {} + + ContextLease acquire( + const EVP_CIPHER* cipher, std::span key) + { + for (auto& cached : contexts) + { + std::unique_lock 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 rawKey) : + key(std::vector(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()) + {} + + 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(key.data()), key.size()); } size_t KeyAesGcm_OpenSSL::key_size() const @@ -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(); 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()) { @@ -113,12 +244,13 @@ namespace ccf::crypto std::span aad, std::vector& 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}; diff --git a/src/crypto/openssl/symmetric_key.h b/src/crypto/openssl/symmetric_key.h index 3729cdb7c9b7..86c99958fce1 100644 --- a/src/crypto/openssl/symmetric_key.h +++ b/src/crypto/openssl/symmetric_key.h @@ -12,18 +12,18 @@ namespace ccf::crypto class KeyAesGcm_OpenSSL : public KeyAesGcm { private: - const std::vector key; - const EVP_CIPHER* evp_cipher = nullptr; + struct ContextPools; + + std::vector key; + OpenSSL::Unique_EVP_CIPHER evp_cipher; const EVP_CIPHER* evp_cipher_wrap_pad; + std::unique_ptr context_pools; public: KeyAesGcm_OpenSSL(std::span rawKey); KeyAesGcm_OpenSSL(const KeyAesGcm_OpenSSL& that) = delete; KeyAesGcm_OpenSSL(KeyAesGcm_OpenSSL&& that) noexcept; - ~KeyAesGcm_OpenSSL() override - { - OPENSSL_cleanse(const_cast(key.data()), key.size()); - } + ~KeyAesGcm_OpenSSL() override; [[nodiscard]] size_t key_size() const override; diff --git a/src/crypto/test/bench.cpp b/src/crypto/test/bench.cpp index 675b8690fec5..748e3cbc6a9a 100644 --- a/src/crypto/test/bench.cpp +++ b/src/crypto/test/bench.cpp @@ -112,6 +112,29 @@ static void benchmark_hmac(picobench::state& s) s.stop_timer(); } +template +static void benchmark_aes_gcm_encrypt(picobench::state& s) +{ + const std::vector key(GCM_DEFAULT_KEY_SIZE, 0x42); + const auto contents = make_contents(); + auto aes_gcm_key = make_key_aes_gcm(key); + StandardGcmHeader header; + std::vector 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 static void benchmark_hash(picobench::state& s) { @@ -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 shares; PICOBENCH_SUITE("share"); diff --git a/src/crypto/test/crypto.cpp b/src/crypto/test/crypto.cpp index 94dcb2bb8678..d9ec1437082f 100644 --- a/src/crypto/test/crypto.cpp +++ b/src/crypto/test/crypto.cpp @@ -25,12 +25,16 @@ #include "crypto/openssl/verifier.h" #include "crypto/openssl/x509_time.h" +#include +#include +#include #include #include #include #include #include #include +#include using namespace std; using namespace ccf::crypto; @@ -809,6 +813,162 @@ static const vector& get_raw_key() return v; } +TEST_CASE("AES-GCM context reuse") +{ + const std::vector key(16, 0); + const std::vector iv(12, 0); + const std::vector plain(16, 0); + const std::vector expected_cipher = { + 0x03, + 0x88, + 0xda, + 0xce, + 0x60, + 0xb6, + 0xa3, + 0x92, + 0xf3, + 0x28, + 0xc2, + 0xb9, + 0x71, + 0xb2, + 0xfe, + 0x78}; + const uint8_t expected_tag[GCM_SIZE_TAG] = { + 0xab, + 0x6e, + 0x47, + 0xd4, + 0x2c, + 0xec, + 0x13, + 0xbd, + 0xf5, + 0x3a, + 0x67, + 0xb2, + 0x12, + 0x57, + 0xbd, + 0xdf}; + auto aes_gcm_key = make_key_aes_gcm(key); + + std::vector cipher; + uint8_t tag[GCM_SIZE_TAG] = {}; + aes_gcm_key->encrypt(iv, plain, {}, cipher, tag); + + REQUIRE(cipher == expected_cipher); + REQUIRE(std::equal(std::begin(tag), std::end(tag), std::begin(expected_tag))); + + std::vector decrypted; + std::array invalid_tag; + std::copy(std::begin(tag), std::end(tag), invalid_tag.begin()); + invalid_tag[0] ^= 1; + REQUIRE_FALSE( + aes_gcm_key->decrypt(iv, invalid_tag.data(), cipher, {}, decrypted)); + REQUIRE(decrypted.empty()); + + REQUIRE(aes_gcm_key->decrypt(iv, tag, cipher, {}, decrypted)); + REQUIRE(decrypted == plain); +} + +TEST_CASE("AES-GCM empty inputs") +{ + auto aes_gcm_key = make_key_aes_gcm(get_raw_key()); + const std::vector iv(12, 0); + const std::vector aad(8, 0x42); + const std::vector plain(8, 0x24); + uint8_t tag[GCM_SIZE_TAG] = {}; + std::vector cipher; + std::vector decrypted; + + aes_gcm_key->encrypt(iv, {}, aad, cipher, tag); + REQUIRE(cipher.empty()); + REQUIRE(aes_gcm_key->decrypt(iv, tag, cipher, aad, decrypted)); + REQUIRE(decrypted.empty()); + + aes_gcm_key->encrypt(iv, plain, {}, cipher, tag); + REQUIRE(aes_gcm_key->decrypt(iv, tag, cipher, {}, decrypted)); + REQUIRE(decrypted == plain); + + REQUIRE_THROWS_AS( + aes_gcm_key->encrypt(iv, {}, {}, cipher, tag), std::logic_error); + + const std::vector empty_key(16, 0); + auto empty_aes_gcm_key = make_key_aes_gcm(empty_key); + const uint8_t empty_tag[GCM_SIZE_TAG] = { + 0x58, + 0xe2, + 0xfc, + 0xce, + 0xfa, + 0x7e, + 0x30, + 0x61, + 0x36, + 0x7f, + 0x1d, + 0x57, + 0xa4, + 0xe7, + 0x45, + 0x5a}; + decrypted.clear(); + REQUIRE(empty_aes_gcm_key->decrypt(iv, empty_tag, {}, {}, decrypted)); + REQUIRE(decrypted.empty()); +} + +TEST_CASE("Concurrent AES-GCM context reuse") +{ + constexpr size_t thread_count = 24; + constexpr size_t iteration_count = 128; + auto aes_gcm_key = make_key_aes_gcm(get_raw_key()); + std::barrier start(thread_count); + std::atomic success = true; + std::vector threads; + + for (size_t thread_index = 0; thread_index < thread_count; ++thread_index) + { + threads.emplace_back([&, thread_index]() { + try + { + start.arrive_and_wait(); + for (size_t i = 0; i < iteration_count; ++i) + { + const uint64_t nonce = (thread_index * iteration_count) + i + 1; + std::vector iv(12, 0); + memcpy(iv.data(), &nonce, sizeof(nonce)); + const std::vector plain(64, thread_index); + const std::vector aad(16, i); + std::vector cipher; + uint8_t tag[GCM_SIZE_TAG] = {}; + + aes_gcm_key->encrypt(iv, plain, aad, cipher, tag); + std::vector decrypted; + if ( + !aes_gcm_key->decrypt(iv, tag, cipher, aad, decrypted) || + decrypted != plain) + { + success = false; + } + } + } + catch (...) + { + success = false; + } + }); + } + + for (auto& thread : threads) + { + thread.join(); + } + + REQUIRE(success); +} + TEST_CASE("ExtendedIv0") { auto k = ccf::crypto::make_key_aes_gcm(get_raw_key()); diff --git a/src/node/test/encryptor.cpp b/src/node/test/encryptor.cpp index 19b819d803a5..8ecce071936a 100644 --- a/src/node/test/encryptor.cpp +++ b/src/node/test/encryptor.cpp @@ -414,18 +414,22 @@ TEST_CASE("Encryptor rollback") ledger_secrets->init(); auto encryptor = std::make_shared(ledger_secrets); store.set_encryptor(encryptor); + std::weak_ptr rolled_back_key; commit_one(store, map); // Assumes tx at seqno 2 rekeys. Txs from seqno 3 will be encrypted with new // secret commit_one(store, map); - ledger_secrets->set_secret(3, ccf::make_ledger_secret()); + auto rolled_back_secret = ccf::make_ledger_secret(); + rolled_back_key = rolled_back_secret->key; + ledger_secrets->set_secret(3, std::move(rolled_back_secret)); commit_one(store, map); // Rollback store at seqno 1, discarding encryption key at 3 store.rollback({store_term, 1}, store.commit_view()); + REQUIRE(rolled_back_key.expired()); commit_one(store, map);