Tehnički članak

Validacija PDF MAC lanca revizija u Delphi-ju (ISO 32004)

HotPDF validira ISO/TS 32004 PDF MAC po reviziji, a ne po fajlu. THotPDF.ValidatePDFMACChain prelazi svaki inkrementalni update od sidra lanca unapred i verifikuje svaki MAC nad read-only prefiks tokom koji se završava na startxref i %%EOF te revizije. Jedan validan MAC na najnovijoj reviziji ne dokazuje ništa o revizijama ispod nje

Evo scenarija koji sve ovo motiviše. Isporučujete AES-256 šifrovan PDF sa PDF MAC-om na njemu. Neko otvori fajl u hex editoru, okrene bajt unutar prve MAC-zaštićene revizije, pa dodaje potpuno novu reviziju koja nosi savršeno ispravan MAC sopstveni. Svaki pregledač otvara fajl bez prigovora, i naivan proveravač koji hešuje tekući bajt opseg protiv MAC-a u aktivnom traileru prijavljuje uspeh — jer taj MAC zaista je ispravan za bajtove koje pokriva. Šteta sedi dve revizije niže, u oblasti koju niko nije ponovo proverio

Zašto validan najviši MAC ne dokazuje da je fajl netaknut?

Jer PDF MAC pokriva prefiks, ne dokument. Inkrementalni update je prvoklasni deo formata: svako čuvanje dodaje novo telo, novu sekciju unakrsnih referenci i novi trailer, dok stariji bajtovi ostaju tačno gde su bili. ISO/TS 32004 jaše na tom modelu, pa svaka revizija nosi svoj /AuthCode rečnik koji autentifikuje fajl kakav je bio u tom trenutku, a verifikacija samo najnovijeg ostavlja svaku raniju reviziju nepregledanu. HotPDF zato izlaže dva pitanja kao dva poziva, i razlika među njima je cela poenta ovog članka. ValidatePDFMAC odgovara na „da li je tekuća revizija autentična", puneći THPDFPDFMACValidationInfo zapis; ValidatePDFMACChain odgovara na „da li je svaka MAC-zaštićena revizija u ovom fajlu autentična", puneći THPDFPDFMACChainValidationInfo nizom po reviziji plus mašinski čitljivim razlogom pada. Na gore tamperovanom pa ponovo MAC-ovanom fajlu prvi poziv vraća True a drugi False protiv indeksa 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
      // Pad je jedan 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 verifikuje se na svom prefiks toku, nikad na konačnoj dužini fajla

Najskuplja greška u ovoj oblasti je korišćenje konačne veličine fajla kao gornje granice pri ponovnom hešovanju starije revizije, što savija prateće bajtove u svaki sažetak osim najnovijeg i prijavljuje neovlašćenu izmenu na zdravom fajlu. HotPDF umesto toga rekonstruiše, za svaku reviziju, ograničen read-only tok koji se završava na startxref vrednosti te revizije praćenoj njenim %%EOF, i hešuje samo to. Lociranje granice mučnije je nego što izgleda: literal %%EOF može se pojaviti unutar content toka ili stringa, pa se kandidat prima samo kada neposredno prethodeći startxref parsira u broj jednak xref ofsetu sekcije koja se validira, bez ičega osim beline između njih. Revizija zatim upija tačno jednu sekvencu kraja reda posle markera — jedan CR, jedan LF, ili jedan CRLF par — i ništa više. To poslednje pravilo ujeda u praksi, jer je pisac koji ispiše dodatni prazan red između dve revizije proizveo bajtove koje pripadaju sledećoj reviziji, i gutanje svih pratećih belina u prethodnu tiho menja oba sažetka. Enumeracija sekcija sledi istu disciplinu: HotPDF prelazi sekcije unakrsnih referenci od najstarije do najnovije tačno jednom, ponavljajući free, direct i object-stream unose tako da kasnije sekcije preslikaju ranije stanje, što je suprotno od prvo-viđen-pobeđuje semantike koju primenjuje aktivni xref parser

HotPDF verifikuje svaki ISO 32004 PDF MAC nad prefiks tokom koji se završava na startxref i oznaci kraja fajla te revizije, pa okrenut bajt unutar revizije 1 obara lanac iako se najnoviji MAC i dalje čisto validira
MAC svake revizije ponovo se hešuje nad njenim sopstvenim ograničenim prefiksom, pa uređivanje revizije 1 i dodavanje sveže MAC-ovane revizije i dalje zadovoljava ValidatePDFMAC dok ValidatePDFMACChain dospeva na reviziju 1

Gde se lanac sidri i šta ga kida?

