Technical Article

PDF цифрови подписи и PAdES в Delphi с HotPDF

PDF подписът е предимно отчитане на байтове и именно в това отчитане нещата се объркват. Криптографията работи с код, който е одитиран в продължение на две десетилетия, и тази част почти никога не се проваля. Това, което се проваля в производствена среда, е по-просто: запазен контейнер, който е твърде малък за истинския подпис, хеш, взет върху грешна част от файла, или „записванеâ€?след подписване, което тихомълком пренаписва байтове, които подписът вече е замразил. Разположете байтовете правилно и зелената отметка ще се погрижи сама за себе си.

HotPDF покрива подписването за Delphi и C++Builder на три нива, като избирате между тях, отговаряйки на един въпрос: къде се намира частният ключ? PFX файл на диска изисква еднократно повикване на функция. Ключ, заключен в HSM или услуга за отдалечено подписване, се нуждае от последователността резервация-хеширане-вмъкване, тъй като нито една библиотека не може да бръкне в токен и да извлече ключа. Подпис, който трябва да отговаря на европейските регулации, се нуждае допълнително и от базовите структури на PAdES. Разделите по-долу следват тази прогресия.

Как /ByteRange фиксира подписаните байтове

Подписът трябва да живее вътре във файла, който подписва, и не може да подпише самия себе си. PDF заобикаля този парадокс, като оставя празнина. Преди подписване записващото устройство резервира запис /Contents с фиксиран размер, пълен с нули, и регистрира масив /ByteRange за двата диапазона от двете му страни: всичко преди празнината и всичко след нея. Подписващият хешира тези два диапазона и записва получената CMS структура в празнината като шестнадесетичен код. Капанът е в думата фиксиран. Вие се ангажирате с размера на тази празнина, преди да знаете колко голям ще бъде крайният подпис, така че резервацията трябва да бъде сигурна надценка. Осем килобайта удобно съдържат отделен CMS подпис с кратка верига от сертификати.

HotPDF разделя двата случая на две извиквания и объркването им е честа ранна грешка. AddSignatureField поставя празно, видимо поле, което потребителят да подпише по-късно в четец на PDF. 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. Write the document with a reserved /Contents hole
  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. Load the saved bytes; the returned offsets are 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. Hash both spans and sign externally (HSM, token, service)
  HashInput := Copy(PdfBytes, R1Start + 1, R1Len) +
               Copy(PdfBytes, R2Start + 1, R2Len);
  SigHex := SignWithHsm(HashInput);  // your integration: returns CMS as hex

  // 4. Splice the signature into the reserved hole
  THotPDF.InsertSignatureHex(PdfBytes, SigHex);
  Fs := TFileStream.Create('signed.pdf', fmCreate);
  try
    Fs.WriteBuffer(PdfBytes[1], Length(PdfBytes));
  finally
    Fs.Free;
  end;
end;

Две подробности в тази последователност причиняват повечето от периодичните проблеми. Първата е, че PreparePDFForSigning работи върху байтовете на завършен файл. Документът с резервираното място трябва да бъде записан и запазен изцяло, преди отместванията (offsets) да означават нещо. Ако ги изчислите спрямо поток, който все още се сглобява, те няма да съвпаднат с байтовете, които впоследствие ще хеширате. Втората подробност е размерът на резервацията. Заявените от вас 8192 байта трябва да поберат крайния CMS код, а подпис, носещ междинни сертификати или декориран от услугата със подписани атрибути, може да надхвърли този размер. InsertSignatureHex няма да увеличи празнината, за да освободи място. Признакът за това е тръбопровод, който работи отлично с един сертификат и се проваля със следващия. Решението е да генерирате отново документа с резервирано място, изчислено от реален подпис, генериран от съответния подписващ, а не на база на предположения.

Базови нива на PAdES и времеви отпечатъци, които поддържат подписа валиден

