SSL

Подписывание и верификация плагинов для безопасности

3 просмотров

Введение

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

Концепция

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

Qt не предоставляет встроенного API для подписывания именно плагинов (в отличие от, например, встроенной поддержки TLS-сертификатов для сетевых соединений) — для криптографической части обычно используется отдельная библиотека (OpenSSL через прямые вызовы, либо более высокоуровневая обёртка), а Qt используется для файлового I/O и интеграции результата проверки с QPluginLoader.

Пример кода

# Генерация пары ключей издателя (выполняется один раз, закрытый ключ хранится секретно)
openssl genpkey -algorithm RSA -out publisher_private.pem -pkeyopt rsa_keygen_bits:2048
openssl rsa -pubout -in publisher_private.pem -out publisher_public.pem

# Подписывание скомпилированного плагина закрытым ключом издателя
openssl dgst -sha256 -sign publisher_private.pem -out csvexportplugin.dll.sig csvexportplugin.dll
// pluginverifier.h — проверка подписи плагина перед его загрузкой
#pragma once
#include <openssl/evp.h>
#include <openssl/pem.h>
#include <QFile>
#include <QDebug>

class PluginVerifier
{
public:
    explicit PluginVerifier(const QByteArray &publicKeyPem) : m_publicKeyPem(publicKeyPem) {}

    bool verify(const QString &pluginPath, const QString &signaturePath)
    {
        const QByteArray pluginData = readFile(pluginPath);
        const QByteArray signatureData = readFile(signaturePath);
        if (pluginData.isEmpty() || signatureData.isEmpty()) {
            return false;
        }

        BIO *bio = BIO_new_mem_buf(m_publicKeyPem.constData(), m_publicKeyPem.size());
        EVP_PKEY *publicKey = PEM_read_bio_PUBKEY(bio, nullptr, nullptr, nullptr);
        BIO_free(bio);
        if (!publicKey) {
            qWarning() << "Не удалось разобрать публичный ключ издателя";
            return false;
        }

        EVP_MD_CTX *ctx = EVP_MD_CTX_new();
        bool result = false;

        if (EVP_DigestVerifyInit(ctx, nullptr, EVP_sha256(), nullptr, publicKey) == 1) {
            const int verifyResult = EVP_DigestVerify(
                ctx,
                reinterpret_cast<const unsigned char *>(signatureData.constData()),
                signatureData.size(),
                reinterpret_cast<const unsigned char *>(pluginData.constData()),
                pluginData.size());
            result = (verifyResult == 1);
        }

        EVP_MD_CTX_free(ctx);
        EVP_PKEY_free(publicKey);

        return result;
    }

private:
    QByteArray readFile(const QString &path)
    {
        QFile file(path);
        if (!file.open(QIODevice::ReadOnly)) return {};
        return file.readAll();
    }

    QByteArray m_publicKeyPem;
};
// Интеграция с загрузкой плагина — отказ от загрузки при невалидной подписи
#include "pluginverifier.h"
#include <QPluginLoader>

void loadVerifiedPlugin(const QString &pluginPath)
{
    // Публичный ключ встроен в приложение при компиляции (например, через ресурсы Qt),
    // НЕ читается из той же недоверенной директории, что и сам плагин
    static const QByteArray trustedPublicKey = R"(-----BEGIN PUBLIC KEY-----
...содержимое publisher_public.pem...
-----END PUBLIC KEY-----)";

    PluginVerifier verifier(trustedPublicKey);
    const QString signaturePath = pluginPath + ".sig";

    if (!verifier.verify(pluginPath, signaturePath)) {
        qWarning() << "Подпись плагина не прошла проверку, загрузка отменена:" << pluginPath;
        return;
    }

    QPluginLoader loader(pluginPath);
    QObject *instance = loader.instance();
    // ... дальнейшее использование плагина ...
}

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

Подпись создаётся внешним инструментом (openssl dgst -sign) на этапе релиза плагина, формируя отдельный файл .sig, который распространяется вместе с самим файлом плагина. PluginVerifier::verify() использует библиотеку OpenSSL напрямую (через её C API EVP_DigestVerify) для криптографической проверки: вычисляется хеш содержимого файла плагина и проверяется, что предоставленная подпись действительно соответствует этому хешу при использовании указанного публичного ключа — это математически гарантирует, что файл не был изменён после подписания владельцем соответствующего закрытого ключа.

Критичная деталь в loadVerifiedPlugin() — публичный ключ издателя (trustedPublicKey) встроен непосредственно в код самого приложения как константа, а не читается из той же директории на диске, что и потенциально скомпрометированный плагин — если бы публичный ключ читался из той же недоверенной локации, злоумышленник мог бы подменить и плагин, и соответствующий ему публичный ключ одновременно, полностью обесценивая проверку.

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

  • Хранение публичного ключа в коде приложения не защищает от подмены самого приложения целиком — если злоумышленник может заменить весь исполняемый файл приложения (а не только файл плагина), встроенная проверка подписи плагинов бесполезна, поскольку модифицированное приложение может содержать как угодно изменённую (или вовсе отключённую) логику проверки. Это не недостаток самого подхода, а ограничение модели угроз: подпись плагинов защищает от подмены именно плагинов при доверенном основном приложении, а не от компрометации всей системы.
  • Производительность проверки подписи для большого числа плагинов, загружаемых при каждом старте приложения. Криптографические операции, хоть и быстрые в абсолютном выражении, при сканировании директории с десятками плагинов и проверке подписи каждого добавляют заметную, хоть и не катастрофическую задержку к времени старта — для приложений с очень большим количеством плагинов стоит измерить реальное влияние на время запуска.
  • Управление жизненным циклом ключей и их возможной компрометацией. Если закрытый ключ издателя когда-либо будет скомпрометирован, все ранее выпущенные подписанные плагины технически остаются «валидными» с точки зрения чистой криптографической проверки — полноценная система должна предусматривать механизм отзыва ключей (например, список отозванных ключей, распространяемый и проверяемый отдельно), аналогично инфраструктуре PKI для TLS-сертификатов, что существенно усложняет реализацию по сравнению с базовой проверкой, показанной в примере.
  • Дополнительная зависимость от OpenSSL (или другой криптографической библиотеки) непосредственно в коде приложения требует отдельного внимания к управлению версиями этой зависимости, её обновлению при обнаружении уязвимостей в самой криптобиблиотеке, и корректной линковке на всех целевых платформах — это нетривиальная дополнительная нагрузка на процесс сборки и поддержки по сравнению с системой плагинов без верификации подписи.