Prva revizija koja nosi ispravan /AuthCode jeste sidro, a FirstMACRevisionIndex prijavljuje gde zaštita počinje; sve pre nje po konstrukciji je nezaštićeno, što je normalno. Sve posle nje mora biti MAC-zaštićeno, pa dodavanje jednog običnog inkrementalnog update-a na MAC-zaštićen fajl pada sa pmcfRequiredRevisionMissing i indeksom problematične revizije — tolerisanje praznine bi napadaču omogućilo da skine zaštitu prosto još jednim čuvanjem. Tri dalje nepromenljive važe kroz lanac, svaka sa svojim kodom pada

  • pmcfKDFSaltChanged/KDFSalt mora ostati stabilan od sidra nadalje, jer bi rotirajuća so krivotvorcu omogućila da ponovo izvede ključeve pod parametrima po sopstvenom izboru
  • pmcfDigestDowngrade — jačina sažetka poredi se sa poslednjim verifikovanim MAC-om, a ne sa neposredno prethodećom revizijom, pa lanac koji počinje Modern profilom na SHA-384 ne može tiho da nastavi sa SHA-256
  • pmcfPermissionDowngrade — revizija ne sme ukinuti zahtev za PDF MAC koji je ranija revizija autentifikovala

Posledica vredi utisnuti u sećanje: istorijski MAC-ovi verifikuju se nezavisno čak i kada više nisu aktivni trailer. Zato napad iz uvoda — izmeni staru reviziju pa nalepi svež MAC — ne preživljava: najnoviji MAC prolazi sam po sebi, ValidatePDFMAC je zadovoljan, a lanac i dalje dospeva na reviziju 1 sa pmcfRevisionInvalid

Redosled potpisa: prvo trailer ključevi, signatureDigest poslednji

Kada je MAC prikačen na CMS potpis umesto da stoji sam, redosled pisanja prestaje da bude stilska pretpostavka. HotPDF zahteva da se /AuthCode, /KDFSalt, ISO 32004 developer ekstenzija i /SigObjRef upišu u istu reviziju pre računanja potpisovog /ByteRange-a; dodajte bilo šta od toga kasnije i ti bajtovi ispadaju van opsega koji potpis pokriva, pa nastaje fajl čiji potpis prolazi dok MAC veza ostaje nepotpisana. Dva sažetka zatim teku suprotnim redom, što na prvi pogled deluje kružno ali nije. PDF MAC signatureDigest vezuje sirove oktete sadržaja CMS SignerInfo.signature OCTET STRING-a — ne ceo CMS DER, a ni potpisane atribute — pa se gradi tek kada sirova vrednost potpisa postoji i ubacuje se kao nepotpisani atribut id-attr-pdfMacData. Pošto je /Contents izuzet iz potpisovog ByteRange-a, a nepotpisani atributi nikad ne ulaze u računanje potpisa, sekvenca izradi-potpis, izgradi-MAC, omotaj-CMS se zatvara čisto, bez kriptografske petlje. Slede dva posledična pravila: /ByteRange čuvar i /Contents rezerva moraju ostati čist tekst i van object stream-ova čak i u šifrovanom fajlu, jer ih inače zakrpljivač fiksne širine ne može naći; i kada je MAC sažetak takođe SHA-256, sažetak za potpis se ponovo koristi direktno, inače se oba konteksta sažetka ažuriraju u jednom prolazu kroz izlazni tok

HotPDF redosled pisanja za PDF MAC prikačen na CMS potpis: MAC ključevi ulaze u reviziju pre merenja ByteRange-a, a sažetak potpisa se gradi kasnije iz sirovih okteta SignerInfo potpisa
Upisivanje AuthCode, KDFSalt, SigObjRef i developer ekstenzije pre merenja ByteRange-a je ono što drži MAC vezu unutar opsega koji potpis pokriva
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
        // se prijavljuju 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;

Validacija pređe isti put s druge strane: pročitajte direktan /AuthCode iz trenutno aktivnog klasičnog trailer-a unakrsnih referenci, pratite indirektan /SigObjRef koji vodi računa o generaciji, potvrdite da vezuje /V jedinog polja potpisa i prijavite pad sažetka dokumenta odvojeno od pada sažetka potpisa. To su dve različite dijagnoze, a njihovo stapanje u jedan boolean odbacuje jedinu informaciju koja govori da li je diran sadržaj stranice ili vrednost potpisa. Ako već radite CMS posao, ovo se nastavlja uz članak o PAdES potpisivanju i vodič za verifikaciju potpisa u učitanim dokumentima

Nikad ne verujte /P: prvo dešifrujte 16-bajtni /Perms

