Techninis straipsnis

Pakeitimų po parašo analizė PDF su PDFium Delphi

Norėdami sužinoti, kas pasikeitė PDF pasirašius, PDFium Component for Delphi and Lazarus pateikia TPdf.AnalyzeSignatureRevisions — revizijų pokyčių po parašo analizatorių, kuris iš originalių failo baitų atkuria kiekvieną inkrementinę reviziją, kiekvieną vėlesnį objekto pokytį įvertina pagal to parašo DocMDP ir FieldMDP taisykles, o shadow objektų apibrėžimus praneša kaip atskirą riziką. Situacija, į kurią jis taikosi, pažįstama kiekvienam, kas tvarko sutartis: sertifikuota forma išvyksta, grįžta su dar dviem inkrementiniais įrašymais, ir kiekvienas parašas vis tiek patvirtinamas. Tai tikėtina, nes parašas dengia tik savosios revizijos baitus. Tikrasis klausimas — ar tie vėlesni įrašymai buvo leistini, o žalias varnelės ant parašo neatsako į jį

Kodėl PDFium parašų API negali parodyti, kas pasikeitė po pasirašymo?

PDFium parašų API negali parodyti pokyčių po parašo, nes ji skaito tik parašo žodyną: /Contents, /ByteRange, /SubFilter ir DocMDP leidimų reikšmę. PDFium neturi inkrementinių revizijų grafo, neskanuoja FieldMDP transformacijos parametrų ir neteikia objektų lygio diff tarp revizijų, tad FPdfPades.pas analizatorius dirba tiesiai su žaliais baitais. Tai turi praktinę išvadą, aplink kurią verta statyti dizainą. TPdf.AnalyzeSignatureRevisions skaito baitus, išsaugotus įkeliant dokumentą, niekada — SaveAs pagamintą kopiją, nes perrašytame faile dingo pati analizuojama revizijų struktūra. Jei dokumentas atėjo iš progresyvaus šaltinio, kuris nesibaigęs siųstis, ataskaita grąžina SourceStatus = pvssIncomplete ir Status = prasIndeterminate, užuot analizavusi nukirptą failą

Revizijų ribų atkūrimas iš startxref, xref srautų ir /Prev

Analizatorius atkuria revizijų ribas sekdamas kiekvieną startxref atgal per klasikines xref lenteles, kryžminių nuorodų srautus, hibridinių nuorodų /XRefStm įrašus ir /Prev grandinę, kaip apibrėžta inkrementiniams atnaujinimams ISO 32000-1 §7.5.6 ir §7.5.8. Kiekvieno parašo dengiamas ilgis yra jo antrojo ByteRange span pabaiga, ir analizatorius tą ilgį susieja su revizija, kurios xref sekcijoje jis krenta. Kai jokia revizija nesutampa, parašas gauna prrCoveredRevisionNotFound ir Indeterminate būseną. Tada kiekvieno objekto būsena atkuriama iki dengiamosios revizijos, ir kiekvienas vėlesnis xref įrašas lyginamas su ta būsena. Tai svarbiau, nei skamba: kai kurie rašytojai kiekvieno inkrementinio įrašymo metu pakartoja pilną xref lentelę, ir įrašas, vis tiek rodantis į tą patį nepakitęs objektą, praleidžiamas, užuot praneštas kaip modifikacija. Be to palyginimo visiškai teisėta formos užpildymas nuskęstų šimtuose netikrų pokyčių

Kaip AnalyzeSignatureRevisions atkuria inkrementines revizijas iš žalių PDF baitų Delphi: parašo ByteRange baigiasi dengiamosios revizijos viduje, xref Prev grandinė atgal apeina kiekvieną vėlesnį įrašymą, objekto būsena atkuriama iki dengiamosios revizijos, o nepakitę pakartoti įrašai praleidžiami, užuot pranešti kaip pokyčiai
Parašas dengia tik savosios revizijos baitus, tad analizatorius antrąjį ByteRange span susieja su revizija ir kiekvieną vėlesnį xref įrašą vertina pagal atkurtą objekto būseną

