Технічна стаття

Постквантове та EdDSA підписання PDF з HotPDF у Delphi

HotPDF перевіряє підписи ML-DSA-44, ML-DSA-65, ML-DSA-87, Ed25519 та Ed448 CMS у завантажених 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 >> у Catalog. На боці читання 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;

Викличте його до збереження, а не після. Декларація є частиною підписаного байтового діапазону, а Catalog, підправлений після, — це або непідписана зміна підписаного файлу, або друга ревізія, яку валідатор звітуватиме як модифікацію

Три родини алгоритмів, одна точка входу перевірки

Усі три родини проходять через 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;

Чому в enum стану шість значень замість булевого

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 щодо повного списку можливостей і пробного завантаження