Technický článek

FieldMDP v Delphi: detekce úprav zamčených polí formuláře

PDFlibPas odpovídá na otázku FieldMDP přes TPDFlib.GetSignatureModificationVerdict. Když metoda dostane podepsané PDF, jeho heslo a jméno podpisového pole, projde každý inkrementální update přidaný za tímto podpisem, vyřeší plně kvalifikované jméno každého formulářového pole, kterého se tyto updaty dotkly, a vrátí 3, když se změnilo pole zamčené FieldMDP, a 2, když jen pole odemčená. GetSignatureModificationDetail pak pojmenuje revizi a pole

Pracovní postup, který to potřebuje, je formulář s více schvalovateli. Žadatel vyplní Order.Customer a Order.Total v nákupní žádosti a podepíše Approval.Requester, čímž ta dvě pole zamkne. Manažer doplní nákladové středisko a podepíše Approval.Manager. O dva týdny později někdo otevře soubor v desktopovém editoru a ve třetím inkrementálním updatu Order.Total přehoupne nahoru. Každý podpis pořád projde ověřením, protože každý pokrývá jen svůj vlastní byte rozsah. Otázky, které auditor skutečně ptá, zní: které pole se změnilo po kterém podpisu a zda byla ta změna povolená

Proč samo DocMDP neodhalí upravené zamčené pole?

DocMDP to neodhalí, protože DocMDP klasifikuje druhy změn, ne jednotlivá pole. Úrovně certifikačních oprávnění v ISO 32000-1 §12.8.2.2 říkají, zda pozdější updaty můžou vyplňovat formuláře a podepisovat (P 2), nebo i anotovat (P 3), a to oprávnění platí pro všechna pole v dokumentu stejně. Úprava Order.Total je vyplňování formuláře, takže certifikace P 2 je splněna právě tou úpravou, kterou žadatel chtěl zakázat. Klasifikace úprav na úrovni dokumentu v PDFlibPas pokrývá tuhle vrstvu a tenhle článek jde o stupeň jemněji

FieldMDP je vrstva s granularitou na úrovni pole. Podpisové pole může nést slovník /Lock (ISO 32000-1 §12.7.4.5) a podpis, který drží, může ve své referenci nést FieldMDP transformaci (ISO 32000-1 §12.8.2.4). Obojí používá tutéž slovní zásobu: /Action /All zamkne všechna pole, /Action /Include zamkne jména uvedená v /Fields a /Action /Exclude zamkne vše kromě jmen v /Fields. Před v3.586.0 věděl GetSignatureModificationVerdict, že pozdější revize změnila nějaké formulářové pole, ale ne které, takže každý dokument se zámkem pole, jehož pozdější updaty přesáhly validační materiál, se vrátil jako 4, nerozhodnuto. Mapa revizí teď zaznamenává vyřešené jméno pole pro každý objekt související s polem, který klasifikuje, a právě to proměňuje onu 4 ve skutečnou 2 nebo 3

Zápis zámku pole při podpisu každého schvalovatele

TPDFlib.SetSignProcessFieldLock nastavuje zámek pro další podpis, který podpisový proces vytvoří. Akce 1 je All, 2 je Include, 3 je Exclude a 0 zámek smaže; Include a Exclude berou seznam plně kvalifikovaných jmen polí oddělených středníky. Podepsané pole pak nese slovník /Lock a odpovídající FieldMDP transformaci — přesně ten pár, který očekávají přísné validátory

Akční kódy PDFlibPas SetSignProcessFieldLock pro FieldMDP podpisování v Delphi: akce 1 All zamkne každé formulářové pole včetně pozdějších podpisových polí, akce 2 Include zamkne jmenovaná datová pole, akce 3 Exclude zamkne vše kromě seznamu a akce 0 zámek smaže
Pro datová pole, která vlastní jednotliví schvalovatelé, volte Include, nebo Exclude se seznamem pozdějších podpisových polí; odmítnuté volání vrátí 0 a předchozí zámek zůstává stát, takže vždy kontrolujte návratovou hodnotu
var
  Pdf: TPDFlib;
  SignId: Integer;
