Компонент PDFium Component подписывает PDF-файл цифровой подписью уровня PAdES B-B с помощью метода SignPades: он загружает документ, вычисляет хэш подписанного диапазона байтов, строит структуру CMS CAdES и добавляет подпись в виде инкрементного сохранения. Криптографический бэкэнд поддерживается только под Windows, поэтому проверяйте возможность подписания с помощью свойства PadesCryptoAvailable
Сценарий знаком многим. На столе лежит договор в формате PDF, юридический отдел требует подписать его цифровой подписью перед отправкой, и вы открываете ту же сборку PDFium, которую используете для отображения и анализа документов, но обнаруживаете, что API подписей в PDFium доступен только для чтения. Движок не имеет средств создания подписей. Компонент PDFium Component решает эту проблему, реализуя весь процесс подписания на коде Pascal (от вычисления хэша до побайтовой записи в файл). В этой статье мы подробно разберем весь этот процесс
Почему PDFium не поддерживает создание подписей?
В PDFium подписи представлены объектами только для чтения, средств создания новых подписей нет. Функции семейства FPDFSignatureObj_* позволяют находить существующие подписи, считывать параметр /Contents и анализировать диапазон /ByteRange, но нет методов создания словаря подписи, резервирования области под значение или записи диапазона байтов; хотя инкрементное сохранение присутствует (FPDF_SaveAsCopy с флагом FPDF_INCREMENTAL), оно не содержит механизмов подписания. Любой компонент, добавляющий подпись в PDF поверх PDFium, должен генерировать каждый байт подписи самостоятельно. Именно поэтому PDFium Component реализует эту логику в трех модулях на чистом Pascal. Поскольку FPC 3.2.2 поддерживает md5 и sha1, но не имеет SHA-2, а API SHA-256 в Delphi (System.Hash) несовместим с FPC, модуль FPdfSha256 предлагает независимую реализацию стандарта FIPS 180-4, сохраняя код CMS на типе TSHA256Digest без ветвления компиляторов. Модуль FPdfAsn1 реализует кодирование и чтение DER-структур, необходимых для CMS, а FPdfCms собирает на их основе контейнер CAdES SignedData
Как подписать файл PDF цифровой подписью в Delphi?
Загрузите документ и вызовите метод SignPades с указанием отпечатка (thumbprint) сертификата. Компонент PDFium Component найдет этот сертификат в системном хранилище Current User "MY", извлечет его закрытый ключ и запишет подписанную копию по указанному пути
uses
PDFium, FPdfCrypto;
procedure SignContract(const AThumbprint: string);
var
Pdf: TPdf;
begin
if not PadesCryptoAvailable then
raise Exception.Create('PAdES signing requires the Windows CNG backend');
Pdf := TPdf.Create(nil);
try
Pdf.FileName := 'contract.pdf'; // the document to sign
Pdf.Active := True;
// Second argument: SHA-1 thumbprint of a certificate in the Current
// User "MY" store. First argument: destination for the signed copy.
if not Pdf.SignPades('contract-signed.pdf', AThumbprint) then
raise Exception.Create('Signing failed');
finally
Pdf.Free;
end;
end;
Свойство PadesCryptoAvailable — первая обязательная проверка. В среде Windows оно возвращает True и активирует бэкэнд crypt32/ncrypt; на других платформах оно вернет False, а вызов метода подписания приведет к ошибке EPadesCrypto. Проверка этого свойства гарантирует, что сборки под Linux или macOS не упадут во время выполнения на неподдерживаемом коде. Отпечаток сертификата представляет собой SHA-1 хэш сертификата (то же значение, которое показывает диспетчер сертификатов Windows на вкладке «Состав»), позволяя указать подписанта без хранения ключевой информации в исходном коде программы
Что содержит контейнер CMS: подписанные атрибуты и RFC 5652
Базовая подпись PAdES не является простым результатом шифрования хэша файла алгоритмом RSA; это структура CAdES CMS SignedData, содержащая обязательный набор подписанных атрибутов. Метод FPdfCms.BuildSignedData формирует именно этот набор: тип содержимого (content-type), хэш сообщения (message-digest) и сертификат подписанта (signing-certificate-v2) — атрибут ESS, связывающий подпись с хэшем сертификата. Здесь есть одна важная деталь, о которую спотыкаются почти все ручные реализации CMS. Стандарт RFC 5652 §5.4 требует, чтобы хэш подписанных атрибутов вычислялся для кодировки DER SET OF (тег 0x31), тогда как те же атрибуты записываются в структуру SignerInfo под тегом IMPLICIT [0] (0xA0). Компонент PDFium Component кодирует множество атрибутов один раз, вычисляет хэш для формы 0x31, а затем просто заменяет первый байт тега на 0xA0 при записи, позволяя использовать один буфер для обеих задач без повторного обхода дерева данных
var
Pdf: TPdf;
Opts: TPadesSignOptions;
begin
Pdf := TPdf.Create(nil);
try
Pdf.FileName := 'contract.pdf';
Pdf.Active := True;
Opts := TPadesSignOptions.Default;
Opts.CertificateThumbprint := 'a1b2c3d4e5f6...'; // signer in the MY store
Opts.Reason := 'I approve this agreement';
Opts.Location := 'Berlin, DE';
Opts.ContentsSize := 16384; // hex width of /Contents
if not Pdf.SignPades('contract-signed.pdf', Opts) then
raise Exception.Create('Signing failed');
finally
Pdf.Free;
end;
end;
Перегрузка с параметрами позволяет указывать метаданные словаря подписи по спецификации ISO 32000-1 §12.8.1: Reason, Location, ContactInfo и Name (все они являются необязательными и записываются в словарь подписи). Однако есть важное ограничение. Если вы задаете параметр CommitmentTypeOid для добавления подписанного атрибута типа обязательства CAdES, не указывайте свойство Reason; стандарт ETSI EN 319 142-1 §6.3 запрещает одновременное использование обоих параметров, так как они выражают одно намерение разными средствами
Взаимодействие ByteRange и области /Contents
Подпись должна покрывать весь файл, за исключением байтов самой подписи. PAdES решает эту циклическую зависимость выделением области фиксированного размера, которой управляет метод SignPadesBytes. Он резервирует в параметре /Contents hex-строку длиной ContentsSize байт (по умолчанию 16384, что гарантированно больше типичной структуры CMS SignedData), сериализует инкрементное обновление для определения точного смещения области, а затем вычисляет диапазон /ByteRange в виде двух диапазонов по краям этой области (все байты до открывающего маркера hex-строки и все байты после закрывающего маркера). Хэширование SHA-256 выполняется только по этим двум диапазонам. Готовая структура CMS кодируется в шестнадцатеричном виде в зарезервированную область с заполнением нулями до фиксированной ширины, после чего дописывается таблица обновлений перекрестных ссылок. Так как ширина области зафиксирована заранее, запись подписи не сдвигает последующие байты, сохраняя валидность диапазона; исходные байты документа сохраняются без изменений, что позволяет не нарушать целостность ранее наложенных подписей в соответствии с требованиями ISO 32000-1 §12.8.1
Windows CNG бэкэнд и его ограничения
Компонент PDFium Component подписывает файлы только в среде Windows, и это ограничение осознанно. Модуль FPdfCryptoWin динамически загружает библиотеки crypt32.dll и ncrypt.dll, не добавляя внешних зависимостей на этапе компиляции, а цепочка вызовов является стандартной для CNG: открывается хранилище MY, выполняется поиск сертификата по хэшу, извлекается дескриптор закрытого ключа через CryptAcquireCertificatePrivateKey и вызывается метод NCryptSignHash. Поддерживаются алгоритмы RSA с PKCS#1 v1.5, RSA-PSS и ECDSA. Алгоритму ECDSA требуется дополнительное преобразование, так как NCryptSignHash возвращает пару r и s по стандарту IEEE P1363, в то время как CMS ожидает структуру DER ECDSA-Sig-Value SEQUENCE, поэтому бэкэнд перекодирует ее по спецификации RFC 5480
var
Pdf: TPdf;
Opts: TPadesSignOptions;
Output: TFileStream;
begin
if not PadesCryptoAvailable then
Exit; // no signing backend on this platform
Opts := TPadesSignOptions.Default;
Opts.CertificateThumbprint := ReadThumbprintFromConfig;
Pdf := TPdf.Create(nil);
Output := TFileStream.Create('contract-signed.pdf', fmCreate);
try
Pdf.FileName := 'contract.pdf';
Pdf.Active := True;
Pdf.SignPadesToStream(Output, Opts);
finally
Output.Free;
Pdf.Free;
end;
end;
В результате закрытый ключ должен обязательно находиться в системном хранилище сертификатов Windows. Сертификат из PFX-файла можно использовать только после его импорта в хранилище Current User, после чего его отпечаток передается в метод SignPades. В текущей версии нет поддержки PKCS#11 или HSM, а также программных ключей из файлов, поэтому при значении PadesCryptoAvailable = False функция подписания на компьютере работать не будет
Границы возможностей уровня PAdES B-B
Уровень PAdES B-B является базовым начальным уровнем: он доказывает автора подписи и неизменность байтов документа с момента подписания, но не более того. Подпись уровня B-B не содержит доверенного штампа времени (поэтому не может доказать точное время подписания) и не несет данных проверки статуса сертификатов (поэтому проверяющая сторона должна самостоятельно запрашивать статусы цепочки сертификатов в будущем). Эти ограничения снимаются на более высоких уровнях. Если вам требуется фиксация времени для аудита, добавление штампа времени RFC 3161 и DSS переводит подпись на уровни B-T и выше; если вам нужно прочитать подпись и определить ее уровень, используйте методы из статьи об анализе подписей PDF и уровней PAdES; а перед подписанием документов полезно запустить проверки, описанные в материале об аудите угроз безопасности PDF
Описанные здесь методы SignPades поставляются в составе компонента PDFium Component для Delphi и C++Builder наряду с функциями чтения подписей, доступными из коробки