PDFlibPas répond à la question FieldMDP via TPDFlib.GetSignatureModificationVerdict. À partir dun PDF signé, de son mot de passe et du nom dun champ de signature, la méthode parcourt chaque mise à jour incrémentale ajoutée après cette signature, résout le nom complet de chaque champ de formulaire touché par ces mises à jour, et renvoie 3 quand un champ verrouillé par FieldMDP a été modifié et 2 quand seuls des champs non verrouillés lont été. GetSignatureModificationDetail nomme ensuite la révision et le champ
Le workflow concerné est le formulaire à plusieurs approbateurs. Un demandeur remplit Order.Customer et Order.Total sur une demande dachat et signe Approval.Requester, verrouillant ces deux champs. Un responsable ajoute un centre de coût et signe Approval.Manager. Deux semaines plus tard, quelquun ouvre le fichier dans un éditeur de bureau et majore Order.Total dans une troisième mise à jour incrémentale. Chaque signature reste valide, parce que chacune ne couvre que son propre intervalle doctets. Les questions que pose réellement un auditeur sont : quel champ a changé après quelle signature, et ce changement était-il autorisé
Pourquoi DocMDP seul ne détecte-t-il pas un champ verrouillé modifié ?
DocMDP ne peut pas le détecter parce que DocMDP classe des types de changement, pas des champs individuels. Les niveaux de permission de certification de ISO 32000-1 §12.8.2.2 disent si les mises à jour ultérieures peuvent remplir des formulaires et signer (P 2) ou aussi annoter (P 3), et cette permission sapplique également à chaque champ du document. Modifier Order.Total est du remplissage de formulaire, si bien quune certification P 2 est satisfaite par exactement la modification que le demandeur voulait interdire. La classification des modifications au niveau document de PDFlibPas couvre cette couche, et cet article descend à la granularité en dessous
FieldMDP est la couche à la granularité du champ. Un champ de signature peut porter un dictionnaire /Lock (ISO 32000-1 §12.7.4.5), et la signature quil contient peut transporter une transformée FieldMDP dans sa référence de signature (ISO 32000-1 §12.8.2.4). Les deux emploient le même vocabulaire : /Action /All verrouille chaque champ, /Action /Include verrouille les noms listés dans /Fields, et /Action /Exclude verrouille tout sauf les noms de /Fields. Avant la v3.586.0, GetSignatureModificationVerdict savait quune révision ultérieure avait modifié un champ de formulaire, mais pas lequel, donc tout document portant un verrou de champ dont les mises à jour ultérieures dépassaient le matériau de validation revenait en 4, indéterminé. La carte des révisions enregistre désormais un nom de champ résolu pour chaque objet lié aux champs quelle classe, et cest ce qui transforme ce 4 en un vrai 2 ou 3
Écrire le verrou de champ quand chaque approbateur signe
TPDFlib.SetSignProcessFieldLock pose le verrou pour la prochaine signature que produit un processus de signature. Action 1 vaut All, 2 Include, 3 Exclude et 0 efface le verrou ; Include et Exclude prennent une liste de noms de champs complets séparés par des points-virgules. Le champ signé se retrouve avec un dictionnaire /Lock et une transformée FieldMDP correspondante, soit exactement la paire quattendent les validateurs stricts
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 : seuls ces champs sont gelés par cette signature
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 renvoie 0 pour un appel Include ou Exclude sans noms, et aussi pour une liste contenant une entrée vide telle que 'Order.Customer;;Order.Total' ; un appel refusé laisse en place le verrou configuré précédemment, donc vérifiez la valeur de retour au lieu de présumer que le dernier appel a gagné. Le reste du paramétrage de signature, choix du digest, réservation /Contents et codes de résultat, est couvert dans le guide de signature et validation PAdES de PDFlibPas. Un point de conception est propre aux chaînes dapprobation : un verrou /All sur la signature du demandeur gèle aussi Approval.Manager, parce quun champ de signature est lui-même un champ de formulaire. Utilisez Include pour les champs de données que possède chaque approbateur, ou Exclude en listant les champs de signature ultérieurs
Appliquer le verdict sur toute une chaîne dapprobation
GetSignatureModificationVerdict juge une signature à la fois, donc une chaîne dapprobation se vérifie en un appel par champ de signature. La valeur de retour forme un petit ensemble fermé, et chaque valeur dit quelque chose de différent pour un relecteur :
- 0 : le fichier ou le champ de signature nommé na pas pu être lu
- 1 : rien na été ajouté après la signature
- 2 : seules des modifications permises par les permissions effectives ont été ajoutées, et aucun champ verrouillé na été touché
- 3 : une révision ultérieure a effectué un changement non autorisé, y compris une édition dun champ que FieldMDP verrouille
- 4 : la réponse na pas pu être déterminée, ce qui ne doit jamais être lu comme une approbation
- 5 : la signature ne se termine pas sur une frontière de révision, donc des octets se trouvent à lintérieur de lintervalle quelle prétend couvrir
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]);
// les deux getters décrivent lappel qui vient dêtre fait, donc lisez-les avant le suivant
Log.Add(Format('%s: verdict %d, MDP level %d, %s',
[Approvals[I], Verdict, Pdf.GetSignatureEffectiveMDPLevel,
Pdf.GetSignatureModificationDetail]));
case Verdict of
1, 2: ; // accepté
3: RejectForm(Approvals[I]); // le détail nomme champ et révision
else
QueueForManualReview(Approvals[I]); // 0, 4, 5
end;
end;
finally
Pdf.Free;
end;
end;
Prenez la demande dachat falsifiée, où la révision 0 est le formulaire vierge, les révisions 1 et 2 sont les deux signatures et la révision 3 est la modification. Comme les définitions de verrous sont mises en commun depuis chaque champ signé, le verdict pour Approval.Manager applique déjà le verrou du demandeur, et sa seule mise à jour ultérieure est la modification elle-même. Quand cette mise à jour ne réécrit que le dictionnaire du champ, le verdict est 3 et le détail lit Revision 3 changed locked form field "Order.Total", which FieldMDP does not allow. Cette seule ligne répond aux deux questions daudit. Le verdict pour Approval.Requester doit dabord passer la révision de signature du responsable, et pour les raisons de la section limites ci-dessous il rapportera souvent 4, si bien que la signature la plus récente donne dordinaire la réponse la plus nette. GetSignatureEffectiveMDPLevel rapporte la plus petite valeur de DocMDP P trouvée parmi les signatures, ou 3 quand rien na certifié le document, et les deux getters ne restent valables que jusquau prochain appel de verdict
Comment PDFlibPas sait-il quel champ une révision a modifié ?
PDFlibPas reconstruit le nom du champ depuis le graphe dobjets de chaque révision, parce quun objet modifié ne porte que rarement son nom complet. Un dictionnaire de champ avec /FT ou /V, ou une annotation widget, ne détient que son nom partiel dans /T. Le nom complet décrit par ISO 32000-1 §12.7.3.2 est la chaîne des noms partiels depuis la racine, joints par des points, donc la carte des révisions remonte les liens /Parent et préfixe chaque /T trouvé. Chaque parent est lu depuis sa définition la plus récente à la révision classée ou avant, ce qui empêche un renommage ultérieur dun nœud parent de réécrire lhistorique des modifications antérieures
Confronter le nom résolu aux verrous est délibérément simple. Un nom correspond à une entrée /Fields quand il égale exactement lentrée ou commence par lentrée suivie dun point, donc verrouiller Order verrouille aussi Order.Total et tout autre descendant. All verrouille chaque nom, Include verrouille les noms correspondants, et Exclude verrouille chaque nom qui ne correspond pas. Les définitions de verrous viennent de chaque champ de signature signé du fichier ; quand un champ possède à la fois un dictionnaire /Lock et une transformée FieldMDP, les valeurs /Lock servent. Pour voir exactement ce que chaque signature a verrouillé, la classe de plus bas niveau TPDFlibSignDoc de lunité PDFlibDigitalSign expose le rapport de permissions que lit le verdict :
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;
Formulaires chiffrés : passez le mot de passe ou les noms sont illisibles
Un formulaire chiffré doit être déchiffré avant quun nom de champ puisse être résolu, donc GetSignatureModificationVerdict a besoin de largument Password pour plus que louverture du fichier. Des chaînes comme /T sont chiffrées par objet, et des champs écrits dans des flux dobjets compressés ne peuvent pas être lus du tout tant que le flux conteneur nest pas déchiffré et décompressé. Depuis la v3.586.0, la carte des révisions interne accepte le mot de passe, attache un contexte de déchiffrement pendant quelle analyse les flux dobjets et les dictionnaires de révision, et déchiffre chaque objet quelle décode, y compris les versions antérieures auxquelles elle se compare et les parents quelle remonte
Le déchiffrement par objet de la carte des révisions ne sactive que quand Password est non vide. Un verdict calculé sur un fichier chiffré avec un mot de passe vide doit donc être traité comme non vérifié, même si le document lui-même souvre sans invite. Stockez le mot de passe du formulaire à côté de lenregistrement du workflow et passez-le à chaque appel
Ce que le verdict FieldMDP ne couvre pas
La vérification des champs est une réponse structurelle à une question étroite, et plusieurs cas tombent hors de son périmètre. Les connaître à lavance est ce qui évite quun 4 ne se transforme en escalade support :
- La comparaison des champs ne tourne quaprès que la vérification au niveau document est sortie propre et quau moins une révision ultérieure a changé autre chose que du matériau de validation ou des horodatages ; une violation DocMDP est rapportée comme 3 avant même quun champ soit regardé
- Des objets classés liés à la signature mais sans nom de champ, comme le dictionnaire de valeur de signature, un dictionnaire AcroForm réécrit, ou un changement de catalogue limité à
/AcroFormou/Perms, font renvoyer 4 au verdict plutôt que deviner, si bien quune révision qui ajoute une signature dapprobation sous un verrou actif tombe souvent sur 4 - Seuls les dictionnaires de champs et les annotations widget reçoivent un nom de champ ; la carte des révisions na pas de classe dédiée aux flux dapparence, donc un éditeur qui régénère les apparences en même temps que la valeur peut être stoppé par la vérification au niveau document comme changement de contenu avant que la vérification des champs ne tourne, et la chaîne de détail distingue les deux cas
- Le balayage sarrête au premier champ verrouillé ou à la première entrée sans nom rencontrés, donc un 3 nomme un champ en cause, pas tous
- Les verrous sont mis en commun depuis chaque champ de signature signé sans les ordonner dans le temps, donc un verrou écrit par un signataire ultérieur sapplique aussi aux mises à jour antérieures ; cela ne peut que pousser un résultat vers 3, jamais vers 2
- Les noms se comparent exactement et en respectant la casse, et la marche
/Parentsarrête après huit niveaux, donc une hiérarchie anormalement profonde se résout en un nom tronqué - Une entrée FieldMDP dont la structure ne valide pas et laction reste illisible fait renvoyer 4 au verdict
Quand un verdict revient en 4, GetSignatureRevisionIndex donne la révision où la signature se termine et GetDocumentRevisionChanges liste chaque objet que chaque révision ultérieure a ajouté, modifié ou libéré, avec sa classe de changement. Cest assez pour quune personne décide en une minute. La mécanique dajout de ces révisions est couverte dans le guide de mise à jour incrémentale de PDFlibPas
FAQ
Un verdict de 2 signifie-t-il que les signatures sont valides ?
Non. GetSignatureModificationVerdict examine ce qui a été ajouté après une signature et ne vérifie jamais la valeur de signature CMS, la chaîne de certificats ni la révocation. Un 2 dit que les mises à jour ultérieures sont restées dans les permissions ; la validation cryptographique est une étape séparée et les deux sont nécessaires
Pourquoi la signature dun second approbateur produit-elle 4 au lieu de 2 ?
Signer ajoute un dictionnaire de valeur de signature et réécrit dordinaire la comptabilité des formulaires telle que le dictionnaire AcroForm. Ces objets nappartiennent à aucun champ nommé, et quand un verrou de champ est actif, le verdict refuse de valider un changement quil ne peut attribuer à un champ. Utilisez GetDocumentRevisionChanges pour confirmer que la révision ne contient que les objets de signature attendus
Un seul nom peut-il verrouiller tout un groupe de champs ?
Oui. Une entrée de verrou correspond au champ nommé et à chaque descendant dont le nom complet commence par lui suivi dun point, donc lister Order dans un verrou Include gèle Order.Customer, Order.Total et tout autre champ sous ce parent
Les verdicts FieldMDP, la signature avec verrou de champ et linspection des révisions sont tous livrés dans la PDFlibPas Delphi PDF Library, avec les fonctions de certification DocMDP et de validation à long terme PAdES dont dépend le workflow dapprobation ci-dessus