La question qui casse les pipelines de signature faibles est rarement cryptographique. Un auditeur demande : « votre rapport de preflight indique que ce lot de factures est conforme PDF/A — cela a-t-il été vérifié avant ou après l’application de la signature ? » Quand validation et signature s’exécutent dans deux outils distincts avec une passe de remédiation entre les deux, au moins trois révisions du fichier existent, et le rapport n’en décrit qu’une seule. PDFlibPas, la bibliothèque PDF pour développeurs Delphi et C++Builder de losLab, expose le preflight et la signature PAdES derrière une même classe façade, ce qui rend réaliste la construction d’un atelier où cette question reçoit une réponse démontrable
Cet article parcourt le modèle d’atelier de bout en bout : preflight sur les octets exacts qui seront signés, signature appliquée via l’API SignProcess, puis audit en relecture qui confirme que le ByteRange couvre réellement le fichier. Chaque appel montré ici existe aujourd’hui dans la bibliothèque, et chaque piège aussi
Trois révisions d’un même document, et où l’écart s’ouvre
Un workflow conformité-puis-signature touche le fichier au moins trois fois. L’original arrive de l’amont. Une passe de remédiation le charge, active un mode de conformité et enregistre une révision corrigée. La passe de signature ajoute ensuite une signature en mise à jour incrémentale. Chacune de ces sauvegardes modifie les octets ; un rapport de preflight n’a donc de sens que s’il indique quelle révision il décrit. La façon la moins coûteuse d’ancrer cela est d’enregistrer un hash SHA-256 du fichier à côté de chaque exécution de preflight et de chaque signature
Un comportement de la bibliothèque rend cet ancrage plus strict qu’on ne l’imagine : les corrections de conformité demandées via SetPDFAMode ou SetPDFUAMode sont appliquées pendant la sauvegarde, pas au moment de l’appel. Les réparations automatiques, comme forcer les indicateurs d’impression des annotations ou attribuer un ordre de tabulation PDF/UA, n’arrivent que dans le fichier de sortie. Exécuter le vérificateur contre le document que vous venez de « corriger » en mémoire ne prouve rien sur les octets que vous allez signer ; relancez toujours le preflight contre le fichier sauvegardé
Preflight depuis le disque, et le zéro qui signifie deux choses
Le point d’entrée plat du preflight est CheckFileCompliance(FileName, Password, ComplianceTest, Options), où le test 1 sélectionne PDF/A (ISO 19005) et le test 2 sélectionne PDF/UA (ISO 14289). Il ouvre le fichier via le lecteur streaming de la bibliothèque, sans nécessiter d’abord LoadFromFile, et renvoie un handle de liste de chaînes avec un constat par entrée :
var
PDF: TPDFlib;
ListID, I: Integer;
begin
PDF := TPDFlib.Create;
try
ListID := PDF.CheckFileCompliance('invoice-fixed.pdf', '', 1, 0); // 1 = PDF/A
if ListID = 0 then
begin
if PDF.LastErrorCode <> 0 then
raise Exception.Create('Preflight could not read the file')
else
Writeln('No PDF/A findings');
end
else
begin
for I := 0 to PDF.GetStringListCount(ListID) - 1 do
Writeln(PDF.GetStringListItem(ListID, I));
PDF.ReleaseStringList(ListID);
end;
finally
PDF.Free;
end;
end;
Le piège est dans la valeur de retour. Zéro signifie « aucun constat », mais aussi « le fichier n’a pas pu être ouvert » — l’implémentation renvoie 0 dès que la liste de résultats finit vide, y compris en cas d’échec de lecture. Un atelier qui traite 0 comme un feu vert approuvera un fichier verrouillé par un autre processus. Associez l’appel à LastErrorCode, comme ci-dessus. Notez aussi que le vérificateur ouvre le fichier avec un mode de partage deny-write ; si votre étape de remédiation conserve encore un handle d’écriture, le preflight échoue pour une raison qui n’a rien à voir avec la conformité
Pour la revue humaine, CreatePreflightReport rend les mêmes constats sous forme de rapport lisible, et ComparePreflightReports compare deux exécutions, ce qui est pratique pour prouver que la remédiation a supprimé des constats sans en introduire de nouveaux
Signer la révision vérifiée avec un SignProcess
Une fois que la révision sauvegardée passe le preflight et que son hash est enregistré, signez exactement ce fichier. L’API SignProcess est un builder : ouvrez un handle de processus, configurez-le, validez, puis lisez le code de résultat
ProcessID := PDF.NewSignProcessFromFile('invoice-fixed.pdf', '');
if ProcessID = 0 then
raise Exception.Create('Cannot open source for signing');
PDF.SetSignProcessField(ProcessID, 'ApprovalSig');
PDF.SetSignProcessPFXFromFile(ProcessID, 'company.pfx', PfxPassword);
PDF.SetSignProcessInfo(ProcessID, 'Invoice approval', 'Berlin', 'billing@example.com');
PDF.SetSignProcessCustomSubFilter(ProcessID, 'ETSI.CAdES.detached'); // PAdES baseline
PDF.SetSignProcessDigestAlgorithm(ProcessID, 2); // SHA-256
PDF.SetSignProcessReserveContentsBytes(ProcessID, 8192); // room for a later timestamp
PDF.EndSignProcessToFile(ProcessID, 'invoice-signed.pdf');
if PDF.GetSignProcessResult(ProcessID) <> 1 then
Writeln('Sign failed, code ', PDF.GetSignProcessResult(ProcessID));
PDF.ReleaseSignProcess(ProcessID);
Deux lignes de configuration méritent l’attention. SetSignProcessCustomSubFilter avec ETSI.CAdES.detached sélectionne une signature PAdES telle que profilée par ETSI EN 319 142-1, plutôt que la famille héritée adbe.pkcs7.detached. Et SetSignProcessReserveContentsBytes réserve de la place dans le placeholder /Contents : si vous comptez ajouter un horodatage de signature plus tard, le CMS agrandi doit tenir dans l’espace réservé maintenant, car le placeholder ne peut pas grossir ensuite sans resignature
GetSignProcessResult renvoie des résultats codés : 1 est le succès, 4 signifie un mauvais mot de passe PDF, 7 un mauvais mot de passe de certificat, 9 un PFX sans clé privée, 11 un échec lors de l’application de la signature. Journalisez le code plutôt qu’un booléen — une grande part des dossiers de support liés à la signature sont des confusions d’identifiants que seules ces valeurs distinguent
Relecture : auditer le fichier que vous venez de produire
Un atelier ne doit jamais faire confiance à son propre chemin d’écriture. La classe d’audit TPDFlibSignDoc rouvre la sortie signée et expose directement les entrées du dictionnaire de signature :
var
Doc: TPDFlibSignDoc;
Names: TStringList;
FS: TFileStream;
I: Integer;
SourceSize, RangeStart, GapStart, TailStart, TailLen: Int64;
begin
// Capture the size before Open: the audit object holds a share lock on the file
FS := TFileStream.Create('invoice-signed.pdf', fmOpenRead or fmShareDenyNone);
SourceSize := FS.Size;
FS.Free;
Doc := TPDFlibSignDoc.Create;
Names := TStringList.Create;
try
if not Doc.Open('invoice-signed.pdf', '', False) then Exit;
Doc.GetSignatureFieldNames(Names);
for I := 0 to Names.Count - 1 do
if Doc.GetSignatureValueObjNum(Names[I]) > 0 then // > 0 means the field is signed
begin
RangeStart := StrToInt64(string(Doc.GetSignatureValueByName(Names[I], 11)));
GapStart := StrToInt64(string(Doc.GetSignatureValueByName(Names[I], 12)));
TailStart := StrToInt64(string(Doc.GetSignatureValueByName(Names[I], 13)));
TailLen := StrToInt64(string(Doc.GetSignatureValueByName(Names[I], 14)));
if (RangeStart = 0) and (TailStart + TailLen = SourceSize) then
Writeln(Names[I], ': signature covers the file to EOF')
else
Writeln(Names[I], ': earlier revision, or unusual ByteRange layout');
end;
Doc.Close;
finally
Names.Free;
Doc.Free;
end;
end;
Les arguments ValueKey correspondent aux entrées du dictionnaire : 0 renvoie le CMS brut de /Contents, 2 et 3 les noms /Filter et /SubFilter, et 11 à 14 les quatre nombres ByteRange. Les valeurs textuelles passent par GetSignatureTextValueByName — la clé 0 est l’heure de signature revendiquée, la clé 5 distingue une signature ordinaire Sig d’un DocTimeStamp
La capture de taille en tête n’est pas décorative. TPDFlibSignDoc.Open maintient le fichier avec un verrou de partage restrictif pendant toute sa durée de vie ; tout ce qui a besoin des octets bruts, comme hacher la plage signée ou recalculer le digest CMS, doit donc lire le fichier avant Open. La démo SigningWorkbench de la bibliothèque lit le fichier entier en mémoire exactement pour cette raison
L’arithmétique ByteRange qui prouve la couverture
Un fichier sain avec une seule signature possède un ByteRange de la forme [0 a b c] : la couverture commence à l’offset 0, saute le placeholder hexadécimal /Contents entre a et b, puis reprend jusqu’à l’octet b+c. Quand b+c égale la taille du fichier, la signature couvre tout jusqu’à la fin du fichier. Quand ce n’est pas le cas, quelqu’un a ajouté une mise à jour incrémentale après signature. C’est légitime selon ISO 32000-1 §12.8 — remplissage ultérieur de formulaire, seconde signature, dictionnaire DSS, tout arrive ainsi — mais c’est précisément le fait qu’une piste d’audit doit enregistrer dès le départ plutôt que découvrir pendant un litige
Surveillez la largeur des entiers pendant le contrôle. L’API plate GetSignProcessByteRange renvoie un Integer 32 bits, alors que les valeurs sous-jacentes sont Int64 ; sur des fichiers au-delà de 2 GB, l’accesseur plat tronque. Utilisez la couche classe TPDFlibSigner.GetByteRange, qui renvoie Int64, ou analysez les valeurs issues de GetSignatureValueByName comme le fait le code d’audit ci-dessus
Ce qui reste sous votre responsabilité
Soyez lucide sur les frontières. L’API plate TPDFlib ne fournit aucun wrapper de vérification de signature ; la vérification cryptographique vit dans la couche classe avec TPDFlibSignatureVerifier, dont VerifySignature répond valide, invalide ou inconnu. Il n’existe pas non plus de client HTTP intégré pour les autorités d’horodatage RFC 3161 — la bibliothèque calcule le hash à soumettre et réintègre le CMS augmenté, mais l’aller-retour réseau appartient à votre code. Prévoyez les deux dans la conception de l’atelier dès le départ ; ils sont faciles à encapsuler, et pénibles à découvrir manquants au dernier sprint
Questions fréquentes
L’ajout d’une signature casse-t-il la conformité PDF/A ? Pas en soi. La signature arrive sous forme de mise à jour incrémentale, et ISO 19005-2 et les versions suivantes autorisent explicitement les documents signés. L’apparence de signature, en revanche, obéit aux mêmes règles que tout contenu de page : polices intégrées, pas de couleur dépendante du périphérique. La porte finale de l’atelier doit donc être une dernière exécution de preflight sur la sortie signée
Pourquoi mon fichier passe ici mais échoue dans un validateur externe ? Les validateurs implémentent des ensembles de règles qui se recouvrent sans être identiques. Traitez CheckFileCompliance comme la porte rapide du pipeline et vérifiez les candidats à la release avec un outil indépendant tel que veraPDF ; lorsque les deux divergent, le texte du constat nomme généralement la clause à lire
Puis-je signer et horodater en une seule passe ? Non — la signature de base est écrite d’abord, puis un processus d’horodatage distinct augmente le CMS à l’intérieur de l’espace /Contents réservé. C’est pourquoi l’appel de réserve d’octets dans l’exemple de signature compte ; dimensionnez-le pour le jeton d’horodatage attendu
Où aller ensuite
Pour les couches d’horodatage et de validation à long terme qui s’appuient sur cet atelier, consultez le guide de signature et de validation PAdES. La moitié preflight est détaillée plus loin dans le guide de preflight PDF/A et PDF/UA
La documentation complète de l’API et les téléchargements d’essai sont sur la page produit PDFlibPas