HotPDF provjerava PDF MAC prema ISO/TS 32004 po reviziji, a ne po datoteci. THotPDF.ValidatePDFMACChain prolazi kroz svaki inkrementalni update od sidra lanca naprijed i provjerava svaki MAC nad tokom samo za čitanje koji završava na startxref i %%EOF te iste revizije. Jedan valjani MAC na najnovijoj reviziji ne dokazuje ništa o revizijama ispod nje
Evo scenarija koji sve ovo motivira. Isporučujete AES-256 šifrirani PDF s PDF MAC-om na njemu. Netko otvori datoteku u heksadecimalnom uređivaču, preokrene jedan bajt unutar prve revizije zaštićene MAC-om, a zatim doda potpuno novu reviziju koja nosi sasvim valjani vlastiti MAC. Svaki preglednik otvara datoteku bez prigovora, i naivan provjeravatelj koji hashira trenutni bajtovski raspon protiv MAC-a u aktivnom traileru javlja uspjeh — jer taj MAC doista je točan za bajtove koje pokriva. Šteta leži dvije revizije niže, u regiji koju nitko nije ponovno provjerio
Zašto valjan MAC na najvišoj razini ne dokazuje da je datoteka netaknuta?
Zato što PDF MAC pokriva prefiks, a ne dokument. Inkrementalni update je sastavni dio formata: svako spremanje dodaje novo tijelo, novu sekciju unakrsnih referenci i novi trailer, dok stariji bajtovi ostaju točno tamo gdje su bili. ISO/TS 32004 počiva na tom modelu, pa svaka revizija nosi vlastiti rječnik /AuthCode koji ovjerava datoteku onakvom kakva je bila u tom trenutku, a provjera samo najnovijeg ostavlja svaku raniju reviziju nepregledanom. HotPDF zato izlaže ta dva pitanja kao dva poziva, a razlika među njima jest cijela poanta ovog članka. ValidatePDFMAC odgovara na pitanje je li trenutna revizija autentična, puneći zapis THPDFPDFMACValidationInfo; ValidatePDFMACChain odgovara na pitanje je li svaka revizija zaštićena MAC-om u ovoj datoteci autentična, puneći THPDFPDFMACChainValidationInfo nizom po reviziji plus strojno čitljivim razlogom pogreške. Na gore opisanoj datoteci koja je nakon neovlaštenog mijenjanja ponovno MAC-irana, prvi poziv vraća True, a drugi vraća False uz indeks revizije 1
var
Pdf: THotPDF;
Chain: THPDFPDFMACChainValidationInfo;
I: Integer;
begin
Pdf := THotPDF.Create(nil);
try
if not Pdf.ValidatePDFMACChain('incoming.pdf', 'user', Chain) then
begin
// Pogreška je jedna od pmcfRevisionBoundary, pmcfNoPDFMAC,
// pmcfRequiredRevisionMissing, pmcfRevisionInvalid,
// pmcfKDFSaltChanged, pmcfDigestDowngrade, pmcfPermissionDowngrade
Writeln('chain rejected: ', Chain.Message);
Writeln('revision ', Chain.FailureRevisionIndex,
' at xref offset ', Chain.FailureXRefOffset);
Exit;
end;
for I := 0 to High(Chain.Revisions) do
Writeln(I, ' len=', Chain.Revisions[I].RevisionLength,
' mac=', Chain.Revisions[I].HasPDFMAC,
' perms=', Chain.Revisions[I].PermissionsAuthenticated);
finally
Pdf.Free;
end;
end;
Svaki MAC provjerava se nad svojim vlastitim prefiksnim tokom, nikada nad konačnom duljinom datoteke
Najskuplja greška u ovom području jest korištenje konačne veličine datoteke kao gornje međe pri ponovnom hashiranju starije revizije, što u svaki sažetak osim najnovijeg uključuje i bajtove na kraju te javlja neovlašteno mijenjanje na ispravnoj datoteci. HotPDF umjesto toga za svaku reviziju rekonstruira ograničeni tok samo za čitanje koji završava na vrijednosti startxref te revizije iza koje slijedi njezin %%EOF, i hashira samo to. Lociranje te međe mukotrpnije je nego što izgleda: literal %%EOF može se pojaviti unutar sadržajnog toka ili niza, pa se kandidat prima samo kada neposredno prethodeći startxref parsira u broj jednak unakrsnom pomaku sekcije koja se provjerava, a među njima nema ničega osim praznina. Revizija potom upije točno jedan niz za kraj retka iza oznake — jedan CR, jedan LF ili jedan par CRLF — i ništa više. To posljednje pravilo u praksi boli, jer zapisivač koji emitira dodatni prazan redak između dvije revizije proizveo je bajtove koji pripadaju sljedećoj reviziji, a gutanje svih praznina na kraju u prethodnu tiho mijenja oba sažetka. Nabrajanje sekcija slijedi istu disciplinu: HotPDF prolazi kroz sekcije unakrsnih referenci od najstarije do najnovije točno jednom, ponavljajući slobodne, direktne i objektno-tokovske unose tako da kasnije sekcije prepisuju ranije stanje, što je suprotno od semantike prvi viđeni pobjeđuje koju primjenjuje parser aktivnog xref-a
Gdje se lanac sidri i što ga lomi?
Prva revizija koja nosi valjani /AuthCode jest sidro, a FirstMACRevisionIndex javlja gdje zaštita počinje; sve prije nje po konstrukciji je nezaštićeno, što je normalno. Sve iza nje mora biti zaštićeno MAC-om, pa dodavanje običnog inkrementalnog updatea datoteci zaštićenoj MAC-om propada s pmcfRequiredRevisionMissing i indeksom problematične revizije — trpljenje praznine omogućilo bi napadaču da skine zaštitu tim što će samo još jednom spremiti. Tri daljnje nepromjenjive osobine drže se kroz lanac, svaka sa svojim kodom pogreške
pmcfKDFSaltChanged—/KDFSaltmora ostati stabilan od sidra nadalje, jer rotirajuća sol omogućila bi krivotvoritelju da ponovno izvede ključeve pod parametrima po svom izborupmcfDigestDowngrade— snaga sažetka uspoređuje se s posljednjim provjerenim MAC-om, a ne s neposredno prethodećom revizijom, pa lanac koji počinje Modern profilom na SHA-384 ne može tiho nastaviti sa SHA-256pmcfPermissionDowngrade— revizija ne smije ukinuti zahtjev PDF MAC koji je ranija revizija ovjerila
Posljedica koju valja usvojiti jest da se povijesni MAC-ovi provjeravaju neovisno čak i kada više nisu aktivni trailer. Zato napad iz uvoda — izmijeni staru reviziju, pa dodaj svjež MAC — ne opstaje: najnoviji MAC prolazi provjeru sam za sebe, ValidatePDFMAC je zadovoljan, a lanac i dalje upada na reviziju 1 s pmcfRevisionInvalid
Redoslijed potpisa: prvo ključevi trailera, signatureDigest posljednji
Kada je MAC priložen uz CMS potpis umjesto da stoji samostalno, redoslijed pisanja prestaje biti stilsko pitanje. HotPDF zahtijeva da se /AuthCode, /KDFSalt, ISO 32004 razvojna ekstenzija i /SigObjRef zapišu u istu reviziju prije nego što se izračuna /ByteRange potpisa; ako bilo što od toga dodate nakon toga, ti bajtovi završe izvan raspona koji potpis pokriva, pa nastane datoteka čiji potpis prolazi provjeru dok je MAC veza nepotpisana. Dva sažetka zatim teku obrnutim smjerom, što na prvi pogled izgleda kružno a nije. signatureDigest PDF MAC-a veže sirove sadržajne oktete CMS SignerInfo.signature OCTET STRING-a — ne cijeli CMS DER i ne potpisane atribute — pa se gradi nakon što sirova vrijednost potpisa postoji i ubacuje se kao nepotpisani atribut id-attr-pdfMacData. Budući da je /Contents isključen iz ByteRange potpisa i da nepotpisani atributi nikada ne ulaze u izračun potpisa, slijed proizvedi-potpis, izgradi-MAC, zamotaj-CMS zatvara se čisto bez kriptografske petlje. Iz toga slijede dvije posljedice: čuvar /ByteRange i rezervirano mjesto /Contents moraju ostati otvoreni tekst i izvan objektnih tokova čak i u šifriranoj datoteci, inače popravljač fiksne širine ne može ih naći; i kada je i MAC sažetak SHA-256, sažetak za potpisivanje izravno se ponovno koristi, inače se oba konteksta sažetka ažuriraju u jednom prolazu kroz izlazni tok
var
Pdf: THotPDF;
Options: THPDFPDFMACOptions;
Info: THPDFPDFMACValidationInfo;
begin
Pdf := THotPDF.Create(nil);
try
Pdf.AutoLaunch := False;
Pdf.FileName := 'unsigned.pdf';
Pdf.ActivateProtection := True;
Pdf.CryptKeyLength := aesgcm;
Pdf.OwnerPassword := 'owner';
Pdf.UserPassword := 'user';
Pdf.ProtectOptions := [prPrint, prExtractContent];
Pdf.BeginDoc;
Pdf.CurrentPage.SetFont('Arial', [], 12);
Pdf.CurrentPage.AddSignedSignatureField('Approval',
Rect(72, 120, 280, 160), 16384);
Pdf.EndDoc;
Options := THPDFPDFMACOptions.Modern; // SHA-384 sažetak dokumenta
if THotPDF.SignPDFWithPFXAndAttachedPDFMAC('unsigned.pdf',
'signed.pdf', 'signer.pfx', 'pfx-secret', 'user', Options) then
if Pdf.ValidatePDFMAC('signed.pdf', 'user', Options, Info) then
begin
// Location = pmlAttachedToSignature, a dva sažetka
// javljaju se odvojeno
Writeln('signature object : ', Info.SignatureObjectNumber);
Writeln('signature digest : ', Info.SignatureDigestMatched);
Writeln('full file coverage: ', Info.FullFileCoverage);
Writeln('perms authentic : ', Info.PermissionsAuthenticated);
end;
finally
Pdf.Free;
end;
end;
Provjera prelazi isti put s druge strane: pročitajte izravni /AuthCode iz trenutno aktivnog klasičnog trailera unakrsnih referenci, slijedite generacijama upućeni indirektni /SigObjRef, potvrdite da veže /V jedinog polja potpisa i javite pogrešku sažetka dokumenta odvojeno od pogreške sažetka potpisa. To su različite dijagnoze, i njihovo stapanje u jedan boolean odbacuje jedinu informaciju koja kaže je li diran sadržaj stranice ili vrijednost potpisa. Ako već radite s CMS-om, ovo stoji uz članak o PAdES potpisivanju i vodič za provjeru potpisa u učitanim dokumentima
Nikada ne vjerujte /P: prvo dešifrirajte 16-bajtni /Perms
ISO/TS 32004 signalizira "ovaj dokument zahtijeva PDF MAC" preko dopuštenjskog bita 13, i očiti način čitanja jest pogrešan, jer je cjelobrojno /P u rječniku šifriranja otvoreni tekst bez ovjere — svatko može preokrenuti taj bit u tekstualnom uređivaču i sniziti zahtjev. ISO 32000-2 §7.6 daje odgovor u unosu /Perms, i HotPDF ga koristi: dešifrirajte 16-bajtni niz /Perms ključem za šifriranje datoteke pod AES-256 CBC, nultim IV-om, bez popune, a zatim provjerite svako polje otvorenog teksta prije nego što išta povjerujete. Bajtovi 1 do 4 drže vrijednost dopuštenja u little-endian redoslijedu i moraju točno odgovarati cjelobrojnom /P; bajtovi 5 do 8 su 0xFF; bajt 9 je zastavica šifriranja metapodataka T ili F; bajtovi 10 do 12 su doslovna oznaka adb. Tek kada sve to stoji, PermissionsAuthenticated postaje True i bit 13 se čita — i pazite na njegovu polaritet, jer se zahtjev MAC-a tvrdi kada je bit 0x1000 očišćen. Nepodudaranje između /P i dešifriranih dopuštenja nije upozorenje koje se zabilježi i preko njega prijeđe; to je krivotvoren skup dopuštenja, a pravi odgovor je propasti zatvoreno
Agilnost algoritma staje na sažetku
ISO/TS 32004 dopušta izbor sažetka dokumenta, i to samo sažetka dokumenta. HotPDF drži HMAC-SHA-256 za ovjeru, HKDF-SHA-256 prema RFC 5869 za izvođenje ključeva i AES-256 key wrap prema RFC 3394 fiksiranim ispod promjenjivog THPDFPDFMACDigestAlgorithm koji obuhvaća pmdaSHA256 do pmdaSHA3_512, jer je prirodna pogreška tretirati "SHA3-512 profil" kao dopuštenje da se zamijeni i HMAC, čime nastaje datoteka koja više nije PDF MAC ni u jednom smislu međusobne suradljivosti. Jedna provedbena pojedinost vrijedi prepisati ako pišete vlastiti provjeravatelj: pročitajte OID sažetka iz CMS AuthenticatedData prije hashiranja bajtovskog raspona, jer učvršćivanje SHA-256 u kod i pomirenje nakon toga pretvara agilnost u oznaku i dopušta neprijateljskoj datoteci da vas natjera da protočite cijeli dokument prije nego otkrijete da algoritam nikada nije ni bio podržan. CMSAlgorithmProtection, algoritam sažetka u AuthenticatedData, integrity-info messageDigest i sažetak bajtovskog raspona moraju svi imenovati jedan algoritam, i svako neslaganje propada zatvoreno
var
Options: THPDFPDFMACOptions;
begin
Options := THPDFPDFMACOptions.Compatibility; // SHA-256, prima svih šest
Options := THPDFPDFMACOptions.Modern; // SHA-384, odbija 256-bitne
Options := THPDFPDFMACOptions.HighAssurance; // samo SHA3-512, AES-GCM
// Prilagođeni profil je zakonit, ali algoritam s kojim generira
// mora se pojaviti i na popisu dopuštenih za provjeru, inače se
// konfiguracija odbija prije nego što se zapiše ijedan bajt
Options.Profile := pmppCustom;
Options.DigestAlgorithm := pmdaSHA512;
Options.AllowedDigestAlgorithms := [pmdaSHA512, pmdaSHA3_512];
Options.RequireAESGCM := True;
end;
Što PDF MAC dokazuje, a što ne
Provjeren lanac PDF MAC-a dokazuje da je svaka zaštićena revizija bajt po bajt identična onome što je zapisao netko tko drži ključ za šifriranje datoteke, da nijedna zaštićena revizija nije uklonjena ni preuređena i da iza sidra nije dodana nijedna nezaštićena revizija — točno ona klasa napada koju obično AES-256 šifriranje ostavlja otvorenom, jer povjerljivost ništa ne govori o integritetu i šifrirani PDF sa zalemljenom revizijom dešifrira se jednako veselo kao netaknut. Ono što ne dokazuje jest autorstvo. MAC ključ izvodi se iz ključa za šifriranje datoteke, pa svatko tko može otvoriti dokument može proizvesti i valjan MAC nad izmijenjenom verzijom, uključujući svakog zakonitog primatelja; to je simetrična primitiva, a simetrične primitive ne mogu pripisivati autorstvo. Ako trebate znati tko je nešto promijenio, treba vam digitalni potpis iza kojeg stoji certifikat, a PDF MAC ga tada dopunjuje štiteći inkrementalnu strukturu koju sam potpis ne pokriva. Tretirajte ih kao slojeve i pustite da se dvije presude javljaju neovisno umjesto da se strpe u jednu ikonicu stanja
Ulazne točke PDF MAC-a opisane ovdje — AddStandalonePDFMAC, SignPDFWithPFXAndAttachedPDFMAC, ValidatePDFMAC i ValidatePDFMACChain — isporučuju se sa standardnom HotPDF Delphi Component komponentom za Delphi i C++Builder, gdje stranica proizvoda nosi potpunu referencu za zapis opcija, nabrajanja stanja i niz provjere po reviziji