
В данной статья я хочу рассказать, как работать с X509 сертификатом используя OpenSSL 3.0.0 в С++, начиная от генерации своего сертификата и заканчивая его валидацией.
Так как информации в интернете по этой теме почти нет, то все, что я вам расскажу, я узнал исходя из своего печального опыта работы с этой библиотекой. Я очень надеюсь, что эта статья окажется вам полезной и сможет сохранить ваше время.
В данной статье, я не буду рассказывать вам, что такое X509 сертификат, надеюсь, что это вы уже знаете, а если нет, то ссылка на статью вот тут.
Создаем сертификат
Начнем с простого, создания сертификата. Для начала нам необходимо подключить заголовочный файл <openssl/x509v3.h>. Тут все просто, для создания - вызываем X509_new(), а для очистки памяти - X509_free().
#include <openssl/x509v3.h>
int main() {
X509* certificate = X509_new();
if (certificate != nullptr) {
X509_free(certificate);
}
}Если у нас уже имеется заполненная структура X509 сертификата, а нам нужно полностью ее скопировать в нашу переменную, то мы можем воспользоваться функцией X509_dup(), в которую нужно передать указатель на сертификат, который мы хотим скопировать.
#include <openssl/x509v3.h>
int main() {
X509* certificate = X509_new();
if (certificate != nullptr) {
X509_free(certificate);
}
}Копируем сертификат
#include <openssl/x509v3.h>
int main() {
X509* certificate = X509_new();
if (certificate != nullptr) {
X509* duplicate = X509_dup(certificate);
X509_free(duplicate);
X509_free(certificate);
}
}Давайте добавим нашему сертификату пропертей, начнем с номера, он же serial number. С помощью X509_get_serialNumber получаем указатель на проперть сертификата, а далее, с помощью ASN1_INTEGER_set() выставляем необходимое значение. ASN1_INTEGER_set() вернет 1 на удачный вызов, 0 на завалившийся. Это правило работает для всех функций при работе с сертификатом.
#include <iostream>
#include <openssl/x509v3.h>
int main() {
X509* certificate = X509_new();
if (certificate != nullptr) {
int32_t serialNum = 1;
ASN1_INTEGER* serialNumber = X509_get_serialNumber(certificate);
if (serialNumber != nullptr) {
const int res = ASN1_INTEGER_set(serialNumber, serial);
if (res != 1) {
std::cerr << "ASN1_INTEGER_set failed" << std::endl;
}
}
X509_free(certificate);
}
}Выставляем версию
Выставим версию нашему сертификату, используя функцию X509_set_version(). На текущий момент есть три версии сертификата, которым соответствуют следующие значения:
0x0
0x1
0x2
Выставляем Subject Name
Добавим Country name для нашего сертификата. Используя функцию X509_get_subject_name мы можем получить указатель на subject name нашего сертификата, а вызвав X509_NAME_add_entry_by_txt(), можем его обновить.
#include <iostream>
#include <openssl/x509v3.h>
int main() {
X509* certificate = X509_new();
if (certificate != nullptr) {
int32_t serialNum = 1;
ASN1_INTEGER* serialNumber = X509_get_serialNumber(certificate);
if (serialNumber != nullptr) {
int res = ASN1_INTEGER_set(serialNumber, serial);
if (res != 1) {
std::cerr << "ASN1_INTEGER_set failed" << std::endl;
}
}
const long ver = 0x0; // version 1
int res = X509_set_version(certificate, ver);
if (res != 1) {
std::cerr << "X509_set_version failed" << std::endl;
}
static const char* name = "Country name";
509_NAME* subjectName = X509_get_subject_name(certificate);
if (subjectName != nullptr) {
res = X509_NAME_add_entry_by_txt(subjectName, "CN", MBSTRING_ASC, (unsigned char*)name, -1, -1, 0)
if (res != 1) {
std::cerr << "X509_NAME_add_entry_by_txt failed" << std::endl;
}
}
X509_free(certificate);
}
}Используя X509_NAME_add_entry_by_txt мы можем добавлять любые ключ-значения к нашему subject name:
#include <iostream>
#include <openssl/x509v3.h>
int main() {
X509* certificate = X509_new();
if (certificate != nullptr) {
int32_t serialNum = 1;
ASN1_INTEGER* serialNumber = X509_get_serialNumber(certificate);
if (serialNumber != nullptr) {
int res = ASN1_INTEGER_set(serialNumber, serial);
if (res != 1) {
std::cerr << "ASN1_INTEGER_set failed" << std::endl;
}
}
const long ver = 0x0; // version 1
int res = X509_set_version(certificate, ver);
if (res != 1) {
std::cerr << "X509_set_version failed" << std::endl;
}
static const char* key = "My Key";
static const char* value = "Value";
509_NAME* subjectName = X509_get_subject_name(certificate);
if (subjectName != nullptr) {
res = X509_NAME_add_entry_by_txt(subjectName, key, MBSTRING_ASC, (unsigned char*)value, -1, -1, 0)
if (res != 1) {
std::cerr << "X509_NAME_add_entry_by_txt failed" << std::endl;
}
}
X509_free(certificate);
}
}Выставляем сроки валидности сертификата
Теперь добавим сроки валидности нашего сертификата, с какой по какую дату он будет работать. Для этого воспользуемся функциями X509_getm_notAfter() и X509_getm_notBefore(). Дальше в примерах я пропускаю обработку ошибок, но вы должны держать ее в голове и использовать в настоящем проекте.
#include <iostream>
#include <ctime>
#include <openssl/x509v3.h>
int main() {
X509* certificate = X509_new();
if (certificate != nullptr) {
uint32_t y = 2023;
uint32_t m = 12;
uint32_t d = 25;
int32_t offset_days = 0;
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);
X509_time_adj(X509_getm_notAfter(&x), 86400L * offset_days, &tm);
X509_free(certificate);
}
}Таким образом, мы сказали, что наш сертификат будет работать начиная с 2023.12.25. Теперь добавим стартовую точку. Код получается точно таким же, как и для начальной, за исключением имени функции для получения проперти - X509_getm_notBefore(). Итак, наш сертификат валиден с 2022.12.25
#include <iostream>
#include <ctime>
#include <openssl/x509v3.h>
int main() {
X509* certificate = X509_new();
if (certificate != nullptr) {
uint32_t y = 2022;
uint32_t m = 12;
uint32_t d = 25;
int32_t offset_days = 0;
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);
X509_time_adj(X509_getm_notBefore(&x), 86400L * offset_days, &tm);
X509_free(certificate);
}
}Выставляем Issuer
Допустим, нам понадобилось выставить Issuer для нашего сертификата, тот сертификат, которым подписан наш. Для этого нужно использовать X509_set_issuer_name().
#include <iostream>
#include <openssl/x509v3.h>
int main() {
X509* rootCertificate = X509_new();
///
X509* certificate = X509_new();
if (certificate != nullptr) {
if (rootCertificate != nullptr) {
X509_set_issuer_name(certificate, X509_get_subject_name(rootCertificate));
X509_free(rootCertificate);
}
X509_free(certificate);
}
}Допустим, нам понадобилось добавить что-то в поле Issuer нашему сертификату, это тоже можно сделать следующим образом, используя уже знакомую нам X509_NAME_add_entry_by_txt():
#include <iostream>
#include <openssl/x509v3.h>
int main() {
X509* certificate = X509_new();
if (certificate != nullptr) {
static const char* key = "My Key";
static const char* value = "Value";
X509_NAME* issuerName = X509_get_issuer_name(certificate);
X509_NAME_add_entry_by_txt(issuerName, key, MBSTRING_ASC, (unsigned char*)value, -1, -1, 0) != 1) {
X509_free(certificate);
}
}Стандартные расширения
Добавим пару стандартных расширений (standart extensions) для нашего сертификата. Для стандартных расширений в OpenSSL существуют собственные ID (nid). Например, для Basic Constraints - это NID_basic_constraints, а для Key Usage - NID_key_usage. Эти айди необходимы, если мы хотим задать те или иные расширения для нашего сертификата.
#include <iostream>
#include <openssl/x509v3.h>
int main() {
X509* rootCertificate = X509_new();
///
X509* certificate = X509_new();
if (certificate != nullptr) {
X509V3_CTX ctx; // create context
X509V3_set_ctx_nodb(&ctx); // init context
X509V3_set_ctx(&ctx, &issuer, &x, nullptr, nullptr, 0); // set context
int nid = NID_basic_constraints;
static const char* value = "critical,CA:TRUE";
X509_EXTENSION* ex = X509V3_EXT_conf_nid(nullptr, &ctx, nid, value);
if (ex != nullptr) {
const int ret = X509_add_ext(&x, ex.get(), -1);
if (ret != 1) {
std::cerr << "Failed to add extension" << std::endl;
}
X509_EXTENSION_free(ex);
}
X509_free(rootCertificate);
X509_free(certificate);
}
}Кастомные расширения
Это все работает до тех пор, пока нам не понадобится добавить кастомное расширение, поддержку которых добавили в третьей версии сертификата. Тут все немного сложнее, но тоже выполнимо. Для начала создадим объект в базе данных OpenSSL, в который позднее запишем наше расширение. По его nid получим сам объект, создадим пустое расширение, свяжем его и наш объект. Затем, выставим критичность расширения. Теперь можно заняться подготовкой данных, создаем строку, которая будет хранить ключ и значение нашего расширения. Присваиваем ее расширению и наконец, добавляем расширение в сертификат.
#include <iostream>
#include <openssl/x509v3.h>
int main() {
X509* certificate = X509_new();
if (certificate != nullptr) {
// create object in OpenSSL
const int nid = OBJ_create(key, value, nullptr);
ASN1_OBJECT* obj = OBJ_nid2obj(nid);
// create empty extenstion
X509_EXTENSION* ex = X509_EXTENSION_new();
// set object to extension
int res = X509_EXTENSION_set_object(ex, obj);
if (res == 1) {
// set critical for our extension
res = X509_EXTENSION_set_critical(ex, 0);
if (res == 1) {
// create string for our data
ASN1_OCTET_STRING* data = ASN1_OCTET_STRING_new();
static const char* key = "My Key";
static const char* value = "Value";
// set key value pair
res = ASN1_OCTET_STRING_set(data, reinterpret_cast<unsigned const char*>(value), strlen(value));
if (res == 1) {
// set data to our extension
res = X509_EXTENSION_set_data(ex, data);
if (res == 1) {
// finally, add custom extension
res = X509_add_ext(&x, ex, -1);
if (res != 1) {
std::cerr << "Failed to add extension" << std::endl;
}
}
}
}
}
X509_free(certificate);
}
}Это довольно сложный и долгий путь, но, рабочий, как я выяснил на практике. Но если у вас есть более быстрый или эффективный вариант - прошу в комментарии.
Ну и какой же сертификат без публичного ключа? Давайте его добавим. Тут все очень просто, вызываем X509_set_pubkey() и готово. Если вам интересен процесс генерации и работы с публичными и приватным ключами в OpenSSL, то пишите комментарии.
Выставляем публичный ключ
#include <iostream>
#include <openssl/x509v3.h>
#include <openssl/evp.h>
int main() {
/// load public key from somewhere
EVP_PKEY key;
///
X509* certificate = X509_new();
if (certificate != nullptr) {
X509_set_pubkey(certificate, &key);
X509_free(certificate);
}
}Подписываем сертификат
И наконец, подписываем наш сертификат. Есть несколько популярных алгоритмов для подписи, но основной - это SHA256, он и приведен в примере. По нему можно понять процесс, а по его имени найти нужный в исходном коде OpenSSL.
#include <iostream>
#include <openssl/x509v3.h>
#include <openssl/evp.h>
int main() {
/// load private key from somewhere
EVP_PKEY key;
///
X509* certificate = X509_new();
if (certificate != nullptr) {
X509_sign(certificate, &key, EVP_sha256());
X509_free(certificate);
}
}Послесловие
На последок, я бы сказал, что для работы с сишными функциями и структурами неплохо подходит std::unique_ptr, который сильно упростит вам жизнь и позволит не забыть освободить ресурсы. Я специально не использовал эту конструкцию в примере, чтобы не загромождать код, но в реальной жизни, лучше использовать их.
std::unique_ptr<X509_EXTENSION, decltype(&::X509_EXTENSION_free)> ex(X509_EXTENSION_new(), ::X509_EXTENSION_free);Если вам понравилась данная статья и вы хотите увидеть продолжение, описывающее работу с ключами и стораджем сертификатов, а также научиться их валидировать - ставьте лайки и оставляйте комментарии.
Надеюсь, данная статья будет вам полезна и сохранит вам кучу времени, избавив от часов чтения скудной документации и редких мануалов.
