Technický článok

Kontrola PDF podpisov a úrovní PAdES v Delphi

Dostali ste podpísaný súbor PDF a vo svojom prehliadači potrebujete zobraziť, kto ho podpísal, kedy bol podpísaný, či podpis pokrýva celý súbor a ako ďaleko zachádza z hľadiska dlhodobého súladu s predpismi. Komponent PDFium Component pre Delphi a Lazarus odpovedá na všetky štyri otázky pomocou volaní len na čítanie: FPDF_GetSignatureCount a rodina funkcií FPDFSignatureObj_* odhaľujú slovník podpisu a metóda TPdf.ValidatePades klasifikuje základnú úroveň PAdES. Toto je prvý z troch článkov o PDF podpisoch s PDFium; dva nasledujúce sa venujú vytvoreniu podpisu B-B a pridávaniu dlhodobých časových pečiatok. Hneď na začiatku je však potrebné uviesť jedno ohraničenie: všetko opísané v tomto článku je len kontrola (inšpekcia), a čítanie toho, čo podpis tvrdí, je iná práca ako overovanie jeho kryptografie alebo rozhodovanie o tom, či dôverujete podpisovateľovi

Prečo slovník PDF podpisu nie je len hromada bajtov

PDF podpis je slovník a nie nepriehľadná príloha a jeho dva najdôležitejšie záznamy vám hovoria, aká časť súboru je skutočne chránená. ISO 32000-1 §12.8 definuje slovník podpisu so záznamami /ByteRange a /Contents. Záznam /Contents obsahuje hexadecimálne kódovanú štruktúru CMS SignedData (RFC 5652), kryptografickú obálku, ktorá nesie certifikát podpisovateľa, podpísané atribúty a samotnú hodnotu podpisu. /ByteRange je časť, ktorú vývojári často podceňujú: je to pole dvoch rozsahov počiatočných pozícií a dĺžok (offset-length), ktoré spolu pokrývajú celý súbor s výnimkou hexadecimálneho reťazca /Contents. Táto medzera je presne miesto, kde sedia bajty podpisu, a tieto dva rozsahy po oboch stranách sú presne to, k čomu sa podpis zaväzuje

Dizajn ByteRange je to, čo umožňuje auditovanie prírastkového ukladania. Keďže podpisovateľ nemôže vytvoriť haš z bajtov podpisu, ktoré ešte neexistujú, súbor sa rozdelí okolo zástupného symbolu /Contents a všetko ostatné sa zahašuje do podpisu. Podpis, ktorého rozsah ByteRange nedosahuje na koniec súboru, je varovným signálom: obsah pripojený za pokrytým rozsahom prostredníctvom neskoršej prírastkovej aktualizácie by neporušil podpis, aj keď mení to, čo čitateľ vidí. Prvá vec, ktorú seriózny kontrolór overuje, teda nie je to, kto dokument podpísal, ale či podpis pokrýva tie bajty, ktoré sa zdá schvaľovať

Čítanie slovníka podpisu pomocou API PDFium len na čítanie

Komponent PDFium Component sprístupňuje slovník podpisu prostredníctvom dvoch členov len na čítanie: SignatureCount a záznamu Signature[Index]. Pod kapotou volajú funkcie FPDF_GetSignatureCount, FPDF_GetSignatureObject a prístupové metódy FPDFSignatureObj_* pre /SubFilter, /ByteRange, /Contents, /Reason a čas podpisu. „Len na čítanie“ je tu kľúčovým slovným spojením: PDFium dokáže prechádzať a čítať podpisy, ale nemá žiadne API na ich vytváranie alebo zápis, čo je dôvod, prečo je podpisovacia strana v tejto sérii implementovaná samotnou knižnicou a nie nástrojom PDFium

var
  Pdf: TPdf;
  i: Integer;
  Sig: TPdfSignature;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.FileName := 'contract-signed.pdf';
    Pdf.Active := True;
    for i := 0 to Pdf.SignatureCount - 1 do
    begin
      Sig := Pdf.Signature[i];
      Writeln('SubFilter : ', Sig.Encoding);        // ETSI.CAdES.detached, adbe.pkcs7.detached, ...
      Writeln('Signed at : ', Sig.Time);            // signer date string, e.g. D:20260708120000+02'00'
      Writeln('Reason    : ', Sig.Reason);
      Writeln('CMS length: ', Length(Sig.Content)); // raw DER SignedData taken from /Contents
      Writeln('DocMDP    : ', Sig.Permission);      // 0 = no certification, 1..3 = MDP level
    end;
  finally
    Pdf.Free;
  end;
end;

