Article technique

Signatures numériques PAdES en Delphi : signature et validation avec PDFlibPas

Adobe Acrobat déclarait toutes les signatures du lot valides. Le contrôleur de conformité eIDAS du client les rejetait toutes. L'écart tenait à un seul nom dans le dictionnaire de signature : les fichiers portaient /SubFilter /adbe.pkcs7.detached, ce qui produit une signature ISO 32000-1 §12.8 parfaitement saine, mais pas une signature PAdES conforme, car ETSI EN 319 142-1 exige ETSI.CAdES.detached à chaque niveau baseline. La cryptographie était irréprochable ; le document ne revendiquait simplement pas le profil demandé par le régulateur. Si votre application Delphi signe des factures, contrats ou rapports de laboratoire qui doivent survivre à une politique de validation de style européen, cette distinction est la première à maîtriser — et c'est un appel dans losLab PDF Library (PDFlibPas), dont cet article parcourt la chaîne de signature, d'horodatage et de DSS depuis la signature baseline jusqu'à la validation long terme.

Ce qui transforme une signature PDF en signature PAdES

ETSI EN 319 142-1 définit quatre niveaux baseline empilés sur le format CMS. PAdES-B-B est le point d'entrée : une signature CAdES dans un champ de signature PDF avec le SubFilter ETSI.CAdES.detached et un attribut signé de certificat de signature. PAdES-B-T ajoute un horodatage RFC 3161 sur la valeur de signature, prouvant que la signature existait avant un instant que personne ne peut antidater. PAdES-B-LT embarque les certificats, CRL et réponses OCSP nécessaires à la validation dans un Document Security Store, afin que le fichier reste vérifiable après le retrait de l'infrastructure de l'autorité émettrice. PAdES-B-LTA coiffe l'ensemble avec un horodatage de document qui reprotège les preuves accumulées à mesure que les algorithmes faiblissent.

PDFlibPas mappe ces concepts sur son API de processus de signature. Le marqueur de profil est SetSignProcessCustomSubFilter ; les indications ETSI de type d'engagement — preuve d'origine, preuve d'approbation et les autres identifiants ETSI 1 à 6 — passent par SetSignProcessCommitmentType ; une politique de signature explicite s'attache avec SetSignProcessSignaturePolicy, qui prend l'OID de politique et son empreinte. Un défaut mérite attention : lorsque l'algorithme de hachage reste en automatique, la bibliothèque choisit SHA-256 pour les signatures ETSI et adbe.pkcs7.detached, et ne retombe sur SHA-1 que sur l'ancien chemin adbe.pkcs7.sha1. Définissez-le quand même explicitement ; les auditeurs demandent, et l'explicite bat l'inféré dans toute revue de conformité.

Produire la signature baseline

L'API flat pilote la signature comme une machine d'état en une passe : ouvrir un processus sur le fichier source, le configurer, terminer vers un fichier de sortie, lire le code de résultat. La séquence ci-dessous produit une signature PAdES-B-B en SHA-256 — et réserve volontairement de l'espace supplémentaire dans l'emplacement /Contents, la seule ligne que vous ne pourrez pas ajouter après coup si un horodatage doit un jour être ajouté à cette signature.

var
  Pdf: TPDFlib;
  SignId: Integer;
begin
  Pdf := TPDFlib.Create;
  try
    SignId := Pdf.NewSignProcessFromFile('invoice.pdf', '');
    if SignId = 0 then
      raise Exception.Create('cannot open source PDF');
    Pdf.SetSignProcessField(SignId, 'Sig1');
    Pdf.SetSignProcessPFXFromFile(SignId, 'company.pfx', PfxPassword);
    Pdf.SetSignProcessInfo(SignId, 'Approved', 'Vienna', 'billing@example.com');
    Pdf.SetSignProcessCustomSubFilter(SignId, 'ETSI.CAdES.detached');
    Pdf.SetSignProcessDigestAlgorithm(SignId, 2);          // SHA-256
    Pdf.SetSignProcessReserveContentsBytes(SignId, 8192);  // room for a timestamp later
    Pdf.EndSignProcessToFile(SignId, 'invoice-signed.pdf');
    if Pdf.GetSignProcessResult(SignId) <> 1 then
      raise Exception.CreateFmt('signing failed, code %d',
        [Pdf.GetSignProcessResult(SignId)]);
    Pdf.ReleaseSignProcess(SignId);
  finally
    Pdf.Free;
  end;
end;

NewSignProcessFromFile renvoie 0 lorsque la source ne peut pas être ouverte du tout. Ensuite, GetSignProcessResult sépare les modes d'échec qui arrivent réellement en production : 4 pour un mauvais mot de passe PDF, 7 pour un mauvais mot de passe PFX, 9 pour un fichier de certificat sans clé privée, 10 pour un chemin de sortie non inscriptible, 11 pour un échec lors de l'application des octets de signature. Journaliser le code numérique à côté du nom du fichier d'entrée transforme un ticket de support vague en diagnostic d'une minute.

