Tehnični članak

Analiza sprememb PDF po podpisu s PDFium v Delphiju

Da bi izvedeli, kaj se je v PDF spremenilo, potem ko je bil podpisan, komponenta PDFium Component za Delphi in Lazarus ponuja TPdf.AnalyzeSignatureRevisions, analizator sprememb revizij po podpisu, ki iz izvirnih bajtov datoteke znova zgradi vsako inkrementalno revizijo, vsako poznejšo spremembo objekta oceni proti pravilom DocMDP in FieldMDP tega podpisa in definicije senc objektov poroča kot ločeno tveganje. Situacija, na katero cilja, je znana vsakomur, ki ravna s pogodbami: overjena obrazca gre ven, vrne se z dvema dodatnima inkrementalnima shranjevanjema in vsak podpis se še vedno potrdi. To je pričakovano, ker podpis pokriva le bajte svoje revizije. Pravo vprašanje je, ali so bila ta poznejša shranjevanja dovoljena, zeleno kljukica na podpisu pa tega ne odgovori

Zakaj API za podpise PDFium ne more pokazati, kaj se je spremenilo po podpisu?

API za podpise PDFium ne more pokazati sprememb po podpisu, ker bere le slovar podpisa: /Contents, /ByteRange, /SubFilter in vrednost dovoljenj DocMDP. PDFium nima grafa inkrementalnih revizij, ne razčlenjuje parametrov transformacije FieldMDP in ne ponuja objektne primerjave med revizijami, zato analizator v FPdfPades.pas dela neposredno na surovih bajtih. To ima praktično posledico, okoli katere se zasnujte. TPdf.AnalyzeSignatureRevisions bere bajte, shranjene ob nalaganju dokumenta, nikoli pa kopije, ustvarjene s SaveAs, ker je prepisana datoteka izgubila prav tisto strukturo revizij, ki se analizira. Če je dokument prišel iz napredovalnega vira, ki ni končal z nalaganjem, poročilo vrne SourceStatus = pvssIncomplete in Status = prasIndeterminate, namesto da bi analiziral odsekano datoteko

Znovogradnja meja revizij iz startxref, tokov xref in /Prev

Analizator meje revizij znova zgradi tako, da sledi vsakemu startxref nazaj skozi klasične tabele xref, tokove navzkrižnih sklicev, vnose /XRefStm hibridnega sklicevanja in verigo /Prev, kot je definirano za inkrementalne posodobitve v ISO 32000-1 §7.5.6 in §7.5.8. Pokrita dolžina vsakega podpisa je konec njegovega drugega razpona ByteRange, analizator pa to dolžino preslika na revizijo, v katerega razdelek xref pade. Ko se ne ujema nobena revizija, podpis dobi prrCoveredRevisionNotFound in status Indeterminate. Stanje vsakega objekta se nato ponovno predvaja do pokrite revizije in vsak poznejši vnos xref se primerja s tem stanjem. To šteje več, kot se sliši: nekateri pisci ob vsakem inkrementalnem shranjevanju ponovno zapišejo celotno tabelo xref, vnos, ki še vedno kaže na isti nespremenjen objekt, pa se preskoči, namesto da bi bil prijavljen kot sprememba. Brez te primerjave bi se povsem zakonita izpolnitev obrazca utopila v stotinah lažnih sprememb

Kako AnalyzeSignatureRevisions iz surovih bajtov PDF v Delphiju znova zgradi inkrementalne revizije: ByteRange podpisa se konča znotraj pokrite revizije, veriga Prev xref hodi nazaj skozi vsako poznejše shranjevanje, stanje objektov se ponovno predvaja do pokrite revizije, nespremenjeni ponovljeni vnosi pa se preskočijo, namesto da bi bili prijavljeni kot spremembe
Podpis pokriva le bajte svoje revizije, zato analizator drugi razpon ByteRange preslika na revizijo in vsak poznejši vnos xref oceni proti ponovno predvajanemu stanju objektov

Sence definicij so primer, ki zasluži največ pozornosti. Telo objekta, ki se pojavi znotraj bajtnega razpona poznejše revizije, a ga xref te revizije ne sklicuje, je za običajen pregledovalnik nevidno, vendar je točno tista vrsta staginga, na katero se zanašajo shadow napadi: skrito vsebino se posadi pred podpisom ali za njim in pozneje aktivira s preklopom sklica. AnalyzePadesSignatureRevisionsBytes tak objekt zabeleži kot neavtoritativno spremembo s IsAuthoritative = False, oceni ga prdSuspicious ne glede na raven dovoljenj in doda prrUnreferencedObjectDefinition k nizu tveganj. Dve sorodni tveganji pokrivata druge strukturne trike: prrDuplicateObjectDefinition se sproži, ko en razdelek xref našteje isti objekt več kot enkrat, prrSignatureObjectRedefined pa, ko poznejša revizija prenovo definira obstoječi objekt podpisa

