С++17 wrapper для OpenSSL: ECDH и AES 256 -8




При написании одного клиент-серверного приложения на С++ потребовалось организовать защищённое соединение между двумя удалёнными узлами. Я сразу обратил внимание на алгоритмы использующиеся в TLS 1.3. Опустив реализацию TLS-сертификатов и взяв только часть ответственную за шифрования данных, я приступил к работе. Информацию пришлось выискивать в во множестве совершенно разных источниках(от официальной документации OpenSSL, до ответов на stackoverflow), а местами даже додумывать каким образом скрепить куски кода из этих самых разных источников. Так что после успешной реализации задуманного, я решил написать данную статью, с целью помочь тем кто будет решать подобную проблему. В данной статье мы рассмотрим простую реализацию связки алгоритма согласования ключей Диффи-Хеллман на элиптических кривых и алгоритма симметричного шифрования AES 256 с использованием библиотеки OpenSSL для организации защищённого соединения.

Генерация ключей ECDH

Заголовочный файл выглядит следующим образом:

#ifndef SECURITY_HPP
#define SECURITY_HPP

#include <openssl/ecdh.h>
#include "byte_array.hpp"

namespace security {

/**
 * @brief AES256 key and initialization vector
 */
struct AES_t {
  uint8_t key[32] = {
    0,0,0,0,0,0,0,0,0,0,
    0,0,0,0,0,0,0,0,0,0,
    0,0,0,0,0,0,0,0,0,0,
    0,0
  };
  uint8_t init_vector[16] = {
    0,0,0,0,0,0,0,0,0,0,
    0,0,0,0,0,0
  };
  bool isEmpty() const {
      static const AES_t empty_aes;
      return !std::memcmp(this, &empty_aes, sizeof (AES_t));
  }
  void clear() {*this = AES_t();}
  AES_t() = default;
  AES_t(ByteArray data) {
    *this = *reinterpret_cast<AES_t*>(data.begin());
  }
};

// ECDH
/**
 * @brief Generate ECDH key pair
 * @return ECDH key pair
 */
EVP_PKEY* genKey();

/**
 * @brief Free key memory
 * @param key
 */
void freeKey(EVP_PKEY* key);

/**
 * @brief Extract public key from key pair
 * @param key_pair
 * @return ByteArray with public key
 */
ByteArray extractPublicKey(EVP_PKEY* key_pair);

/**
 * @brief Extract private key from key pair
 * @param key_pair
 * @return ByteArray with private key
 */
ByteArray extractPrivateKey(EVP_PKEY* key_pair);

/**
 * @brief Conver ByteArrays to key pair
 * @param priv_key_raw
 * @param pub_key_raw
 * @return ECDH key pair
 */
EVP_PKEY* getKeyPair(ByteArray priv_key_raw, ByteArray pub_key_raw);

/**
 * @brief Get AES256 key and initialization vector from ECDH keys
 * @param peer_key - public key from other side
 * @param key_pair - private key from this side
 * @return AES256 key and initialization vector
 */
AES_t getSecret(ByteArray peer_key, EVP_PKEY* key_pair);

// AES256
/**
 * @brief Encrypt message
 * @param plain_text
 * @param aes_struct
 * @return Cyphertext
 */
ByteArray encrypt(ByteArray plain_text, AES_t aes_struct);

/**
 * @brief Decrypt message
 * @param ciphertext
 * @param aes_struct
 * @return plain_text
 */
ByteArray decrypt(ByteArray ciphertext, AES_t aes_struct);

/**
* @brief Encode data with base64
* @param decoded
* @return
*/
ByteArray encodeBase64(ByteArray decoded);

/**
* @brief Decode base64 data
* @param encoded
* @return
*/
ByteArray decodeBase64(ByteArray encoded);

}

#endif // SECURITY_HPP

Для работы со всеми нижеописанными функциями OpenSSL нам потрубуется подключить в файл реализации следующие заголовочные файлы:

// файл security.cpp
#include "security.hpp"

#include <openssl/conf.h>
#include <openssl/err.h>
// EVP
#include <openssl/evp.h>
// AES
#include <openssl/aes.h>
// ECDH
#include <openssl/ec.h>
#include <openssl/pem.h>

#include <stdexcept>

// Обработка ошибок
void handleErrors() {
  ERR_print_errors_fp(stderr);
  throw std::runtime_error("Security error");
}

// Нижеприведённый код здесь

Для начала каждая из сторон должна сгенрировать пары ключей ECDH. Для работы с ключами ECDH будем использовать высокоуровневый интерфейс OpenSSL - EVP.

