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ą
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
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:
// 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