Artykuł techniczny

HotPDF: digital signatures and PAdES-ready signing in Delphi

Podpis PDF to głównie księgowanie bajtów, i to właśnie księgowanie bajtów jest miejscem, gdzie coś idzie źle. Kryptografia działa na kodzie audytowanym od dwóch dekad, i ta część niemal nigdy nie zawodzi. To, co zawodzi na produkcji, jest skromniejsze: symbol zastępczy zarezerwowany zbyt mały dla prawdziwego podpisu, hash wzięty z niewłaściwego odcinka pliku, albo "zapis" po podpisaniu, który po cichu przepisał bajty, które podpis już zamroził. Ułóż bajty poprawnie, a zielony znacznik zaakceptowania zajmie się sobą sam

HotPDF pokrywa podpisywanie dla Delphi i C++Builder na trzech poziomach, a wybierasz między nimi, odpowiadając na jedno pytanie: gdzie mieszka klucz prywatny? Plik PFX na dysku potrzebuje jednego wywołania funkcji. Klucz zamknięty w HSM albo w zdalnej usłudze podpisywania potrzebuje sekwencji rezerwuj-hashuj-wstaw, bo żadna biblioteka nie potrafi sięgnąć do tokenu i wyciągnąć z niego klucza. Podpis, który musi spełniać europejskie regulacje, potrzebuje ponad to struktur bazowych PAdES. Poniższe sekcje podążają za tą progresją

Diagram decyzyjny wybierający między jednowywołaniowym podpisywaniem PFX HotPDF, ścieżką rezerwuj-hasz-wstaw, gdy klucz siedzi w HSM lub zdalnej usłudze, a strukturami bazowymi PAdES dla regulowanego europejskiego podpisywania
Wybierz poziom podpisywania, pytając, gdzie mieszka klucz prywatny; czytelny plik PFX zwija podpisywanie do jednego wywołania, klucze trzymane w tokenie wymuszają objazd na poziomie bajtów, a europejskie prawo dokłada warstwę PAdES

Jak /ByteRange precyzuje podpisane bajty

Podpis musi mieszkać wewnątrz pliku, który podpisuje, a nie może podpisać samego siebie. PDF omija ten paradoks, zostawiając dziurę. Przed podpisaniem zapisujący rezerwuje wpis /Contents o ustalonym rozmiarze, wypełniony zerami, i zapisuje tablicę /ByteRange dla dwóch odcinków po obu jego stronach: wszystko przed dziurą, wszystko po niej. Podpisujący haszuje te dwa odcinki i zapisuje wynikowy blob CMS w dziurze jako szesnastkowe cyfry. Pułapka tkwi w słowie ustalonym. Zobowiązujesz się do rozmiaru tej dziury, zanim wiesz, jak duży będzie gotowy podpis, więc rezerwacja musi być pewnym siebie zawyżeniem. Osiem kilobajtów wygodnie mieści odłączony podpis CMS z krótkim łańcuchem certyfikatów

HotPDF dzieli te dwa przypadki na dwa wywołania, a ich pomylenie to częsty początkujący błąd. AddSignatureField umieszcza puste, widoczne pole, które osoba podpisze później w przeglądarce. AddSignedSignatureField tworzy pole i rezerwuje dziurę /Contents, czyli to, czego chcesz zawsze, gdy podpis dokończy kod, a nie człowiek. Podaj zewnętrznemu podpisującemu puste pole, a nie będzie miał czego wypełnić

Ścieżka jednego wywołania: podpisywanie z PFX

Gdy certyfikat i jego klucz prywatny siedzą w pliku PFX/PKCS#12, który twój proces potrafi odczytać, cały potok sprowadza się do jednej funkcji klasowej:

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');

Gdy to zawodzi, rzadko problemem jest PDF. Problemem jest PFX. HotPDF odczytuje kontenery chronione PBES2, czyli wyprowadzanie klucza PBKDF2 nad AES-256-CBC. PFX wyeksportowany przez starszy kreator certyfikatów Windows, albo przez OpenSSL sprzed wersji 3.0, jest zwykle owinięty zamiast tego przestarzałym RC2 albo 3DES, i po prostu się nie sparsuje. Naprawą jest ponowny eksport kontenera raz, z nowoczesną ochroną; dzisiejszy OpenSSL robi to domyślnie, i nie jest to zmiana w kodzie. Więc gdy podpisywanie umiera natychmiast na certyfikacie, który "działa wszędzie indziej," spójrz na to, jak PFX powstał, zanim podejrzewasz własny kod

