Tehnički članak

Analiza PDF izmena posle potpisa sa PDFium-om u Delphi-ju

Da biste saznali šta je menjano u PDF-u posle što je potpisan, PDFium Component za Delphi i Lazarus daje TPdf.AnalyzeSignatureRevisions, analizator izmena revizija posle potpisa koji svaku inkrementalnu reviziju gradi iznova iz originalnih bajtova fajla, ocenjuje svaku kasniju izmenu objekta prema DocMDP i FieldMDP pravilima tog potpisa, i prijavljuje shadow definicije objekata kao poseban rizik. Situacija na koju cilja poznata je svakome ko rukuje ugovorima: sertifikovani obrazac ode, vrati se sa još dva inkrementalna čuvanja, i svaki potpis i dalje verifikuje. To je očekivano, jer potpis pokriva samo bajtove svoje revizije. Pravo pitanje je da li su ta kasnija čuvanja bila dozvoljena, i zeleni ček na potpisu na to ne odgovara

Zašto PDFium signature API ne može da pokaže šta je menjano posle potpisa?

PDFium signature API ne može da pokaže izmene posle potpisa jer čita samo rečnik potpisa: /Contents, /ByteRange, /SubFilter i DocMDP vrednost dozvola. PDFium nema graf inkrementalnih revizija, ne parsira FieldMDP transform parametre, i ne nudi diff objekata između revizija, pa analizator u FPdfPades.pas radi direktno na sirovim bajtovima. To ima praktičnu posledicu oko koje treba da dizajnirate. TPdf.AnalyzeSignatureRevisions čita bajtove zadržane pri učitavanju dokumenta, nikada kopiju proizvedenu sa SaveAs, jer preslovljen fajl izgubio je upravo strukturu revizija koja se analizira. Ako je dokument došao iz progresivnog izvora koji nije završio skidanje, izveštaj vraća SourceStatus = pvssIncomplete i Status = prasIndeterminate umesto da analizira odsečen fajl

Gradnja granica revizija iznova iz startxref, xref stream-ova i /Prev

Analizator gradi granice revizija iznova prateći svaki startxref unazad kroz klasične xref tabele, cross-reference stream-ove, hybrid-reference /XRefStm unose i lanac /Prev, kako je definisano za inkrementalne update-ove u ISO 32000-1 §7.5.6 i §7.5.8. Pokrivena dužina svakog potpisa je kraj njegovog drugog ByteRange raspona, i analizator preslikava tu dužinu na reviziju čijem se xref odeljku nalazi unutra. Kad se nijedna revizija ne poklopi, potpis dobija prrCoveredRevisionNotFound i Indeterminate status. Stanje svakog objekta se zatim reprizira do pokrivene revizije, i svaki kasniji xref unos poredi se sa tim stanjem. To je bitnije nego što zvuči: neki pisci ponavljaju kompletnu xref tabelu pri svakom inkrementalnom čuvanju, i unos koji i dalje pokazuje na isti nepromenjen objekat se preskače umesto da se prijavi kao izmena. Bez tog poređenja, sasvim legalno popunjavanje obrasca davilo bi se u stotinama lažnih izmena

Kako AnalyzeSignatureRevisions gradi inkrementalne revizije iznova iz sirovih PDF bajtova u Delphi-ju: ByteRange potpisa završava unutar pokrivene revizije, lanac xref Prev šeta unazad kroz svako kasnije čuvanje, stanje objekata se reprizira do pokrivene revizije, i nepromenjeni ponovljeni unosi se preskaču umesto da se prijave kao izmene
Potpis pokriva samo bajtove svoje revizije, pa analizator preslikava drugi ByteRange raspon na reviziju i ocenjuje svaki kasniji xref unos prema repriziranom stanju objekata

Shadow definicije su slučaj koji zasluguje najviše pažnje. Telo objekta koje se pojavi unutar bajt raspona kasnije revizije, a ne referencira ga xref te revizije, nevidljivo je običnom viewer-u, a baš je to vrsta pripreme na koju se shadow napadi oslanjaju: skriveni sadržaj se zasađuje pre ili posle potpisa i kasnije aktivira okretanjem reference. AnalyzePadesSignatureRevisionsBytes zabeleži takav objekat kao neautoritativnu izmenu sa IsAuthoritative = False, oceni ga prdSuspicious bez obzira na nivo dozvola, i dodaje prrUnreferencedObjectDefinition u skup rizika. Dva srodna rizika pokrivaju druge strukturne trikove: prrDuplicateObjectDefinition opali kad jedan xref odeljak izlista isti objekat više od jednom, a prrSignatureObjectRedefined kad kasnija revizija predefiniše postojeći objekat potpisa

