Article technique

Signatures numériques PAdES en Delphi

Valider une signature PAdES revient à vérifier trois choses indépendantes, et la coche verte d'une visionneuse ne vous renseigne que sur la troisième. Premièrement, le tableau /ByteRange doit couvrir les bons octets : les plages qu'il nomme doivent reconstituer exactement l'entrée sur laquelle l'empreinte CMS a été prise, sans laisser d'octet signé en dehors. Deuxièmement, le certificat contenu dans le CMS doit se rattacher à une racine de confiance et porter l'attribut signé signing-certificate qu'exige PAdES. Troisièmement, si le profil revendique un horodatage, un jeton RFC 3161 doit lier la valeur de signature à un instant antérieur à l'expiration du certificat. Acrobat écrase les trois en une icône ; un vérificateur de conformité les garde séparées, et le code qui produit ces fichiers devrait faire de même. losLab PDF Library (PDF Library for Delphi) vous donne le versant signature, la réintégration de l'horodatage et les appels d'audit permettant d'inspecter un ByteRange avant de lui faire confiance

Une distinction fait trébucher presque toutes les premières implémentations PAdES, elle mérite donc d'être posée avant tout code. Une signature écrite avec /SubFilter /adbe.pkcs7.detached est une signature ISO 32000-1 §12.8 parfaitement saine, qu'Acrobat déclarera valide. Ce n'est pas pour autant une signature PAdES, car ETSI EN 319 142-1 exige ETSI.CAdES.detached à chaque niveau de base. Un vérificateur de conformité eIDAS rejette la première et accepte la seconde alors même que la cryptographie est identique. Le profil est une affirmation que le document fait sur lui-même, et poser correctement cette affirmation tient en un appel dans PDF Library for Delphi

Ce qui transforme une signature PDF en signature PAdES

ETSI EN 319 142-1 définit quatre niveaux de base 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é signing-certificate. 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 incorpore les certificats, CRL et réponses OCSP nécessaires à la validation dans un Document Security Store, si bien que le fichier reste vérifiable après que l'autorité émettrice a retiré son infrastructure. PAdES-B-LTA coiffe la pile d'un horodatage de document qui re-protège les preuves accumulées à mesure que les algorithmes faiblissent

PDF Library for Delphi fait correspondre ces concepts à son API de processus de signature. Le marqueur de profil est SetSignProcessCustomSubFilter. Si votre politique exige une indication de type d'engagement (preuve d'origine, preuve d'approbation ou l'un des autres identifiants ETSI numérotés de 1 à 6), cela passe par SetSignProcessCommitmentType. Une politique de signature explicite s'attache avec SetSignProcessSignaturePolicy, qui prend l'OID de la politique et son empreinte. Un réglage par défaut mérite attention : l'algorithme d'empreinte laissé en automatique, la bibliothèque choisit SHA-256 pour les signatures ETSI et adbe.pkcs7.detached et ne retombe sur SHA-1 que sur le chemin hérité adbe.pkcs7.sha1. Définissez-le explicitement malgré tout. Les auditeurs demandent quelle empreinte vous avez utilisée, et une valeur explicite dans le code est plus facile à défendre qu'un défaut qu'il faut aller lire dans le manuel pour expliquer

Échelle des niveaux de base PAdES B-B, B-T, B-LT et B-LTA construits avec PDF Library for Delphi, montrant comment chaque niveau ajoute horodatages, preuves DSS ou horodatage de document renouvelable au-dessus du noyau ETSI.CAdES.detached
Chaque niveau de base ETSI empile une garantie de plus sur le même noyau CAdES, des attributs signés jusqu'à un horodatage de document renouvelable

Produire la signature de base

L'API plate pilote la signature comme un automate à passe unique : ouvrez un processus sur le fichier source, configurez-le, terminez vers un fichier de sortie, lisez le code de résultat. La séquence ci-dessous produit une signature PAdES-B-B en SHA-256. La ligne qui compte le plus n'a rien à voir avec la signature elle-même. C'est la réservation /Contents délibérément surdimensionnée, car c'est la seule chose que vous ne pourrez plus changer 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);  // place pour un horodatage ultérieur
    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 quand la source ne peut pas être ouverte du tout. Ensuite, GetSignProcessResult sépare les modes de défaillance qui surviennent réellement en production : 4 signale un mauvais mot de passe PDF, 7 un mauvais mot de passe PFX, 9 un fichier de certificat sans clé privée, 10 un chemin de sortie non inscriptible, 11 un échec pendant l'application des octets de signature. Journaliser le code numérique à côté du nom du fichier d'entrée transforme un ticket d'assistance vague en un diagnostic d'une minute

Ajouter l'horodatage RFC 3161 que la bibliothèque ne va pas chercher pour vous

PDF Library for Delphi ne livre aucun client TSA, et c'est une frontière délibérée plutôt qu'une lacune. La bibliothèque calcule l'empreinte que l'autorité d'horodatage doit contresigner et réintègre ensuite le CMS enrichi ; l'échange HTTP et la chirurgie CMS entre les deux appartiennent à l'appelant. Il y a une raison technique dure à cette séparation. Le contrôle CryptoAPI de Windows censé ajouter des attributs non signés, CMSG_CTRL_ADD_SIGNER_UNAUTH_ATTR, échoue avec CRYPT_E_INVALID_INDEX sur la disposition SignedData détachée qu'utilise PAdES. Le CMS enrichi doit donc venir d'un encodeur CMS sous votre propre contrôle. Aucune bibliothèque ne peut glisser discrètement le jeton en un appel système, et toute bibliothèque qui le prétend fait la chirurgie quelque part où vous ne la voyez pas