Každý záznam TPdfSignature sa mapuje priamo na slovník. Encoding je /SubFilter, čo je najvýznamnejšie diagnostické pole, pretože pomenúva obsluhu podpisu a okamžite odlišuje moderný podpis ETSI.CAdES.detached od historických alebo zakázaných. Time je čas podpísania deklarovaný autorom ako reťazec dátumu PDF, čo je tvrdenie podpisovateľa a nie dôveryhodný čas. Content obsahuje surové CMS SignedData a Permission odhaľuje certifikačnú úroveň DocMDP (0 pre bežný podpis schválenia, 1 až 3 pre certifikačný podpis, ktorý uzamkne neskoršie zmeny). Jediné pole, ktoré tento záznam nezobrazuje, je parsovaný ByteRange a toto vynechanie je zámerné, pretože metóda ValidatePades robí matematiku pokrytia ByteRange za vás, namiesto toho, aby ste ju museli počítať ručne

Aký je rozdiel medzi PAdES B-B, B-T, B-LT a B-LTA?

Štyri základné úrovne profilu PAdES tvoria rebríček od minimálne platného podpisu až po podpis navrhnutý tak, aby prežil desaťročia archivácie, pričom každá úroveň striktne obsahuje úroveň pod ňou. Norma ETSI EN 319 142-1 definuje osadenie ako B-B, B-T, B-LT a B-LTA. B-B (Basic) je samotný podpis plus povinné podpísané atribúty a nič viac. B-T (Timestamp) pridáva dôveryhodnú časovú pečiatku RFC 3161 nad podpis, takže moment podpísania dosvedčuje autorita časových pečiatok a netvrdia ho len hodiny podpisovateľa. B-LT (Long-Term) integruje validačné materiály — reťazec certifikátov a voliteľne odpovede OCSP alebo zoznamy odvolaných certifikátov (CRL) — priamo do súboru, takže podpis je možné overiť aj po rokoch, keď už vydávateľská infraštruktúra neexistuje. B-LTA (Long-Term s archívnou časovou pečiatkou) obaľuje tieto materiály časovou pečiatkou dokumentu, čím chráni samotné dlhodobé dáta a poskytuje vám bod na opätovné opečiatkovanie predtým, než podkladová kryptografia zostarne

Praktický výklad je o časovom horizonte. Podpis B-B odpovedá na otázku „podpísal to niekto“. B-T odpovedá „a kedy, preukázateľne“. B-LT odpovedá „a môžem to stále skontrolovať, keď certifikáty vypršia“. B-LTA odpovedá „a bude táto kontrola platiť aj o dvadsať rokov“. Regulačné profily si vyberajú svoju úroveň: mnohé kontexty elektronickej fakturácie a nariadenia eIDAS vyžadujú minimálne úroveň B-T, a archívne mandáty siahajú po B-LT alebo B-LTA. Poznať, ktorú úroveň dokument skutočne dosahuje predtým, než ho prijmete alebo odmietnete, je hlavným zmyslom celého kroku kontroly

Detekcia základnej úrovne pomocou TPdf.ValidatePades

Komponent PDFium Component redukuje celú otázku úrovne na jediné volanie. TPdf.ValidatePades vracia záznam TPadesValidationResult, ktorého pole Level je enum typu TPadesLevelplNone, plUnknown, plB_B, plB_T, plB_LT alebo plB_LTA — spolu so sadou problémov (issues), počtom podpisov a počtom časových pečiatok dokumentu. Úroveň sa odvodzuje monotónne: validátor najprv určí B-B, potom ju povýši na B-T, ak je prítomná časová pečiatka podpisu alebo dokumentu, na B-LT, ak katalóg obsahuje /DSS s certifikátmi a značku úrovne 1 /Extensions /ESIC, a na B-LTA, ak je prítomná časová pečiatka dokumentu a súčasne značka ESIC úrovne 2. Výsledok robia akčným dvaja pomocníci: IsCompliant je True iba vtedy, keď úroveň dosiahne aspoň B-B a sada problémov je prázdna, a IsCompliantAt vám umožňuje vyžadovať minimálnu úroveň politiky, napríklad plB_T

var
  Pdf: TPdf;
  R: TPadesValidationResult;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.FileName := 'contract-signed.pdf';
    Pdf.Active := True;
    R := Pdf.ValidatePades;
    case R.Level of
      plNone:    Writeln('No PAdES signature present');
      plUnknown: Writeln('Signature present but level undeterminable');
      plB_B:     Writeln('PAdES B-B   (basic)');
      plB_T:     Writeln('PAdES B-T   (trusted timestamp)');
      plB_LT:    Writeln('PAdES B-LT  (long-term material embedded)');
      plB_LTA:   Writeln('PAdES B-LTA (archive timestamp)');
    end;
    Writeln('Signatures   : ', R.SignatureCount);
    Writeln('DocTimeStamps: ', R.DocTimeStampCount);
    if R.IsCompliantAt(plB_T) then
      Writeln('Meets the B-T policy floor')
    else
      Writeln('Below the required B-T level');
  finally
    Pdf.Free;
  end;