Ajouter l'horodatage RFC 3161 que la bibliothèque ne récupère pas pour vous

PDFlibPas n'inclut pas de client TSA, et c'est une frontière volontaire plutôt qu'un manque. La bibliothèque calcule le hash que l'autorité d'horodatage doit contresigner puis réembarque le CMS augmenté ensuite ; l'échange HTTP et la chirurgie CMS intermédiaire relèvent de l'appelant. Il y a une raison technique dure à cette séparation : le contrôle Windows CryptoAPI qui ajoute nominalement des attributs non signés, CMSG_CTRL_ADD_SIGNER_UNAUTH_ATTR, échoue avec CRYPT_E_INVALID_INDEX sur la disposition SignedData détachée utilisée par PAdES. Le CMS enrichi doit donc venir d'un encodeur CMS sous votre contrôle — aucune bibliothèque ne peut le faire discrètement avec un simple appel système.

var
  Pdf: TPDFlib;
  StsId: Integer;
  HashHex, TstDer, TsAttr, AugmentedCms: AnsiString;
begin
  Pdf := TPDFlib.Create;
  try
    StsId := Pdf.NewPAdESSignatureTimeStampProcessFromFile('invoice-signed.pdf', '');
    Pdf.SetPAdESSignatureTimeStampField(StsId, 'Sig1');
    Pdf.SetPAdESSignatureTimeStampDigestAlgorithm(StsId, 2);
    HashHex := Pdf.GetPAdESSignatureValueHashHex(StsId);
    // both calls below are application code: an HTTP POST to your TSA,
    // and a CMS re-encode that attaches the token as an unsigned attribute
    TstDer := RequestTimeStampToken(HashHex);
    TsAttr := Pdf.BuildPAdESSignatureTimeStampAttribute(TstDer);
    AugmentedCms := AttachUnsignedAttribute(Pdf.GetPAdESSignatureCMSBytes(StsId), TsAttr);
    Pdf.SetPAdESSignatureCMSBytes(StsId, AugmentedCms);
    Pdf.EndPAdESSignatureTimeStampProcessToFile(StsId, 'invoice-bt.pdf');
    if Pdf.GetPAdESSignatureTimeStampProcessResult(StsId) <> 1 then
      raise Exception.Create('timestamp embedding failed');
    Pdf.ReleasePAdESSignatureTimeStampProcess(StsId);
  finally
    Pdf.Free;
  end;
end;

Surveillez ici les codes de résultat : 12 signifie que le champ de signature nommé n'existe pas, 11 que le CMS existant n'a pas pu être analysé, et 13 que le CMS augmenté ne tient plus dans l'emplacement /Contents réservé. Le code 13 est celui qui fait mal, car la seule correction consiste à resignature : un jeton d'horodatage typique avec sa chaîne de certificats occupe 4 à 6 Ko, et la réservation de 8192 octets faite à l'étape B-B existe précisément pour que cette étape puisse aboutir.

La validation commence au ByteRange, pas à la chaîne de certificats

Une coche verte dans un visualiseur est une décision de confiance face au magasin de certificats de cette machine, pas un verdict structurel sur le fichier. La validation programmatique doit commencer plus bas, avec la question que les mises à jour incrémentales rendent subtile : quels octets chaque signature couvre-t-elle réellement ? Toutes les améliorations discutées ici — secondes signatures, dictionnaires DSS, horodatages de document — arrivent par mise à jour incrémentale, et chaque mise à jour ajoute des octets hors du /ByteRange de la signature précédente. Ces octets ajoutés sont légitimes, mais un validateur doit les classer selon la politique de modification du document ; le niveau DocMDP par champ est lisible avec GetSignatureDocMDPLevelByName.

var
  Doc: TPDFlibSignDoc;
  Names: TStringList;
  I: Integer;
  B0, B1, B2, B3, FileSize: Int64;
begin
  FileSize := TFile.GetSize('invoice-bt.pdf');  // before Open: SignDoc holds a share lock
  Doc := TPDFlibSignDoc.Create;
  try
    if not Doc.Open('invoice-bt.pdf', '', False) then
      raise Exception.Create('cannot open for audit');
    Names := TStringList.Create;
    try
      Doc.GetSignatureFieldNames(Names);
      for I := 0 to Names.Count - 1 do
        if Doc.GetSignatureValueObjNum(Names[I]) > 0 then   // >0 means actually signed
        begin
          B0 := StrToInt64(string(Doc.GetSignatureValueByName(Names[I], 11)));
          B1 := StrToInt64(string(Doc.GetSignatureValueByName(Names[I], 12)));
          B2 := StrToInt64(string(Doc.GetSignatureValueByName(Names[I], 13)));
          B3 := StrToInt64(string(Doc.GetSignatureValueByName(Names[I], 14)));
          if (B0 = 0) and (B2 + B3 = FileSize) then
            Writeln(Names[I], ': covers the file to EOF')
          else
            Writeln(Names[I], ': earlier revision, or unexpected ByteRange layout');
        end;
    finally
      Names.Free;
    end;
    Doc.Close;
  finally
    Doc.Free;
  end;
