Коротко. Подпись PDF — это подпись CMS (PKCS #7), которая хранится в записи /Contents словаря подписи. Она охватывает байты, перечисленные в /ByteRange, то есть весь файл на момент подписания, кроме самого значения /Contents. Валидаторы отвечают на три отдельных вопроса. Целостность: по-прежнему ли подписанные байты соответствуют подписи? Доверие: доходит ли цепочка сертификатов подписанта до корневого сертификата, которому доверяет валидатор? Изменения: что дописано после подписания и допустимо ли это? Вердикт «подпись действительна, но доверие не подтверждено» (жёлтый вопросительный знак в Acrobat) почти всегда означает, что байты не изменены, но центра сертификации (CA) подписанта нет в списке доверия программы для чтения PDF. PAdES (ETSI EN 319 142-1) определяет уровни от B-B до B-LTA. Метка времени RFC 3161 (B-T) доказывает, в какой момент подпись уже существовала, а встроенные данные для проверки вместе с метками времени документа (B-LT, B-LTA) позволяют проверить подпись и после того, как истечёт срок действия сертификатов.
Что внутри подписанного PDF
В тесте ниже одностраничный счёт подписывается с помощью pyHanko 0.38 (CLI 0.6.0) ключом P-256 от небольшого тестового CA, а метку времени выдаёт публичная служба меток времени (TSA) компании DigiCert:
$ pyhanko sign addsig --field Sig1 --use-pades --timestamp-url http://timestamp.digicert.com --reason Approved \
pemder --key signer.key --cert signer.pem --chain root.pem --no-pass invoice.pdf invoice-signed.pdf
Подписывающая программа не переписывает файл. Она дописывает к нему инкрементное обновление (incremental update): новые объекты, новый раздел перекрёстных ссылок и новый трейлер после исходного %%EOF. Исходный файл размером 627 байт целиком, без единого изменённого байта, остаётся в начале подписанного. Обновление добавляет поле подписи и словарь подписи. Показанный ниже словарь получен при другом запуске той же команды, поэтому числа в нём не совпадают с числами файла, который разбирается дальше (шестнадцатеричное значение /Contents сокращено):
9 0 obj
<<
/Contents <30821CFC06092A864886F70D010702A0821CED3...(zeros)...0000000>
/ByteRange [0 1331 23601 1118]
/Type /Sig
/Filter /Adobe.PPKLite
/SubFilter /ETSI.CAdES.detached
/M (D\07220261020140517Z)
/Reason (Approved)
/Prop_Build <<
/App <<
/Name /pyHanko#20CLI
/REx (0\05638\0560)
>>
>>
>>
endobj
Значение /M — дата PDF D:20261020140517Z, а \072 — экранированное двоеточие. Небольшой скрипт, который читает /ByteRange и размер /Contents, выдаёт числа для файла, на котором построена остальная часть страницы:
file size: 24869 bytes, revisions (%%EOF markers): 2
/ByteRange [0 1331 23751 1118]
signed: bytes 0-1330 and 23751-24868; the 22420-byte gap is the /Contents value
/Type /Sig /SubFilter /ETSI.CAdES.detached
/Contents: 11209 bytes reserved, 7472 bytes used by the CMS object
range ends at end of file: True
Как связаны части:
/ByteRange [a b c d]задаёт два диапазона байтов:bбайт начиная с позицииa, затемdбайт начиная с позицииc. Промежуток между ними — это ровно шестнадцатеричная строка/Contentsвместе с ограничителями. Всё остальное, что было в файле на момент подписания, хешируется. Размер файла становится известен только после того, как подпись займёт своё место, поэтому подписывающие программы сначала записывают заполнитель, заполняют/ByteRange, вычисляют хеш, подписывают и затем записывают подпись в зарезервированное место./Contentsсодержит структуру CMSSignedDataв кодировке DER, записанную в шестнадцатеричном виде и дополненную нулями. pyHanko зарезервировал 11 209 байт и занял 7 472. Остальное — дополнение: место приходится резервировать до того, как станет известен размер подписи./SubFilterназывает формат.ETSI.CAdES.detached— формат PAdES,adbe.pkcs7.detached— более старый формат из ISO 32000-1. Оба формата — отсоединённые подписи CMS, вычисленные по байтам из ByteRange./M— время подписания, заявленное подписантом. В PAdES оно указывается именно здесь: базовый профиль гласит, что атрибут CMSsigning-time«не должен присутствовать» ни на одном уровне. Это всего лишь заявление: любой, у кого есть ключ, может записать любое время.
Объект CMS самый обычный, и OpenSSL проверяет его по извлечённым диапазонам:
CMS Verification successful
1 object: contentType (1.2.840.113549.1.9.3)
1 object: id-smime-aa-signingCertificateV2 (1.2.840.113549.1.9.16.2.47)
1 object: id-smime-aa-timeStampToken (1.2.840.113549.1.9.16.2.14)
1 object: messageDigest (1.2.840.113549.1.9.4)
Подписанные атрибуты — тип содержимого, messageDigest байтов из ByteRange и signingCertificateV2, хеш сертификата подписанта: он не даёт подставить другой сертификат с тем же ключом. Неподписанный атрибут timeStampToken — метка времени RFC 3161, выданная на значение подписи. Инструмент проверки подписей PDF показывает такой разбор для любого PDF, в том числе какие байты охватывает каждая подпись. Инспектор меток времени декодирует встроенный токен.
Три вопроса, на которые отвечает валидатор
Отчёт pyHanko разделяет эти вопросы, поэтому на нём удобно учиться. Если передать корневой сертификат тестового CA как доверенный:
Trust anchor: "Organization: Example GmbH, Common Name: Example Signing Root CA"
The signer's certificate is trusted.
Integrity
---------
The signature is cryptographically sound.
Signing time
------------
Signing time as reported by signer: 2026-10-20T14:04:36+00:00
Signature timestamp token: 2026-10-20T14:04:37+00:00
The token is guaranteed to be newer than the signature.
This timestamp is backed by a time stamping authority.
The timestamp token is cryptographically sound.
TSA certificate subject: "Common Name: DigiCert SHA256 RSA4096 Timestamp Responder 2026 1; Organization: DigiCert, Inc.; Country: US"
The TSA certificate is trusted.
Modifications
-------------
The signature covers the entire file.
Bottom line
-----------
The signature is judged VALID.
Подпись действительна, но доверие не подтверждено
Тот же файл, проверенный без тестового корневого сертификата:
Trust anchor: "No path to trust anchor found."
The signer's certificate is untrusted.
Integrity
---------
The signature is cryptographically sound.
...
Bottom line
-----------
The signature is judged INVALID.
Целостность не нарушена, доверия нет. Именно в этом случае Acrobat выдаёт «Signature validity is UNKNOWN» с пояснением, что удостоверение подписанта неизвестно. Это самая частая жалоба на подписанные PDF, хотя с документом обычно всё в порядке. Программы для чтения PDF доверяют сертификатам подписи по собственным спискам. Acrobat использует Adobe Approved Trust List (AATL) и, если это включено, доверенные списки ЕС (EU Trusted Lists). В это состояние попадают сертификаты корпоративного CA, национальной ID-карты, которую программа не знает, и самоподписанные сертификаты. Исправить это можно на стороне читателя (доверять CA, выпустившему сертификат, или включить доверенные списки ЕС) или на стороне подписанта (подписывать сертификатом от CA, которому доверяют получатели). Обратите внимание на отдельный вердикт для сертификата TSA: цепочка TSA DigiCert доходит до корневого сертификата из системного хранилища, поэтому доверие к метке времени подтверждено, даже когда к подписанту его нет.
Изменённые байты внутри подписанного диапазона
Заменим EUR 1,250.00 на EUR 9,250.00 прямо в файле:
Integrity
---------
The signature is cryptographically unsound.
...
Bottom line
-----------
The signature is judged INVALID.
Здесь обойти проверку невозможно. Каждый байт в диапазонах хешируется, поэтому любая правка на месте делает подпись неверной.
Изменения, дописанные после подписания
Интересные атаки не трогают подписанные байты, а добавляют новую редакцию. PDF допускает инкрементные обновления, и они правомерны, когда нужно заполнить формы, добавить аннотации или новые подписи. Тест дописывает редакцию, которая заменяет содержимое страницы на вариант с новой суммой:
file size: 26337 bytes, revisions (%%EOF markers): 3
/ByteRange [0 1331 23751 1118]
range ends at end of file: False
...
The signature is cryptographically sound.
...
Modifications
-------------
The signature does not cover the entire file.
Some modifications may be illegitimate, and they appear to be incompatible with the current document modification policy.
Bottom line
-----------
The signature is judged INVALID.
Криптографически подпись по-прежнему верна, потому что подписанная редакция не тронута. Но видит читатель новую редакцию. Подмену обнаруживает только анализ различий (difference analysis): сравнение того, что изменилось после подписания, с тем, что разрешено. Программа просмотра, которая проверяет только «подпись верна, подписанту доверяем», покажет сумму 9 250 EUR под действительной подписью. Исследования программ просмотра PDF, проведённые в 2019 году и позже, выявили именно такие пробелы во многих приложениях: атаки через инкрементное сохранение (incremental saving) и теневые атаки (shadow attacks). Добавление второй подписи, напротив, относится к разрешённым изменениям:
Modifications
-------------
The signature does not cover the entire file.
All modifications relate to signing and form filling operations, and they appear to be compatible with the current document modification policy.
Bottom line
-----------
The signature is judged VALID.
Сертифицирующая подпись (--certify в pyHanko, «certify» в Acrobat) записывает в своих разрешениях DocMDP политику автора в отношении последующих изменений: никаких изменений, только заполнение форм или заполнение форм и аннотации. Валидаторы применяют эту политику при анализе различий.
Время: метки времени RFC 3161
Подпись создаётся с сертификатом, срок действия которого истекает и который могут отозвать. Без независимого доказательства того, когда подпись создана, валидатор может проверить сертификат только на текущий момент. Как только срок действия сертификата истёк или сертификат отозван по любой причине, подпись выглядит так же плохо, как созданная вчера украденным ключом. Запись /M здесь не поможет: её выбирает сам подписант.
Эту проблему решает метка времени RFC 3161 от доверенной службы меток времени (timestamp authority, TSA). TSA подписывает токен, в котором содержатся хеш значения подписи и время по часам самой TSA. В тесте TSA DigiCert вернула токен со временем 14:04:37 UTC, через 1 секунду после заявленного времени подписания. Это доказывает, что подпись уже существовала в тот момент, когда сертификат был действителен. Это уровень PAdES B-T. Метку времени можно добавить и отдельно, как метку времени документа (document timestamp): это словарь подписи с /Type /DocTimeStamp и /SubFilter /ETSI.RFC3161, который ставит метку времени на весь документ в его текущем виде.
Долгосрочная проверка: B-LT и B-LTA
Чтобы подпись можно было проверить через 10 лет, одной метки времени мало. CRL или ответчиков OCSP центра сертификации к тому времени может уже не быть, а алгоритмы могут ослабнуть. ETSI EN 319 142-1 (V1.2.1, январь 2024 года) определяет уровни:
| Уровень | Что добавляет | Назначение |
|---|---|---|
| B-B | Подпись CMS с обязательными подписанными атрибутами | Базовая подпись |
| B-T | Доверенная метка времени | Доказывает, что подпись существовала в определённый момент |
| B-LT | Все данные для проверки: сертификаты, CRL и ответы OCSP в хранилище DSS документа | Проверка без обращения к сети |
| B-LTA | Метки времени документа, которые охватывают документ вместе с этими данными и периодически обновляются | Сохраняет доверие к данным и меткам времени, пока истекают сроки действия сертификатов и стареют алгоритмы |
Данные для проверки лежат в хранилище Document Security Store (/DSS в каталоге документа); его добавляет инкрементное обновление после подписания. Подпишем тот же счёт на уровне B-LTA, используя CRL, опубликованный тестовым CA:
file size: 54674 bytes, revisions (%%EOF markers): 4
/ByteRange [0 1331 23745 1099]
/Type /Sig /SubFilter /ETSI.CAdES.detached
range ends at end of file: False
/ByteRange [0 34471 52463 668]
/Type /DocTimeStamp /SubFilter /ETSI.RFC3161
range ends at end of file: False
catalog /DSS: True
/Certs present: True
/CRLs present: True
/OCSPs present: True
/VRI present: True
...
Modifications
-------------
The signature does not cover the entire file.
All modifications relate to signature maintenance, and they appear to be compatible with the current document modification policy.
Bottom line
-----------
The signature is judged VALID.
Четыре редакции: исходный документ; подпись; обновление, которое добавляет DSS с данными для проверки и завершается меткой времени документа, диапазон байтов которой заканчивается на байте 53 131; и последнее обновление размером 1 543 байта, дописанное после метки времени и отнесённое валидатором к обслуживанию подписи (signature maintenance). Если вывести содержимое DSS файла, подписанного тем же способом, видно, что на практике означают «все данные для проверки». Там лежат цепочка сертификатов подписанта и CRL тестового CA, а также цепочка ответчика меток времени DigiCert с двумя ответами OCSP, потому что метка времени тоже должна оставаться проверяемой:
certs:
Organization: Example GmbH, Common Name: Example Signing Root CA
Organization: Example GmbH, Common Name: Alice Example
Common Name: DigiCert Trusted Root G4, Organizational Unit: www.digicert.com, Organization: DigiCert Inc, Country: US
Common Name: DigiCert Trusted G4 TimeStamping RSA4096 SHA256 2025 CA1; Organization: DigiCert, Inc.; Country: US
Common Name: DigiCert SHA256 RSA4096 Timestamp Responder 2026 1; Organization: DigiCert, Inc.; Country: US
CRLs:
Organization: Example GmbH, Common Name: Example Signing Root CA
OCSP responses:
ef6f534ae9e4067c7acae29056f62fd449eccb4e
ecd7e382d2715d644cdf2e673fe7ba98ae1c0f4f
Ответчики OCSP указаны хешем ключа. Метку времени документа, в свою очередь, нужно обновить до того, как истечёт срок действия её сертификата TSA: новую метку добавляет pyhanko sign ltaupdate. Подписи, данные для проверки которых встроены в документ, Acrobat помечает как «LTV enabled».
Какой уровень создавать
- Всегда ставьте метку времени. B-B достаточно только для документов, которые прочитают и сдадут в архив, пока сертификат действует, но даже тогда метка времени стоит всего одного HTTP-запроса к TSA. Минимум — B-T.
- Для всего, что архивируется, используйте B-LTA. Договорам, документам, на которые распространяются требования регуляторов, и счетам, которые должны проверяться спустя годы, нужны встроенные данные для проверки и метки времени документа. Запускайте
ltaupdateпо расписанию с запасом: задолго до того, как истечёт срок действия сертификата TSA у самой новой метки времени документа, и до того, как её алгоритмы начнут считаться слабыми. - Юридическая сила — отдельный вопрос. Согласно регламенту eIDAS (Регламент (ЕС) № 910/2014) для квалифицированной электронной подписи нужны квалифицированный сертификат от поставщика доверенных услуг из доверенных списков ЕС и квалифицированное средство создания подписи. PAdES описывает формат файла, а не этот юридический уровень. Параметры pyHanko
--eutl-territoriesи--require-qualifiedпроверяют то, что касается доверенных списков. - Подписывайте сертификатом от CA, которому доверяют ваши читатели. Для внешних получателей это CA из AATL или из доверенных списков ЕС. Внутренний CA подходит, если у каждого читателя можно настроить доверие к нему.
Как проверить подписанный PDF
- Смотрите на все подписи и редакции, а не только на зелёную плашку. Сколько редакций следует за последней подписью и что они меняют?
- Разделяйте вердикты. Не пройдена проверка целостности → подписанные байты изменились. Не подтверждено доверие → это вопрос настройки доверия на стороне читателя. Отмечены изменения → после подписания добавлено содержимое, и нужно посмотреть, какое.
- Проверьте доказательства времени. Есть ли метка времени от TSA, которой вы доверяете, или только
/Mот подписанта? - Для архивов проверьте LTV. Встроены ли данные для проверки и действует ли ещё сертификат TSA последней метки времени документа?
- Используйте валидатор, который сообщает обо всём этом. В скриптах —
pyhanko sign validate --pretty-printс явным--trustилиpyhanko sign adesverifyв стиле AdES. pyHanko 0.38 предупреждает, что использование хранилища доверенных сертификатов операционной системы в качестве запасного варианта устарело и будет удалено в одном из следующих выпусков («is deprecated and will be removed in a future release»), поэтому передавайте доверенные корневые сертификаты явно. В браузере то же самое делает инструмент проверки подписей PDF, не загружая документ.
Типичные ошибки
- Доверие к видимому изображению подписи. Штамп на странице — украшение. Подпись — это объект CMS, а изображение ничего не доказывает.
- Отождествление «подпись действительна» с «документ не изменён». О том, что документ не изменён, говорит только вывод «covers the entire file» или анализ различий без замечаний.
- Добавление собственного сертификата подписанта в доверенные, чтобы убрать предупреждение. Доверяйте выпустившему его CA, предварительно проверив этот CA, или не доверяйте вовсе.
- Нет метки времени. Без метки времени от TSA подпись перестаёт что-либо доказывать, когда истекает срок действия сертификата.
- Повторное сохранение подписанного PDF в инструменте, который переписывает файл. Многие редакторы выполняют полное сохранение, а не инкрементное, и это делает недействительными все существующие подписи.
Источники
- ISO 32000-2 (PDF 2.0), § 12.8, цифровые подписи. ISO 32000-1 определяет
adbe.pkcs7.detachedи словарь подписи. - ETSI EN 319 142-1 V1.2.1 (2024-01): компоненты PAdES и базовые уровни, метки времени документа.
- RFC 5652 (CMS), RFC 3161 (метки времени), RFC 5035 (signingCertificateV2).
- pyHanko 0.38.0: документация и CLI.
- PDF Insecurity: исследования атак через инкрементное сохранение, атак с обёрткой подписи (signature wrapping) и теневых атак.
Все файлы на этой странице созданы 20 октября 2026 года в node:24.22.0-trixie с помощью pyHanko 0.38.0 (pyhanko-cli 0.6.0, pyhanko-certvalidator 0.33.0) в virtualenv и OpenSSL 3.5.7, а метки времени получены от http://timestamp.digicert.com.