Shadow definicija objekta unutar bajt raspona kasnije PDF revizije: telo objekta postoji ali ga nijedan xref unos ne referencira, pa viewer-i ga nikada ne prikažu, a AnalyzeSignatureRevisions u PDFium Component-u ga zabeleži kao neautoritativnog, oceni ga prdSuspicious i podigne prrUnreferencedObjectDefinition uz rizike duplikata i predefinisanog potpisa
Skriveni sadržaj se zasađuje pre ili posle potpisa i kasnije aktivira okretanjem reference, pa se nereferencirano telo ocenjuje kao sumnjivo bez obzira na nivo DocMDP dozvola
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 sprovode za svaki potpis?

DocMDP i FieldMDP sprovode se odvojeno za svaki potpis, na sopstvenoj pokrivenoj reviziji tog potpisa, pa sertifikacioni potpis i kasniji potpis odobrenja u istom fajlu mogu doći do različitih presuda o istoj izmeni. Svaki kasniji objekat se prvo klasifikuje 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 prati ISO 32000-1 §12.8.2.2: sa P=1 sve osim cross-reference podataka i materijala validacije je zabranjeno; P=2 dozvoljava popunjavanje obrazaca i dalje potpise ali odbija izmene anotacija; P=3 dozvoljava i anotacije. Sadržaj stranice, struktura dokumenta, metapodaci, aktivni sadržaj i obrisani objekti zabranjeni su na svakom DocMDP nivou, i ocenjuju se prdSuspicious kad potpis uopšte ne nosi DocMDP, jer potpis odobrenja ništa formalno ne zabranjuje ali čitač više ne vidi šta je potpisano

FieldMDP, iz ISO 32000-1 §12.8.2.4, dodatno sužava odluku o poljima obrasca. pfmaAll zaključava svako polje, pfmaInclude zaključava samo izlistana polja, a pfmaExclude zaključava sve osim izlistanih polja. Da primeni Include ili Exclude, analizator razrešava svako menjano polje u njegovo potpuno kvalifikovano ime kroz lanac /Parent i poredi ga sa listom zaključanih tačnim poklapanjem, pa izlistavajte terminalna imena polja umesto da očekujete da ime roditelja pokrije decu. Kad ime ne može da se razreši ili transform koristi akciju koju parser ne prepoznaje, izmena postaje prdIndeterminate i podiže se prrFieldMdpUnresolved. Odluke po izmeni se zatim valjaju najgore-prvo, sa Suspicious iznad Disallowed, Disallowed iznad Indeterminate, i Indeterminate iznad Allowed, pa jedan shadow objekat nadjačava bilo koji broj legitimnih izmena polja

Pipeline ocenjivanja koji AnalyzeSignatureRevisions primenjuje na svaku izmenu posle potpisa u Delphi-ju: TPadesRevisionChangeKind iz unosa Type i Subtype, DocMDP odluka na pokrivenoj reviziji od P=1 do P=3, FieldMDP provera zaključavanja na potpuno kvalifikovanim imenima polja, i valjanje najgore-prvo od prdSuspicious do prdAllowed
Jedan shadow objekat nadjačava bilo koji broj legitimnih izmena polja jer je Suspicious iznad Disallowed, Indeterminate i Allowed, dok se neki rizici zabeležuju pored statusa bez njegovog spuštanja

Zašto neke izmene vraćaju Indeterminate umesto bezbedno?

Izmene vraćaju Indeterminate svaki put kad analizator ne može da dokaže da je izmena dozvoljena, jer u proveri potpisa nepoznato nikada ne sme da se prijavi kao dozvoljeno. Jedan uobičajen slučaj postupa se umesto toga precizno: dugoročna validacija dodaje /DSS i preslovljava katalog, što bi inače računalo se kao strukturna izmena pod P=1. Analizator skine /DSS i /Extensions sa starog i novog rečnika kataloga i uporedi ostatak; kad se ništa drugo ne razlikuje, preslovljavanje se tretira kao update validacionog materijala i dozvoljava, pa B-LT i B-LTA nadogradnja ne slomi sertifikacioni potpis. Druge praznine ostavljaju se otvorene namerno. Type-2 unosi u cross-reference stream-u pokazuju u komprimovane object stream-ove, i analizator ne širi object stream-ove unutar ove sigurnosne granice, pa te izmene isplivaju kao prckCompressedObject sa prrCompressedObjectUnresolved, zabranjene pod P=1 i Indeterminate inače. Tvrsti budžeti od 1024 revizija, 1.000.000 brojeva objekata i 2.000.000 prijavljenih izmena proizvode prrResourceLimitExceeded, a polomljen xref lanac proizvodi prrMalformedRevisionChain; oba se završe kao Indeterminate, nikada kao prolaz

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