end;

Deux pièges vivent dans ce chemin d'audit. TPDFlibSignDoc.Open détient le fichier avec un verrou de partage exclusif ; un validateur qui veut aussi hacher les octets bruts du fichier pour une vérification CMS doit donc lire le fichier en mémoire avant de l'ouvrir pour audit — l'ordre compte. Et le pendant flat API GetSignProcessByteRange renvoie un Integer alors que les offsets sous-jacents sont en Int64 : au-delà de 2 Go, l'appel flat tronque silencieusement, d'où l'exemple qui récupère les offsets via la classe d'audit. Notez aussi ce qui est volontairement absent de la couche flat : il n'y a pas de wrapper VerifySignature. Les verdicts cryptographiques viennent de la classe TPDFlibSignatureVerifier, qui renvoie vsValid, vsInvalid ou vsUnknown, ou d'un validateur externe déjà approuvé par votre politique de conformité.

Validation long terme : DSS, VRI et horodatage de document

PAdES-B-LT existe parce que l'infrastructure de révocation est mortelle. ETSI EN 319 142-1 §5.4.2.2 spécifie le Document Security Store : un dictionnaire au niveau du document qui porte certificats, CRL et réponses OCSP, éventuellement indexé par signature via des entrées VRI clésées par le hash du /Contents de chaque signature. Le flux PDFlibPas reflète la conception de l'horodatage : NewPAdESDSSProcessFromFile ouvre le processus ; AddPAdESDSSCertificate, AddPAdESDSSCRL et AddPAdESDSSOCSP acceptent des blobs DER ; AddPAdESDSSVRI lie le matériel choisi à une signature ; EndPAdESDSSProcessToFile écrit le tout comme mise à jour incrémentale. Récupérer le matériel de révocation — et juger s'il est assez frais pour être embarqué — reste la responsabilité de l'appelant ; la bibliothèque garantit que les dictionnaires sont structurellement conformes, pas que votre répondeur OCSP a dit vrai.

Le point d'arrivée d'archivage, B-LTA, ajoute un horodatage de document : un champ de signature séparé dont le type est DocTimeStamp plutôt que Sig, produit via SetSignProcessDocTimeStamp avec une longueur de signature réservée. Pour les lecteurs antérieurs à ces structures, TPDFlibSignDoc.EnsurePAdESExtensions inscrit l'extension développeur ESIC dans le catalogue du document, signalant que le fichier utilise des fonctionnalités définies par l'ETSI.

Questions fréquentes

Pourquoi Acrobat indique-t-il « validité inconnue » alors que la structure PAdES est correcte ?

Parce que confiance et structure sont indépendantes. Le visualiseur ne peut pas chaîner le signataire vers une racine qu'il approuve sur cette machine — cas courant avec des CA privées et des certificats de test — alors que l'audit ByteRange et la vérification CMS réussissent au même moment. Distribuez correctement le certificat racine, ou évaluez par rapport aux listes de confiance de l'UE lorsque la qualification eIDAS est l'objectif réel.

Peut-on ajouter un horodatage à une signature qui n'a réservé aucun espace Contents supplémentaire ?

Généralement non. Le CMS augmenté doit tenir dans l'emplacement d'origine, et un emplacement de taille par défaut ajuste serré la signature initiale. Attendez-vous au code de résultat 13, et prévoyez de signer dès le départ avec SetSignProcessReserveContentsBytes.

Un horodatage de document remplace-t-il l'horodatage de signature ?

Non. L'horodatage de signature prouve quand une signature existait ; l'horodatage de document protège tout le fichier, y compris ses preuves DSS, et c'est l'élément qui se renouvelle sur des décennies. Les profils d'archivage finissent par porter les deux.

Pour le point de vue côté audit — énumérer les champs de signature sur un corpus, vider les dispositions ByteRange et lire les niveaux DocMDP en masse — voyez l'article compagnon sur le workbench de conformité et de signature. Les documents signés qui doivent aussi satisfaire une politique d'archivage relèvent du flux décrit dans le preflight PDF/A et PDF/UA en Delphi. La documentation complète de l'API et les téléchargements d'évaluation se trouvent sur la page produit losLab PDF Library pour Delphi.