Műszaki cikk

Aláírás utáni PDF-változás elemzés PDFiummal Delphiben

Hogy kiderítse, mi változott egy PDF-ben az aláírása után, a Delphi és Lazarus számára készült PDFium Component nyújtja a TPdf.AnalyzeSignatureRevisions-t, egy aláírás utáni revízió-változás elemzőt, amely minden inkrementális revíziót újraépít az eredeti fájl bájtjaiból, minden későbbi objektumváltozást az adott aláírás DocMDP és FieldMDP szabályaihoz mér, és az árnyékobjektum-definíciókat külön kockázatként jelenti. A helyzet, amelyet céloz, mindenki számára ismerős, aki szerződéseket kezel: egy hitelesített űrlap kimegy, két további inkrementális mentéssel jön vissza, és minden aláírás mégis ellenőrzésen átmegy. Ez elvárható, mert egy aláírás csak a saját revíziója bájtjait fedi le. A valódi kérdés az, hogy azok a későbbi mentések meg voltak-e engedve, és ezt a zöld pipa az aláíráson nem válaszolja meg

Miért nem tudja megmutatni a PDFium aláírás API-ja, mi változott az aláírás után?

A PDFium aláírás API-ja azért nem tudja megmutatni az aláírás utáni változásokat, mert csak az aláírási szótárt olvassa: /Contents, /ByteRange, /SubFilter és a DocMDP jogosultsági érték. A PDFiumnak nincs inkrementális revíziógráfja, nem elemzi a FieldMDP transzformparamétereit, és nem kínál objektumszintű diffet a revíziók között, így az FPdfPades.pas elemzője közvetlenül a nyers bájtokon dolgozik. Ebből gyakorlati következmény származik, amelyre tervezni kell. A TPdf.AnalyzeSignatureRevisions a dokumentum betöltésekor megtartott bájtokat olvassa, soha nem olyan másolatot, amelyet a SaveAs állított elő, mert egy újraírt fájl éppen azt a revízióstruktúrát vesztette el, amelyet elemezni kellene. Ha a dokumentum olyan progresszív forrásból jött, amely még nem fejezte be a letöltést, a jelentés SourceStatus = pvssIncomplete-ot és Status = prasIndeterminate-et ad csonka fájl elemzése helyett

Revízióhatárok újraépítése startxref-ből, xref streamekből és /Prev-ből

Az elemző a revízióhatárokat minden startxref nyomán építi újra, klasszikus xref táblákon, kereszthivatkozási streameken, hibrid /XRefStm bejegyzéseken és a /Prev láncon át, ahogy az ISO 32000-1 §7.5.6 és §7.5.8 definiálja az inkrementális frissítéseket. Minden aláírás lefedett hossza a második ByteRange szakaszának vége, és az elemző ezt a hosszt ahhoz a revízióhoz rendeli, amelynek xref szekciójába esik. Ha egyetlen revízió sem illeszkedik, az aláírás prrCoveredRevisionNotFound-ot és Indeterminate státuszt kap. Ezután minden objektum állapota a lefedett revízióig visszajátszásra kerül, és minden későbbi xref bejegyzést ahhoz az állapothoz hasonlítanak. Ez fontosabb, mint amilyennek hangzik: egyes írók minden inkrementális mentéskor újra leírják a teljes xref táblát, és egy változatlan objektumra továbbra is mutató bejegyzést átugranak ahelyett, hogy módosításként jelentenék. Ezen összehasonlítás nélkül egy tökéletesen legális űrlapkitöltés száznyi hamis változásban fulladna

Hogyan építi újra az AnalyzeSignatureRevisions az inkrementális revíziókat nyers PDF bájtokból Delphiben: az aláírás ByteRange-e a lefedett revízióban ér véget, az xref Prev lánc minden későbbi mentésen visszasétál, az objektumállapot a lefedett revízióig játszódik vissza, a változatlanul újramondott bejegyzések pedig átugródnak változásként jelentés helyett
Egy aláírás csak a saját revíziója bájtjait fedi le, ezért az elemző a második ByteRange szakaszt egy revízióhoz rendeli, és minden későbbi xref bejegyzést a visszajátszott objektumállapothoz mér

