Articol tehnic

Analiza schimbărilor PDF după semnare cu PDFium în Delphi

Ca să aflați ce s-a schimbat într-un PDF după ce a fost semnat, PDFium Component pentru Delphi și Lazarus furnizează TPdf.AnalyzeSignatureRevisions, un analizator de schimbări de revizuire după semnare care reconstruiește fiecare revizuire incrementală din byte-ii originali ai fișierului, evaluează fiecare schimbare de obiect ulterioară față de regulile DocMDP și FieldMDP ale semnăturii aceleia și raportează definițiile shadow de obiecte ca risc separat. Situația la care țintește este familiară oricui se ocupă de contracte: un formular certificat pleacă, revine cu încă două salvări incrementale, iar fiecare semnătură se verifică în continuare. Este de așteptat, pentru că o semnătură acoperă doar byte-ii propriei revizuiiri. Întrebarea reală este dacă acele salvări ulterioare au fost permise, iar un bifa verde pe semnătură nu răspunde la ea

De ce nu poate API-ul de semnături PDFium arăta ce s-a schimbat după semnare?

API-ul de semnături PDFium nu poate arăta schimbările de după semnare pentru că citește doar dicționarul semnăturii: /Contents, /ByteRange, /SubFilter și valoarea de permisiune DocMDP. PDFium nu are graf de revizuiiri incrementale, nu parsează parametrii de transformare FieldMDP și nu oferă niciun diff la nivel de obiect între revizuiiri, deci analizatorul din FPdfPades.pas lucrează în schimb direct pe byte brut. Asta are o consecință practică în jurul căreia merită să proiectați. TPdf.AnalyzeSignatureRevisions citește byte-ii reținuți la încărcarea documentului, niciodată o copie produsă de SaveAs, pentru că un fișier rescris a pierdut chiar structura de revizuire care se analizează. Dacă documentul a venit dintr-o sursă progresivă care nu a terminat descărcarea, raportul întoarce SourceStatus = pvssIncomplete și Status = prasIndeterminate în loc să analizeze un fișier trunchiat

Reconstruirea granițelor de revizuire din startxref, fluxuri xref și /Prev

Analizatorul reconstruiește granițele de revizuire urmărind fiecare startxref înapoi prin tabele xref clasice, fluxuri de referințe încrucișate, intrări /XRefStm de referință hibridă și lanțul /Prev, așa cum sunt definite pentru actualizări incrementale în ISO 32000-1 §7.5.6 și §7.5.8. Lungimea acoperită a fiecărei semnături este sfârșitul celui de-al doilea interval ByteRange al ei, iar analizatorul mapează lungimea aceea la revizuirea a cărei secțiune xref o conține. Când nicio revizuire nu se potrivește, semnătura primește prrCoveredRevisionNotFound și un status Indeterminate. Starea fiecărui obiect este apoi redată până la revizuirea acoperită, iar fiecare intrare xref ulterioară este comparată cu starea aceea. Asta contează mai mult decât sună: unii scriitori reformulează tabelul xref complet la fiecare salvare incrementală, iar o intrare care arată încă la același obiect neschimbat este sărită în loc să fie raportată ca modificare. Fără comparația aceea, o umplere de formular perfect legală s-ar îneca în sute de schimbări false

Cum reconstruiește AnalyzeSignatureRevisions revizuirile incrementale din byte PDF brut în Delphi: ByteRange-ul semnăturii se termină în interiorul revizuirii acoperite, lanțul Prev al xref-ului se plimbă înapoi prin fiecare salvare ulterioară, starea obiectelor este redată până la revizuirea acoperită, iar intrările reformulate neschimbate sunt sărite în loc să fie raportate ca schimbări
O semnătură acoperă doar byte-ii propriei revizuiiri, deci analizatorul mapează al doilea interval ByteRange la o revizuire și evaluează fiecare intrare xref ulterioară față de starea redată a obiectelor

Definițiile shadow sunt cazul care merită cea mai multă atenție. Un corp de obiect care apare în interiorul intervalului de byte al unei revizuiiri ulterioare dar nu este referențiat de xref-ul revizuirii aceleia este invizibil pentru un viewer normal, și tocmai felul acesta de înscenare este pe care se sprijină atacurile shadow: conținut ascuns este plantat înainte sau după semnare și activat mai târziu răsturnând o referință. AnalyzePadesSignatureRevisionsBytes înregistrează un asemenea obiect ca schimbare neautoritară cu IsAuthoritative = False, îl evaluează prdSuspicious indiferent de nivelul de permisiune și adaugă prrUnreferencedObjectDefinition la mulțimea de riscuri. Două riscuri înrudite acoperă alte trucuri structurale: prrDuplicateObjectDefinition se declanșează când o secțiune xref listează același obiect de mai multe ori, iar prrSignatureObjectRedefined când o revizuire ulterioară redefineste un obiect de semnătură existent

