Otrzymałeś podpisany plik PDF i musisz pokazać w swojej przeglądarce, kto go podpisał, kiedy został podpisany, czy podpis obejmuje cały plik oraz jak daleko mu do długoterminowej zgodności. Komponent PDFium dla Delphi i Lazarus odpowiada na wszystkie cztery pytania za pomocą wywołań tylko do odczytu: FPDF_GetSignatureCount oraz rodzina funkcji FPDFSignatureObj_* udostępniają słownik podpisu, a metoda TPdf.ValidatePades klasyfikuje podstawowy poziom PAdES. Jest to pierwszy z trzech artykułów na temat podpisów PDF w PDFium; dwa kolejne dotyczą tworzenia podpisu B-B oraz dodawania długoterminowych znaczników czasu. Jedna granica musi być jednak jasna na samym początku: wszystko tutaj to inspekcja, a odczytywanie deklaracji podpisu to inne zadanie niż weryfikacja jego kryptografii lub decydowanie, czy ufasz podpisującemu
Dlaczego słownik podpisu PDF to nie tylko blok bajtów
Podpis PDF to słownik, a nie nieprzezroczysty załącznik, a jego dwa najważniejsze wpisy informują o tym, jaka część pliku jest rzeczywiście chroniona. Norma ISO 32000-1 §12.8 definiuje słownik podpisu z wpisami /ByteRange oraz /Contents. Wpis /Contents zawiera zakodowaną szesnastkowo strukturę CMS SignedData (RFC 5652), czyli kopertę kryptograficzną przenoszącą certyfikat podpisującego, podpisane atrybuty oraz samą wartość podpisu. Wpis /ByteRange to część, którą programiści często bagatelizują: jest to tablica dwóch przedziałów przesunięcie-długość, które razem obejmują cały plik z wyjątkiem szesnastkowego ciągu znaków /Contents. Ta luka to dokładnie miejsce, w którym znajdują się bajty podpisu, a dwa przedziały po obu jej stronach to dokładnie to, pod czym podpis się podpisuje
Projekt ByteRange to element pozwalający na audyt zapisu przyrostowego. Ponieważ podpisujący nie może haszować bajtów podpisu, które jeszcze nie istnieją, plik jest dzielony wokół miejsca zastępczego /Contents, a cała reszta jest haszowana w podpisie. Podpis, którego ByteRange nie sięga końca pliku, to znak ostrzegawczy: zawartość dołączona po objętym zakresie za pomocą późniejszej aktualizacji przyrostowej nie naruszy podpisu, mimo że zmienia to, co widzi czytelnik. Zatem pierwszą rzeczą, którą sprawdza poważny inspektor, nie jest to, kto podpisał, ale to, czy podpis obejmuje bajty, które rzekomo potwierdza
Odczytywanie słownika podpisu za pomocą interfejsu API PDFium tylko do odczytu
Komponent PDFium udostępnia słownik podpisu poprzez dwa elementy tylko do odczytu: SignatureCount oraz rekord Signature[Index]. Pod maską wywołują one funkcje FPDF_GetSignatureCount, FPDF_GetSignatureObject oraz akcesory FPDFSignatureObj_* dla /SubFilter, /ByteRange, /Contents, /Reason i czasu podpisania. Wyrażenie „tylko do odczytu” jest tu kluczowe: PDFium potrafi wyliczać i czytać podpisy, ale nie posiada interfejsu API do tworzenia lub zapisywania ich, dlatego część dotycząca podpisywania w tej serii artykułów jest realizowana przez samą bibliotekę, a nie przez PDFium
var
Pdf: TPdf;
i: Integer;
Sig: TPdfSignature;
begin
Pdf := TPdf.Create(nil);
try
Pdf.FileName := 'contract-signed.pdf';
Pdf.Active := True;
for i := 0 to Pdf.SignatureCount - 1 do
begin
Sig := Pdf.Signature[i];
Writeln('SubFilter : ', Sig.Encoding); // ETSI.CAdES.detached, adbe.pkcs7.detached, ...
Writeln('Signed at : ', Sig.Time); // signer date string, e.g. D:20260708120000+02'00'
Writeln('Reason : ', Sig.Reason);
Writeln('CMS length: ', Length(Sig.Content)); // raw DER SignedData taken from /Contents
Writeln('DocMDP : ', Sig.Permission); // 0 = no certification, 1..3 = MDP level
end;
finally
Pdf.Free;
end;
end;
Każdy rekord TPdfSignature mapuje się bezpośrednio na słownik. Właściwość Encoding to /SubFilter, czyli najbardziej diagnostyczne pole, ponieważ definiuje moduł obsługi podpisu i natychmiast odróżnia nowoczesny podpis ETSI.CAdES.detached od podpisu przestarzałego lub niedozwolonego. Z kolei Time to zadeklarowany przez autora czas podpisania w postaci ciągu daty PDF, co jest deklaracją podpisującego, a no zaufanym czasem. Właściwość Content to surowe dane CMS SignedData, a Permission ujawnia poziom certyfikacji DocMDP (0 dla zwykłego podpisu zatwierdzającego, od 1 do 3 dla podpisu certyfikującego, który blokuje późniejsze zmiany). Jedynym polem, którego rekord nie udostępnia, jest przetworzony zakres ByteRange, co jest celowym pominięciem, ponieważ metoda ValidatePades wykonuje za Ciebie obliczenia pokrycia ByteRange, zamiast zmuszać Cię do ręcznego ich powtarzania
Jaka jest różnica między PAdES B-B, B-T, B-LT a B-LTA?
Cztery podstawowe poziomy PAdES tworzą drabinę od minimalnie ważnego podpisu do takiego, który został stworzony z myślą o przetrwaniu dziesięcioleci w archiwum, a każdy poziom ściśle zawiera w sobie poziom poniżej. Norma ETSI EN 319 142-1 definiuje je jako B-B, B-T, B-LT i B-LTA. B-B (Basic) to podpis wraz z wymaganymi podpisanymi atrybutami i niczym więcej. B-B (Timestamp) dodaje zaufany znacznik czasu RFC 3161 do podpisu, dzięki czemu moment podpisania jest poświadczony przez urząd ds. znaczników czasu, a nie tylko deklarowany przez zegar podpisującego. B-LT (Long-Term) osadza materiały walidacyjne — łańcuch certyfikatów oraz opcjonalnie odpowiedzi OCSP lub listy CRL — wewnątrz pliku, dzięki czemu podpis można zweryfikować lata później, gdy infrastruktura wydawcy już nie będzie działać. B-LTA (Long-Term z archiwalnym znacznikiem czasu) otacza te materiały znacznikiem czasu dokumentu, chroniąc same dane długoterminowe i dając punkt na ponowne nałożenie znacznika czasu, zanim bazowa kryptografia się zestarzeje
Praktyczne odczytywanie dotyczy horyzontu czasowego. Podpis B-B odpowiada na pytanie: „czy ktoś to podpisał”. B-T odpowiada: „i kiedy, w sposób udowodniony”. B-LT odpowiada: „i czy nadal mogę to sprawdzić po wygaśnięciu certyfikatów”. Z kolei B-LTA odpowiada: „i czy to sprawdzenie nadal będzie ważne za dwadzieścia lat”. Profile regulacyjne wybierają konkretny szczebel: wiele kontekstów e-fakturowania i eIDAS wymaga co najmniej B-T, a nakazy archiwalne sięgają po B-LT lub B-LTA. Ustalenie, który szczebel dokument faktycznie osiąga przed jego zaakceptowaniem lub odrzuceniem, to główny cel etapu inspekcji
Wykrywanie poziomu podstawowego za pomocą TPdf.ValidatePades
Komponent PDFium sprowadza całe zagadnienie poziomów do jednego wywołania. Metoda TPdf.ValidatePades zwraca rekord TPadesValidationResult, którego pole Level jest wartością TPadesLevel — plNone, plUnknown, plB_B, plB_T, plB_LT lub plB_LTA — wraz z zestawem problemów, liczbą podpisów oraz liczbą znaczników czasu dokumentu. Poziom jest wnioskowany monotonicznie: walidator najpierw ustala poziom B-B, a następnie podnosi go do B-T, jeśli obecny jest znacznik czasu podpisu lub dokumentu, do B-LT, jeśli katalog zawiera wpis /DSS z certyfikatami i znacznik /Extensions /ESIC poziomu 1, oraz do B-LTA, jeśli obecny jest zarówno znacznik czasu dokumentu, jak i znacznik ESIC poziomu 2. Dwie metody pomocnicze sprawiają, że wynik jest użyteczny: IsCompliant ma wartość True tylko wtedy, gdy poziom sięga co najmniej B-B i zestaw problemów jest pusty, a IsCompliantAt pozwala wymusić minimalny próg polityki, np. plB_T
var
Pdf: TPdf;
R: TPadesValidationResult;
begin
Pdf := TPdf.Create(nil);
try
Pdf.FileName := 'contract-signed.pdf';
Pdf.Active := True;
R := Pdf.ValidatePades;
case R.Level of
plNone: Writeln('No PAdES signature present');
plUnknown: Writeln('Signature present but level undeterminable');
plB_B: Writeln('PAdES B-B (basic)');
plB_T: Writeln('PAdES B-T (trusted timestamp)');
plB_LT: Writeln('PAdES B-LT (long-term material embedded)');
plB_LTA: Writeln('PAdES B-LTA (archive timestamp)');
end;
Writeln('Signatures : ', R.SignatureCount);
Writeln('DocTimeStamps: ', R.DocTimeStampCount);
if R.IsCompliantAt(plB_T) then
Writeln('Meets the B-T policy floor')
else
Writeln('Below the required B-T level');
finally
Pdf.Free;
end;
end;
Dlaczego adbe.pkcs7.sha1 jest zabronionym SubFilterem?
Ponieważ funkcja SHA-1 jest złamana, a moduł obsługi adbe.pkcs7.sha1 ma ją wbudowaną. Ten SubFilter wstępnie haszuje dokument algorytmem SHA-1 przed opakowaniem go w PKCS#7, a algorytm SHA-1 od lat jest podatny na kolizje, dlatego klauzula 6.3 normy EN 319 142-1 zabrania go wprost w przypadku podpisów podstawowych. Metoda ValidatePades zgłasza błąd ppeiForbiddenSubFilter, gdy widzi adbe.pkcs7.sha1 lub adbe.x509.rsa_sha1, oraz ppeiBadDigestAlgorithm, gdy sam CMS używa MD5 lub SHA-1 jako skrótu wiadomości (klauzula 6.2.1). Są to dwa odrębne testy wykrywające tę samą klasę słabości na dwóch różnych warstwach
Zestaw problemów liczy łącznie 26 elementów, a te, z którymi spotkasz się najczęściej, skupiają się wokół struktury i pokrycia pliku. ppeiByteRangeNotCoveringFile to opisane wcześniej sprawdzenie pokrycia. Błąd ppeiForbiddenCertKey pojawia się, gdy słownik podpisu zawiera wpis /Cert, czego zabrania PAdES, ponieważ łańcuch musi znajdować się w SignedData.certificates w strukturze CMS. Z kolei ppeiMissingSigningCertificate, ppeiMissingContentType oraz ppeiMissingMessageDigest wskazują na brak obowiązkowych podpisanych atrybutów, a ppeiDetachedContentViolation wychwytuje podpis, który błędnie osadza podpisaną zawartość zamiast ją odłączyć. Przejście przez ten zestaw zmienia suche odrzucenie w konkretną diagnozę, którą można zapisać w dzienniku
var
R: TPadesValidationResult;
Issue: TPadesValidationIssue;
begin
R := Pdf.ValidatePades;
if R.Issues <> [] then
for Issue := Low(TPadesValidationIssue) to High(TPadesValidationIssue) do
if Issue in R.Issues then
Writeln('Issue: ',
GetEnumName(TypeInfo(TPadesValidationIssue), Ord(Issue)));
end;
Czego nie sprawdza ValidatePades
Metoda ValidatePades sprawdza strukturę, a nie zaufanie, a pomylenie tych dwóch pojęć jest niebezpiecznym błędem. Wynik plB_LTA oznacza, że dokument zawiera poprawnie sformatowany podpis B-LTA ze wszystkimi wymaganymi atrybutami, materiałami i znacznikami czasu we właściwych miejscach — nie oznacza to jednak, że podpis jest ważny kryptograficznie, że łańcuch certyfikatów prowadzi do zaufanego urzędu certyfikacji ani że żaden certyfikat w łańcuchu nie został odwołany. Walidator celowo nie wykonuje weryfikacji kryptograficznej: nie oblicza ponownie podpisu na obszarze ByteRange, nie buduje ani nie ocenia łańcucha zaufania i nie sprawdza statusu odwołania OCSP ani list CRL. Ten podział jest celowy i użyteczny, ponieważ inspekcja struktury jest szybka, w pełni deterministyczna i nie wymaga kluczy, sieci ani systemowej kryptografii, dzięki czemu ValidatePades działa identycznie na systemach Windows, Linux i macOS jako czysty kod Pascala na bajtach pliku. Z kolei walidacja łańcucha zaufania jest nierozłączna z polityką bezpieczeństwa — którym urzędom ufasz, jak pobierasz dane o odwołaniach, jak rygorystyczna jest tolerancja znacznika czasu — i należy do późniejszego etapu zależnego od systemowego magazynu certyfikatów. Dlatego traktuj pomyślny wynik z ValidatePades jako niezbędny warunek potwierdzający poprawny kształt podpisu, a następnie przekaż strukturalnie poprawny podpis do rzeczywistej weryfikacji kryptograficznej, zanim na nim polegniesz
Ten przebieg strukturalny to właściwe miejsce na start i łączy się naturalnie z szerszymi testami tylko do odczytu opisanymi w artykule na temat audytu ryzyk bezpieczeństwa PDF z komponentem PDFium oraz pracami nad zgodnością formatów, np. przy walidacji gotowych do druku dokumentów PDF/X. Gdy potrafisz już odczytać i sklasyfikować podpis, kolejnym krokiem jest jego wygenerowanie: drugi artykuł z tej serii omawia podpisywanie plików PDF w standardzie PAdES B-B, a trzeci rozszerza ten temat o zaufane znaczniki czasu oraz długoterminowe podpisy B-LT i B-LTA. Inspekcja podpisu tylko do odczytu oraz klasyfikator ValidatePades pokazane tutaj są częścią komponentu PDFium dla Delphi, C++Builder i Lazarus