Teknisk artikel

FieldMDP i Delphi: Find redigeringer af låste formfelter

PDFlibPas besvarer FieldMDP-spørgsmålet via TPDFlib.GetSignatureModificationVerdict. Givet en signeret PDF, dens adgangskode og et signaturfeltnavn går metoden igennem hver trinvis opdatering, der er tilføjet efter den signatur, resolver det fuldt kvalificerede navn på hvert formularfelt, disse opdateringer har rørt, og returnerer 3, når et felt låst af FieldMDP er blevet ændret, og 2, når kun ulåste felter blev det. GetSignatureModificationDetail navngiver derefter revisionen og feltet

Workflowet, der har brug for dette, er formularen med flere godkendere. En anmoder udfylder Order.Customer og Order.Total på en købsanmodning og signerer Approval.Requester, hvilket låser de to felter. En leder tilføjer et koststed og signerer Approval.Manager. To uger senere åbner en person filen i et desktopprogram og skruer Order.Total op i en tredje trinvis opdatering. Alle signaturer verificerer stadig, fordi hver af dem kun dækker sit eget byte-range. De spørgsmål, en revisor reelt stiller, er, hvilket felt der ændredes efter hvilken signatur, og om den ændring var tilladt

Hvorfor kan DocMDP alene ikke fange en redigering af et låst felt?

DocMDP kan ikke fange det, fordi DocMDP klassificerer typer af ændringer, ikke enkelte felter. Certificeringstilladelsesniveauerne i ISO 32000-1 §12.8.2.2 siger, om senere opdateringer må udfylde formularer og signere (P 2) eller også annotere (P 3), og den tilladelse gælder alle felter i dokumentet lige. At redigere Order.Total er formularudfyldning, så en P 2-certificering er opfyldt af præcis den redigering, anmoderen ville forbyde. Dokumentniveau-klassificeringen af ændringer i PDFlibPas dækker det lag, og denne artikel går et trin finere

FieldMDP er laget med feltgranularitet. Et signaturfelt kan bære en /Lock-dictionary (ISO 32000-1 §12.7.4.5), og signaturen, det rummer, kan bære en FieldMDP-transform i sin signaturreference (ISO 32000-1 §12.8.2.4). Begge bruger samme ordforråd: /Action /All låser alle felter, /Action /Include låser navnene opført i /Fields, og /Action /Exclude låser alt undtagen navnene i /Fields. Før v3.586.0 vidste GetSignatureModificationVerdict, at en senere revision havde ændret et formularfelt, men ikke hvilket, så ethvert dokument med en feltlås, hvis senere opdateringer gik ud over valideringsmateriale, kom tilbage som 4, uafklaret. Revisionskortet registrerer nu et resolveret feltnavn for hvert feltrelateret objekt, det klassificerer, og det er præcis det, der forvandler de 4 til en reel 2 eller 3

At skrive feltlåsen, når hver godkender signerer

TPDFlib.SetSignProcessFieldLock sætter låsen for den næste signatur, en signeringsproces producerer. Action 1 er All, 2 er Include, 3 er Exclude, og 0 rydder låsen; Include og Exclude tager en semikolonsepareret liste over fuldt kvalificerede feltnavne. Det signede felt ender med en /Lock-dictionary og en matchende FieldMDP-transform, hvilket er det par, strenge validatorer forventer

PDFlibPas SetSignProcessFieldLock action-koder til Delphi-FieldMDP-signering: action 1 All låser alle formularfelter inklusive senere signaturfelter, action 2 Include låser de navngivne datafelter, action 3 Exclude låser alt undtagen listen, og action 0 rydder låsen
Vælg Include til de datafelter, hver godkender ejer, eller Exclude med de senere signaturfelter opført; et afvist kald returnerer 0 og lader den tidligere lås stå, så tjek altid returværdien
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: kun disse felter fryses af denne signatur
      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 returnerer 0 for et Include- eller Exclude-kald uden navne, og også for en liste med en tom indgang som 'Order.Customer;;Order.Total'; et afvist kald lader den tidligere konfigurerede lås stå, så tjek returværdien i stedet for at gå ud fra, at sidste kald vandt. Resten af signeringsopsætningen, valg af digest, /Contents-reservationen og resultatkoderne er dækket i PAdES-signerings- og valideringsguiden til PDFlibPas. Ét designpunkt er specifikt for godkendelseskæder: en /All-lås på anmoderens signatur fryser også Approval.Manager, fordi et signaturfelt også er et formularfelt. Brug Include til de datafelter, hver godkender ejer, eller Exclude med de senere signaturfelter opført

At køre vurderingen hen over en godkendelseskæde

