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

Перевірка цифрових підписів PDF у Delphi з HotPDF

HotPDF перевіряє цифрові підписи в завантажених документах PDF через три методи THotPDF: GetLoadedSignatureInfo, VerifyLoadedSignature та VerifyLoadedSignatureEx, запроваджені у v2.259.0. Компонент перехешовує сегменти /ByteRange початкового файлу, перевіряє атрибут CMS messageDigest і виконує звірку RSA PKCS#1 v1.5 проти вбудованого сертифіката підписувача, повертаючи svValid, коли байти документа цілі

Сценарій буденний, а ставки ні. Контрагент повертає підписаний договір, ваш робочий процес має його підшити, і хтось ставить єдине питання, яке має значення: чи це той документ, який ми надіслали, байт у байт, підписаний тим сертифікатом, на який він претендує? Відповідь на це в коді є перевірочним боком історії з підписами; бік підписання, тобто побудова й вбудовування підписів PAdES, розглянуто в супутній статті про створення цифрових підписів PAdES у HotPDF. Ця стаття про інший напрямок: PDF надходить уже підписаним, і вам потрібен програмний вердикт, а не знімок екрана із зеленою галочкою Acrobat

Як підписаний PDF доводить, що його не підробили?

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

Цей задум має наслідок, який формує весь API: перевірка має хешувати початкові серіалізовані байти, рівно такими, як вони лежать на диску. Розібрана об'єктна модель для цього марна, бо повторна серіалізація навіть незміненого документа дає інші байти. Тому HotPDF перевіряє проти вихідного файлу, з якого документ був завантажений, або проти TStream із сирими байтами, які постачаєте ви, і ніколи проти свого представлення в пам'яті

Схема того, як HotPDF перевіряє підписаний PDF у Delphi, перехешовуючи два сегменти ByteRange вихідного файлу навколо дірки Contents, тоді як розібрана модель у пам'яті ніколи не хешується, бо повторна серіалізація змінює байти
Перевірка хешує два сегменти ByteRange вихідних байтів рівно такими, як вони серіалізовані; розібрана модель у пам'яті марна, бо повторна серіалізація навіть незміненого документа дає інші байти

Читання метаданих підпису до будь-якої перевірки

GetLoadedSignatureInfo розбирає словник підпису та його контейнер CMS, не торкаючись жодного байта документа, і це робить його правильним першим викликом тоді, коли вам треба лише показати, хто підписав і коли. Поля підписів індексуються з 0 у порядку полів форми, а GetLoadedSignatureFieldCount каже, скільки їх існує. Повернутий запис THPDFSignatureInfo несе ім'я поля, /SubFilter, загальне ім'я сертифіката підписувача, розрізнювальні імена суб'єкта та видавця, серійний номер, дати чинності, час підписання (з підписаного атрибута, коли він є, інакше із запису /M у словнику), назву алгоритму хешування, а також рядки /Reason, /Location та /ContactInfo. Його член Status лишається svNotVerified - чесна мітка для "розібрано, не перевірено"

var
  Pdf: THotPDF;
  Info: THPDFSignatureInfo;
  I: Integer;
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.LoadFromFile('signed-contract.pdf');
    for I := 0 to Pdf.GetLoadedSignatureFieldCount - 1 do
    begin
      Info := Pdf.GetLoadedSignatureInfo(I);
      Writeln('Field:     ', Info.FieldName);
      Writeln('Signer:    ', Info.SignerName);
      Writeln('Issuer:    ', Info.IssuerDN);
      Writeln('Algorithm: ', Info.HashAlgorithm);
      Writeln('SubFilter: ', Info.SubFilter);
    end;
  finally
    Pdf.Free;
  end;
end;

Запуск криптографічної перевірки

