Введение
Шифрование (статьи 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 могут возвращать отрицательные значения при внутренних ошибках выполнения (а не только явный, понятный отказ проверки самой подписи), и неаккуратная обработка этого может привести к ложно-положительному результату проверки при фактической внутренней ошибке, а не реальном успешном прохождении проверки. - Хранение или распространение публичного ключа для проверки подписи без должной защиты его аутентичности (например, скачивание публичного ключа по тому же незащищённому каналу, что и сам подписанный файл) — если атакующий способен подменить и сам файл, и публичный ключ для его проверки одновременно (заменив оба на собственную пару), проверка подписи не даст никакой реальной защиты; публичный ключ для проверки критичных операций (как обновления приложения) должен распространяться через отдельный, более доверенный канал (зашитый непосредственно в код самого приложения при его сборке, а не загружаемый динамически с того же сервера, что и сами обновления).