HotPDF проверяет CMS-подписи ML-DSA-44, ML-DSA-65, ML-DSA-87, Ed25519 и Ed448 в загруженных PDF-документах и подписывает через подключаемые провайдеры, поэтому закрытому ключу не обязательно жить внутри вашего Delphi-процесса. Именно вторая половина нужна большинству команд первой. Аппаратный токен, удалённая служба подписания и национальная eID-карта все отказываются отдавать ключ, и пока конвейер подписания не отделён от хранилища ключей, ни один из них невозможно использовать вовсе
Это разделение — суть THPDFSignatureProvider. HotPDF держит то, чем должен владеть: разбор CMS, построение SignedData, размещение /ByteRange — и делегирует ту единственную операцию, которой владеть не может: превращение дайджеста в подпись ключом, который ему не положено видеть. Всё нижеследующее вытекает из этого разделения
Почему корректная подпись ML-DSA не проходит проверку?
Потому что HotPDF отклоняет ML-DSA на загруженном документе, не объявляющем расширения для него. ML-DSA — схема решёточной подписи, стандартизированная как FIPS 204 и причина, по которой говорят «постквантовый PDF», — ещё не имеет регистрации в ISO 32000-2. PDF, несущий её, использует алгоритм, который базовый стандарт не называет, а файл, молча использующий безымянный алгоритм, — это файл, чей вердикт никто другой не сможет воспроизвести
Поэтому HotPDF делает претензию явной. EnsureMLDSAExtensions повышает документ до PDF 2.0 там, где это разрешено, и записывает /Extensions /HotPDF << /BaseVersion /2.0 /ExtensionLevel 1 >> в каталог. На стороне чтения LoadedDocumentDeclaresMLDSAExtension сообщает, пережило ли это объявление, а VerifyLoadedSignatureWithOptions применяет ту же проверку, прежде чем учесть Options.AllowMLDSA. Установите флаг на необъявленном документе, и он останется выключенным: опция может ослабить политику, но никогда — структурное требование
var
Pdf: THotPDF;
begin
Pdf := THotPDF.Create(nil);
try
Pdf.FileName := 'contract-pq.pdf';
Pdf.BeginDoc;
Pdf.CurrentPage.SetFont('Arial', [], 11);
Pdf.CurrentPage.TextOut(50, 720, 0, 'Supply agreement 2026-114');
Pdf.EnsureMLDSAExtensions; // declare before the signature is written
Pdf.EndDoc;
finally
Pdf.Free;
end;
end;
Вызывайте это до сохранения, а не после. Объявление — часть подписанного диапазона байтов, а каталог, пропатченный после, — либо неподписанное изменение подписанного файла, либо вторая ревизия, которую валидатор отметит как модификацию
Три семейства алгоритмов, одна точка входа проверки
Все три семейства проходят через VerifyLoadedSignatureWithOptions, принимающую индекс подписи, исходный поток, запись THPDFCMSVerifyOptions и выходной параметр с деталями подписи. Запись имеет ровно три поля, и каждое отвечает на вопрос, прежде требовавший пересборки
SignatureProvider подставляет вашего провайдера вместо встроенного платформенного. OpenSSLLibraryPath выбирает библиотеку OpenSSL 3, поставляющую проверку Ed25519 и Ed448 в чистом режиме, которую Windows CNG предлагает не везде. AllowMLDSA подключает решёточные алгоритмы при условии проверки расширения выше. Точный OID алгоритма, который был распознан, возвращается в THPDFSignatureInfo.SignatureAlgorithmOID, поэтому журнал аудита фиксирует, что именно проверялось, а не что запрашивалось
var
Opts: THPDFCMSVerifyOptions;
Info: THPDFSignatureInfo;
Status: THPDFSignatureVerifyStatus;
Src: TFileStream;
begin
Opts := THPDFCMSVerifyOptions.Default;
Opts.OpenSSLLibraryPath := 'C:\openssl3\libcrypto-3-x64.dll';
Opts.AllowMLDSA := Pdf.LoadedDocumentDeclaresMLDSAExtension;
Src := TFileStream.Create('contract-pq.pdf', fmOpenRead or fmShareDenyWrite);
try
Status := Pdf.VerifyLoadedSignatureWithOptions(0, Src, Opts, Info);
if Status = svValid then
Memo1.Lines.Add('signed with OID ' + string(Info.SignatureAlgorithmOID));
finally
Src.Free;
end;
end;
Ed25519 и Ed448 не требуют объявления расширения, потому что ISO 32000-2 уже их признаёт. Им нужен провайдер, их реализующий, что в большинстве Windows-развёртываний означает указание OpenSSLLibraryPath на библиотеку, которую вы поставляете и контролируете, а не на ту, что случайно оказалась на машине
Что на деле обещает провайдер подписания?
Провайдер обещает одно: получив запрос, вернуть статус и при подписании — байты. THPDFSignatureProviderRequest несёт алгоритм и его OID, OID дайджеста, длину соли PSS, является ли вход сообщением или уже подсчитанным дайджестом, сами входные данные, открытый ключ или сертификат, идентификатор ключа и идентификатор операции. Ничего в этой записи не специфично для HotPDF — это словарь, на котором уже говорит драйвер токена или служба подписания
Три реализации поставляются с библиотекой. THPDFCallbackSignatureProvider оборачивает анонимные методы — кратчайший путь от существующей внутренней подписной процедуры до рабочей PDF-подписи. THPDFRemoteSignatureProvider оборачивает транспортный callback с лимитом повторов, реестром отмены и ограничениями на размер входа и подписи, поэтому зависший HSM не превращается в зависшее приложение. THPDFPKCS11SignatureProvider сериализует операции RSA против принадлежащего вызывающему, уже аутентифицированного PKCS#11-сеанса и дескриптора закрытого ключа — HotPDF никогда не входит в систему, не видит PIN и не закрывает сеанс, который не открывал
var
Provider: THPDFRemoteSignatureProvider;
begin
Provider := THPDFRemoteSignatureProvider.Create(
function(const Req: THPDFSignatureProviderRequest; Attempt: Integer;
out Signature: TBytes): THPDFSignatureProviderStatus
begin
// POST Req.Input to the signing service; Req.KeyIdentifier selects the key
if PostToSigningService(Req.KeyIdentifier, Req.Input, Signature) then
Result := spsValid
else
Result := spsProviderError;
end,
3, // RetryLimit
1048576, // MaxInputBytes
65536); // MaxSignatureBytes
try
// hand Provider to the signing call
finally
Provider.Free;
end;
end;
Почему у перечисления статусов шесть значений, а не булев
THPDFSignatureProviderStatus различает spsValid, spsInvalid, spsUnsupported, spsMalformed, spsProviderError и spsCancelled, и их сворачивание лишает вас способности реагировать правильно. Криптографически неверная подпись (spsInvalid) — инцидент безопасности. Алгоритм, не реализованный провайдером (spsUnsupported), — пробел в развёртывании. Сбой транспорта (spsProviderError) заслуживает повтора, а отменённый пользователем запрос токена (spsCancelled) повтора не заслуживает вовсе
Правило для подписания узко: провайдер подписания возвращает spsValid только с непустой подписью. Провайдеры проверки возвращают spsValid или spsInvalid, а остальные четыре остаются раздельными на обоих путях. Если вы пишете провайдер, противьтесь искушению отображать всё нераспознанное на spsInvalid — это превращает отсутствующую DLL в отчёт о том, что подпись клиента подделана
Где подпись в действительности попадает в файл
Две функции связывают провайдеров с реальными PDF-байтами. HPDFCMSBuildSignedDataWithProvider строит отсоединённый CMS из дайджеста документа SHA-256 — та точка входа, что нужна, когда ваш рабочий процесс считает дайджест в другом месте. HPDFCMSSignPDFStreamWithProvider подписывает существующий заполнитель подписи в потоке PDF и сохраняет стандартный конвейер /ByteRange — та точка входа, что нужна, когда HotPDF сам разместил заполнитель
Сохранение этого конвейера важнее, чем кажется. Соглашение /ByteRange — два диапазона, минующих шестнадцатеричное окно подписи, — это то, что каждый валидатор проверяет первым, а основанный на провайдере путь, переписавший его, сломал бы соответствие PAdES при любой корректности криптографии. HotPDF сохраняет расположение идентичным встроенному пути подписания, поэтому документ, подписанный через PKCS#11-токен, проверяется тем же кодом проверки подписей, что и подписанный из PFX-файла. Правила профиля, стоящие над выбором алгоритма, описаны в разборе базовых подписей PAdES в Delphi, а для специфичных для ECDSA ловушек кодирования, предшествующих этой модели провайдеров, — заметки о проверке ECDSA CMS и форматах подписей P1363
Порядок миграции, не оставляющий документы на мели
Постквантовая готовность — это проблема расписания, а не тумблер. Почти ни один развёрнутый PDF-просмотрщик сегодня не проверяет ML-DSA, поэтому документ, подписанный только им, с точки зрения читателя — документ с непроверяемой подписью. Порядок, переживающий контакт с реальными архивами: держите RSA или ECDSA как подпись, которую оценит валидатор, добавьте объявление расширения и вторую подпись ML-DSA там, где политика требует квантово-устойчивого доказательства, и переводите основную подпись лишь тогда, когда принимающие системы догонят
То, что HotPDF даёт вам сегодня, — это способность писать и проверять обе одним кодом, с алгоритмом, честно зафиксированным в файле и в результате проверки. HotPDF — это нативный VCL PDF-компонент для Delphi и C++Builder без внешнего PDF-рантайма, поэтому пути подписания и проверки поставляются внутри вашего исполняемого файла, а не рядом с ним — см. страницу Delphi PDF-компонента HotPDF для полного списка возможностей и пробной загрузки