Article technique

Vérifier les signatures numériques de PDF en Delphi avec HotPDF

HotPDF vérifie les signatures numériques dans les documents PDF chargés via trois méthodes de THotPDF : GetLoadedSignatureInfo, VerifyLoadedSignature et VerifyLoadedSignatureEx, introduites dans la version 2.259.0. Le composant re-hache les segments de /ByteRange du fichier d'origine, contrôle l'attribut CMS messageDigest et exécute une vérification RSA PKCS#1 v1.5 par rapport au certificat de signature intégré, en renvoyant svValid lorsque les octets du document sont intacts

Le scénario est banal, mais les enjeux ne le sont pas. Un cocontractant renvoie un contrat signé, votre flux de travail doit l'archiver, et quelqu'un pose la seule question qui importe : s'agit-il du document que nous avons envoyé, octet par octet, signé par le certificat déclaré ? Répondre à cela en code constitue le volet vérification de la signature ; le volet création, qui consiste à générer et intégrer des signatures PAdES au départ, est traité dans l'article connexe sur la création de signatures numériques PAdES avec HotPDF. Cet article traite de l'autre sens : un PDF arrive déjà signé, et vous souhaitez obtenir un verdict par programme plutôt qu'une capture d'écran de la coche verte d'Acrobat

Comment un PDF signé prouve-t-il qu'il n'a pas été altéré ?

Une signature PDF protège des plages d'octets spécifiques du fichier, et non une notion abstraite de « document ». L'ISO 32000-1 §12.8 définit le mécanisme : le champ de formulaire de signature porte un dictionnaire dont l'entrée /Contents contient un conteneur CMS SignedData (RFC 5652), et dont le tableau /ByteRange nomme les régions exactes du fichier couvertes par la signature, conformément au §12.8.1. Le tableau est une liste de paires de décalages et de longueurs, en pratique deux segments : tout ce qui précède la chaîne hexadécimale de /Contents, et tout ce qui la suit. La valeur de la signature ne pouvant pas se couvrir elle-même, le fichier est haché autour de cette zone

Cette conception a une conséquence qui structure l'ensemble de l'API : la vérification doit hacher les octets sérialisés d'origine, exactement tels qu'ils se trouvent sur le disque. Un modèle d'objet analysé est inutile pour cela, car la resérialisation d'un document, même inchangé, produit des octets différents. HotPDF effectue donc la vérification par rapport au fichier source à partir duquel le document a été chargé, ou par rapport à un TStream d'octets bruts que vous fournissez, jamais par rapport à sa représentation en mémoire

Lire les métadonnées de la signature avant toute vérification

La fonction GetLoadedSignatureInfo analyse le dictionnaire de signature et son conteneur CMS sans toucher au moindre octet du document, ce qui en fait l'appel idéal lorsque vous avez seulement besoin d'afficher l'auteur de la signature et sa date. Les champs de signature sont indexés à partir de 0 dans l'ordre des champs du formulaire, et GetLoadedSignatureFieldCount indique le nombre de champs existants. L'enregistrement THPDFSignatureInfo renvoyé contient le nom du champ, le /SubFilter, le nom commun (common name) du certificat du signataire, les noms distinctifs (distinguished names) du sujet et de l'émetteur, le numéro de série, les dates de validité, l'heure de signature (à partir de l'attribut signé s'il est présent, sinon à partir de l'entrée /M du dictionnaire), le nom de l'algorithme de hachage, ainsi que les chaînes /Reason, /Location et /ContactInfo. Son membre Status reste à svNotVerified, une désignation honnête pour « analysé, non vérifié »