O definiție shadow de obiect în interiorul intervalului de byte al unei revizuiiri PDF ulterioare: corpul obiectului există dar nicio intrare xref nu îl referențiază, deci viewer-ele nu îl arată niciodată, iar AnalyzeSignatureRevisions din PDFium Component îl înregistrează ca neautoritar, îl evaluează prdSuspicious și ridică prrUnreferencedObjectDefinition alături de riscurile de duplicat și de semnătură redefinită
Conținutul ascuns este plantat înainte sau după semnare și activat mai târziu răsturnând o referință, motiv pentru care un corp nereferențiat este evaluat suspect indiferent de nivelul de permisiune 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;

Cum sunt impuse DocMDP și FieldMDP pentru fiecare semnătură?

DocMDP și FieldMDP sunt impuse separat pentru fiecare semnătură, la propria revizuire acoperită a semnăturii aceleia, astfel încât o semnătură de certificare și o semnătură de aprobare ulterioară în același fișier pot ajunge la verdicte diferite despre aceeași editare. Fiecare obiect ulterior este clasificat întâi într-un TPadesRevisionChangeKind din intrările lui /Type, /Subtype și /FT și din rolul pe care îl joacă în grafurile de pagină, formular, adnotare și DSS. Orice poartă /JavaScript, /JS, /Launch, /OpenAction, /AA, /RichMedia sau /EmbeddedFile devine prckActiveContent. Decizia urmează apoi ISO 32000-1 §12.8.2.2: cu P=1 totul în afară de datele de referință încrucișată și materialul de validare este interzis; P=2 permite umplerea formularelor și semnături în plus, dar respinge schimbările de adnotări; P=3 permite și adnotări. Conținutul paginii, structura documentului, metadatele, conținutul activ și obiectele șterse sunt interzise sub orice nivel DocMDP, și evaluate prdSuspicious când semnătura nu poartă deloc DocMDP, fiindcă o semnătură de aprobare nu interzice formal nimic, dar cititorul nu mai vede ce a fost semnat

FieldMDP, din ISO 32000-1 §12.8.2.4, restrânge și mai mult decizia despre câmpurile de formular. pfmaAll blochează fiecare câmp, pfmaInclude blochează doar câmpurile listate, iar pfmaExclude blochează totul în afară de câmpurile listate. Ca să aplice Include sau Exclude, analizatorul rezolvă fiecare câmp schimbat la numele lui complet calificat prin lanțul /Parent și îl compară cu lista de blocare prin potrivire exactă, deci listați nume de câmpuri terminale în loc să vă așteptați ca un nume de părinte să își acopere copiii. Când un nume nu poate fi rezolvat sau transformarea folosește o acțiune pe care parserul nu o recunoaște, schimbarea devine prdIndeterminate și prrFieldMdpUnresolved este ridicat. Deciziile per schimbare se strâng apoi în sus cel-mai-rău-întâi, cu Suspicious deasupra lui Disallowed, Disallowed deasupra lui Indeterminate, iar Indeterminate deasupra lui Allowed, astfel încât un singur obiect shadow cântărește mai mult decât oricâte actualizări legitime de câmpuri

Pipeline-ul de evaluare pe care AnalyzeSignatureRevisions îl aplică fiecărei schimbări de după semnare în Delphi: un TPadesRevisionChangeKind din intrările Type și Subtype, o decizie DocMDP la revizuirea acoperită de la P=1 la P=3, o verificare de blocare FieldMDP pe nume de câmpuri complet calificate, și o strângere cel-mai-rău-întâi de la prdSuspicious până la prdAllowed
Un singur obiect shadow cântărește mai mult decât oricâte actualizări legitime de câmpuri pentru că Suspicious se clasează deasupra lui Disallowed, Indeterminate și Allowed, în timp ce unele riscuri sunt înregistrate lângă status fără să îl retrogradeze

De ce revin unele schimbări ca Indeterminate în loc de sigure?

Schimbările revin Indeterminate ori de câte ori analizatorul nu poate dovedi că o schimbare este permisă, pentru că într-o verificare de semnătură un necunoscut nu trebuie niciodată raportat ca permis. Un caz comun este tratat în schimb precis: validarea pe termen lung adaugă un /DSS și rescrie catalogul, ceea ce altfel ar conta ca schimbare structurală sub P=1. Analizatorul îndepărtează /DSS și /Extensions din dicționarele de catalog vechi și nou și compară restul; când nimic altceva nu diferă, rescrierea este tratată ca actualizare de material de validare și permisă, astfel încât augmentarea B-LT și B-LTA nu rupe o semnătură de certificare. Alte goluri sunt lăsate deschise din intenție. Intrările de tip 2 dintr-un flux de referințe încrucișate arată în fluxuri de obiecte comprimate, iar analizatorul nu expandează fluxurile de obiecte în interiorul acestei granițe de securitate, deci acele schimbări ies la suprafață ca prckCompressedObject cu prrCompressedObjectUnresolved, interzise sub P=1 și Indeterminate altfel. Bugetele fixe de 1024 de revizuiiri, 1.000.000 de numere de obiect și 2.000.000 de schimbări raportate produc prrResourceLimitExceeded, iar un lanț xref rupt produce prrMalformedRevisionChain; ambele se termină ca Indeterminate, niciodată ca un succes

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

