Teknisk artikel

FieldMDP i Delphi: upptäck ändringar i låsta formulärfält

PDFlibPas besvarar FieldMDP-frågan via TPDFlib.GetSignatureModificationVerdict. Metoden tar en signerad PDF, dess lösenord och namnet på ett signaturfält, går igenom varje inkrementell uppdatering som lagts till efter den signaturen, slår upp det fullständiga namnet på varje formulärfält som uppdateringarna rörde, och returnerar 3 när ett fält som FieldMDP låst har ändrats och 2 när bara olåsta fält blev det. GetSignatureModificationDetail namnger sedan revisionen och fältet

Arbetsflödet som behöver det här är formuläret med flera godkännare. En beställare fyller i Order.Customer och Order.Total på en inköpsförfrågan och signerar Approval.Requester, vilket låser de två fälten. En chef lägger till ett kostnadsställe och signerar Approval.Manager. Två veckor senare öppnar någon filen i en skrivbordseditor och trycker upp Order.Total i en tredje inkrementell uppdatering. Varje signatur verifieras fortfarande, eftersom var och en bara täcker sitt eget byteintervall. Frågorna en revisor faktiskt ställer är vilka fält som ändrades efter vilken signatur, och om den ändringen var tillåten

Varför räcker inte DocMDP ensamt för att fånga ett redigerat låst fält?

DocMDP fångar den inte, eftersom DocMDP klassificerar slaget av ändring, inte enskilda fält. Behörighetsnivåerna för certifiering i ISO 32000-1 §12.8.2.2 säger om senare uppdateringar får fylla i formulär och signera (P 2) eller även annotera (P 3), och den behörigheten gäller alla fält i dokumentet lika. Att redigera Order.Total är formulärfyllning, så en P 2-certifiering uppfylls av exakt den redigering beställaren avsåg att förbjuda. Dokumentnivåklassificeringen av ändringar i PDFlibPas täcker det lagret, och den här artikeln går ett steg finare

FieldMDP är lagret med fältgranularitet. Ett signaturfält kan bära en /Lock-ordbok (ISO 32000-1 §12.7.4.5), och signaturen det håller kan bära en FieldMDP-transform i sin signaturreferens (ISO 32000-1 §12.8.2.4). Båda använder samma vokabulär: /Action /All låser varje fält, /Action /Include låser namnen som listas i /Fields, och /Action /Exclude låser allt utom namnen i /Fields. Före v3.586.0 visste GetSignatureModificationVerdict att en senare revision hade ändrat ett formulärfält men inte vilket, så varje dokument med ett fältlås vars senare uppdateringar gick utöver valideringsmaterial kom tillbaka som 4, obestämt. Revisionskartan registrerar nu ett upplöst fältnamn för varje fältrelaterat objekt den klassificerar, och det är det som förvandlar den 4:an till en äkta 2:a eller 3:a

Att skriva fältlåset när varje godkännare signerar

TPDFlib.SetSignProcessFieldLock ställer in låset för nästa signatur som en signeringsprocess producerar. Åtgärd 1 är All, 2 är Include, 3 är Exclude och 0 rensar låset; Include och Exclude tar en semikolonseparerad lista med fullständiga fältnamn. Det signerade fältet får en /Lock-ordbok och en matchande FieldMDP-transform, den kombination som strikta validatorer förväntar sig

PDFlibPas SetSignProcessFieldLock-åtgärdskoder för FieldMDP-signering i Delphi: åtgärd 1 All låser varje formulärfält inklusive senare signaturfält, åtgärd 2 Include låser de namngivna datafälten, åtgärd 3 Exclude låser allt utom listan, och åtgärd 0 rensar låset
Välj Include för de datafält varje godkännare äger, eller Exclude med de senare signaturfälten listade; ett avvisat anrop returnerar 0 och låter föregående lås stå kvar, så kontrollera alltid returvärdet
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: endast de här fälten fryses av den här signaturen
      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 returnerar 0 för ett Include- eller Exclude-anrop utan namn, och även för en lista med en tom post som 'Order.Customer;;Order.Total'; ett avvisat anrop låter det tidigare konfigurerade låset ligga kvar, så kontrollera returvärdet i stället för att anta att sista anropet vann. Resten av signeringsuppsättningen, val av digest, /Contents-reservation och resultatkoder, tas upp i PAdES-guiden för signering och validering i PDFlibPas. En designpunkt är specifik för godkännandekedjor: ett /All-lås på beställarens signatur fryser också Approval.Manager, eftersom ett signaturfält också är ett formulärfält. Använd Include för de datafält varje godkännare äger, eller Exclude med de senare signaturfälten listade

Att köra bedömningen genom en godkännandekedja