EVP_PKEY* security::genKey() {
  EVP_PKEY* key_pair = nullptr;						// Ключевая пара
  EVP_PKEY_CTX* param_gen_ctx = nullptr; 	// Контекст генерации параметров
  EVP_PKEY_CTX* key_gen_ctx = nullptr;		// Контекст генерации ключа
  EVP_PKEY* params= nullptr;							// Параметры ключа

  // Выделяем память для контекста генерации параметров EC-ключа
  if(!(param_gen_ctx = EVP_PKEY_CTX_new_id(EVP_PKEY_EC, NULL))) handleErrors();
  // Инициализируем контекст генерации параметров EC-ключа
  if(!EVP_PKEY_paramgen_init(param_gen_ctx)) handleErrors();

  // Задаём элиптичекую кривую prime256v1
  if(!EVP_PKEY_CTX_set_ec_paramgen_curve_nid(param_gen_ctx, NID_X9_62_prime256v1))
    handleErrors();

  // Генерируем параметры
  if(!EVP_PKEY_paramgen(param_gen_ctx, &params)) handleErrors();

  // Выделяем память для контекста генерации EC-ключа
  if(!(key_gen_ctx = EVP_PKEY_CTX_new(params, nullptr))) handleErrors();
  // Инициализируем контекст генерации EC-ключа
  if(!EVP_PKEY_keygen_init(key_gen_ctx)) handleErrors();
  // Генерируем ключ
  if(!EVP_PKEY_keygen(key_gen_ctx, &key_pair)) handleErrors();

  // Высвобождаем память контекста генерации параметров
  EVP_PKEY_CTX_free(param_gen_ctx);
  // Высвобождаем память контекста генерации ключа
  EVP_PKEY_CTX_free(key_gen_ctx);
  // Возвращаем указатель ключевой пары
  return key_pair;
}

Извлечение ключей ECDH из ключевой пары EVP_PKEY* в "сырой буффер"

Для передачи по сети или же для хранения в файле полезно знать как извлечь публичный и приватный ключ из указателя на EVP_PKEY.

Поскольку работаем мы с C++ для хранения сырых данных можно использовать std::vector<uint8_t> или класс на подобии нижеописанного:

// Файл byte_array.hpp
#ifndef BYTE_ARRAY_HPP
#define BYTE_ARRAY_HPP

#include <cstdint>
#include <cstring>
#include <utility>
#include <new>
#include <malloc.h>

class ByteArray {
  uint8_t* byte_array = nullptr;
  uint64_t _length = 0;
  public:
  typedef uint8_t* iterator;
  // Конструктор по умолчанию
  ByteArray() = default;
  
  // Коснтруктор с выделением памяти
  ByteArray(uint64_t length)
    : byte_array(new uint8_t[length]),
  		_length(length) {}
  
  // Конструктор копирования из сырого буфера
  ByteArray(void* buffer, uint64_t length)
    : byte_array(new uint8_t[length]),
      _length(length) {
        memcpy(byte_array, buffer, _length);
      }
  
  // Конструктор копирования
  ByteArray(ByteArray& other)
    : byte_array(new uint8_t[other._length]),
      _length(other._length) {
        memcpy(byte_array, other.byte_array, _length);
      }
  
  // Конструктор перемещения
  ByteArray(ByteArray&& other)
    : byte_array(other.byte_array),
      _length(other._length) {
        other.byte_array = nullptr;
      }
  
  // Деструктор
  ~ByteArray() {if(byte_array) delete[] byte_array;}
  
  // Изменить размер
  void resize(uint64_t new_length) {
  	_length = new_length;
  	byte_array = (uint8_t*)realloc(byte_array, _length);
  }
  
  // Добавить размер
  iterator addSize(uint64_t add) {
    byte_array = (uint8_t*)realloc(byte_array, _length + add);
    iterator it = byte_array + _length;
    _length += add;
    memset(it, 0, add);
    return it;
  }
  
  // Getter для размера
  inline uint64_t length() {return _length;}
  // Оператор взятие елемента
  inline uint8_t& operator[](uint64_t index) {return byte_array[index];}
  // Оператор присвоения
  inline ByteArray& operator=(ByteArray other) {
    this->~ByteArray();
    return *new(this) ByteArray(std::move(other));
  }
  
  // Итераторы для range-based for
  // for(auto byte : byte_array_object) {...}
  inline iterator begin() {return byte_array;}
  inline iterator end() {return byte_array + _length;}
};

#endif // BYTE_ARRAY_HPP

Функция извлечения публичного ключа:

