Technický článek

DocMDP a FieldMDP: audit revizí PDF v Delphi

Podepsaný PDF, který se po podpisu změnil, není automaticky vadný. ISO 32000-1 povoluje přírůstkové aktualizace nad podpisem a jen některé z nich porušují zásadu, kterou podepisující nastavil. HotPDF Component pro Delphi a C++Builder na tuto otázku odpovídá pomocí AnalyzeLoadedSignatureRevisions, která klasifikuje každou revizi po podpisu a ohodnotí ji vůči DocMDP a FieldMDP. Scénář je známý každému, kdo dodává software pro smlouvy: váš zákazník podepíše kupní smlouvu, odešle ji a dostane ji zpět s přiloženou stránkou přílohy. Čtečka zobrazí žlutý pruh s tím, že podpis je neporušený, ale dokument byl od podpisu změněn, a nikdo v místnosti neumí říct, jde-li o běžný pracovní postup s protipodpisem, nebo o někoho, kdo potichu upravuje podepsanou smlouvu

Co se počítá jako legální změna po podpisu?

Změna je legální, spadá-li její sémantická kategorie do oprávnění, které deklaroval certifikující podpis. ISO 32000-1 §12.8.2.2 definuje transformaci DocMDP s hodnotou /P rovnou 1, 2 nebo 3: 1 nepovoluje žádné změny, 2 povoluje vyplňování formulářů a podepisování, 3 povoluje vyplňování formulářů, podepisování a anotace. HotPDF je vystavuje jako hodnoty THPDFDocMDPPermission dmpNoChanges, dmpFormFillAndSign a dmpFormFillSignAndAnnotate, přičemž dmpNone je vyhrazeno pro výsledky inspekce, které nenesou žádnou transformaci DocMDP

Kategorie jsou seřazené a toto řazení je motorem celé kontroly. THPDFRevisionModificationLevel obsahuje rmlNone, rmlLongTermValidation, rmlFormFillAndSign, rmlAnnotations, rmlOther, záměrně uspořádané tak, aby vyšší pořadové číslo nikdy neznamenalo menší omezení. Celý dokument se redukuje na maximální úroveň pozorovanou napříč všemi revizemi po podpisu a porovnání s DocMDP se stává jednoduchým celočíselným testem. Jedna nuance je důležitá už na začátku: na úrovni dmpNoChanges analýza stále akceptuje rmlLongTermValidation. Přidání validačního materiálu DSS a VRI nebo časového razítka dokumentu do certifikovaného souboru je údržbou podpisu, ne úpravou dokumentu, a považovat to za porušení by rozbilo úplně každý pracovní postup dlouhodobé archivace, jaký existuje

Jak HotPDF rekonstruuje řetěz revizí?

Strukturálně, ne heuristicky. Podle ISO 32000-1 §7.5.6 přírůstková aktualizace připojí novou cross-reference sekci, jejíž /Prev ukazuje na tu předchozí, takže HotPDF přečte startxref z konce souboru, zpracuje sekci na tomto místě, sleduje /Prev zpětně a opakuje, přičemž sekce vrací od nejstarší po nejnovější. V této smyčce jsou dva bezpečnostní limity a oba stojí za znalost při triáži souboru, který selže: /Prev ukazující na offset, který už byl navštíven, ukončí procházení s explicitní diagnostikou cyklu místo zacyklení, a řetěz delší než tisíc revizí je rovnou odmítnut. Oba se projeví v Analysis.Issue s funkcí vracející False, a ani jeden by neměl být přehlédnut, protože cyklický /Prev je poškozený nebo nepřátelský soubor, ne pouze neobvyklý

