Odborný článok

Validácia reťazca revízií PDF MAC v Delphi (ISO 32004)

HotPDF validuje ISO/TS 32004 PDF MAC na revíziu, nie na súbor. THotPDF.ValidatePDFMACChain prechádza každú inkrementálnu aktualizáciu od kotvy reťazca vpred a overuje každý MAC proti read-only prefixovému streamu končiacemu na vlastnom startxref a %%EOF tej revízie. Jeden platný MAC na najnovšej revízii nedokáže nič o revíziách pod ňou

Tu je scenár, ktorý to celé motivuje. Dodávate AES-256 šifrované PDF s PDF MAC. Niekto otvorí súbor v hex editore, preklopí bajt vo vnútri prvej MAC-chránenej revízie a potom pripojí úplne novú revíziu nesúcu úplne platný MAC po svojom. Každý prehliadač otvorí súbor bez sťažnosti a naivný kontrolór, ktorý hašuje aktuálny bajtový rozsah proti MAC v aktívnom traileri, hlási úspech — pretože ten MAC je skutočne správny pre bajty, ktoré pokrýva. Škoda sedí dve revízie nižšie, v regióne, ktorý nikto znovu neskontroloval

Prečo platný MAC najvyššej úrovne nedokáže, že súbor je neporušený?

Pretože PDF MAC pokrýva prefix, nie dokument. Inkrementálna aktualizácia je prvoradá časť formátu: každé uloženie pripojí nové telo, novú sekciu krížových referencií a nový trailer, kým staršie bajty zostávajú presne tam, kde boli. ISO/TS 32004 jazdí na tom modele, takže každá revízia nesie vlastný slovník /AuthCode autentizujúci súbor tak, ako stál v tom momente, a overenie len najnovšej necháva každú skoršiu revíziu nepreskúmanú. HotPDF preto vystavuje tie dve otázky ako dve volania a rozdiel medzi nimi je celá pointa tohto článku. ValidatePDFMAC odpovedá „je aktuálna revízia autentická" a plní záznam THPDFPDFMACValidationInfo; ValidatePDFMACChain odpovedá „je každá MAC-chránená revízia v tomto súbore autentická" a plní THPDFPDFMACChainValidationInfo s polom na revíziu plus strojovo čitateľným dôvodom zlyhania. Na vyššie popísanom súbore poškodenom a znovu MACnutom prvá volanie vráti True a druhé False pri indexe revízie 1

var
  Pdf: THotPDF;
  Chain: THPDFPDFMACChainValidationInfo;
  I: Integer;
begin
  Pdf := THotPDF.Create(nil);
  try
    if not Pdf.ValidatePDFMACChain('incoming.pdf', 'user', Chain) then
    begin
      // Zlyhanie je jedno z 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;

Každý MAC sa overuje na vlastnom prefixovom streame, nikdy na finálnej dĺžke súboru

Najdrahšia chyba v tejto oblasti je použiť finálnu veľkosť súboru ako hornú hranicu pri re-hashovaní staršej revízie, čo zloží koncové bajty do každého digestu okrem najnovšieho a hlási porušenie na zdravom súbore. HotPDF namiesto toho pre každú revíziu rekonštruuje ohraničený read-only stream končiaci na hodnote startxref tej revízie nasledovanej jej %%EOF a hašuje len ten. Nájsť hranicu je náročnejšie, než vyzerá: doslovné %%EOF sa môže objaviť vnútri obsahového streamu alebo reťazca, takže kandidát sa prijíma len keď bezprostredne predchádzajúce startxref parsuje na číslo rovné ofsetu krížovej referencie sekcie, ktorá sa validuje, s ničím okrem bieleho znaku medzi nimi. Revízia potom pohltí presne jednu sekvenciu konca riadku za markerom — jediný CR, jediný LF alebo jeden pár CRLF — a nič viac. To posledné pravidlo hrýze v praxi, lebo pisateľ, ktorý vyhodí extra prázdny riadok medzi dvomi revíziami, vyrobil bajty patriace ďalšej revízii a prehltnutie všetkých koncových bielych znakov do predchádzajúcej poticho mení oba digesty. Enumerácia sekcií sleduje tú istú disciplínu: HotPDF prechádza sekcie krížových referencií od najstaršej po najnovšiu presne raz, prehrávajúc položky free, direct aj object-stream, takže neskoršie sekcie prepisujú skorší stav, čo je opak sémantiky first-seen-wins, ktorú aplikuje parser aktívnych xref

