Генерируем X509 сертификат с OpenSSL C++

от автора

Генерируем собственный сертификат

Генерируем собственный сертификат

В данной статья я хочу рассказать, как работать с X509 сертификатом используя OpenSSL 3.0.0 в С++, начиная от генерации своего сертификата и заканчивая его валидацией.

Так как информации в интернете по этой теме почти нет, то все, что я вам расскажу, я узнал исходя из своего печального опыта работы с этой библиотекой. Я очень надеюсь, что эта статья окажется вам полезной и сможет сохранить ваше время.

В данной статье, я не буду рассказывать вам, что такое X509 сертификат, надеюсь, что это вы уже знаете, а если нет, то ссылка на статью вот тут.

Создаем сертификат

Начнем с простого, создания сертификата. Для начала нам необходимо подключить заголовочный файл <openssl/x509v3.h>. Тут все просто, для создания — вызываем X509_new(), а для очистки памяти — X509_free().

#include <openssl/x509v3.h> #include <iostream>  int main() {     std::unique_ptr<X509, decltype(&::X509_free)> certificate(X509_new(), ::X509_free);     if (certificate == nullptr) {         std::cerr << "Failed to create certificate" << std::endl;         return -1;     } }

Копируем сертификат

Если у нас уже имеется заполненная структура X509 сертификата, а нам нужно полностью ее скопировать в нашу переменную, то мы можем воспользоваться функцией X509_dup(), в которую нужно передать указатель на сертификат, который мы хотим скопировать.

#include <openssl/x509v3.h> #include <iostream>  int main() {     std::unique_ptr<X509, decltype(&::X509_free)> certificate(X509_new(), ::X509_free);     if (certificate == nullptr) {         std::cerr << "Failed to create certificate" << std::endl;         return -1;     }        std::unique_ptr<X509, decltype(&::X509_free)> duplicate(X509_dup(certificate.get()), ::X509_free);     if (duplicate == nullptr) {         std::cerr << "Failed to duplicate certificate" << std::endl;         return -1;     } }

Добавим Serial Number

Давайте добавим нашему сертификату пропертей, начнем с номера, он же serial number. С помощью X509_get_serialNumber получаем указатель на проперть сертификата, а далее, с помощью RAND_bytes генерируем уникальный серийный номер и выставляем его используя ASN1_STRING_set(). ASN1_STRING_set() вернет 1 на удачный вызов, 0 на завалившийся. Это правило работает для всех функций при работе с сертификатом.

bool setSerialNumber(X509* cert, uint32_t bytes) {     bool result = false;     ASN1_STRING* serialNumber = X509_get_serialNumber(cert);     if (serialNumber != nullptr && bytes != 0) {        std::vector<unsigned char> serial(bytes);         RAND_bytes(serial.data(), static_cast<int>(serial.size()));         if (ASN1_STRING_set(serialNumber, serial.data(), static_cast<int>(serial.size())) == 1) {             result = true;         }     }     return result; }

Выставляем версию

Выставим версию нашему сертификату, используя функцию X509_set_version(). На текущий момент есть три версии сертификата, которым соответствуют следующие значения:

  1. 0x0

  2. 0x1

  3. 0x2

bool setVersion(X509* cert, long version) {     return X509_set_version(cert, version) == 1; }

Выставляем Subject Name

Используя функцию X509_get_subject_name мы можем получить указатель на subject name нашего сертификата, а вызвав X509_NAME_add_entry_by_txt(), можем его обновить.

bool updateSubjectName(X509* cert, const char* key, const char* value) {     bool result = false;     X509_NAME* subjectName = X509_get_subject_name(cert);     if (subjectName != nullptr) {         const int res = X509_NAME_add_entry_by_txt(subjectName, key, MBSTRING_ASC, (unsigned char*)value, -1, -1, 0);         result = res == 1;     }     return result; }

Выставляем сроки валидности сертификата

Теперь добавим сроки валидности нашего сертификата, с какой по какую дату он будет работать. Для этого воспользуемся функциями X509_getm_notAfter() и X509_getm_notBefore(). Дальше в примерах я пропускаю обработку ошибок, но вы должны держать ее в голове и использовать в настоящем проекте.