ISO/TS 32004 signalizira „ovaj dokument zahteva PDF MAC" kroz permission bit 13, a očigledan način da ga pročitate je pogrešan, jer je /P integer u encryption rečniku čist tekst i nije autentifikovan — svako može da okrene taj bit u tekst editoru i spusti zahtev. ISO 32000-2 §7.6 daje odgovor u /Perms unosu, i HotPDF ga koristi: dešifrujte 16-bajtni /Perms string ključem za šifrovanje fajla pod AES-256 CBC, nula IV, bez paddinga, a zatim proverite svako polje čistog teksta pre nego što išta prihvatite. Bajtovi 1 do 4 nose vrednost dozvola u little-endian redosledu i moraju se tačno poklapati sa /P integer-om; bajtovi 5 do 8 su 0xFF; bajt 9 je T ili F flag šifrovanja metapodataka; bajtovi 10 do 12 su literalni marker adb. Tek kada sve to stoji, PermissionsAuthenticated postaje True i bit 13 se čita — i pripazite na njegov polaritet, jer se zahtev za MAC tvrdi kada je 0x1000 bit nije postavljen. Neslaganje između /P i dešifrovanih dozvola nije upozorenje za dnevnik koje se preskače; to je krivotvoren skup dozvola, a prava reakcija je fail closed

HotPDF autentifikuje PDF dozvole dešifrovanjem šesnaestobajtnog Perms stringa ključem za šifrovanje fajla i proverom little-endian vrednosti dozvola, FF punjenja, flaga metapodataka i adb markera pre čitanja bita 13
Običan /P integer nije autentifikovan, pa se zahtev za PDF MAC čita tek kada je svako polje dešifrovanog /Perms provereno

Algoritamska agilnost staje na sažetku

ISO/TS 32004 ostavlja vam izbor sažetka dokumenta, i samo sažetka dokumenta. HotPDF drži HMAC-SHA-256 za autentifikaciju, HKDF-SHA-256 po RFC 5869 za izvođenje ključa i AES-256 key wrap po RFC 3394 fiksiranim ispod promenljive THPDFPDFMACDigestAlgorithm koja se proteže od pmdaSHA256 do pmdaSHA3_512, jer je prirodna greška da „SHA3-512 profil" shvatite kao dozvolu da zamenite i HMAC, čime nastaje fajl koji više nije PDF MAC u nikakvom interoperabilnom smislu. Jedan detalj implementacije vredi prekopirati ako pišete sopstveni verifikator: pročitajte digest OID iz CMS AuthenticatedData-a pre hešovanja bajt opsega, jer ukodirati SHA-256 na tvrdo pa se pomiriti kasnije pretvara agilnost u nalepnicu i pušta neprijateljskom fajlu da vas natera da pročitate ceo dokument pre nego što otkrijete da algoritam nikad nije bio podržan. CMSAlgorithmProtection, digest algoritam AuthenticatedData-a, integrity-info messageDigest i sažetak bajt opsega moraju svi imenovati jedan algoritam, i svako neslaganje je fail closed

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đen profil je legalan, ali algoritam kojim generiše
  // mora se pojavljivati i u allowlisti validacije, inače je
  // konfiguracija odbijena pre nego što se upiše jedan bajt
  Options.Profile := pmppCustom;
  Options.DigestAlgorithm := pmdaSHA512;
  Options.AllowedDigestAlgorithms := [pmdaSHA512, pmdaSHA3_512];
  Options.RequireAESGCM := True;
end;

Šta PDF MAC dokazuje, a šta ne

Verifikovan lanac PDF MAC-ova dokazuje da je svaka zaštićena revizija bajt-identična onome što je upisao neko ko drži ključ za šifrovanje fajla, da nijedna zaštićena revizija nije uklonjena ni preuređena i da nijedna nezaštićena revizija nije dodata posle sidra — upravo klasa napada koju obično AES-256 šifrovanje ostavlja otvorenom, jer poverljivost ništa ne kaže o celovitosti, a šifrovan PDF sa zalepljenom revizijom dešifruje se podjednako rado kao netaknut. Ono što ne dokazuje je autorstvo. MAC ključ izvodi se iz ključa za šifrovanje fajla, pa svako ko može da otvori dokument može da proizvede i ispravan MAC nad izmenjenom verzijom, svaki legitimni primalac uključen; to je simetrična primitiva, a simetrične primitive ne umeju da pripisuju autorstvo. Ako treba da znate ko je nešto izmenio, treba vam digitalni potpis sa sertifikatom iza njega, a PDF MAC ga onda dopunjuje štiteći inkrementalnu strukturu koju sam potpis ne pokriva. Tretirajte ih kao slojeve i pustite da se dve presude prijavljuju nezavisno, umesto da se stope u jednu ikonicu statusa

Ulazne tačke PDF MAC-a opisane ovde — AddStandalonePDFMAC, SignPDFWithPFXAndAttachedPDFMAC, ValidatePDFMAC i ValidatePDFMACChain — stižu uz standardni HotPDF Delphi Component za Delphi i C++Builder, gde stranica proizvoda nosi kompletnu referencu za zapis opcija, enumeracije statusa i niz validacije po reviziji