function RevisionVerdict(const R: TPadesRevisionAnalysisReport): string;
begin
  // Neki rizici se zabeležavaju bez menjanja Status-a, pa ih testirajte prvo
  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;

Redosled u toj kapiji je nameran. prrDuplicateObjectDefinition se dodaje u skup rizika bez spuštanja Status-a sam po sebi, i FieldMDP transform koji ne može da se parsira utiče na status tek kad se polje obrasca zaista promeni, pa kapija koja gleda samo u Status može promašiti dokaz koji izveštaj već sadrži. Imajte na umu i ono što izveštaj ne tvrdi. TPadesRevisionAnalysisReport ne govori ništa o tome da li je CMS potpis kriptografski validan ili da li sertifikat potpisoca vezuje lanac do korena kome verujete. Analiza revizija odgovara na pitanje šta se desilo posle potpisa, i pripada uz strukturnu i validaciju poverenja, ne umesto njih

Pisanje seed vrednosti i MDP zaključavanja u trenutku potpisivanja

Ista pravila mogu se autorati pri potpisivanju kroz TPadesSignatureFieldOptions, koji je član FieldOptions i TPadesSignOptions-a i TPadesRemoteSignOptions-a. PDFium može da napravi widget ali ne može da napiše /SV, /Lock, FieldMDP ili DocMDP transform, ili rečnik kataloga /Perms, pa sopstveni inkrementalni PAdES pisac komponente proizvodi te objekte unutar istog xref update-a kao potpis. FieldName postavlja ime korenskog polja, RequiredSeedValues postaje /Ff bitovi rečnika seed vrednosti opisanog u ISO 32000-1 §12.7.4.5, Reasons, LegalAttestations i AcceptableCertificates ograničavaju šta kasniji potpisnik može da izabere, LockAction sa LockFields piše indirektan /SigFieldLock, a CertificationPermission od 1 do 3 pretvara potpis u sertifikacioni. DocMDP i FieldMDP transformi idu oboje u jedan /Reference niz na vrednosti potpisa, svaki sa /Data koji 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 obrazaca 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 pogrešiti ako ovo rukama gradite. Katalog /Perms /DocMDP mora da referencira rečnik vrednosti potpisa, ne widget anotaciju, i pisac zadržava vrednost potpisa kao sopstveni indirektan objekat iz tog razloga. Postojeći rečnik /Perms možda već drži /UR3 prava korišćenja, pa ga pisac kopira i ubacuje /DocMDP umesto da ga zameni, prateći rečnik dozvola u ISO 32000-1 §12.8.4. Dokument koji već nosi /DocMDP odbija drugi sertifikacioni potpis sa EPadesCrypto, i nekonzistentne opcije takođe: Include ili Exclude zaključavanje bez imena polja, All zaključavanje sa listom polja, legalna attestacija na potpisu koji nije sertifikacioni, ili tačka u imenu korenskog polja. Remote potpisivanje dodaje još jedno pravilo, jer je potpisni sertifikat nepoznat kad PreparePadesRemoteSignature radi: postavljanje CertificateRequired tamo traži eksplicitnu listu AcceptableCertificates, dok lokalno potpisivanje može da se vrati na razrešeni sertifikat potpisoca

Analiza revizija dopunjuje alat za potpise umesto da zameni bilo koji njegov deo. Počnite sa pregledom PDF potpisa i PAdES nivoa sa PDFium Component-om da pročitate rečnik i bazni nivo, pogledajte zašto validatori odbijaju PAdES potpise za strukturne kvarove koji dolaze pre bilo kog pitanja revizija, i utkajte presudu u širu audit PDF sigurnosnih rizika uz provere JavaScript-a i ugrađenih fajlova. TPdf.AnalyzeSignatureRevisions, TPadesSignatureFieldOptions i inkrementalni PAdES pisac prikazani ovde isporučuju se uz PDFium Component za Delphi, C++Builder i Lazarus