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ų
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ą
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ų
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