HotPDF načítá podepsaná, AES-256 šifrovaná PDF v Delphi mnohem rychleji než dřív, protože se z cesty načítání odstranily tři oddělené náklady: nebufferované parsování tokenů zdrojového streamu, druhý běh kontroly hesla Algorithm 2.A z ISO 32000-2 a pomalá vnitřní smyčka AES-128-CBC uvnitř R6 hardened hashe. Na Delphi cílech vzala sama oprava R6 načtení dokumentu z asi 66 ms na 16 ms
Ani jeden z nich se neukázal na malých testovacích souborech, které většina sad používá. Nepodepsaný, nešifrovaný vzorek o jednom či dvou kilobajtech se otevře za pár setin sekundy, ať dělá parser cokoli. Potíže začínají u dokumentů, které zákazníci skutečně posílají: smlouva s embedded CMS podpisem, kolem 35 KB, chráněná heslem revize 6. Ta kombinace zapne každou drahou kódovou cestu najednou a každá z nich měla vlastní důvod, proč byla pomalejší, než musela být. Tenhle článek jimi projde v pořadí, v jakém se našly, spolu s chybami cache klíčů, které se objevily, když se začaly memoizovat výsledky ověřování podpisů
Proč načtení podepsaného PDF 34 KB trvá 170 ms?
Podepsané PDF 34 KB se načítalo 170 ms, protože četl parser holý THandleStream přímo a každý bajt tokenu se měnil ve vlastní systémové volání ReadFile. Tokenizér PDF postupuje bajt po bajtu, dokud hledá oddělovače, takže nebufferovaný handle stream promění levný sken v paměti na tisíce přechodů do jádra
Detaily vysvětlují, proč bug přežil. THotPDF.LoadFromFile už dlouho balí svůj souborový zdroj do interního read bufferu a AutoDecrypt sonda se už dřív opravila ze stejného důvodu, když trávila 1,4 s znovu dotazováním velikosti souboru 56 MB. Podepsané a šifrované dokumenty ale berou jinou cestu. Aby se zajistilo, že líné čtení objektů a inspekce podpisů vždycky vidí tutéž neměnnou revizi, i poté, co volající uvolní svůj vlastní stream, bere THotPDF.LoadFromStream snapshot zdroje a parsuje ho podruhé. Ten druhý parse, spolu s reuse load používaným pro autentizované revize, četl snapshot handle bez bufferu před ním. Oba teď jdou přes stejného interního bufferovaného čteče, kterého používá souborová cesta
Ten buffer je interní, takže si ho nikdy nevytváříte sami. Ovládáte stream, který předáte LoadFromStream, a HotPDF ho čte tak, jak je, během prvního strukturálního průchodu. Jsou-li data už v paměti, předejte TMemoryStream. Jsou-li na disku, volejte LoadFromFile, nebo na Delphi 10.1 Berlin a novější dejte loaderu TBufferedFileStream místo holého TFileStream
uses
System.SysUtils, System.Classes, HPDFDoc;
function LoadEncryptedFromBytes(const FileName: string;
const Password: AnsiString): THotPDF;
var
Mem: TMemoryStream;
begin
Result := THotPDF.Create(nil);
Mem := TMemoryStream.Create;
try
Mem.LoadFromFile(FileName); // jeden hromadný read místo readů po tokenech
Mem.Position := 0;
// LoadFromStream vrací počet stránek, 0 když se dokument nenačetl
if Result.LoadFromStream(Mem, Password) < 1 then
raise Exception.Create('Could not load ' + FileName);
except
Result.Free;
Mem.Free;
raise;
end;
Mem.Free;
end;
Uvolnění memory streamu hned po načtení je bezpečné pro podepsaný a šifrovaný vstup, protože komponenta už drží vlastní snapshot bajtů. Ponechte si stejně původní soubor nebo buffer, pokud plánujete poté ověřovat podpisy, protože ověření hashuje syrové segmenty /ByteRange souboru, ne dešifrovaný objektový model
Měření načtení a ověření jako oddělených fází
Čas načtení a čas ověření podpisu se mají měřit odděleně, protože selhávají odlišně a opravují se na různých místech. Fáze načtení pokrývá parsování, kontrolu hesla a dešifrování. Fáze ověření znovu hashuje podepsané bajtové rozsahy a kontroluje CMS podpis. Slijete-li je do jednoho čísla, regrese v jednom se skryje v druhém
RTL TStopwatch ze System.Diagnostics na to stačí. Harness níže načítá s heslovým overloadem THotPDF.LoadFromFile a potom ověří první podpis přes TStream overload VerifyLoadedSignature. Stream overload je správná volba pro šifrovaný soubor, protože heslová načítací cesta nedrží název zdrojového souboru, který by bezparametrové overloady mohly znovuotevřít
uses
System.SysUtils, System.Classes, System.Diagnostics,
HPDFDoc, HPDFSignVerify;
procedure TimeSignedEncryptedLoad(const FileName: string;
const Password: AnsiString);
var
Pdf: THotPDF;
Src: TFileStream;
Info: THPDFSignatureInfo;
Status: THPDFSignatureVerifyStatus;
Watch: TStopwatch;
Pages: Integer;
begin
Pdf := THotPDF.Create(nil);
try
Watch := TStopwatch.StartNew;
Pages := Pdf.LoadFromFile(FileName, Password);
Writeln(Format('Load + decrypt: %d ms, %d pages, encrypted=%s',
[Watch.ElapsedMilliseconds, Pages, BoolToStr(Pdf.IsEncrypted, True)]));
if (Pages < 1) or (Pdf.GetLoadedSignatureFieldCount = 0) then
Exit;
Src := TFileStream.Create(FileName, fmOpenRead or fmShareDenyWrite);
try
Watch := TStopwatch.StartNew;
Status := Pdf.VerifyLoadedSignature(0, Src, Info);
Writeln(Format('Verify signature 0: %d ms, status %d, signer %s',
[Watch.ElapsedMilliseconds, Ord(Status), string(Info.SignerName)]));
finally
Src.Free;
end;
finally
Pdf.Free;
end;
end;
Pusťte to několikrát a ignorujte první běh. První volání platí jednorázové náklady nastavení, včetně stavby AES tabulek a běhu jejich self-testu, a disková cache zkreslí i první čtení. Hledáte ustálené číslo pro skutečný zákaznický soubor, měřené proti nepodepsané, nešifrované kopii téhož dokumentu
Proč se AES-256 souborový klíč odvozoval dvakrát?
HotPDF odvozoval AES-256 souborový klíč dvakrát, protože dvě fáze téhož načtení si každá pustila celou kontrolu hesla sama. Před strukturálním parsem prescanuje RecoverReloadKeysFromStreamRanges syrové bajty, aby se daly dešifrovat kontejnery šifrovaných object streamů, zatímco se dekódují filtry. Ten presken už provádí kontroly Algorithm 2.A z ISO 32000-2 §7.6.4.3.3, validuje heslo proti solím uživatele a vlastníka a obnovuje souborový šifrovací klíč. DecryptLoadedDocument pak ten výsledek ignoroval a dělal celé odvození znovu
U revize 4 a starších stála ta duplikace málo víc než smyčka MD5. U revize 6 volá Algorithm 2.A hardened hash Algorithm 2.B, takže duplikace zdvojnásobila nejdražší část otevírání souboru. Znovupoužít obnovený klíč je jednoduché. Znovu ho použít bezpečně je ta těžší část, protože cachovaný klíč, který přežije vlastní kontrolu hesla, se změní v obejití autentizace
Oprava omezuje znovupoužití scope příznakem, který je true jen mezi preskenem a voláním dešifrování uvnitř heslových overloadů LoadFromFile a LoadFromStream a maže se v bloku finally. Příznak se nastavuje jen tehdy, když presken heslo skutečně zvalidoval. Přímé volání DecryptLoadedDocument po prostém načtení vždycky pustí celé odvození, protože to heslo nebylo nikdy preskenované. Bez toho omezení by mohl zastaralý reload klíč z dřívějšího úspěšného otevření tiše dešifrovat dokument pro pozdějšího volajícího, který zadal špatné heslo, a to je o dost horší bug než pomalé načtení
Kde hardened hash R6 tráví svůj čas?
Hardened hash R6 tráví skoro všechen čas šifrováním AES-128-CBC. ISO 32000-2 §7.6.4.3.4 Algorithm 2.B opakuje kolo nejméně 64krát. Každé kolo postaví buffer 64 kopií hesla, aktuálního hashe a, na straně vlastníka, řetězce /U, zašifruje ten buffer AES-128-CBC s prvních 16 bajty hashe jako klíčem a dalšími 16 jako IV a vybere SHA-256, SHA-384 nebo SHA-512 pro další hash z prvních 16 bajtů ciphertextu čtených jako číslo modulo 3. Hashy jsou krátké. Buffery, které se šifrují, jsou několik kilobajtů na kolo
Implementace té smyčky v HotPDF, PDF20HashDanceR6, šifrovala buffer blok po bloku přes bajtově orientované referenční AES. Teď nejdřív zkusí nativní CBC rutinu bez paddingu, která na Delphi cílech dosáhne na linkovaný C AES backend. S tou změnou kleslo celé načtení dokumentu z asi 66 ms na 16 ms. Není-li nativní rutina dostupná nebo vrací-li délku, která nesedí, sáhne kód po historické blokové smyčce a ta smyčka může zase použít table-driven AES-128 blok, jehož T-tabulky se generují z existujícího S-boxu. Šifra je ve všech případech stejná, takže odvozený klíč je bajtově identický, bez ohledu na to, která cesta ho spočítala
Tenhle článek je o rychlosti. Dostat správně terminační pravidlo Algorithm 2.B je samostatný korektnostní problém a off-by-one tam odmítá správná hesla jen občas. Konfigurační stranu R5 versus R6 popisuje průvodce nastavením šifrování PDF AES-256 s HotPDF
Čtyři způsoby, jak vám rychlý AES blok může lhat
Table-driven AES blok může produkovat špatný ciphertext a pořád projít vlastním self-testem. Každá ze čtyř pastí níže se objevila při stavbě rychlé cesty a dvě z nich přešly self-testem, jak byl poprvé navržený
- Reentrance self-testu. Rutina připravenosti nastavuje svůj inicializační příznak před během self-testu, takže reentratní volání udělané během testu sáhne po referenční cestě. První verze testu proto srovnávala referenční implementaci se sebou samou. Opravou je nechráněný syrový rychlý blok, který self-test volá přímo, takže testovaným kódem je opravdu cesta tabulek
- Prohozené layouty tabulek. Výměna byte pořadí dvou ze čtyř round tabulek (Te1 a Te3) produkuje špatný ciphertext, který reentratní self-test neměl jak si všimnout. Chytila ho samostatná průzkumná utilitka, která tiskla ciphertext FIPS-197 přílohy C.1
69c4e0d86a7b0430d8cdb78070b4c55apro klíč000102…0fa plaintext00112233…ff. Self-test teď vyžaduje obojí, ten vektor i bajtovou rovnost s referenčním blokem, než rychlé cestě věří - Range a overflow kontroly FPC. Pod Free Pascal s
-Cra-Covyhazují Delphi stylu explicitní konverzeByte()a aritmetika posunů v bajtové šířce výjimky range nebo overflow. Indexy tabulek a zdvojnásobení GF(2^8) se teď počítají v nativní integer šířce a maskují sand $FF - Miskompilace inlinerem FPC. Malé helpery big-endian load a store beroucí parametry
PBytese zkompilovaly do access violation, když je pod FPC označilinline. Odstraněníinlineje opravilo a cena volání je zanedbatelná vedle table lookupů
Za finálním zapojením je ta samá zkušenost. Směrování každého AES-128 bloku v knihovně přes rychlé tabulky způsobilo deterministickou FPC access violation během jednoho načtení publikačního kandidáta, takže ta změna se vrátila zpět. Dodaný design je konzervativní: nejdřív nativní CBC, explicitní rychlotabulková větev uvnitř smyčky R6 a referenční blok všude jinde. Obecná lekce je, že nový kryptografický kód, který se musí sestavit pod několika kompilátory, potřebuje nezávislou known-answer sondu puštěnou na každém cílovém kompilátoru. Selhání testu na úrovni DLL vám neřekne, zda máte špatnou tabulku, nebo pád. Více ke straně Free Pascal v poznámkách o backendech Deflate a AES v HotPDF pod Free Pascal
Jak cachovat ověření podpisu, aniž byste cachovali špatnou odpověď?
Cache ověřování podpisů je bezpečná jen tehdy, když její klíč pokrývá každý vstup, který může změnit verdikt, včetně vstupů žijících mimo podepsané bajtové rozsahy. Headless dokumentová vrstva HotPDF memoizuje výsledky CMS ověření hashováním bajtů CMS, bajtů podpisového slovníku v mezeře /Contents, obsahů pokrytých rozsahů, fingerprintu politiky, rozpočtů ověření, celkové velikosti zdroje a jednominutového časového kbelíku. Tři z těch vstupů se přidaly jen poté, co bug ukázal, že chybí
Důvěryhodné certifikátové soubory se klíčují obsahem, ne cestou. Operace vrstva zapisuje tutéž PEM materiál pod čerstvým dočasným názvem souboru pro každý příkaz, takže klíč založený na cestě se nikdy netrefil a cache nikdy nezabrala. Celková velikost zdroje musela být součástí klíče, protože hlášené UnsignedTrailingBytes na ní závisí: základní revize a pozdější inkrementální update mohou sdílet přesně tentýž pokrytý obsah při různých délkách závěru a cache klíčovaná jen rozsahy by podala verdikt základní revize updatovanému souboru. Kontext toho, co ty závěrečné bajty znamenají, je v průvodci HotPDF ověřováním digitálních podpisů PDF v Delphi
Třetí bug byla jazyková past v Pascalu, ne designová chyba. Hash rutiny berou netypovaný const buffer a předání dynamického pole do netypovaného parametru předá adresu reference na pole, ne data pole
function Checksum(const Data; Count: Integer): Cardinal;
var
P: PByte;
I: Integer;
begin
Result := 0;
P := @Data;
for I := 0 to Count - 1 do
begin
Result := Result + P^;
Inc(P);
end;
end;
procedure DigestCMS(const CMS: TBytes);
begin
// Špatně: čte referenci na pole a cokoli, co za ní následuje na zásobníku.
// Čtení 16 KB odtud přeběhne stack page a vyhodí access
// violation, a Delphi a FPC selhávají na různých místech.
Writeln(Checksum(CMS, Length(CMS)));
// Správně: předejte první prvek, ohlídané pro prázdné pole
if Length(CMS) > 0 then
Writeln(Checksum(CMS[0], Length(CMS)));
end;
Zámek cache je SyncObjs.TCriticalSection místo API Windows InitializeCriticalSection, protože tutéž unit se staví i pod FPC na Linuxu, kde to API neexistuje
Co zůstává pomalé záměrně
Samotné odvození klíče R6 se mezi načteními nikdy necachuje a je to záměr. Externí bajty a explicitně zadané heslo vždycky procházejí skutečnou autentizační cestou, takže každé heslové načtení dokumentu R6 platí aspoň jeden hash Algorithm 2.B a obojí, hash ze strany uživatele i vlastníka, když se zkouší heslo vlastníka. Scope znovupoužití popsané výše odstraní duplikaci uvnitř jediného načtení a nic víc
- Číslo 66 ms na 16 ms platí pro Delphi cíle s linkovaným nativním AES backendem. FPC buildy berou table-driven blok a FPC generování kódu na Windows běželo pod stejnými asserty 2 až 4krát pomaleji než Delphi
- Ověřovací cache se váže na jednominutový časový kbelík, takže opakované kontroly stejných bajtů v krátkém okně jsou levné, zatímco cachovaný verdikt nikdy nepřežije minutu, ve které se spočítal
- Autority cache sloučených revizí se implementovala, odladila a pak odstranila. Nezavřela FPC mezeru a znamenitě zpomalila Delphi bezpečnostní callbacky, což je dobrá připomínka měřit, než cache necháte
- Ověření podpisu potřebuje původní šifrované bajty souboru, takže zdroj mějte dostupný. Rychlejší dešifrování ten požadavek neodstraní
FAQ
Oslabuje znovupoužití preskenovaného klíče kontrolu hesla?
Ne. HotPDF znovu používá obnovený souborový klíč jen uvnitř téhož volání heslového overloadu, které zvalidovalo heslo plnými kontrolami Algorithm 2.A, a scope příznak se maže v bloku finally. Samostatné volání DecryptLoadedDocument vždycky provede kompletní odvození, takže klíč zbylý z dřívějšího otevření se nepůjčí se špatným heslem
Proč necachovat výsledek R6 hashe mezi načteními?
Cachování hashe mezi načteními by znamenalo přijmout heslo bez zkontrolování proti otevíranému souboru. HotPDF bere každý externí bajtový zdroj a každé explicitní heslo jako nedůvěryhodný vstup, který se musí autentizovat sám, takže měřitelné zrychlení přišlo z toho, že se Algorithm 2.B zrychlil, ne z jeho vynechání
Mám předávat TFileStream do LoadFromStream?
Pro soubory na disku preferujte LoadFromFile, protože bufferuje interně sám. Když potřebujete stream overload, předejte TMemoryStream nebo TBufferedFileStream, protože HotPDF čte dodaný stream přímo během prvního strukturálního průchodu a holý handle stream stojí systémové volání za každé malé čtení
Načítání chráněné heslem, šifrování AES-256 R6, ověřování CMS podpisů i zbytek API načteného dokumentu dodává najednou jedna nativní VCL knihovna pro Delphi a C++Builder. Detaily funkcí a podporované verze IDE jsou na stránce produktu HotPDF Component