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
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
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
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
/AcroFormeller/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å