Article technique

Atelier de conformité et de signature PDFlibPas en Delphi

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