Articol tehnic

Validarea lanțului de revizii PDF MAC în Delphi (ISO 32004)

HotPDF validează un PDF MAC ISO/TS 32004 per revizie, nu per fișier. THotPDF.ValidatePDFMACChain parcurge fiecare actualizare incrementală de la ancora lanțului înainte și verifică fiecare MAC contra unui flux prefix doar în citire care se termină la propriul startxref și %%EOF al reviziei. Un MAC valid pe cea mai nouă revizie nu dovedește nimic despre reviziile de sub ea

Iată scenariul care motivează totul. Livrați un PDF criptat AES-256 cu un PDF MAC pe el. Cineva deschide fișierul într-un editor hex, întoarce un octet în interiorul primei revizii protejate de MAC, apoi anexează o revizie complet nouă care poartă un MAC perfect valid al său. Fiecare vizualizator deschide fișierul fără plângere, iar un verificator naiv care hash-uiește intervalul de octeți curent contra MAC-ului din trailerul activ raportează succes — pentru că acel MAC este într-adevăr corect pentru octeții pe care îi acoperă. Dauna stă două revizii mai jos, într-o regiune pe care nimeni nu a reverificat-o

De ce un MAC valid de nivel superior nu dovedește că fișierul este intact?

Pentru că un PDF MAC acoperă un prefix, nu un document. Actualizarea incrementală este o parte de primă clasă a formatului: fiecare salvare anexează un corp nou, o secțiune de referințe încrucișate nouă și un trailer nou, în timp ce octeții mai vechi rămân exact unde erau. ISO/TS 32004 călărește pe acel model, astfel încât fiecare revizie poartă propriul dicționar /AuthCode care autentifică fișierul așa cum era în acel moment, iar verificarea doar a celui mai nou lasă fiecare revizie anterioară neexaminată. HotPDF expune de aceea cele două întrebări ca două apeluri, iar diferența dintre ele este tot sensul acestui articol. ValidatePDFMAC răspunde la „este autentică revizia curentă”, umplând o înregistrare THPDFPDFMACValidationInfo; ValidatePDFMACChain răspunde la „este autentică fiecare revizie protejată de MAC din acest fișier”, umplând THPDFPDFMACChainValidationInfo cu un tablou per revizie plus un motiv de eșec lizibil de mașină. Pe fișierul alterat-apoi-re-MAC-uit de mai sus, primul apel returnează True, iar al doilea returnează False contra indexului de revizie 1

var
  Pdf: THotPDF;
  Chain: THPDFPDFMACChainValidationInfo;
  I: Integer;
begin
  Pdf := THotPDF.Create(nil);
  try
    if not Pdf.ValidatePDFMACChain('incoming.pdf', 'user', Chain) then
    begin
      // Eșecul este unul dintre 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;

Fiecare MAC se verifică pe propriul flux prefix, niciodată pe lungimea finală a fișierului

Bug-ul cel mai scump din această zonă este folosirea dimensiunii finale a fișierului ca margine superioară la re-hash-uirea unei revizii mai vechi, ceea ce pliază octeții de coadă în fiecare digest mai puțin cel mai nou și raportează alterare pe un fișier sănătos. HotPDF reconstruiește în schimb, pentru fiecare revizie, un flux delimitat doar în citire care se termină la valoarea proprie startxref a reviziei urmată de %%EOF-ul ei, și hash-uiește doar atât. Localizarea marginii este mai capricioasă decât pare: literalul %%EOF poate apărea în interiorul unui flux de conținut sau al unui șir, astfel încât un candidat este acceptat doar când startxref-ul imediat precedent se parsează la un număr egal cu offsetul de referințe încrucișate al secțiunii validate, fără nimic între ele în afară de spații albe. Revizia absoarbe apoi exact o secvență de sfârșit de linie după marker — un singur CR, un singur LF, sau o pereche CRLF — și nimic mai mult. Regula din urmă mușcă în practică, pentru că un scriitor care emite o linie goală în plus între două revizii a produs octeți care aparțin reviziei următoare, iar înghițirea tuturor spațiilor albe de la coadă în cea anterioară schimbă în tăcere ambele digeste. Enumerarea secțiunilor urmează aceeași disciplină: HotPDF parcurge secțiunile de referințe încrucișate de la cea mai veche la cea mai nouă exact o dată, re-jucând intrările free, direct și object-stream astfel încât secțiunile ulterioare suprascriu starea anterioară, opusul semanticii primul-văzut-câștigă pe care un parser de xref activ o aplică

HotPDF verifică fiecare PDF MAC ISO 32004 contra unui flux prefix care se termină la propriul startxref și markerul de sfârșit de fișier al reviziei, astfel încât un octet întors în interiorul reviziei 1 prăbușește lanțul chiar dacă cel mai nou MAC se validează încă curat
MAC-ul fiecărei revizii este re-hash-uit peste propriul prefix delimitat, astfel încât editarea reviziei 1 și anexarea unei revizii proaspăt MAC-uite satisfac în continuare ValidatePDFMAC, în timp ce ValidatePDFMACChain aterizează pe revizia 1

Unde se ancorează lanțul și ce îl rupe?