VerifyLoadedSignatureEx виконує повну перевірку для документа, завантаженого з файлу, і повертає заповнений запис інформації одним викликом: він знову відкриває вихідний файл, хешує сегменти /ByteRange алгоритмом хешування з SignerInfo, порівнює результат із підписаним атрибутом messageDigest (RFC 5652 §5.4), а потім звіряє RSA-підпис над повторно закодованим DER SET підписаних атрибутів. Коли підпис не несе підписаних атрибутів, перевірка RSA виконується прямо над хешем документа. Підтримуються підписи RSA PKCS#1 v1.5 з хешами SHA-1, SHA-256, SHA-384 чи SHA-512, а це покриває підфільтри adbe.pkcs7.detached та ETSI.CAdES.detached, які видають основні інструменти підписання

Конвеєр VerifyLoadedSignatureEx у HotPDF для Delphi: повторне відкриття вихідного файлу, хешування сегментів ByteRange, порівняння з підписаним атрибутом CMS messageDigest, потім звірка RSA PKCS#1 v1.5, що дає svValid, svDigestMismatch або svSignatureInvalid
VerifyLoadedSignatureEx перехешовує сегменти ByteRange, порівнює їх із підписаним атрибутом messageDigest і звіряє RSA над DER SET підписаних атрибутів, перш ніж повідомити svValid чи конкретний статус збою
var
  Status: THPDFSignatureVerifyStatus;
  Info: THPDFSignatureInfo;
begin
  Status := Pdf.VerifyLoadedSignatureEx(0, Info);
  case Status of
    svValid:
      if Info.CoversWholeDocument then
        Writeln('Valid; signature covers the whole file')
      else
        Writeln('Valid; file was extended after signing');
    svDigestMismatch:
      Writeln('Document bytes changed after signing');
    svSignatureInvalid:
      Writeln('RSA check failed over signed attributes');
    svUnsupportedAlgorithm:
      Writeln('Non-RSA key or unknown digest algorithm');
    svMalformed:
      Writeln('CMS container could not be parsed');
    svSourceUnavailable:
      Writeln('No source bytes; use the TStream overload');
  end;
end;

Дві деталі реалізації варто знати, бо вони пояснюють збої, які ззовні виглядають загадковими. По-перше, перевірка підписаних атрибутів прискіплива до кодування: усередині файлу атрибути позначені як [0] IMPLICIT, але підпис обчислювався над їхньою формою DER SET OF, тож верифікатор перепозначає їх перед хешуванням, рівно так, як вимагає RFC 5652 §5.4. Саморобний верифікатор, який хешує байти в тому вигляді, у якому вони лежать у файлі, відхилятиме кожен правильно підписаний документ. По-друге, /Contents за домовленістю доповнюється нулями до зарезервованого байтового бюджету, тож верифікатор обрізає блок DER до фактичної довжини його зовнішньої SEQUENCE перед розбором; хвостові нулі, що виглядають сміттям, є нормою, а не пошкодженням. Та сама родина небезпек розбору ASN.1, тільки з боку імпорту сертифікатів, є темою статті про зміцнення безпеки PKCS#12 та ASN.1 у HotPDF

Що насправді гарантує дійсний підпис?

svValid означає рівно таке: байти, названі в /ByteRange, хешуються у значення, яке підписав підписувач, і підпис звіряється під відкритим ключем сертифіката, вбудованого в контейнер CMS. Це цілісність байтів плюс прив'язка до ключа, і нічого більше. Перевірка ланцюжка сертифікатів і довіри явно лежить поза межами верифікатора HotPDF: він не проходить ланцюжок до кореня, не перевіряє відкликання й не звертається до жодного сховища довіри. Самопідписаний сертифікат зловмисника, який перепідписав змінений документ, звіриться як svValid, бо математика внутрішньо узгоджена. Чи є підписувач тим, ким себе називає, і чи має хтось йому довіряти, - це рішення політики, яке належить окремому шару, чи то білому списку сертифікатів вашої організації, чи то сховищу сертифікатів Windows, чи то органу перевірки

