Techninis straipsnis

PDF MAC revizijų grandinės patikra Delphi (ISO 32004)

HotPDF ISO/TS 32004 PDF MAC tikrina pagal revizijas, o ne pagal failą. THotPDF.ValidatePDFMACChain eina per kiekvieną žingsninį atnaujinimą nuo grandinės inkaro pirmyn ir kiekvieną MAC verifikuoja prieš tik skaitymui skirtą prefikso srautą, besibaigiantį tos pačios revizijos startxref ir %%EOF. Vienas teisingas MAC naujausioje revizijoje nieko nepasako apie revizijas žemiau jos

Štai scenarijus, kuris visa tai motyvuoja. Išsiunčiate AES-256 užšifruotą PDF su PDF MAC. Kas nors atidaro failą šešioliktainių redaktoriuje, pakeičia vieną baitą pirmojoje MAC saugomoje revizijoje ir prideda visiškai naują reviziją su savo tobulo teisingumo MAC. Kiekviena žiūrimoji programa atidaro failą be priekaištų, o naivus tikrintuvas, maišantis dabartinį baitų intervalą su aktyvaus trailerio MAC, praneša sėkmę — nes tas MAC tikrai teisingas baitams, kuriuos jis dengia. Žala sėdi dviem revizijomis žemiau, srityje, kurios niekas pakartotinai netikrino

Kodėl teisingas viršutinio lygio MAC neįrodo, kad failas nepažeistas?

Nes PDF MAC dengia prefiksą, o ne dokumentą. Žingsninis atnaujinimas yra pilnateisė formato dalis: kiekvienas išsaugojimas prijungia naują turinį, naują kryžminių nuorodų sekciją ir naują trailerį, o senesni baitai lieka lygiai ten, kur buvo. ISO/TS 32004 remiasi tuom modeliu, tad kiekviena revizija neša savą /AuthCode žodyną, autentikuojantį failą tos akimirkos būsenoje, ir vien naujausios verifikavimas palieka visas ankstesnes revizijas netirtas. HotPDF todėl abu klausimus atveria kaip du iškvietimus, ir skirtumas tarp jų yra viso šio straipsnio esmė. ValidatePDFMAC atsako, ar dabartinė revizija autentiška, užpildydamas THPDFPDFMACValidationInfo įrašą; ValidatePDFMACChain atsako, ar kiekviena šio failo MAC saugoma revizija autentiška, užpildydamas THPDFPDFMACChainValidationInfo su atskirai revizijai skirtu masyvu ir mašininio skaitomo žlugimo priežastimi. Ant aukščiau sugadinto ir nauju MAC apklijuoto failo pirmasis iškvietimas grąžina True, o antrasis — False ties revizijos indeksu 1

var
  Pdf: THotPDF;
  Chain: THPDFPDFMACChainValidationInfo;
  I: Integer;
begin
  Pdf := THotPDF.Create(nil);
  try
    if not Pdf.ValidatePDFMACChain('incoming.pdf', 'user', Chain) then
    begin
      // Žlugimas yra vienas iš 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;

Kiekvienas MAC verifikuojamas ant savo prefikso srauto, niekada ant galutinio failo ilgio

Brangiausia klaida šioje srityje — naudoti galutinį failo dydį kaip viršutinę ribą pakartotinai maišant senesnę reviziją: tai supina galinius baitus į kiekvieną maišos sandaugą, išskyrus naujausią, ir praneša apie klastotę ant sveiko failo. HotPDF vietoj to kiekvienai revizijai atkuria ribotą tik skaitymui skirtą srautą, besibaigiantį tos pačios revizijos startxref reikšme ir po jos einančiu %%EOF, ir maišo tik tą. Ribos suradimas sudėtingesnis, nei atrodo: literálą %%EOF galima sutikti turinio srauto viduje arba eilutėje, tad kandidatas priimamas tik tada, kai iškart prieš jį einantis startxref išanalizuotas į skaičių, lygų tikrinamos sekcijos kryžminių nuorodų poslinkiui, o tarp jų — nieko, išskyrus tarpus. Revizija paskui prisisiauna lygiai vieną eilutės pabaigos seką po žymeklio — vieną CR, vieną LF arba vieną CRLF porą — ir nieko daugiau. Tas paskutinė taisyklė praktikoje įkanda, nes rašytojas, išduodantis papildomą tuščią eilutę tarp dviejų revizijų, yra pagaminęs baitus, priklausančius sekančiai revizijai, o visų galinių tarpų prarijimas į ankstesnę reviziją abu maišus keičia nepastebimai. Sekcijų išvardijimas laikosi tos pačios disciplinos: HotPDF eina per kryžminių nuorodų sekcijas nuo seniausios iki naujausios lygiai kartą, atkurdamas laisvus, tiesioginius ir objektų srautų įrašus, kad vėlesnės sekcijos perrašytų ankstesnę būseną — tai priešingybė pirmasis-laimi semantikai, kuria vadovaujasi aktyvaus xref analizatorius

