Введение
Для официальных и юридически значимых документов (договоры, счета, отчёты для регулирующих органов) часто требуется цифровая подпись PDF — криптографическое подтверждение того, что документ не был изменён после подписания конкретным лицом или организацией. Qt не предоставляет встроенного высокоуровневого API именно для цифровой подписи PDF (в отличие от базовой генерации PDF через QPdfWriter), и в статье разберём, как решать эту задачу через комбинацию метаданных, доступных в Qt, и внешних инструментов для собственно криптографической подписи.
Концепция
Полноценная цифровая подпись PDF по стандарту PAdES (PDF Advanced Electronic Signatures) — сложная задача, требующая работы с X.509-сертификатами, временными метками от доверенных служб (timestamp authority) и встраиванием подписи непосредственно в структуру PDF-файла особым образом, совместимым со спецификацией PDF для подписей. Qt сам не реализует эту спецификацию — практический подход заключается в генерации PDF через QPdfWriter (с обычными, неподписанными метаданными, разобранными в статье 191), а затем применении внешнего инструмента или библиотеки (например, через вызов специализированной утилиты командной строки) для добавления собственно криптографической подписи как отдельного постпроцессинга сгенерированного файла.
Пример кода: Цифровые подписи и метаданные PDF-документов
#include <QPdfWriter>
#include <QPainter>
#include <QProcess>
#include <QTemporaryFile>
#include <QDebug>
bool generateUnsignedPdf(const QString &outputPath, const QString &documentText)
{
QPdfWriter writer(outputPath);
writer.setPageSize(QPageSize(QPageSize::A4));
writer.setTitle("Договор оказания услуг № 2024-15");
writer.setCreator("MyApp Document Generator");
QPainter painter;
if (!painter.begin(&writer)) return false;
QFont font("Arial", 12);
painter.setFont(font);
painter.drawText(QRectF(50, 50, writer.width() - 100, writer.height() - 100),
Qt::TextWordWrap, documentText);
painter.end();
return true;
}
// Подписывание через внешний инструмент (например, pyhanko, OpenSSL-based решение,
// или платную библиотеку с поддержкой PAdES) — Qt здесь только готовит исходный PDF
bool signPdfExternally(const QString &unsignedPdfPath, const QString &signedPdfPath,
const QString &certificatePath, const QString &keyPath)
{
QProcess signingProcess;
signingProcess.start("pyhanko", {
"sign", "addsig",
"--field", "Signature1",
"--with-validation-info",
"pkcs12",
"--p12-setup",
certificatePath,
unsignedPdfPath,
signedPdfPath
});
if (!signingProcess.waitForFinished(30000)) {
qWarning() << "Процесс подписания не завершился за отведённое время";
return false;
}
if (signingProcess.exitCode() != 0) {
qWarning() << "Ошибка подписания:" << signingProcess.readAllStandardError();
return false;
}
return true;
}
void generateAndSignContract()
{
const QString tempPath = "contract_unsigned.pdf";
const QString finalPath = "contract_signed.pdf";
if (!generateUnsignedPdf(tempPath, "Текст договора оказания услуг между сторонами...")) {
return;
}
if (signPdfExternally(tempPath, finalPath, "company_cert.p12", "")) {
qDebug() << "Договор успешно подписан:" << finalPath;
QFile::remove(tempPath); // удаляем промежуточный неподписанный файл
}
}
Пояснения к коду
generateUnsignedPdf() использует уже знакомый из статьи 191 подход с QPdfWriter для формирования содержимого документа — на этом этапе PDF полностью готов с точки зрения содержания, но не содержит криптографической подписи. signPdfExternally() демонстрирует архитектурный паттерн делегирования специализированной задачи внешнему инструменту через QProcess (аналогично статье 21 основного цикла про запуск внешних программ) — в примере используется гипотетический вызов инструмента pyhanko (реального open-source инструмента для PAdES-подписи PDF на Python), но принцип применим к любому внешнему инструменту с подходящим CLI-интерфейсом или библиотеке с C++ API.
Финальная функция generateAndSignContract() связывает оба шага в последовательный пайплайн: генерация → подписание → удаление промежуточного неподписанного файла, оставляя для дальнейшего использования только финальный, криптографически подписанный документ.
Подводные камни
- Зависимость от внешнего инструмента/библиотеки для реальной криптографической части — в отличие от большинства задач, разобранных в этом цикле, цифровая подпись PDF не решается средствами одного только Qt, и выбор конкретного инструмента (open-source CLI-утилита, коммерческая C++ библиотека, облачный сервис подписания) — отдельное архитектурное решение со своими компромиссами по стоимости, лицензированию и удобству интеграции.
- Хранение закрытого ключа и сертификата для подписания в файловой системе без должной защиты. Если ключ, используемый для автоматического подписания документов (как в серверном сценарии массовой генерации подписанных счетов), хранится в открытом виде на том же сервере, его компрометация позволяет злоумышленнику подписывать произвольные документы от имени организации — для критичных по значимости подписей нужно рассматривать аппаратные модули безопасности (HSM) или защищённое хранилище ключей, а не простой файл на диске.
- Юридическая значимость подписи зависит от соответствия конкретному стандарту и юрисдикции, а не только от технической корректности криптографии. В разных странах (и отраслях) могут быть разные требования к формату электронной подписи для юридической силы документа (например, квалифицированная электронная подпись в одних юрисдикциях против простой цифровой подписи в других) — техническая реализация через инструмент типа pyhanko не гарантирует автоматического соответствия конкретным юридическим требованиям без отдельной проверки соответствия нужному регламенту.
- Отсутствие проверки результата подписания перед удалением исходного неподписанного файла. В примере
QFile::remove(tempPath)вызывается сразу после успешного завершения процесса подписания, но без дополнительной верификации, что итоговый подписанный файл действительно валиден и содержит корректную подпись (например, через повторное открытие и программную проверку подписи) — для критичных документов стоит добавить такую верификацию перед удалением единственного «чистого» исходника на случай скрытого сбоя процесса подписания, не отразившегося в коде возврата.