Shadow apibrėžimai — tas atvejis, nusipelnantis daugiausia dėmesio. Objekto kūnas, atsirandantis vėlesnės revizijos baitų ruože, bet nė nevedamas tos revizijos xref, įprastai žiūryklei nematomas, tačiau tai būtent tas paruošiamasis darbas, ant kurio stovi shadow atakos: paslėptas turinys pasodinamas prieš pasirašymą ar po jo, o vėliau suaktyvinamas apvertus nuorodą. AnalyzePadesSignatureRevisionsBytes tokį objektą užregistruoja kaip neautoritetingą pokytį su IsAuthoritative = False, įvertina prdSuspicious nepaisydama leidimų lygio ir prideda prrUnreferencedObjectDefinition prie rizikų aibės. Dvi susijusios rizikos dengia kitus struktūrinius triukus: prrDuplicateObjectDefinition suveikia, kai viena xref sekcija tą patį objektą išvardija daugiau nei kartą, o prrSignatureObjectRedefined — kai vėlesnė revizija perapibrėžia esamą parašo objektą

Shadow objekto apibrėžimas vėlesnės PDF revizijos baitų ruože: objekto kūnas egzistuoja, bet jokia xref į jį neveda, tad žiūryklės jo niekada nerodo, o AnalyzeSignatureRevisions PDFium Component jį užregistruoja kaip neautoritetingą, įvertina prdSuspicious ir kelia prrUnreferencedObjectDefinition šalia dubliavimo ir perapibrėžto parašo rizikų
Paslėptas turinys pasodinamas prieš pasirašymą ar po jo, o vėliau suaktyvinamas apvertus nuorodą, todėl nevedamas kūnas įvertinamas įtartinu nepaisant DocMDP leidimų lygio
uses
  SysUtils, TypInfo, PDFium, FPdfPades;

const
  ShadowTag: array[Boolean] of string = ('', ' (shadow)');
var
  Pdf: TPdf;
  Report: TPadesRevisionAnalysisReport;
  i, j: Integer;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.FileName := 'contract-returned.pdf';
    Pdf.Active := True;
    Report := Pdf.AnalyzeSignatureRevisions;
    Writeln('Revisions: ', Report.RevisionCount,
      '  Signatures: ', Report.SignatureCount,
      '  Overall: ', GetEnumName(TypeInfo(TPadesRevisionAnalysisStatus),
        Ord(Report.Status)));
    for i := 0 to High(Report.Signatures) do
      with Report.Signatures[i] do
      begin
        Writeln(Format('Signature %d covers revision %d, %d later, P=%d, FieldMDP=%s',
          [SignatureIndex, CoveredRevisionIndex, LaterRevisionCount,
           DocMdpPermission,
           GetEnumName(TypeInfo(TPadesFieldMdpAction), Ord(FieldMdpAction))]));
        for j := 0 to High(Changes) do
          Writeln(Format('  rev %d  obj %d  %s -> %s%s',
            [Changes[j].RevisionIndex, Changes[j].ObjectNumber,
             GetEnumName(TypeInfo(TPadesRevisionChangeKind), Ord(Changes[j].Kind)),
             GetEnumName(TypeInfo(TPadesRevisionDecision), Ord(Changes[j].Decision)),
             ShadowTag[not Changes[j].IsAuthoritative]]));
      end;
  finally
    Pdf.Free;
  end;
end;

Kaip DocMDP ir FieldMDP vykdomi kiekvienam parašui?

DocMDP ir FieldMDP vykdomi atskirai kiekvienam parašui, to parašo paties dengiamojoje revizijoje, tad sertifikavimo parašas ir vėlesnis patvirtinimo parašas tame pačiame faile gali priimti skirtingus verdiktus dėl to paties pakeitimo. Kiekvienas vėlesnis objektas pirmiausia skirstomas į TPadesRevisionChangeKind pagal savo /Type, /Subtype ir /FT įrašus ir pagal vaidmenį, kurį jis atlieka puslapio, formos, pastabų ir DSS grafe. Bet kas neša /JavaScript, /JS, /Launch, /OpenAction, /AA, /RichMedia ar /EmbeddedFile, tampa prckActiveContent. Sprendimas tada seka ISO 32000-1 §12.8.2.2: su P=1 draudžiama viskas, išskyrus kryžminių nuorodų duomenis ir patvirtinimo medžiagą; P=2 leidžia formos užpildymą ir tolesnius parašus, bet atmeta pastabų pokyčius; P=3 leidžia ir pastabas. Puslapio turinys, dokumento struktūra, metaduomenys, aktyvus turinys ir ištrinti objektai draudžiami bet kuriame DocMDP lygyje, o parašas be DocMDP apskritai tokius įvertina prdSuspicious, nes patvirtinimo parašas formaliai nedraudžia nieko, bet skaitytojas nebemato to, kas buvo pasirašyta