Prima revizie care poartă un /AuthCode valid este ancora, iar FirstMACRevisionIndex raportează unde începe protecția; orice înainte de ea este neprotejat prin construcție, ceea ce este normal. Tot ce vine după trebuie să fie protejat de MAC, astfel încât anexarea unei simple actualizări incrementale la un fișier protejat de MAC eșuează cu pmcfRequiredRevisionMissing și indexul reviziei vinovate — tolerarea unui gol ar lăsa un atacator să dezbrace protecția pur și simplu salvând o dată în plus. Trei invariante suplimentare se susțin pe tot lanțul, fiecare cu propriul cod de eșec

  • pmcfKDFSaltChanged/KDFSalt trebuie să rămână stabil de la ancoră înainte, pentru că o sare rotativă ar lăsa un falsificator să re-deriveze chei sub parametri aleși de el
  • pmcfDigestDowngrade — rezistența digestului este comparată contra ultimului MAC verificat mai degrabă decât contra reviziei imediat precedente, astfel încât un lanț care începe sub profilul Modern la SHA-384 nu poate continua în tăcere cu SHA-256
  • pmcfPermissionDowngrade — o revizie nu poate șterge o cerință de PDF MAC pe care o revizie anterioară o autentificase

Consecința care merită interiorizată este că MAC-urile istorice sunt verificate independent chiar și odată ce nu mai sunt trailerul activ. De aceea atacul editează-o-revizie-veche-apoi-anexează-un-MAC-proaspăt de la început nu supraviețuiește: cel mai nou MAC se verifică de sine stătător, ValidatePDFMAC este mulțumit, iar lanțul aterizează tot pe revizia 1 cu pmcfRevisionInvalid

Ordinea semnăturii: chei de trailer primele, signatureDigest ultimul

Când MAC-ul este atașat unei semnături CMS în loc să stea singur, ordinea de scriere nu mai este o chestiune stilistică. HotPDF cere ca /AuthCode, /KDFSalt, extensia de dezvoltator ISO 32004 și /SigObjRef să fie scrise în aceeași revizie înainte să fie calculat /ByteRange-ul semnăturii; le anexați după și acei octeți ajung în afara intervalului pe care semnătura îl acoperă, producând un fișier a cărui semnătură se verifică în timp ce legarea MAC este nesemnată. Cele două digeste rulează apoi în cealaltă direcție, ceea ce pare circular la prima vedere și nu este. signatureDigest-ul PDF MAC leagă octeții de conținut bruți ai OCTET STRING-ului CMS SignerInfo.signature — nu tot DER-ul CMS și nu atributele semnate — deci este construit după ce valoarea brută a semnăturii există și injectat ca un atribut nesemnat id-attr-pdfMacData. Întrucât /Contents este exclus din ByteRange-ul semnăturii, iar atributele nesemnate nu hrănesc niciodată calculul semnăturii, secvența produce-semnătura, construiește-MAC, îmbracă-CMS se închide curat fără buclă criptografică. Două corolare urmează: sentinela /ByteRange și substituentul /Contents trebuie să rămână în clar și în afara fluxurilor de obiecte chiar și într-un fișier criptat, altfel patcherul cu lățime fixă nu le poate găsi; iar când digestul MAC este și el SHA-256, digestul de semnare este reutilizat direct, altfel ambele contexte de digest sunt actualizate într-o singură trecere peste fluxul de ieșire

Ordinea de scriere HotPDF pentru un PDF MAC atașat unei semnături CMS: cheile MAC intră în revizie înainte ca ByteRange să fie măsurat, iar digestul semnăturii este construit apoi din octeții bruți ai semnăturii SignerInfo
Scrierea AuthCode, KDFSalt, SigObjRef și a extensiei de dezvoltator înainte ca ByteRange să fie măsurat este ce ține legarea MAC în interiorul intervalului pe care semnătura îl acoperă
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;      // digest de document 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, iar cele două digeste
        // sunt raportate separat
        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;

Validarea reface același drum de la celălalt capăt: citește /AuthCode-ul direct din trailerul clasic de referințe încrucișate activ în prezent, urmărește /SigObjRef-ul indirect conștient de generație, confirmă că leagă /V-ul unicului câmp de semnătură și raportează un eșec de digest de document separat de un eșec de digest de semnătură. Acestea sunt diagnostice diferite, iar prăbușirea lor într-un singur boolean aruncă singura informație care spune dacă conținutul paginii sau valoarea semnăturii a fost atinsă. Dacă faceți deja lucru cu CMS, asta stă alături de articolul despre semnarea PAdES și ghidul verificării semnăturilor în documentele încărcate

Nu aveți încredere niciodată în /P: decriptați mai întâi /Perms de 16 octeți

