Tehnički članak

Provjera PDF potpisa i PAdES razina u Delphiju

Primili ste potpisani PDF i trebate u svom pregledniku prikazati tko ga je potpisao, kada je potpisan, pokriva li potpis cijelu datoteku i koliko je usklađen na dugi rok. Komponenta PDFium Component za Delphi i Lazarus odgovara na sva četiri pitanja pomoću poziva samo za čitanje: FPDF_GetSignatureCount i obitelj FPDFSignatureObj_* izlažu rječnik potpisa, a TPdf.ValidatePades klasificira osnovnu razinu PAdES-a. Ovo je prvi od tri članka o PDF potpisima pomoću PDFiuma; dva koja slijede pokrivaju stvaranje B-B potpisa i dodavanje dugoročnih vremenskih žigova. Međutim, jednu granicu treba postaviti odmah na početku: sve ovdje opisano je provjera, a čitanje onoga što potpis tvrdi sasvim je drugačiji zadatak od provjere njegove kriptografije ili donošenja odluke vjerujete li potpisniku

Zašto rječnik PDF potpisa nije samo hrpa bajtova

PDF potpis je rječnik (dictionary), a ne neprozirni privitak, a njegova dva najvažnija unosa govore vam koliki je dio datoteke zapravo zaštićen. ISO 32000-1 §12.8 definira rječnik potpisa s unosima /ByteRange i /Contents. Unos /Contents sadrži heksadecimalno kodiranu strukturu CMS SignedData (RFC 5652), kriptografsku omotnicu koja prenosi certifikat potpisnika, potpisane atribute i samu vrijednost potpisa. /ByteRange je dio koji razvojni programeri često podcjenjuju: to je niz od dva raspona pomaka i duljine koji zajedno pokrivaju cijelu datoteku osim heksadecimalnog niza /Contents. Taj je jaz točno mjesto gdje se nalaze bajtovi potpisa, a dva raspona sa svake strane su točno ono na što se potpis obvezuje

Dizajn ByteRange-a je ono što inkrementalno spremanje čini podložnim reviziji. Budući da potpisnik ne može izračunati hash bajtova potpisa koji još ne postoje, datoteka se dijeli oko rezerviranog mjesta /Contents, a sve ostalo se sažima (hash) u potpis. Potpis čiji ByteRange ne doseže kraj datoteke znak je upozorenja: sadržaj dodan nakon pokrivenog raspona, kroz kasnije inkrementalno ažuriranje, neće prekinuti potpis iako mijenja ono što čitatelj vidi. Stoga prva stvar koju ozbiljan provjeritelj kontrolira nije tko je potpisao, već pokriva li potpis bajtove koje naizgled podržava

Čitanje rječnika potpisa pomoću PDFium API-ja samo za čitanje

PDFium komponenta prikazuje rječnik potpisa kroz dva člana samo za čitanje: SignatureCount i zapis Signature[Index]. U pozadini oni pozivaju FPDF_GetSignatureCount, FPDF_GetSignatureObject i pristupnike FPDFSignatureObj_* za /SubFilter, /ByteRange, /Contents, /Reason i vrijeme potpisivanja. "Samo za čitanje" je ovdje ključan pojam: PDFium može nabrojati i čitati potpise, ali nema API za stvaranje ili pisanje istih, zbog čega je strana potpisivanja u ovoj seriji članaka implementirana od strane same knjižnice, a ne PDFiuma

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;

Koja je razlika između PAdES razina B-B, B-T, B-LT i B-LTA?

Četiri osnovne PAdES razine čine ljestvicu od minimalno valjanog potpisa do onoga izgrađenog da preživi desetljeća arhiviranja, pri čemu svaka razina u potpunosti uključuje onu ispod sebe. ETSI EN 319 142-1 definira ih kao B-B, B-T, B-LT i B-LTA. B-B (Basic) is potpis plus obavezni potpisani atributi i ništa više. B-T (Timestamp) dodaje pouzdani vremenski žig RFC 3161 preko potpisa, tako da trenutak potpisivanja potvrđuje tijelo za vremensko žigosanje, umjesto da se oslanja na sat potpisnika. B-LT (Long-Term) ugrađuje materijal za provjeru — lanac certifikata, i opcionalno OCSP ili CRL odgovore — unutar datoteke, tako da se potpis i dalje može provjeriti godinama kasnije kada izdavateljska infrastruktura više ne postoji. B-LTA (Long-Term s arhivskim vremenskim žigom) omotava vremenski žig dokumenta oko tog materijala, štiteći same dugoročne podatke i dajući vam točku za ponovno vremensko žigosanje prije nego što temeljna kriptografija zastari

Praktično tumačenje odnosi se na vremenski horizont. B-B potpis odgovara na pitanje "je li netko ovo potpisao". B-T odgovara na "i kada, dokazivo". B-LT odgovara na "i mogu li to još uvijek provjeriti nakon što certifikati isteknu". B-LTA odgovara na "i hoće li ta provjera i dalje vrijediti za dvadeset godina". Regulatorni profili biraju razinu: mnogi konteksti e-fakturiranja i eIDAS-a zahtijevaju najmanje B-T, a arhivski mandati posežu za B-LT ili B-LTA. Znati koju razinu dokument stvarno doseže, prije nego što ga prihvatite ili odbijete, cijeli je smisao koraka provjere

