В данной статья я хочу рассказать, как работать с 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 получаем указатель на проперть сертификата, а далее, с помощью ASN1_INTEGER_set() выставляем необходимое значение. ASN1_INTEGER_set() вернет 1 на удачный вызов, 0 на завалившийся. Это правило работает для всех функций при работе с сертификатом.
bool setSerialNumber(X509* cert, int32_t serial) {
bool result = false;
ASN1_INTEGER* serialNumber = X509_get_serialNumber(cert);
if (serialNumber != nullptr) {
const int res = ASN1_INTEGER_set(serialNumber, serial);
result = res == 1;
}
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, int32_t serial) {
bool result = false;
ASN1_INTEGER* serialNumber = X509_get_serialNumber(cert);
if (serialNumber != nullptr) {
const int res = ASN1_INTEGER_set(serialNumber, serial);
result = res == 1;
}
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 int32_t serialNum = 1;
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: Добавил примеры кода на гитхаб, буду обновлять по мере написания статей
Комментарии (24)
lrrr11
22.04.2023 01:59Это довольно сложный и долгий путь, но, рабочий, как я выяснил на практике. Но если у вас есть более быстрый или эффективный вариант - прошу в комментарии.
`X509_EXTENSION_create_by_NID`, по сути все из этого сниппета в одной функции.
А вообще код сложный, потому что вся эта предметная область (сертификаты X509 и т.п.) сложная. Спецификации на десятки и сотни страниц, ничего не поделаешь.
А у openssl на самом деле вполне себе единообразный API. Если к нему привыкнуть, то потом проблем не будет, а вместо документации лучше читать исходники. Причем не самого openssl, а boringssl - там все очень сильно упростили внутри.
Dmitri-D
22.04.2023 01:59+3как же так? Это не С++, а простой старый С. Где владение ресурсами? Где гарантия освобождения ресурсов при исключениях? Где умные указатели?
Witcher136 Автор
22.04.2023 01:59Все есть, но как я и написал, чтобы не загромождать пример, умные указатели я оставил на конец. Где и написал, как их можно использовать: std::unique_ptr<X509_EXTENSION, decltype(&::X509_EXTENSION_free)> ex(X509_EXTENSION_new(), ::X509_EXTENSION_free); Моей главной задачей было показать последовательность вызовов, которая поможет решить человеку проблему. Красоту он сможет навести позже, когда у него наконец-то все заработает :)
Dmitri-D
22.04.2023 01:59+1ну это был сарказм, я не против и С, сам использую с 80х. Но С++ требует другого подхода, спасибо что проапдйтили.
Я использую openssl довольно давно, хотя переход на 3.х только в планах на это лето. Что касается документации - увы, она хромает. Но, если вы находите публикации или примеры как использовать сам openssl бинарник, то несложно раскрутить что он внутри делает, благо вся коммуникация с остальной частью идет по тому же API. И вообще примеры в openssl вполне вполне полезные.
Witcher136 Автор
22.04.2023 01:59Обновил примеры :)
Dmitri-D
22.04.2023 01:59а как насчет длинных серийников? Они там байт 20, а вовсе не int32_t. Нужно бы через BN.
Witcher136 Автор
22.04.2023 01:59Спасибо за замечание, в моем проекте просто таких длинных не встречается. Обновлю пример, чтобы можно было для всех использовать:)
Dmitri-D
22.04.2023 01:59как насчет использования C++17 std::filesystem для сохранения в файл?
Witcher136 Автор
22.04.2023 01:59На работе просто C++11, поэтому я еще не работал с 17. А чем плохи вызовы самой библиотеки? BIO_write_filename() и PEM_write_bio_X509()?
Dmitri-D
22.04.2023 01:59перед подписью, openssl, во всяком случае в версии 1.1, еще ставит NID_subject_key_identifier в "hash" и NID_authority_key_identifier в "keyid:always" и для CA ключей делает NID_key_usage в "keyCertSign,cRLSign", а для остальных в "digitalSignature,keyEncipherment"
Evengard
22.04.2023 01:59+2Ещё могу посоветовать https://lapo.it/asn1js/ для просмотра контента сгенеренного сертификата (или даже ключа, или чего-то ещё в asn1/pem кодировке) если у вас с ним что-то не так. Сильно помогает иногда. Меня спасло от сумасшедствия когда я не мог понять почему сгенеренный мной сертификат Фаерфокс упрямо не желает импортировать, хотя такой же сгенеренный командной строкой openssl работал безо всяких проблем...
Mingun
22.04.2023 01:59Только зачем версию сертификату самому ставить? Она вычисляется по правилам спецификации, чтобы парсер знал, что дальше парсить. Уж хотя бы это поле библиотека должна самостоятельно генерировать. BouncyCastle так и делает в своем low-level API.
Witcher136 Автор
22.04.2023 01:59Может быть я чего-то не знаю, но когда я добавлял расширения, которые есть только в v3 у меня все равно выставлялась версия 1
BackLaN
22.04.2023 01:59Версия должна быть == 2. Там вообще все просто делается и примеров куча в самой OpenSSL. В версиях 1.0.x был пример ~/demos/x509/mkcert.c , потом примеры убрали, но там ничего особо не поменялось в вызовах. Ну и код в ~/apps/x509.c как был так и остался, там больше всего, но тоже все просто. Весь код для генерации сертификата с ключами и подписью можно было уложить в 100 строк или меньше, у вас куча кусков кода повставлено и результат по итогу практически нулевой.
Dmitri-D
22.04.2023 01:59So X509_VERSION_3 has value 2
https://www.openssl.org/docs/man3.1/man3/X509_get_version.html
Evengard
А я-то думал в BouncyCastle заморочно. OpenSSL вообще очень low level API имеет, в очередной раз убеждаюсь.
Witcher136 Автор
Все так) и документация оставляет желать лучшего :) Поэтому я и решил написать статью, чтобы помочь остальным в этом нелегком пути познания опенссл
Mingun
В BouncyCastle тоже low level API есть, и к слову, если просто смотреть в спецификации, то даже без документации все достаточно понятно — там просто прямолинейное отображение ASN.1 структур. Вот их high level API действительно не понять, как использовать и зачем он вообще нужен. Мой issue полностью игнорируют.
Evengard
Вы его в Java мире используете... В C# мире документации нет вообще =) Официальный ответ "смотрите исходники или документацию на Java версию".
API в принципе не такое плохое, но местами конечно особенное.