FieldMDP, iš ISO 32000-1 §12.8.2.4, formos laukų sprendimą dar susiaurina. pfmaAll užrakina kiekvieną lauką, pfmaInclude — tik išvardytuosius, o pfmaExclude — viską, išskyrus išvardytuosius. Taikydamas Include arba Exclude, analizatorius kiekvieną pakeistą lauką per /Parent grandinę išsprendžia iki pilnai kvalifikuoto vardo ir tiksliai sugretina su užrakinimo sąrašu, tad išvardykite galinių laukų vardus, o ne tikėkitės, kad tėvo vardas padengs vaikus. Kai vardo išspręsti nepavyksta arba transformacija naudoja veiksmą, kurio parseris neatpažįsta, pokytis tampa prdIndeterminate, o keliamas prrFieldMdpUnresolved. Atskirų pokyčių verdiktai tada susukami blogiausias-pirmas: Suspicious virš Disallowed, Disallowed virš Indeterminate, Indeterminate virš Allowed, tad vienas shadow objektas nusveria bet kiek teisėtų laukų atnaujinimų

Vertinimo konvejeris, kurį AnalyzeSignatureRevisions taiko kiekvienam pokyčiui po parašo Delphi: TPadesRevisionChangeKind iš Type ir Subtype įrašų, DocMDP sprendimas dengiamoje revizijoje nuo P=1 iki P=3, FieldMDP užrakinimo patikra pilnai kvalifikuotiems laukų vardams ir blogiausias-pirmas suvyniojimas nuo prdSuspicious žemyn iki prdAllowed
Vienas shadow objektas nusveria bet kiek teisėtų laukų atnaujinimų, nes Suspicious viršuje prieš Disallowed, Indeterminate ir Allowed, o dalis rizikų registruojamos šalia būsenos jos nenusleidždamos

Kodėl dalis pokyčių grįžta Indeterminate, o ne saugūs?

Pokyčiai grįžta Indeterminate kaskart, kai analizatorius negali įrodyti, jog pokytis leistinas, nes parašo patikroje nežinoma negali būti pranešama kaip leistina. Vienas dažnas atvejis apdorojamas tiksliai: ilgalaikė patikra prideda /DSS ir perrašo katalogą, kas kitaip skaičiuotųsi kaip struktūrinis pokytis su P=1. Analizatorius nulupa /DSS ir /Extensions iš senojo ir naujojo katalogų žodynų ir lygina likusį; kai niekas kitas nesiskiria, perrašymas laikomas patvirtinimo medžiagos atnaujinimu ir leidžiamas, tad B-LT ir B-LTA papildymas nesulaužo sertifikavimo parašo. Kitos spragos paliekamos sąmoningai. 2-ojo tipo įrašai kryžminių nuorodų sraute rodo į suspaustus objektų srautus, o analizatorius objektų srautų šioje saugumo riboje neišskleidžia, tad tie pokyčiai iškyla kaip prckCompressedObject su prrCompressedObjectUnresolved — draudžiami su P=1 ir Indeterminate kitaip. Kietosios ribos: 1024 revizijos, 1 000 000 objekto numerių ir 2 000 000 pranešamų pokyčių duoda prrResourceLimitExceeded, o sulaužyta xref grandinė — prrMalformedRevisionChain; abi baigiasi Indeterminate, niekada — praeitimi

const
  StructuralRisks: TPadesRevisionRisks = [prrMalformedRevisionChain,
    prrDuplicateObjectDefinition, prrUnreferencedObjectDefinition,
    prrSignatureObjectRedefined, prrResourceLimitExceeded];

function RevisionVerdict(const R: TPadesRevisionAnalysisReport): string;
begin
  // Dalis rizikų registruojama nekeičiant Status, tad patikrinkite jas pirmiausia
  if R.Risks * StructuralRisks <> [] then
    Exit('review: structural risk in the revision chain');
  case R.Status of
    prasNoLaterChanges: Result := 'accept: nothing was added after signing';
    prasAllowed:        Result := 'accept: every later change is permitted';
    prasDisallowed:     Result := 'reject: a change violates DocMDP or FieldMDP';
    prasSuspicious:     Result := 'reject: shadow or unconstrained content change';
    prasIndeterminate:  Result := 'review: the analyzer could not decide';
  else
    Result := 'not checked: no signatures or no original bytes';
  end;
end;

Tvarka tame vartuose sąmoninga. prrDuplicateObjectDefinition į rizikų aibę pridedamas pats, prieš darydamas ką nors Status, o FieldMDP transformacija, kurios nepavyksta perskaityti, būseną veikia tik tada, kai formos laukas iš tiesų pasikeičia, tad vartai, žiūrintys vien į Status, gali praleisti įrodymus, kuriuos ataskaita jau turi. Atsiminkite ir tai, ko ataskaita neteigia. TPadesRevisionAnalysisReport nesako nė žodžio apie tai, ar CMS parašas kriptografiškai teisėtas, ar pasirašiusiojo sertifikatas grandinėje siekia jums patikimą šaknį. Revizijų analizė atsako į klausimą, kas nutiko po pasirašymo, ir jai vieta šalia struktūrinės ir pasitikėjimo patikros, o ne vietoj jų