Otkrivanje osnovne razine pomoću TPdf.ValidatePades

PDFium komponenta svodi cijelo pitanje razine na jedan poziv. TPdf.ValidatePades vraća zapis TPadesValidationResult čije je polje Level tipa TPadesLevelplNone, plUnknown, plB_B, plB_T, plB_LT ili plB_LTA — zajedno sa skupom problema (issues), brojem potpisa i brojem vremenskih žigova dokumenta. Razina se zaključuje monotono: provjeritelj najprije utvrđuje B-B, zatim prelazi na B-T ako je prisutan vremenski žig potpisa ili vremenski žig dokumenta, na B-LT ako katalog sadrži /DSS s certifikatima i marker /Extensions /ESIC razine 1, te na B-LTA ako su prisutni i vremenski žig dokumenta i marker ESIC razine 2. Dva pomoćnika čine rezultat primjenjivim: IsCompliant je istinit (True) samo kada razina dosegne barem B-B, a skup problema je prazan, dok vam IsCompliantAt omogućuje definiranje minimalne razine pravila, kao što je 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;

Zašto je adbe.pkcs7.sha1 zabranjen SubFilter?

Zato što je SHA-1 nesiguran (broken) i rukovatelj adbe.pkcs7.sha1 ga sadrži ugrađenog. Taj SubFilter unaprijed sažima dokument pomoću SHA-1 prije umotavanja u PKCS#7, a SHA-1 je godinama osjetljiv na kolizije, pa ga EN 319 142-1 članak 6.3 izričito zabranjuje za osnovni potpis. ValidatePades podiže problem ppeiForbiddenSubFilter kada vidi adbe.pkcs7.sha1 ili adbe.x509.rsa_sha1, te podiže ppeiBadDigestAlgorithm kada sam CMS koristi MD5 ili SHA-1 kao sažetak poruke (članak 6.2.1). To su dvije različite provjere koje hvataju istu klasu slabosti na dva različita sloja

Skup problema ima ukupno 26 članova, a oni s kojima ćete se najčešće susresti vrte se oko strukture i pokrivenosti. ppeiByteRangeNotCoveringFile je provjera pokrivenosti opisana ranije. ppeiForbiddenCertKey se aktivira kada rječnik potpisa sadrži unos /Cert, što PAdES zabranjuje jer lanac mora živjeti unutar CMS-a SignedData.certificates. ppeiMissingSigningCertificate, ppeiMissingContentType i ppeiMissingMessageDigest označavaju obavezne potpisane atribute koji nedostaju, a ppeiDetachedContentViolation hvata potpis koji pogrešno ugrađuje potpisani sadržaj umjesto da ga odvoji. Nabrajanje ovog skupa pretvara obično odbijanje u dijagnozu koju možete zapisati u dnevnik rada

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;

Što ValidatePades ne provjerava

ValidatePades provjerava strukturu, a ne povjerenje, i miješanje ta dva pojma opasna je pogreška. Rezultat plB_LTA znači da dokument sadrži ispravno oblikovan B-LTA potpis sa svim propisanim atributima, materijalima i vremenskim žigovima na pravim mjestima — to ne znači da je potpis kriptografski valjan, da se lanac certifikata povezuje s korijenom kojem vjerujete niti da ijedan certifikat u lancu nije opozvan. Provjeritelj namjerno ne provodi kriptografsku provjeru: on ne izračunava ponovno potpis preko ByteRange-a, ne gradi niti procjenjuje lanac povjerenja i ne provjerava status opoziva OCSP ili CRL. Ta je podjela namjerna i korisna, jer je strukturna provjera brza, potpuno deterministička i ne zahtijeva ključeve, mrežu niti kriptografiju platforme, pa ValidatePades radi identično na Windowsima, Linuxu i macOS-u kao čisti Pascal preko bajtova datoteke. Provjera lanca povjerenja, nasuprot tome, neodvojiva je od pravila — kojim korijenima vjerujete, kako dohvaćate opozive, koliko je stroga vaša tolerancija na vremenske žigove — i pripada kasnijoj fazi koja ovisi o spremištu certifikata platforme, stoga tretirajte prolaz kroz ValidatePades kao nužan uvjet da je potpis ispravno oblikovan, a potom strukturno ispravan potpis predajte stvarnoj kriptografskoj provjeri prije nego što se oslonite na njega

Strukturni prolaz je pravo mjesto za početak i prirodno se povezuje s širim provjerama samo za čitanje u članku o reviziji sigurnosnih rizika PDF-a pomoću PDFium komponente te s radom na sukladnosti formata kao što je provjera PDF/X dokumenata spremnih za tisak. Nakon što možete čitati i klasificirati potpis, sljedeći korak je njegovo stvaranje: drugi članak u ovoj seriji pokriva potpisivanje PDF-ova s PAdES B-B, a treći to proširuje na pouzdane vremenske žigove i dugoročne B-LT i B-LTA potpise. Provjera potpisa samo za čitanje i klasifikator ValidatePades prikazani ovdje dio su komponente PDFium Component za Delphi, C++Builder i Lazarus