Technisch artikel

FieldMDP in Delphi: gewijzigde vergrendelde velden opsporen

PDFlibPas beantwoordt de FieldMDP-vraag via TPDFlib.GetSignatureModificationVerdict. Gegeven een ondertekende PDF, diens wachtwoord en een handtekeningveld loopt de methode elke incrementele update na die na die handtekening is toegevoegd, lost de volledig gekwalificeerde naam op van elk formulierveld dat die updates hebben aangeraakt, en geeft 3 terug wanneer een door FieldMDP vergrendeld veld is gewijzigd en 2 wanneer alleen ontgrendelde velden werden aangepast. GetSignatureModificationDetail noemt er vervolgens de revisie en het veld bij

De workflow die dit nodig heeft is het formulier met meerdere goedkeurders. Een aanvrager vult Order.Customer en Order.Total op een inkoopverzoek in en tekent Approval.Requester, waarmee die twee velden vergrendeld zijn. Een manager voegt een kostenplaats toe en tekent Approval.Manager. Twee weken later opent iemand het bestand in een desktopeditor en schuift Order.Total omhoog in een derde incrementele update. Elke handtekening verifieert nog steeds, omdat elke er alleen zijn eigen bytebereik dekt. De vragen die een auditor echt stelt zijn welk veld na welke handtekening veranderde, en of die verandering was toegestaan

Waarom vangt DocMDP alleen een gewijzigd vergrendeld veld niet?

DocMDP vangt het niet, omdat DocMDP soorten veranderingen classificeert en geen individuele velden. De certificeringspermissieniveaus in ISO 32000-1 §12.8.2.2 zeggen of latere updates formulieren mogen invullen en ondertekenen (P 2) of ook mogen annoteren (P 3), en die permissie geldt voor elk veld in het document even sterk. Het wijzigen van Order.Total is formuliervelden invullen, dus een P 2-certificering wordt precies voldaan door die ene bewerking die de aanvrager verbieden wilde. De documentniveauclassificatie van wijzigingen in PDFlibPas dekt die laag, en dit artikel gaat één stap fijner

FieldMDP is de laag op veldniveau. Een handtekeningveld kan een /Lock-dictionary meedragen (ISO 32000-1 §12.7.4.5), en de handtekening die het bevat kan een FieldMDP-transform meedragen in zijn signature reference (ISO 32000-1 §12.8.2.4). Beide gebruiken dezelfde woordenschat: /Action /All vergrendelt elk veld, /Action /Include vergrendelt de namen die in /Fields staan, en /Action /Exclude vergrendelt alles behalve de namen in /Fields. Vóór v3.586.0 wist GetSignatureModificationVerdict wel dat een latere revisie een formulierveld had gewijzigd maar niet welke, dus elk document met een veldvergrendeling waarvan de latere updates verder gingen dan validatiemateriaal kwam terug als 4, onbepaald. De revisiekaart legt nu een opgeloste veldnaam vast voor elk veldgerelateerd object dat hij classificeert, en precies dat maakt van die 4 een echte 2 of 3

De veldvergrendeling schrijven wanneer elke goedkeurder tekent

TPDFlib.SetSignProcessFieldLock zet de vergrendeling voor de volgende handtekening die een ondertekenproces oplevert. Action 1 is All, 2 is Include, 3 is Exclude en 0 wist de vergrendeling; Include en Exclude nemen een lijst van volledig gekwalificeerde veldnamen, gescheiden door puntkomma's. Het ondertekende veld krijgt zo een /Lock-dictionary en een matchende FieldMDP-transform, precies het paar dat strenge validators verwachten

