Techninis straipsnis

PDF parašų bei PAdES lygių tikrinimas Delphi aplinkoje

Gavote pasirašytą PDF failą ir peržiūros programoje turite parodyti, kas jį pasirašė, kada jis buvo pasirašytas, ar parašas apima visą failą bei kiek jis atitinka ilgalaikio saugojimo reikalavimus. PDFium komponentas, skirtas Delphi bei Lazarus, atsako į visus keturis klausimus tik skaitomais iškvietimais: FPDF_GetSignatureCount bei FPDFSignatureObj_* šeima atveria parašo žodyną, o TPdf.ValidatePades klasifikuoja PAdES bazinį lygį. Tai pirmasis iš trijų straipsnių apie PDF parašus naudojant PDFium; kiti du aprašys B-B parašo kūrimą bei ilgalaikių laiko žymų pridėjimą. Tačiau viena riba yra aiški iš pat pradžių: čia atliekamas tik parašo struktūros tikrinimas, o nuskaityti tai, ką deklaruoja parašas, yra kitas darbas, nei patikrinti jo kriptografiją ar nuspręsti, ar pasitikite pasirašiusiuoju

Tai yra svarbu, nes vietinė DLL gali sugesti trimis skirtingais būdais, o neapdorotas operacinės sistemos tekstas juos visus suplaka į vieną. Architektūra gali būti neteisinga, įdiegtas dvejetainis failas gali būti per senas jį iškviečiančiam Pascal susiejimui, arba dvi gijos gali konkuruoti dėl jo susiejimo tuo pačiu metu. Kiekvienas iš šių atvejų turi skirtingą sprendimą, o diagnostikos, įdiegtos v2.11.0 įkėlimo gyvavimo ciklo darbuose, tikslas yra parodyti, į kurį iš jų iš tikrųjų žiūrite

Kodėl PDF parašo žodynas nėra tik paprastas baitų rinkinys

PDF parašas yra žodynas, o ne nepermatomas priedas, ir du svarbiausi jo įrašai praneša, kokia failo dalis iš tikrųjų yra apsaugota. ISO 32000-1 §12.8 aprinia parašo žodyną su /ByteRange bei /Contents įrašais. /Contents saugo šešioliktainiu formatu užkoduotą CMS SignedData struktūrą (RFC 5652) — kriptografinį voką, pernešantį pasirašiusiojo sertifikatą, pasirašytus atributus bei pačią parašo reikšmę. /ByteRange yra dalis, kurią programuotojai dažnai nuvertina: tai dviejų poslinkio ir ilgio intervalų masyvas, kuris kartu apima visą failą, išskyrus /Contents šešioliktainę eilutę. Šis tarpas yra būtent ta vieta, kurioje randasi parašo baitai, o du intervalai iš abiejų pusių yra būtent tai, ką patvirtina parašas

ByteRange struktūra leidžia audituoti prieaugio išsaugojimą. Kadangi pasirašantysis negali sukurti parašo baitų, kurių dar nėra, maišos skaičiavimui failas yra padalijamas aplink /Contents vietos rezervuarą, o visa kita yra įtraukiama į parašą. Parašas, kurio ByteRange nepasiekia failo pabaigos, yra įspėjamasis ženklas: turinys, pridėtas po apsaugotos srities vėlesniu prieaugio atnaujinimu, nepažeis parašo, nors ir pakeičia tai, ką mato skaitytojas. Todėl pirmasis dalykas, kurį tikrina rimtas inspektorius, yra ne tai, kas pasirašė, bet ar parašas apima baitus, kuriuos jis tariamai patvirtina

Parašo žodyno nuskaitymas naudojant PDFium tik skaitomą API

PDFium komponentas atveria parašo žodyną per dvi tik skaitomas savybes: SignatureCount bei Signature[Index] įrašą. Viduje jie iškviečia FPDF_GetSignatureCount, FPDF_GetSignatureObject bei FPDFSignatureObj_* funkcijas, skirtas gauti /SubFilter, /ByteRange, /Contents, /Reason bei pasirašymo laikui. „Tik skaitomas“ čia yra pagrindinė frazė: PDFium gali enumerate bei skaityti parašus, tačiau neteri API parašui kurti ar rašyti; būtent todėl pasirašymo dalį šiame cikle atlieka pati biblioteka, o ne 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;

