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

Перевірка підписів PDF та рівнів PAdES у Delphi

Ви отримали підписаний PDF і вам потрібно показати у своєму переглядачі, хто його підписав, коли, чи покриває підпис увесь файл і наскільки він відповідає вимогам довгострокового зберігання. Компонент PDFium Component для Delphi та Lazarus відповідає на всі чотири питання за допомогою викликів у режимі читання: функція FPDF_GetSignatureCount та сімейство FPDFSignatureObj_* надають доступ до словника підпису, а метод TPdf.ValidatePades визначає базовий рівень PAdES. Це перша з трьох статей про підписи PDF за допомогою PDFium. Наступні дві статті будуть присвячені створенню підпису B-B та додаванню міток довгострокового часу. Проте варто одразу зазначити обмеження: усе описане тут стосується лише перевірки структури підпису, а читання заявлених даних підпису відрізняється від криптографічної перевірки або прийняття рішення про довіру підписувачу.

Чому словник підпису PDF — це не просто набір байтів

Підпис PDF — це словник, а не просто непрозоре вкладення, і дві його найважливіші властивості вказують, яка саме частина файлу захищена. Стандарт ISO 32000-1 §12.8 визначає словник підпису із записами /ByteRange та /Contents. /Contents містить шістнадцятковий рядок структури CMS SignedData (RFC 5652) — криптографічного конверта, який зберігає сертифікат підписувача, підписані атрибути та саме значення підпису. /ByteRange — це частина, яку розробники часто недооцінюють: це масив із двох діапазонів «зміщення-довжина», які разом охоплюють весь файл, крім шістнадцяткового рядка /Contents. У цьому проміжку і знаходяться байти підпису, а два діапазони по обидва боки — це саме те, що підпис засвідчує.

Структура ByteRange робить можливим перевірку інкрементного збереження. Оскільки підписувач не може вирахувати хеш байтів підпису, яких ще немає, файл розділяється навколо плейсхолдера /Contents, а все інше хешується у підпис. Підпис, діапазон ByteRange якого не досягає кінця файлу, є підозрілим: вміст, доданий після охопленого діапазону шляхом інкрементного оновлення, не порушить цілісність підпису, хоча й змінить те, що бачить користувач. Тому перше, що перевіряє серйозний інспектор підписів, — це не особа підписувача, а те, чи покриває підпис усі байти, які він мав би засвідчувати.

Читання словника підпису за допомогою read-only API в PDFium

Компонент PDFium надає доступ до словника підпису через два read-only елементи: SignatureCount та запис Signature[Index]. Під капотом вони викликають FPDF_GetSignatureCount, FPDF_GetSignatureObject та методи доступу FPDFSignatureObj_* для отримання значень /SubFilter, /ByteRange, /Contents, /Reason та часу підписання. Вирішальним є те, що це API працює лише для читання: PDFium може перелічувати та читати підписи, але не має функцій для їхнього створення чи запису. Саме тому створення підписів реалізується самою бібліотекою, а не методами PDFium.

var
  Pdf: TPdf;
  i: Integer;
  Sig: TPdfSignature;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.FileName := 'contract-signed.pdf';
    Pdf.Active := True;
    for i := 0 to Pdf.SignatureCount - 1 do
    begin
      Sig := Pdf.Signature[i];
      Writeln('SubFilter : ', Sig.Encoding);        // ETSI.CAdES.detached, adbe.pkcs7.detached, ...
      Writeln('Signed at : ', Sig.Time);            // signer date string, e.g. D:20260708120000+02'00'
      Writeln('Reason    : ', Sig.Reason);
      Writeln('CMS length: ', Length(Sig.Content)); // raw DER SignedData taken from /Contents
      Writeln('DocMDP    : ', Sig.Permission);      // 0 = no certification, 1..3 = MDP level
    end;
  finally
    Pdf.Free;
  end;
end;