PDFlibPas SetSignProcessFieldLock-actioncodes voor Delphi FieldMDP-ondertekening: action 1 All vergrendelt elk formulierveld inclusief latere handtekeningvelden, action 2 Include vergrendelt de benoemde gegevensvelden, action 3 Exclude vergrendelt alles behalve de lijst, en action 0 wist de vergrendeling
Kies Include voor de gegevensvelden die elke goedkeurder bezit of Exclude met de latere handtekeningvelden erop vermeld; een geweigerde aanroep geeft 0 terug en laat de vorige vergrendeling staan, dus controleer altijd de retourwaarde
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: alleen deze velden worden door deze handtekening bevroren
      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 geeft 0 terug voor een Include- of Exclude-aanroep zonder namen, en ook voor een lijst met een lege entry zoals 'Order.Customer;;Order.Total'; een geweigerde aanroep laat de eerder ingestelde vergrendeling staan, dus controleer de retourwaarde in plaats van ervan uit te gaan dat de laatste aanroep won. De rest van de ondertekenopzet, de keuze van de digest, de /Contents-reservering en de resultaatcodes, staat in de PAdES-gids voor ondertekening en validatie van PDFlibPas. Eén ontwerppunt is specifiek voor goedkeuringsketens: een /All-vergrendeling op de handtekening van de aanvrager bevriest ook Approval.Manager, want een handtekeningveld is ook een formulierveld. Gebruik Include voor de gegevensvelden die elke goedkeurder bezit, of Exclude met de latere handtekeningvelden erop vermeld

De uitspraak over een goedkeuringsketen heen laten lopen

GetSignatureModificationVerdict beoordeelt één handtekening per keer, dus een goedkeuringsketen wordt gecontroleerd door hem één keer per handtekeningveld aan te roepen. De retourwaarde is een kleine, gesloten verzameling en elke waarde betekent iets anders voor een reviewer:

  • 0: het bestand of het genoemde handtekeningveld was niet leesbaar
  • 1: na de handtekening is niets toegevoegd
  • 2: er zijn alleen veranderingen toegevoegd die de effectieve permissies toestaan, en er is geen vergrendeld veld aangeraakt
  • 3: een latere revisie bracht een niet toegestane verandering aan, waaronder een bewerking van een veld dat FieldMDP vergrendelt
  • 4: het antwoord kon niet worden vastgesteld, wat nooit als goedkeuring mag worden gelezen
  • 5: de handtekening eindigt niet op een revisiegrens, dus er zitten bytes binnen het bereik waarvan hij beweert dat hij dat dekt
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]);
      // beide getters beschrijven de zojuist gedane aanroep, dus lees ze vóór de volgende
      Log.Add(Format('%s: verdict %d, MDP level %d, %s',
        [Approvals[I], Verdict, Pdf.GetSignatureEffectiveMDPLevel,
         Pdf.GetSignatureModificationDetail]));
      case Verdict of
        1, 2: ;                                 // geaccepteerd
        3: RejectForm(Approvals[I]);            // detail noemt veld en revisie
      else
        QueueForManualReview(Approvals[I]);     // 0, 4, 5
      end;
    end;
  finally
    Pdf.Free;
  end;
end;

Neem het gemanipuleerde inkoopverzoek, waarin revisie 0 het lege formulier is, revisies 1 en 2 de twee handtekeningen zijn en revisie 3 de bewerking is. Omdat de vergrendelingsdefinities uit elk ondertekend veld worden gebundeld, handhaaft de uitspraak voor Approval.Manager al de vergrendeling van de aanvrager, en zijn enige latere update is de bewerking zelf. Wanneer die update alleen de velddictionary herschreef, is de uitspraak 3 en luidt de detail Revision 3 changed locked form field "Order.Total", which FieldMDP does not allow. Die ene regel beantwoordt beide auditvragen. De uitspraak voor Approval.Requester moet eerst langs de revisie waarmee de manager tekende, en om de redenen in de beperkingssectie hieronder meldt hij vaak 4, dus de meest recente handtekening geeft doorgaans het scherpste antwoord. GetSignatureEffectiveMDPLevel meldt de laagste DocMDP P-waarde die onder de handtekeningen is gevonden, of 3 wanneer niets het document certificeerde, en beide getters blijven alleen geldig tot de volgende uitspraakaanroep