bool setNotAfter(X509* cert, uint32_t y, uint32_t m, uint32_t d, int32_t offset_days) {     struct tm base;     memset(&base, 0, sizeof(base));     base.tm_year = y - 1900;     base.tm_mon = m - 1;     base.tm_mday = d;     time_t tm = mktime(&base);      bool result = false;     ASN1_STRING* notAfter = X509_getm_notAfter(cert);     if (notAfter != nullptr) {         X509_time_adj(notAfter, 86400L * offset_days, &tm);         result = true;     }     return result; }

Теперь добавим стартовую точку. Код получается точно таким же, как и для начальной, за исключением имени функции для получения проперти — X509_getm_notBefore()

bool setNotBefore(X509* cert, uint32_t y, uint32_t m, uint32_t d, int32_t offset_days) {     struct tm base;     memset(&base, 0, sizeof(base));     base.tm_year = y - 1900;     base.tm_mon = m - 1;     base.tm_mday = d;     time_t tm = mktime(&base);      bool result = false;     ASN1_STRING* notBefore = X509_getm_notBefore(cert);     if (notBefore != nullptr) {         X509_time_adj(notBefore, 86400L * offset_days, &tm);         result = true;     }     return result; }

Выставляем Issuer

Допустим, нам понадобилось выставить Issuer для нашего сертификата, тот сертификат, которым подписан наш. Для этого нужно использовать X509_set_issuer_name().

bool setIssuer(X509* cert, X509* issuer) {     bool result = false;     X509_NAME* subjectName = X509_get_subject_name(issuer);     if (subjectName != nullptr) {         result = X509_set_issuer_name(cert, subjectName) == 1;     }     return result; }

Добавить что-то в поле Issuer нашему сертификату тоже очень просто, можно сделать следующим образом, используя уже знакомую нам X509_NAME_add_entry_by_txt():

bool addIssuerInfo(X509* cert, const char* key, const char* value) {     bool result = false;     X509_NAME* issuerName = X509_get_issuer_name(cert);     if (issuerName != nullptr) {         result = X509_NAME_add_entry_by_txt(issuerName, key, MBSTRING_ASC, (unsigned char*)value, -1, -1, 0) == 1;     }     return result; }

Стандартные расширения

Добавим пару стандартных расширений (standart extensions) для нашего сертификата. Для стандартных расширений в OpenSSL существуют собственные ID (nid). Например, для Basic Constraints — это NID_basic_constraints, а для Key Usage — NID_key_usage. Эти айди необходимы, если мы хотим задать те или иные расширения для нашего сертификата.