Seed reikšmių ir MDP užrakinimų rašymas pasirašymo metu

Tas pačias taisykles galima sudėlioti rašantis per TPadesSignatureFieldOptions — FieldOptions narį ir TPadesSignOptions, ir TPadesRemoteSignOptions. PDFium sugeba sukurti widget'ą, bet nemoka rašyti /SV, /Lock, FieldMDP ar DocMDP transformacijos ar katalogo /Perms žodyno, tad paties komponento inkrementinis PAdES rašytojas šiuos objektus pagamina tame pačiame xref atnaujinime kaip parašą. FieldName nustato šakninio lauko vardą, RequiredSeedValues tampa seed-value žodyno /Ff bitais, aprašytais ISO 32000-1 §12.7.4.5, Reasons, LegalAttestations ir AcceptableCertificates riboja, ką vėlesnis pasirašantis gali rinktis, LockAction su LockFields rašo netiesioginę /SigFieldLock, o CertificationPermission nuo 1 iki 3 paverčia parašą sertifikavimo parašu. DocMDP ir FieldMDP transformacijos abi patenka į vieną /Reference masyvą ant parašo reikšmės, kiekviena su /Data, rodančia į katalogą

var
  Options: TPadesSignOptions;
begin
  Options := TPadesSignOptions.Default;
  Options.CertificateThumbprint := 'A1B2C3D4E5F60718293A4B5C6D7E8F9012345678';
  Options.Reason := 'Approved for release';
  Options.FieldOptions.FieldName := 'Certification';
  Options.FieldOptions.CertificationPermission := 2;   // tik formos užpildymas ir pasirašymas
  Options.FieldOptions.RequiredSeedValues := [psvcSubFilter, psvcDigestMethod];
  Options.FieldOptions.LockAction := pfmaInclude;      // užrakinti tik šiuos laukus
  SetLength(Options.FieldOptions.LockFields, 2);
  Options.FieldOptions.LockFields[0] := 'Total';
  Options.FieldOptions.LockFields[1] := 'IBAN';
  if not Pdf.SignPades('contract-certified.pdf', Options) then
    Writeln('Signing failed');
end;

Keliais smulkmenomis lengva suklysti, jei tai kraunatės patys. Katalogo /Perms /DocMDP turi rodyti į parašo reikšmės žodyną, o ne į widget pastabą, ir rašytojas todėl laiko parašo reikšmę savo atskiru netiesioginiu objektu. Egzistuojantis /Perms žodynas gali jau turėti /UR3 naudojimo teises, tad rašytojas jį nukopijuoja ir įterpia /DocMDP, užuot pakeitęs, sekdamas leidimų žodyną ISO 32000-1 §12.8.4. Dokumentas, jau nešantis /DocMDP, atsisako antro sertifikavimo parašo su EPadesCrypto, ir nederintos parinktys taip pat: Include arba Exclude užrakinimas be laukų vardų, All užrakinimas su laukų sąrašu, teisinis liudijimas nesertifikavimo parašui arba taškas šakninio lauko varde. Nuotolinis pasirašymas prideda dar vieną taisyklę, nes pasirašančiojo sertifikatas nežinomas, kai vyksta PreparePadesRemoteSignature: CertificateRequired nustatymas ten reikalauja aiškaus AcceptableCertificates sąrašo, o vietinis pasirašymas gali nusileisti iki išspręsto pasirašančiojo sertifikato

Revizijų analizė papildo parašų įrankių dėžę, o nepakeičia nė vienos jos dalies. Pradėkite nuo PDF parašų ir PAdES lygių apžiūros su PDFium Component, kad perskaitytumėte žodyną ir bazinį lygį, pažiūrėkite kodėl valdytojai atmeta PAdES parašus dėl struktūrinių nesėkmių, ateinančių prieš bet kokį revizijų klausimą, o verdiktą susukite į platesnę PDF saugumo rizikų auditą šalia JavaScript ir įdėtų failų patikrų. TPdf.AnalyzeSignatureRevisions, TPadesSignatureFieldOptions ir čia parodytas inkrementinis PAdES rašytojas atkeliauja su PDFium Component Delphi, C++Builder ir Lazarus