HotPDF kiekvieną ISO 32004 PDF MAC verifikuoja prieš prefikso srautą, besibaigiantį tos revizijos paties startxref ir failo pabaigos žymekliu, tad baitas, pakeistas revizijos 1 viduje, sugriauna grandinę, nors naujausias MAC tebeatitinka švariai
Kiekvienos revizijos MAC pakartotinai maišomas ant jos pačios riboto prefikso, tad revizijos 1 redagavimas ir šviežio MAC pridėjimas vis dar tenkina ValidatePDFMAC, o ValidatePDFMACChain nusileidžia ties revizija 1

Kur inkaruojasi grandinė ir kas ją sugriauna?

Pirmoji revizija su teisingu /AuthCode yra inkaras, o FirstMACRevisionIndex praneša, kur prasideda apsauga; visa prieš ją pagal konstrukciją neapsaugota, ir tai normalu. Viskas po jos turi būti MAC saugoma, tad vieno paprasto žingsninio atnaujinimo pridėjimas prie MAC saugomo failo žlunga su pmcfRequiredRevisionMissing ir kaltos revizijos indeksu — tarpo toleravimas leistų puolėjui nuplėšti apsaugą vien dar kartą išsaugojus. Trys tolesnės invariantos laiko per visą grandinę, kiekviena su savo žlugimo kodu

  • pmcfKDFSaltChanged/KDFSalt turi likti stabilus nuo inkaro pirmyn, nes besisukanti druska leistų klastotojui iš naujo išvesti raktus savo pasirinktais parametrais
  • pmcfDigestDowngrade — maišos stiprumas lyginamas su paskutiniu patikrintu MAC, o ne su iškart ankstesne revizija, tad grandinė, prasidėjusi Modern profiliu su SHA-384, negali tyliai tęstis su SHA-256
  • pmcfPermissionDowngrade — revizija negali atšaukti PDF MAC reikalavimo, kurį ankstesnė revizija buvo autentikavusi

Pasekmė, verta įsisavinti: istoriniai MAC verifikuojami nepriklausomai net tada, kai jie nebėra aktyvus traileris. Būtent todėl atakos iš įvado — paredaguoti seną reviziją ir prijungti šviežią MAC — nepavyksta: naujausias MAC atsiskaitys pats, ValidatePDFMAC patenkins, o grandinė vis tiek nusileis ties revizija 1 su pmcfRevisionInvalid

Parašo tvarka: trailerio raktai pirmiau, signatureDigest paskiausiai