Kiekvienas TPdfSignature įrašas tiesiogiai atitinka žodyną. Encoding yra /SubFilter — svarbiausias diagnostinis laukas, nes jis įvardija parašo apdorojimo programą bei iškart atskiria šiuolaikinį ETSI.CAdES.detached parašą nuo pasenusio ar draudžiamo. Time is the author-declared signing time as a PDF date string, which is a claim by the signer and not trusted time. Content yra neapdoroti CMS SignedData duomenys, o Permission atveria DocMDP sertifikavimo lygį (0 įprastam patvirtinimo parašui, nuo 1 iki 3 — sertifikavimo parašui, kuris blokuoja vėlesnius pakeitimus). Vienintelis laukas, kurio įrašas nepateikia, yra išanalizuotas ByteRange; šis praleidimas yra sąmoningas, nes ValidatePades atlieka ByteRange srities skaičiavimus už jus, nversdama to daryti rankiniu būdu

Kuo skiriasi PAdES B-B, B-T, B-LT ir B-LTA?

Keturi PAdES baziniai lygiai sudaro laiptus nuo minimaliai galiojančio parašo iki parašo, skirto išlaikyti dešimtmečius archyvuose; kiekvienas aukštesnis lygis griežtai apima žemesnįjį. ETSI EN 319 142-1 juos apibrėžia kaip B-B, B-T, B-LT bei B-LTA. B-B (Basic) yra parašas bei privalomi pasirašyti atributai ir nieko daugiau. B-T (Timestamp) prideda patikimą RFC 3161 laiko žymą virš parašo, todėl pasirašymo momentą patvirtina laiko žymėjimo institucija, o ne pasirašiusiojo laikrodis. B-LT (Long-Term) įterpia patvirtinimo duomenis — sertifikatų grandinę bei pasirinktinai OCSP arba CRL atsakymus — failo viduje, kad parašą būtų galima patikrinti po daugelio metų, kai išdavimo infrastruktūra bus išjungta. B-LTA (Long-Term with Archive timestamp) apgaubia šiuos duomenis dokumento laiko žyma, apsaugodama pačius ilgalaikius duomenis bei suteikdama galimybę iš naujo uždėti laiko žymą prieš pasenstant pačiai kriptografijai

Praktinis vertinimas priklauso nuo laiko horizonto. B-B parašas atsako į klausimą „ar kas nors tai pasirašė“. B-T atsako „ir kada, įrodomai“. B-LT atsako „ir ar vis tiek galėsiu jį patikrinti pasibaigus sertifikatų galiojimui“. B-LTA atsako „ir ar šis patikrinimas vis dar galios po dvidešimties metų“. Teisiniai reikalavimai pasirenka savo pakopą: daugelis e. sąskaitų faktūrų bei eIDAS kontekstų reikalauja bent B-T lygio, o archyvavimo reikalavimai siekia B-LT arba B-LTA. Žinojimas, kurią pakopą dokumentas iš tikrųjų pasiekia prieš jį priimant ar atmetant, yra visa patikrinimo žingsnio esmė

Bazinio lygio nustatymas su TPdf.ValidatePades

PDFium komponentas sumažina visą lygio nustatymo klausimą iki vieno iškvietimo. TPdf.ValidatePades grąžina TPadesValidationResult įrašą, kurio Level laukas yra TPadesLevel (plNone, plUnknown, plB_B, plB_T, plB_LT arba plB_LTA), kartu su klaidų rinkiniu, parašų skaičiumi bei dokumentų laiko žymų skaičiumi. Lygis nustatomas nuosekliai: tikrintuvas pirmiausia nustato B-B, tada perkelia į B-T, jei yra parašo arba dokumento laiko žyma, į B-LT, jei kataloge yra /DSS su sertifikatais bei /Extensions /ESIC 1 lygio žymeklis, ir į B-LTA, jei yra tiek dokumento laiko žyma, tiek ESIC 2 lygio žymeklis. Du pagalbiniai metodai leidžia panaudoti rezultatą: IsCompliant yra True tik tada, kai lygis pasiekia bent B-B, o klaidų rinkinys yra tuščias, ir IsCompliantAt leidžia reikalauti tam tikro lygio, pavyzdžiui, 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;

Kodėl adbe.pkcs7.sha1 yra draudžiamas SubFilter?