var
  Pdf: THotPDF;
  Info: THPDFSignatureInfo;
  I: Integer;
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.LoadFromFile('signed-contract.pdf');
    for I := 0 to Pdf.GetLoadedSignatureFieldCount - 1 do
    begin
      Info := Pdf.GetLoadedSignatureInfo(I);
      Writeln('Field:     ', Info.FieldName);
      Writeln('Signer:    ', Info.SignerName);
      Writeln('Issuer:    ', Info.IssuerDN);
      Writeln('Algorithm: ', Info.HashAlgorithm);
      Writeln('SubFilter: ', Info.SubFilter);
    end;
  finally
    Pdf.Free;
  end;
end;

Exécuter le contrôle cryptographique

La fonction VerifyLoadedSignatureEx effectue la vérification complète d'un document chargé depuis un fichier et renvoie l'enregistrement d'informations complété en un seul appel : elle réouvre le fichier source, hache les segments de /ByteRange avec l'algorithme de hachage de SignerInfo, compare le résultat à l'attribut signé messageDigest (RFC 5652 §5.4), puis effectue la vérification RSA de la signature sur le ré-encodage DER SET des attributs signés. Lorsqu'une signature ne contient pas d'attributs signés, le contrôle RSA s'exécute directement sur le hachage du document. Les signatures prises en charge sont RSA PKCS#1 v1.5 avec des hachages SHA-1, SHA-256, SHA-384 ou SHA-512, ce qui couvre les sous-filtres adbe.pkcs7.detached et ETSI.CAdES.detached générés par les principaux outils de signature

var
  Status: THPDFSignatureVerifyStatus;
  Info: THPDFSignatureInfo;
begin
  Status := Pdf.VerifyLoadedSignatureEx(0, Info);
  case Status of
    svValid:
      if Info.CoversWholeDocument then
        Writeln('Valid; signature covers the whole file')
      else
        Writeln('Valid; file was extended after signing');
    svDigestMismatch:
      Writeln('Document bytes changed after signing');
    svSignatureInvalid:
      Writeln('RSA check failed over signed attributes');
    svUnsupportedAlgorithm:
      Writeln('Non-RSA key or unknown digest algorithm');
    svMalformed:
      Writeln('CMS container could not be parsed');
    svSourceUnavailable:
      Writeln('No source bytes; use the TStream overload');
  end;
end;

Deux détails d'implémentation méritent d'être connus car ils expliquent des échecs qui peuvent sembler mystérieux vus de l'extérieur. Premièrement, le contrôle des attributs signés est strict sur l'encodage : dans le fichier, les attributs sont marqués [0] IMPLICIT, mais la signature a été calculée sur leur forme DER SET OF, de sorte que le vérificateur applique de nouveau le marquage avant le hachage, exactement comme l'exige la RFC 5652 §5.4. Un vérificateur développé manuellement qui hacherait les octets tels qu'ils apparaissent dans le fichier rejetrait tous les documents correctement signés. Deuxièmement, l'élément /Contents est classiquement complété par des zéros pour correspondre à un espace d'octets réservé, de sorte que le vérificateur tronque le blob DER à la longueur réelle de sa structure SEQUENCE externe avant l'analyse ; la présence de zéros de fin à l'aspect incohérent est normale et ne constitue pas une corruption. La même famille de risques liés à l'analyse ASN.1, côté importation de certificats, est traitée dans l'article sur le renforcement de la sécurité PKCS#12 et ASN.1 dans HotPDF

Que garantit réellement une signature valide ?

La valeur svValid signifie précisément ceci : les octets nommés par /ByteRange produisent le hachage signé par le signataire, et la signature est vérifiée sous la clé publique du certificat intégré dans le conteneur CMS. Il s'agit d'une intégrité d'octets et d'une liaison de clé, rien de plus. La validation de la chaîne de certificats et de la confiance est explicitement hors de portée pour le vérificateur de HotPDF : il ne parcourt pas la chaîne jusqu'à une racine, ne vérifie pas la révocation et ne consulte aucun magasin de confiance. Un certificat auto-signé provenant d'un attaquant qui a signé de nouveau un document modifié se vérifiera comme svValid, car la logique mathématique interne est respectée. Savoir si le signataire est bien celui qu'il prétend être, et si vous devez lui faire confiance, est une décision de politique générale qui relève d'une autre couche, qu'il s'agisse de la liste blanche de certificats de votre organisation, du magasin de certificats Windows ou d'une autorité de validation

Le drapeau CoversWholeDocument protège d'un écart plus subtil. Une signature ne couvre jamais que son /ByteRange, et le mécanisme de mise à jour incrémentielle du PDF permet d'ajouter du contenu après une signature sans l'invalider, ce qui est prévu par conception et permet le fonctionnement des flux de signature multiples. Ce drapeau est calculé lors de la vérification et n'est vrai que si les deux segments et l'espace de /Contents couvrent l'intégralité du fichier. Lorsque svValid est renvoyé avec CoversWholeDocument à faux, la version signée est intacte mais le fichier contient des ajouts ultérieurs, et votre flux de travail doit décider s'il convient ou non de tolérer les modifications apportées par ces ajouts

Les documents chargés en flux et chiffrés ont besoin de leurs propres octets sources

Les fonctions sans paramètre VerifyLoadedSignature et VerifyLoadedSignatureEx dépendent de la capacité du composant à mémoriser le fichier dont provient le document. Si vous chargez le document à partir d'un flux, il n'y a pas de nom de fichier à réouvrir ; il en va de même après le rechargement par mot de passe utilisé pour les documents chiffrés, flux de travail décrit dans l'article sur le chiffrement PDF AES-256 avec HotPDF. Dans ces deux cas, les surcharges basées sur un fichier renvoient svSourceUnavailable plutôt que de deviner. La solution est la surcharge TStream, qui vous permet de transmettre les octets bruts d'origine de l'endroit où vous les avez conservés, un fichier existant, un tampon mémoire ou un blob de base de données

var
  Src: TFileStream;
  Status: THPDFSignatureVerifyStatus;
  Info: THPDFSignatureInfo;
begin
  // Stream-loaded document: the component holds no source
  // file name, so supply the original bytes yourself.
  Src := TFileStream.Create('signed-contract.pdf',
    fmOpenRead or fmShareDenyWrite);
  try
    Status := Pdf.VerifyLoadedSignature(0, Src, Info);
    if Status <> svValid then
      Writeln('Verification failed: ', Ord(Status));
  finally
    Src.Free;
  end;
end;

Signaler ce que vous ne pouvez pas vérifier

Un vérificateur qui ne connaîtrait que « valide » et « invalide » signalerait à tort des documents qu'il ne comprend tout simplement pas. C'est pourquoi l'énumération des états sépare les cas que votre interface utilisateur doit distinguer. La valeur svDigestMismatch signifie que les octets du document ont été modifiés après la signature, le signal d'altération classique. La valeur svSignatureInvalid signifie que le hachage des octets est correct mais que le contrôle RSA a échoué, ce qui indique une valeur de signature corrompue ou falsifiée. La valeur svUnsupportedAlgorithm est la réponse honnête pour les clés ECDSA et les hachages non reconnus : la signature peut être tout à fait valide, HotPDF ne peut tout simplement pas la vérifier, et la signaler comme « invalide » discréditerait à tort un document sain. La valeur svMalformed signale un conteneur CMS qui n'a pas pu être analysé du tout. Pour les contrôles de type barrière, VerifyAllLoadedSignatures renvoie vrai uniquement si au moins un champ de signature existe et que chacun d'eux se vérifie comme svValid, un booléen unique pratique pour un pipeline de réception d'archives qui rejette tout élément non conforme

La vérification des signatures, la signature PAdES, le chiffrement AES-256 et l'API de modification des documents chargés sont tous livrés dans la même bibliothèque VCL native pour Delphi et C++Builder, sans dépendance externe à des DLL ; la liste complète des fonctionnalités et les versions d'IDE prises en charge se trouvent sur la page produit de HotPDF Component