![Генерируем собственный сертификат Генерируем собственный сертификат](https://habrastorage.org/getpro/habr/upload_files/612/884/6c4/6128846c40172146473de0ad918e85fe.png)
В данной статья я хочу рассказать, как работать с 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(). На текущий момент есть три версии сертификата, которым соответствуют следующие значения:
-
0x0
-
0x1
-
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: Добавил примеры кода на гитхаб, буду обновлять по мере написания статей
ссылка на оригинал статьи https://habr.com/ru/articles/730852/
Добавить комментарий