begin
  Pdf := TPDFlib.Create;
  try
    SignId := Pdf.NewSignProcessFromFile('purchase-request.pdf', '');
    if SignId = 0 then
      raise Exception.Create('cannot open the form');
    try
      Pdf.SetSignProcessField(SignId, 'Approval.Requester');
      Pdf.SetSignProcessPFXFromFile(SignId, 'requester.pfx', PfxPassword);
      // 2 = Include: tímto podpisem se zamknou jen tato pole
      if Pdf.SetSignProcessFieldLock(SignId, 2,
        'Order.Customer;Order.Total') <> 1 then
        raise Exception.Create('field lock rejected');
      Pdf.EndSignProcessToFile(SignId, 'purchase-request-signed.pdf');
      if Pdf.GetSignProcessResult(SignId) <> 1 then
        raise Exception.CreateFmt('signing failed, code %d',
          [Pdf.GetSignProcessResult(SignId)]);
    finally
      Pdf.ReleaseSignProcess(SignId);
    end;
  finally
    Pdf.Free;
  end;
end;

SetSignProcessFieldLock vrátí 0 pro volání Include nebo Exclude bez jmen a také pro seznam obsahující prázdnou položku, třeba 'Order.Customer;;Order.Total'; odmítnuté volání nechá dříve nastavený zámek v platnosti, takže kontrolujte návratovou hodnotu, místo abyste předpokládali, že poslední volání vyhrálo. Zbytek nastavení podpisu — volbu digestu, rezervaci /Contents a result kódy — popisuje průvodce PAdES podpisem a validací pro PDFlibPas. Jeden designový bod se týká jen schvalovacích řetězců: zámek /All na podpisu žadatele zamkne i Approval.Manager, protože podpisové pole je taky formulářové pole. Pro datová pole, která vlastní jednotliví schvalovatelé, použijte Include, nebo Exclude se seznamem pozdějších podpisových polí

Verdikt napříč schvalovacím řetězcem

GetSignatureModificationVerdict soudí jeden podpis najednou, takže schvalovací řetězec se kontroluje zavoláním jednou za každé podpisové pole. Návratová hodnota je malá uzavřená množina a každá hodnota pro kontrolora znamená něco jiného:

  • 0: soubor nebo jmenované podpisové pole se nepodařilo přečíst
  • 1: za podpisem se nic nepřidalo
  • 2: přidaly se jen změny, které efektivní oprávnění dovolují, a zamčeného pole se nic nedotklo
  • 3: pozdější revize udělala změnu, která není povolená, včetně úpravy pole, které FieldMDP zamyká
  • 4: odpověď se nepodařilo určit, což se nesmí nikdy číst jako schválení
  • 5: podpis nekončí na hranici revize, takže bajty leží uvnitř rozsahu, který tvrdí, že pokrývá
const
  Approvals: array[0..2] of WideString =
    ('Approval.Requester', 'Approval.Manager', 'Approval.Finance');
var
  Pdf: TPDFlib;
  I, Verdict: Integer;
begin
  Pdf := TPDFlib.Create;
  try
    for I := Low(Approvals) to High(Approvals) do
    begin
      Verdict := Pdf.GetSignatureModificationVerdict(
        'purchase-request-final.pdf', FormPassword, Approvals[I]);
      // oba gettery popisují volání, které právě proběhlo, takže je čtěte před dalším voláním
      Log.Add(Format('%s: verdict %d, MDP level %d, %s',
        [Approvals[I], Verdict, Pdf.GetSignatureEffectiveMDPLevel,
         Pdf.GetSignatureModificationDetail]));
      case Verdict of
        1, 2: ;                                 // přijato
        3: RejectForm(Approvals[I]);            // detail pojmenuje pole a revizi
      else
        QueueForManualReview(Approvals[I]);     // 0, 4, 5
      end;
    end;
  finally
    Pdf.Free;
  end;
end;

Vezměte si manipulovanou nákupní žádost, kde revize 0 je prázdný formulář, revize 1 a 2 jsou oba podpisy a revize 3 je ta úprava. Protože definice zámků se sbírají ze všech podepsaných polí, verdikt pro Approval.Manager už vynucuje zámek žadatele a jeho jediným pozdějším updatem je sama úprava. Když tento update přepsal jen slovník pole, verdikt je 3 a detail zní Revision 3 changed locked form field "Order.Total", which FieldMDP does not allow. Ta jediná řádka odpovídá na obě auditní otázky. Verdikt pro Approval.Requester musí nejdřív projít kolem revize s podpisem manažera, a z důvodů z oddílu o omezeních níže často nahlásí 4, takže nejnovější podpis obvykle dává nejostřejší odpověď. GetSignatureEffectiveMDPLevel hlásí nejnižší hodnotu DocMDP P nalezenou mezi podpisy, nebo 3, když dokument nic necertifiovalo, a oba gettery zůstávají platné jen do dalšího volání verdiktu