ByteArray security::extractPublicKey(EVP_PKEY* key_pair) {
  EC_KEY* ec_key = EVP_PKEY_get1_EC_KEY(key_pair);
  EC_POINT* ec_point = const_cast<EC_POINT*>(EC_KEY_get0_public_key(ec_key));

  EVP_PKEY* public_key = EVP_PKEY_new();
  EC_KEY* public_ec_key = EC_KEY_new_by_curve_name(NID_X9_62_prime256v1);

  EC_KEY_set_public_key(public_ec_key, ec_point);
  EVP_PKEY_set1_EC_KEY(public_key, public_ec_key);


  EC_KEY *temp_ec_key = EVP_PKEY_get0_EC_KEY(public_key);

  if(temp_ec_key == NULL) handleErrors();

  const EC_GROUP* group = EC_KEY_get0_group(temp_ec_key);
  point_conversion_form_t form = EC_GROUP_get_point_conversion_form(group);

  unsigned char* pub_key_buffer;
  size_t length = EC_KEY_key2buf(temp_ec_key, form, &pub_key_buffer, NULL);
  if(!length) handleErrors();
  ByteArray data(pub_key_buffer, length);

  OPENSSL_free(pub_key_buffer);
  EVP_PKEY_free(public_key);
  EC_KEY_free(ec_key);
  EC_KEY_free(public_ec_key);
  EC_POINT_free(ec_point);

  return data;
}

Функция извлечения приватного ключа:

ByteArray security::extractPrivateKey(EVP_PKEY* key_pair) {
  EC_KEY* ec_key = EVP_PKEY_get1_EC_KEY(key_pair);
  const BIGNUM* ec_priv = EC_KEY_get0_private_key(ec_key);
  int length = BN_bn2mpi(ec_priv, nullptr);
  ByteArray data(length);
  BN_bn2mpi(ec_priv, data.begin());
  return data;
}

Для получения пары ключей EVP из двух "сырых буферов" можно воспользоваться следующей функцией:

EVP_PKEY* security::getKeyPair(ByteArray priv_key_raw, ByteArray pub_key_raw) {
  EVP_PKEY* key_pair = EVP_PKEY_new();
  EC_KEY *ec_key = EC_KEY_new_by_curve_name(NID_X9_62_prime256v1);

  const EC_GROUP* ec_group = EC_KEY_get0_group(ec_key);
  EC_POINT* ec_point = EC_POINT_new(ec_group);
  EC_POINT_oct2point(ec_group, ec_point, pub_key_raw.begin(), pub_key_raw.length(), nullptr);
  EC_KEY_set_public_key(ec_key, ec_point);
  EC_POINT_free(ec_point);

  BIGNUM* priv = BN_mpi2bn(priv_key_raw.begin(), priv_key_raw.length(), nullptr);
  EC_KEY_set_private_key(ec_key, priv);
  BN_free(priv);

  EVP_PKEY_set1_EC_KEY(key_pair, ec_key);
  EC_KEY_free(ec_key);
  return key_pair;
}

Для хранения или передачи ключей не в бинарном, а в текстовом формате можно закодировать буффер с помощью кодировки base64 которая так же поддерживается в OpenSSL:

// Закодировать в base64
ByteArray security::encodeBase64(ByteArray decoded) {
  ByteArray encoded((4*((decoded.length()+2)/3)) + 1);
  EVP_EncodeBlock(encoded.begin(), decoded.begin(), decoded.length());
  return encoded;
}

// Декодировать из base64
ByteArray security::decodeBase64(ByteArray encoded) {
  ByteArray decoded((3*encoded.length()/4) + 1);
  size_t recived_data_size = EVP_DecodeBlock(decoded.begin(), encoded.begin(), encoded.length());
  if(recived_data_size < decoded.length())
    decoded.resize(recived_data_size);
  return decoded;
}

Получение общего секрета через протокол ECDH

Каждая из сторон сгенерировала по ключевой паре и успешно обменялась своими публичными ключами. Теперь каждая из сторон должна получить "общий секрет" хэш от которого будет использоваться как ключ AES 256. Для начала определим структуру ключа AES 256:

struct AES_t {
  // 32 байта для ключа
  uint8_t key[32] = {
    0,0,0,0,0,0,0,0,0,0,
    0,0,0,0,0,0,0,0,0,0,
    0,0,0,0,0,0,0,0,0,0,
    0,0
  };
  // 16 байт для вектора инициализации
  uint8_t init_vector[16] = {
    0,0,0,0,0,0,0,0,0,0,
    0,0,0,0,0,0
  };
  