Az árnyékdefiníciók az az eset, amely a legtöbb figyelmet érdemli. Egy objektumtörzs, amely egy későbbi revízió bájttartományán belül jelenik meg, de arra a revízióra nem hivatkozik xref, egy normál nézegető elől láthatatlan, és pontosan az a fajta előkészítés, amelyre az árnyéktámadások építenek: rejtett tartalmat ültetnek az aláírás elé vagy mögé, majd egy hivatkozás megfordításával aktiválják. Az AnalyzePadesSignatureRevisionsBytes egy ilyen objektumot nem hitelesítő változásként rögzít, IsAuthoritative = False-szal, a jogosultsági szinttől függetlenül prdSuspicious-ra értékeli, és prrUnreferencedObjectDefinition-t fűz a kockázatkészlethez. Két rokon kockázat fedi le a többi szerkezeti fogást: a prrDuplicateObjectDefinition akkor sül el, ha egy xref szekció ugyanazt az objektumot egynél többször sorolja fel, a prrSignatureObjectRedefined pedig akkor, ha egy későbbi revízió újradefiniál egy meglévő aláírási objektumot

Egy árnyékobjektum-definíció egy későbbi PDF revízió bájttartományán belül: az objektumtörzs létezik, de egyetlen xref bejegyzés sem hivatkozik rá, tehát a nézegetők soha nem mutatják, az AnalyzeSignatureRevisions a PDFium Component-ben nem hitelesítőként rögzíti, prdSuspicious-ra értékeli, és prrUnreferencedObjectDefinition-t vet fel a duplikált és újradefiniált aláírási kockázatok mellett
A rejtett tartalmat az aláírás elé vagy mögé ültetik, és később egy hivatkozás megfordításával aktiválják, ezért egy hivatkozatlan törzs a DocMDP jogosultsági szintjétől függetlenül gyanúsként értékelődik
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;

Hogyan érvényesül a DocMDP és FieldMDP aláírásonként?

A DocMDP és a FieldMDP minden aláírásnál külön érvényesül, az adott aláírás saját lefedett revízióján, tehát egy hitelesítő aláírás és egy későbbi jóváhagyó aláírás ugyanabban a fájlban eltérő verdiktet adhat ugyanarról a szerkesztésről. Minden későbbi objektumot előbb egy TPadesRevisionChangeKind-ba sorolnak a /Type, /Subtype és /FT bejegyzéseiből, valamint abból a szerepből, amelyet az oldal, űrlap, annotáció és DSS gráfokban játszik. Minden, ami /JavaScript-et, /JS-t, /Launch-t, /OpenAction-t, /AA-t, /RichMedia-t vagy /EmbeddedFile-t hordoz, prckActiveContent- lesz. A döntés ezután az ISO 32000-1 §12.8.2.2-t követi: P=1-nél minden tiltott, a kereszthivatkozási adatok és az érvényesítő anyag kivételével; a P=2 engedi az űrlapkitöltést és a további aláírásokat, de elutasítja az annotációváltozásokat; a P=3 az annotációkat is engedi. Az oldaltartalom, a dokumentumszerkezet, a metaadat, az aktív tartalom és a törölt objektumok bármely DocMDP szint alatt tiltottak, és prdSuspicious-ra értékelődnek, amikor az aláírás egyáltalán nem hordoz DocMDP-t, hiszen egy jóváhagyó aláírás formálisan semmit sem tilt, de az olvasó már nem látja, mi volt aláírva

