Skip to content
Merged
21 changes: 20 additions & 1 deletion include/ccf/crypto/symmetric_key.h
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@ namespace ccf::crypto
[[nodiscard]] std::span<const uint8_t> get_iv() const;

[[nodiscard]] size_t serialised_size() const;
std::vector<uint8_t> serialise();
[[nodiscard]] std::vector<uint8_t> serialise() const;
void serialise(std::span<uint8_t> output) const;

void deserialise(const std::vector<uint8_t>& ser);
void deserialise(const uint8_t*& data, size_t& size);
Expand Down Expand Up @@ -92,6 +93,15 @@ namespace ccf::crypto
std::vector<uint8_t>& cipher,
uint8_t tag[GCM_SIZE_TAG]) = 0;

// cipher must be exactly plain.size() bytes. Input and output must not
// partially overlap; identical plaintext and ciphertext spans are valid.
virtual void encrypt(
std::span<const uint8_t> iv,
std::span<const uint8_t> plain,
std::span<const uint8_t> aad,
std::span<uint8_t> cipher,
uint8_t tag[GCM_SIZE_TAG]);

// Replaces plain on success and clears it if authentication fails.
virtual bool decrypt(
std::span<const uint8_t> iv,
Expand All @@ -118,6 +128,15 @@ namespace ccf::crypto
std::vector<uint8_t>& cipher,
uint8_t tag[GCM_SIZE_TAG]) const = 0;

// cipher must be exactly plain.size() bytes. Input and output must not
// partially overlap; identical plaintext and ciphertext spans are valid.
virtual void encrypt(
std::span<const uint8_t> iv,
std::span<const uint8_t> plain,
std::span<const uint8_t> aad,
std::span<uint8_t> cipher,
Comment thread
achamayou marked this conversation as resolved.
uint8_t tag[GCM_SIZE_TAG]) const;

// AES-GCM decryption
// Replaces plain on success and clears it if authentication fails.
virtual bool decrypt(
Expand Down
58 changes: 53 additions & 5 deletions src/crypto/openssl/symmetric_key.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -59,13 +59,19 @@ namespace ccf::crypto
std::span<const uint8_t> iv,
std::span<const uint8_t> plain,
std::span<const uint8_t> aad,
std::vector<uint8_t>& cipher,
std::span<uint8_t> cipher,
uint8_t tag[GCM_SIZE_TAG])
{
if (aad.empty() && plain.empty())
{
throw std::logic_error("aad and plain cannot both be empty");
}
if (
cipher.size() != plain.size() || plain.size() > INT_MAX ||
aad.size() > INT_MAX)
{
throw std::logic_error("Invalid AES-GCM input or output size");
}

CHECK1(EVP_CIPHER_CTX_ctrl(
context, EVP_CTRL_GCM_SET_IVLEN, iv.size(), nullptr));
Expand All @@ -79,16 +85,15 @@ namespace ccf::crypto
context, nullptr, &aad_outl, aad.data(), aad.size()));
}

std::vector<uint8_t> ciphertext(plain.size());
if (!plain.empty())
{
int cipher_outl{0};
CHECK1(EVP_EncryptUpdate(
context,
ciphertext.data(),
cipher.data(),
&cipher_outl,
plain.data(),
plain.size()));
static_cast<int>(plain.size())));

// As we use no padding, we expect the input and output lengths to
// match.
Expand All @@ -106,8 +111,28 @@ namespace ccf::crypto

CHECK1(EVP_CIPHER_CTX_ctrl(
context, EVP_CTRL_GCM_GET_TAG, GCM_SIZE_TAG, &tag[0]));
}

cipher = std::move(ciphertext);
void encrypt_with_context(
EVP_CIPHER_CTX* context,
std::span<const uint8_t> iv,
std::span<const uint8_t> plain,
std::span<const uint8_t> aad,
std::vector<uint8_t>& cipher,
uint8_t tag[GCM_SIZE_TAG])
{
if (cipher.size() == plain.size())
{
encrypt_with_context(
context, iv, plain, aad, std::span<uint8_t>(cipher), tag);
}
else
{
std::vector<uint8_t> output(plain.size());
encrypt_with_context(
context, iv, plain, aad, std::span<uint8_t>(output), tag);
cipher = std::move(output);
}
}

bool decrypt_with_context(
Expand Down Expand Up @@ -194,6 +219,16 @@ namespace ccf::crypto
encrypt_with_context(encrypt_context, iv, plain, aad, cipher, tag);
}

void encrypt(
std::span<const uint8_t> iv,
std::span<const uint8_t> plain,
std::span<const uint8_t> aad,
std::span<uint8_t> cipher,
uint8_t tag[GCM_SIZE_TAG]) override
{
encrypt_with_context(encrypt_context, iv, plain, aad, cipher, tag);
}