V reálných dokumentech se objevují čtyři historické tvary a všechny čtyři jsou podporovány: tradiční tabulky xref zpracované po řádcích, cross-reference streamy dekomprimované a dekódované přes svá pole /W a /Index, hybridní referenční soubory, jejichž tradiční trailer nese klíč /XRefStm, který se zpracuje a sloučí do stejné revize (případ producenta Office, popsaný v článku o hybridních cross-reference streamech), a objekty žijící uvnitř kontejneru ObjStm, které mají význam proto, že moderní aktualizace obvykle vloží změněný slovník do komprimovaného streamu místo přímého zápisu, jak popisuje článek o object streamech a přírůstkových aktualizacích. Podpis kotví rozdělení: /ByteRange[2] + /ByteRange[3] se stává SignedRevisionLength, a každá sekce na tomto offsetu nebo za ním je považována za revizi po podpisu. Zda se rozsah bajtů stále hashuje správně, je samostatná otázka, na niž odpovídá VerifyLoadedSignature, popsaná v článku o ověřování digitálních podpisů PDF

Jak se klasifikuje každý změněný objekt

Klasifikace probíhá pro každý objekt zvlášť a poté se šíří podél odkazů. Pro každé číslo objektu, kterého se sekce po podpisu dotkne, HotPDF přečte nové tělo a tělo tak, jak vypadalo v podepsaném snímku; identické tělo je rmlNone, protože producenti objekty někdy přepisují, aniž by je měnili. Rozpoznávače jsou záměrně úzké. Objekt /Type /DocTimeStamp, nebo takový, jehož /SubFilter je ETSI.RFC3161, je rmlLongTermValidation, stejně jako cokoli dosažitelné ze stromu /DSS katalogu; slovník /Type /Sig je rmlFormFillAndSign. U kontejnerů je testem to, které klíče se přesunuly, ne co objekt je: katalog smí pouze získat nebo změnit /DSS, /Extensions nebo /AcroForm; slovník AcroForm pouze /Fields, /SigFlags, /NeedAppearances, /DR, /DA nebo /Q; stránka pouze /Annots; pole nebo widget pouze /V, /AP, /AS nebo /M. Cokoli mimo tyto množiny klesá na rmlOther, což je přesně způsob, jak se odhalí připojená stránka přílohy: přidání stránky přeuspořádá strom stránek způsobem, který žádný seznam povolených klíčů nepokrývá, a žádné množství legitimního vyplňování formulářů se mu nepodobá

Poté se úrovně šíří, přičemž každý kontejner zdědí maximální úroveň změněných potomků, na které odkazuje, opakovaně, dokud se přiřazení neustálí. Právě to zajišťuje, že fungují appearance streamy. Vyplněné textové pole přepíše /V a odkáže na nový stream /AP, a tento stream sám o sobě je anonymní shluk obsahových operátorů bez typu, který by šlo rozpoznat; protože pole, které jej vlastní, je rmlFormFillAndSign, stream zdědí stejnou úroveň místo toho, aby propadl na rmlOther. Stejné šíření přenáší kontext DSS na streamy certifikátů a odvolání, které by jinak byly neklasifikovatelné

Proč se nečitelný objekt počítá jako porušení?

Protože alternativou je validátor poražený tím, že se do souboru zapíše něco, čemu nerozumí. Tři situace v HotPDF končí na rmlOther bez odvolání: objekt, jehož tělo se z revize nepodařilo přečíst, objekt, který revize označuje jako uvolněný, a objekt neodpovídající žádnému z výše uvedených rozpoznávačů. Každá zaznamená konkrétní diagnostiku do pole Issue revize, takže operátor vidí, které číslo objektu vedlo k danému verdiktu

Uvolnění je z těchto tří nejostřejší. Revize po podpisu, která označí dříve definovaný objekt jako volný, odstranila obsah z podepsaného dokumentu, a žádná úroveň oprávnění podle §12.8.2.2 to nepovoluje; čísla objektů se ocitnou v FreedObjectNumbers a revize se povýší na rmlOther. Nečitelné objekty se řídí stejnou logikou z jiného důvodu. Validátor, který objekt nedokáže zpracovat, nemá důvod prohlásit jej za neškodný, a poctivou reakcí na to není ticho. Nahlásit neobvyklý, ale neškodný konstrukt jako porušení stojí lidskou kontrolu; opačná chyba dodá podepsanou smlouvu s nepovšimnutou úpravou uvnitř

Čtení verdiktu v Delphi