Pipeline d'ajout d'un horodatage RFC 3161 à une signature PAdES en Delphi, séparant le calcul d'empreinte et la réintégration par PDF Library for Delphi de la requête TSA et du ré-encodage CMS de l'appelant à l'intérieur de l'espace /Contents réservé
La bibliothèque calcule l'empreinte et réintègre pendant que votre code récupère le jeton et pratique la chirurgie CMS, et le résultat doit tenir dans la réservation /Contents de 8192 octets
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);
    // les deux appels ci-dessous sont du code applicatif : un POST HTTP vers votre TSA,
    // et un ré-encodage CMS qui attache le jeton en attribut non signé
    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 les codes de résultat ici : 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 enrichi ne tient plus dans l'espace réservé /Contents. Le code 13 est celui qui fait mal, car le seul remède est de resigner : un jeton d'horodatage typique avec sa chaîne de certificats pèse de 4 à 6 Ko, et la réservation de 8192 octets faite à l'étape B-B existe précisément pour que cette étape ait où atterrir

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

Une coche verte dans une visionneuse est une décision de confiance vis-à-vis du magasin de certificats de cette machine, pas un verdict structurel sur le fichier. La validation programmatique devrait commencer plus bas, avec la question que les mises à jour incrémentielles rendent subtile : quels octets chaque signature couvre-t-elle réellement ? Chaque enrichissement évoqué ici, deuxième signature, dictionnaire DSS ou horodatage de document, arrive par mise à jour incrémentielle, et chaque mise à jour ajoute des octets hors du /ByteRange de la signature antérieure. Ces octets ajoutés sont légitimes. Un validateur doit tout de même les classer au regard de la politique de modification du document, et le niveau DocMDP par champ dans lequel vit cette politique se lit avec GetSignatureDocMDPLevelByName

Audit de la disposition des octets d'un PDF signé en Delphi montrant les plages couvertes par le ByteRange, les octets /Contents exclus, les mises à jour incrémentielles ajoutées hors plage et le verdict de couverture face à la taille du fichier
Deux plages couvertes, les octets propres à la signature exclus, racontent la vraie histoire de la couverture, et les mises à jour ajoutées se classent au regard de la politique DocMDP plutôt que de faire peur
var
  Doc: TPDFlibSignDoc;
  Names: TStringList;
  I: Integer;
  B0, B1, B2, B3, FileSize: Int64;
begin
  FileSize := TFile.GetSize('invoice-bt.pdf');  // avant Open : SignDoc pose un verrou de partage
  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 signifie réellement signé
        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 habitent ce chemin d'audit. TPDFlibSignDoc.Open tient le fichier sous un verrou de partage exclusif, si bien qu'un validateur qui veut aussi hacher les octets bruts du fichier pour la vérification CMS doit lire le fichier en mémoire avant de l'ouvrir pour audit. Inversez cet ordre et la lecture échoue sur un verrou que vous avez posé vous-même. Le second piège est silencieux plutôt que bruyant : l'équivalent en API plate GetSignProcessByteRange renvoie un Integer alors que les offsets sous-jacents sont des Int64, si bien qu'au-delà de 2 Go l'appel plat tronque sans se plaindre, raison pour laquelle cet exemple tire les offsets via la classe d'audit. Une absence mérite aussi d'être nommée. La couche plate n'a aucun enrobage VerifySignature. Les verdicts cryptographiques viennent du TPDFlibSignatureVerifier de niveau classe, qui renvoie vsValid, vsInvalid ou vsUnknown, ou bien d'un validateur externe auquel votre politique de conformité fait déjà confiance

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 document portant certificats, CRL et réponses OCSP, éventuellement indexés par signature via des entrées VRI clés sur l'empreinte du /Contents de chaque signature. Le flux de PDF Library for Delphi 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 en mise à jour incrémentielle. La partie difficile reste de votre côté. Récupérer le matériel de révocation, et juger s'il est assez frais pour valoir la peine d'être incorporé, est le travail de l'appelant. La bibliothèque garantit que les dictionnaires sont structurellement conformes ; elle ne peut pas garantir que votre répondeur OCSP a dit la vérité

Le point d'arrivée archivistique, B-LTA, ajoute un horodatage de document : un champ de signature distinct dont le type est DocTimeStamp et non Sig, produit par SetSignProcessDocTimeStamp avec une longueur de signature réservée. Il ne remplace pas l'horodatage de signature de l'étape B-T. L'horodatage de signature prouve à quel moment une signature particulière existait ; l'horodatage de document protège tout le fichier, preuves DSS comprises, et c'est l'élément qu'une archive de longue durée renouvelle tous les quelques années à mesure que les algorithmes faiblissent. Un profil archivistique mûr porte les deux. Pour les lecteurs antérieurs à ces structures, TPDFlibSignDoc.EnsurePAdESExtensions consigne l'extension développeur ESIC dans le catalogue du document, annonçant que le fichier utilise des fonctionnalités définies par l'ETSI

Une réaction à tout ceci mérite d'être devancée, car elle ressemble à un bug sans en être un. Une visionneuse signale souvent "validité inconnue" sur un fichier dont la structure PAdES est entièrement correcte. Confiance et structure sont des axes indépendants. La visionneuse ne parvient simplement pas à rattacher le signataire à une racine de confiance sur cette machine, ce qui est courant avec des autorités privées et des certificats de test, alors même que l'audit du ByteRange et la vérification CMS passent tous deux. Le remède est de distribuer correctement le certificat racine, ou d'évaluer face aux listes de confiance de l'UE quand le statut eIDAS qualifié est le vrai but, pas de toucher au code de signature

Pour la perspective côté audit, à savoir énumérer les champs de signature sur un corpus, vider les dispositions de ByteRange et lire les niveaux DocMDP en masse, voyez la pièce compagnon sur l'atelier 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 sont sur la page produit de losLab PDF Library for Delphi