bool decrypt(
std::span<const uint8_t> iv,
const uint8_t tag[GCM_SIZE_TAG],
Expand Down Expand Up @@ -241,6 +276,19 @@ namespace ccf::crypto
encrypt_with_context(context, iv, plain, aad, cipher, tag);
}

void KeyAesGcm_OpenSSL::encrypt(
std::span<const uint8_t> iv,
std::span<const uint8_t> plain,
std::span<const uint8_t> aad,
std::span<uint8_t> cipher,
uint8_t tag[GCM_SIZE_TAG]) const
{
Unique_EVP_CIPHER_CTX context;
CHECK1(
EVP_EncryptInit_ex2(context, evp_cipher, key.data(), nullptr, nullptr));
encrypt_with_context(context, iv, plain, aad, cipher, tag);
}

bool KeyAesGcm_OpenSSL::decrypt(
std::span<const uint8_t> iv,
const uint8_t tag[GCM_SIZE_TAG],
Expand Down
7 changes: 7 additions & 0 deletions src/crypto/openssl/symmetric_key.h
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,13 @@ namespace ccf::crypto
std::vector<uint8_t>& cipher,
uint8_t tag[GCM_SIZE_TAG]) const override;

void encrypt(
std::span<const uint8_t> iv,
std::span<const uint8_t> plain,
std::span<const uint8_t> aad,
std::span<uint8_t> cipher,
uint8_t tag[GCM_SIZE_TAG]) const override;

bool decrypt(
std::span<const uint8_t> iv,
const uint8_t tag[GCM_SIZE_TAG],
Expand Down
70 changes: 63 additions & 7 deletions src/crypto/symmetric_key.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
#include "ccf/crypto/symmetric_key.h"
#include "ds/serialized.h"

#include <algorithm>

#define FMT_HEADER_ONLY
#include <fmt/format.h>

Expand Down Expand Up @@ -39,16 +41,24 @@ namespace ccf::crypto
return sizeof(tag) + iv.size();
}

std::vector<uint8_t> GcmHeader::serialise()
std::vector<uint8_t> GcmHeader::serialise() const
{
auto space = serialised_size();
std::vector<uint8_t> serial_hdr(space);
std::vector<uint8_t> serial_hdr(serialised_size());
serialise(serial_hdr);
return serial_hdr;
}

auto* data_ = serial_hdr.data();
serialized::write(data_, space, static_cast<uint8_t*>(tag), sizeof(tag));
serialized::write(data_, space, iv.data(), iv.size());
void GcmHeader::serialise(std::span<uint8_t> output) const
{
if (output.size() != serialised_size())
{
throw std::logic_error("Incorrect GCM header output size");
}

return serial_hdr;
auto* data_ = output.data();
auto space = output.size();
serialized::write(data_, space, tag, sizeof(tag));
serialized::write(data_, space, iv.data(), iv.size());
}

void GcmHeader::deserialise(const std::vector<uint8_t>& ser)
Expand All @@ -68,6 +78,52 @@ namespace ccf::crypto
iv = serialized::read(data, size, iv.size());
}

void KeyAesGcm::Context::encrypt(
std::span<const uint8_t> iv,
std::span<const uint8_t> plain,
std::span<const uint8_t> aad,
std::span<uint8_t> cipher,
uint8_t tag[GCM_SIZE_TAG])
{
if (cipher.size() != plain.size())
{
throw std::logic_error("Incorrect AES-GCM output size");
}
std::vector<uint8_t> output;
encrypt(iv, plain, aad, output, tag);
if (output.size() != cipher.size())
{
throw std::logic_error("Unexpected AES-GCM ciphertext size");
}
if (!output.empty())
{
std::copy(output.begin(), output.end(), cipher.begin());
}
}

void KeyAesGcm::encrypt(
std::span<const uint8_t> iv,
std::span<const uint8_t> plain,
std::span<const uint8_t> aad,
std::span<uint8_t> cipher,
uint8_t tag[GCM_SIZE_TAG]) const
{
if (cipher.size() != plain.size())
{
throw std::logic_error("Incorrect AES-GCM output size");
}
std::vector<uint8_t> output;
encrypt(iv, plain, aad, output, tag);
if (output.size() != cipher.size())
{
throw std::logic_error("Unexpected AES-GCM ciphertext size");
}
if (!output.empty())
{
std::copy(output.begin(), output.end(), cipher.begin());
}
}

/// GcmCipher implementation
GcmCipher::GcmCipher() = default;

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

template <size_t NContents, bool ReuseContext>
static void benchmark_aes_gcm_encrypt_span(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);
auto context = aes_gcm_key->make_context();
StandardGcmHeader header;
std::vector<uint8_t> cipher(contents.size());
uint64_t iv = 0;