Kai MAC pririšamas prie CMS parašo, o ne stovi savarankiškai, rašymo tvarka nustoja būti stiliaus klausimu. HotPDF reikalauja, kad /AuthCode, /KDFSalt, ISO 32004 kūrėjo plėtinys ir /SigObjRef būtų įrašyti į tą pačią reviziją prieš skaičiuojant parašo /ByteRange; pridėjus ką nors iš jų vėliau, tie baitai atsiduria už intervalo, kurį dengia parašas, ir failas gaunasi su parašu, kuris atitinka, o MAC susiejimas nepasirašytas. Abu maišai paskui eina priešinga kryptimi, kas iš pirmo žvilgsnio atrodo cikliškai ir nėra tokia. PDF MAC signatureDigest suriša žalius CMS SignerInfo.signature OCTET STRING turinio oktetus — ne visą CMS DER ir ne pasirašytus atributus — todėl jis sudaromas, kai žaliąja parašo reikšmė jau egzistuoja, ir įšvirkščiamas kaip id-attr-pdfMacData nepasirašytas atributas. Kadangi /Contents iš parašo ByteRange išbrauktas, o nepasirašyti atributai niekada nemaitina parašo skaičiavimo, seka pagamink-parašą, sukonstruok-MAC, apvyniok-CMS užsidaro švariai be kriptografinės kilpos. Iš to dvi išvados: /ByteRange žymeklis ir /Contents vietos ženklas turi likti atviro teksto ir už objektų srautų ribų net užšifruotame faile, kitaip fiksuoto pločio lopys jų neras; o kai MAC maiša irgi SHA-256, pasirašymo maiša pernaudojama tiesiogiai, kitu atveju abu maišos kontekstai atnaujinami vienu pravažiavimu per išvesties srautą

HotPDF rašymo tvarka PDF MAC, pririštam prie CMS parašo: MAC raktai į reviziją įeina prieš matuojant ByteRange, o parašo maiša paskui sukonstruojama iš žalių SignerInfo parašo oktetų
AuthCode, KDFSalt, SigObjRef ir kūrėjo plėtinio įrašymas prieš matuojant ByteRange yra tai, kas laiko MAC susiejimą intervalo, kurį dengia parašas, viduje
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 dokumento maiša
    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, o abu maišai
        // pranešami atskirai
        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;

Verifikavimas grįžta tomis pačiomis kryptimis iš kito galo: skaito tiesioginį /AuthCode iš dabar aktyvaus klasikinio kryžminių nuorodų trailerio, seka į kartas atsižūrintį netiesioginį /SigObjRef, patvirtina, kad jis suriša to vienintelio parašo lauko /V, ir praneša dokumento maišos žlugimą atskirai nuo parašo maišos žlugimo. Tai skirtingos diagnozės, o jų suspaudimas į vieną loginę reikšmę išmeta vienintelę informaciją, sakiusią, ar buvo liesti puslapio turinys, ar parašo reikšmė. Jei jau dirbate su CMS, tai dedasi šalia PAdES pasirašymo straipsnio ir parašų tikrinimo įkeltuose dokumentuose vadovo

Nepasitikėk /P: pirmiau iššifruok 16 baitų /Perms

ISO/TS 32004 signalą „šiam dokumentui reikia PDF MAC" perduoda per leidimų bitą 13, ir akivaizdus būdas jį skaityti yra neteisingas, nes /P sveikasis skaičius šifravimo žodyne yra atviro teksto ir neautentikuotas — bet kas gali tą bitą perjungti tekstų redaktoriuje ir pažeminti reikalavimą. ISO 32000-2 §7.6 atsakymą duoda /Perms įraše, ir HotPDF juo naudojasi: 16 baitų /Perms eilutę iššifruokite failo šifravimo raktu su AES-256 CBC, nuliniu IV, be užpildo, paskui patikrinkite kiekvieną atviro teksto lauką, prieš tikėdami kuo nors. Baitai 1–4 laiko leidimų reikšmę little-endian tvarka ir turi lygiai sutapti su /P sveikuoju skaičiumi; baitai 5–8 yra 0xFF; baitas 9 yra T arba F metaduomenų šifravimo požymis; baitai 10–12 yra literálas adb. Tik kai visa tai tenkinama, PermissionsAuthenticated tampa True ir bitas 13 skaitomas — ir atsiminkite, kokia jo reikšmė reiškia ką: MAC reikalavimas teigiamas, kai 0x1000 bitas nuimtas. Nesutapimas tarp /P ir iššifruotų leidimų nėra įspėjimas, kurį užregistruoji ir praeini; tai suklastotas leidimų rinkinys, ir teisingas atsakas — žlugti uždarai

HotPDF autentikuoja PDF leidimus iššifruodamas šešiolikos baitų Perms eilutę failo šifravimo raktu ir patikrindamas little-endian leidimų reikšmę, FF užpildo baitus, metaduomenų požymį ir adb žymeklį prieš skaitant bitą 13
Atviro teksto /P sveikasis skaičius neautentikuotas, todėl PDF MAC reikalavimas skaitomas tik patikrinus kiekvieną iššifruoto /Perms lauką

