Un PDF semnat care s-a schimbat după semnare nu este automat stricat. ISO 32000-1 permite actualizări incrementale peste o semnătură, iar doar unele dintre ele încalcă politica pe care a stabilit-o semnatarul. HotPDF Component pentru Delphi și C++Builder răspunde la această întrebare cu AnalyzeLoadedSignatureRevisions, care clasifică fiecare revizie post-semnătură și o notează față de DocMDP și FieldMDP. Scenariul este familiar oricui livrează software pentru contracte: clientul dumneavoastră semnează un acord de cumpărare, îl trimite și îl primește înapoi cu o pagină de anexă atașată. Cititorul afișează o bară galbenă care spune că semnătura este intactă, dar documentul a fost schimbat de la semnare, iar nimeni din cameră nu poate spune dacă este un flux normal de contrasemnare sau cineva editând discret un contract semnat
Ce se consideră o schimbare legală după semnare?
O schimbare este legală când categoria ei semantică se încadrează în permisiunea pe care a declarat-o semnătura certificatoare. ISO 32000-1 §12.8.2.2 definește transformarea DocMDP cu o valoare /P de 1, 2 sau 3: 1 nu permite nicio schimbare, 2 permite completarea formularelor și semnarea, 3 permite completarea formularelor, semnarea și adnotările. HotPDF expune acestea ca valori THPDFDocMDPPermission dmpNoChanges, dmpFormFillAndSign și dmpFormFillSignAndAnnotate, cu dmpNone rezervat pentru rezultatele de inspecție care nu poartă deloc o transformare DocMDP
Categoriile sunt ordonate, iar acea ordonare este motorul întregii verificări. THPDFRevisionModificationLevel rulează rmlNone, rmlLongTermValidation, rmlFormFillAndSign, rmlAnnotations, rmlOther, aranjate deliberat astfel încât un ordinal mai mare nu este niciodată mai puțin restrictiv. Un document întreg se reduce la nivelul maxim observat pe toate reviziile de după semnătură, iar comparația DocMDP devine un singur test pe întreg. O nuanță contează devreme: la dmpNoChanges, analiza tot acceptă rmlLongTermValidation. Adăugarea materialului de validare DSS și VRI sau a unei ștampile de timp de document la un fișier certificat este întreținerea semnăturii, nu modificarea documentului, iar tratarea ei ca o încălcare ar strica orice flux de arhivare pe termen lung existent
Cum reconstruiește HotPDF lanțul de revizii?
Structural, nu euristic. Conform ISO 32000-1 §7.5.6, o actualizare incrementală adaugă o nouă secțiune cross-reference al cărei /Prev indică spre cea anterioară, așa că HotPDF citește startxref de la coadă, analizează secțiunea de acolo, urmează /Prev înapoi și repetă, returnând secțiunile de la cea mai veche la cea mai nouă. Două limite de siguranță stau în acea buclă și ambele merită cunoscute când se triază un fișier care eșuează: un /Prev care indică spre un offset deja vizitat termină parcurgerea cu un diagnostic explicit de ciclu, în loc să intre în buclă infinită, iar un lanț mai lung de o mie de revizii este respins direct. Ambele apar în Analysis.Issue, funcția returnând False, și niciunul nu ar trebui trecut cu vederea, deoarece un /Prev ciclic este un fișier malformat sau ostil, nu unul doar neobișnuit
Patru forme istorice apar în documente reale și toate patru sunt gestionate: tabelele xref tradiționale analizate linie cu linie, fluxurile cross-reference decomprimate și decodate prin câmpurile lor /W și /Index, fișierele cu referință hibridă al căror trailer tradițional poartă o cheie /XRefStm care este analizată și îmbinată în aceeași revizie (cazul producătorului Office, tratat în articolul despre fluxurile cross-reference hibride), și obiectele care trăiesc într-un container ObjStm, care contează pentru că o actualizare modernă pune de obicei dicționarul schimbat într-un flux comprimat, în loc să îl scrie direct, așa cum este descris în articolul despre fluxurile de obiecte și actualizările incrementale. Semnătura ancorează divizarea: /ByteRange[2] + /ByteRange[3] devine SignedRevisionLength, iar fiecare secțiune la sau după acel offset este post-semnătură. Dacă intervalul de octeți încă hashuiește corect este o întrebare separată, la care răspunde VerifyLoadedSignature, tratată în articolul despre verificarea semnăturilor digitale PDF
Cum este clasificat fiecare obiect schimbat
Clasificarea rulează per obiect, apoi se propagă de-a lungul referințelor. Pentru fiecare număr de obiect atins de o secțiune post-semnătură, HotPDF citește noul corp și corpul așa cum stătea el în instantaneul semnat; un corp identic este rmlNone, deoarece producătorii chiar rescriu obiecte fără să le schimbe. Recunoscătoarele sunt deliberat înguste. Un obiect /Type /DocTimeStamp, sau unul al cărui /SubFilter este ETSI.RFC3161, este rmlLongTermValidation, la fel ca orice accesibil din arborele /DSS al catalogului; un dicționar /Type /Sig este rmlFormFillAndSign. Pentru containere, testul este care chei s-au mutat, nu ce este obiectul: catalogul poate doar câștiga sau modifica /DSS, /Extensions sau /AcroForm; dicționarul AcroForm doar /Fields, /SigFlags, /NeedAppearances, /DR, /DA sau /Q; o pagină doar /Annots; un câmp sau widget doar /V, /AP, /AS sau /M. Orice iese din acele seturi cade la rmlOther, exact modul în care este prinsă pagina de anexă adăugată: adăugarea unei pagini rearanjează arborele de pagini în moduri pe care nicio listă albă nu le acoperă, iar nicio completare legitimă de formular nu seamănă cu asta
Apoi nivelurile se propagă, fiecare container moștenind nivelul maxim al copiilor schimbate spre care indică, iterat până când atribuirea se stabilizează. Acesta este ceea ce face să funcționeze fluxurile de aspect. Un câmp de text completat rescrie /V și indică spre un flux /AP nou, iar acel flux, luat singur, este un blob anonim de operatori de conținut fără niciun tip de recunoscut; deoarece câmpul care îl deține este rmlFormFillAndSign, fluxul moștenește același nivel în loc să cadă la rmlOther. Aceeași propagare poartă contextul DSS spre fluxurile de certificate și revocare care altfel ar fi neclasificabile
De ce contează un obiect ilizibil drept încălcare?
Pentru că alternativa este un validator învins de scrierea a ceva ce nu înțelege. Trei situații ajung la rmlOther fără drept de apel în HotPDF: un obiect al cărui corp nu a putut fi citit din revizie, un obiect pe care revizia îl marchează ca eliberat și un obiect care nu se potrivește cu niciunul din recunoscătoarele de mai sus. Fiecare înregistrează un diagnostic specific în câmpul Issue al reviziei, astfel încât un operator poate vedea ce număr de obiect a produs verdictul
Eliberarea este cea mai tăioasă dintre cele trei. O revizie post-semnătură care marchează un obiect anterior definit ca eliberat a șters conținut dintr-un document semnat, iar niciun nivel de permisiune conform §12.8.2.2 nu permite asta; numerele de obiecte ajung în FreedObjectNumbers, iar revizia este ridicată la rmlOther. Obiectele ilizibile urmează aceeași logică dintr-un motiv diferit. Un validator care nu poate analiza un obiect nu are niciun temei să îl numească inofensiv, iar răspunsul cinstit la asta nu este tăcerea. Raportarea unei construcții neobișnuite dar benigne ca încălcare costă o verificare umană; greșeala opusă livrează un contract semnat cu o editare neobservată în interior
Citirea verdictului în Delphi
Apelul este scurt. Încărcați documentul, alegeți un index de semnătură, citiți înregistrarea; supraîncărcarea fără parametri redeschide fișierul din care a fost încărcat documentul, iar supraîncărcarea TStream preia octeți furnizați de apelant și restaurează poziția fluxului înainte de a returna. PolicyCompliant este singurul boolean pe care majoritatea apelanților îl doresc, combinând trei decizii independente: validitatea structurală a dicționarelor de permisiune, DocMDPCompliant și FieldMDPCompliant. Păstrați componentele vizibile în interfața dumneavoastră, în loc să le colapsați, și rețineți că un document fără transformare DocMDP lasă DocMDPCompliant pe True, deoarece o semnătură de aprobare obișnuită nu declară nicio politică de încălcat, iar ModificationLevel agregat este atunci descriptiv, nu un verdict
var
Pdf: THotPDF;
Analysis: THPDFSignatureRevisionAnalysis;
begin
Pdf := THotPDF.Create(nil);
try
if Pdf.LoadFromFile('contract-countersigned.pdf') > 0 then
begin
if Pdf.AnalyzeLoadedSignatureRevisions(0, Analysis) then
begin
if Analysis.PolicyCompliant then
Writeln('Post-signature changes stay inside the signing policy')
else
Writeln('Policy violation: ', string(Analysis.Issue));
end
else
Writeln('Analysis could not run: ', string(Analysis.Issue));
end;
finally
Pdf.Free;
end;
end;
Pentru triaj, de obicei doriți defalcarea per revizie în loc de rezumat, deoarece vă spune când în istoria documentului lucrurile au mers prost. Fiecare intrare din Analysis.Revisions poartă indexul ei în lanț, offset-ul cross-reference la care a fost scrisă, propriul nivel de modificare și numerele de obiecte implicate
const
LevelNames: array[THPDFRevisionModificationLevel] of string =
('none', 'long-term validation', 'form fill and sign',
'annotations', 'other');
var
I: Integer;
begin
Writeln(Format('%d revisions in chain, signature sits at index %d',
[Analysis.TotalRevisionCount, Analysis.SignedRevisionIndex]));
for I := 0 to High(Analysis.Revisions) do
Writeln(Format(' rev %d at offset %d: %s (%d changed, %d freed) %s',
[Analysis.Revisions[I].RevisionIndex,
Analysis.Revisions[I].XRefOffset,
LevelNames[Analysis.Revisions[I].ModificationLevel],
Length(Analysis.Revisions[I].ChangedObjectNumbers),
Length(Analysis.Revisions[I].FreedObjectNumbers),
string(Analysis.Revisions[I].Issue)]));
end;
FieldMDP este judecat separat, iar asta este deliberat
Un document poate satisface DocMDP și tot poate fi ilegitim, motiv pentru care FieldMDPCompliant este un boolean distinct, nu pliat în comparația de nivel. ISO 32000-1 §12.8.2.4 definește transformarea FieldMDP, iar §12.7.5.5 intrarea aferentă /SigFieldLock, pentru a îngheța câmpurile de formular numite la momentul semnării, chiar și acolo unde documentul ca întreg încă permite completarea de formulare. Completarea unui câmp este o acțiune de nivel 2; completarea unui câmp pe care semnatarul l-a blocat este o încălcare indiferent de nivel. HotPDF citește domeniul de aplicare în THPDFFieldLockAction ca flaAll, flaInclude sau flaExclude, cu flaNone pentru rezultatele care nu poartă nicio politică de blocare, iar numele în Permissions.FieldNames: flaAll blochează totul, flaInclude blochează numele listate, flaExclude blochează totul mai puțin pe ele. Un detaliu contează la citirea rezultatelor, anume că doar câmpurile deja prezente în instantaneul semnat sunt raportate în ChangedFieldNames, deoarece un câmp creat în întregime după semnare nu are nicio stare semnată de contrazis și este prins în schimb de calea DocMDP
var
Source: TFileStream;
Analysis: THPDFSignatureRevisionAnalysis;
I: Integer;
begin
Source := TFileStream.Create('contract.pdf', fmOpenRead or fmShareDenyWrite);
try
if Pdf.AnalyzeLoadedSignatureRevisions(0, Source, Analysis) then
if Analysis.Permissions.HasFieldMDP and (not Analysis.FieldMDPCompliant) then
for I := 0 to High(Analysis.ChangedFieldNames) do
Writeln('modified after locking: ',
string(Analysis.ChangedFieldNames[I]));
finally
Source.Free; // stream position was restored before the call returned
end;
end;
Ce nu vă spune această analiză
Nu verifică o semnătură. AnalyzeLoadedSignatureRevisions raționează despre structură și permisiuni; dacă intervalul de octeți semnat încă hashuiește la valoarea din blob-ul CMS, și dacă certificatul semnatarului se înlănțuie la ceva în care aveți încredere, la acestea răspund VerifyLoadedSignature și VerifyLoadedSignatureWithTrust. Un fișier poate fi perfect conform politicii și complet lipsit de valoare criptografică, așa că cele două verificări aparțin una lângă alta în orice poartă reală de acceptare. De asemenea nu citește intenția din interiorul fluxurilor de conținut: o pagină al cărei flux de conținut a fost înlocuit în întregime este prinsă drept o schimbare din afara listei albe, dar analiza nu vă va spune că înlocuirea a schimbat o sumă de plată. Un verdict rmlOther înseamnă că un om ar trebui să se uite, nu că a avut loc o fraudă, iar un verdict conform înseamnă că schimbarea se încadrează într-o categorie permisă, nu că schimbarea a fost dorită. Când tot ce aveți nevoie este ce a declarat semnatarul, fără parcurgerea reviziilor, GetLoadedSignaturePermissions returnează singure dicționarele de politică
Tot ce este descris aici rulează nativ în Delphi și C++Builder, fără niciun serviciu extern de semnare implicat, ceea ce face practică rularea pe fiecare document primit, nu doar pe cele deja suspectate de cineva. API-ul complet de semnătură și revizie, inclusiv metodele de permisiune și verificare cu care se asociază, face parte din HotPDF Component pentru Delphi și C++Builder