s.start_timer();
for (auto _ : s)
{
(void)_;
memcpy(header.iv.data(), &iv, sizeof(iv));
++iv;
if constexpr (ReuseContext)
{
context->encrypt(
header.get_iv(), contents, {}, std::span<uint8_t>(cipher), header.tag);
}
else
{
aes_gcm_key->encrypt(
header.get_iv(), contents, {}, std::span<uint8_t>(cipher), header.tag);
}
do_not_optimize(cipher);
clobber_memory();
}
s.stop_timer();
}

template <size_t NContents, bool ReuseContext>
static void benchmark_aes_gcm_decrypt(picobench::state& s)
{
Expand Down Expand Up @@ -537,6 +570,14 @@ namespace AES_GCM_ENCRYPT_64

auto aes_gcm_encrypt_reused_context = benchmark_aes_gcm_encrypt<64, true>;
PICOBENCH(aes_gcm_encrypt_reused_context).iterations({100000});

auto aes_gcm_encrypt_new_context_span =
benchmark_aes_gcm_encrypt_span<64, false>;
PICOBENCH(aes_gcm_encrypt_new_context_span).iterations({100000});

auto aes_gcm_encrypt_reused_context_span =
benchmark_aes_gcm_encrypt_span<64, true>;
PICOBENCH(aes_gcm_encrypt_reused_context_span).iterations({100000});
}

PICOBENCH_SUITE("aes gcm encrypt 1024 bytes");
Expand Down
81 changes: 81 additions & 0 deletions src/crypto/test/crypto.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1260,6 +1260,87 @@ TEST_CASE("AES-GCM empty inputs")
REQUIRE(decrypted.empty());
}

TEST_CASE("AES-GCM caller-owned ciphertext")
{
auto key = make_key_aes_gcm(get_raw_key());
auto context = key->make_context();
const std::vector<uint8_t> iv(12, 0);
const std::vector<uint8_t> plain(31, 0x24);
const std::vector<uint8_t> aad(7, 0x42);
std::vector<uint8_t> expected;
uint8_t expected_tag[GCM_SIZE_TAG] = {};
key->encrypt(iv, plain, aad, expected, expected_tag);

std::array<uint8_t, 33> storage = {};
storage.front() = 0xAA;
storage.back() = 0xBB;
auto output = std::span<uint8_t>(storage).subspan(1, plain.size());
uint8_t tag[GCM_SIZE_TAG] = {};
key->encrypt(iv, plain, aad, output, tag);
REQUIRE(std::equal(output.begin(), output.end(), expected.begin()));
REQUIRE(std::equal(std::begin(tag), std::end(tag), std::begin(expected_tag)));
REQUIRE(storage.front() == 0xAA);
REQUIRE(storage.back() == 0xBB);

context->encrypt(iv, plain, aad, output, tag);
REQUIRE(std::equal(output.begin(), output.end(), expected.begin()));
REQUIRE(std::equal(std::begin(tag), std::end(tag), std::begin(expected_tag)));

std::vector<uint8_t> preallocated(plain.size(), 0);
auto* original_data = preallocated.data();
key->encrypt(iv, plain, aad, preallocated, tag);
REQUIRE(preallocated.data() == original_data);
REQUIRE(preallocated == expected);

auto in_place_plain = plain;
key->encrypt(
iv,
std::span<const uint8_t>(in_place_plain),
aad,
std::span<uint8_t>(in_place_plain),
tag);
REQUIRE(in_place_plain == expected);

auto vector_in_place = plain;
key->encrypt(
iv, std::span<const uint8_t>(vector_in_place), aad, vector_in_place, tag);
REQUIRE(vector_in_place == expected);

REQUIRE_THROWS_AS(
key->encrypt(iv, plain, aad, output.first(output.size() - 1), tag),
std::logic_error);
REQUIRE(storage.front() == 0xAA);
REQUIRE(storage.back() == 0xBB);

std::vector<uint8_t> decrypted;
REQUIRE(key->decrypt(iv, tag, output, aad, decrypted));
REQUIRE(decrypted == plain);
auto bad_aad = aad;
bad_aad.front() ^= 1;
REQUIRE_FALSE(key->decrypt(iv, tag, output, bad_aad, decrypted));

std::span<uint8_t> empty_output;
key->encrypt(iv, {}, aad, empty_output, tag);
REQUIRE(key->decrypt(iv, tag, empty_output, aad, decrypted));
REQUIRE(decrypted.empty());
}

TEST_CASE("Const GCM header serialisation")
{
StandardGcmHeader header;
std::iota(std::begin(header.tag), std::end(header.tag), 0);
std::iota(header.iv.begin(), header.iv.end(), GCM_SIZE_TAG);
const auto& const_header = header;

std::vector<uint8_t> expected(const_header.serialised_size());
std::iota(expected.begin(), expected.end(), 0);
REQUIRE(const_header.serialise() == expected);

std::vector<uint8_t> output(expected.size());
const_header.serialise(output);
REQUIRE(output == expected);
}

TEST_CASE("Concurrent AES-GCM convenience calls")
{
constexpr size_t thread_count = 24;
Expand Down
Loading
Loading