HotPDF overuje každý PDF MAC ISO 32004 proti prefixovému streamu končiacemu na vlastnom startxref a markeri konca súboru tej revízie, takže preklopnutý bajt vo vnútri revízie 1 zlyhá reťazec, aj keď najnovší MAC sa stále čisto validuje
MAC každej revízie sa re-hashuje cez jej vlastný ohraničený prefix, takže úprava revízie 1 a pripojenie čerstvo MACnutej revízie stále uspokojí ValidatePDFMAC, zatiaľ čo ValidatePDFMACChain dopadne na revíziu 1

Kde sa reťazec kotví a čo ho láme?

Prvá revízia nesúca platný /AuthCode je kotva a FirstMACRevisionIndex hlási, kde ochrana začína; čokoľvek pred ňou nie je konštrukciou chránené, čo je normálne. Všetko po nej musí byť MAC-chránené, takže pripojenie jednej obyčajnej inkrementálnej aktualizácie k MAC-chránenému súboru zlyhá s pmcfRequiredRevisionMissing a indexom previnilej revízie — znášanie medzery by dovolilo útočníkovi strhnúť ochranu tým, že uloží ešte raz. Tri ďalšie invarianty držia cez reťazec, každý s vlastným kódom zlyhania

  • pmcfKDFSaltChanged/KDFSalt musí zostávať stabilný od kotvy, keďže rotujúca soľ by dovolila falšovateľovi znovu odvodiť kľúče pod parametrami vlastnej voľby
  • pmcfDigestDowngrade — sila digestu sa porovnáva proti poslednému overenému MAC, nie proti bezprostredne predchádzajúcej revízii, takže reťazec začínajúci pod profilom Modern pri SHA-384 nemôže poticho pokračovať so SHA-256
  • pmcfPermissionDowngrade — revízia nesmie zrušiť požiadavku PDF MAC, ktorú skoršia revízia autentizovala

Dôsledok, ktorý stojí za internalizáciu, je, že historické MACy sa overujú nezávisle aj potom, čo už nie sú aktívnym trailerom. Preto útok z úvodu — upraviť starú revíziu a pripojiť čerstvý MAC — neprežije: najnovší MAC sa správne overí, ValidatePDFMAC je spokojný a reťazec aj tak dopadne na revíziu 1 s pmcfRevisionInvalid

Poradie podpisov: kľúče trailera prvé, signatureDigest posledný

Keď je MAC pripojený k CMS podpisu, nie samostatný, poradie zápisu prestáva byť štylistická otázka. HotPDF vyžaduje, aby /AuthCode, /KDFSalt, vývojárska extenzia ISO 32004 a /SigObjRef boli zapísané do tej istej revízie pred výpočtom /ByteRange podpisu; pripojiť ktorýkoľvek z nich potom znamená, že tie bajty dopadnú mimo rozsah, ktorý podpis pokrýva, a vyrobí súbor, ktorého podpis sa overí, zatiaľ čo väzba MAC zostane nepodpísaná. Oba digesty potom bežia druhým smerom, čo vyzerá cyklicky na prvý pohľad a nie je. PDF MAC signatureDigest viaže surové oktety obsahu CMS SignerInfo.signature OCTET STRING — nie celé CMS DER a nie podpísané atribúty — takže sa konštruuje, keď surová hodnota podpisu už existuje, a vstrekuje ako nepodpísaný atribút id-attr-pdfMacData. Keďže /Contents je vylúčené z ByteRange podpisu a nepodpísané atribúty sa nikdy nekŕmia do výpočtu podpisu, sekvencia vyrobiť-podpis, postaviť-MAC, zabaliť-CMS sa čisto zatvára bez kryptografickej slučky. Nasledujú dva dôsledky: sentinel /ByteRange a placeholder /Contents musia zostať v plaintexte a mimo object streamov aj v šifrovanom súbore, inak ich patcher s pevnou šírkou nenájde; a keď je MAC digest tiež SHA-256, podpisový digest sa znovu použije celý, inak sa oba digestové kontexty aktualizujú v jednom prechode cez výstupný stream

