Данная статья является логическим продолжением статьи про генерацию сертификата. В ней мы рассмотрим, как мы можем провалидировать сертификат используя OpenSSL и C++.

В OpenSSL, для работы с сертификатами используется понятие storage. Давайте создадим его используя X509_STORE_new();

std::unique_ptr<X509_STORE, decltype(&::X509_STORE_free)> store(
  X509_STORE_new(), &::X509_STORE_free);

Далее, нам нужно "заполнить" наше хранилище сертификатами, которыми мы планируем проверять наш целевой сертификат. Если наш сертификат подписан сертификатом, который уже находится в trusted storage, то нам достаточно вызвать лишь одну функцию - X509_STORE_load_path(). В нее необходимо передать указатель на наш сторадж, а также полный путь к сертификатам. Для линукса такой путь может быть: /etc/ssl/certs.

bool addCAPath(X509_STORE* store, const char* path) {
    return X509_STORE_load_path(store, path) == 1;
}

Также, в OpenSSL есть возможность загрузить файл с несколькими сертификатами, используя функцию X509_STORE_load_file(). В нее также передаем указатель на сторадж, а также имя файла. Например, /etc/ssl/certs/ca-certificates.crt.

bool addCABundle(X509_STORE* store, const char* path) {
    X509_STORE_load_file(store, path) == 1;
}

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

bool addCert(X509_STORE* store, X509* cert) {
    return X509_STORE_add_cert(store, cert) == 1;
}

Далее, мы можем начинать процесс валидации сертификата. Для начала, нам необходимо сконструировать контекcт X509_CTX. К слову сказать, почти все новое АПИ OpenSSL 3.0.0 строится вокруг различных контекстов. Чтобы не мучиться с управлением ресурсами, создадим нашему контексту специальный делитор, состоящий из вызова двух функций: очистить контекст и освободить память под контекст.

auto storeContextDeleter = [](X509_STORE_CTX* ctx) {
    X509_STORE_CTX_cleanup(ctx);
    X509_STORE_CTX_free(ctx);
};

std::unique_ptr<X509_STORE_CTX, decltype(storeContextDeleter)>
        storeCtx(X509_STORE_CTX_new(), storeContextDeleter);
if (storeCtx == nullptr) {
    std::cerr << "Failed X509_STORE_CTX_new" << std::endl;
    return -1;
}

Итак, мы создали контекст, далее, мы можем выставить колбек, который будет вызываться OpenSSL после валидации сертификата. Вы спросите - зачем это нужно, а нужно это затем, что в этом колбеке мы можем подавить различные ошибки OpenSSL, чтобы валидация считалась успешной. Например, если OpenSSL вернул нам в этом колбеке ошибку - X509_V_ERR_CERT_HAS_EXPIRED, что значит, что сертификат уже "протух", но мы по своим причинам хотим разрешить такое непотребство, то мы можем на нее просто вернуть OK.

static int verifyCallback(int ok, X509_STORE_CTX *ctx) {
    const int err = X509_STORE_CTX_get_error(ctx);
    if (err != 0) {
        std::cerr << "Failed to verify cert " << err << std::endl;
    }
    return ok;
}
///
X509_STORE_set_verify_cb_func(store.get(), verifyCallback);

Далее, инициализируем наш контекст и вызываем функцию валидации:

if (X509_STORE_CTX_init(storeCtx.get(), store.get(), cert.get(), nullptr) == 0) {
    std::cerr << "Failed X509_STORE_CTX_init" << std::endl;
    return -1;
}

if (X509_verify_cert(storeCtx.get()) != 1) {
    std::cerr << "Failed X509_verify_cert" << std::endl;
    return -1;
}

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

Комментарии (6)


  1. lexore
    14.05.2023 09:10

    Например, если OpenSSL вернул нам в этом колбеке ошибку - X509_V_ERR_CERT_HAS_EXPIRED, что значит, что сертификат уже "протух", но мы по своим причинам хотим разрешить такое непотребство, то мы можем на нее просто вернуть OK.

    А если у сертификата не только эта проблема? Сертификат может быть не только протухим, но и иметь другие проблемы с валидностью.


    1. Witcher136 Автор
      14.05.2023 09:10
      -1

      Конечно, можно подавить любую ошибку. Это я написал как пример:) обновлю


      1. lexore
        14.05.2023 09:10

        Я немного про другое. Openssl выдает ошибку на первой же проблеме. Следующую проблему он не показывает. Если после этого вы включаете callback, который выдает ok, как вы узнаете, что больше проблем нет?


        1. MrFuNTiK
          14.05.2023 09:10

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

          Недавно столкнулся с необходимостью при проверке подписи различать ошибки выполнения (например, ошибки выделения памяти) от криптографических (не сошелся хэш или подпись) и проверок цепочек сертификатов. Да еще и так, чтобы при обнаружении ошибок в крипте или цепочках проверка не завершалась, а продолжалась дальше. При ошибках выполнения, понятно, дальше идти не имеет смысла. Сложность кода сильно возрасла, причем пришлось забирать и править часть реализации OpenSSL. Квест был интересный, в прочем, он все еще в процессе.


  1. MrFuNTiK
    14.05.2023 09:10

    Контексты OpenSSL действительно удобны. Здесь нет на мой взгляд существенного уточнения о том, что контекст проверки сертификата одноразовый, то есть после того, как контекст был использован для проверки одного сертификата, его нельзя сразу же повторно переиспользовать для проверки другого сертификата. Нужно либо сбрасывать контекст функцией X509_STORE_CTX_reset(), либо создавать новый, то есть как-то так:

    STACK_OF( X509 )* certsToVerify = ...; // Сертификаты, которые собираемся
                                           // проверить.
    STACK_OF( X509 )* untrusted = ...;     // Промежуточные недоверенные сертификаты,
                                           // которые могут быть использованы для
                                           // построения цепочек.
    X509_STORE* store = ...;               // Хранилище доверенных сертификатов,
                                           // уже сконфигурированное нужным образом.
    
    X509_STORE_CTX* ctx = X509_STORE_CTX_new();
    if( !ctx )
    {
        HandleError();
    }
    
    for( int i = 0; i < sk_X509_num( certsToVerify ); ++i )
    {
        X509* cert = sk_X509_value( certsToVerify, i );
        if( !X509_STORE_CTX_init( ctx, store, cert, untrusted ) )
        {
            HandleError();
        }
    
        if( 1 > X509_STORE_CTX_verify( ctx ) )
        {
            const char* err = X509_verify_cert_error_string( X509_STORE_CTX_get_error( ctx ) );
            X509_NAME* X509_get_subject_name( cert );
            fprintf( stderr, "Verification failure: %s\n", err );
            X509_NAME_print_ex( stderr, name, 0, 0 );
        }
        X509_STORE_CTX_reset( ctx ); // Сбрасываем контекст для проверки следующего сертификата.
    }

    А сбрасывать контекст перед освобождением, как это делаете Вы, смысла не имеет.

    Да и обертки над X509_STORE_load_path(), X509_STORE_load_file(), X509_STORE_add_cert() избыточны, так как функции OpenSSL возвращают 1 при успехе и 0 при ошибке, что и так без проблем кастится в плюсовый bool, а так только дополнительный вызов получается.

    На самом деле проверка сертификатов - интересный процесс, там можно много разных коллбэков переопределить, задавать различные параметры проверки (например, время или назначение сертификата). Подумайте, может, захотите сделать более развернутую статью, многим будет полезно :)


    1. Witcher136 Автор
      14.05.2023 09:10

      Да, надо бы добавить это. Спасибо:)