Definicija senc objekta znotraj bajtnega razpona poznejše revizije PDF: telo objekta obstaja, a ga noben vnos xref ne sklicuje, zato ga pregledovalniki nikoli ne pokažejo, AnalyzeSignatureRevisions v PDFium Component pa ga zabeleži kot neavtoritativnega, oceni prdSuspicious in sproži prrUnreferencedObjectDefinition poleg tveganj podvojitve in prenovljene definicije podpisa
Skrito vsebino se posadi pred podpisom ali za njim in aktivira pozneje s preklopom sklica, zato je nesklicano telo ocenjeno kot sumljivo ne glede na raven dovoljenj DocMDP
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;

Kako se DocMDP in FieldMDP uveljavljata za vsak podpis?

DocMDP in FieldMDP sta uveljavljena ločeno za vsak podpis, pri njegovi lastni pokriti reviziji, tako da lahko overitveni podpis in poznejši odobritveni podpis v isti datoteki prideta do različnih sodb o isti spremembi. Vsak poznejši objekt je najprej razvrščen v TPadesRevisionChangeKind iz svojih vnosov /Type, /Subtype in /FT ter iz vloge, ki jo igra v grafih strani, obrazca, pripombe in DSS. Karkoli nosi /JavaScript, /JS, /Launch, /OpenAction, /AA, /RichMedia ali /EmbeddedFile, postane prckActiveContent. Odločitev nato sledi ISO 32000-1 §12.8.2.2: pri P=1 je vse razen podatkov navzkrižnih sklicev in validacijskega materiala nedovoljeno; P=2 dovoljuje izpolnjevanje obrazcev in nadaljnje podpise, a zavrne spremembe pripomb; P=3 dovoljuje tudi pripombe. Vsebina strani, struktura dokumenta, metapodatki, dejavna vsebina in izbrisani objekti so nedovoljeni pod katero koli ravnijo DocMDP in ocenjeni prdSuspicious, kadar podpis sploh ne nosi DocMDP, saj odobritveni podpis formalno nič ne prepoveduje, bralec pa ne vidi več, kaj je bilo podpisano

FieldMDP, iz ISO 32000-1 §12.8.2.4, odločitev o poljih obrazca še zoži. pfmaAll zaklene vsako polje, pfmaInclude zaklene le našteta polja, pfmaExclude pa zaklene vse razen naštetih. Za uporabo Include ali Exclude analizator vsako spremenjeno polje razreši v njegovo popolnoma kvalificirano ime skozi verigo /Parent in ga primerja s seznamom zaklepov po točnem ujemanju, zato naštevajte končna imena polj in ne pričakujte, da ime starša pokrije svoje otroke. Ko se imena ne da razrešiti ali transformacija uporabi dejanje, ki ga razčlenjevalnik ne prepozna, sprememba postane prdIndeterminate in se sproži prrFieldMdpUnresolved. Odločitve na spremembo se nato strnejo z najhujšim na prvem mestu, pri čemer je Suspicious nad Disallowed, Disallowed nad Indeterminate in Indeterminate nad Allowed, tako da en senc objekt prevaga nad poljubnim številom zakonitih posodobitev polj

Cevovod ocenjevanja, ki ga AnalyzeSignatureRevisions uveljavlja na vsaki spremembi po podpisu v Delphiju: TPadesRevisionChangeKind iz vnosov Type in Subtype, odločitev DocMDP pri pokriti reviziji od P=1 do P=3, preizkus zaklepa FieldMDP na popolnoma kvalificiranih imenih polj in strnitev z najhujšim na prvem mestu od prdSuspicious navzdol do prdAllowed
En senc objekt prevaga nad poljubnim številom zakonitih posodobitev polj, ker je Suspicious nad Disallowed, Indeterminate in Allowed, nekatera tveganja pa so zabeležena ob statusu, ne da bi ga znižala

Zakaj se nekatere spremembe vrnejo kot Indeterminate namesto varne?

Spremembe se vrnejo kot Indeterminate vsakič, ko analizator ne more dokazati, da je sprememba dovoljena, ker mora biti v preizkusu podpisov neznano nikoli prijavljeno kot dovoljeno. En pogost primer je namesto tega obravnavan natančno: dolgoročna validacija doda /DSS in prenovo kataloga, kar bi sicer štelo kot strukturna sprememba pod P=1. Analizator odstrani /DSS in /Extensions iz starega in novega kataloga ter primerja preostanek; kadar se nič drugega ne razlikuje, se prenova obravnava kot posodobitev validacijskega materiala in je dovoljena, tako da razširitev B-LT in B-LTA ne pokvarita overitvenega podpisa. Druge vrzeli so namerno ostavljene odprte. Vnosi tipa 2 v toku navzkrižnih sklicev kažejo v stisnjene toke objektov, analizator pa ne razširja tokov objektov znotraj te varnostne meje, zato te spremembe pridejo na plan kot prckCompressedObject s prrCompressedObjectUnresolved, nedovoljene pod P=1 in sicer Indeterminate. Trdna proračuna 1024 revizij, 1.000.000 številk objektov in 2.000.000 prijavljenih sprememb proizvedeta prrResourceLimitExceeded, zlomljena veriga xref pa prrMalformedRevisionChain; oba se končata kot Indeterminate, nikoli kot uspeh

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