Tijdlijn van incrementele updates van een gemanipuleerd Delphi-PDF-formulier gelezen door PDFlibPas GetSignatureModificationVerdict: revisie 1 draagt een Include-vergrendeling op Order.Customer en Order.Total, revisie 2 tekent Approval.Manager, en de herschrijving van het vergrendelde veld Order.Total in revisie 3 levert uitspraak 3 op voor de handtekening van de manager en 4 voor die van de aanvrager
Vergrendelingen worden uit elk ondertekend veld gebundeld, dus de uitspraak voor Approval.Manager handhaaft al de vergrendeling van de aanvrager en de detailstring noemt het overtredende veld en de revisie, terwijl de vroegste handtekening vaak op 4 uitkomt

Hoe weet PDFlibPas welk veld een revisie heeft gewijzigd?

PDFlibPas reconstrueert de veldnaam uit de objectgraaf van elke revisie, want een gewijzigd object draagt zelden zijn volledige naam. Een velddictionary met /FT of /V, of een widgetannotatie, bevat alleen zijn deelnaam in /T. De volledig gekwalificeerde naam uit ISO 32000-1 §12.7.3.2 is de keten van deelnamen vanaf de wortel, samengevoegd met punten, dus de revisiekaart volgt de /Parent-verwijzingen omhoog en plakt elke /T die hij tegenkomt vooraan aan. Elke ouder wordt gelezen uit zijn nieuwste definitie op of vóór de te classificeren revisie, waardoor een latere hernoeming van een ouderknoop de geschiedenis van eerdere bewerkingen niet herschrijft

Resolutie van de volledig gekwalificeerde naam door FieldMDP in PDFlibPas voor Delphi: de revisiekaart volgt de /Parent-verwijzingen omhoog vanaf de gewijzigde widget, plakt elke /T-deelnaam met punten samengevoegd vooraan aan om Order.Total te bouwen, en matcht hem tegen /Fields-entries op exacte gelijkheid of puntvoorvoegsel zodat een vergrendeling op Order elke afstammeling dekt
Een gewijzigd object draagt zelden zijn volledige naam: ouders worden gelezen uit hun nieuwste definitie op of vóór de geclassificeerde revisie, namen worden hoofdlettergevoelig vergeleken en de tocht stopt na acht niveaus

De opgeloste naam tegen de vergrendelingen afzetten is met opzet simpel gehouden. Een naam matcht een /Fields-entry wanneer hij exact gelijk is aan de entry of begint met de entry gevolgd door een punt, dus het vergrendelen van Order vergrendelt ook Order.Total en alle andere afstammelingen. All vergrendelt elke naam, Include vergrendelt de matchende namen, en Exclude vergrendelt elke naam die niet matcht. De vergrendelingsdefinities komen uit elk ondertekend handtekeningveld in het bestand; waar een veld zowel een /Lock-dictionary als een FieldMDP-transform heeft, worden de /Lock-waarden gebruikt. Om precies te zien wat elke handtekening vergrendelde, stelt de onderliggende klasse TPDFlibSignDoc uit de unit PDFlibDigitalSign het permissierapport beschikbaar dat de uitspraak leest:

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;

Versleutelde formulieren: geef het wachtwoord mee of de namen zijn onleesbaar

Een versleuteld formulier moet eerst worden ontsleuteld voordat ook maar één veldnaam kan worden opgelost, dus GetSignatureModificationVerdict heeft het argument Password voor meer nodig dan alleen het openen van het bestand. Strings zoals /T worden per object versleuteld, en velden die in gecomprimeerde objectstreams zijn geschreven zijn helemaal niet leesbaar totdat de omvattende stream is ontsleuteld en gedecomprimeerd. Sinds v3.586.0 accepteert de interne revisiekaart het wachtwoord, koppelt hij een decryptiecontext aan terwijl hij objectstreams en revisiedictionaries parseert, en ontsleutelt hij elk object dat hij decodeert, inclusief de eerdere versies waarmee hij difft en de ouders waarlangs hij loopt

De decryptie per object in de revisiekaart schakelt alleen in wanneer Password niet leeg is. Een uitspraak die op een versleuteld bestand met een leeg wachtwoord is berekend, moet daarom als ongeverifieerd worden behandeld, ook al opende het document zelf zonder vragen. Bewaar het formulierwachtwoord naast het workflowdossier en geef het bij elke aanroep mee

Wat de FieldMDP-uitspraak niet dekt