Poradie zápisu HotPDF pre PDF MAC pripojený k CMS podpisu: kľúče MAC vstupujú do revízie skôr, než sa zmeria ByteRange, a podpisový digest sa stavia potom zo surových oktetov SignerInfo.signature
Zápis AuthCode, KDFSalt, SigObjRef a vývojárskej extenzie skôr, než sa zmeria ByteRange, je to, čo drží väzbu MAC vnútri rozsahu, ktorý podpis pokrýva
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;      // dokumentový digest SHA-384
    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 oba digesty
        // sa hlásia oddelene
        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;

Validácia sleduje tú istú cestu z druhého konca: prečíta priame /AuthCode z aktuálne aktívneho klasického traileru krížových referencií, nasleduje generácie-zohľadňujúcu nepriamu referenciu /SigObjRef, potvrdí, že viaže /V jediného podpisového poľa, a hlási zlyhanie dokumentového digestu oddelene od zlyhania podpisového digestu. To sú rôzne diagnózy a ich zbalenie do jedného booleanu zahodí jedinú informáciu, ktorá hovorí, či bolo dotknuté obsahová strana alebo hodnota podpisu. Ak už robíte CMS prácu, sedí to po boku článku o podpisovaní PAdES a sprievodcu overovaním podpisov v načítaných dokumentoch

Never dôverujte /P: najprv dešifrujte 16-bajtové /Perms

ISO/TS 32004 signalizuje „tento dokument vyžaduje PDF MAC" cez povolovací bit 13 a očividný spôsob čítania je nesprávny, lebo celé číslo /P v šifrovacom slovníku je plaintext a neautentizované — ktokoľvek môže preklopiť ten bit v textovom editore a degradovať požiadavku. ISO 32000-2 §7.6 dodáva odpoveď v položke /Perms a HotPDF ju používa: dešifrovať 16-bajtový reťazec /Perms kľúčom šifrovania súboru pod AES-256 CBC, nulové IV, bez výplne, potom skontrolovať každé pole plaintextu skôr, než uveríte čomukoľvek. Bajty 1 až 4 držia povolovaciu hodnotu v poradí little-endian a musia sa presne rovnať celému číslu /P; bajty 5 až 8 sú 0xFF; bajt 9 je vlajka šifrovania metadát T alebo F; bajty 10 až 12 sú doslovný marker adb. Len keď všetko toto drží, PermissionsAuthenticated sa stáva True a bit 13 sa číta — a majte na zreteli jeho polaritu, keďže požiadavka MAC sa tvrdí, keď je bit 0x1000 čistý. Nesúlad medzi /P a dešifrovanými povoleniami nie je varovanie na zalogovanie a prekročenie; je to falšovaná množina povolení a správna odpoveď je zlyhať bezpečne

HotPDF autentizuje povolenia PDF dešifrovaním šestnásťbajtového reťazca Perms kľúčom šifrovania súboru a kontrolou povolovacej hodnoty little-endian, výplňových bajtov FF, vlajky metadát a markera adb skôr, než prečíta bit 13
Plaintextové celé číslo /P nie je autentizované, takže požiadavka PDF MAC sa číta až po skontrolovaní každého poľa dešifrovaného /Perms