Časová osa inkrementálních updatů manipulovaného formuláře PDF v Delphi čteného přes PDFlibPas GetSignatureModificationVerdict: revize 1 nese Include zámek na Order.Customer a Order.Total, revize 2 podepisuje Approval.Manager a přepsání zamčeného pole Order.Total v revizi 3 dá verdikt 3 pro podpis manažera a 4 pro podpis žadatele
Zámky se sbírají ze všech podepsaných polí, takže verdikt pro Approval.Manager už vynucuje zámek žadatele a detailní řetězec pojmenuje provinilé pole a revizi, zatímco nejstarší podpis často skončí na 4

Jak PDFlibPas pozná, které pole revize změnila?

PDFlibPas rekonstruuje jméno pole z grafu objektů každé revize, protože změněný objekt nosí své celé jméno jen zřídka. Slovník pole s /FT nebo /V, stejně jako widget anotace, drží své částečné jméno jen v /T. Plně kvalifikované jméno popsané v ISO 32000-1 §12.7.3.2 je řetěz částečných jmen od kořene, spojených tečkami, takže mapa revizí stoupá nahoru po odkazech /Parent a každé nalezené /T předřadí. Každý rodič se čte ze své nejnovější definice platné k klasifikované revizi nebo starší, což zabrání tomu, aby pozdější přejmenování rodičovského uzlu přepsalo historii dřívějších úprav

Řešení plně kvalifikovaného jména FieldMDP v PDFlibPas pro Delphi: mapa revizí stoupá po odkazech /Parent nahoru ze změněného widgetu, předřazuje každou nalezenou částečnou část /T spojenou tečkami, až složí Order.Total, a porovnává ji s položkami /Fields přesnou rovností nebo prefixem s tečkou, takže zámek na Order pokryje každého potomka
Změněný objekt nosí své celé jméno jen zřídka: rodiče se čtou ze své nejnovější definice k klasifikované revizi nebo starší, jména se porovnávají včetně velikosti písmen a chůze končí po osmi úrovních

Porovnávání vyřešeného jména se zámky je záměrně jednoduché. Jméno odpovídá položce v /Fields, když se jí rovná přesně, nebo když začíná položkou následovanou tečkou, takže zámek na Order zamkne i Order.Total a každého dalšího potomka. All zamyká všechna jména, Include zamyká odpovídající jména a Exclude zamyká každé jméno, které neodpovídá. Definice zámků přicházejí ze všech podepsaných podpisových polí v souboru; když má pole obojí, slovník /Lock i FieldMDP transformaci, použijí se hodnoty z /Lock. Abychom přesně viděli, co který podpis zamkl, nižší třída TPDFlibSignDoc z unitu PDFlibDigitalSign vystavuje report oprávnění, který verdikt čte:

uses
  PDFlibDigitalSign;

var
  SignDoc: TPDFlibSignDoc;
  Report: TPDFlibSignaturePermissionReport;
  Names: TStringList;
  I, J: Integer;
  Line: string;
begin
  SignDoc := TPDFlibSignDoc.Create;
  Names := TStringList.Create;
  try
    if not SignDoc.Open('purchase-request-final.pdf', FormPassword, False) then
      Exit;
    try
      SignDoc.GetSignatureFieldNames(Names);
      for I := 0 to Names.Count - 1 do
      begin
        SignDoc.GetSignaturePermissionInfoByName(Names[I], Report);
        if not (Report.HasFieldLock or Report.HasFieldMDP) then
          Continue;
        Line := Names[I] + ': ' + string(Report.FieldLockAction);
        for J := 0 to High(Report.FieldNames) do
          Line := Line + ' ' + string(Report.FieldNames[J]);
        if Report.HasFieldLock and Report.HasFieldMDP and
          not Report.FieldMDPMatches then
          Line := Line + ' (lock and FieldMDP transform disagree)';
        Log.Add(Line);
      end;
    finally
      SignDoc.Close;
    end;
  finally
    Names.Free;
    SignDoc.Free;
  end;
end;

Šifrované formuláře: předejte heslo, jinak jsou jména nečitelná

Šifrovaný formulář se musí nejdřív dešifrovat, než se dá vyřešit jakékoli jméno pole, takže GetSignatureModificationVerdict potřebuje argument Password k něčemu víc, než jen k otevření souboru. Řetězce jako /T jsou šifrované po objektech a pole zapsaná do komprimovaných object streamů se nedají přečíst vůbec, dokud se obsahující stream nedešifruje a nerozbalí. Od v3.586.0 interní mapa revizí bere heslo, připojuje dešifrovací kontext, když parsuje object streamy a revizní slovníky, a dešifruje každý objekt, který dekóduje, včetně dřívějších verzí, vůči kterým dělá diff, a rodičů, po kterých stoupá

