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

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.
Генерируем собственный сертификат
Генерируем собственный сертификат

В данной статья я хочу рассказать, как работать с 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(). На текущий момент есть три версии сертификата, которым соответствуют следующие значения:

  1. 0x0

  2. 0x1

  3. 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);

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

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

Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.
Хотите продолжения про ключи, шифрование и валидацию сертификатов?
100% Да 1
0% Нет 0
0% Наверное 0
Проголосовал 1 пользователь. Воздержался 1 пользователь.
Источник: https://habr.com/ru/articles/730852/


Интересные статьи

Интересные статьи

Знакомый айтишник собрался подаваться на британскую визу. Среди прочих документов для подачи на Skilled Worker visa потребовался сертификат В1, которого у него не было, и надо было срочно готовиться к...
По статистике, 91% кибератак начинается с фишинга. Жертва получает письмо с поддельным именем и адресом. Это основная уязвимость, которую хакеры эффективно используют уже сорок лет, с момента изоб...
SSL-сертификат является обязательным элементом информационной защиты почтового сервера Zimbra OSE. Он позволяет устанавливать защищенное соединение с сервером и обеспечить шифрование данных, чтобы иск...
Поставив и настроив комьюнити версию этого пакета я столкнулся с тем что нет официальных рекомендаций как сгенерировать и запустить работу этого комплекса по защищенному ...
Всем привет! Все кто используют продукты vmware, знают или не знаю, что проблема с сертификатами может принести много боли и страдания. Поэтому чтобы нам не наступить на эти грабли я п...