Algoritmická obratnosť končí na digeste

ISO/TS 32004 nechá vybrať dokumentový digest a len dokumentový digest. HotPDF drží HMAC-SHA-256 na autentizáciu, HKDF-SHA-256 per RFC 5869 na odvodenie kľúčov a AES-256 key wrap per RFC 3394 pevne pod variabilným THPDFPDFMACDigestAlgorithm rozpätím pmdaSHA256 po pmdaSHA3_512, lebo prirodzená chyba je brať „profil SHA3-512" ako licenciu vymeniť aj HMAC, čo dáva súbor, ktorý už nie je PDF MAC v žiadnom interoperabilnom zmysle. Jeden implementačný detail stojí za okopírovanie, ak píšete vlastný verifikátor: prečítať digest OID z CMS AuthenticatedData pred hašovaním bajtového rozsahu, keďže hardcodovanie SHA-256 a vysporiadanie sa potom mení obratnosť na nálepku a dovoľuje nepriateľskému súboru nechať vás streamovať celý dokument skôr, než objavíte, že algoritmus nebol nikdy podporovaný. CMSAlgorithmProtection, digestový algoritmus AuthenticatedData, integrity-info messageDigest a digest bajtového rozsahu musia všetky menovať jeden algoritmus a každý nesúlad zlyhá bezpečne

var
  Options: THPDFPDFMACOptions;
begin
  Options := THPDFPDFMACOptions.Compatibility;  // SHA-256, prijíma všetkých šesť
  Options := THPDFPDFMACOptions.Modern;         // SHA-384, zamieta 256-bit
  Options := THPDFPDFMACOptions.HighAssurance;  // len SHA3-512, AES-GCM

  // Vlastný profil je legálny, ale algoritmus, ktorým generuje,
  // sa musí tiež objaviť v allowliste validácie, inak je konfigurácia
  // zamietnutá skôr, než sa zapíše jediný bajt
  Options.Profile := pmppCustom;
  Options.DigestAlgorithm := pmdaSHA512;
  Options.AllowedDigestAlgorithms := [pmdaSHA512, pmdaSHA3_512];
  Options.RequireAESGCM := True;
end;

Čo PDF MAC dokáže a čo nedokáže

Overený reťazec PDF MAC dokazuje, že každá chránená revízia je bajtovo identická s tým, čo napísal niekto držiaci kľúč šifrovania súboru, že žiadna chránená revízia nebola odstránená ani preusporiadaná a že žiadna nechránená revízia nebola pripojená po kotve — presne trieda útokov, ktorú obyčajné šifrovanie AES-256 necháva otvorenú, keďže dôvernosť nehovorí nič o integrite a šifrované PDF so spájanou revíziou sa dešifruje rovnako spokojne ako neporušené. Čo nedokazuje, je autorstvo. Kľúč MAC sa odvádza z kľúča šifrovania súboru, takže ktokoľvek, kto môže otvoriť dokument, dokáže produkovať aj platný MAC nad zmenenou verziou, vrátane každého legitímneho príjemcu; je to symetrická primitíva a symetrické primitívy nemôžu atribuovať. Ak potrebujete vedieť kto niečo zmenil, potrebujete digitálny podpis s certifikátom v pozadí a PDF MAC ho potom dopĺňa tým, že chráni inkrementálnu štruktúru, ktorú samotný podpis nepokrýva. Zaobchádzajte s nimi ako s vrstvami a nechajte oba rozsudky hlásiť nezávisle namiesto zbalenia do jednej stavovej ikony

Vstupné body PDF MAC popísané tu — AddStandalonePDFMAC, SignPDFWithPFXAndAttachedPDFMAC, ValidatePDFMAC a ValidatePDFMACChain — dodáva štandardný HotPDF Delphi Component pre Delphi a C++Builder, kde produktová stránka nesie kompletnú referenciu pre záznam volieb, stavové enumerácie a validačné pole na revíziu