Кожен запис TPdfSignature безпосередньо відображає словник підпису. Властивість Encoding відповідає значенню /SubFilter — найважливішому діагностичному полю, оскільки воно визначає обробник підпису та дозволяє одразу відрізнити сучасний підпис ETSI.CAdES.detached від застарілих або заборонених типів. Властивість Time — це вказаний автором час підписання у вигляді рядка дати PDF, що є твердженням самого підписувача, а не довіреним часом. Content — це сирі дані CMS SignedData, а Permission повертає рівень сертифікації DocMDP (0 для звичайного схвалення, від 1 до 3 для сертифікаційного підпису, який забороняє подальші зміни). Запис не містить розібраного діапазону ByteRange, і це зроблено навмисно, оскільки метод ValidatePades самостійно виконує всі перевірки покриття ByteRange.

Яка різниця між PAdES B-B, B-T, B-LT та B-LTA?

Чотири базові рівні PAdES формують ієрархію від мінімально коректного підпису до формату, створеного для зберігання протягом десятиліть, де кожен наступний рівень включає всі вимоги попереднього. Стандарт ETSI EN 319 142-1 визначає їх як B-B, B-T, B-LT та B-LTA. B-B (Basic) — це сам підпис та обов'язкові підписані атрибути. B-T (Timestamp) додає довірену мітку часу RFC 3161 timestamp PDF поверх підпису, тому момент підписання підтверджується службою міток часу, а не локальним годинником підписувача. B-LT (Long-Term) вбудовує дані для валідації (ланцюжок сертифікатів та, за наявності, відповіді OCSP або CRL) всередину файлу, завдяки чому підпис можна перевірити через роки, навіть після зникнення інфраструктури видавця сертифіката. B-LTA (Long-Term with Archive timestamp) обгортає ці дані архівною міткою часу документа, захищаючи самі довгострокові дані та дозволяючи оновлювати мітки часу до того, як застаріють криптографічні алгоритми.

На практиці різниця полягає у часовому горизонті. Підпис B-B дає відповідь на питання «чи підписав це хтось». Рівень B-T відповідає «і коли саме, з доказом». Рівень B-LT додає «і чи зможу я перевірити підпис після закінчення терміну дії сертифікатів». Рівень B-LTA гарантує «і чи буде ця перевірка дійсною через двадцять років». Залежно від вимог регуляторів вибирається відповідний рівень: багато сценаріїв електронних інвойсів та eIDAS вимагають щонайменше B-T, а вимоги до архівів передбачають використання B-LT або B-LTA. Визначення конкретного рівня документа перед його прийняттям чи відхиленням і є основним завданням перевірки.

Визначення базового рівня за допомогою TPdf.ValidatePades

Компонент PDFium дозволяє вирішити питання визначення рівня одним викликом. Метод TPdf.ValidatePades повертає запис TPadesValidationResult, властивість Level якого має тип TPadesLevel (plNone, plUnknown, plB_B, plB_T, plB_LT або plB_LTA) разом зі списком виявлених проблем, кількістю підписів та кількістю міток часу документа. Рівень визначається послідовно: спочатку перевіряється відповідність B-B, потім рівень підвищується до B-T при наявності мітки часу підпису або документа, до B-LT при наявності словника /DSS із сертифікатами та маркера /Extensions /ESIC Level 1, і до B-LTA, якщо присутні як мітка часу документа, так і маркер ESIC Level 2. Два допоміжні методи спрощують аналіз результату: IsCompliant повертає True, лише якщо рівень не нижчий за B-B і список проблем порожній, а IsCompliantAt дозволяє задати мінімальний рівень політики безпеки (наприклад, plB_T).

var
  Pdf: TPdf;
  R: TPadesValidationResult;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.FileName := 'contract-signed.pdf';
    Pdf.Active := True;
    R := Pdf.ValidatePades;
    case R.Level of
      plNone:    Writeln('No PAdES signature present');
      plUnknown: Writeln('Signature present but level undeterminable');
      plB_B:     Writeln('PAdES B-B   (basic)');
      plB_T:     Writeln('PAdES B-T   (trusted timestamp)');
      plB_LT:    Writeln('PAdES B-LT  (long-term material embedded)');
      plB_LTA:   Writeln('PAdES B-LTA (archive timestamp)');
    end;
    Writeln('Signatures   : ', R.SignatureCount);
    Writeln('DocTimeStamps: ', R.DocTimeStampCount);
    if R.IsCompliantAt(plB_T) then
      Writeln('Meets the B-T policy floor')
    else
      Writeln('Below the required B-T level');
  finally
    Pdf.Free;
  end;