Algoritmų lankstumas stoja ties maiša

ISO/TS 32004 leidžia rinktis dokumento maišą — ir tik dokumento maišą. HotPDF laiko HMAC-SHA-256 autentikacijai, HKDF-SHA-256 pagal RFC 5869 raktų išvedimui ir AES-256 key wrap pagal RFC 3394 fiksuotus po kintamu THPDFPDFMACDigestAlgorithm, besitęsiančiu nuo pmdaSHA256 iki pmdaSHA3_512, nes natūrali klaida — „SHA3-512 profilį" traktuoti kaip licenziją keisti ir HMAC, iš ko gaunamas failas, kuris jokia tarpusavio suderinamumo prasme nebėra PDF MAC. Viena realizacijos detalė verta perimti, jei rašote savą verifikatorių: maišos OID paimkite iš CMS AuthenticatedData prieš maišydami baitų intervalą, nes užkoduotas SHA-256 ir vėlesnis suderinimas iš lankstumo daro tik etiketę ir leidžia priešiškam failui priversti pravėsti visą dokumentą prieš sužinant, kad algoritmas niekada nebuvo palaikomas. CMSAlgorithmProtection, AuthenticatedData maišos algoritmas, integrity-info messageDigest ir baitų intervalo maiša turi visi įvardyti vieną algoritmą, o bet koks nesutapimas žlunga uždarai

var
  Options: THPDFPDFMACOptions;
begin
  Options := THPDFPDFMACOptions.Compatibility;  // SHA-256, priima visus šešis
  Options := THPDFPDFMACOptions.Modern;         // SHA-384, atmeta 256 bitų
  Options := THPDFPDFMACOptions.HighAssurance;  // tik SHA3-512, AES-GCM

  // Pasirinktinis profilis teisėtas, bet algoritmas, kuriuo jis generuoja,
  // turi taip pat atsirasti patikros leistinųjų sąraše, kitaip konfigūracija
  // atmetama, kol dar neįrašytas nė vienas baitas
  Options.Profile := pmppCustom;
  Options.DigestAlgorithm := pmdaSHA512;
  Options.AllowedDigestAlgorithms := [pmdaSHA512, pmdaSHA3_512];
  Options.RequireAESGCM := True;
end;

Ką PDF MAC įrodo ir ko neįrodo

Patikrinta PDF MAC grandinė įrodo, kad kiekviena saugoma revizija baitu į baitus identiška tai, ką parašė failo šifravimo rakto turėtojas, kad nė viena saugoma revizija nepašalinta ir neperrūšiuota, ir kad po inkaro nepridėta jokia neapsaugota revizija — lygiai ta atakų klasė, kurią palieka atvirą paprastas AES-256 šifravimas, nes konfidencialumas nieko nesako apie vientisumą, ir užšifruotas PDF su įkišta revizija iššifruojamas taip pat laimingai kaip nepažeistas. Ko tai neįrodo — autorystės. MAC raktas išvedamas iš failo šifravimo rakto, tad bet kas, kas gali atidaryti dokumentą, gali pagaminti ir teisingą MAC ant pakeistos versijos, įskaitant kiekvieną teisėtą gavėją; tai simetrinis primitivas, o simetriniai primitivai negali priskirti autorystės. Jei reikia žinoti, kas pakeitė, jums reikia skaitmeninio parašo su sertifikatu, o PDF MAC tada jį papildo, saugodamas žingsninę struktūrą, kurios vienas parašas nedengia. Laikykite juos sluoksniais ir leiskite abi išvadas pranešti nepriklausomai, užuot suglaudus į vieną būsenos piktogramą

Čia aprašyti PDF MAC įėjimo taškai — AddStandalonePDFMAC, SignPDFWithPFXAndAttachedPDFMAC, ValidatePDFMAC ir ValidatePDFMACChain — tiekiami su standartiniu HotPDF Delphi Component Delphi ir C++Builder aplinkoms, kur produkto puslapyje yra pilna parinkčių įrašo, būsenų išvardijimų ir atskirai revizijai skirtos patikros masyvo dokumentacija