GetSignatureModificationVerdict bedömer en signatur i taget, så en godkännandekedja kontrolleras genom att anropa den en gång per signaturfält. Returvärdet är en liten, sluten mängd, och varje värde betyder något olika för en granskare:

  • 0: filen eller det namngivna signaturfältet kunde inte läsas
  • 1: inget lades till efter signaturen
  • 2: bara ändringar som de gällande behörigheterna tillåter lades till, och inget låst fält rördes
  • 3: en senare revision gjorde en ändring som inte är tillåten, inklusive en redigering av ett fält som FieldMDP låser
  • 4: svaret kunde inte bestämmas, vilket aldrig får läsas som godkännande
  • 5: signaturen slutar inte på en revisionsgräns, så byte ligger inuti intervallet den påstår sig täcka
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]);
      // båda getterna beskriver anropet som gjordes, läs dem före nästa anrop
      Log.Add(Format('%s: verdict %d, MDP level %d, %s',
        [Approvals[I], Verdict, Pdf.GetSignatureEffectiveMDPLevel,
         Pdf.GetSignatureModificationDetail]));
      case Verdict of
        1, 2: ;                                 // godkänt
        3: RejectForm(Approvals[I]);            // detaljen anger fält och revision
      else
        QueueForManualReview(Approvals[I]);     // 0, 4, 5
      end;
    end;
  finally
    Pdf.Free;
  end;
end;

Ta den manipulerade inköpsförfrågan, där revision 0 är det tomma formuläret, revision 1 och 2 är de två signaturerna och revision 3 är redigeringen. Eftersom låsdefinitionerna samlas in från varje signerat fält verkställer bedömningen för Approval.Manager redan beställarens lås, och dess enda senare uppdatering är själva redigeringen. När den uppdateringen skrev om bara fältordboken är bedömningen 3 och detaljen lyder Revision 3 changed locked form field "Order.Total", which FieldMDP does not allow. Den raden besvarar båda granskningsfrågorna. Bedömningen för Approval.Requester måste först komma förbi att chefen signerar en revision, och av skälen i begränsningsavsnittet nedan rapporterar den ofta 4, så den senaste signaturen ger vanligtvis det skarpaste svaret. GetSignatureEffectiveMDPLevel rapporterar det lägsta DocMDP P-värde som hittats bland signaturerna, eller 3 när inget certifierat dokumentet, och båda getterna gäller bara fram till nästa bedömningsanrop

Inkrementell uppdateringstidslinje för ett manipulerat Delphi-PDF-formulär läst med PDFlibPas GetSignatureModificationVerdict: revision 1 bär ett Include-lås på Order.Customer och Order.Total, revision 2 signerar Approval.Manager, och revision 3:s omskrivning av det låsta fältet Order.Total ger bedömning 3 för chefsignaturen och 4 för beställarens signatur
Lås samlas från varje signerat fält, så bedömningen för Approval.Manager verkställer redan beställarens lås och detaljsträngen namnger det felande fältet och revisionen, medan den tidigaste signaturen ofta hamnar på 4

Hur vet PDFlibPas vilket fält en revision ändrade?

PDFlibPas rekonstruerar fältnamnet från objektgrafen i varje revision, eftersom ett ändrat objekt sällan bär sitt fullständiga namn. En fältordbok med /FT eller /V, eller en widgetanotering, håller bara sitt partiella namn i /T. Det fullständigt kvalificerade namn som beskrivs i ISO 32000-1 §12.7.3.2 är kedjan av partiella namn från roten, sammanfogade med punkter, så revisionskartan går /Parent-länkar uppåt och fogar fram varje /T den hittar. Varje förälder läses från sin nyaste definition vid eller före revisionen som klassificeras, vilket hindrar en senare namnändring av en föräldernod från att skriva om historiken för tidigare redigeringar

FieldMDP-upplösning av fullständigt namn i PDFlibPas för Delphi: revisionskartan går /Parent-länkar uppåt från den ändrade widgeten, fogar fram varje /T-partiellt namn sammanfogat med punkter för att bygga Order.Total, och matchar det mot /Fields-poster med exakt likhet eller punkt-prefix så att ett lås på Order täcker varje avkomling
Ett ändrat objekt bär sällan sitt fullständiga namn: föräldrar läses från sin nyaste definition vid eller före den klassificerade revisionen, namn jämförs skiftlägeskänsligt, och vandringen stannar efter åtta nivåer

Att matcha det upplösta namnet mot låsen är medvetet enkelt. Ett namn matchar en /Fields-post när det är exakt lika med posten eller börjar med posten följt av en punkt, så att låsa Order också låser Order.Total och varje annan avkomling. All låser varje namn, Include låser de matchande namnen, och Exclude låser varje namn som inte matchar. Låsdefinitionerna kommer från varje signerat signaturfält i filen; där ett fält har både en /Lock-ordbok och en FieldMDP-transform används /Lock-värdena. För att se exakt vad varje signatur låste exponerar klassen TPDFlibSignDoc på lägre nivå, från uniten PDFlibDigitalSign, behörighetsrapporten som bedömningen läser:

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;

