HotPDF, le composant PDF Delphi, rejette maintenant le signature wrapping : depuis la v2.759.0, VerifyLoadedSignatureEx comme le validateur par lot exigent que le trou entre les deux segments /ByteRange soit exactement la chaîne hexadécimale /Contents, délimiteurs compris, et la v2.761.0 ajoute AddLoadedSignedSignatureField pour qu'une seconde signature puisse s'ajouter à un PDF déjà signé comme une révision incrémentale propre. Les deux changements vont ensemble, parce qu'une seconde signature correcte est précisément la disposition qu'attend le vérificateur plus strict
La situation qui a exposé le problème est banale. Un contrat est signé par le fournisseur, puis acheminé vers un approbateur qui doit contre-signer sans déranger la première signature. La seconde révision s'ajoute après la première, son propre /ByteRange couvre tout le fichier grandi, et les deux signatures devraient se vérifier. Y arriver à la main voulait dire écrire soi-même une section incrémentale, et la fixture de test qui faisait exactement ça s'est révélée être une structure de signature wrapping de manuel que l'ancien vérificateur acceptait volontiers. Si vous n'avez pas encore regardé l'API de vérification, le guide de vérification des signatures numériques PDF avec HotPDF couvre les bases sur lesquelles cet article bâtit
Que doit contenir exactement le trou du ByteRange ?
Le trou doit contenir la valeur /Contents complète et rien d'autre : ISO 32000-1 §12.8.3.3 dit que la chaîne hexadécimale, avec ses délimiteurs < et >, tient précisément dans l'espace entre les deux plages d'octets, et ISO 32000-2 §12.8.1 fait porter la même règle en avant. La Table 252 et les documents PAdES disent seulement que le condensat exclut la valeur Contents, ce qui se lit facilement comme excluant juste les chiffres hexadécimaux. Les versions HotPDF antérieures lisaient ainsi : PreparePDFForSigning et la préparation CMS en flux hachaient aussi les chevrons, avec un commentaire source insistant pour que les chevrons soient couverts. Les validateurs qui comparent le trou à la valeur de la signature signalent cette disposition comme une plage d'octets invalide, donc la v2.759.0 sort les deux délimiteurs des plages signées. Un contrôle indépendant rapide sur n'importe quel fichier signé consiste à regarder deux octets : l'octet au décalage ByteRange[1] doit être < et l'octet au décalage ByteRange[2] - 1 doit être >
Pourquoi un contrôle de trou non vide rate-t-il le signature wrapping ?
Un contrôle de trou non vide prouve seulement que quelque chose a été laissé dehors du condensat, pas quoi, et c'est toute la surface d'attaque. Le placeholder /Contents est réservé avec des milliers de chiffres zéro, alors qu'un vrai conteneur CMS le remplit rarement. Un attaquant peut fermer la chaîne hexadécimale en avance au milieu de ce bourrage de zéros avec un >, écrire de nouveaux objets ou une révision forgée dans le reste de l'espace réservé, et laisser les plages d'octets intactes. La signature CMS se vérifie toujours parce que chaque octet signé est inchangé, les plages commencent toujours à 0 et finissent à la taille du fichier, et l'ancien vérificateur HotPDF rapportait svValid avec CoversWholeDocument posé à True. Un lecteur PDF, pendant ce temps, parse tout ce qui loge dans ce trou non signé
HotPDF traite maintenant le trou comme des données à valider octet par octet. Le vérificateur lit le trou, rogne les délimiteurs, n'accepte que des chiffres hexadécimaux plus les espaces blancs PDF (tabulation, saut de ligne, saut de page, retour chariot, espace), décode les chiffres et exige que le résultat égale exactement le /Contents du dictionnaire de signature. Tout le reste rétrograde le résultat en svInvalidByteRange. Le contrôle tourne dans la voie à signature unique comme dans ValidateLoadedSignatureBatch, qui gardait sa propre logique de couverture et avait besoin de la même correction. Les fichiers produits par HotPDF avant la v2.759.0, dont le trou ne portait que des chiffres avec les crochets logeant juste à l'intérieur des plages, se vérifient toujours, donc les documents archivés ne virent pas au rouge du jour au lendemain
var
Pdf: THotPDF;
Info: THPDFSignatureInfo;
Status: THPDFSignatureVerifyStatus;
I: Integer;
begin
Pdf := THotPDF.Create(nil);
try
Pdf.LoadFromFile('SignedTwice.pdf');
for I := 0 to Pdf.GetLoadedSignatureFieldCount - 1 do
begin
Status := Pdf.VerifyLoadedSignatureEx(I, Info);
case Status of
svValid:
if Info.CoversWholeDocument then
Writeln(Info.FieldName, ': valid, covers the whole file')
else
Writeln(Info.FieldName, ': valid, ',
Info.UnsignedTrailingBytes, ' bytes appended later');
svInvalidByteRange:
Writeln(Info.FieldName, ': ByteRange gap rejected (wrapping?)');
else
Writeln(Info.FieldName, ': failed, status ', Ord(Status));
end;
end;
finally
Pdf.Free;
end;
end;
Comment ajouter une seconde signature à un PDF déjà signé ?
Ouvrez le fichier signé avec BeginIncrementalUpdate, appelez AddLoadedSignedSignatureField, sauvegardez avec SaveIncrementalUpdate, puis signez le fichier préparé avec la fonction de classe THotPDF.SignPDFWithPFX. Avant la v2.761.0, la recette documentée qui consistait à appeler THPDFPage.AddSignedSignatureField après BeginIncrementalUpdate ne pouvait pas marcher, parce que CurrentPage est nil en mode incrémental et que rien ne pouvait accrocher un placeholder /V à un champ sur un document chargé. La nouvelle méthode crée le widget sur la page chargée et suspend sous le /V le même dictionnaire de placeholder qu'emploie la voie du nouveau document, si bien que les deux voies de signature partagent une seule sérialisation. Pour la première signature elle-même, l'article sur la création de signatures numériques PAdES en Delphi parcourt le pipeline PFX
var
Pdf: THotPDF;
FieldIndex: Integer;
begin
Pdf := THotPDF.Create(nil);
try
Pdf.BeginIncrementalUpdate('Signed.pdf');
// Page 0, rectangle de widget en points, 8192 octets réservés pour le CMS
FieldIndex := Pdf.AddLoadedSignedSignatureField(0, 320, 60, 520, 120,
'ApproverSignature', 8192);
if FieldIndex < 0 then
raise Exception.Create('Page index out of range');
Pdf.SaveIncrementalUpdate('Prepared.pdf');
finally
Pdf.Free;
end;
if not THotPDF.SignPDFWithPFX('Prepared.pdf', 'SignedTwice.pdf',
'approver.pfx', 'pfx-password') then
raise Exception.Create('Second signature failed');
end;
AddLoadedSignedSignatureField est délibérément plus discret que ses frères. Les autres créateurs de champs AddLoaded* posent /NeedAppearances true sur l'AcroForm, ce qui dit à un lecteur de régénérer les apparences de champs ; sur un document signé, cette régénération peut réécrire du contenu signé, donc la nouvelle méthode retire à nouveau le drapeau sauf si la source le portait déjà. /SigFlags garde sa valeur d'origine OR 3 (SignaturesExist plus AppendOnly, ISO 32000-1 Table 219). Vous n'avez pas non plus besoin d'appeler MarkDirty sur la page : l'ajout aux /Annots et aux /Fields propage le drapeau dirty à l'objet indirect propriétaire, et un marquage explicite de la page ne ferait que traîner un dictionnaire de page inchangé dans la nouvelle révision, que l'analyse de révision rapporterait ensuite comme une modification de page. Enfin, le placeholder écrit /ByteRange avant /Contents, parce que le patcher localise d'abord le sentinel /ByteRange et cherche vers l'avant la chaîne hexadécimale appariée
Que change le fait qu'un signeur externe ou un HSM produise le CMS ?
Rien ne change dans le workflow, mais les décalages signifient maintenant ce que dit la spécification. PreparePDFForSigning renvoie deux plages base 0 dont le trou est toute la chaîne /Contents, et ContentsHexStart est l'index base 1 du premier chiffre hexadécimal dans l'AnsiString. Un CMS plus court est bourré de 0 à la fin, avant le > de fermeture. Comme PreparePDFForSigning patche le premier sentinel non patché qu'il trouve, préparez exactement un placeholder par révision, et préférez InsertSignatureHexAt avec les décalages renvoyés à l'InsertSignatureHex fondée sur la recherche quand des signatures antérieures existent déjà dans le fichier
var
Bytes, ToSign, CmsHex: AnsiString;
R1Start, R1Len, R2Start, R2Len, HexStart, HexLen: Integer;
begin
Bytes := LoadFileAsAnsiString('Prepared.pdf'); // votre helper
if not THotPDF.PreparePDFForSigning(Bytes, R1Start, R1Len,
R2Start, R2Len, HexStart, HexLen) then
raise Exception.Create('No signature placeholder found');
// Le trou est toute la chaîne hex : '<' termine la plage 1, '>' précède la plage 2
Assert(Bytes[R1Start + R1Len + 1] = '<');
Assert(Bytes[R2Start] = '>');
ToSign := Copy(Bytes, R1Start + 1, R1Len) + Copy(Bytes, R2Start + 1, R2Len);
CmsHex := SignDetachedWithHsm(ToSign); // votre signeur CMS, DER hex
if not THotPDF.InsertSignatureHexAt(Bytes, HexStart, HexLen, CmsHex) then
raise Exception.Create('CMS does not fit the reserved space');
SaveAnsiStringToFile(Bytes, 'SignedTwice.pdf'); // votre helper
end;
Où sont les limites des nouveaux contrôles ?
Le contrôle du trou ferme un trou bien précis et ne doit pas être survendu. svValid signifie toujours intégrité d'octets plus une clé qui colle au certificat embarqué ; la confiance dans ce certificat est une décision séparée. Le trou n'est validé que quand le vérificateur dispose des octets sources, que VerifyLoadedSignatureEx lit dans le fichier chargé et que les surcharges TStream reçoivent de vous. Pour la première signature d'un fichier contre-signé, CoversWholeDocument est correctement False, et savoir si la révision ajoutée n'a fait qu'ajouter une signature ou a aussi changé des pages est une question pour l'analyse DocMDP, FieldMDP et révision dans HotPDF. Notez aussi que le contrôle de PDF MAC attaché compare les décalages aux positions < et >, donc il accepte l'ancienne comme la nouvelle disposition ; tout outil à vous qui coderait en dur les décalages d'avant la v2.759.0 échouera d'abord en rencontrant un fichier fraîchement signé
Si votre application Delphi ou C++Builder signe, contre-signe ou audite des PDF, la voie la plus sûre est de laisser une seule bibliothèque produire et vérifier la même disposition. HotPDF, le composant PDF Delphi natif, livre la validation de trou plus stricte, les secondes signatures incrémentales et les hooks de signeur externe montrés ci-dessus dans un seul composant