bool addStandardExtension(X509* cert, X509* issuer, int nid, const char* value) {     X509V3_CTX ctx; // create context     X509V3_set_ctx_nodb(&ctx); // init context     X509V3_set_ctx(&ctx, issuer, cert, nullptr, nullptr, 0); // set context      std::unique_ptr<X509_EXTENSION, decltype(&::X509_EXTENSION_free)> ex(X509V3_EXT_conf_nid(nullptr, &ctx, nid, value), ::X509_EXTENSION_free);     if (ex != nullptr) {         return X509_add_ext(cert, ex.get(), -1) == 1;     }     return false; }

Кастомные расширения

Это все работает до тех пор, пока нам не понадобится добавить кастомное расширение, поддержку которых добавили в третьей версии сертификата. Тут все немного сложнее, но тоже выполнимо. Для начала создадим объект в базе данных OpenSSL, в который позднее запишем наше расширение. Теперь можно заняться подготовкой данных, создаем строку, которая будет хранить ключ и значение нашего расширения. Добавляем расширение в сертификат.

bool addCustomExtension(X509* cert, const char* key, const char* value, bool critical) {     const int nid = OBJ_create(key, value, nullptr);      std::unique_ptr<ASN1_OCTET_STRING, decltype(&::ASN1_OCTET_STRING_free)> data(ASN1_OCTET_STRING_new(), ::ASN1_OCTET_STRING_free);     int ret = ASN1_OCTET_STRING_set(data.get(), reinterpret_cast<unsigned const char*>(value), strlen(value));     if (ret != 1) {         return false;     }      std::unique_ptr<X509_EXTENSION, decltype(&::X509_EXTENSION_free)> ex(X509_EXTENSION_create_by_NID(nullptr, nid, critical, data.get()), ::X509_EXTENSION_free);     return X509_add_ext(cert, ex.get(), -1) == 1; }

Это довольно сложный и долгий путь, но, рабочий, как я выяснил на практике. Но если у вас есть более быстрый или эффективный вариант — прошу в комментарии.

Ну и какой же сертификат без публичного ключа? Давайте его добавим. Тут все очень просто, вызываем X509_set_pubkey() и готово. Если вам интересен процесс генерации и работы с публичными и приватным ключами в OpenSSL, то пишите комментарии.

Выставляем публичный ключ

Тут все просто, генерируем пару ключей — приватный и публичный, используя EVP_RSA_gen(). Подробнее о генерации ключей и работе с ними читайте в следующей статье.

Выставляем ключ с помощью X509_set_pubkey()

#include <openssl/evp.h> /// std::unique_ptr<EVP_PKEY, decltype(&::EVP_PKEY_free)> generateKeyPair(int32_t bits) {     std::unique_ptr<EVP_PKEY, decltype(&::EVP_PKEY_free)> key(EVP_RSA_gen(bits), ::EVP_PKEY_free);     return std::move(key); } /// bool setPublicKey(X509* cert, EVP_PKEY* key) {     return X509_set_pubkey(cert, key) == 1; }

Подписываем сертификат

И наконец, подписываем наш сертификат. Есть несколько популярных алгоритмов для подписи, но основной — это SHA256, он и приведен в примере. По нему можно понять процесс, а по его имени найти нужный в исходном коде OpenSSL.

signCert(certificate.get(), keyPair.get(), EVP_sha256()); /// bool signCert(X509* cert, EVP_PKEY* key, const EVP_MD* algo) {     return X509_sign(cert, key, algo) != 0; }

Сохраняем сертификат в файл

Подробнее о работе с BIO будет написано в следующей статье, пока что не будем на этом останавливаться. Создаем био BIO_new(BIO_s_file(), с помощью функции BIO_write_filename создаем файл и используя PEM_write_bio_X509() сохраняем сертификат в файл в формате PEM.

#include <openssl/pem.h> /// bool saveCertToPemFile(X509* cert, const std::string& file) {     bool result = false;     std::unique_ptr<BIO, decltype(&::BIO_free)> bio(BIO_new(BIO_s_file()), ::BIO_free);     if (bio != nullptr) {         if (BIO_write_filename(bio.get(), const_cast<char*>(file.c_str())) > 0) {             result = PEM_write_bio_X509(bio.get(), cert) == 1;         }     }     return result; }
Полный код примера
#include <openssl/x509v3.h> #include <openssl/evp.h> #include <openssl/pem.h>  #include <memory> #include <iostream>  bool setSerialNumber(X509* cert, uint32_t bytes) {     bool result = false;     ASN1_STRING* serialNumber = X509_get_serialNumber(cert);     if (serialNumber != nullptr && bytes != 0) {        std::vector<unsigned char> serial(bytes);         RAND_bytes(serial.data(), static_cast<int>(serial.size()));         if (ASN1_STRING_set(serialNumber, serial.data(), static_cast<int>(serial.size())) == 1) {             result = true;         }     }     return result; }  bool setVersion(X509* cert, long version) {     return X509_set_version(cert, version) == 1; }  bool updateSubjectName(X509* cert, const char* key, const char* value) {     bool result = false;     X509_NAME* subjectName = X509_get_subject_name(cert);     if (subjectName != nullptr) {         const int res = X509_NAME_add_entry_by_txt(subjectName, key, MBSTRING_ASC, (unsigned char*)value, -1, -1, 0);         result = res == 1;     }     return result; }  bool setNotAfter(X509* cert, uint32_t y, uint32_t m, uint32_t d, int32_t offset_days) {     struct tm base;     memset(&base, 0, sizeof(base));     base.tm_year = y - 1900;     base.tm_mon = m - 1;     base.tm_mday = d;     time_t tm = mktime(&base);      bool result = false;     ASN1_STRING* notAfter = X509_getm_notAfter(cert);     if (notAfter != nullptr) {         X509_time_adj(notAfter, 86400L * offset_days, &tm);         result = true;     }     return result; }  bool setNotBefore(X509* cert, uint32_t y, uint32_t m, uint32_t d, int32_t offset_days) {     struct tm base;     memset(&base, 0, sizeof(base));     base.tm_year = y - 1900;     base.tm_mon = m - 1;     base.tm_mday = d;     time_t tm = mktime(&base);      bool result = false;     ASN1_STRING* notBefore = X509_getm_notBefore(cert);     if (notBefore != nullptr) {         X509_time_adj(notBefore, 86400L * offset_days, &tm);         result = true;     }     return result; }  bool setPublicKey(X509* cert, EVP_PKEY* key) {     return X509_set_pubkey(cert, key) == 1; }  bool signCert(X509* cert, EVP_PKEY* key, const EVP_MD* algo) {     return X509_sign(cert, key, algo) != 0; }  bool saveCertToPemFile(X509* cert, const std::string& file) {     bool result = false;     std::unique_ptr<BIO, decltype(&::BIO_free)> bio(BIO_new(BIO_s_file()), ::BIO_free);     if (bio != nullptr) {         if (BIO_write_filename(bio.get(), const_cast<char*>(file.c_str())) > 0) {             result = PEM_write_bio_X509(bio.get(), cert) == 1;         }     }     return result; }  std::unique_ptr<EVP_PKEY, decltype(&::EVP_PKEY_free)> generateKeyPair(int32_t bits) {     std::unique_ptr<EVP_PKEY, decltype(&::EVP_PKEY_free)> key(EVP_RSA_gen(bits), ::EVP_PKEY_free);     return std::move(key); }  bool addCustomExtension(X509* cert, const char* key, const char* value, bool critical) {     const int nid = OBJ_create(key, value, nullptr);      std::unique_ptr<ASN1_OCTET_STRING, decltype(&::ASN1_OCTET_STRING_free)> data(ASN1_OCTET_STRING_new(), ::ASN1_OCTET_STRING_free);     int ret = ASN1_OCTET_STRING_set(data.get(), reinterpret_cast<unsigned const char*>(value), strlen(value));     if (ret != 1) {         return false;     }      std::unique_ptr<X509_EXTENSION, decltype(&::X509_EXTENSION_free)> ex(X509_EXTENSION_create_by_NID(nullptr, nid, critical, data.get()), ::X509_EXTENSION_free);     return X509_add_ext(cert, ex.get(), -1) == 1; }  bool addStandardExtension(X509* cert, X509* issuer, int nid, const char* value) {     X509V3_CTX ctx; // create context     X509V3_set_ctx_nodb(&ctx); // init context     X509V3_set_ctx(&ctx, issuer, cert, nullptr, nullptr, 0); // set context      std::unique_ptr<X509_EXTENSION, decltype(&::X509_EXTENSION_free)> ex(X509V3_EXT_conf_nid(nullptr, &ctx, nid, value), ::X509_EXTENSION_free);     if (ex != nullptr) {         return X509_add_ext(cert, ex.get(), -1) == 1;     }     return false; }  bool setIssuer(X509* cert, X509* issuer) {     bool result = false;     X509_NAME* subjectName = X509_get_subject_name(issuer);     if (subjectName != nullptr) {         result = X509_set_issuer_name(cert, subjectName) == 1;     }     return result; }  bool addIssuerInfo(X509* cert, const char* key, const char* value) {     bool result = false;     X509_NAME* issuerName = X509_get_issuer_name(cert);     if (issuerName != nullptr) {         result = X509_NAME_add_entry_by_txt(issuerName, key, MBSTRING_ASC, (unsigned char*)value, -1, -1, 0) == 1;     }     return result; }  int main() {     std::unique_ptr<X509, decltype(&::X509_free)> certificate(X509_new(), ::X509_free);     if (certificate == nullptr) {         std::cerr << "Failed to create certificate" << std::endl;         return -1;     }      const unt32_t serialNum = 20;     bool res = setSerialNumber(certificate.get(), serialNum);     if (!res) {         std::cerr << "Failed to setSerialNumber" << std::endl;         return -1;     }      const long ver = 0x0; // version 1     res = setVersion(certificate.get(), ver);     if (!res) {         std::cerr << "Failed to setVersion" << std::endl;         return -1;     }      static constexpr const char* key = "CN";     static constexpr const char* value = "Common Name";     res = updateSubjectName(certificate.get(), key, value);     if (!res) {         std::cerr << "Failed to updateSubjectName" << std::endl;         return -1;     }      const uint32_t y = 2022;     const uint32_t m = 12;     const uint32_t d = 25;     const int32_t offset_days = 0;     res = setNotAfter(certificate.get(), y, m, d, offset_days);     if (!res) {         std::cerr << "Failed to setNotAfter" << std::endl;         return -1;     }      res = setNotBefore(certificate.get(), y, m, d, offset_days);     if (!res) {         std::cerr << "Failed to setNotBefore" << std::endl;         return -1;     }      const int32_t bits = 2048;     std::unique_ptr<EVP_PKEY, decltype(&::EVP_PKEY_free)> keyPair = generateKeyPair(bits);     res = setPublicKey(certificate.get(), keyPair.get());     if (!res) {         std::cerr << "Failed to setPublicKey" << std::endl;         return -1;     }      const int nid = NID_basic_constraints;     static const char* extensionValue = "critical,CA:TRUE";      res = addStandardExtension(certificate.get(), nullptr, nid, extensionValue);     if (!res) {         std::cerr << "Failed to addStandardExtension" << std::endl;         return -1;     }      res = addCustomExtension(certificate.get(), "1.2.3", "myvalue", false);     if (!res) {         std::cerr << "Failed to addCustomExtension" << std::endl;         return -1;     }      res = signCert(certificate.get(), keyPair.get(), EVP_sha256());     if (!res) {         std::cerr << "Failed to signCert" << std::endl;         return -1;     }      std::unique_ptr<X509, decltype(&::X509_free)> duplicate(X509_dup(certificate.get()), ::X509_free);     if (duplicate == nullptr) {         std::cerr << "Failed to duplicate certificate" << std::endl;         return -1;     }      res = setIssuer(certificate.get(), duplicate.get());     if (!res) {         std::cerr << "Failed to setIssuer" << std::endl;         return -1;     }      res = addIssuerInfo(certificate.get(), key, value);     if (!res) {         std::cerr << "Failed to addIssuerInfo" << std::endl;         return -1;     }      res = signCert(certificate.get(), keyPair.get(), EVP_sha256());     if (!res) {         std::cerr << "Failed to signCert" << std::endl;         return -1;     }      static const std::string filename = "certificate.pem";     res = saveCertToPemFile(certificate.get(), filename);     if (!res) {         std::cerr << "Failed to saveCertToPemFile" << std::endl;         return -1;     } }

Послесловие

Если вам понравилась данная статья и вы хотите увидеть продолжение, описывающее работу с ключами и стораджем сертификатов, а также научиться их валидировать — ставьте лайки и оставляйте комментарии.

Надеюсь, данная статья будет вам полезна и сохранит вам кучу времени, избавив от часов чтения скудной документации и редких мануалов.

UPD: Добавил примеры кода на гитхаб, буду обновлять по мере написания статей

Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.

Хотите продолжения про ключи, шифрование и валидацию сертификатов?

78.05% Да32
12.2% Нет5
9.76% Наверное4

Проголосовал 41 пользователь. Воздержались 4 пользователя.

ссылка на оригинал статьи https://habr.com/ru/articles/730852/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *