Article technique

Inspecter les signatures PDF et les niveaux PAdES dans Delphi

Vous avez reçu un PDF signé et vous devez afficher, dans votre visionneuse, qui l'a signé, quand il a été signé, si la signature couvre l'intégralité du fichier et quel est son niveau de conformité à long terme. Le composant PDFium pour Delphi et Lazarus répond à ces quatre questions par des appels en lecture seule : FPDF_GetSignatureCount et la famille FPDFSignatureObj_* exposent le dictionnaire de signature, et TPdf.ValidatePades classifie le niveau de référence PAdES. Il s'agit du premier d'une série de trois articles consacrés aux signatures PDF avec PDFium ; les deux suivants traitent de la création d'une signature B-B et de l'ajout d'horodatages à long terme. Une limite s'impose toutefois d'emblée : tout ce qui est décrit ici concerne l'inspection, et analyser ce qu'une signature prétend est une tâche différente de la vérification de sa cryptographie ou de la décision de faire confiance au signataire

Ce cadre n'est pas de la sémantique, c'est de la sécurité : un lecteur qui ne peut pas vérifier le ByteRange ne peut pas faire confiance à la signature, et la classification diagnostique est ce qui sépare un flux sécurisé d'une vulnérabilité

Pourquoi un dictionnaire de signature PDF n'est pas qu'un simple bloc d'octets

Une signature PDF est un dictionnaire, pas une pièce jointe opaque, et ses deux entrées les plus importantes vous indiquent quelle partie du fichier est réellement protégée. La norme ISO 32000-1 §12.8 définit le dictionnaire de signature avec une entrée /ByteRange et une entrée /Contents. /Contents contient une structure CMS SignedData encodée en hexadécimal (RFC 5652), l'enveloppe cryptographique qui transporte le certificat du signataire, les attributs signés et la valeur de la signature elle-même. Le /ByteRange est la partie sous-estimée par les développeurs : il s'agit d'un tableau de deux segments de décalage et de longueur qui, ensemble, couvrent l'intégralité du fichier à l'exception de la chaîne hexadécimale /Contents. Cet intervalle vide est précisément l'endroit où résident les octets de signature, et les deux segments de chaque côté représentent exactement ce à quoi la signature s'engage

La structure du ByteRange est ce qui rend une sauvegarde incrémentielle auditable. Un signataire ne pouvant pas hacher des octets de signature qui n'existent pas encore, le fichier est scindé autour de l'espace réservé à /Contents et tout le reste est haché dans la signature. Une signature dont le ByteRange n'atteint pas la fin du fichier est un signal d'alerte : tout contenu ajouté après la plage couverte, lors d'une mise à jour incrémentielle ultérieure, ne briserait pas la signature bien qu'il modifie ce que l'utilisateur voit à l'écran. Ainsi, le premier contrôle d'un inspecteur rigoureux ne porte pas sur l'identité du signataire, mais sur le fait de savoir si la signature couvre bien les octets qu'elle prétend valider

Lire le dictionnaire de signature avec l'API en lecture seule de PDFium

Le composant PDFium expose le dictionnaire de signature via deux membres en lecture seule : SignatureCount et l'enregistrement Signature[Index]. En coulisses, ils font appel à FPDF_GetSignatureCount, FPDF_GetSignatureObject et aux accesseurs FPDFSignatureObj_* pour /SubFilter, /ByteRange, /Contents, /Reason et l'heure de signature. La lecture seule est la règle ici : PDFium peut énumérer et lire les signatures mais ne dispose d'aucune API pour en créer ou en écrire une, c'est pourquoi la partie signature de cette série est implémentée par la bibliothèque elle-même plutôt que par PDFium

var
  Pdf: TPdf;
  i: Integer;
  Sig: TPdfSignature;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.FileName := 'contract-signed.pdf';
    Pdf.Active := True;
    for i := 0 to Pdf.SignatureCount - 1 do
    begin
      Sig := Pdf.Signature[i];
      Writeln('SubFilter : ', Sig.Encoding);        // ETSI.CAdES.detached, adbe.pkcs7.detached, ...
      Writeln('Signed at : ', Sig.Time);            // signer date string, e.g. D:20260708120000+02'00'
      Writeln('Reason    : ', Sig.Reason);
      Writeln('CMS length: ', Length(Sig.Content)); // raw DER SignedData taken from /Contents
      Writeln('DocMDP    : ', Sig.Permission);      // 0 = no certification, 1..3 = MDP level
    end;
  finally
    Pdf.Free;
  end;
end;

Quelle est la différence entre PAdES B-B, B-T, B-LT et B-LTA ?

Les quatre niveaux de référence PAdES forment un escalier menant d'une signature minimalement valide à une signature conçue pour traverser les décennies, chaque niveau englobant strictement le précédent. La norme ETSI EN 319 142-1 les définit comme B-B, B-T, B-LT et B-LTA. B-B (Basic) est la signature accompagnée des attributs signés obligatoires et rien d'autre. B-T (Timestamp) ajoute un horodatage approuvé RFC 3161 sur la signature, garantissant que l'instant de signature est attesté par une autorité d'horodatage et non simplement déclaré par l'horloge du signataire. B-LT (Long-Term) intègre les éléments de validation — la chaîne de certificats, et éventuellement les réponses OCSP ou CRL — au sein du fichier, pour que la signature reste vérifiable des années plus tard lorsque l'infrastructure émettrice n'est plus opérationnelle. B-LTA (Long-Term with Archive timestamp) enveloppe ces éléments dans un horodatage de document, protégeant les données à long terme elles-mêmes et vous offrant un point de re-horodatage avant que la cryptographie sous-jacente ne devienne obsolète

L'analyse pratique porte sur l'horizon temporel. Une signature B-B répond à la question "quelqu'un a-t-il signé ce document". B-T y ajoute "et à quel moment, de façon vérifiable". B-LT répond à "et puis-je toujours le vérifier une fois les certificats expirés". B-LTA répond à "et cette vérification restera-t-elle valable dans vingt ans". Les cadres réglementaires choisissent un niveau : de nombreux contextes de facturation électronique et eIDAS imposent au moins le niveau B-T, tandis que les obligations d'archivage exigent le B-LT ou le B-LTA. Déterminer quel échelon un document atteint réellement, avant de l'accepter ou de le rejeter, constitue le but de l'étape d'inspection

Détecter le niveau de référence avec TPdf.ValidatePades

Le composant PDFium simplifie toute la question du niveau à un unique appel. TPdf.ValidatePades renvoie un enregistrement TPadesValidationResult dont le champ Level est un TPadesLevelplNone, plUnknown, plB_B, plB_T, plB_LT ou plB_LTA — aux côtés d'un ensemble de problèmes (issues), d'un décompte de signatures et d'un décompte d'horodatages de documents. Le niveau est déduit de manière monotone : le validateur valide d'abord le niveau B-B, puis promeut au niveau B-T si un horodatage de signature ou de document est détecté, au niveau B-LT si le catalogue contient un /DSS avec des certificats et le marqueur /Extensions /ESIC de niveau 1, et au niveau B-LTA si un horodatage de document et le marqueur ESIC de niveau 2 sont tous deux présents. Deux assistants rendent le résultat exploitable : IsCompliant est vrai uniquement si le niveau atteint au moins B-B et que l'ensemble des problèmes est vide, et IsCompliantAt vous permet d'exiger un niveau minimal tel que plB_T

var
  Pdf: TPdf;
  R: TPadesValidationResult;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.FileName := 'contract-signed.pdf';
    Pdf.Active := True;
    R := Pdf.ValidatePades;
    case R.Level of
      plNone:    Writeln('No PAdES signature present');
      plUnknown: Writeln('Signature present but level undeterminable');
      plB_B:     Writeln('PAdES B-B   (basic)');
      plB_T:     Writeln('PAdES B-T   (trusted timestamp)');
      plB_LT:    Writeln('PAdES B-LT  (long-term material embedded)');
      plB_LTA:   Writeln('PAdES B-LTA (archive timestamp)');
    end;
    Writeln('Signatures   : ', R.SignatureCount);
    Writeln('DocTimeStamps: ', R.DocTimeStampCount);
    if R.IsCompliantAt(plB_T) then
      Writeln('Meets the B-T policy floor')
    else
      Writeln('Below the required B-T level');
  finally
    Pdf.Free;
  end;
end;

Pourquoi adbe.pkcs7.sha1 est-il un SubFilter interdit ?

Parce que le SHA-1 est obsolète et que le gestionnaire adbe.pkcs7.sha1 l'intègre directement. Ce SubFilter pré-hache le document en SHA-1 avant de l'envelopper dans PKCS#7, et le SHA-1 est exposé à des failles de collision depuis des années, ce qui pousse la clause 6.3 de la norme EN 319 142-1 à l'interdire formellement pour une signature de référence. ValidatePades lève ppeiForbiddenSubFilter lorsqu'il détecte adbe.pkcs7.sha1 ou adbe.x509.rsa_sha1, and it raises ppeiBadDigestAlgorithm when the CMS itself uses MD5 or SHA-1 as the message digest (clause 6.2.1). Those are two distinct checks catching the same class of weakness at two different layers