end;

Prečo je adbe.pkcs7.sha1 zakázaným SubFilterom?

Pretože algoritmus SHA-1 je prelomený a obsluha adbe.pkcs7.sha1 ho natvrdo obsahuje. Tento SubFilter predbežne hašuje dokument pomocou SHA-1 pred jeho zabalením do PKCS#7. Keďže SHA-1 je už roky zraniteľný voči kolíziám, norma EN 319 142-1 článok 6.3 ho pre základný podpis úplne zakazuje. ValidatePades vyvolá ppeiForbiddenSubFilter, keď narazí na adbe.pkcs7.sha1 alebo adbe.x509.rsa_sha1, and it raises ppeiBadDigestAlgorithm when the CMS itself uses MD5 or SHA-1 as the message digest (clause 6.2.1). Ide o dve odlišné kontroly zachytávajúce rovnakú triedu slabín na dvoch rôznych vrstvách

Sada problémov má celkovo 26 členov a tie, s ktorými sa stretnete najčastejšie, sa točia okolo štruktúry a pokrytia. ppeiByteRangeNotCoveringFile je kontrola pokrytia popísaná skôr. ppeiForbiddenCertKey sa aktivuje, keď slovník podpisu obsahuje položku /Cert, čo PAdES zakazuje, pretože reťazec musí namiesto toho žiť vo vnútri SignedData.certificates v CMS. ppeiMissingSigningCertificate, ppeiMissingContentType a ppeiMissingMessageDigest označujú chýbajúce povinné podpísané atribúty a ppeiDetachedContentViolation zachytáva podpis, ktorý nesprávne vkladá podpísaný obsah namiesto jeho odpojenia. Prechádzanie tejto sady premení strohé odmietnutie na diagnózu, ktorú môžete zapísať do logu

var
  R: TPadesValidationResult;
  Issue: TPadesValidationIssue;
begin
  R := Pdf.ValidatePades;
  if R.Issues <> [] then
    for Issue := Low(TPadesValidationIssue) to High(TPadesValidationIssue) do
      if Issue in R.Issues then
        Writeln('Issue: ',
          GetEnumName(TypeInfo(TPadesValidationIssue), Ord(Issue)));
end;

Čo ValidatePades neoveruje

ValidatePades overuje štruktúru, nie dôveru, a ich vzájomná zámena je nebezpečným omylom. Výsledok plB_LTA znamená, že dokument obsahuje správne formátovaný podpis B-LTA so všetkými predpísanými atribútmi, materiálmi a časovými pečiatkami na správnych miestach — neznamená to však, že podpis je kryptograficky platný, že certifikát sa spája s koreňom, ktorému dôverujete, alebo že žiadny certifikát v reťazci nebol odvolaný. Validátor zámerne nevykonáva žiadne kryptografické overovanie: neprepočítava podpis nad ByteRange, nezostavuje ani nevyhodnocuje reťazec dôvery a nekontroluje stav odvolania certifikátov cez OCSP alebo CRL. Toto rozdelenie je úmyselné a užitočné, pretože štrukturálna inšpekcia je rýchla, plne deterministická a nevyžaduje žiadne kľúče, sieťové pripojenie ani kryptografiu platformy, takže ValidatePades beží identicky na systémoch Windows, Linux a macOS ako čistý Pascal nad bajtmi súboru. Validácia reťazca dôvery je naproti tomu neoddeliteľná od politiky — ktorým koreňom dôverujete, ako získavate zoznamy odvolaných certifikátov, aká prísna je vaša tolerancia časových pečiatok — a patrí do neskoršej fázy, ktorá závisí od úložiska certifikátov platformy, takže úspešné prebehnutie ValidatePades považujte za nevyhnutnú bránu potvrdzujúcu, že podpis má správny tvar, a až potom odovzdajte štrukturálne správny podpis na skutočné kryptografické overenie predtým, než sa naň spoľahnete

Tento štrukturálny prechod je správnym miestom, kde začať, a prirodzene sa spája s rozsiahlejšími kontrolami len na čítanie v článku o audite bezpečnostných rizík PDF s komponentom PDFium Component a s prácou na overovaní zhody formátov, ako napríklad overovanie PDF/X dokumentov pripravených na tlač. Keď už dokážete podpis prečítať a klasifikovať, ďalším krokom je jeho vytvorenie: druhý článok v tejto sérii sa venuje podpisovaniu PDF pomocou PAdES B-B a tretí to rozširuje na dôveryhodné časové pečiatky a dlhodobé podpisy B-LT a B-LTA. Inšpekcia podpisu len na čítanie a klasifikátor ValidatePades zobrazené v tomto článku sú súčasťou komponentu PDFium Component pre Delphi, C++Builder a Lazarus