GetSignatureModificationVerdict bedømmer én signatur ad gangen, så en godkendelseskæde tjekkes ved at kalde den én gang pr. signaturfelt. Returværdien er et lille, lukket sæt, og hver værdi betyder noget forskelligt for en reviewer:

  • 0: filen eller det navngivne signaturfelt kunne ikke læses
  • 1: intet blev tilføjet efter signaturen
  • 2: kun ændringer, som de effektive tilladelser tillader, blev tilføjet, og intet låst felt blev rørt
  • 3: en senere revision foretog en ændring, der ikke er tilladt, herunder en redigering af et felt, som FieldMDP låser
  • 4: svaret kunne ikke bestemmes, hvilket aldrig må læses som godkendelse
  • 5: signaturen ender ikke på en revisionsgrænse, så bytes ligger inden for det interval, den hævder at dække
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]);
      // begge getters beskriver kaldet, der lige er foretaget, så læs dem før det næste
      Log.Add(Format('%s: verdict %d, MDP level %d, %s',
        [Approvals[I], Verdict, Pdf.GetSignatureEffectiveMDPLevel,
         Pdf.GetSignatureModificationDetail]));
      case Verdict of
        1, 2: ;                                 // accepteret
        3: RejectForm(Approvals[I]);            // detaljen navngiver felt og revision
      else
        QueueForManualReview(Approvals[I]);     // 0, 4, 5
      end;
    end;
  finally
    Pdf.Free;
  end;
end;

Tag den manipulerede købsanmodning, hvor revision 0 er den tomme formular, revisionerne 1 og 2 er de to signaturer, og revision 3 er redigeringen. Fordi låsedefinitionerne pools fra hvert signeret felt, håndhæver vurderingen for Approval.Manager allerede anmoderens lås, og dens eneste senere opdatering er selve redigeringen. Da den opdatering kun omskrev felt-dictionaryen, er vurderingen 3, og detaljen lyder Revision 3 changed locked form field "Order.Total", which FieldMDP does not allow. Den ene linje besvarer begge revisionsspørgsmål. Vurderingen for Approval.Requester skal først forbi lederens signering af en revision, og af grundene i begrænsningsafsnittet nedenfor rapporterer den ofte 4, så den seneste signatur giver typisk det skarpeste svar. GetSignatureEffectiveMDPLevel rapporterer den laveste DocMDP P-værdi fundet blandt signaturerne, eller 3, når intet certificerede dokumentet, og begge getters forbliver gyldige kun indtil næste vurderingskald

Tidslinje for trinvise opdateringer i en manipuleret Delphi-PDF-formular læst af PDFlibPas GetSignatureModificationVerdict: revision 1 bærer en Include-lås på Order.Customer og Order.Total, revision 2 signerer Approval.Manager, og revision 3's omskrivning af det låste Order.Total-felt giver vurdering 3 for ledersignaturen og 4 for anmoderens signatur
Låse pools fra hvert signeret felt, så vurderingen for Approval.Manager allerede håndhæver anmoderens lås, og detaljestrengen navngiver det krænkende felt og revisionen, mens den tidligste signatur ofte lander på 4

Hvordan ved PDFlibPas, hvilket felt en revision ændrede?

PDFlibPas rekonstruerer feltnavnet ud fra hver revisions objektgraf, fordi et ændret objekt sjældent bærer sit fulde navn. En felt-dictionary med /FT eller /V, eller en widget-annotation, rummer kun sit delvise navn i /T. Det fuldt kvalificerede navn beskrevet i ISO 32000-1 §12.7.3.2 er kæden af delvise navne fra roden, samlet med punktummer, så revisionskortet går /Parent-linkene opad og sætter hvert /T, det finder, foranstillet. Hver forælder læses fra sin nyeste definition på eller før revisionen, der klassificeres, hvilket forhindrer en senere omdøbning af en forældrenode i at omskrive historikken for tidligere redigeringer

FieldMDP-opløsning af fuldt kvalificeret navn i PDFlibPas til Delphi: revisionskortet går /Parent-linkene opad fra den ændrede widget, sætter hvert /T-delnavn, samlet med punktummer, foranstillet for at bygge Order.Total og matcher det mod /Fields-indgange med eksakt lighed eller punktum-præfiks, så en lås på Order dækker alle efterkommere
Et ændret objekt bærer sjældent sit fulde navn: forældre læses fra deres nyeste definition på eller før den klassificerede revision, navne sammenlignes case-sensitivt, og gennemgangen stopper efter otte niveauer

Sammenligningen af det resolverede navn med låsene er med vilje enkel. Et navn matcher en /Fields-indgang, når det er eksakt lig indgangen eller starter med indgangen efterfulgt af et punktum, så en lås på Order også låser Order.Total og alle andre efterkommere. All låser alle navne, Include låser de matchende navne, og Exclude låser alle navne, der ikke matcher. Låsedefinitionerne kommer fra hvert signeret signaturfelt i filen; har et felt både en /Lock-dictionary og en FieldMDP-transform, bruges /Lock-værdierne. For at se præcis, hvad hver signatur låste, eksponerer den lavniveau-klasse TPDFlibSignDoc fra PDFlibDigitalSign-uniten den tilladelsesrapport, vurderingen 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;

