Weź plik PDF faktury, który już niesie szyfrowanie AES-256, i poproś komponent PDFium dla Delphi i C++Buildera (PDFiumPas), aby ostemplował go jako PDF/A do przechowywania archiwalnego, albo podpisał go PAdES, poprzez aktualizację przyrostową zamiast pełnego przepisania. Biblioteka nie osiągnie tego, łatając zaszyfrowane bajty bezpośrednio: jej sześć iniektorów znaczników zgodności wykrywa istniejący wpis /Encrypt i przepuszcza źródło do wyjścia bez zmian, bajt w bajt, a jej moduł podpisujący PAdES zgłasza wyjątek zamiast wyemitować podpis, którego żaden walidator nie zaakceptuje
To inne pytanie niż audytowanie PDF, którego sam nie stworzyłeś, pod kątem ukrytego ryzyka, co jest własnym ćwiczeniem tylko-do-odczytu. Ten artykuł dotyczy strony zapisu tej samej granicy zaufania: co twój własny kod ma prawo zrobić z plikiem, którego bajty są już zamknięte za cudzym hasłem, w chwili gdy ten kod próbuje cokolwiek do niego dodać po fakcie
Czego wymaga ISO 32000-1, gdy aktualizujesz zaszyfrowany PDF?
ISO 32000-1 §7.5.6 wymaga, aby trailer aktualizacji przyrostowej powtarzał każdy wpis z poprzedniego trailera z wyjątkiem /Prev, a Tabela 15 wymienia /Encrypt wśród wpisów, jakie trailer może nieść. Pomiń go w nowym trailerze, a zgodny ze specyfikacją czytnik nie ma powodu wątpić w to pominięcie: najnowszy trailer jest autorytatywny, więc czytnik, który nie znajdzie tam /Encrypt, decyduje, że cały plik jest niezaszyfrowany i próbuje sparsować starsze, wciąż zaszyfrowane ciało jako zwykłe bajty. Zachowaj /Encrypt w nowym trailerze, ale zapisz własne obiekty aktualizacji jako zwykły tekst, a awaria po prostu przesuwa się o jeden krok później: czytnik poprawnie wykrywa szyfrowanie, przepuszcza każdy obiekt, którego dotyka, przez szyfr pliku, w tym nowe, które nigdy nie były zaszyfrowane, i dostaje z powrotem szum dla zawartości, która była całkowicie czytelna, zanim odszyfrowanie jej dotknęło. Każdy z tych błędów produkuje plik, który wygląda jak normalna, dobrze sformułowana aktualizacja przyrostowa na poziomie bajtów, aż do chwili, gdy zgodny czytnik go otworzy
Sześć iniektorów znaczników, jedna brama szyfrowania w v2.14.2
PDFiumPas dostarcza sześć iniektorów znaczników na poziomie bajtów, po jednym dla każdego podzbioru ISO PDF, który potrafi oznaczyć: PDF/A (ISO 19005), PDF/X (ISO 15930), PDF/UA (ISO 14289-1), PDF/E-1 (ISO 24517-1), PDF/R-1 (ISO 23504-1) i PDF/VT-1 (ISO 16612-2). Każdy z nich bierze bajty, które już zapisało własne FPDF_SaveAsCopy PDFium, i nakłada na nie drugą, mniejszą aktualizację przyrostową: nowy strumień metadanych XMP, edycję słownika katalogu, która na niego wskazuje, a dla podzbiorów zorientowanych na druk — OutputIntent i profil ICC. Od wersji v2.14.2 każdy z InjectPdfAMarkers, InjectPdfXMarkers, InjectPdfUaMarkers, InjectPdfEMarkers, InjectPdfRMarkers i InjectPdfVTMarkers najpierw czyta trailer źródła, a jeśli zgłasza on istniejący wpis /Encrypt, kopiuje źródło do strumienia docelowego bez modyfikacji i natychmiast zwraca sterowanie. Żadnego XMP, żadnego OutputIntent, żadnej edycji katalogu — wywołujący dostaje z powrotem oryginalny plik, bajt w bajt
var
Src, Dst: TFileStream;
Opts: TPdfXSaveOptions;
begin
Src := TFileStream.Create('signed-encrypted-proof.pdf', fmOpenRead);
Dst := TFileStream.Create('pdfx-attempt.pdf', fmCreate);
try
Opts.Conformance := pxc4;
InjectPdfXMarkers(Src, Dst, Opts);
// pdfx-attempt.pdf is byte-identical to the source: still encrypted,
// no /GTS_PDFXVersion, no OutputIntent. Nothing was written, and
// nothing was corrupted either
finally
Dst.Free;
Src.Free;
end;
end;
Dozwolone bycie zaszyfrowanym to nie to samo co bezpieczne do wstrzykiwania
PDF/E-1 i PDF/R-1 oba jawnie pozwalają, by ich dokument gospodarz był zaszyfrowany na poziomie specyfikacji, co czyta się jak wyjątek, dopóki nie spojrzysz na to, co faktycznie musi się wydarzyć na dysku. ISO 24517-1 §6.3 zezwala na szyfrowanie dla PDF/E-1, a ISO 23504-1 §6.2.3 zezwala na nie dla PDF/R-1, pod warunkiem że nagłówek deklaruje %PDF-2.0. Żadna z tych klauzul nie mówi nic o tym, czy postprocesor na poziomie bajtów może bezpiecznie dodać obiekt w postaci zwykłego tekstu do tego zaszyfrowanego kontenera, a nie może, z tych samych powodów §7.5.6, które dotyczą każdego innego podzbioru. Własne walidatory zgodności PDFiumPas dla tych dwóch profili, ValidatePdfECompliance i ValidatePdfRCompliance, celowo odnotowują obecność /Encrypt bez oznaczania jej jako defektu, co jest poprawne dla walidatora tylko-do-odczytu, który nigdy nie zapisuje żadnego bajtu. To też łatwy wzorzec, by przelecieć go wzrokiem i założyć, że bliźniaczy iniektor nie potrzebuje osobnego zabezpieczenia, podczas gdy iniektor jest tą jedną funkcją z pary, która faktycznie musi odmówić
Czy SaveAsPdfX po cichu odszyfrowuje twój dokument?
Tak, za każdym razem, gdy przechodzisz przez publiczne metody wygody zamiast wywoływać iniektor bezpośrednio. Każda z TPdf.SaveAsPdfA, SaveAsPdfX, SaveAsPdfUa, SaveAsPdfE, SaveAsPdfR i SaveAsPdfVT renderuje bieżący dokument do strumienia tymczasowego za pomocą SaveAs(Tmp, saRemoveSecurity), zanim przekaże te bajty odpowiedniemu iniektorowi. saRemoveSecurity mapuje się na własną flagę PDFium FPDF_REMOVE_SECURITY, więc tymczasowa kopia, którą otrzymuje iniektor, nigdy w ogóle nie była zaszyfrowana, a zabezpieczenie /Encrypt iniektora nigdy nie ma powodu się uruchomić. Wynik niesie twoje znaczniki PDF/A, PDF/X, PDF/UA, PDF/E-1, PDF/R-1 lub PDF/VT-1, ale nie jest już chroniony jakimkolwiek hasłem, które otwierało źródło
Ten kompromis jest niewidoczny, dopóki ktoś niżej w potoku nie otworzy "chronionej" kopii archiwalnej bez hasła i nie zauważy, że po prostu działa. Poprawka to nie inne wywołanie metody; PDFiumPas nie ma odpowiednika saAddSecurity, który sparowałby się z saRemoveSecurity, ponieważ bazowy silnik PDFium nigdy nie został zbudowany, by zapisywać nowe szyfrowanie, tylko żeby je usuwać. Jeśli obie właściwości mają znaczenie dla jednego pliku, szyfrowanie musi być osobnym krokiem, który sam kontrolujesz, zastosowanym po znacznikach zgodności, a nie wtopionym w to samo wywołanie SaveAsPdfA
var
Pdf: TPdf;
begin
Pdf := TPdf.Create(nil);
try
Pdf.Password := 'open-secret'; // needed to open the source at all
Pdf.FileName := 'signed-encrypted-invoice.pdf';
Pdf.LoadDocument;
Pdf.SaveAsPdfA('invoice-pdfa.pdf', pac2b);
// invoice-pdfa.pdf now declares PDF/A-2b, but SaveAs(saRemoveSecurity)
// ran first inside SaveAsPdfA: the output opens without a password
finally
Pdf.Free;
end;
end;
Co się dzieje, gdy podpisujesz zaszyfrowany PDF za pomocą PAdES?
PDFiumPas odmawia wprost, zamiast po cichu porzucać żądanie tak jak robi to iniektor znaczników. TPdf.SignPades i SignPadesToStream obie przechodzą przez wewnętrzne SignPadesBytes, a pierwsze, co ono robi po sparsowaniu trailera źródła, to sprawdzenie obecności /Encrypt. Jeśli wpis jest obecny, zgłasza EPadesCrypto z komunikatem "SignPadesBytes: the source document is encrypted; remove encryption before signing" zamiast kontynuować dalej. InjectPadesDssMarkers, funkcja osadzająca certyfikaty, odpowiedzi OCSP i CRL na potrzeby długoterminowej walidacji, stosuje identyczne sprawdzenie z identycznego powodu, z własnym komunikatem: "InjectPadesDssMarkers: the source document is encrypted; remove encryption before embedding DSS validation material"
Rozumowanie tutaj jest surowsze niż przepuszczenie stosowane przez iniektory znaczników, i celowo tak. Ciche przepuszczenie jest bezpieczne dla stempla PDF/A, ponieważ pominięcie go zostawia ci ten sam ważny PDF, od którego zacząłeś, tylko nieoznaczony. Podpisywanie nie może zawieść tak cicho: podpis, który po cichu nigdy nie został dodany, wygląda dla dowolnego kodu wywołującego, który sprawdza tylko wynik logiczny, dokładnie jak podpis, który został dodany pomyślnie. EPadesCrypto dziedziczy z zwykłej klasy Exception, więc jego przechwycenie to zwykła obsługa wyjątków, nie specjalna konwencja przepływu sterowania, której musisz się nauczyć
var
Pdf: TPdf;
begin
Pdf := TPdf.Create(nil);
try
Pdf.Password := 'open-secret';
Pdf.FileName := 'encrypted-contract.pdf';
Pdf.LoadDocument;
try
Pdf.SignPades('encrypted-contract-signed.pdf', 'A1B2C3D4E5F6...');
except
on E: EPadesCrypto do
// E.Message: 'SignPadesBytes: the source document is encrypted;
// remove encryption before signing'
raise Exception.Create('Decrypt the contract before signing: '+ E.Message);
end;
finally
Pdf.Free;
end;
end;
Kolejność stempli zgodności, podpisów i szyfrowania
Praktyczną poprawką jest kolejność, nie inna biblioteka. Zastosuj znaczniki PDF/A, PDF/X, PDF/UA, PDF/E-1, PDF/R-1 lub PDF/VT-1 jako pierwsze, dodaj dowolny podpis PAdES jako następny, i dopiero potem uruchom cokolwiek w twoim potoku, co faktycznie odpowiada za szyfrowanie, czy to dedykowany writer PDF, urządzenie podpisujące, czy twoją własną implementację AES. Warstwa aktualizacji przyrostowej PDFiumPas mieści się naturalnie w środku tej sekwencji, dołączając małe, celowane obiekty do pliku, który jest poza tym gotowy, a szyfrowanie należy na koniec dokładnie dlatego, że to jedyna operacja w łańcuchu, której PDFiumPas sam nie potrafi wykonać ani odwrócić
Nic z tego nie zmienia sposobu, w jaki PDFiumPas czyta dane trailera i odniesień krzyżowych, od których zależy każda aktualizacja przyrostowa, co jest własnym źródłem subtelności, gdy w grę wchodzą strumienie xref; walidacja strumieni obiektów i xref PDF omawia, jak ta sama ścieżka odczytu trailera obsługuje skompresowane struktury PDF 1.5+. A gdy dokument jest już gotowy na coś silniejszego niż stempel zgodności, podpisywanie PDF podpisem PAdES B-B w Delphi to miejsce, w którym SignPades przejmuje pałeczkę dokładnie tam, gdzie kończy się ten artykuł
Iniektory znaczników i metody SignPades opisane tutaj są dostarczane jako część komponentu PDFium dla Delphi i C++Buildera, obok renderowania i inspekcji tylko-do-odczytu, które PDFium zapewnia natywnie