Komponent PDFium podpisuje plik PDF podpisem cyfrowym PAdES B-B za pomocą metody SignPades: ładuje dokument, haszuje podpisany zakres bajtów, buduje strukturę CAdES CMS i dołącza podpis jako aktualizację przyrostową. Backend kryptograficzny jest dostępny wyłącznie w systemie Windows, dlatego przed podpisaniem zabezpiecz każde wywołanie właściwością PadesCryptoAvailable
Sytuacja jest dobrze znana. Umowa w formacie PDF ląduje na Twoim biurku, dział prawny chce jej cyfrowego podpisania przed wysyłką, a Ty sięgasz po tę samą wersję PDFium, której już używasz do renderowania i inspekcji dokumentów, tylko po to, by odkryć, że PDFium w ogóle nie potrafi zapisać podpisu. Jego interfejs API podpisów jest ściśle przeznaczony tylko do odczytu. Komponent PDFium wypełnia tę lukę, przejmując cały potok podpisujący w kodzie Pascala, od funkcji skrótu aż po wstrzykiwanie na poziomie bajtów, a ten artykuł omawia ten potok od początku do końca
Dlaczego PDFium nie potrafi zapisać podpisu cyfrowego?
PDFium udostępnia podpisy jako obiekty tylko do odczytu i nie oferuje żadnych metod do ich tworzenia. Rodzina funkcji FPDFSignatureObj_* pozwala na wyliczenie istniejącego podpisu, odczytanie jego /Contents i sprawdzenie /ByteRange, ale nie ma odpowiednika, który tworzyłby słownik podpisu, rezerwował miejsce na /Contents lub zapisywał zakres bajtów; zapis przyrostowy istnieje (FPDF_SaveAsCopy z flagą FPDF_INCREMENTAL), jednak nie niesie ze sobą żadnego punktu zaczepienia dla podpisów. Każdy komponent, który podpisuje plik PDF na bazie PDFium, musi zatem samodzielnie wygenerować każdy bajt podpisu, dlatego komponent PDFium buduje te mechanizmy z trzech czystych modułów w języku Pascal. FPC 3.2.2 dostarcza md5 i sha1, ale nie oferuje w ogóle SHA-2, a interfejs API SHA-256 System.Hash w Delphi nie jest zgodny na poziomie kodu źródłowego z FPC, dlatego FPdfSha256 to samodzielna implementacja FIPS 180-4, która utrzymuje każdą ścieżkę kodu CMS na jednym typie TSHA256Digest bez rozgałęziania kodu dla kompilatorów. Moduł FPdfAsn1 dostarcza koder i czytnik DER, których potrzebują struktury CMS, a FPdfCms składa strukturę CAdES SignedData na bazie obu tych modułów
Jak cyfrowo podpisać plik PDF w Delphi?
Załaduj dokument, a następnie wywołaj metodę SignPades z odciskiem palca certyfikatu. Komponent PDFium porównuje ten odcisk z magazynem certyfikatów „MY” bieżącego użytkownika, pobiera pasujący certyfikat i jego klucz prywatny, a następnie zapisuje podpisaną kopię pod wskazaną ścieżką
uses
PDFium, FPdfCrypto;
procedure SignContract(const AThumbprint: string);
var
Pdf: TPdf;
begin
if not PadesCryptoAvailable then
raise Exception.Create('PAdES signing requires the Windows CNG backend');
Pdf := TPdf.Create(nil);
try
Pdf.FileName := 'contract.pdf'; // the document to sign
Pdf.Active := True;
// Second argument: SHA-1 thumbprint of a certificate in the Current
// User "MY" store. First argument: destination for the signed copy.
if not Pdf.SignPades('contract-signed.pdf', AThumbprint) then
raise Exception.Create('Signing failed');
finally
Pdf.Free;
end;
end;
Właściwość PadesCryptoAvailable to test, który sprawdzasz w pierwszej kolejności za każdym razem. W systemie Windows zwraca wartość True i systemowy backend crypt32/ncrypt jest aktywny; na każdej innej platformie zwraca False, a wywołanie podpisywania zgłosiłoby błąd EPadesCrypto. Traktowanie tego warunku jako obowiązkowego zapobiega awarii kompilacji dla systemów Linux lub macOS w czasie wykonywania na ścieżce, która tam nie może zadziałać. Sam odcisk palca to hash SHA-1 certyfikatu, czyli ta sama wartość, którą menedżer certyfikatów systemu Windows pokazuje na karcie Szczegóły, i wskazuje ona konkretnego podpisującego bez umieszczania materiału klucza w kodzie źródłowym
Co wchodzi w skład CMS: podpisane atrybuty i norma RFC 5652
Podstawowy podpis PAdES nie jest surowym podpisem RSA na pliku; jest to struktura CAdES CMS SignedData przenosząca wymagany zestaw podpisanych atrybutów, a metoda FPdfCms.BuildSignedData emituje dokładnie ten zestaw: content-type, message-digest oraz signing-certificate-v2, czyli atrybut ESS, który wiąże podpis z certyfikatem podpisującego za pomocą skrótu. Jeden szczegół uniemożliwia realizację niemal każdej ręcznie robionej implementacji CMS. Sekcja §5.4 normy RFC 5652 wymaga, aby skrót podpisanych atrybutów był obliczany na podstawie kodowania DER SET OF, znacznik 0x31, podczas gdy te same atrybuty poruszają się wewnątrz SignerInfo pod znacznikiem IMPLICIT [0], czyli 0xA0. Komponent PDFium koduje zestaw atrybutów raz, tworzy skrót z postaci 0x31, a następnie przepisuje tylko wiodący bajt znacznika na 0xA0 do emisji, dzięki czemu jeden bufor obsługuje obie role bez drugiego przebiegu po drzewie struktury
var
Pdf: TPdf;
Opts: TPadesSignOptions;
begin
Pdf := TPdf.Create(nil);
try
Pdf.FileName := 'contract.pdf';
Pdf.Active := True;
Opts := TPadesSignOptions.Default;
Opts.CertificateThumbprint := 'a1b2c3d4e5f6...'; // signer in the MY store
Opts.Reason := 'I approve this agreement';
Opts.Location := 'Berlin, DE';
Opts.ContentsSize := 16384; // hex width of /Contents
if not Pdf.SignPades('contract-signed.pdf', Opts) then
raise Exception.Create('Signing failed');
finally
Pdf.Free;
end;
end;
Przeciążenie opcji dodaje metadane słownika podpisu określone w normie ISO 32000-1 §12.8.1: Reason, Location, ContactInfo oraz Name, wszystkie opcjonalne i wszystkie zapisywane do słownika wartości podpisu. Jedno ograniczenie jest łatwe do przeoczenia. Jeśli ustawisz CommitmentTypeOid w celu dodania podpisanego atrybutu wskazującego zaangażowanie CAdES (commitment-type-indication), nie ustawiaj jednocześnie Reason; norma ETSI EN 319 142-1 §6.3 zabrania przenoszenia obu tych parametrów, ponieważ wyrażają one ten sam zamiar za pomocą innych środków
Jak współgrają ze sobą ByteRange i gniazdo /Contents?
Podpis musi obejmować cały plik z wyjątkiem bajtów, które przechowują sam podpis, a PAdES rozwiązuje tę kołowość za pomocą zastępczego obszaru o stałej szerokości, którym precyzyjnie zarządza metoda SignPadesBytes. Rezerwuje szesnastkowy ciąg znaków /Contents o wielkości ContentsSize bajtów (domyślnie 16384, znacznie więcej niż typowe dane CMS SignedData), serializuje aktualizację przyrostową w celu zlokalizowania dokładnego przesunięcia gniazda, a następnie oblicza /ByteRange as two spans that bracket the slot, everything before the opening delimiter of the hex string and everything after its closing delimiter. Skrót SHA-256 jest obliczany wyłącznie na tych dwóch przedziałach. Gotowy CMS jest kodowany szesnastkowo w zarezerwowanym gnieździe, dopełniany zerami do stałej szerokości, po czym dołączana jest aktualizacja odsyłaczy. Ponieważ szerokość jest ustalana z góry, wypełnienie gniazda nie przesuwa żadnego bajtu w dół pliku, co jest jedynym powodem zachowania ważności zakresu bajtów; oryginalne bajty dokumentu są zachowywane verbatim, dzięki czemu wcześniejszy podpis na tym samym pliku pozostaje nienaruszony, dokładnie tak, jak wymaga tego zapis przyrostowy normy ISO 32000-1 §12.8.1
Backend Windows CNG i jego ograniczenia
Komponent PDFium podpisuje wyłącznie w systemie Windows i ta granica jest celowa. Moduł FPdfCryptoWin dynamicznie wiąże biblioteki crypt32.dll oraz ncrypt.dll, nie wprowadzając żadnych zależności DLL w czasie kompilacji, a łańcuch podpisujący to standardowe CNG: otwarcie magazynu certyfikatów „MY”, wyszukanie certyfikatu po skrócie, uzyskanie uchwytu klucza prywatnego poprzez CryptAcquireCertificatePrivateKey oraz wywołanie NCryptSignHash. Obsługiwane są algorytmy RSA z PKCS#1 v1.5, RSA-PSS oraz ECDSA. Algorytm ECDSA wymaga jednej poprawki, której inne nie potrzebują, ponieważ funkcja NCryptSignHash zwraca surową parę r-i-s IEEE P1363, podczas gdy CMS oczekuje sekwencji DER ECDSA-Sig-Value, dlatego backend ponownie ją koduje zgodnie z RFC 5480
var
Pdf: TPdf;
Opts: TPadesSignOptions;
Output: TFileStream;
begin
if not PadesCryptoAvailable then
Exit; // no signing backend on this platform
Opts := TPadesSignOptions.Default;
Opts.CertificateThumbprint := ReadThumbprintFromConfig;
Pdf := TPdf.Create(nil);
Output := TFileStream.Create('contract-signed.pdf', fmCreate);
try
Pdf.FileName := 'contract.pdf';
Pdf.Active := True;
Pdf.SignPadesToStream(Output, Opts);
finally
Output.Free;
Pdf.Free;
end;
end;
Praktyczną konsekwencją jest to, że klucz prywatny musi znajdować się w magazynie certyfikatów systemu Windows. Certyfikat przechowywany w pliku PFX działa dopiero po zaimportowaniu go do magazynu bieżącego użytkownika (Current User), kiedy to jego odcisk palca staje się wartością przekazywaną do SignPades. Ta wersja nie obsługuje ścieżek PKCS#11 ani HSM i nie posiada programowego backendu dla plików kluczy, więc gdy właściwość PadesCryptoAvailable zwraca False, na danej maszynie podpisywanie jest po prostu niedostępne
Gdzie kończy się poziom PAdES B-B
Poziom PAdES B-B to absolutna podstawa, minimum z czterech poziomów PAdES: potwierdza, kto złożył podpis oraz że bajty nie uległy zmianie od tego czasu, i nic poza tym. Podpis B-B nie niesie ze sobą wiarygodnego znacznika czasu, więc nie może udowodnić, kiedy nastąpiło podpisanie, i nie osadza danych o odwołaniach, przez co weryfikator po latach musi samodzielnie pobrać łańcuch certyfikatów i jego status. Te luki zamykają właśnie wyższe poziomy. Gdy potrzebujesz czasu podpisania, który zaakceptuje audytor, dodanie znacznika czasu RFC 3161 oraz magazynu DSS dla długoterminowej walidacji przenosi podpis na poziom B-T i wyższe; gdy chcesz odczytać gotowy podpis z powrotem i potwierdzić osiągnięty poziom, narzędziem pomocniczym jest inspekcja podpisu PDF i jego poziomu PAdES; a przed podpisaniem czegokolwiek, audyt pliku PDF pod kątem ryzyk bezpieczeństwa informuje Cię o tym, pod czym zamierzasz się podpisać
Pokazane tutaj metody SignPades są dostarczane z komponentem PDFium dla Delphi i C++Builder, wraz z funkcją inspekcji podpisów tylko do odczytu dostarczaną przez PDFium bezpośrednio po wyjęciu z pudełka