De veldcontrole is een structureel antwoord op één smalle vraag, en een aantal gevallen valt erbuiten. Die van tevoren kennen voorkomt dat een 4 uitgroeit tot een supportescalatie:

  • De veldvergelijking draait pas nadat de documentniveaucontrole schoon is uitgevallen en ten minste één latere revisie meer veranderde dan validatiemateriaal of tijdstempels; een DocMDP-schending wordt als 3 gemeld voordat er ook maar naar een veld wordt gekeken
  • Objecten die als handtekeninggerelateerd zijn geclassificeerd maar geen veldnaam dragen, zoals de signature value dictionary, een herschreven AcroForm-dictionary of een cataloguswijziging beperkt tot /AcroForm of /Perms, laten de uitspraak 4 teruggeven in plaats van gokken, dus een revisie die onder een actieve vergrendeling een latere goedkeuringshandtekening toevoegt komt vaak op 4 uit
  • Alleen velddictionaries en widgetannotaties krijgen een veldnaam; de revisiekaart heeft geen aparte klasse voor appearance streams, dus een editor die appearances samen met de waarde regenereert kan door de documentniveaucontrole als inhoudswijziging worden tegengehouden voordat de veldcontrole draait, en de detailstring onderscheidt de twee gevallen
  • De scan stopt bij het eerste vergrendelde veld of de eerste naamloze entry die hij tegenkomt, dus een 3 noemt één overtredend veld, niet alle
  • Vergrendelingen worden uit elk ondertekend handtekeningveld gebundeld zonder ze in de tijd te ordenen, dus een vergrendeling van een latere ondertekenaar geldt ook voor updates die ervoor kwamen; dat kan een resultaat alleen richting 3 duwen, nooit richting 2
  • Namen worden exact en hoofdlettergevoelig vergeleken, en de /Parent-tocht stopt na acht niveaus, dus een uitzonderlijk diepe hiërarchie lost op tot een afgekorte naam
  • Een FieldMDP-entry waarvan de structuur niet valideert en waarvan de action niet leesbaar is, laat de uitspraak 4 teruggeven

Wanneer een uitspraak als 4 terugkomt, geeft GetSignatureRevisionIndex de revisie waarmee de handtekening eindigt en zet GetDocumentRevisionChanges elk object dat elke latere revisie toevoegde, wijzigde of vrijgaf op een rij, met zijn change class. Dat is genoeg voor een mens om binnen een minuut te beslissen. De werking van het toevoegen van die revisies staat in de incrementele-updategids voor PDFlibPas

FAQ

Betekent een uitspraak van 2 dat de handtekeningen geldig zijn?

Nee. GetSignatureModificationVerdict bekijkt wat er na een handtekening is toegevoegd en controleert nooit de CMS-handtekeningwaarde, de certificaatketen of intrekking. Een 2 zegt dat de latere updates binnen de permissies bleven; cryptografische validatie is een aparte stap en beide zijn nodig

Waarom levert een tweede goedkeurder die tekent 4 op in plaats van 2?

Ondertekenen voegt een signature value dictionary toe en herschrijft doorgaans de formulierboekhouding zoals de AcroForm-dictionary. Die objecten behoren tot geen enkel benoemd veld, en wanneer een veldvergrendeling actief is weigert de uitspraak een verandering goed te keuren die hij niet aan een veld kan toeschrijven. Gebruik GetDocumentRevisionChanges om te bevestigen dat de revisie alleen de verwachte handtekeningobjecten bevat

Kan één naam een hele groep velden vergrendelen?

Ja. Een vergrendelingsentry matcht het genoemde veld en elke afstammeling waarvan de volledig gekwalificeerde naam ermee begint gevolgd door een punt, dus Order in een Include-vergrendeling bevriest Order.Customer, Order.Total en elk ander veld onder die ouder

FieldMDP-uitspraken, het ondertekenen van veldvergrendelingen en revisie-inspectie zitten allemaal in de PDFlibPas Delphi PDF Library, samen met de DocMDP-certificering en de PAdES-longtermvalidatiefuncties waar de goedkeuringsworkflow hierboven op leunt