HotPDF weryfikuje podpisy cyfrowe w załadowanych dokumentach PDF za pomocą trzech metod klasy THotPDF: GetLoadedSignatureInfo, VerifyLoadedSignature oraz VerifyLoadedSignatureEx, wprowadzonych w wersji 2.259.0. Komponent ponownie haszuje segmenty /ByteRange oryginalnego pliku, sprawdza atrybut CMS messageDigest i wykonuje weryfikację RSA PKCS#1 v1.5 w odniesieniu do osadzonego certyfikatu podpisującego, zwracając status svValid, gdy bajty dokumentu są nienaruszone
Scenariusz ten jest prozaiczny, ale stawka — nie. Druga strona odsyła podpisaną umowę, Twój system musi ją zarchiwizować i ktoś zadaje jedyne pytanie, które naprawdę się liczy: czy to jest dokładnie ten dokument, który wysłaliśmy, bajt po bajcie, podpisany przez wskazany certyfikat? Odpowiedź na to pytanie w kodzie to strona weryfikacji w historii o podpisach; strona tworzenia, czyli budowanie i osadzanie podpisów PAdES na samym początku, jest omówiona w powiązanym artykule o tworzeniu podpisów cyfrowych PAdES za pomocą HotPDF. Ten artykuł dotyczy drugiego kierunku: plik PDF trafia do nas już podpisany, a my chcemy uzyskać programowy werdykt, a nie zrzut ekranu z zielonym ptaszkiem w programie Acrobat
W jaki sposób podpisany PDF dowodzi, że nie został zmodyfikowany?
Podpis PDF chroni określone zakresy bajtów pliku, a nie abstrakcyjne pojęcie „dokumentu”. Norma ISO 32000-1 §12.8 definiuje ten mechanizm: pole formularza podpisu zawiera słownik, którego wpis /Contents przechowuje kontener CMS SignedData (RFC 5652), a tablica /ByteRange określa dokładne obszary pliku objęte podpisem, zgodnie z §12.8.1. Tablica ta jest listą par przesunięcia i długości, w praktyce składającą się z dwóch segmentów: wszystkiego przed szesnastkowym ciągiem /Contents oraz wszystkiego po nim. Wartość podpisu nie może obejmować samej siebie, więc plik jest haszowany wokół tej luki
Ta konstrukcja ma konsekwencję, która kształtuje całe API: weryfikacja musi obliczyć skrót z oryginalnych, zserializowanych bajtów pliku w takiej postaci, w jakiej znajdują się na dysku. Parsowany model obiektowy jest do tego bezużyteczny, ponieważ ponowna serializacja nawet niezmienionego dokumentu daje inne bajty. W związku z tym HotPDF dokonuje weryfikacji w odniesieniu do pliku źródłowego, z którego dokument został załadowany, lub w odniesieniu do strumienia TStream z surowymi bajtami dostarczonymi przez Ciebie — nigdy w odniesieniu do reprezentacji w pamięci
Odczytywanie metadanych podpisu przed weryfikacją
Metoda GetLoadedSignatureInfo analizuje słownik podpisu i jego kontener CMS bez dotykania chociażby jednego bajtu dokumentu, co czyni ją właściwym pierwszym wywołaniem, gdy chcesz jedynie wyświetlić informację o tym, kto i kiedy podpisał plik. Pola podpisu są indeksowane od 0 w kolejności pól formularza, a funkcja GetLoadedSignatureFieldCount zwraca ich liczbę. Zwracany rekord THPDFSignatureInfo zawiera nazwę pola, /SubFilter, nazwę pospolitą (common name) certyfikatu podpisującego, nazwy wyróżniające (distinguished names) podmiotu i wystawcy, numer seryjny, daty ważności, czas podpisania (z podpisanego atrybutu, jeśli jest obecny, w przeciwnym razie z wpisu /M w słowniku), nazwę algorytmu skrótu oraz ciągi znaków /Reason, /Location i /ContactInfo. Element Status ma wartość svNotVerified, co jest uczciwą etykietą oznaczającą „sparsowany, niezweryfikowany”
var
Pdf: THotPDF;
Info: THPDFSignatureInfo;
I: Integer;
begin
Pdf := THotPDF.Create(nil);
try
Pdf.LoadFromFile('signed-contract.pdf');
for I := 0 to Pdf.GetLoadedSignatureFieldCount - 1 do
begin
Info := Pdf.GetLoadedSignatureInfo(I);
Writeln('Field: ', Info.FieldName);
Writeln('Signer: ', Info.SignerName);
Writeln('Issuer: ', Info.IssuerDN);
Writeln('Algorithm: ', Info.HashAlgorithm);
Writeln('SubFilter: ', Info.SubFilter);
end;
finally
Pdf.Free;
end;
end;
Uruchamianie sprawdzenia kryptograficznego
Metoda VerifyLoadedSignatureEx wykonuje pełną weryfikację dokumentu załadowanego z pliku i zwraca uzupełniony rekord informacyjny w jednym wywołaniu: otwiera ponownie plik źródłowy, oblicza skrót segmentów /ByteRange za pomocą algorytmu skrótu określonego w SignerInfo, porównuje wynik z podpisanym atrybutem messageDigest (RFC 5652 §5.4), a następnie weryfikuje podpis RSA na kodowaniu DER SET podpisanych atrybutów. Gdy podpis nie zawiera podpisanych atrybutów, test RSA jest uruchamiany bezpośrednio na skrócie dokumentu. Obsługiwane są podpisy RSA PKCS#1 v1.5 ze skrótami SHA-1, SHA-256, SHA-384 lub SHA-512, co pokrywa podfiltry adbe.pkcs7.detached oraz ETSI.CAdES.detached generowane przez popularne narzędzia podpisujące
var
Status: THPDFSignatureVerifyStatus;
Info: THPDFSignatureInfo;
begin
Status := Pdf.VerifyLoadedSignatureEx(0, Info);
case Status of
svValid:
if Info.CoversWholeDocument then
Writeln('Valid; signature covers the whole file')
else
Writeln('Valid; file was extended after signing');
svDigestMismatch:
Writeln('Document bytes changed after signing');
svSignatureInvalid:
Writeln('RSA check failed over signed attributes');
svUnsupportedAlgorithm:
Writeln('Non-RSA key or unknown digest algorithm');
svMalformed:
Writeln('CMS container could not be parsed');
svSourceUnavailable:
Writeln('No source bytes; use the TStream overload');
end;
end;
Warto znać dwa szczegóły implementacji, ponieważ wyjaśniają one niepowodzenia, które z zewnątrz wyglądają tajemniczo. Po pierwsze, weryfikacja podpisanych atrybutów jest bardzo wrażliwa na kodowanie: wewnątrz pliku atrybuty są oznaczone jako [0] IMPLICIT, ale podpis został obliczony na ich formie DER SET OF, więc moduł weryfikujący zmienia oznaczenie przed haszowaniem, dokładnie tak, jak wymaga tego specyfikacja RFC 5652 §5.4. Samodzielnie napisany weryfikator, który haszuje bajty w takiej postaci, w jakiej występują w pliku, odrzuci każdy poprawnie podpisany dokument. Po drugie, wpis /Contents jest tradycyjnie dopełniany zerami do zarezerwowanego rozmiaru bajtowego, więc weryfikator przed analizą obcina dane DER do rzeczywistej długości zewnętrznej sekwencji SEQUENCE; wyglądające jak śmieci zera końcowe są normalnym zjawiskiem, a nie uszkodzeniem pliku. Ta sama rodzina zagrożeń związanych z analizą ASN.1 po stronie importu certyfikatów jest tematem artykułu o zabezpieczaniu i utwardzaniu analizy PKCS#12 i ASN.1 w HotPDF
Co tak naprawdę gwarantuje poprawny podpis?
Status svValid oznacza dokładnie tyle: bajty wskazane przez /ByteRange dają po zhaszowaniu wartość podpisaną przez podpisującego, a sam podpis weryfikuje się pomyślnie kluczem publicznym certyfikatu osadzonego w kontenerze CMS. To integralność bajtów oraz powiązanie z kluczem, i nic więcej. Walidacja łańcucha certyfikatów i zaufania pozostaje poza zakresem weryfikatora HotPDF: nie przechodzi on po łańcuchu do urzędu certyfikacji (certyfikatu głównego), nie sprawdza list unieważnień ani nie konsultuje się z żadnym magazynem zaufanych certyfikatów. Samopodpisany certyfikat od napastnika, który podpisał zmieniony dokument, również zweryfikuje się jako svValid, ponieważ matematycznie wszystko się zgadza. To, czy podpisujący jest tym, za kogo się podaje, i czy należy mu ufać, jest decyzją należącą do innej warstwy — na przykład białej listy certyfikatów w Twojej organizacji, magazynu certyfikatów systemu Windows lub zewnętrznego urzędu walidacji
Flaga CoversWholeDocument chroni przed subtelniejszą luką. Podpis zawsze obejmuje wyłącznie swój zakres /ByteRange, a mechanizm aktualizacji przyrostowych PDF pozwala na dołączanie zawartości po podpisie bez jego unieważniania, co wynika z założeń projektowych i umożliwia pracę z wieloma podpisami. Flaga ta jest obliczana podczas weryfikacji i ma wartość true tylko wtedy, gdy oba segmenty wraz z luką /Contents obejmują cały plik. Gdy otrzymujesz status svValid z flagą CoversWholeDocument o wartości false, oznacza to, że podpisana rewizja jest nienaruszona, ale plik zawiera późniejsze modyfikacje, a to, czy te zmiany są dopuszczalne, zależy już od logiki biznesowej Twojego systemu
Dokumenty załadowane ze strumienia i szyfrowane potrzebują własnych bajtów źródłowych
Bezparametrowe metody VerifyLoadedSignature i VerifyLoadedSignatureEx wymagają, aby komponent pamiętał, z jakiego pliku pochodzi dokument. Jeśli załadujesz dokument ze strumienia, nie będzie nazwy pliku do ponownego otwarcia; to samo dotyczy ścieżki przeładowania hasła używanej dla dokumentów szyfrowanych, przepływu opisanego w artykule o szyfrowaniu PDF algorytmem AES-256 w HotPDF. W obu przypadkach przeciążenia oparte na plikach zwracają status svSourceUnavailable. Rozwiązaniem jest przeciążenie z parametrem TStream, które pozwala przekazać oryginalne surowe bajty z dowolnego miejsca, w którym są przechowywane: pliku, bufora w pamięci czy bazy danych
var
Src: TFileStream;
Status: THPDFSignatureVerifyStatus;
Info: THPDFSignatureInfo;
begin
// Stream-loaded document: the component holds no source
// file name, so supply the original bytes yourself.
Src := TFileStream.Create('signed-contract.pdf',
fmOpenRead or fmShareDenyWrite);
try
Status := Pdf.VerifyLoadedSignature(0, Src, Info);
if Status <> svValid then
Writeln('Verification failed: ', Ord(Status));
finally
Src.Free;
end;
end;
Zgłaszanie tego, czego nie można zweryfikować
Weryfikator, który zna jedynie stany „poprawny” i „błędny”, błędnie zaraportuje dokumenty, których po prostu nie potrafi zinterpretować, dlatego wyliczenie statusu rozdziela przypadki, które Twój interfejs użytkownika powinien rozróżniać. Status svDigestMismatch oznacza, że bajty dokumentu uległy zmianie po podpisaniu — to klasyczny sygnał manipulacji plikiem. svSignatureInvalid oznacza, że skróty bajtów zgadzają się, ale test RSA nie powiódł się, co wskazuje na uszkodzoną lub sfałszowaną wartość podpisu. svUnsupportedAlgorithm to rzetelna odpowiedź w przypadku kluczy ECDSA i nierozpoznanych algorytmów skrótu: podpis może być całkowicie poprawny, ale HotPDF po prostu nie potrafi go sprawdzić, a oznaczenie go jako „błędnego” oczerniłoby poprawny dokument. Status svMalformed oznacza, że kontener CMS nie mógł zostać w ogóle sparsowany. Do prostych walidacji bramkowych funkcja VerifyAllLoadedSignatures retorna wartość true tylko wtedy, gdy istnieje co najmniej jedno pole podpisu i każde z nich weryfikuje się pomyślnie jako svValid — to wygodna wartość logiczna dla potoku pobierania archiwum, który odrzuca wszystko, co niepełne
Weryfikacja podpisu, podpisywanie PAdES, szyfrowanie AES-256 oraz interfejs API edycji załadowanych dokumentów są dostarczane w tej samej natywnej bibliotece VCL dla Delphi i C++Builder, bez zewnętrznych zależności od bibliotek DLL; pełna lista funkcji i obsługiwane wersje środowiska IDE znajdują się na stronie produktu HotPDF Component