A FieldMDP, az ISO 32000-1 §12.8.2.4 alapján, tovább szűkíti az űrlapmező-döntést. A pfmaAll minden mezőt zárol, a pfmaInclude csak a felsorolt mezőket, a pfmaExclude pedig mindent, a felsoroltak kivételével. Az Include vagy Exclude alkalmazásához az elemző minden megváltozott mezőt a /Parent láncon át a teljesen minősített nevére old fel, és pontos egyezéssel hasonlítja a zárolási listához, tehát a listában terminál mezőneveket soroljon fel, ne várja el, hogy egy szülőnév lefedje a gyerekeit. Ha egy név nem oldható fel, vagy a transzformáció olyan műveletet használ, amelyet az elemző nem ismer, a változás prdIndeterminate lesz, és prrFieldMdpUnresolved vetődik fel. A változásonkénti döntések ezután a legrosszabbat előre összegződnek, a Suspicious a Disallowed fölött, a Disallowed az Indeterminate fölött, az Indeterminate az Allowed fölött rangsorol, tehát egyetlen árnyékobjektum nehezebb bármennyi legitim mezőfrissítésnél

Az értékelési folyam, amelyet az AnalyzeSignatureRevisions minden aláírás utáni változásra alkalmaz Delphiben: TPadesRevisionChangeKind a Type és Subtype bejegyzésekből, DocMDP döntés a lefedett revízión P=1-től P=3-ig, FieldMDP zárolásellenőrzés teljesen minősített mezőneveken, és legrosszabbat-előre összegzés a prdSuspicious-tól a prdAllowed-ig
Egyetlen árnyékobjektum nehezebb bármennyi legitim mezőfrissítésnél, mert a Suspicious a Disallowed, Indeterminate és Allowed fölött rangsorol, egyes kockázatok pedig a státusz mellett rögzülnek, anélkül hogy lefokoznák

Miért tér vissza néhány változás Indeterminate-tel a biztonságos helyett?

A változások azért térnek vissza Indeterminate-tel, valahányszor az elemző nem tudja bizonyítani, hogy egy változás megengedett, mert egy aláírásellenőrzésben az ismeretlent soha nem szabad engedélyezettként jelenteni. Egy gyakori esetet viszont pontosan kezelnek: a hosszú távú érvényesítés hozzáad egy /DSS-t, és újraírja a katalógust, ami egyébként szerkezeti változásként számolna P=1 alatt. Az elemző lehántja a /DSS-t és a /Extensions-t a régi és új katalógusszótárakról, és a maradékot hasonlítja; ha más sem tér el, az újraírás érvényesítő anyag frissítéseként kezelődik és engedélyezett, tehát a B-LT és B-LTA bővítés nem töri meg a hitelesítő aláírást. Más hézagok szándékosan nyitva maradnak. A kereszthivatkozási stream 2-es típusú bejegyzései tömörített objektumstreamekbe mutatnak, és az elemző e biztonsági határon belül nem bont ki objektumstreameket, így ezek a változások prckCompressedObject-ként bukkannak fel prrCompressedObjectUnresolved-del, P=1 alatt tiltottként, egyébként Indeterminate-ként. A 1024 revíziós, 1 000 000 objektumszámú és 2 000 000 jelentett változásos kemény költségvetések prrResourceLimitExceeded-et adnak, egy törött xref lánc pedig prrMalformedRevisionChain-et; mindkettő Indeterminate-ként végződik, soha nem átmenetként

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

function RevisionVerdict(const R: TPadesRevisionAnalysisReport): string;
begin
  // Egyes kockázatok a Status változtatása nélkül rögzülnek, ezért először ezeket tesztelje
  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;

A sorrend abban a kapuban szándékos. A prrDuplicateObjectDefinition a kockázatkészlethez fűződik anélkül, hogy önmagában lefokozná a Status-t, és egy elemezhetetlen FieldMDP transzform csak akkor érinti a státuszt, ha egy űrlapmező ténylegesen változik, tehát egy csak a Status-ra néző kapu lemaradhat olyan bizonyítékról, amelyet a jelentés már tartalmaz. Tartsa észben azt is, mit nem állít a jelentés. A TPadesRevisionAnalysisReport nem szól arról, hogy a CMS aláírás kriptográfiailag érvényes-e, vagy hogy az aláíró tanúsítvány egy Ön által bízott gyökérhez láncolódik-e. A revízióelemzés arra a kérdésre válaszol, mi történt az aláírás után, és a szerkezeti és bizalmi validáció mellé való, nem a helyébe