Прапорець CoversWholeDocument стереже тоншу прогалину. Підпис завжди покриває лише свій /ByteRange, а механізм інкрементних оновлень PDF дозволяє дописувати вміст після підпису, не роблячи його недійсним, що є задумом і саме так працюють процеси з кількома підписами. Прапорець обчислюється під час перевірки й дорівнює true лише тоді, коли два сегменти плюс проміжок /Contents охоплюють увесь файл. Коли svValid приходить із CoversWholeDocument, що дорівнює false, підписана редакція ціла, але файл містить пізніші доповнення, і чи терпіти те, що ці доповнення змінили, має вирішити ваш робочий процес

Схема меж того, що гарантує svValid у перевірці підписів HotPDF: цілісність байтів ByteRange і прив'язка до ключа, тоді як проходження ланцюжка, відкликання та сховища довіри лишаються поза межами, а CoversWholeDocument позначає дописані інкрементні оновлення
svValid означає цілісність байтів плюс прив'язку до ключа й нічого більше; проходження ланцюжка, відкликання та сховища довіри належать окремому шару політики, а CoversWholeDocument=false сигналізує про байти, дописані після підписання

Документи, завантажені з потоку, та зашифровані потребують власних вихідних байтів

Безпараметричні VerifyLoadedSignature та VerifyLoadedSignatureEx залежать від того, що компонент пам'ятає, з якого файлу прийшов документ. Завантажте документ із потоку - і імені файлу, який можна знову відкрити, не буде; те саме стосується шляху перезавантаження з паролем, який використовують зашифровані документи, тобто робочого процесу, описаного в статті про шифрування PDF з AES-256 у HotPDF. В обох випадках файлові перевантаження повертають svSourceUnavailable, а не гадають. Ліками є перевантаження з TStream, яке дає вам передати початкові сирі байти звідки завгодно, де ви їх зберегли: з файлу, який у вас досі є, з буфера в пам'яті, з великого об'єкта в базі даних

var
  Src: TFileStream;
  Status: THPDFSignatureVerifyStatus;
  Info: THPDFSignatureInfo;
begin
  // Документ, завантажений із потоку: компонент не має імені
  // вихідного файлу, тож початкові байти постачаєте ви самі.
  Src := TFileStream.Create('signed-contract.pdf',
    fmOpenRead or fmShareDenyWrite);
  try
    Status := Pdf.VerifyLoadedSignature(0, Src, Info);
    if Status <> svValid then
      Writeln('Verification failed: ', Ord(Status));
  finally
    Src.Free;
  end;
end;

Повідомлення про те, чого ви перевірити не можете

Верифікатор, який знає лише "дійсний" та "недійсний", неправильно звітуватиме про документи, яких він просто не розуміє, тож перелік статусів розділяє випадки, які ваш інтерфейс має розрізняти. svDigestMismatch означає, що байти документа змінилися після підписання, класичний сигнал підробки. svSignatureInvalid означає, що байти хешуються правильно, але перевірка RSA не пройшла, і це вказує на пошкоджене чи підроблене значення підпису. svUnsupportedAlgorithm є чесною відповіддю для ключів ECDSA та нерозпізнаних хешів: підпис може бути цілком добрим, HotPDF просто не може його перевірити, а повідомляти про це як про "недійсний" означало б обмовити здоровий документ. svMalformed позначає контейнер CMS, який узагалі не вдалося розібрати. Для перевірок у стилі шлагбаума VerifyAllLoadedSignatures повертає true лише тоді, коли існує щонайменше одне поле підпису й кожне з них звіряється як svValid, - зручний єдиний булевий прапорець для конвеєра приймання в архів, який відмовляє всьому меншому

Перевірка підписів, підписання PAdES, шифрування AES-256 та API редагування завантажених документів постачаються в одній нативній бібліотеці VCL для Delphi та C++Builder, без залежностей від зовнішніх DLL; повний перелік можливостей і підтримувані версії IDE є на сторінці продукту HotPDF Delphi Component