Підпис PDF — це здебільшого облік байтів, і саме на обліку байтів усе й ламається. Криптографія працює на коді, який перевіряли й аудіювали два десятиліття, і ця частина майже ніколи не підводить. У продакшені зазвичай підводить щось скромніше: заповнювач (placeholder), зарезервований замалим для реального підпису, хеш, узятий не з того відрізка файлу, або "збереження" після підписання, яке тихо переписало байти, що підпис уже заморозив. Розкладіть байти правильно — і зелена галочка подбає про себе сама
HotPDF охоплює підписання для Delphi та C++Builder на трьох рівнях, і вибір між ними зводиться до однієї відповіді: де живе приватний ключ? Файл PFX на диску потребує лише одного виклику функції. Ключ, замкнений в HSM або віддаленому сервісі підписання, потребує послідовності «резервувати-хешувати-вставити», бо жодна бібліотека не може дотягнутися до токена й дістати з нього ключ. Підпис, що має задовольняти європейське регулювання, потребує додатково базових структур PAdES. Розділи нижче йдуть саме цим прогресом
Як /ByteRange фіксує підписані байти
Підпис має жити всередині файлу, який він підписує, а сам себе підписати не може. PDF обходить цей парадокс, залишаючи діру. Перед підписанням модуль запису резервує запис /Contents фіксованого розміру, заповнений нулями, і фіксує масив /ByteRange для двох відрізків по обидва боки від нього: усе до діри, усе після. Підписувач хешує ці два відрізки й записує отриманий CMS-блок у діру у вигляді шістнадцяткового рядка. Пастка — у слові фіксованого. Розмір цієї діри доводиться визначати ще до того, як стане відомо, наскільки великим виявиться готовий підпис, тож резервування має бути впевненою оцінкою із запасом. Вісім кілобайтів із запасом вміщують відокремлений (detached) підпис CMS із коротким ланцюжком сертифікатів
HotPDF розділяє ці два випадки на два виклики, і їх сплутування — поширена помилка новачків. AddSignatureField розміщує порожнє, видиме поле, щоб людина підписала його пізніше в переглядачі. AddSignedSignatureField створює поле й резервує діру /Contents — саме це потрібно, коли підпис завершуватиме код, а не людина. Передайте зовнішньому підписувачу порожнє поле — і йому буде нічого заповнювати
Шлях в один виклик: підписання з PFX
Коли сертифікат і його приватний ключ лежать у файлі PFX/PKCS#12, який ваш процес може прочитати, увесь конвеєр зводиться до одного виклику функції класу:
if THotPDF.SignPDFWithPFX('invoice-unsigned.pdf', 'invoice-signed.pdf',
'company-cert.pfx', 'pfx-password') then
Writeln('Signed: invoice-signed.pdf')
else
raise Exception.Create('PFX signing failed');
Коли це не спрацьовує, проблема рідко в PDF. Проблема в PFX. HotPDF читає контейнери, захищені PBES2, тобто з виведенням ключа PBKDF2 поверх AES-256-CBC. PFX, експортований старішим майстром сертифікатів Windows або OpenSSL до версії 3.0, зазвичай натомість загорнутий у застарілий RC2 чи 3DES, і просто не розбереться. Виправлення — переекспортувати контейнер один раз із сучасним захистом; сучасний OpenSSL робить це за замовчуванням, і це не зміна коду. Тож коли підписання миттєво падає на сертифікаті, що "працює всюди", спершу подивіться, як був створений PFX, і лише потім підозрюйте власний код
Шлях «резервувати-хешувати-вставити» для HSM і токенів
Шлях в один виклик передбачає, що ваш процес може прочитати ключ як файл. Дедалі частіше це не так. Ключ сидить в HSM, на USB-токені або за API сервісу підписання, і бібліотека не має способу дотягнутися до нього напряму. HotPDF розв'язує це, розбиваючи підписання на кроки рівня байтів: записати документ-заповнювач, запитати в бібліотеки діапазони для хешування, передати вхідні дані хешу тому, що тримає ключ, а потім вставити повернутий CMS назад у діру
var
Doc: THotPDF;
Fs: TFileStream;
PdfBytes, HashInput, SigHex: AnsiString;
R1Start, R1Len, R2Start, R2Len, CStart, CLen: Integer;
begin
// 1. Записати документ із зарезервованою дірою /Contents
Doc := THotPDF.Create(nil);
try
Doc.FileName := 'placeholder.pdf';
Doc.BeginDoc;
Doc.CurrentPage.AddSignedSignatureField('Sig1',
Rect(50, 100, 350, 150), 8192, 'adbe.pkcs7.detached',
'Contract approval', 'Boston, MA', 'legal@example.com');
Doc.EndDoc;
finally
Doc.Free;
end;
// 2. Завантажити збережені байти; повернуті зсуви є 0-based
Fs := TFileStream.Create('placeholder.pdf', fmOpenRead);
try
SetLength(PdfBytes, Fs.Size);
Fs.ReadBuffer(PdfBytes[1], Fs.Size);
finally
Fs.Free;
end;
THotPDF.PreparePDFForSigning(PdfBytes, R1Start, R1Len, R2Start, R2Len,
CStart, CLen);
// 3. Хешувати обидва відрізки й підписати зовні (HSM, токен, сервіс)
HashInput := Copy(PdfBytes, R1Start + 1, R1Len) +
Copy(PdfBytes, R2Start + 1, R2Len);
SigHex := SignWithHsm(HashInput); // ваша інтеграція: повертає CMS у hex
// 4. Вставити підпис у зарезервовану діру
THotPDF.InsertSignatureHex(PdfBytes, SigHex);
Fs := TFileStream.Create('signed.pdf', fmCreate);
try
Fs.WriteBuffer(PdfBytes[1], Length(PdfBytes));
finally
Fs.Free;
end;
end;
Дві деталі в цій послідовності спричиняють більшість переривчастих збоїв. Перша — PreparePDFForSigning працює з байтами вже готового файлу. Заповнювач має бути повністю записаний і збережений, перш ніж зсуви набудуть сенсу; обчисліть їх для потоку, який усе ще збирається, і вони не збіжаться з байтами, які ви зрештою хешуватимете. Друга деталь — знову розмір резервування. Запитані вами 8192 байти мають вмістити кінцевий CMS, а підпис, що несе проміжні сертифікати, або той, який сервіс прикрашає підписаними атрибутами, може вийти за ці межі. InsertSignatureHex не розширить діру, щоб звільнити місце. Ознака — конвеєр, що успішно підписує з одним сертифікатом і падає з наступним; ліки — перегенерувати заповнювач із резервуванням, виміряним за реальним підписом, отриманим від справжнього підписувача, а не вгаданим
Базові рівні PAdES і мітки часу, що підтримують підпис живим
Якщо ви підписуєте за європейськими правилами, чинний стандарт — ETSI EN 319 142-1, який вибудовує чотири базові рівні PAdES. B-B — це звичайний підпис. B-T додає довірену мітку часу, що доводить, коли підпис було створено. B-LT вбудовує матеріали для перевірки — сертифікати й дані про відкликання — прямо в документ, щоб його можна було перевірити ще й через роки. B-LTA додає поверх періодичні мітки часу документа, тож докази переживають алгоритми, на яких їх побудовано. HotPDF генерує структури на боці документа для кожного рівня:
// Поле базового підпису PAdES (ETSI EN 319 142-1)
Pdf.CurrentPage.AddPAdESSignatureField(
'ApprovalSig', Rect(50, 100, 350, 150), 'B-B',
'Contract approval', 'Boston, MA', 'legal@example.com');
// Мітка часу документа: більше резервування для токена TSA та ланцюжка
Pdf.CurrentPage.AddDocumentTimestampSignature('ArchiveTS', 16384);
Резервування у 16384 байти для мітки часу — свідомий вибір. Служба міток часу (timestamp authority) повертає токен, що тягне за собою власний ланцюжок сертифікатів, тож йому зазвичай потрібно більше місця, ніж вистачає звичайному підпису в 8 КБ. Ці мітки часу документа — це також механізм, що стоїть за B-LTA: повторне проставлення мітки часу на архівному підписі раз на кілька років, за допомогою ще актуальних алгоритмів, — саме те, що дозволяє документу, підписаному у 2026 році, залишатися перевірюваним у 2040-му
Кілька слів про рядки reason, location і contact, які приймають обидва виклики полів: це лише зручні метадані і нічого більше. HotPDF зберігає їх як звичайні записи словника й малює у видимому оформленні підпису, але жоден валідатор нічого проти них не звіряє. Заповнюйте їх послідовно даними з вашого робочого процесу, оскільки аудитори їх таки читають, але ніколи не сприймайте їх як доказ. Справжнє криптографічне твердження живе цілком у CMS та його ланцюжку сертифікатів, а перевіряльник повністю ігнорує видимий текст
Після підписання файл може лише зростати
У момент появи підпису байти в межах його діапазонів заморожуються. Єдиний законний спосіб потім змінити файл — інкрементне оновлення за ISO 32000-1 §7.5.6, яке дописує нові й змінені об'єкти після початкових байтів і прив'язує до них новий розділ перехресних посилань. Зроблено так — і підпис лишається дійсним для своєї ревізії, а переглядач чесно повідомляє стан: підписана ревізія непошкоджена, документ пізніше розширили. Натомість повна пересеріалізація файлу переписує підписані відрізки, що руйнує підпис, навіть коли візуально нічого не змінилося. Той самий механізм ревізій дозволяє й одному документу нести кілька підписів: кожен новий підпис потрапляє у своє власне інкрементне оновлення, а його діапазони охоплюють усе, що було перед ним, включно з попередніми підписами. Механіку «лише дописування», і коли її безпечно ущільнювати, розглянуто в статті про потоки об'єктів та інкрементні оновлення
Варто тримати в голові два обмеження під час проєктування. Режим виводу PDF/A у HotPDF повністю відхиляє поля підпису, тож архівна відповідність і вбудований підпис мають постачатися як окремі файли. І підписання нічого не каже про секретність: воно доводить, хто створив документ і що він відтоді не змінювався, але прочитати його може будь-хто. Приховування вмісту — це окреме завдання, яке вирішує шифрування AES-256 і політика дозволів
Що б ви не будували, перевіряйте це чимось іншим, ніж код, який писав файл. Відкрийте результат у панелі підписів Acrobat і підтвердьте три речі: підпис дійсний, ланцюжок ідентичності веде до очікуваного кореня, і панель не повідомляє про жодних змін після підписання. Потім змініть один-єдиний байт усередині підписаного діапазону в одноразовій копії і перевірте, що тепер панель називає документ зміненим. Конвеєр підписання, який ви жодного разу не бачили за відхиленням підробленого файлу, — це конвеєр, чия перевірка насправді не була протестована
Усі три рівні підписання постачаються з HotPDF Delphi Component для Delphi та C++Builder; сторінка продукту містить посилання на повний довідник API підписання