Ścieżka rezerwuj-hashuj-wstaw dla HSM-ów i tokenów

Ścieżka jednego wywołania zakłada, że twój proces potrafi odczytać klucz jako plik. Coraz częściej nie potrafi. Klucz siedzi w HSM, na tokenie USB, albo za API usługi podpisywania, i biblioteka nie ma jak sięgnąć do niego bezpośrednio. HotPDF radzi sobie z tym, rozbijając podpisywanie na kroki na poziomie bajtów: zapisz dokument-symbol zastępczy, poproś bibliotekę o zakresy do haszowania, przekaż wejście hasza do tego, co trzyma klucz, a potem wklej zwrócony CMS z powrotem do dziury

HotPDF: Czterostopniowy potok rezerwuj-hasz-wstaw nad placeholder.pdf pokazujący zarezerwowaną dziurę /Contents między dwoma zakresami ByteRange i HSM wymieniający skrót na szesnastkowy CMS
HotPDF rezerwuje dziurę i raportuje oba zakresy ByteRange, twój depozytariusz klucza podpisuje je na zewnątrz, a zwrócony CMS jest wstawiany z powrotem bajt w bajt bez dotykania zamrożonego bajtu
var
  Doc: THotPDF;
  Fs: TFileStream;
  PdfBytes, HashInput, SigHex: AnsiString;
  R1Start, R1Len, R2Start, R2Len, CStart, CLen: Integer;
begin
  // 1. Zapisz dokument z zarezerwowaną dziurą /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. Wczytaj zapisane bajty; zwrócone przesunięcia liczone są od 0
  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. Zhaszuj oba odcinki i podpisz zewnętrznie (HSM, token, usługa)
  HashInput := Copy(PdfBytes, R1Start + 1, R1Len) +
               Copy(PdfBytes, R2Start + 1, R2Len);
  SigHex := SignWithHsm(HashInput);  // twoja integracja: zwraca CMS jako hex

  // 4. Wklej podpis do zarezerwowanej dziury
  THotPDF.InsertSignatureHex(PdfBytes, SigHex);
  Fs := TFileStream.Create('signed.pdf', fmCreate);
  try
    Fs.WriteBuffer(PdfBytes[1], Length(PdfBytes));
  finally
    Fs.Free;
  end;
end;

Dwa szczegóły w tej sekwencji powodują większość przejściowych awarii. Pierwszy to fakt, że PreparePDFForSigning działa na bajtach gotowego pliku. Symbol zastępczy musi zostać zapisany w całości, zanim przesunięcia zaczną cokolwiek znaczyć; policz je względem strumienia wciąż będącego w budowie, a nie zgodzą się z bajtami, które ostatecznie zhaszujesz. Drugi to znów rozmiar rezerwacji. 8192 bajty, o które poprosiłeś, muszą pomieścić ostateczny CMS, a podpis niosący certyfikaty pośrednie, albo taki, który usługa ozdabia podpisanymi atrybutami, może go przekroczyć. InsertSignatureHex nie powiększy dziury, by zrobić miejsce. Objawem jest potok, który podpisuje się dobrze z jednym certyfikatem, a zawodzi z kolejnym; lekarstwem jest regeneracja symbolu zastępczego z rezerwacją zmierzoną na prawdziwym podpisie wyprodukowanym przez faktycznego podpisującego, a nie zgadniętą

Poziomy bazowe PAdES i znaczniki czasu, które utrzymują podpis przy życiu

Jeśli podpisujesz zgodnie z regułami europejskimi, obowiązującym standardem jest ETSI EN 319 142-1, który układa w stos cztery poziomy bazowe PAdES. B-B to zwykły podpis. B-T dodaje zaufany znacznik czasu, który dowodzi, kiedy podpis powstał. B-LT osadza materiał walidacyjny, certyfikaty i dane o unieważnieniu, wewnątrz dokumentu, żeby dało się go sprawdzić jeszcze po latach. B-LTA nakłada na to okresowe znaczniki czasu dokumentu, tak by dowód przetrwał dłużej niż algorytmy, na których został zbudowany. HotPDF emituje struktury po stronie dokumentu dla każdego poziomu:

