Различные статьи по Qt и c++

Цифровые подписи: подпись и проверка данных

Введение

Шифрование (статьи 373-374) решает задачу конфиденциальности — скрытия содержимого данных от посторонних. Цифровая подпись решает принципиально другую задачу — подтверждение того, что данные действительно созданы конкретным владельцем приватного ключа и не были изменены после подписания, не скрывая при этом сами данные. В статье разберём создание и проверку подписей через RSA, используя те же пары ключей, что и в статье 374, но в другой роли.

Концепция

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

Пример кода: Цифровые подписи

// Подпись данных приватным ключом
#include <openssl/evp.h>

QByteArray signData(const QByteArray &data, EVP_PKEY *privateKey)
{
    EVP_MD_CTX *mdCtx = EVP_MD_CTX_new();
    EVP_DigestSignInit(mdCtx, nullptr, EVP_sha256(), nullptr, privateKey);
    EVP_DigestSignUpdate(mdCtx, data.constData(), data.size());

    size_t signatureLength;
    EVP_DigestSignFinal(mdCtx, nullptr, &signatureLength); // первый вызов — узнать нужный размер буфера

    QByteArray signature(signatureLength, 0);
    EVP_DigestSignFinal(mdCtx, reinterpret_cast<unsigned char *>(signature.data()), &signatureLength);
    signature.resize(signatureLength);

    EVP_MD_CTX_free(mdCtx);
    return signature;
}

// Проверка подписи публичным ключом
bool verifySignature(const QByteArray &data, const QByteArray &signature, EVP_PKEY *publicKey)
{
    EVP_MD_CTX *mdCtx = EVP_MD_CTX_new();
    EVP_DigestVerifyInit(mdCtx, nullptr, EVP_sha256(), nullptr, publicKey);
    EVP_DigestVerifyUpdate(mdCtx, data.constData(), data.size());

    int result = EVP_DigestVerifyFinal(mdCtx,
        reinterpret_cast<const unsigned char *>(signature.constData()), signature.size());

    EVP_MD_CTX_free(mdCtx);
    return result == 1; // ровно 1 означает успешную проверку; 0 или отрицательное — неудача/ошибка
}
// Практический сценарий — подписание файла обновления приложения перед распространением
// (продолжение темы OTA-обновлений из статей 22, 317, 349)
bool verifyUpdatePackage(const QString &updateFilePath, const QString &signatureFilePath, EVP_PKEY *trustedPublicKey)
{
    QFile updateFile(updateFilePath);
    updateFile.open(QIODevice::ReadOnly);
    QByteArray updateData = updateFile.readAll();

    QFile signatureFile(signatureFilePath);
    signatureFile.open(QIODevice::ReadOnly);
    QByteArray signature = signatureFile.readAll();

    if (!verifySignature(updateData, signature, trustedPublicKey)) {
        qWarning() << "Подпись пакета обновления НЕ ВЕРНА — установка отменена";
        return false;
    }

    qDebug() << "Подпись пакета обновления подтверждена — пакет можно безопасно устанавливать";
    return true;
}

Пояснения к коду

EVP_DigestSignInit()/EVP_DigestVerifyInit() объединяют хеширование и операцию подписи/проверки в единый, удобный высокоуровневый интерфейс — внутренне OpenSSL сам хеширует переданные через EVP_DigestSignUpdate() данные алгоритмом, указанным явно (EVP_sha256()), и затем применяет операцию RSA к получившемуся хешу, так что вызывающему коду не нужно отдельно вычислять хеш через QCryptographicHash перед вызовом функций подписи. Двойной вызов EVP_DigestSignFinal() (первый раз с nullptr для буфера, чтобы узнать требуемый размер, второй — для фактического получения подписи) — стандартный паттерн OpenSSL API для функций с переменным размером выходных данных, аналогично паттерну работы с буферами переменного размера, встречавшемуся в других примерах цикла (статьи 373-374). verifyUpdatePackage() показывает практичное, реальное применение цифровых подписей — проверка того, что скачанный файл обновления действительно выпущен легитимным разработчиком приложения, а не подменён злоумышленником в процессе передачи (атака «человек посередине», man-in-the-middle), что особенно критично для функциональности автообновления, разбиравшейся ранее в основном цикле статьи 22.

Подводные камни

  • Установка обновлений или выполнение иных критичных по безопасности действий на основе данных без проверки цифровой подписи, особенно если эти данные получены по сети — без подписи (или эквивалентной защиты, например HTTPS с проверкой сертификата, статья 376) злоумышленник, способный вмешаться в передачу данных, может подменить легитимное обновление вредоносным кодом, и проверка подписи перед установкой любого скачанного исполняемого кода — критичная, не опциональная мера безопасности для систем автообновления.
  • Использование одной и той же пары ключей одновременно и для шифрования (статья 374), и для подписи, без понимания, что некоторые криптографические схемы рекомендуют разделение ролей ключей (отдельная пара для шифрования, отдельная — для подписи) ради дополнительной защиты от определённых теоретических атак, эксплуатирующих именно такое совмещение ролей — для систем с особо высокими требованиями к безопасности стоит явно рассмотреть использование раздельных пар ключей для разных криптографических операций, а не одной «универсальной» пары для всего.
  • Игнорирование результата проверки подписи или неполная, частичная её обработка (например, проверка возвращаемого значения только на != 0 вместо явного, точного сравнения с 1, как корректно показано в примере) — некоторые реализации криптографических API могут возвращать отрицательные значения при внутренних ошибках выполнения (а не только явный, понятный отказ проверки самой подписи), и неаккуратная обработка этого может привести к ложно-положительному результату проверки при фактической внутренней ошибке, а не реальном успешном прохождении проверки.
  • Хранение или распространение публичного ключа для проверки подписи без должной защиты его аутентичности (например, скачивание публичного ключа по тому же незащищённому каналу, что и сам подписанный файл) — если атакующий способен подменить и сам файл, и публичный ключ для его проверки одновременно (заменив оба на собственную пару), проверка подписи не даст никакой реальной защиты; публичный ключ для проверки критичных операций (как обновления приложения) должен распространяться через отдельный, более доверенный канал (зашитый непосредственно в код самого приложения при его сборке, а не загружаемый динамически с того же сервера, что и сами обновления).