Article technique

Signatures numériques PDF et PAdES dans Delphi avec HotPDF

Avant qu'un lecteur affiche la coche verte sur un PDF signé, il fait trois choses mécaniques : il lit le tableau /ByteRange du dictionnaire de signature, hache exactement les deux plages d'octets décrites par ce tableau, puis vérifie la signature CMS stockée, en hexadécimal, dans l'entrée /Contents placée entre ces plages. Presque tous les échecs de signature en production remontent à l'un de ces trois gestes mal arrangés : un emplacement réservé trop petit pour le blob de signature final, un hash calculé sur les mauvais octets, ou un enregistrement après signature qui réécrit des octets déjà couverts par les plages. La cryptographie échoue presque jamais. La tenue des comptes en octets, elle, échoue.

HotPDF offre aux applications Delphi et C++Builder trois niveaux de support de signature : signature PFX en un appel, workflow de signataire externe pour HSM et services de signature, et champs profilés PAdES avec horodatages de document. Ils sont présentés dans cet ordre, car chaque niveau existe pour traiter un mode de défaillance du précédent.

Le contrat ByteRange dans ISO 32000-1 §12.8

Une signature PDF doit vivre dans le fichier qu'elle signe, ce qui crée un problème de poule et d'œuf : la valeur de signature ne peut pas se couvrir elle-même. Le format le résout par un trou. L'écrivain réserve une entrée /Contents de taille fixe remplie de zéros, et /ByteRange enregistre deux plages, tout ce qui précède le trou et tout ce qui le suit. Le signataire hache ces deux plages, puis la structure CMS résultante est écrite dans le trou en hexadécimal. La conséquence qui piège les ingénieurs : la taille de réservation est figée avant la signature ; la signature finale, certificats compris, doit donc tenir dans un trou choisi plus tôt. Environ 8 Ko suffisent à une signature CMS détachée avec une courte chaîne de certificats.

HotPDF expose directement cette distinction. AddSignatureField crée un champ visible vide que quelqu'un signera plus tard dans un lecteur ; AddSignedSignatureField crée le champ et réserve le trou /Contents pour une complétion programmatique. Choisir le mauvais est une erreur classique de première semaine : un champ vide ne donne rien à remplir à un signataire externe.

Un seul appel quand la clé est dans un fichier PFX

Quand le certificat de signature et la clé privée vivent dans un fichier PFX/PKCS#12, tout le pipeline tient dans une fonction de classe :

if THotPDF.SignPDFWithPFX('invoice-unsigned.pdf', 'invoice-signed.pdf',
    'company-cert.pfx', 'pfx-password') then
  Writeln('Signed: invoice-signed.pdf')
else
  raise Exception.Create('PFX signing failed');

L'échec qui domine le support ici n'a rien à voir avec le PDF : le conteneur PFX lui-même. HotPDF lit les fichiers PFX protégés par PBES2, dérivation PBKDF2 avec AES-256-CBC. Les conteneurs exportés par d'anciens assistants de certificats Windows, ou par des versions d'OpenSSL antérieures à 3.0, utilisent par défaut une protection héritée RC2/3DES et ne seront pas parsés. Le remède est une réexportation unique du conteneur avec des paramètres modernes, ce que l'OpenSSL actuel fait par défaut, pas un changement de code. Vérifiez cela en premier quand la signature échoue immédiatement sur un certificat qui « marche partout ailleurs ».

Signature externe : réserver, hacher, insérer

Le chemin en un appel suppose que la clé privée est un fichier lisible par votre processus. Les clés de signature de production le sont de moins en moins : elles résident dans un HSM, un jeton USB ou un service de signature distant, et aucune bibliothèque ne peut les appeler directement. Pour cette topologie, HotPDF sépare le workflow en étapes au niveau octets : écrire un document avec placeholder, calculer les plages de hash, transmettre l'entrée de hash à ce qui détient la clé, puis recoller le CMS retourné.

var
  Doc: THotPDF;
  Fs: TFileStream;
  PdfBytes, HashInput, SigHex: AnsiString;
  R1Start, R1Len, R2Start, R2Len, CStart, CLen: Integer;
begin
  // 1. Write the document with a reserved /Contents hole
  Doc := THotPDF.Create(nil);
  try
    Doc.FileName := 'placeholder.pdf';
    Doc.BeginDoc;
    Doc.CurrentPage.AddSignedSignatureField('Sig1',
      Rect(50, 100, 350, 150), 8192, 'adbe.pkcs7.detached',
      'Contract approval', 'Boston, MA', 'legal@example.com');
    Doc.EndDoc;
  finally
    Doc.Free;
  end;

  // 2. Load the saved bytes; the returned offsets are 0-based
  Fs := TFileStream.Create('placeholder.pdf', fmOpenRead);
  try
    SetLength(PdfBytes, Fs.Size);
    Fs.ReadBuffer(PdfBytes[1], Fs.Size);
  finally
    Fs.Free;
  end;
  THotPDF.PreparePDFForSigning(PdfBytes, R1Start, R1Len, R2Start, R2Len,
    CStart, CLen);

  // 3. Hash both spans and sign externally (HSM, token, service)
  HashInput := Copy(PdfBytes, R1Start + 1, R1Len) +
               Copy(PdfBytes, R2Start + 1, R2Len);
  SigHex := SignWithHsm(HashInput);  // your integration: returns CMS as hex

  // 4. Splice the signature into the reserved hole
  THotPDF.InsertSignatureHex(PdfBytes, SigHex);
  Fs := TFileStream.Create('signed.pdf', fmCreate);
  try
    Fs.WriteBuffer(PdfBytes[1], Length(PdfBytes));
  finally
    Fs.Free;
  end;
