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

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

HotPDF перевіряє цифрові підписи в завантажених документах PDF за допомогою трьох методів THotPDF: GetLoadedSignatureInfo, VerifyLoadedSignature, та VerifyLoadedSignatureEx, представлених у версії v2.259.0. Компонент повторно хешує сегменти /ByteRange оригінального файлу, перевіряє атрибут messageDigest CMS та виконує перевірку 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 сирих байтів, які ви надаєте, і ніколи щодо його представлення в пам'яті

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

Метод GetLoadedSignatureInfo аналізує словник підпису та його контейнер CMS, не торкаючись жодного байта документа, що робить його правильним першим викликом, коли вам потрібно лише відобразити, хто і коли підписав документ. Поля підпису індексуються з 0 у порядку полів форми, а GetLoadedSignatureFieldCount повідомляє, скільки їх існує. Повернений запис THPDFSignatureInfo містить назву поля, /SubFilter, загальне ім'я сертифіката підписанта, відмітні імена (DN) суб'єкта та емітента, серійний номер, дати дії, час підписання (із підписаного атрибута, якщо він є, інакше із запису /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 за допомогою PKCS#1 v1.5 щодо повторного кодування DER SET підписаних атрибутів. Коли підпис не містить підписаних атрибутів, перевірка RSA виконується безпосередньо щодо хешу документа. Підтримуються підписи RSA PKCS#1 v1.5 з дайджестами SHA-1, SHA-256, SHA-384 або SHA-512, що охоплює субфільтри adbe.pkcs7.detached та ETSI.CAdES.detached, які створюються основними інструментами підписання

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 дозволяє додавати вміст після підпису без його анулювання, що зроблено навмисно і саме так працюють процеси з кількома підписами. Прапорець обчислюється під час перевірки і є істинним лише тоді, коли два сегменти плюс проміжок /Contents охоплюють весь файл. Коли повертається svValid з прапорцем CoversWholeDocument у значенні false, це означає, що підписана ревізія є недоторканою, але файл містить пізніші додавання, і ваш робочий процес повинен вирішити, чи допускати ці додавання

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

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

var
  Src: TStream;
  Status: THPDFSignatureVerifyStatus;
  Info: THPDFSignatureInfo;
begin
  // Stream-loaded document: the component holds no source
  // file name, so supply the original bytes yourself.
  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;

Reporting what you cannot verify

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

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