Krypterede formularer: giv adgangskoden, ellers kan navnene ikke læses

En krypteret formular skal dekrypteres, før noget feltnavn kan resolveres, så GetSignatureModificationVerdict har brug for Password-argumentet til mere end at åbne filen. Strenge som /T krypteres pr. objekt, og felter skrevet ind i komprimerede object streams kan slet ikke læses, før den indeholdende stream er dekrypteret og inflateret. Siden v3.586.0 accepterer det interne revisionskort adgangskoden, knytter en dekripteringskontekst, mens det parser object streams og revisions-dictionaries, og dekrypterer hvert objekt, det decoder, herunder de tidligere versioner, det diff'er imod, og de forældre, det går

Den pr. objekt-dekryptering i revisionskortet slås kun til, når Password ikke er tom. En vurdering beregnet på en krypteret fil med en tom adgangskode bør derfor behandles som uverificeret, selv hvis dokumentet selv åbnede uden spørgsmål. Gem formularadgangskoden ved workflow-posten og giv den med ved hvert kald

Det, FieldMDP-vurderingen ikke dækker

Felttjekket er et strukturelt svar på ét snævert spørgsmål, og flere tilfælde falder uden for det. At kende dem på forhånd er det, der forhindrer en 4 i at ende som en support-eskalation:

  • Feltsammenligningen kører først, efter at dokumentniveau-tjekket er faldet rent ud, og mindst én senere revision ændrede mere end valideringsmateriale eller tidsstempler; en DocMDP-overtrædelse rapporteres som 3, før der kigges på et eneste felt
  • Objekter klassificeret som signaturrelaterede, men uden feltnavn, såsom signaturens value-dictionary, en omskrevet AcroForm-dictionary eller en katalogændring begrænset til /AcroForm eller /Perms, får vurderingen til at returnere 4 frem for at gætte, så en revision, der tilføjer en senere godkendelsessignatur under en aktiv lås, lander ofte på 4
  • Kun felt-dictionaries og widget-annotationer får et feltnavn; revisionskortet har ingen dedikeret klasse til appearance streams, så et redigeringsprogram, der regenererer appearances sammen med værdien, kan blive standset af dokumentniveau-tjekket som en indholdsændring, før felttjekket kører, og detaljestrengen adskiller de to tilfælde
  • Scanningen stopper ved det første låste felt eller unavngivne indgang, den møder, så en 3 navngiver ét krænkende felt, ikke alle
  • Låse pools fra hvert signeret signaturfelt uden at sortere dem i tid, så en lås skrevet af en senere signerende også anvendes på opdateringer, der kom før den; det kan kun skubbe et resultat mod 3, aldrig mod 2
  • Navne sammenlignes eksakt og case-sensitivt, og /Parent-gennemgangen stopper efter otte niveauer, så en usædvanligt dyb hierarkistruktur resolver til et afkortet navn
  • En FieldMDP-indgang, hvis struktur ikke validerer, og hvis action ikke kan læses, får vurderingen til at returnere 4

Når en vurdering kommer tilbage som 4, giver GetSignatureRevisionIndex revisionen, signaturen ender ved, og GetDocumentRevisionChanges lister hvert objekt, hver senere revision tilføjede, ændrede eller frigav, med sin ændringsklasse. Det er nok for en person at beslutte på et minut. Mekanikken bag, hvordan de revisioner tilføjes, er dækket i incremental update-guiden til PDFlibPas

FAQ

Betyder en vurdering på 2, at signaturerne er gyldige?

Nej. GetSignatureModificationVerdict undersøger, hvad der blev tilføjet efter en signatur, og tjekker aldrig CMS-signaturværdien, certifikatkæden eller tilbagekaldelsen. En 2 siger, at de senere opdateringer forblev inden for tilladelserne; kryptografisk validering er et separat trin, og begge er nødvendige

Hvorfor giver en anden godkenders signering 4 i stedet for 2?

Signering tilføjer en signaturværdi-dictionary og omskriver normalt formularbogholderi som AcroForm-dictionaryen. De objekter tilhører intet navngivet felt, og når en feltlås er aktiv, nægter vurderingen at godkende en ændring, den ikke kan tilskrive et felt. Brug GetDocumentRevisionChanges til at bekræfte, at revisionen kun indeholder de forventede signaturobjekter

Kan ét navn låse en hel gruppe af felter?

Ja. En låseindgang matcher det navngivne felt og alle efterkommere, hvis fuldt kvalificerede navn starter med det efterfulgt af et punktum, så en Order opført i en Include-lås fryser Order.Customer, Order.Total og ethvert andet felt under den forælder

FieldMDP-vurderinger, signering med feltlås og revisionsinspektion følger alle med i PDFlibPas Delphi PDF Library, sammen med DocMDP-certificeringen og PAdES long-term validation-funktionerne, som godkendelses-workflowet ovenfor er afhængig af