HotPDF valide un PDF MAC ISO/TS 32004 par révision plutôt que par fichier. THotPDF.ValidatePDFMACChain parcourt chaque mise à jour incrémentale depuis l’ancre de la chaîne vers l’avant et vérifie chaque MAC contre un flux de préfixe en lecture seule se terminant au startxref et au %%EOF propres à cette révision. Un MAC valide sur la révision la plus récente ne prouve rien sur les révisions qui sont dessous
Voici le scénario qui motive tout cela. Vous livrez un PDF chiffré AES-256 portant un PDF MAC. Quelqu’un ouvre le fichier dans un éditeur hexadécimal, retourne un octet à l’intérieur de la première révision protégée par MAC, puis ajoute une révision toute neuve portant un MAC parfaitement valide de son cru. Chaque visionneuse ouvre le fichier sans broncher, et un vérificateur naïf qui hache la plage d’octets courante contre le MAC du trailer actif annonce succès — car ce MAC est bel et bien correct pour les octets qu’il couvre. Le dégât se trouve deux révisions plus bas, dans une région que personne n’a revérifiée
Pourquoi un MAC de premier niveau valide ne prouve-t-il pas que le fichier est intact ?
Parce qu’un PDF MAC couvre un préfixe, pas un document. La mise à jour incrémentale est une partie de premier rang du format : chaque sauvegarde ajoute un nouveau corps, une nouvelle section de table de référence croisée et un nouveau trailer, tandis que les octets anciens restent exactement où ils étaient. L’ISO/TS 32004 chevauche ce modèle, si bien que chaque révision porte son propre dictionnaire /AuthCode authentifiant le fichier tel qu’il se tenait à cet instant, et ne vérifier que le plus récent laisse toute révision antérieure sans examen. HotPDF expose donc les deux questions comme deux appels, et la différence entre eux est tout l’objet de cet article. ValidatePDFMAC répond à « la révision courante est-elle authentique », en remplissant un enregistrement THPDFPDFMACValidationInfo ; ValidatePDFMACChain répond à « chaque révision protégée par MAC de ce fichier est-elle authentique », en remplissant THPDFPDFMACChainValidationInfo avec un tableau par révision plus une raison d’échec lisible par machine. Sur le fichier ci-dessus altéré puis re-MACé, le premier appel renvoie True et le second renvoie False contre l’index de révision 1
var
Pdf: THotPDF;
Chain: THPDFPDFMACChainValidationInfo;
I: Integer;
begin
Pdf := THotPDF.Create(nil);
try
if not Pdf.ValidatePDFMACChain('incoming.pdf', 'user', Chain) then
begin
// L'échec est l'un de pmcfRevisionBoundary, pmcfNoPDFMAC,
// pmcfRequiredRevisionMissing, pmcfRevisionInvalid,
// pmcfKDFSaltChanged, pmcfDigestDowngrade, pmcfPermissionDowngrade
Writeln('chain rejected: ', Chain.Message);
Writeln('revision ', Chain.FailureRevisionIndex,
' at xref offset ', Chain.FailureXRefOffset);
Exit;
end;
for I := 0 to High(Chain.Revisions) do
Writeln(I, ' len=', Chain.Revisions[I].RevisionLength,
' mac=', Chain.Revisions[I].HasPDFMAC,
' perms=', Chain.Revisions[I].PermissionsAuthenticated);
finally
Pdf.Free;
end;
end;
Chaque MAC se vérifie sur son propre flux de préfixe, jamais sur la longueur finale du fichier
Le bogue le plus coûteux dans ce domaine consiste à utiliser la taille finale du fichier comme borne supérieure quand on re-hache une révision ancienne, ce qui fond les octets de queue dans chaque empreinte sauf la plus récente et annonce une altération sur un fichier sain. HotPDF reconstruit au contraire, pour chaque révision, un flux en lecture seule borné se terminant à la valeur startxref propre à cette révision suivie de son %%EOF, et ne hache que cela. Localiser la frontière est plus pointilleux qu’il n’y paraît : le littéral %%EOF peut apparaître à l’intérieur d’un flux de contenu ou d’une chaîne, si bien qu’un candidat n’est accepté que lorsque le startxref immédiatement précédent s’analyse en un nombre égal au décalage de référence croisée de la section en cours de validation, avec rien d’autre que des blancs entre eux. La révision absorbe alors exactement une séquence de fin de ligne après le marqueur — un CR seul, un LF seul, ou une paire CRLF — et rien de plus. Cette dernière règle mord en pratique, car un écrivain qui émet une ligne vide de plus entre deux révisions a produit des octets appartenant à la révision suivante, et avaler tous les blancs de queue dans la précédente change silencieusement les deux empreintes. L’énumération des sections suit la même discipline : HotPDF parcourt les sections de référence croisée de la plus ancienne à la plus récente exactement une fois, en rejouant les entrées libres, directes et de flux d’objets afin que les sections postérieures écrasent l’état antérieur, ce qui est l’opposé de la sémantique premier-trouvé-gagnant qu’applique un analyseur de xref actif
Où la chaîne s’ancre-t-elle, et qu’est-ce qui la rompt ?
La première révision portant un /AuthCode valide est l’ancre, et FirstMACRevisionIndex indique où commence la protection ; tout ce qui est avant est non protégé par construction, ce qui est normal. Tout ce qui vient après doit être protégé par MAC, si bien qu’ajouter une mise à jour incrémentale ordinaire à un fichier protégé par MAC échoue avec pmcfRequiredRevisionMissing et l’index de révision fautif — tolérer un trou laisserait un attaquant ôter la protection simplement en sauvegardant une fois de plus. Trois autres invariants tiennent à travers la chaîne, chacun avec son propre code d’échec
pmcfKDFSaltChanged— le/KDFSaltdoit rester stable depuis l’ancre, car un sel qui tourne laisserait un faussaire re-dériver des clés sous des paramètres de son choixpmcfDigestDowngrade— la force du digest est comparée au dernier MAC vérifié plutôt qu’à la révision immédiatement précédente, si bien qu’une chaîne qui démarre sous le profil Modern en SHA-384 ne peut pas continuer en sourdine avec SHA-256pmcfPermissionDowngrade— une révision ne peut pas effacer une exigence de PDF MAC qu’une révision antérieure avait authentifiée
La conséquence qui mérite d’être intériorisée est que les MAC historiques sont vérifiés indépendamment même quand ils ne sont plus le trailer actif. Voilà pourquoi l’attaque d’ouverture — modifier une vieille révision puis ajouter un MAC frais — ne survit pas : le MAC le plus récent se vérifie de son côté, ValidatePDFMAC est content, et la chaîne atterrit quand même sur la révision 1 avec pmcfRevisionInvalid
Ordre de signature : les clés du trailer d’abord, signatureDigest en dernier
Quand le MAC est attaché à une signature CMS plutôt que de tenir seul, l’ordre d’écriture cesse d’être une question de style. HotPDF exige que /AuthCode, /KDFSalt, l’extension de développeur ISO 32004 et /SigObjRef soient écrits dans la même révision avant que le /ByteRange de la signature ne soit calculé ; en ajouter un seul après coup place ces octets hors de la plage couverte par la signature, produisant un fichier dont la signature se vérifie tandis que la liaison MAC n’est pas signée. Les deux digests tournent alors dans l’autre sens, ce qui paraît circulaire au premier regard et ne l’est pas. Le signatureDigest du PDF MAC lie les octets de contenu bruts de l’OCTET STRING SignerInfo.signature du CMS — pas tout le DER du CMS, et pas les attributs signés — si bien qu’il est construit après que la valeur de signature brute existe et injecté comme attribut non signé id-attr-pdfMacData. Comme /Contents est exclu du ByteRange de la signature et que les attributs non signés n’alimentent jamais le calcul de signature, la séquence produire-signature, construire-MAC, envelopper-CMS se referme proprement sans boucle cryptographique. Deux corollaires suivent : la sentinelle du /ByteRange et le /Contents de substitution doivent rester en clair et hors des flux d’objets même dans un fichier chiffré, sinon le rustineur à largeur fixe ne peut pas les trouver ; et quand le digest du MAC est aussi SHA-256, le digest de signature est réutilisé tel quel, sinon les deux contextes de digest sont mis à jour en une seule passe sur le flux de sortie
var
Pdf: THotPDF;
Options: THPDFPDFMACOptions;
Info: THPDFPDFMACValidationInfo;
begin
Pdf := THotPDF.Create(nil);
try
Pdf.AutoLaunch := False;
Pdf.FileName := 'unsigned.pdf';
Pdf.ActivateProtection := True;
Pdf.CryptKeyLength := aesgcm;
Pdf.OwnerPassword := 'owner';
Pdf.UserPassword := 'user';
Pdf.ProtectOptions := [prPrint, prExtractContent];
Pdf.BeginDoc;
Pdf.CurrentPage.SetFont('Arial', [], 12);
Pdf.CurrentPage.AddSignedSignatureField('Approval',
Rect(72, 120, 280, 160), 16384);
Pdf.EndDoc;
Options := THPDFPDFMACOptions.Modern; // digest de document SHA-384
if THotPDF.SignPDFWithPFXAndAttachedPDFMAC('unsigned.pdf',
'signed.pdf', 'signer.pfx', 'pfx-secret', 'user', Options) then
if Pdf.ValidatePDFMAC('signed.pdf', 'user', Options, Info) then
begin
// Location = pmlAttachedToSignature, et les deux digests
// sont rapportés séparément
Writeln('signature object : ', Info.SignatureObjectNumber);
Writeln('signature digest : ', Info.SignatureDigestMatched);
Writeln('full file coverage: ', Info.FullFileCoverage);
Writeln('perms authentic : ', Info.PermissionsAuthenticated);
end;
finally
Pdf.Free;
end;
end;
La validation retrace le même chemin depuis l’autre bout : lire le /AuthCode direct depuis le trailer de référence croisée classique actuellement actif, suivre le /SigObjRef indirect conscient des générations, confirmer qu’il lie le /V de l’unique champ de signature, et rapporter un échec de digest de document séparément d’un échec de digest de signature. Ce sont des diagnostics différents, et les fondre en un seul booléen jette la seule information qui dit si le contenu de page ou la valeur de signature a été touché. Si vous faites déjà du CMS, ceci se place à côté de l’article sur la signature PAdES et du guide de vérification des signatures dans les documents chargés
Ne faites jamais confiance à /P : déchiffrez d’abord les 16 octets de /Perms
L’ISO/TS 32004 signale « ce document exige un PDF MAC » à travers le bit de permission 13, et la manière évidente de le lire est la mauvaise, car l’entier /P du dictionnaire de chiffrement est en clair et non authentifié — n’importe qui peut retourner ce bit dans un éditeur de texte et dégrader l’exigence. L’ISO 32000-2 §7.6 fournit la réponse dans l’entrée /Perms, et HotPDF l’utilise : déchiffrer la chaîne /Perms de 16 octets avec la clé de chiffrement du fichier en AES-256 CBC, IV zéro, sans remplissage, puis vérifier chaque champ du texte clair avant de croire quoi que ce soit. Les octets 1 à 4 portent la valeur de permission en ordre petit-boutiste et doivent égaler exactement l’entier /P ; les octets 5 à 8 sont 0xFF ; l’octet 9 est le drapeau de chiffrement des métadonnées T ou F ; les octets 10 à 12 sont le marqueur littéral adb. Seulement quand tout cela tient, PermissionsAuthenticated devient True et le bit 13 est lu — et attention à sa polarité, car l’exigence de MAC est affirmée quand le bit 0x1000 est clair. Une discordance entre /P et les permissions déchiffrées n’est pas un avertissement à journaliser et à laisser passer ; c’est un jeu de permissions falsifié, et la bonne réponse est d’échouer fermé
L’agilité algorithmique s’arrête au digest
L’ISO/TS 32004 vous laisse choisir le digest de document, et seulement le digest de document. HotPDF garde HMAC-SHA-256 pour l’authentification, HKDF-SHA-256 selon la RFC 5869 pour la dérivation de clés et l’enveloppage de clés AES-256 selon la RFC 3394 fixés sous une variable THPDFPDFMACDigestAlgorithm allant de pmdaSHA256 à pmdaSHA3_512, car l’erreur naturelle consiste à traiter un « profil SHA3-512 » comme une licence pour échanger aussi le HMAC, ce qui produit un fichier qui n’est plus un PDF MAC en aucun sens interopérable. Un détail d’implémentation vaut d’être copié si vous écrivez votre propre vérificateur : lire l’OID de digest hors du AuthenticatedData du CMS avant de hacher la plage d’octets, car coder SHA-256 en dur et se réconcilier ensuite transforme l’agilité en étiquette et laisse un fichier hostile vous faire streamer tout le document avant que vous ne découvriez que l’algorithme n’a jamais été pris en charge. CMSAlgorithmProtection, l’algorithme de digest du AuthenticatedData, le messageDigest de l’integrity-info et le digest de plage d’octets doivent tous nommer un seul algorithme, et toute discordance échoue fermé
var
Options: THPDFPDFMACOptions;
begin
Options := THPDFPDFMACOptions.Compatibility; // SHA-256, accepte les six
Options := THPDFPDFMACOptions.Modern; // SHA-384, rejette 256 bits
Options := THPDFPDFMACOptions.HighAssurance; // SHA3-512 seulement, AES-GCM
// Un profil personnalisé est légal, mais l'algorithme avec lequel il
// génère doit aussi figurer dans la liste d'admission de validation,
// sinon la configuration est rejetée avant qu'un octet ne soit écrit
Options.Profile := pmppCustom;
Options.DigestAlgorithm := pmdaSHA512;
Options.AllowedDigestAlgorithms := [pmdaSHA512, pmdaSHA3_512];
Options.RequireAESGCM := True;
end;
Ce qu’un PDF MAC prouve et ne prouve pas
Une chaîne de PDF MAC vérifiée prouve que chaque révision protégée est octet pour octet identique à ce qui a été écrit par quelqu’un détenant la clé de chiffrement du fichier, qu’aucune révision protégée n’a été retirée ou réordonnée, et qu’aucune révision non protégée n’a été ajoutée après l’ancre — exactement la classe d’attaque que le simple chiffrement AES-256 laisse ouverte, puisque la confidentialité ne dit rien de l’intégrité et qu’un PDF chiffré avec une révision greffée se déchiffre aussi joyeusement qu’un fichier intact. Ce qu’elle ne prouve pas, c’est la paternité. La clé MAC dérive de la clé de chiffrement du fichier, si bien que quiconque peut ouvrir le document peut aussi produire un MAC valide sur une version modifiée, tout destinataire légitime inclus ; c’est une primitive symétrique, et les primitives symétriques ne savent pas attribuer. Si vous devez savoir qui a changé quelque chose, il vous faut une signature numérique avec un certificat derrière, et le PDF MAC la complète alors en protégeant la structure incrémentale que la signature seule ne couvre pas. Traitez-les comme des couches et laissez les deux verdicts être rapportés indépendamment plutôt que fondus en une seule icône d’état
Les points d’entrée PDF MAC décrits ici — AddStandalonePDFMAC, SignPDFWithPFXAndAttachedPDFMAC, ValidatePDFMAC et ValidatePDFMACChain — sont livrés avec le composant PDF HotPDF pour Delphi standard pour Delphi et C++Builder, où la page produit porte la référence complète de l’enregistrement d’options, des énumérations d’état et du tableau de validation par révision