A HotPDF revíziónként, nem fájlonként validálja az ISO/TS 32004 PDF MAC-et. A THotPDF.ValidatePDFMACChain végigmegy minden inkrementális frissítésen a lánc-horgonytól előre, és minden MAC-et egy csak olvasható prefix streamen ellenőriz, amely az adott revízió saját startxref-jénél és %%EOF-jánál ér véget. Egy érvényes MAC a legújabb revízión semmit sem bizonyít az alatta lévő revíziókról
Íme az a forgatókönyv, amely mindezt motiválja. AES-256 titkosított PDF-et szállít rajta PDF MAC-kel. Valaki hexszerkesztőben nyitja meg a fájlt, átállít egy bájtot az első MAC-védett revízióban, majd egy vadonatúj revíziót fűz hozzá, amely tökéletesen érvényes, saját MAC-et visel. Minden megjelenítő panasz nélkül nyitja meg a fájlt, és a naiv ellenőrző, amely az aktív trailer MAC-éhez hasheli az aktuális bájttartományt, sikert jelent — mert az a MAC tényleg helyes az általa lefedett bájtokra. A kár két revízióval lejjebb ül, egy senki által újra nem ellenőrzött régióban
Miért nem bizonyítja egy érvényes legfelső MAC, hogy a fájl érintetlen?
Mert a PDF MAC prefixet fed le, nem dokumentumot. Az inkrementális frissítés a formátum elsőrendű része: minden mentés új törzset, új kereszthivatkozási szakaszt és új trailert fűz hozzá, miközben a régebbi bájtok pontosan ott maradnak, ahol voltak. Az ISO/TS 32004 erre a modellre épül, így minden revízió viseli a saját /AuthCode szótárát, amely a fájlt abban a pillanatban hitelesíti, amikor volt, és csak a legújabb ellenőrzése minden korábbi revíziót ellenőrizetlenül hagy. A HotPDF ezért a két kérdést két hívásként tárja fel, és a köztük lévő különbség e cikk egész lényege. A ValidatePDFMAC arra válaszol, hogy „hiteles-e az aktuális revízió", és kitölt egy THPDFPDFMACValidationInfo rekordot; a ValidatePDFMACChain arra, hogy „hiteles-e a fájl minden MAC-védett revíziója", és revíziónkénti tömbbel plusz géppel olvasható hibaokkal tölti a THPDFPDFMACChainValidationInfo-t. A fenti, megbolygatott, majd friss MAC-kel újraellátott fájlon az első hívás True-t ad, a második False-t az 1-es revízióindexre
var
Pdf: THotPDF;
Chain: THPDFPDFMACChainValidationInfo;
I: Integer;
begin
Pdf := THotPDF.Create(nil);
try
if not Pdf.ValidatePDFMACChain('incoming.pdf', 'user', Chain) then
begin
// A hiba egyike: 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;
Minden MAC a saját prefix streamén ellenőrizdik, sosem a végső fájlméreten
A terület legköltségesebb hibája a végső fájlméret használata felső határként egy régebbi revízió újrahashelésekor, ami a záró bájtokat minden digestbe begyűjti a legújabb kivételével, és hibásnak jelent egy ép fájlt. A HotPDF ehelyett minden revízióhoz egy határolt, csak olvasható streamet rekonstruál, amely az adott revízió saját startxref értékénél ér véget, amelyet a %%EOF-ja követ, és csak azt hasheli. A határ megkeresése alattomosabb, mint látszik: a literális %%EOF megjelenhet tartalomstreamben vagy sztringben, ezért egy jelölt csak akkor fogadható el, ha az azt közvetlenül megelőző startxref olyan számra parseol, amely egyenlő a validált szakasz kereszthivatkozási offszetjével, köztük kizárólag szóköz-karakterekkel. A revízió ezután pontosan egy sortörés-szekvenciát nyel el a marker után — egyetlen CR-t, egyetlen LF-et vagy egy CRLF párt — és semmi több. Ez az utolsó szabály a gyakorlatban csíp: az az író, amely két revízió közé plusz üres sort ír, a következő revízióhoz tartozó bájtokat termelt, és a záró szóköz-karakterek teljes lenyelése az előzőbe csendesen mindkét digestet megváltoztatja. A szakaszok felsorolása ugyanazt a fegyelmet követi: a HotPDF a kereszthivatkozási szakaszokat a legrégibbtől a legújabbig pontosan egyszer járja, szabadon, közvetlen és objektumstream-bejegyzéseket visszajátszva, így a későbbi szakaszok felülírják a korábbi állapotot, ami ellentéte az első-megnyerős szemantikának, amelyet egy aktív-xref elemző alkalmaz
Hol horgonyzik a lánc, és mi töri meg?
Az első, érvényes /AuthCode-ot viselő revízió a horgony, és a FirstMACRevisionIndex jelenti, hol kezdődik a védelem; minden előtte lévő a konstrukcióból fakadóan védetlen, ami normális. Utána mindennek MAC-védettnek kell lennie, ezért egy sima inkrementális frissítés hozzáfűzése MAC-védett fájlhoz pmcfRequiredRevisionMissing-el bukik meg, a vétkes revízióindexszel — egy rés tűrése lehetővé tenné a támadónak, hogy egyszerűen még egyszer mentve csupaszítsa le a védelmet. Három további invariáns áll a lánc mentén, mindegyik saját hibakóddal
pmcfKDFSaltChanged— a/KDFSalt-nek a horgonytól kezdve stabilnak kell maradnia, mert a forgó salt lehetővé tenné a hamisítónak, hogy saját maga által választott paraméterek alatt származtassa újra a kulcsokatpmcfDigestDowngrade— a digest erősségét az utoljára ellenőrzött MAC-hez mérik, nem a közvetlenül megelőző revízióhoz, így a Modern profil alatt SHA-384-gyel induló lánc nem folytatódhat csendesen SHA-256-talpmcfPermissionDowngrade— egy revízió nem törölhet olyan PDF MAC követelményt, amelyet egy korábbi revízió hitelesített
A belsővé tenni érdemes következmény az, hogy a történelmi MAC-ek egymástól függetlenül ellenőrizdnek, még akkor is, amikor már nem az aktív trailer. Ezért nem éli túl a bevezetőből ismert régi-revíziót-szerkeszt-majd-friss-MAC-et-fűz-hozzá támadás: a legújabb MAC önmagában kijön, a ValidatePDFMAC elégedett, és a lánc mégis az 1-es revízión landol pmcfRevisionInvalid-tal
Aláírás-sorrend: előbb a trailer kulcsok, a signatureDigest utoljára
Amikor a MAC CMS aláíráshoz kapcsolódik, nem pedig önállóan áll, az írási sorrend megszűnik stílus kérdése lenni. A HotPDF megköveteli, hogy a /AuthCode, a /KDFSalt, az ISO 32004 fejlesztői kiterjesztés és a /SigObjRef ugyanabba a revízióba íródjanak mielőtt az aláírás /ByteRange-jét kiszámolják; bármelyiket utána fűzve azok a bájtok az aláírás által lefedett tartományon kívülre kerülnek, és olyan fájl készül, amelynek aláírása validál, miközben a MAC kötés aláíratlan. A két digest ezután ellenkező irányban fut, ami első ránézésre körkörösnek tűnik, és nem az. A PDF MAC signatureDigest-je a CMS SignerInfo.signature OCTET STRING nyers tartalomoktettjeit köti — nem a teljes CMS DER-t, és nem az aláírt attribútumokat —, így akkor készül el, amikor a nyers aláírásérték már létezik, és id-attr-pdfMacData aláíratlan attribútumként kerül befecskendezésre. Mivel a /Contents ki van zárva az aláírás ByteRange-éből, és az aláíratlan attribútumok sosem táplálják az aláírásszámítást, az aláírás-előállítás, MAC-építés, CMS-csomagolás sorozat tisztán zárul, kriptográfiai kör nélkül. Két következmény következik: a /ByteRange jelzőnek és a /Contents helyőrzőnek titkosított fájlban is sík szövegben és objektumstreameken kívül kell maradnia, különben a fix szélességű patcher nem találja őket; és amikor a MAC digestje is SHA-256, az aláírási digest egyből újrahasznosul, különben mindkét digest kontextus egyetlen átmenésben frissül a kimeneti streamen
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 dokumentumdigest
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, és a két digest
// külön kerül jelentésre
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;
A validálás ugyanezt az utat járja vissza a másik végéről: kiolvassa a közvetlen /AuthCode-ot az éppen aktív klasszikus kereszthivatkozási trailerből, követi a generáció-tudatos közvetett /SigObjRef-et, megerősíti, hogy az az egyetlen aláírásmező /V-jéhez kötődik, és a dokumentumdigest-hibát elkülönítve jelenti az aláírási digest hibájától. Ezek különböző diagnózisok, és egyetlen booleanba süllyesztésük eldobja az egyetlen információt, amely megmondja, az oldaltartalmat vagy az aláírásértéket nyúlták-e. Ha már most CMS munkát végez, ez a PAdES aláíró cikk és a betöltött dokumentumokban történő aláírás-ellenőrzés útmutatója mellett ül
Sose bízzon a /P-ben: előbb fejtse vissza a 16 bájtos /Perms-t
Az ISO/TS 32004 a 13-as jogosultságbiten jelzi, hogy „ez a dokumentum PDF MAC-et követel", és a kézenfekvő olvasási mód a rossz, mert a titkosítási szótár /P egészszáma sík szöveg és hitelesítetlen — bárki átállíthatja azt a bitet egy szövegszerkesztőben, és leszállíthatja a követelményt. Az ISO 32000-2 §7.6 a választ a /Perms bejegyzésben adja, és a HotPDF használja: fejtse vissza a 16 bájtos /Perms sztringet a fájl titkosítási kulcsával AES-256 CBC alatt, nulla IV-vel, padding nélkül, majd ellenőrizze a sík szöveg minden mezőjét, mielőtt bármit elhigyen. Az 1–4. bájtok a jogosultságértéket tartalmazzák little-endian sorrendben, és pontosan egyezniük kell a /P egészszámmal; az 5–8. bájtok 0xFF; a 9. bájt a T vagy F metaadat-titkosítás jelző; a 10–12. bájtok a literális adb marker. Csak amikor mindez áll, lesz a PermissionsAuthenticated True, és olvasódik le a 13-as bit — és figyeljen a polaritására, mert a MAC követelmény akkor áll fenn, amikor a 0x1000 bit tisztán áll. A /P és a visszafejtett jogosultságok közötti eltérés nem naplózandó és továbblépendő figyelmeztetés; hamisított jogosultsághalmaz, és a helyes válasz a zárt módon való megbukás
Az algoritmus-ágilitás a digestnél megáll
Az ISO/TS 32004 a dokumentumdigest kiválasztását engedi, és csak a dokumentumdigestet. A HotPDF a HMAC-SHA-256-ot hitelesítésre, az RFC 5869 szerinti HKDF-SHA-256-ot kulcsszármaztatásra és az RFC 3394 szerinti AES-256 key wrap-et fixen tartja egy változó THPDFPDFMACDigestAlgorithm alatt, amely pmdaSHA256-tól pmdaSHA3_512-ig terjed, mert a kézenfekvő hiba a „SHA3-512 profilt" a HMAC cseréjére adott licencként kezelni, ami olyan fájlt termel, amely semmilyen interoperábilis értelemben már nem PDF MAC. Egy implementációs részletet érdemes átemelni, ha saját ellenőrzőt ír: olvassa ki a digest OID-t a CMS AuthenticatedData-ból mielőtt hashelné a bájttartományt, mert a SHA-256 bedrótozása és az utólagos egyeztetés az ágilitást címkévé zülleszti, és lehetővé teszi, hogy egy ellenséges fájl az egész dokumentumot streameltesse Önnel, mielőtt felfedezi, hogy az algoritmust sosem támogatták. A CMSAlgorithmProtection, a AuthenticatedData digest algoritmus, az integrity-info messageDigest és a bájttartomány-digest ugyanazt az egy algoritmust kell megneveznie, és bármilyen eltérés zárt módon bukik meg
var
Options: THPDFPDFMACOptions;
begin
Options := THPDFPDFMACOptions.Compatibility; // SHA-256, mind a hatot elfogadja
Options := THPDFPDFMACOptions.Modern; // SHA-384, a 256 bitest elutasítja
Options := THPDFPDFMACOptions.HighAssurance; // csak SHA3-512, AES-GCM
// Egy egyéni profil legális, de az az algoritmus, amellyel generál,
// szintén szerepelnie kell a validálás allowlistáján, különben a
// konfiguráció egyetlen bájt írása előtt elutasításra kerül
Options.Profile := pmppCustom;
Options.DigestAlgorithm := pmdaSHA512;
Options.AllowedDigestAlgorithms := [pmdaSHA512, pmdaSHA3_512];
Options.RequireAESGCM := True;
end;
Mit bizonyít és mit nem bizonyít a PDF MAC
Egy ellenőrzött PDF MAC lánc bizonyítja, hogy minden védett revízió bájtonként azonos azzal, amit a fájl titkosítási kulcsával rendelkező valaki írt, hogy egyetlen védett revíziót sem távolítottak el vagy rendeztek át, és hogy a horgony után nem fűztek hozzá védetlen revíziót — pontosan annak a támadásosztálynak a terepe, amelyet a sima AES-256 titkosítás nyitva hagy, hiszen a bizalmasságról nem szól az integritás, és egy beillesztett revíziójú titkosított PDF ugyanolyan vidáman fejt vissza, mint egy érintetlen. Amit nem bizonyít, az a szerzőség. A MAC kulcs a fájl titkosítási kulcsából származik, így bárki, aki megnyithatja a dokumentumot, érvényes MAC-et is termelhet módosított változaton felett, minden jogos címzettet beleértve; ez szimmetrikus primitív, a szimmetrikus primitívek pedig nem képesek tulajdonítani. Ha tudni akarja, ki változtatott meg valamit, tanúsítvánnyal háttérben álló digitális aláírásra van szüksége, és a PDF MAC azt egészíti ki azzal, hogy védi azt az inkrementális struktúrát, amelyet az aláírás önmagában nem fed. Kezelje őket rétegekként, és a két ítéletet jelentessék egymástól függetlenül, ahelyett, hogy egyetlen státuszikonba süllyesztenék őket
Az itt leírt PDF MAC belépőpontok — AddStandalonePDFMAC, SignPDFWithPFXAndAttachedPDFMAC, ValidatePDFMAC és ValidatePDFMACChain — a standard HotPDF Delphi Component-dal szállulnak Delphihez és C++Builderhez, ahol a terméklap viseli a teljes referenciát az options rekordhoz, a státusz enumerációkhoz és a revíziónkénti validálási tömbhöz