Ако подписвате съгласно европейските правила, използваният стандарт е ETSI EN 319 142-1, който съчетава четири базови нива на PAdES. B-B е обикновеният подпис. B-T добавя доверен времеви отпечатък, който доказва кога е направен подписът. B-LT вгражда данните за валидиране, сертификатите и данните за оттегляне вътре в документа, за да може той да бъде проверяван години по-късно. B-LTA добавя периодични времеви отпечатъци за документи, така че доказателствата да надживеят алгоритмите, върху които са изградени. HotPDF създава съответните структури от страна на документа за всяко ниво:

// PAdES baseline signature field (ETSI EN 319 142-1)
Pdf.CurrentPage.AddPAdESSignatureField(
  'ApprovalSig', Rect(50, 100, 350, 150), 'B-B',
  'Contract approval', 'Boston, MA', 'legal@example.com');

// Document timestamp: larger reservation for the TSA token and chain
Pdf.CurrentPage.AddDocumentTimestampSignature('ArchiveTS', 16384);

Резервацията от 16384 байта за времевия отпечатък е умишлена. Органът за времеви отпечатъци (TSA) връща токен, който носи своята собствена верига от сертификати, така че обикновено се нуждае от повече място, отколкого 8 KB, които са достатъчни за обикновен подпис. Тези времеви отпечатъци на документи са също така механизмът зад B-LTA: повторното поставяне на времеви отпечатък върху архивиран подпис на всеки няколко години с актуални алгоритми е това, което поддържа документа, подписан през 2026 г., проверим и през 2040 г.

Няколко думи за низовете за причина (reason), местоположение (location) и контакт (contact), които и двете извиквания на полета приемат: те са просто метаданни за удобство и нищо повече. HotPDF ги съхранява като обикновени записи в речник и ги изобразява във видимия облик на подписа, но никой валидатор не ги проверява. Попълвайте ги последователно от данните за работния ви процес, тъй като одиторите ги четат, но никога не ги бъркайте с доказателство. Действителното криптографско твърдение се съдържа изцяло в CMS и неговата верига от сертификати, а валидаторът напълно игнорира видимия текст.

След подписване файлът може само да расте

В момента, в който подписът съществува, байтовете в неговите диапазони се замразяват. Единственият легитимен начин за промяна на файла след това е инкрементално актуализиране по ISO 32000-1 §7.5.6 изисквания, което добавя нови и променени обекти след оригиналните байтове и свързва нова секция за кръстосани препратки обратно към тях. При този подход подписът остава валиден за своята версия и четецът отчита правилното състояние: подписаната версия е непокътната, документът е бил разширен след това. Вместо това, ако ресериализирате целия файл, ще пренапишете подписаните диапазони, което унищожава подписа, дори ако нищо видимо не се е променило. Същият механизъм за версии е и начинът, по който един документ може да носи няколко подписа: всеки нов подпис се разполага в своя инкрементална актуализация, а неговите диапазони обхващат всичко преди него, включително по-ранните подписи. Механиката на добавяне в края на файла и кога е безопасно да се компресират обектите са разгледани в статията за потоци от обекти и инкрементални актуализации.

Струва си да имате предвид две ограничения, докато проектирате. Изходният режим PDF/A на HotPDF отхвърля напълно полетата за подпис, така че архивното съответствие и вграденият подпис трябва да се доставят като отделни файлове. Освен това подписването не гарантира конфиденциалност: то доказва кой е създал документа и че той не е променян оттогава, но всеки може да го прочете. Скриването на съдържанието е отделна задача, която се решава чрез AES-256 шифроване и политика на разрешения.

Каквото и да разработите, тествайте го с нещо различно от кода, който е записал файла. Отворете резултата в панела за подписи на Acrobat и потвърдете три неща: подписът е валиден, идентичността се свързва с очаквания от вас коренов сертификат и панелът не отчита промени след подписването. След това променете един байт в подписания диапазон на тестово копие и се уверете, че панелът вече определя документа като променен. Процес на подписване, при който никога не сте виждали отхвърляне на подправен файл, всъщност не е бил изцяло тестван за проверка.

И трите нива на подписване се доставят с компонента HotPDF за Delphi и C++Builder; продуктовата страница съдържа връзка към пълния справочник за API на подписите.