Da biste saznali što se u PDF-u promijenilo nakon što je potpisan, PDFium Component za Delphi i Lazarus nudi TPdf.AnalyzeSignatureRevisions, analizator promjena revizija nakon potpisa koji svaku inkrementalnu reviziju gradi iznova iz izvornih bajtova datoteke, svaku kasniju promjenu objekta ocjenjuje prema DocMDP i FieldMDP pravilima tog potpisa i definicije shadow objekata javlja kao zaseban rizik. Situacija koju cilja poznata je svakomu tko rukuje ugovorima: ovjereni obrazac ode, vrati se s još dvama inkrementalnim spremanjima, i svaki potpis i dalje verificira. To se i očekuje, jer potpis pokriva samo bajtove vlastite revizije. Pravo je pitanje je li tim kasnijim spremanjima bilo dopušteno, i zelena kvačica na potpisu na to ne odgovara
Zašto PDFium signature API ne može pokazati što se promijenilo nakon potpisivanja?
PDFium signature API ne može pokazati promjene nakon potpisa jer čita samo rječnik potpisa: /Contents, /ByteRange, /SubFilter i DocMDP vrijednost dopuštenja. PDFium nema graf inkrementalnih revizija, ne parsira FieldMDP parametre transformacije i ne nudi diff između revizija na razini objekata, pa analizator u FPdfPades.pas radi izravno nad sirovim bajtovima. To ima praktičnu posljedicu oko koje trebate dizajnirati. TPdf.AnalyzeSignatureRevisions čita bajtove zadržane pri učitavanju dokumenta, nikad kopiju proizvedenu s SaveAs, jer je prepravljena datoteka izgubila upravo strukturu revizija koja se analizira. Ako je dokument došao iz progresivnog izvora koji nije završio s preuzimanjem, izvještaj vraća SourceStatus = pvssIncomplete i Status = prasIndeterminate umjesto da analizira skraćenu datoteku
Gradnja granica revizija iz startxref, xref streamova i /Prev
Analizator gradi granice revizija iznova prateći svaki startxref natrag kroz klasične xref tablice, cross-reference streamove, hybrid-reference unose /XRefStm i lanac /Prev, kako je definirano za inkrementalne update u ISO 32000-1 §7.5.6 i §7.5.8. Pokrivena duljina svakog potpisa jest kraj njegovog drugog ByteRange raspona, i analizator tu duljinu preslikava na reviziju čijoj xref sekciji pada unutra. Kad nijedna revizija ne odgovara, potpis dobiva prrCoveredRevisionNotFound i Indeterminate status. Stanje svakog objekta zatim se reproducira do pokrivene revizije, i svaki kasniji xref unos uspoređuje se s tim stanjem. To je važnije nego što zvuči: neki pisci na svakom inkrementalnom spremanju ponovno navode potpunu xref tablicu, i unos koji i dalje pokazuje na isti nepromijenjeni objekt preskače se umjesto da se javi kao modifikacija. Bez te usporedbe, potpuno legalno popunjavanje obrasca utopilo bi se u stotine lažnih promjena
Shadow definicije su slučaj koji zaslužuje najviše pozornosti. Tijelo objekta koje se pojavljuje unutar bajt raspona kasnije revizije, a nije referencirano xrefom te revizije, nevidljivo je normalnom vieweru, a upravo je to vrsta pripreme na kojoj počivaju shadow napadi: skriveni sadržaj posadi se prije ili nakon potpisivanja i kasnije aktivira okretanjem reference. AnalyzePadesSignatureRevisionsBytes takav objekt bilježi kao nearutoritativnu promjenu s IsAuthoritative = False, ocjenjuje ga prdSuspicious bez obzira na razinu dopuštenja i dodaje prrUnreferencedObjectDefinition u skup rizika. Dva srodna rizika pokrivaju druge strukturne trikove: prrDuplicateObjectDefinition opali kad jedna xref sekcija navede isti objekt više od jednom, a prrSignatureObjectRedefined kad kasnija revizija redefinira postojeći objekt potpisa
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 i FieldMDP provode za svaki potpis?
DocMDP i FieldMDP provode se odvojeno za svaki potpis, na vlastitoj pokrivenoj reviziji tog potpisa, pa certificirajući potpis i kasniji potpis odobrenja u istoj datoteci mogu doći do različitih presuda o istoj izmjeni. Svaki kasniji objekt najprije se klasificira u TPadesRevisionChangeKind iz svojih unosa /Type, /Subtype i /FT i iz uloge koju igra u grafovima stranice, obrasca, anotacija i DSS-a. Sve što nosi /JavaScript, /JS, /Launch, /OpenAction, /AA, /RichMedia ili /EmbeddedFile postaje prckActiveContent. Odluka zatim slijedi ISO 32000-1 §12.8.2.2: s P=1 sve osim cross-reference podataka i materijala validacije zabranjeno je; P=2 dopušta popunjavanje obrasca i daljnje potpise ali odbija promjene anotacija; P=3 dopušta i anotacije. Sadržaj stranice, struktura dokumenta, metapodaci, aktivni sadržaj i obrisani objekti zabranjeni su na bilo kojoj DocMDP razini, i ocjenjuju se prdSuspicious kad potpis uopće ne nosi DocMDP, jer potpis odobrenja ništa formalno ne zabranjuje, ali čitatelj više ne vidi što je potpisano
FieldMDP, iz ISO 32000-1 §12.8.2.4, dalje sužava odluku o poljima obrasca. pfmaAll zaključava svako polje, pfmaInclude samo navedena polja, a pfmaExclude sve osim navedenih polja. Da primijenio Include ili Exclude, analizator svako promijenjeno polje razrješuje do potpuno kvalificiranog imena kroz lanac /Parent i uspoređuje ga s popisom zaključanih točnim poklapanjem, pa navodite terminalna imena polja umjesto da očekujete da ime roditelja pokrije djecu. Kad se ime ne može razriješiti ili transformacija koristi akciju koju parser ne prepoznaje, promjena postaje prdIndeterminate i diže se prrFieldMdpUnresolved. Odluke po promjeni zatim se sažimaju najgore-prvo, sa Suspicious iznad Disallowed, Disallowed iznad Indeterminate i Indeterminate iznad Allowed, pa jedan shadow objekt nadjačava bilo koji broj legitimnih updateova polja
Zašto neke promjene dolaze natrag kao Indeterminate umjesto kao sigurne?
Promjene dolaze natrag kao Indeterminate kad god analizator ne može dokazati da je promjena dopuštena, jer u provjeri potpisa nepoznato se nikad ne smije javiti kao dopušteno. Jedan uobičajeni slučaj rješava se precizno: dugoročna validacija dodaje /DSS i prepravlja katalog, što bi inače računalo kao strukturnu promjenu pod P=1. Analizator skida /DSS i /Extensions sa starih i novih katalog rječnika i uspoređuje ostatak; kad se ništa drugo ne razlikuje, prepravka tretira se kao update validacijskog materijala i dopušta se, pa B-LT i B-LTA augmentacija ne lomi certificirajući potpis. Ostale praznine namjerno su ostavljene otvorenima. Type-2 unosi u cross-reference streamu pokazuju u komprimirane object streamove, i analizator ne širi object streamove unutar ove sigurnosne granice, pa te promjene izbijaju kao prckCompressedObject s prrCompressedObjectUnresolved, zabranjene pod P=1 i Indeterminate inače. Krute budžete od 1024 revizije, 1.000.000 brojeva objekata i 2.000.000 prijavljenih promjena proizvode prrResourceLimitExceeded, a slomljen xref lanac prrMalformedRevisionChain; obje završavaju kao Indeterminate, nikad kao prolaz
const
StructuralRisks: TPadesRevisionRisks = [prrMalformedRevisionChain,
prrDuplicateObjectDefinition, prrUnreferencedObjectDefinition,
prrSignatureObjectRedefined, prrResourceLimitExceeded];
function RevisionVerdict(const R: TPadesRevisionAnalysisReport): string;
begin
// Neki rizici bilježe se bez mijenjanja Statusa, pa ih testirajte prve
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;
Redoslijed u toj kapiji namjeran je. prrDuplicateObjectDefinition dodaje se u skup rizika bez da sama spušta Status, a FieldMDP transformacija koja se ne može parsirati utječe na status tek kad se polje obrasca stvarno promijeni, pa kapija koja gleda samo Status može promašiti dokaze koje izvještaj već sadrži. Imajte na umu i što izvještaj ne tvrdi. TPadesRevisionAnalysisReport ne govori ništa o tome je li CMS potpis kriptografski valjan ili lanac certifikata potpisnika vodi do korijena kojem Vi vjerujete. Analiza revizija odgovara na pitanje što se dogodilo nakon potpisivanja, i pripada uz strukturnu i validaciju povjerenja, ne umjesto njih
Pisanje seed vrijednosti i MDP zaključavanja u trenutku potpisivanja
Ista pravila mogu se autorirati pri potpisivanju kroz TPadesSignatureFieldOptions, koji je član FieldOptions i TPadesSignOptions i TPadesRemoteSignOptions. PDFium može stvoriti widget ali ne može napisati /SV, /Lock, FieldMDP ili DocMDP transformaciju, ni katalog rječnik /Perms, pa komponentin vlastiti inkrementalni PAdES pisac proizvodi te objekte unutar istog xref updatea kao i potpis. FieldName postavlja korijensko ime polja, RequiredSeedValues postaje /Ff bitovi rječnika seed vrijednosti opisanog u ISO 32000-1 §12.7.4.5, Reasons, LegalAttestations i AcceptableCertificates ograničavaju što kasniji potpisnik može odabrati, LockAction s LockFields piše neizravni /SigFieldLock, a CertificationPermission od 1 do 3 pretvara potpis u certificirajući potpis. DocMDP i FieldMDP transformacije obje idu u jedno polje /Reference na vrijednosti potpisa, svaka s /Data koja pokazuje 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; // samo popunjavanje obrasca i potpisivanje
Options.FieldOptions.RequiredSeedValues := [psvcSubFilter, psvcDigestMethod];
Options.FieldOptions.LockAction := pfmaInclude; // zaključaj samo ova 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;
Par detalja lako je pokvariti ako ovo rukotvorite sami. Katalog /Perms /DocMDP mora referencirati rječnik vrijednosti potpisa, ne widget anotaciju, i pisac vrijednost potpisa drži kao vlastiti neizravni objekt upravo iz tog razloga. Postojeći rječnik /Perms možda već nosi /UR3 prava korištenja, pa ga pisac kopira i umetne /DocMDP umjesto da ga zamijeni, slijedeći rječnik dopuštenja u ISO 32000-1 §12.8.4. Dokument koji već nosi /DocMDP odbija drugi certificirajući potpis s EPadesCrypto, i nekonzistentne opcije isto: Include ili Exclude zaključavanje bez imena polja, All zaključavanje s popisom polja, pravna attestacija na necertificirajućem potpisu ili točka u korijenskom imenu polja. Daljinsko potpisivanje dodaje još jedno pravilo, jer je potpisni certifikat nepoznat kad PreparePadesRemoteSignature radi: postavljanje CertificateRequired tamo traži eksplicitni popis AcceptableCertificates, dok lokalno potpisivanje može pasti na razriješeni certifikat potpisnika
Analiza revizija upotpunjuje signature toolbox umjesto da bilo koji njegov dio zamijeni. Počnite s pregledom PDF potpisa i PAdES razina s PDFium Componentom da pročitate rječnik i baznu razinu, pogledajte zašto validatori odbijaju PAdES potpise za strukturne padove koji dolaze prije bilo kakvog pitanja revizija, i presudu ugradite u širu PDF reviziju sigurnosnih rizika uz provjere JavaScripta i ugrađenih datoteka. TPdf.AnalyzeSignatureRevisions, TPadesSignatureFieldOptions i inkrementalni PAdES pisac prikazani ovdje isporučuju se s PDFium Componentom za Delphi, C++Builder i Lazarus