Todėl, kad SHA-1 yra nesaugus ir adbe.pkcs7.sha1 apdorojimo programa jį naudoja tiesiogiai. Šis SubFilter iš anksto sukuria dokumento maišą su SHA-1 prieš apgaubdamas jį PKCS#7 struktūra, o SHA-1 jau daugelį metų yra pažeidžiamas dėl susidūrimų, todėl EN 319 142-1 standarto 6.3 punktas jį visiškai draudžia baziniam parašui. ValidatePades iškelia ppeiForbiddenSubFilter, kai mato adbe.pkcs7.sha1 arba adbe.x509.rsa_sha1, bei iškelia ppeiBadDigestAlgorithm, kai pats CMS naudoja MD5 arba SHA-1 kaip pranešimo maišą. Tai du atskiri patikrinimai, fiksuojantys tą pačią silpnybę dviejuose skirtinguose sluoksniuose

Klaidų rinkinys iš viso turi 26 narius, o tie, kuriuos sutiksite dažniausiai, yra susiję su struktūra bei sritimi. ppeiByteRangeNotCoveringFile yra anksčiau aprašyta srities patikra. ppeiForbiddenCertKey suveikia, kai parašo žodyne yra /Cert įrašas, kurį PAdES draudžia, nes grandinė turi būti CMS SignedData.certificates viduje. ppeiMissingSigningCertificate, ppeiMissingContentType bei ppeiMissingMessageDigest pažymi privalomų pasirašytų atributų trūkumą, o ppeiDetachedContentViolation užfiksuoja parašą, kuris klaidingai įterpia pasirašytą turinį, užuot jį atskyręs. Šio rinkinio išvardijimas paverčia paprastą atmetimą diagnoze, kurią galite įrašyti į žurnalą

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;

Ko ValidatePades netikrina

ValidatePades tikrina struktūrą, o ne pasitikėjimą, ir jų sumaišymas yra pavojinga klaida. Rezultatas plB_LTA reiškia, kad dokumente yra teisingai suformuotas B-LTA parašas su visais reikalingais atributais, duomenimis bei laiko žymomis reikiamose vietose; tačiau tai nereiškia, kad parašas yra kriptografiškai teisingas, kad sertifikatas vedamas iki jūsų pasitikimo šakninio sertifikato arba kad joks grandinės sertifikatas nebuvo atšauktas. Tikrintuvas sąmoningai neatlieka kriptografinio tikrinimo: jis neperskaičiuoja parašo pagal ByteRange sritį, nekuria bei nevertina pasitikėjimo grandinės bei netikrina OCSP ar CRL atšaukimo būsenos. Šis padalijimas yra tikslingas bei naudingas, nes struktūrinis patikrinimas yra greitas, visiškai determinuotas bei nereikalauja nei raktų, nei tinklo, nei platformos kriptografijos, todėl ValidatePades veikia vienodai Windows, Linux bei macOS sistemose kaip grynas Pascal kodas virš failo baitų. Priešingai, pasitikėjimo grandinės tikrinimas yra neatsiejamas nuo politikos — kokiomis šaknimis pasitikite, jak gaunate atšaukimo duomenis, koks yra jūsų laiko žymų tolerancijos griežtumas — ir priklauso vėlesniam etapui, kuris priklauso nuo platformos sertifikatų saugyklos; todėl sėkmingą ValidatePades patikrą vertinkite kaip būtiną žingsnį, patvirtinantį, kad parašas suformuotas teisingai, o tada struktūriškai teisingą parašą perduokite tikram kriptografiniam tikrinimui prieš pradėdami juo pasikliauti

Šis struktūrinis tikrinimas yra tinkama vieta pradėti, ir jis natūraliai derinamas su platesniais tik skaitomais patikrinimais straipsniuose PDF saugumo rizikų auditas su PDFium komponentu bei su formato atitikties darbais, tokiais kaip spaudai paruoštų PDF/X dokumentų tikrinimas. Kai jau mokate skaityti bei klasifikuoti parašą, kitas žingsnis yra jo sukūrimas: antrasis šios serijos straipsnis aprašo PDF pasirašymą su PAdES B-B, o trečiasis išplečia tai iki patikimų laiko žymų bei ilgalaikių B-LT bei B-LTA parašų. Čia parodytas tik skaitomas parašų patikrinimas bei ValidatePades klasifikatorius platinami kaip PDFium komponento, skirto Delphi, C++Builder bei Lazarus, dalis