Vezmite si faktúru PDF, ktorá už nesie šifrovanie AES-256, a požiadajte komponent PDFium pre Delphi a C++Builder (PDFiumPas), aby ju označil ako PDF/A pre archívnu retenciu, alebo ju podpísal cez PAdES, prostredníctvom inkrementálnej aktualizácie namiesto plného prepísania. Knižnica sa tam nedostane záplatovaním šifrovaných bajtov priamo: jej šesť injektorov značiek zhody odhalí existujúci záznam /Encrypt a prepustí zdroj do cieľa bajt po bajte nezmenený, a jej signer PAdES vyvolá výnimku namiesto toho, aby vyprodukoval podpis, ktorý žiadny validátor neprijme
To je odlišná otázka od auditovania PDF, ktoré ste nevytvorili vy, na skryté riziko, čo je vlastné, iba na čítanie zamerané cvičenie. Tento článok je o zápisovej strane tej istej hranice dôvery: čo smie váš vlastný kód urobiť so súborom, ktorého bajty sú už uzamknuté za heslom niekoho iného, vo chvíli, keď sa tento kód pokúsi doň dodatočne čokoľvek pridať
Čo ISO 32000-1 vyžaduje, keď aktualizujete šifrované PDF?
ISO 32000-1 §7.5.6 vyžaduje, aby trailer inkrementálnej aktualizácie zopakoval každý záznam z predchádzajúceho trailera okrem /Prev, a tabuľka 15 uvádza /Encrypt medzi záznamami, ktoré trailer smie niesť. Vynechajte ho z nového trailera a normu rešpektujúca čítačka nemá žiadny dôvod túto medzeru spochybniť: najnovší trailer je autoritatívny, takže čítačka, ktorá tam nenájde žiadne /Encrypt, rozhodne, že celý súbor je nešifrovaný, a pokúsi sa staršie, stále zašifrované telo naparsovať ako obyčajné bajty. Ponechajte /Encrypt v novom trailere, no zapíšte vlastné objekty aktualizácie ako čistý text, a zlyhanie sa iba presunie o krok neskôr: čítačka správne odhalí šifrovanie, prežene cez šifru súboru každý objekt, ktorého sa dotkne, vrátane nových, ktoré nikdy neboli zašifrované, a dostane späť šum tam, kde bol obsah pred dešifrovaním dokonale čitateľný. Ktorákoľvek z týchto chýb vyprodukuje súbor, ktorý na úrovni bajtov vyzerá ako normálna, správne zostavená inkrementálna aktualizácia, presne až do chvíle, keď ho otvorí normu rešpektujúca čítačka
Šesť injektorov značiek, jedna brána šifrovania z v2.14.2
PDFiumPas dodáva šesť injektorov značiek na úrovni bajtov, po jednom pre každú podmnožinu ISO PDF, akú dokáže označiť: 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), a PDF/VT-1 (ISO 16612-2). Každý z nich zoberie bajty, ktoré už zapísala vlastná funkcia PDFium FPDF_SaveAsCopy, a navrství na ne druhú, menšiu inkrementálnu aktualizáciu: nový prúd metadát XMP, úpravu slovníka katalógu, ktorá naň ukazuje, a pre tlačovo orientované podmnožiny OutputIntent a profil ICC. Od v2.14.2 každá z InjectPdfAMarkers, InjectPdfXMarkers, InjectPdfUaMarkers, InjectPdfEMarkers, InjectPdfRMarkers, a InjectPdfVTMarkers najprv prečíta zdrojový trailer, a ak hlási existujúci záznam /Encrypt, skopíruje zdroj do cieľového prúdu nezmenený a okamžite sa vráti. Žiadne XMP, žiadny OutputIntent, žiadna úprava katalógu — volajúci dostane späť pôvodný súbor, bajt po bajte
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;
Povolené byť šifrované nie je to isté ako bezpečné vkladať do
PDF/E-1 aj PDF/R-1 obe výslovne dovoľujú, aby ich hostiteľský dokument bol šifrovaný na úrovni špecifikácie, čo znie ako výnimka, kým sa nepozriete na to, čo sa v skutočnosti musí stať na disku. ISO 24517-1 §6.3 povoľuje šifrovanie pre PDF/E-1, a ISO 23504-1 §6.2.3 ho povoľuje pre PDF/R-1 za predpokladu, že hlavička deklaruje %PDF-2.0. Ani jedna z týchto klauzúl nehovorí nič o tom, či postprocesor na úrovni bajtov dokáže bezpečne pridať nešifrovaný objekt do tohto šifrovaného kontajnera, a nedokáže, z tých istých dôvodov §7.5.6, aké platia pre každú inú podmnožinu. Vlastné validátory zhody PDFiumPas pre tieto dva profily, ValidatePdfECompliance a ValidatePdfRCompliance, zaznamenávajú prítomnosť /Encrypt zámerne bez toho, aby ju označili ako chybu, čo je správne pre validátor iba na čítanie, ktorý nikdy nezapíše ani bajt. Je to tiež vzor, ktorý je ľahké prebehnúť očami a predpokladať, že súrodenský injektor nepotrebuje samostatnú stráž, hoci injektor je práve tá jedna funkcia z dvojice, ktorá musí skutočne odmietnuť
Dešifruje SaveAsPdfX váš dokument ticho?
Áno, vždy, keď prechádzate cez verejné pohodlné metódy namiesto priameho volania injektora. Každá z TPdf.SaveAsPdfA, SaveAsPdfX, SaveAsPdfUa, SaveAsPdfE, SaveAsPdfR, a SaveAsPdfVT vykreslí aktuálny dokument do dočasného prúdu pomocou SaveAs(Tmp, saRemoveSecurity) ešte pred tým, než tieto bajty odovzdá svojmu zodpovedajúcemu injektorovi. saRemoveSecurity mapuje na vlastný príznak PDFium FPDF_REMOVE_SECURITY, takže dočasná kópia, ktorú injektor dostane, na začiatku nikdy nebola šifrovaná, a stráž /Encrypt injektora nemá nikdy dôvod sa spustiť. Výstup nesie vaše značky PDF/A, PDF/X, PDF/UA, PDF/E-1, PDF/R-1, alebo PDF/VT-1, no už nie je chránený akýmkoľvek heslom, ktoré otvorilo zdroj
Tento kompromis je neviditeľný, kým niekto downstream neotvorí „chránenú“ archívnu kópiu bez hesla a nevšimne si, že to jednoducho funguje. Oprava nie je iné volanie metódy; PDFiumPas nemá žiadny náprotivok saAddSecurity na spárovanie s saRemoveSecurity, pretože podkladový engine PDFium nikdy nebol postavený na zápis nového šifrovania, iba na jeho odstránenie. Ak na oboch vlastnostiach záleží pre jeden súbor, šifrovanie musí byť samostatný krok, ktorý vlastníte vy, aplikovaný po značkách zhody, nie zabalený do toho istého volania 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;
Čo sa stane, keď podpíšete šifrované PDF pomocou PAdES?
PDFiumPas rovno odmietne, namiesto toho, aby požiadavku ticho zahodil, ako to robí injektor značiek. TPdf.SignPades aj SignPadesToStream obe smerujú cez internú SignPadesBytes, a prvá vec, ktorú urobí hneď po naparsovaní zdrojového trailera, je kontrola na /Encrypt. Ak je záznam prítomný, vyvolá EPadesCrypto so správou „SignPadesBytes: the source document is encrypted; remove encryption before signing“ namiesto toho, aby pokračovala ďalej. InjectPadesDssMarkers, funkcia, ktorá vkladá certifikáty, odpovede OCSP, a CRL pre dlhodobú validáciu, aplikuje identickú kontrolu z identického dôvodu, s vlastnou správou: „InjectPadesDssMarkers: the source document is encrypted; remove encryption before embedding DSS validation material“
Úvaha tu je prísnejšia ako u prepustenia injektorov značiek, a zámerne tak. Tiché prepustenie je pre pečiatku PDF/A bezpečné, pretože jeho preskočenie vás necháva s tým istým platným PDF, s akým ste začali, iba neoznačeným. Podpisovanie nemôže zlyhať rovnako ticho: podpis, ktorý bol ticho nikdy nepridaný, vyzerá pre akýkoľvek volajúci kód, ktorý kontroluje iba booleovský výsledok, presne ako podpis, ktorý bol úspešne pridaný. EPadesCrypto dedí od obyčajnej triedy Exception, takže jej zachytenie je normálne ošetrenie výnimiek, nie špeciálna konvencia riadenia toku, ktorú by ste sa museli učiť
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;
Poradie pečiatok zhody, podpisov, a šifrovania
Praktická oprava je poradie, nie iná knižnica. Aplikujte značky PDF/A, PDF/X, PDF/UA, PDF/E-1, PDF/R-1, alebo PDF/VT-1 ako prvé, pridajte akýkoľvek podpis PAdES ďalej, a až potom spustite akýkoľvek krok vo vašej pipeline, ktorý v skutočnosti vlastní šifrovanie, či už je to vyhradený zapisovač PDF, podpisovací prístroj, alebo vaša vlastná implementácia AES. Vrstva inkrementálnych aktualizácií PDFiumPas prirodzene zapadá do stredu tejto postupnosti, pripájajúc malé, cielené objekty na súbor, ktorý je inak hotový, a šifrovanie patrí na koniec presne preto, lebo je to jediná operácia v reťazci, ktorú samotný PDFiumPas nedokáže vykonať ani zvrátiť
Nič z toho nemení to, ako PDFiumPas číta trailer a dáta krížových odkazov, na ktorých každá inkrementálna aktualizácia závisí, čo je vlastný zdroj jemnosti, hneď ako do obrazu vstúpia prúdy xref; validácia prúdov objektov a xref v PDF pokrýva, ako tá istá cesta čítania trailera zvláda komprimované štruktúry PDF 1.5+. A hneď ako je dokument pripravený na niečo silnejšie než pečiatku zhody, podpisovanie PDF podpisom PAdES B-B v Delphi je miesto, kde SignPades preberá presne od bodu, kde tento článok končí
Injektory značiek a metódy SignPades opísané tu sa dodávajú ako súčasť komponentu PDFium pre Delphi a C++Builder, spolu s vykresľovaním a inšpekciou iba na čítanie, ktoré PDFium poskytuje natívne