end;

Deux contraintes de cette séquence provoquent des échecs intermittents en production quand elles sont oubliées. Premièrement, PreparePDFForSigning travaille sur les octets d'un fichier entièrement enregistré : le document placeholder doit être complètement écrit avant que les plages aient un sens ; calculer des plages sur un flux en cours produit des offsets qui ne correspondent plus à la sérialisation finale. Deuxièmement, la réservation de 8192 octets doit contenir le CMS final. Une signature intégrant des certificats intermédiaires, ou retournée par un service qui ajoute des attributs signés, peut la dépasser ; InsertSignatureHex ne peut pas agrandir le trou. Le symptôme est un job qui réussit avec un certificat et échoue avec un autre ; la correction consiste à régénérer le placeholder avec une réservation plus grande, dimensionnée à partir d'une vraie signature produite par le signataire réel.

Baselines PAdES et horodatages de document

La réglementation européenne de signature s'appuie sur ETSI EN 319 142-1, qui définit quatre niveaux baseline PAdES : B-B est la signature de base ; B-T ajoute un horodatage de confiance prouvant quand elle a été faite ; B-LT intègre dans le document les matériaux de validation, certificats et données de révocation ; B-LTA ajoute des horodatages périodiques du document afin que la preuve survive au vieillissement des algorithmes. HotPDF crée les structures côté document pour ce cycle de vie :

// PAdES baseline signature field (ETSI EN 319 142-1)
Pdf.CurrentPage.AddPAdESSignatureField(
  'ApprovalSig', Rect(50, 100, 350, 150), 'B-B',
  'Contract approval', 'Boston, MA', 'legal@example.com');

// Document timestamp: larger reservation for the TSA token and chain
Pdf.CurrentPage.AddDocumentTimestampSignature('ArchiveTS', 16384);

Notez la réservation de 16384 octets sur l'horodatage : le jeton d'une autorité d'horodatage transporte sa propre chaîne de certificats et dépasse donc régulièrement les 8 Ko suffisants pour une signature simple. Les horodatages de document sont aussi le mécanisme derrière la maintenance B-LTA : réhorodater une archive signée tous les quelques ans avec des algorithmes courants est ce qui maintient une signature 2026 vérifiable en 2040.

Les chaînes reason, location et contact acceptées par les deux appels de champ méritent une note de politique : elles sont stockées comme entrées de dictionnaire ordinaires et rendues dans l'apparence visible, mais rien ne les vérifie. Elles documentent l'intention pour les lecteurs humains, « Approbation du contrat », une ville, une boîte mail, et les auditeurs les liront ; remplissez-les donc de façon cohérente depuis les données de workflow. Ne traitez simplement jamais le texte visible comme preuve : l'énoncé cryptographique vit entièrement dans le CMS et sa chaîne de certificats, et un validateur ignore totalement l'apparence.

Pourquoi les fichiers signés ne doivent que grandir

Une fois la signature présente, les octets situés dans ses plages sont gelés pour toujours. Les mises à jour incrémentales ISO 32000-1 §7.5.6 sont la seule manière légitime de changer le fichier ensuite : les objets nouveaux et modifiés sont ajoutés après les octets originaux, avec une nouvelle section de références croisées chaînée vers l'ancienne. La signature reste valide pour sa révision, et les lecteurs rapportent l'état honnête : « révision signée intacte, document modifié ensuite ». Une resérialisation complète, à l'inverse, réécrit les plages signées et détruit la signature, même si aucun élément visible n'a changé. Les mécaniques d'enregistrement append-only, et le moment où les compacter, sont couvertes dans l'article sur les flux d'objets et les mises à jour incrémentales.

Une contrainte de bibliothèque complète la planification : le mode PDF/A de HotPDF rejette les champs de signature ; la conformité d'archivage et les signatures intégrées doivent donc être séparées en livrables distincts plutôt que combinées dans un seul fichier. La signature est également orthogonale à la confidentialité : elle prouve origine et intégrité mais ne cache rien, domaine couvert par le chiffrement AES-256 et la politique de permissions.

Les tests d'acceptation d'un pipeline de signature doivent être indépendants du code qui a produit le fichier. Ouvrez la sortie dans le panneau de signatures d'Acrobat et confirmez trois états : la signature est valide, l'identité chaîne vers la racine attendue, et le panneau ne signale aucun changement après signature. Corrompez ensuite un octet dans la plage signée d'une copie et confirmez que le même panneau signale le document comme altéré ; un pipeline dont on n'a jamais vu la validation échouer est un pipeline dont la validation n'a pas vraiment été testée.

Questions qui reviennent en revue de code de signature

Quelle taille prévoir pour la réservation /Contents ?

8192 octets pour un CMS détaché avec une courte chaîne ; 16384 lorsque des horodatages ou des intermédiaires intégrés sont concernés. Mesurez le CMS produit par votre vrai signataire et ajoutez de la marge : la réservation ne peut pas grandir plus tard.

Un document peut-il porter deux signatures ?

Oui. Chaque signature vit dans sa propre révision incrémentale, et les plages de la seconde signature couvrent la première, exactement comme se construisent les workflows de contre-signature.

La signature protège-t-elle le contenu du document ?

Non. Une signature fournit une preuve d'intégrité et d'origine ; tout le monde peut encore lire le fichier. La confidentialité exige un chiffrement configuré séparément.

Les trois niveaux de signature sont livrés avec le HotPDF Component pour Delphi et C++Builder ; la page produit renvoie à la référence complète de l'API de signature.