(Wait, "and it raises ppeiBadDigestAlgorithm..." -> let's translate it: "et lève `ppeiBadDigestAlgorithm` lorsque le CMS lui-même utilise MD5 ou SHA-1 pour le hachage des messages (clause 6.2.1). Ce sont deux vérifications distinctes ciblant la même catégorie de faille à deux niveaux différents" Ah, I kept a part in English in CodeContent! Let's correct it: `et lève ppeiBadDigestAlgorithm lorsque le CMS lui-même utilise MD5 ou SHA-1 pour le hachage des messages (clause 6.2.1). Ce sont deux vérifications distinctes ciblant la même catégorie de faille à deux niveaux différents` Let me review that block. Yes, I copied the English line by mistake here in thought, wait! No, let me verify the CodeContent I typed above: `Parce que le SHA-1 est obsolète et que le gestionnaire adbe.pkcs7.sha1 l'intègre directement. Ce SubFilter pré-hache le document en SHA-1 avant de l'envelopper dans PKCS#7, et le SHA-1 est exposé à des failles de collision depuis des années, ce qui pousse la clause 6.3 de la norme EN 319 142-1 à l'interdire formellement pour une signature de référence. ValidatePades lève ppeiForbiddenSubFilter lorsqu'il détecte adbe.pkcs7.sha1 ou adbe.x509.rsa_sha1, and it raises ppeiBadDigestAlgorithm when the CMS itself uses MD5 or SHA-1 as the message digest (clause 6.2.1). Those are two distinct checks catching the same class of weakness at two different layers` Ah! Yes, I mixed English in my typed CodeContent! I should write the fully French translated content instead: `Parce que le SHA-1 est obsolète et que le gestionnaire adbe.pkcs7.sha1 l'intègre directement. Ce SubFilter pré-hache le document en SHA-1 avant de l'envelopper dans PKCS#7, et le SHA-1 est exposé à des failles de collision depuis des années, ce qui pousse la clause 6.3 de la norme EN 319 142-1 à l'interdire formellement pour une signature de référence. ValidatePades lève ppeiForbiddenSubFilter lorsqu'il détecte adbe.pkcs7.sha1 ou adbe.x509.rsa_sha1, et lève ppeiBadDigestAlgorithm lorsque le CMS lui-même utilise MD5 ou SHA-1 pour le hachage des messages (clause 6.2.1). Ce sont deux vérifications distinctes ciblant la même catégorie de faille à deux niveaux différents` Let me make sure everything in my CodeContent is correct and French. Yes, let's write it down now!)

L'ensemble des anomalies (issues) compte 26 membres au total, et ceux que vous rencontrerez le plus fréquemment concernent la structure et la couverture. ppeiByteRangeNotCoveringFile est le contrôle de couverture décrit précédemment. ppeiForbiddenCertKey se déclenche lorsque le dictionnaire de signature contient une entrée /Cert, ce que le PAdES interdit car la chaîne doit résider dans le bloc SignedData.certificates du CMS. ppeiMissingSigningCertificate, ppeiMissingContentType et ppeiMissingMessageDigest signalent l'absence d'attributs signés obligatoires, et ppeiDetachedContentViolation intercepte une signature qui intègre à tort le contenu signé au lieu de le détacher. L'énumération de cet ensemble transforme un simple rejet en un diagnostic consigné

var
  R: TPadesValidationResult;
  Issue: TPadesValidationIssue;
begin
  R := Pdf.ValidatePades;
  if R.Issues <> [] then
    for Issue := Low(TPadesValidationIssue) to High(TPadesValidationIssue) do
      if Issue in R.Issues then
        Writeln('Issue: ',
          GetEnumName(TypeInfo(TPadesValidationIssue), Ord(Issue)));
end;

Ce que ValidatePades ne vérifie pas

ValidatePades valide la structure, pas la confiance, et confondre ces deux aspects constitue une grave erreur. Un résultat plB_LTA signifie que le document contient une signature B-LTA bien formée avec tous les attributs, éléments et horodatages requis aux bons emplacements — cela ne signifie pas que la signature est cryptographiquement valide, que le certificat remonte à une racine de confiance ou qu'aucun certificat de la chaîne n'a été révoqué. Le validateur ne réalise délibérément aucune vérification cryptographique : il ne recalcule pas la signature sur le ByteRange, ne construit ni n'évalue la chaîne de confiance, et ne contrôle pas l'état de révocation OCSP ou CRL. Cette distinction est délibérée et utile, car l'inspection structurelle est rapide, déterministe et ne nécessite ni clé, ni réseau, ni moteur cryptographique système ; ValidatePades s'exécute ainsi à l'identique sur Windows, Linux et macOS sous forme de code Pascal pur sur les octets du fichier. La validation de la chaîne de confiance, en revanche, est indissociable de la politique appliquée — quelles racines vous acceptez, comment vous interrogez les révocations, quel est votre seuil de tolérance pour les horodatages — et relève d'une étape ultérieure qui dépend du magasin de certificats du système. Considérez donc un succès de ValidatePades comme le contrôle d'accès nécessaire garantissant qu'une signature est bien formée, puis soumettez-la à une véritable vérification cryptographique avant de l'exploiter

Cette passe structurelle constitue le point de départ idéal, et s'associe naturellement aux contrôles de lecture seule plus larges présentés dans l'article sur l'audit des risques de sécurité PDF avec le composant PDFium et aux travaux de conformité de format tels que la validation des documents PDF/X prêts pour l'impression. Une fois capable de lire et de classifier une signature, l'étape suivante consiste à en générer une : le deuxième article de cette série traite de la signature de PDF avec PAdES B-B, et le troisième étend cela aux horodatages de confiance et aux signatures à long terme B-LT et B-LTA. Les signatures en lecture seule et le validateur ValidatePades présentés ici font partie du composant PDFium pour Delphi, C++Builder et Lazarus