function RevisionVerdict(const R: TPadesRevisionAnalysisReport): string;
begin
  // Nekatera tveganja so zabeležena brez spremembe Status, zato jih preizkusite najprej
  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;

Vrstni red v tej pregradi je namernen. prrDuplicateObjectDefinition se doda k nizu tveganj, ne da bi sam znižal Status, transformacija FieldMDP, ki je ni mogoče razčleniti, pa vpliva na status šele, ko se dejansko spremeni polje obrazca, zato pregrada, ki gleda samo na Status, lahko spregleda dokaze, ki jih poročilo že vsebuje. Upoštevajte tudi, česa poročilo ne trdi. TPadesRevisionAnalysisReport ne pove ničesar o tem, ali je podpis CMS kriptografsko veljaven, in ali veriga potrdila podpisovalca vodi do korena, ki mu zaupate. Analiza revizij odgovori na vprašanje, kaj se je zgodilo po podpisu, in pripada poleg strukturne in validacije zaupanja, ne namesto njiju

Zapis izvornih vrednosti in zaklepov MDP ob podpisovanju

Ista pravila se lahko zapišejo ob podpisovanju prek TPadesSignatureFieldOptions, ki je član FieldOptions obeh TPadesSignOptions in TPadesRemoteSignOptions. PDFium zna ustvariti pripomoček (widget), ne zna pa zapisati /SV, /Lock, transformacije FieldMDP ali DocMDP ali kataloga slovarja /Perms, zato lasten inkrementalni pisec PAdES komponente te objekte proizvede znotraj iste posodobitve xref kot podpis. FieldName nastavi korensko ime polja, RequiredSeedValues postane biti /Ff slovarja izvornih vrednosti (seed value), opisanega v ISO 32000-1 §12.7.4.5, Reasons, LegalAttestations in AcceptableCertificates omejijo, kaj si lahko izbere poznejši podpisovalec, LockAction z LockFields zapiše nedirektni /SigFieldLock, CertificationPermission od 1 do 3 pa spremeni podpis v overitvenega. Transformaciji DocMDP in FieldMDP gredo obe v eno tabelo /Reference na vrednosti podpisa, vsaka s /Data, ki kaže na katalog

var
  Options: TPadesSignOptions;
begin
  Options := TPadesSignOptions.Default;
  Options.CertificateThumbprint := 'A1B2C3D4E5F60718293A4B5C6D7E8F9012345678';
  Options.Reason := 'Approved for release';
  Options.FieldOptions.FieldName := 'Certification';
  Options.FieldOptions.CertificationPermission := 2;   // le izpolnjevanje formul in podpisovanje
  Options.FieldOptions.RequiredSeedValues := [psvcSubFilter, psvcDigestMethod];
  Options.FieldOptions.LockAction := pfmaInclude;      // zakleni le ta polja
  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;

Nekaj podrobnosti se zlahka pokvari, če to ročno sestavljate sami. Katalog /Perms /DocMDP se mora sklicevati na slovar vrednosti podpisa in ne na pripombo pripomočka (widget), pisec pa ravno zato obdrži vrednost podpisa kot svoj nedirektni objekt. Obstojeci slovar /Perms lahko že nosi pravice uporabe /UR3, zato ga pisec kopira in vstavi /DocMDP, namesto da bi ga zamenjal, v skladu s slovarjem dovoljenj v ISO 32000-1 §12.8.4. Dokument, ki že nosi /DocMDP, odkloni drug overitveni podpis s EPadesCrypto, enako neskladne možnosti: zaklep Include ali Exclude brez imen polj, zaklep All s seznamom polj, pravno potrdilo na ne-overitvenem podpisu ali pika v korenskem imenu polja. Oddaljeno podpisovanje doda še eno pravilo, ker je potrdilo za podpisovanje neznano, kadar teče PreparePadesRemoteSignature: nastavitev CertificateRequired tam zahteva izrecen seznam AcceptableCertificates, lokalno podpisovanje pa lahko pade nazaj na razrešeno potrdilo podpisovalca

Analiza revizij dopolni orodjarno podpisov, ne da bi zamenjala kateri koli njen del. Začnite z pregledovanjem podpisov PDF in ravni PAdES s komponento PDFium Component, da preberete slovar in osnovno raven, poglejte zakaj validatorji zavrnejo podpise PAdES za strukturne odpovedi, ki pridejo pred vsakim vprašanjem revizij, in vključite sodbo v širšo revizijo varnostnih tveganj PDF poleg preizkusov JavaScript in vgrajenih datotek. TPdf.AnalyzeSignatureRevisions, TPadesSignatureFieldOptions in inkrementalni pisec PAdES, prikazan tukaj, pridejo s PDFium Component za Delphi, C++Builder in Lazarus