Krypterade formulär: skicka med lösenordet eller går namnen inte att läsa

Ett krypterat formulär måste dekrypteras innan något fältnamn kan slås upp, så GetSignatureModificationVerdict behöver argumentet Password till mer än att öppna filen. Strängar som /T krypteras per objekt, och fält som skrivits in i komprimerade objektströmmar går inte alls att läsa förrän den omslutande strömmen dekrypterats och inflaterats. Sedan v3.586.0 accepterar den interna revisionskartan lösenordet, fäster en dekrypteringskontext medan den parsar objektströmmar och revisionsordböcker, och dekrypterar varje objekt den avkodar, inklusive de tidigare versioner den diffar mot och de föräldrar den vandrar genom

Dekrypteringen per objekt i revisionskartan slås på bara när Password inte är tomt. En bedömning beräknad på en krypterad fil med tomt lösenord bör därför behandlas overifierad, även om dokumentet i sig öppnades utan fråga. Lagra formulärets lösenord intill arbetsflödesposten och skicka med det vid varje anrop

Vad FieldMDP-bedömningen inte täcker

Fältkontrollen är ett strukturellt svar på en smal fråga, och flera fall ligger utanför den. Att känna till dem i förväg är det som hindrar en 4:a från att bli en supporteskalering:

  • Fältjämförelsen körs bara efter att dokumentnivåkontrollen kommit ut ren och minst en senare revision ändrat mer än valideringsmaterial eller tidsstämplar; en DocMDP-överträdelse rapporteras som 3 innan något fält tittas på
  • Objekt klassade som signaturrelaterade men utan fältnamn, som signaturvärdesordboken, en omskriven AcroForm-ordbok, eller en katalogändring begränsad till /AcroForm eller /Perms, får bedömningen att returnera 4 i stället för att gissa, så en revision som lägger till en senare godkännandesignatur under ett aktivt lås hamnar ofta på 4
  • Bara fältordböcker och widgetanoteringar får ett fältnamn; revisionskartan har ingen dedikerad klass för utseendeströmmar, så en editor som regenererar utseenden tillsammans med värdet kan stoppas av dokumentnivåkontrollen som en innehållsändring innan fältkontrollen körs, och detaljsträngen skiljer de två fallen åt
  • Skanningen stannar vid det första låsta fältet eller namnlösa post den träffar på, så en 3:a namnger ett felande fält, inte alla
  • Lås samlas från varje signerat signaturfält utan att ordnas i tiden, så ett lås skrivet av en senare signerare tillämpas även på uppdateringar som kom före det; det kan bara driva ett resultat mot 3, aldrig mot 2
  • Namn jämförs exakt och skiftlägeskänsligt, och /Parent-vandringen stannar efter åtta nivåer, så en ovanligt djup hierarki slås upp till ett avkortat namn
  • En FieldMDP-post vars struktur inte validerar och vars åtgärd inte kan läsas får bedömningen att returnera 4

När en bedömning kommer tillbaka som 4 ger GetSignatureRevisionIndex revisionen som signaturen slutar vid och GetDocumentRevisionChanges listar varje objekt varje senare revision lade till, ändrade eller frigjorde, med sin ändringsklass. Det räcker för en människa att besluta på en minut. Mekaniken i hur de revisionerna läggs till tas upp i guiden om inkrementella uppdateringar för PDFlibPas

FAQ

Betyder en bedömning på 2 att signaturerna är giltiga?

Nej. GetSignatureModificationVerdict granskar vad som lades till efter en signatur och kontrollerar aldrig CMS-signaturvärdet, certifikatkedjan eller spärrningen. En 2:a säger att de senare uppdateringarna stannade inom behörigheterna; kryptografisk validering är ett separat steg och båda behövs

Varför ger en andra godkännare som signerar 4 i stället för 2?

Signering lägger till en signaturvärdesordbok och skriver vanligen om formulärbokföring som AcroForm-ordboken. De objekten tillhör inget namngivet fält, och när ett fältlås är aktivt vägrar bedömningen att godkänna en ändring den inte kan tillskriva ett fält. Använd GetDocumentRevisionChanges för att bekräfta att revisionen bara innehåller de förväntade signaturobjekten

Kan ett enda namn låsa en hel grupp av fält?

Ja. En låspost matchar det namngivna fältet och varje avkomling vars fullständiga namn börjar med det följt av en punkt, så att lista Order i ett Include-lås fryser Order.Customer, Order.Total och varje annat fält under den föräldern

FieldMDP-bedömningar, signering av fältlås och revisionsgranskning ingår alla i PDFlibPas Delphi PDF Library, tillsammans med DocMDP-certifiering och PAdES-funktionerna för långtidsvalidering som godkännandearbetsflödet ovan förlitar sig på