Seed értékek és MDP zárolások írása aláíráskor

Ugyanezek a szabályok már aláíráskor is megírhatók a TPadesSignatureFieldOptions útján, amely mind a TPadesSignOptions, mind a TPadesRemoteSignOptions FieldOptions tagja. A PDFium widgetet tud létrehozni, de nem tud /SV-t, /Lock-ot, FieldMDP-t vagy DocMDP transzformot, sem a katalógus /Perms szótárát írni, ezért a komponens saját inkrementális PAdES írója ugyanabban az xref frissítésben állítja elő ezeket az objektumokat, mint az aláírást. A FieldName beállítja a gyökérmező nevét, a RequiredSeedValues az ISO 32000-1 §12.7.4.5-ben leírt seed-érték szótár /Ff bitjeivé válik, a Reasons, LegalAttestations és AcceptableCertificates korlátozzák, mit választhat egy későbbi aláíró, a LockAction a LockFields-szel közvetett /SigFieldLock-ot ír, a CertificationPermission pedig 1-től 3-ig hitelesítő aláírássá teszi az aláírást. A DocMDP és FieldMDP transzformok egyaránt az aláírási érték egyetlen /Reference tömbjébe kerülnek, mindegyik /Data-ja a katalógusra mutat

var
  Options: TPadesSignOptions;
begin
  Options := TPadesSignOptions.Default;
  Options.CertificateThumbprint := 'A1B2C3D4E5F60718293A4B5C6D7E8F9012345678';
  Options.Reason := 'Approved for release';
  Options.FieldOptions.FieldName := 'Certification';
  Options.FieldOptions.CertificationPermission := 2;   // csak űrlapkitöltés és aláírás
  Options.FieldOptions.RequiredSeedValues := [psvcSubFilter, psvcDigestMethod];
  Options.FieldOptions.LockAction := pfmaInclude;      // csak ezeket a mezőket zárolja
  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;

Néhány részleten könnyű elbukni, ha ezt kézzel gördíti. A katalógus /Perms /DocMDP-jének az aláírási érték szótárra kell hivatkoznia, nem a widget annotációra, és az író emiatt az aláírási értéket saját közvetett objektumként tartja meg. Egy meglévő /Perms szótár már hordozhat /UR3 használati jogokat, tehát az író lemásolja, és /DocMDP-t szúr be a csere helyett, az ISO 32000-1 §12.8.4 jogosultsági szótárát követve. Egy, már /DocMDP-t hordozó dokumentum második hitelesítő aláírást tagad meg EPadesCrypto-val, ugyanúgy az ellentmondásos opciók is: egy Include vagy Exclude zárolás mezőnevek nélkül, egy All zárolás mezőlistával, jogi nyilatkozat nem hitelesítő aláíráson, vagy pont a gyökérmező nevében. A távoli aláírás még egy szabályt ad, mert az aláíró tanúsítvány ismeretlen, amikor a PreparePadesRemoteSignature fut: ott a CertificateRequired beállítása explicit AcceptableCertificates listát követel, míg a helyi aláírás visszaeshet a feloldott aláíró tanúsítványra

A revízióelemzés az aláírási eszközkészletet egészíti ki, nem helyettesíti egyik részét sem. Kezdje a PDF aláírások és PAdES szintek átvizsgálásával a PDFium Component-tel, hogy olvassa a szótárt és az alapszintet, nézze meg a miért utasítják el a validátorok a PAdES aláírásokat a szerkezeti hibákért, amelyek bármely revíziókérdés előtt jönnek, és fésülje a verdiktet egy tágabb PDF biztonsági kockázat auditba a JavaScript- és beágyazottfájl-ellenőrzések mellé. A TPdf.AnalyzeSignatureRevisions, a TPadesSignatureFieldOptions és az itt bemutatott inkrementális PAdES író a PDFium Component csomagban érkezik Delphihez, C++Builderhez és Lazarushoz