Volání je krátké. Načtěte dokument, vyberte index podpisu, přečtěte záznam; přetížení bez parametrů znovu otevře soubor, ze kterého byl dokument načten, a přetížení s TStream přebírá bajty dodané volajícím a před návratem obnoví pozici streamu. PolicyCompliant je jediná booleovská hodnota, kterou většina volajících chce, a kombinuje tři nezávislá rozhodnutí: strukturální platnost slovníků oprávnění, DocMDPCompliant a FieldMDPCompliant. Ponechte tyto složky viditelné ve svém uživatelském rozhraní místo toho, abyste je sloučili, a mějte na paměti, že dokument bez transformace DocMDP ponechává DocMDPCompliant na True, protože běžný schvalovací podpis nedeklaruje žádnou zásadu, kterou by bylo možné porušit, a souhrnná hodnota ModificationLevel je pak popisná, ne verdikt

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;

Pro triáž obvykle chcete rozpad podle jednotlivých revizí místo souhrnu, protože ten vám řekne, kdy v historii dokumentu se něco pokazilo. Každá položka v Analysis.Revisions nese svůj index v řetězu, offset cross-reference, na kterém byla zapsána, vlastní úroveň modifikace a zúčastněná čísla objektů

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 se posuzuje samostatně, a je to záměr

Dokument může splňovat DocMDP a přesto být neoprávněný, a proto je FieldMDPCompliant samostatná booleovská hodnota místo toho, aby byla sloučena do porovnání úrovní. ISO 32000-1 §12.8.2.4 definuje transformaci FieldMDP a §12.7.5.5 související položku /SigFieldLock, které zmrazují pojmenovaná pole formuláře v okamžiku podpisu, i když dokument jako celek stále povoluje vyplňování formulářů. Vyplnění pole je akce úrovně 2; vyplnění pole, které podepisující uzamkl, je porušením bez ohledu na úroveň. HotPDF čte rozsah do THPDFFieldLockAction jako flaAll, flaInclude nebo flaExclude, s flaNone pro výsledky bez zásady uzamčení, a názvy do Permissions.FieldNames: flaAll uzamkne vše, flaInclude uzamkne vyjmenované názvy, flaExclude uzamkne vše kromě nich. Jeden detail je důležitý při čtení výsledků: v ChangedFieldNames se hlásí pouze pole, která byla přítomna už v podepsaném snímku, protože pole vytvořené zcela až po podpisu nemá žádný podepsaný stav, kterému by odporovalo, a zachytí je místo toho cesta 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;

Co vám tato analýza neřekne

Neověřuje podpis. AnalyzeLoadedSignatureRevisions uvažuje o struktuře a oprávněních; zda se podepsaný rozsah bajtů stále hashuje na hodnotu v CMS blobu a zda se certifikát podepisujícího řetězí k něčemu, čemu důvěřujete, na to odpovídají VerifyLoadedSignature a VerifyLoadedSignatureWithTrust. Soubor může být dokonale v souladu se zásadou a kryptograficky bezcenný zároveň, takže obě kontroly patří vedle sebe v každé skutečné akceptační bráně. Analýza také nečte záměr uvnitř content streamů: stránka, jejíž content stream byl kompletně nahrazen, se zachytí jako změna mimo seznam povolených klíčů, ale analýza vám neřekne, že náhrada vyměnila platební částku. Verdikt rmlOther znamená, že by se měl podívat člověk, ne že došlo k podvodu, a vyhovující verdikt znamená, že změna zapadá do povolené kategorie, ne že byla změna žádoucí. Pokud potřebujete jen to, co podepisující deklaroval, bez procházení revizí, GetLoadedSignaturePermissions vrací slovníky zásad samostatně

Vše zde popsané běží nativně v Delphi a C++Builder bez jakékoli externí podepisovací služby v cyklu, což z toho dělá praktický nástroj ke spuštění nad každým příchozím dokumentem, ne jen nad těmi, které už někdo podezříval. Kompletní API pro podpisy a revize, včetně metod pro oprávnění a ověřování, se kterými spolupracuje, je součástí komponenty HotPDF pro Delphi a C++Builder