  // В общей сумме размер ключа 48 байт или 384 бита
  
  // Проверка на пустоту ключа
  bool isEmpty() const {
    static const AES_t empty_aes;
    return !std::memcmp(this, &empty_aes, sizeof (AES_t));
  }
  // Очистить ключ
  void clear() {*this = AES_t();}
  // Конструктор по умолчанию
  AES_t() = default;
  // Конструктор из ByteArray
  AES_t(ByteArray data) {
    *this = *reinterpret_cast<AES_t*>(data.begin());
  }
}

Как мы видим структура для AES 256 ключа занимает 48 байт или 384 бита, а следовательно для получения AES 256 ключа из общего секрета подойдёт хэш-дайджест размером в 384 бит, то есть нам подходят такие алгоритмы хэширования как sha384(sha2) и sha3_384(ранее известен как Keccak). Не стоит воспринимать алгоритм sha3 как приемника алгоритма sha2, на текущий момент принято считать что оба алгоритма достаточно безопасны для использования, что не сказать про sha1. Оба этих алгоритма поддерживаются OpenSSL, но в данном случае всё-таки воспользуемся алгоритмом sha3_384.

AES_t security::getSecret(ByteArray peer_key, EVP_PKEY* key_pair) {
  EC_KEY *temp_ec_key = nullptr;
  EVP_PKEY *peerkey = nullptr;

  // Извлекаем полученный с другой стороны публичный ключ
  // из сырого буффера в EVP_PKEY*
  temp_ec_key = EC_KEY_new_by_curve_name(NID_X9_62_prime256v1);
  if(temp_ec_key == nullptr)
    handleErrors();
  if(EC_KEY_oct2key(temp_ec_key, peer_key.begin(), peer_key.length(), NULL) != 1)
    handleErrors();
  if(EC_KEY_check_key(temp_ec_key) != 1) handleErrors();
  peerkey = EVP_PKEY_new();
  if(peerkey == NULL)
    handleErrors();
  if(EVP_PKEY_assign_EC_KEY(peerkey, temp_ec_key)!= 1)
    handleErrors();

  // Получение общего секрета
  EVP_PKEY_CTX *derivation_ctx = EVP_PKEY_CTX_new(key_pair, NULL);
  EVP_PKEY_derive_init(derivation_ctx);
  EVP_PKEY_derive_set_peer(derivation_ctx, peerkey);
  size_t lenght;	// Размер общего секрета
  void* ptr;			// Указатель на буффер с общим секретом
  if(1 != EVP_PKEY_derive(derivation_ctx, NULL, &lenght)) handleErrors();
  if(NULL == (ptr = OPENSSL_malloc(lenght))) handleErrors();
  if(1 != (EVP_PKEY_derive(derivation_ctx, (unsigned char*)ptr, &lenght))) handleErrors();
  EVP_PKEY_CTX_free(derivation_ctx);
  EVP_PKEY_free(peerkey);

  // Хэшируем общий секрет и записываем в структуру AES_t
  AES_t aes_key;
  EVP_MD_CTX *mdctx;
  if((mdctx = EVP_MD_CTX_new()) == NULL)
    handleErrors();
  if(1 != EVP_DigestInit_ex(mdctx, EVP_sha384(), NULL))
    handleErrors();
  if(1 != EVP_DigestUpdate(mdctx, ptr, lenght))
    handleErrors();
  unsigned int length;
  if(1 != EVP_DigestFinal_ex(mdctx, (unsigned char*)&aes_key, &length))
    handleErrors();
  EVP_MD_CTX_free(mdctx);
  OPENSSL_free(ptr);
  return aes_key;
}

Шифрование и дешифрование с использованием AES 256

На текущий момент обе стороны имеют согласованный ключ AES 256 и теперь можно приступить непосредственно к шифрованию данных:

ByteArray security::encrypt(ByteArray plain_text, AES_t aes_struct) {
  // Рассчитываем длинну шифротекста
  ByteArray ciphertext(plain_text.length() % AES_BLOCK_SIZE == 0
                       ? plain_text.length()
                       : (plain_text.length() / AES_BLOCK_SIZE + 1) * AES_BLOCK_SIZE);

  // Инициализация контекста шифра
  EVP_CIPHER_CTX *ctx;
  if(!(ctx = EVP_CIPHER_CTX_new()))
    handleErrors();
  if(1 != EVP_EncryptInit_ex(ctx, EVP_aes_256_cbc(), nullptr, aes_struct.key, aes_struct.init_vector))
    handleErrors();

  // Шифрование исходного текста
  int f_length, s_length;
  if(1 != EVP_EncryptUpdate(ctx, ciphertext.begin(), &f_length, plain_text.begin(), plain_text.length()))
    handleErrors();

  // Иногда для записи шифротекста требутеся дополнительный AES блок
  if(uint64_t(f_length) == ciphertext.length())
    ciphertext.addSize(AES_BLOCK_SIZE);
  else if(uint64_t(f_length) > ciphertext.length())
    throw std::runtime_error("Predicted ciphertext size lower then actual!");

  // Запись последнего AES блока
  if(1 != EVP_EncryptFinal_ex(ctx, ciphertext.begin() + f_length, &s_length))
    handleErrors();
  
  // Уменьшение размера данных до размера записанного шифротекста
  if(uint64_t reuired_length = f_length + s_length; reuired_length < ciphertext.length())
    ciphertext.resize(f_length + s_length);
  else if(reuired_length > ciphertext.length())
    throw std::runtime_error("Predicted ciphertext size lower then actual!");

  // Высвобождения выделенной памяти для контекста шифра
  EVP_CIPHER_CTX_free(ctx);

  return ciphertext;
}

Дешифровка данных выглядит следующим образом:

ByteArray security::decrypt(ByteArray ciphertext, AES_t aes_struct) {
  ByteArray plain_text(ciphertext.length());

  // Инициализация контекста шифра
  EVP_CIPHER_CTX *ctx;
  if(!(ctx = EVP_CIPHER_CTX_new()))
    handleErrors();
  if(1 != EVP_DecryptInit_ex(ctx, EVP_aes_256_cbc(), NULL, aes_struct.key, aes_struct.init_vector))
    handleErrors();

  // Дешифровка шифротекста
  int f_length, s_length;
  if(1 != EVP_DecryptUpdate(ctx, plain_text.begin(), &f_length, ciphertext.begin(), ciphertext.length()))
    handleErrors();
  if(1 != EVP_DecryptFinal_ex(ctx, plain_text.begin() + f_length, &s_length))
    handleErrors();
  
  // Уменьшение размера буффера до размера полученных данных
  plain_text.resize(f_length + s_length);

  // Высвобождения выделенной памяти для контекста шифра
  EVP_CIPHER_CTX_free(ctx);

  return plain_text;
}

Пример использования

Ниже приведён простой пример использования вышеописанного кода:

#include "security.hpp"
#include <iostream>

int main(int argc, char* argv[]) {
  using namespace security;
  // Алиса генерирует ключ
  EVP_PKEY* alice_key_pair = genKey();
  // Алиса извлекает публичный ключ
  ByteArray alice_peer_key = extractPublicKey(alice_key_pair);
  
  // Боб генерирует ключ
  EVP_PKEY* bob_key_pair = genKey();
  // Боб извлекает публичный ключ
  ByteArray bob_peer_key = extractPublicKey(bob_key_pair);
  
  // Алиса и Боб обмениваются публичными ключами
  // через открытый канал передачи данных
  
  // Боб получает согласованный AES 256 ключ
  AES_t bob_aes_key = getSecret(alice_peer_key, bob_key_pair);
  
  // Алиса получает согласованный AES 256 ключ
  AES_t alice_aes_key = getSecret(bob_peer_key, alice_key_pair);
  
  // Алиса шифрует сообщение
  std::string alice_msg = "Hello, Bob";
  ByteArray alice_msg_buffer(alice_msg.data(), alice_msg.length() + 1);
  ByteArray alice_enc_msg = encrypt(alice_msg_buffer, alice_aes_key);
  // И передаёт зашифрованное сообщение по открытому каналу Бобу
  
  // Боб шифрует сообщение
  std::string bob_msg = "Hello, Alice";
  ByteArray bob_msg_buffer(bob_msg.data(), bob_msg.length() + 1);
  ByteArray bob_enc_msg = encrypt(bob_msg_buffer, bob_aes_key);
  // И передаёт зашифрованное сообщение по открытому каналу Алисе
  
  // Алиса получает и дешифровывет сообщение
  ByteArray alice_recived_msg = decrypt(bob_enc_msg, alice_aes_key);
  std::cout << "Bob: " << (char*)alice_recived_msg.begin() << '\n';
  
  // Боб получает и дешифровывет сообщение
  ByteArray bob_recived_msg = decrypt(alice_enc_msg, bob_aes_key);
  std::cout << "Alice: " << (char*)bob_recived_msg.begin() << '\n';

  return 0;
}

Исходный код представлен в этом репозитории GitHub.




Комментарии (8):