end;

Чому підтип adbe.pkcs7.sha1 є забороненим?

Тому що алгоритм SHA-1 є застарілим та вразливим, а підтип adbe.pkcs7.sha1 використовує його за замовчуванням. Цей SubFilter попередньо хешує документ за допомогою SHA-1 перед його обгортанням у PKCS#7. Оскільки SHA-1 вже багато років є вразливим до колізій, стандарт EN 319 142-1 розділ 6.3 повністю забороняє його використання для базових підписів. Метод ValidatePades додає проблему ppeiForbiddenSubFilter при виявленні adbe.pkcs7.sha1 або adbe.x509.rsa_sha1, а також проблему ppeiBadDigestAlgorithm, якщо сам CMS використовує MD5 або SHA-1 як хеш повідомлення (розділ 6.2.1). Це дві різні перевірки, які виявляють один і той самий клас вразливостей на різних рівнях.

Список можливих проблем містить 26 елементів, і найчастіше зустрічаються проблеми зі структурою та охопленням файлу. ppeiByteRangeNotCoveringFile — це згадана раніше перевірка покриття файлу. ppeiForbiddenCertKey додається, якщо словник підпису містить запис /Cert, що заборонено стандартом PAdES (оскільки ланцюжок сертифікатів має знаходитися в полі SignedData.certificates структури CMS). Проблеми ppeiMissingSigningCertificate, ppeiMissingContentType та ppeiMissingMessageDigest сигналізують про відсутність обов'язкових підписаних атрибутів, а ppeiDetachedContentViolation виявляє підпис, який помилково містить підписані дані всередині себе замість від'єднаного формату. Перебір цього списку дозволяє сформувати детальний опис проблеми для логування.

var
  R: TPadesValidationResult;
  Issue: TPadesValidationIssue;
begin
  R := Pdf.ValidatePades;
  if R.Issues <> [] then
    for Issue := Low(TPadesValidationIssue) to High(TPadesValidationIssue) do
      if Issue in R.Issues then
        Writeln('Issue: ',
          GetEnumName(TypeInfo(TPadesValidationIssue), Ord(Issue)));
end;

Що не перевіряє метод ValidatePades

Метод ValidatePades перевіряє структуру, а не рівень довіри, і плутанина між цими поняттями є небезпечною помилкою. Результат plB_LTA означає лише те, що документ містить структурно коректний підпис B-LTA з усіма необхідними атрибутами та мітками часу в потрібних місцях. Це не гарантує, що підпис є криптографічно дійсним, що ланцюжок сертифікатів веде до довіреного кореневого центру або що жоден сертифікат у ланцюжку не був відкликаний. Валідатор навмисно не виконує криптографічних перевірок: він не перераховує хеш файлу по ByteRange, не будує ланцюжок довіри та не перевіряє стан відкликання через OCSP або CRL. Це розділення є свідомим та корисним, оскільки структурна перевірка виконується дуже швидко, є повністю детермінованою та не потребує ключів, мережевих запитів чи криптосистем платформи. Тому метод ValidatePades працює однаково на Windows, Linux та macOS як чистий Pascal-код над байтами файлу. Навпаки, валідація ланцюжка довіри нерозривно пов'язана з політикою безпеки (яким кореневим сертифікатам довіряти, як отримувати списки відкликання, які допуски за часом застосовувати) і виконується на наступному етапі з використанням сховища сертифікатів платформи. Сприймайте успішне виконання ValidatePades як підтвердження правильної структури підпису перед його передачею на повноцінну криптографічну перевірку.

Цей структурний аналіз є правильним першим кроком, який логічно поєднується з іншими перевірками, описаними у статтях про аудит безпеки PDF за допомогою компонента PDFium та про перевірку файлів PDF/X. Наступним кроком після зчитування та класифікації підпису є його створення: друга стаття серії присвячена підписанню PDF-файлів у форматі PAdES B-B, а третя описує роботу з мітками часу та формування рівнів B-LT і B-LTA. Описані тут засоби інспекції підписів та метод ValidatePades є частиною компонента PDFium Component для Delphi, C++Builder та Lazarus.