ISO/TS 32004 semnalează „acest document cere un PDF MAC” prin bitul de permisiune 13, iar modul evident de a-l citi este cel greșit, pentru că întregul /P din dicționarul de criptare este în clar și neautentic — oricine poate întoarce acel bit într-un editor de text și coborî cerința. ISO 32000-2 §7.6 furnizează răspunsul în intrarea /Perms, iar HotPDF îl folosește: decriptează șirul /Perms de 16 octeți cu cheia de criptare a fișierului sub AES-256 CBC, IV zero, fără padding, apoi verifică fiecare câmp al textului în clar înainte să credeți ceva. Octeții 1 până la 4 țin valoarea de permisiune în ordine little-endian și trebuie să fie egali exact cu întregul /P; octeții 5 până la 8 sunt 0xFF; octetul 9 este indicatorul de criptare a metadatelor T sau F; octeții 10 până la 12 sunt markerul literal adb. Doar când toate acestea se susțin devine PermissionsAuthenticated True și se citește bitul 13 — și aveți grijă la polaritatea lui, pentru că cerința MAC este afirmată când bitul 0x1000 este stins. O nepotrivire între /P și permisiunile decriptate nu este un avertisment de consemnat și trecut cu vederea; este un set de permisiuni falsificat, iar răspunsul corect este să eșuați închis

HotPDF autentifică permisiunile PDF decriptând șirul Perms de șaisprezece octeți cu cheia de criptare a fișierului și verificând valoarea de permisiune little-endian, octeții de umplutură FF, indicatorul de metadate și markerul adb înainte să citească bitul 13
Întregul /P în clar este neautentic, astfel încât cerința PDF MAC este citită doar după ce fiecare câmp al /Perms decriptat a fost verificat

Agilitatea algoritmică se oprește la digest

ISO/TS 32004 vă lasă să alegeți digestul de document, și doar digestul de document. HotPDF păstrează HMAC-SHA-256 pentru autentificare, HKDF-SHA-256 per RFC 5869 pentru derivarea cheilor și AES-256 key wrap per RFC 3394 fixe dedesubtul unui THPDFPDFMACDigestAlgorithm variabil care se întinde de la pmdaSHA256 la pmdaSHA3_512, pentru că greșeala naturală este să tratați un „profil SHA3-512” drept licență de a schimba și HMAC-ul, ceea ce produce un fișier care nu mai este un PDF MAC în vreun sens interoperabil. Un detaliu de implementare merită copiat dacă vă scrieți propriul verificator: citiți OID-ul digestului din AuthenticatedData-ul CMS înainte să hash-uiți intervalul de octeți, deoarece hardcodarea SHA-256 și reconcilierea după aceea transformă agilitatea într-o etichetă și lasă un fișier ostil să vă facă să streamați tot documentul înainte să descoperiți că algoritmul nu a fost niciodată suportat. CMSAlgorithmProtection, algoritmul de digest al AuthenticatedData-ului, messageDigest-ul integrity-info și digestul intervalului de octeți trebuie toți să numească un singur algoritm, iar orice dezacord eșuează închis

var
  Options: THPDFPDFMACOptions;
begin
  Options := THPDFPDFMACOptions.Compatibility;  // SHA-256, acceptă toți cei șase
  Options := THPDFPDFMACOptions.Modern;         // SHA-384, respinge 256-bit
  Options := THPDFPDFMACOptions.HighAssurance;  // doar SHA3-512, AES-GCM

  // Un profil personalizat este legal, dar algoritmul cu care generează
  // trebuie să apară și în lista albă de validare, altfel configurația
  // este respinsă înainte să fie scris un singur octet
  Options.Profile := pmppCustom;
  Options.DigestAlgorithm := pmdaSHA512;
  Options.AllowedDigestAlgorithms := [pmdaSHA512, pmdaSHA3_512];
  Options.RequireAESGCM := True;
end;

Ce dovedește și ce nu dovedește un PDF MAC

Un lanț PDF MAC verificat dovedește că fiecare revizie protejată este identică octet cu octet cu ce a fost scris de cineva care deține cheia de criptare a fișierului, că nicio revizie protejată nu a fost eliminată sau rearanjată, și că nicio revizie neprotejată nu a fost anexată după ancoră — exact clasa de atac pe care criptarea AES-256 simplă o lasă deschisă, deoarece confidențialitatea nu spune nimic despre integritate, iar un PDF criptat cu o revizie lipită se decriptează la fel de bucuros ca unul intact. Ceea ce nu dovedește este autoratul. Cheia MAC derivă din cheia de criptare a fișierului, astfel încât oricine poate deschide documentul poate produce și un MAC valid peste o versiune modificată, fiecare destinatar legitim inclus; este o primitivă simetrică, iar primitivele simetrice nu pot atribui. Dacă trebuie să știți cine a schimbat ceva, aveți nevoie de o semnătură digitală cu un certificat în spate, iar PDF MAC-ul o completează apoi protejând structura incrementală pe care semnătura singură nu o acoperă. Tratați-le ca straturi și lăsați cele două verdicte să fie raportate independent în loc să fie prăbușite într-o singură iconiță de stare

Punctele de intrare PDF MAC descrise aici — AddStandalonePDFMAC, SignPDFWithPFXAndAttachedPDFMAC, ValidatePDFMAC și ValidatePDFMACChain — vin cu HotPDF Delphi Component standard pentru Delphi și C++Builder, unde pagina de produs poartă referința completă pentru înregistrarea de opțiuni, enumerările de stare și tabloul de validare per revizie