HotPDF: Ułożone w stos poziomy bazowe PAdES od B-B przez B-T i B-LT do B-LTA, z osią czasu odnowień pokazującą, że okresowe znaczniki czasu dokumentu trzymają podpis weryfikowalny dekady później
Każdy poziom dokłada nową ochronę na poprzednią; B-LTA wciąż nakłada od nowa znaczniki czasu dokumentu, żeby dowód przeżył algorytmy, na których został pierwotnie zbudowany
// Pole podpisu bazowego PAdES (ETSI EN 319 142-1)
Pdf.CurrentPage.AddPAdESSignatureField(
  'ApprovalSig', Rect(50, 100, 350, 150), 'B-B',
  'Contract approval', 'Boston, MA', 'legal@example.com');

// Znacznik czasu dokumentu: większa rezerwacja na token TSA i łańcuch
Pdf.CurrentPage.AddDocumentTimestampSignature('ArchiveTS', 16384);

Rezerwacja 16384 bajtów na znacznik czasu jest celowa. Urząd znakowania czasem zwraca token, który ciągnie za sobą własny łańcuch certyfikatów, więc rutynowo potrzebuje więcej miejsca niż 8 KB, którymi zadowala się zwykły podpis. Te znaczniki czasu dokumentu to też mechanizm stojący za B-LTA: ponowne znakowanie czasem zarchiwizowanego podpisu co kilka lat, algorytmami wciąż aktualnymi, jest tym, co utrzymuje dokument podpisany w 2026 roku weryfikowalnym w 2040

Słowo o ciągach powodu, lokalizacji i kontaktu, które przyjmują oba wywołania pola: to metadane dla wygody i nic więcej. HotPDF przechowuje je jako zwykłe wpisy słownikowe i maluje je w widocznym wyglądzie podpisu, ale żaden walidator niczego przez nie nie sprawdza. Wypełniaj je spójnie danymi ze swojego przepływu pracy, bo audytorzy naprawdę je czytają, a potem nigdy nie myl ich z dowodem. Prawdziwe twierdzenie kryptograficzne mieszka wyłącznie w CMS i jego łańcuchu certyfikatów, a weryfikator całkowicie ignoruje widoczny tekst

Po podpisaniu plik może już tylko rosnąć

W chwili, gdy podpis zaczyna istnieć, bajty wewnątrz jego zakresów zostają zamrożone. Jedynym legalnym sposobem zmiany pliku potem jest przyrostowa aktualizacja wg ISO 32000-1 §7.5.6, która dopisuje nowe i zmienione obiekty za oryginalnymi bajtami i doczepia do nich świeżą sekcję odniesień krzyżowych. Zrobione w ten sposób, podpis pozostaje ważny dla swojej rewizji, a przeglądarka zgłasza uczciwy stan: podpisana rewizja jest nienaruszona, dokument został potem rozszerzony. Zamiast tego przeserializuj cały plik od nowa, a przepiszesz podpisane odcinki, co niszczy podpis nawet wtedy, gdy nic widocznego się nie zmieniło. Ten sam mechanizm rewizji jest też sposobem, w jaki jeden dokument nosi kilka podpisów: każdy nowy podpis ląduje we własnej przyrostowej aktualizacji, a jego zakresy obejmują wszystko przed nim, łącznie z wcześniejszymi podpisami. Mechanika append-only, i moment, w którym bezpiecznie jest ją skompaktować, są omówione w artykule o strumieniach obiektów i przyrostowych aktualizacjach

Dwie granice warto mieć w pamięci podczas projektowania. Tryb wyjścia PDF/A w HotPDF od razu odrzuca pola podpisu, więc zgodność archiwalna i osadzony podpis muszą trafić do osobnych plików. A podpisywanie nic nie mówi o tajności: dowodzi, kto stworzył dokument i że nie zmienił się od tamtej pory, ale każdy wciąż może go przeczytać. Ukrywanie treści to osobne zadanie, obsługiwane przez szyfrowanie AES-256 i politykę uprawnień

Cokolwiek zbudujesz, przetestuj to czymś innym niż kod, który zapisał plik. Otwórz wynik w panelu podpisów Acrobata i potwierdź trzy rzeczy: podpis jest ważny, tożsamość łańcuchuje się do oczekiwanego korzenia, a panel nie zgłasza żadnych zmian od podpisania. Potem zmień jeden bajt wewnątrz podpisanego zakresu kopii przeznaczonej do wyrzucenia i potwierdź, że panel teraz nazywa dokument zmienionym. Potok podpisywania, którego nigdy nie widziałeś odrzucającego zmanipulowany plik, to taki, którego weryfikacja tak naprawdę nie została przetestowana

Wszystkie trzy poziomy podpisywania są dostępne w HotPDF Delphi Component dla Delphi i C++Builder; strona produktu odsyła do kompletnej dokumentacji API podpisu