Dešifrování po objektech v mapě revizí se zapne jen tehdy, když Password není prázdné. Verdikt spočtený na šifrovaném souboru s prázdným heslem je proto potřeba brát jako neověřený, i když se dokument sám otevřel bez dotazu. Uložte heslo formuláře vedle záznamu pracovního postupu a předávejte ho při každém volání

Co verdikt FieldMDP nepokrývá

Kontrola polí je strukturální odpověď na jednu úzkou otázku a několik případů leží mimo ni. Znalost těchto případů předem je to, co brání tomu, aby se ze 4 stala eskalace na podporu:

  • Srovnávání polí běží jen poté, co kontrola na úrovni dokumentu vyšla čistě a alespoň jedna pozdější revize změnila víc než validační materiál nebo časová razítka; porušení DocMDP se nahlásí jako 3, než se na jakékoli pole podívá
  • Objekty klasifikované jako podpisové, ale bez jména pole — třeba slovník hodnoty podpisu, přepsaný slovník AcroForm nebo změna katalogu omezená na /AcroForm nebo /Perms — donutí verdikt vrátit 4, místo aby hádal, takže revize přidávající pozdější schvalovací podpis pod aktivním zámkem často skončí na 4
  • Jméno pole dostávají jen slovníky polí a widget anotace; mapa revizí nemá žádnou dedikovanou třídu pro appearance streamy, takže editor, který regeneruje vzhledy spolu s hodnotou, může kontrola na úrovni dokumentu zastavit jako změnu obsahu dřív, než se kontrola polí spustí, a detailní řetězec oba případy odliší
  • Sken se zastaví u prvního zamčeného pole nebo nepojmenované položky, kterou potká, takže 3 pojmenuje jedno provinilé pole, ne všechny
  • Zámky se sbírají ze všech podepsaných podpisových polí bez řazení v čase, takže zámek napsaný pozdějším podepisujícím se aplikuje i na updaty, které přišly před ním; to může výsledek jen tlačit k 3, nikdy k 2
  • Jména se porovnávají přesně a včetně velikosti písmen a chůze po /Parent končí po osmi úrovních, takže neobvykle hluboká hierarchie se vyřeší na zkrácené jméno
  • Položka FieldMDP, jejíž struktura neprojde validací a jejíž akci se nepodaří přečíst, donutí verdikt vrátit 4

Když verdikt přijde jako 4, GetSignatureRevisionIndex dá revizi, na které podpis končí, a GetDocumentRevisionChanges vypíše každý objekt, který každá pozdější revize přidala, změnila nebo uvolnila, včetně jeho změnové třídy. To člověku stačí na rozhodnutí během minuty. Mechaniku, jak se tyto revize přidávají, popisuje průvodce inkrementálním updatem pro PDFlibPas

FAQ

Znamená verdikt 2, že podpisy jsou platné?

Ne. GetSignatureModificationVerdict zkoumá, co se přidalo za podpisem, a nikdy nekontroluje hodnotu CMS podpisu, řetěz certifikátů ani revokaci. Dvojka říká, že pozdější updaty zůstaly v rámci oprávnění; kryptografická validace je samostatný krok a potřeba jsou obojí

Proč podpis druhého schvalovatele dá 4 místo 2?

Podepsání přidá slovník hodnoty podpisu a obvykle přepíše formulářovou agendu jako slovník AcroForm. Ty objekty nepatří žádnému pojmenovanému poli, a když je zámek pole aktivní, verdikt odmítne schválit změnu, kterou nedokáže přisoudit poli. Použijte GetDocumentRevisionChanges, abyste potvrdili, že revize obsahuje jen očekávané podpisové objekty

Může jedno jméno zamknout celou skupinu polí?

Ano. Položka zámku odpovídá pojmenovanému poli a každému potomkovi, jehož plně kvalifikované jméno začíná tímto jménem následovaným tečkou, takže uvedení Order v zámku Include zamkne Order.Customer, Order.Total a jakékoli další pole pod tímto rodičem

Verdikty FieldMDP, podpisování se zámky polí i prohlížení revizí dodává PDFlibPas Delphi PDF Library, spolu s certifikací DocMDP a dlouhodobou validací PAdES, na kterých se výše uvedený schvalovací postup spoléhá