function RevisionVerdict(const R: TPadesRevisionAnalysisReport): string;
begin
  // Unele riscuri sunt înregistrate fără să schimbe Status, deci testați-le întâi
  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;

Ordinea din poarta aceea este deliberată. prrDuplicateObjectDefinition este adăugat la mulțimea de riscuri fără să retrogradeze singur Status, iar o transformare FieldMDP care nu poate fi parsată afectează statusul doar când un câmp de formular se schimbă efectiv, deci o poartă care privește doar Status poate rata dovezi pe care raportul le conține deja. Țineți minte și ce nu pretinde raportul. TPadesRevisionAnalysisReport nu spune nimic despre dacă semnătura CMS este criptografic validă sau despre dacă certificatul semnatarului se înlănțuie către o rădăcină în care aveți încredere. Analiza revizuirilor răspunde la întrebarea ce s-a întâmplat după semnare, și aparține lângă validarea structurală și de încredere, nu în locul lor

Scrierea de seed values și blocări MDP la momentul semnării

Aceleași reguli pot fi scrise la semnare prin TPadesSignatureFieldOptions, care este membrul FieldOptions atât al lui TPadesSignOptions, cât și al lui TPadesRemoteSignOptions. PDFium poate crea un widget dar nu poate scrie /SV, /Lock, o transformare FieldMDP sau DocMDP, sau dicționarul /Perms al catalogului, deci scriitorul PAdES incremental al componentei produce aceste obiecte în interiorul aceleiași actualizări xref ca semnătura. FieldName stabilește numele câmpului rădăcină, RequiredSeedValues devine biții /Ff ai dicționarului de seed values descris în ISO 32000-1 §12.7.4.5, Reasons, LegalAttestations și AcceptableCertificates restrâng ce poate alege un semnatar ulterior, LockAction cu LockFields scrie un /SigFieldLock indirect, iar CertificationPermission de la 1 la 3 transformă semnătura într-o semnătură de certificare. Transformările DocMDP și FieldMDP intră ambele într-un singur tablou /Reference pe valoarea semnăturii, fiecare cu /Data arătând către catalog

var
  Options: TPadesSignOptions;
begin
  Options := TPadesSignOptions.Default;
  Options.CertificateThumbprint := 'A1B2C3D4E5F60718293A4B5C6D7E8F9012345678';
  Options.Reason := 'Approved for release';
  Options.FieldOptions.FieldName := 'Certification';
  Options.FieldOptions.CertificationPermission := 2;   // doar umplere de formular și semnare
  Options.FieldOptions.RequiredSeedValues := [psvcSubFilter, psvcDigestMethod];
  Options.FieldOptions.LockAction := pfmaInclude;      // blochează doar aceste câmpuri
  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;

Câteva detalii sunt ușor de greșit dacă rulați totul de mână. /Perms /DocMDP-ul din catalog trebuie să referențieze dicționarul valorii semnăturii, nu adnotarea widget, iar scriitorul ține valoarea semnăturii ca propriul obiect indirect din acest motiv. Un dicționar /Perms existent poate ține deja drepturi de utilizare /UR3, deci scriitorul îl copiază și inserează /DocMDP în loc să îl înlocuiască, urmând dicționarul de permisiuni din ISO 32000-1 §12.8.4. Un document care poartă deja /DocMDP refuză o a doua semnătură de certificare cu EPadesCrypto, la fel și opțiunile inconsistente: o blocare Include sau Exclude fără nume de câmpuri, o blocare All cu o listă de câmpuri, o atestare legală pe o semnătură non-certificare, sau un punct în numele câmpului rădăcină. Semnarea la distanță adaugă încă o regulă, pentru că certificatul de semnare este necunoscut când rulează PreparePadesRemoteSignature: setarea lui CertificateRequired acolo cere o listă explicită AcceptableCertificates, în timp ce semnarea locală poate cădea pe certificatul semnatarului rezolvat

Analiza revizuirilor completează trusa de unelte pentru semnături în loc să înlocuiască orice parte din ea. Începeți cu inspectarea semnăturilor PDF și a nivelurilor PAdES cu PDFium Component ca să citiți dicționarul și nivelul de bază, priviți de ce resping validatorii semnăturile PAdES pentru eșecurile structurale care vin înaintea oricărei întrebări de revizuire, și împăturați verdictul într-un audit mai larg de riscuri de securitate PDF alături de verificările JavaScript și de fișiere încorporate. TPdf.AnalyzeSignatureRevisions, TPadesSignatureFieldOptions și scriitorul PAdES incremental arătat aici se livrează cu PDFium Component pentru Delphi, C++Builder și Lazarus