Un PDF chiffré par certificats porte une seule valeur /P dans son dictionnaire de chiffrement, quels que soient les destinataires du fichier ; dans les fichiers construits plus bas dans cet article, elle vaut -64. Trois destinataires avec trois jeux de permissions différents partagent pourtant ce même nombre. Le code qui répond à « cet utilisateur peut-il imprimer ? » depuis /P lit un placeholder, parce que sous le gestionnaire de sécurité à clé publique le vrai mot de permissions voyage à l'intérieur de l'enveloppe chiffrée de chaque destinataire et n'apparaît qu'une fois que la clé privée de ce destinataire l'a ouverte
Cette conception est la raison pour laquelle le chiffrement par certificats vaut la peine. Un dossier d'audit trimestriel part vers trois réviseurs externes ; le juridique veut que tous le lisent, qu'un seul puisse l'imprimer, et personne ne veut un mot de passe qui traîne dans le fil de courriel à côté de la pièce jointe. Un mot de passe ne sait pas dire cela. Les certificats, si : chaque destinataire déverrouille le fichier avec une clé qu'il détient déjà, et chaque enveloppe porte son propre mot de permissions
En quoi le chiffrement PDF par certificats diffère-t-il d'un mot de passe ?
Un PDF chiffré à clé publique dérive sa clé de fichier d'une graine aléatoire plus les octets exacts de chaque enveloppe de destinataire, et rien de ce qu'une personne tape n'entre dans le calcul. Le gestionnaire est spécifié dans ISO 32000-1 §7.6.4 (§7.6.5 dans ISO 32000-2), et les enveloppes sont des structures CMS EnvelopedData du RFC 5652. HotPDF écrit /Filter /Adobe.PubSec avec /SubFilter /adbe.pkcs7.s5. Pour AES-256 cela signifie /V 5, une entrée /DefaultCryptFilter sous /CF avec /CFM /AESV3, et le tableau /Recipients placé à l'intérieur de ce crypt filter au lieu du dictionnaire de premier niveau
Chaque enveloppe chiffre 24 octets : la graine de 20 octets suivie du mot de permissions 32 bits de ce destinataire. Au chargement, un lecteur déballe une enveloppe, récupère la graine, et hache la graine avec chaque enveloppe dans l'ordre de /Recipients (SHA-256 pour AES-256, SHA-1 pour les chiffreurs plus anciens) pour reconstruire la clé de fichier. Le réviseur B n'apprend jamais ce que le réviseur A peut faire, pourtant le lecteur de B hache quand même les octets de l'enveloppe de A, si bien que tout le tableau doit survivre octet pour octet. Si vous hésitez encore entre ce modèle et des mots de passe ordinaires, le guide du chiffrement par mot de passe AES-256 et des drapeaux de permissions couvre l'autre versant de cet arbitrage
Écrire des destinataires RSA avec EnablePubKeyEncryption
Pour les certificats RSA la séquence est courte : EnablePubKeyEncryption avec aes256, puis un appel AddPubKeyRecipientCertificate par certificat encodé en DER, tout avant BeginDoc. Le helper construit une enveloppe RSAES-OAEP dans le processus. Ses deux derniers paramètres choisissent le digest OAEP et le digest MGF1 parmi THPDFRSAOAEPHash (rohSHA256, rohSHA384 ou rohSHA512), tous deux à rohSHA256 par défaut, et le contenu de l'enveloppe est chiffré en AES-256-CBC
uses
System.SysUtils, System.IOUtils, HPDFDoc, HPDFCrypt, HPDFRSA;
procedure WriteAuditPack(const OutFile: string);
var
Pdf: THotPDF;
Seed: AnsiString;
begin
SetLength(Seed, 20); // exactement 20 octets, même pour AES-256
AESGenerateRandomBytes(@Seed[1], Length(Seed));
Pdf := THotPDF.Create(nil);
try
Pdf.AutoLaunch := False;
Pdf.FileName := OutFile;
Pdf.EnablePubKeyEncryption(Seed, aes256, True); // le type de clé par défaut est aes128
// Le réviseur A peut imprimer ; le réviseur B ne peut que lire et extraire
Pdf.AddPubKeyRecipientCertificate(TFile.ReadAllBytes('reviewer-a.cer'),
[prPrint, prPrint12bit, prExtractContent], rohSHA256, rohSHA256);
Pdf.AddPubKeyRecipientCertificate(TFile.ReadAllBytes('reviewer-b.cer'),
[prExtractContent]);
Pdf.BeginDoc;
Pdf.CurrentPage.SetFont('Arial', [], 11);
Pdf.CurrentPage.TextOut(72, 720, 0, 'Q3 audit pack');
Pdf.EndDoc;
finally
Pdf.Free;
end;
end;
Trois détails de ce listing portent la charge. La graine fait 20 octets pour tout type de clé, AES-256 compris, et EnablePubKeyEncryption lève sur toute autre longueur. Le type de clé vaut aes128 par défaut tandis que les deux helpers de certificats exigent aes256, si bien qu'omettre le second argument fait lever le premier appel AddPubKeyRecipientCertificate avec « certificate envelopes require aes256 ». Les chiffreurs historiques (k40, k128, aes128) restent disponibles via AddPubKeyRecipient, qui prend une enveloppe DER complète construite par un autre outillage
Le troisième détail est la version. AES-256 sous le gestionnaire à clé publique est une fonctionnalité PDF 2.0, et HotPDF monte la version du document à 2.0 tout seul. Avec StrictVersionLock positionné sur une version inférieure, ce bump n'est pas permis : EnablePubKeyEncryption rend la main sans rien activer, et l'erreur surgit une ligne plus tard quand le helper de destinataire signale « call EnablePubKeyEncryption first ». Ce message envoie les gens chercher une erreur d'ordre d'appel. Vérifiez plutôt le verrou de version. Changer le chiffrement pendant une mise à jour incrémentale est refusé sans autre forme de procès avec EInvalidOpException
Ajouter des destinataires ECDH sur P-256, P-384, P-521, X25519 et X448
Les certificats à courbes elliptiques passent par AddPubKeyAgreementRecipientWithSecret, qui écrit un destinataire d'accord de clé CMS (KeyAgreeRecipientInfo du RFC 5753, avec le profil X25519 et X448 du RFC 8418) et calcule le secret partagé ECDH dans le processus. Une valeur THPDFPubKeyAgreementScheme nomme la courbe : pkasECDHP256, pkasECDHP384, pkasECDHP521, pkasX25519 ou pkasX448. Le schéma doit correspondre à l'algorithme de clé du certificat, à sa courbe et à sa longueur de clé, sinon l'appel lève « Certificate key does not match the requested agreement scheme »
Chaque enveloppe reçoit un UKM aléatoire tout neuf de 32 octets, une clé de chiffrement de clé issue du KDF stdDH (SHA-256 pour P-256 et X25519, SHA-384 pour P-384, SHA-512 pour P-521 et X448), et un key wrap AES-256 au sens du RFC 3394. L'arithmétique des courbes est du Pascal pur sans fournisseur crypto de plateforme dessous ; l'article sur l'arithmétique des courbes NIST en Pascal pur parcourt la façon dont cette couche est assemblée. Sur les courbes de Montgomery, la paire de clés éphémère entière peut être générée localement :
uses
System.SysUtils, System.IOUtils, HPDFDoc, HPDFCrypt, HPDFPubSec,
HPDFKeyAgreement;
procedure AddLegalRecipient(Pdf: THotPDF);
var
Scalar, OriginatorPublic: TBytes;
begin
// Scalaire éphémère neuf par enveloppe ; le clamping se produit dans la ladder
SetLength(Scalar, 32);
AESGenerateRandomBytes(@Scalar[0], Length(Scalar));
try
OriginatorPublic := HPDFX25519PublicFromScalar(Scalar);
Pdf.AddPubKeyAgreementRecipientWithSecret(
TFile.ReadAllBytes('legal-x25519.cer'),
[prPrint, prExtractContent], pkasX25519,
OriginatorPublic, Scalar,
[]); // OwnPublicPoint : n'a de sens que pour les courbes NIST
finally
HPDFSecureClearBytes(Scalar);
end;
end;
Les courbes NIST demandent plus à l'appelant. HotPDF dérive des clés publiques d'initiateur seulement pour X25519 et X448 (HPDFX25519PublicFromScalar, HPDFX448PublicFromScalar), donc pour P-256, P-384 et P-521 la paire éphémère vient de votre propre outillage. Passez un scalaire big-endian d'exactement la taille de corps (32, 48 ou 66 octets) et le point non compressé 0x04||X||Y correspondant (65, 97 ou 133 octets) en tant que OriginatorPublicKey, et remettez le même point à OwnPublicPoint, le paramètre réservé aux schémas NIST. Des longueurs fausses lèvent immédiatement. Le point du destinataire tiré du certificat est confronté à l'équation de la courbe avant que toute multiplication ne tourne
Ce qu'aucune bibliothèque ne peut vérifier, c'est si votre clé publique d'initiateur appartient à votre scalaire. Des moitiés qui ne correspondent pas produisent quand même une enveloppe bien formée. L'écrivain dérive sa clé depuis scalaire fois point du destinataire, le destinataire dérive la sienne depuis sa clé privée fois le mauvais point d'initiateur, et le contrôle d'intégrité RFC 3394 côté destinataire est le premier à s'en apercevoir, alors le fichier a déjà été livré. Un chargement aller-retour contre une clé de test sur la même courbe a sa place dans votre build ; un EndDoc réussi et une taille de fichier plausible ne prouvent rien ici
Cette étape d'appariement est la raison pour laquelle le choix de la courbe compte en pratique. Si vous contrôlez l'émission des certificats, X25519 est la courbe plus facile à manipuler correctement depuis Delphi : un scalaire aléatoire de 32 octets, un appel de helper, aucun point à garder en synchronisation à la main. Les courbes NIST gagnent leur place quand les certificats des destinataires viennent d'une PKI qui n'émet que du P-256 ou du P-384, et dans ce cas le chargement aller-retour cesse d'être optionnel
Pourquoi l'ordre de /Recipients compte-t-il ?
L'ordre de /Recipients compte parce que la clé de fichier est un condensat sur la graine suivie de chaque enveloppe dans l'ordre du tableau, si bien qu'écrivain et lecteur doivent hacher des octets identiques dans une séquence identique. HotPDF garde les enveloppes dans l'ordre où vous les ajoutez et les écrit inchangées, ce qui vous laisse libre de choisir n'importe quel ordre. Ce qui arrive au tableau ensuite est la partie risquée. Les outils de post-traitement, les pipelines de signature et les constructeurs d'enveloppes maison sont là où ça casse, et les défaillances partagent une même forme — deux parties hachant des octets légèrement différents :
- La
TListnon typée de Delphi stocke un pointeur brut. Collecter des enveloppes avecList.Add(Pointer(Bytes))laisse le compte de références à la variable locale, si bien que le prochainSetLengthsur cette variable libère ou réutilise le tampon et chaque case peut finir par aliaser la dernière enveloppe.TList<TBytes>ou un simpleTArray<TBytes>garde les références comptées - Les parseurs DER qui travaillent en place modifient le tampon qu'ils parsent. Hachez une copie immaculée de chaque enveloppe, prise avant que toute tentative de déballage ne la touche
- Une
TStringListUnicode n'est pas l'endroit du DER binaire : les octets à$80ou au-dessus se font réencoder par la page de code en chemin. Gardez les enveloppes enTBytes, ou hex-encodez-les d'abord - Le chiffré appartient aux chaînes hexadécimales. À l'intérieur d'une chaîne littérale, CR, LF et CRLF se relisent tous comme un unique LF (ISO 32000-1 §7.3.4.2), si bien qu'une enveloppe qui contient par hasard
$0D $0Aperd un octet. HotPDF écrit chaque entrée/Recipientscomme chaîne hexadécimale et la laisse hors du chiffrement des chaînes, puisqu'un lecteur a besoin des enveloppes avant de détenir une clé - Le premier octet de contenu d'un
BIT STRINGDER compte les bits inutilisés et doit valoir$00pour des clés alignées sur l'octet. Du DER écrit à la main qui laisse tomber cet octet et met le point0x04||X||Ydirectement dans le contenu décale toute la clé d'un octet, et un parseur strict rejette la clé d'initiateur sans appel - Quand une clé qui devrait marcher ne peut toujours pas déchiffrer, comparez couche par couche, dans cet ordre : clé de fichier, l'IV à l'avant du chiffré, clé par objet, clair. La faute se trouve juste après la première couche qui diverge
Charger un PDF chiffré par certificats avec des clés privées
Les clés privées doivent être enregistrées avant que LoadFromFile ne tourne, parce que HotPDF récupère la clé de fichier pendant la passe structurelle. Les identités RSA parsées avec HPDFParsePFX vont dans PubSecKeyMaterial, et les clés RSA supplémentaires dans AddPubSecKeyMaterial, qui refuse toute matière sans module et sans exposant privé. Ces deux emplacements de clé se reconnaissent sur le module RSA et l'exposant privé, si bien que les clés EC, celles issues de PFX comprises, empruntent une troisième voie : AddPubSecAgreementKeyMaterial(CurveOID, PrivateScalar, OwnPublicPoint), avec l'une des constantes HPDFOIDX25519, HPDFOIDX448, HPDFOIDECP256, HPDFOIDECP384 ou HPDFOIDECP521. Les courbes NIST exigent le point public non compressé propre au destinataire ; les courbes de Montgomery l'ignorent
Une identité EC qui arrive en fichier PFX passe par le même appel. HPDFParsePFX la signale avec KeyAlgorithm = pfxkaECDSA (identités X25519 et X448 comprises) et remplit CurveOID, le scalaire privé dans PrivateKey et le point du certificat dans PublicKey, qui se mappent directement sur les trois paramètres
uses
System.SysUtils, System.IOUtils, HPDFDoc, HPDFPFX, HPDFKeyAgreement;
procedure OpenAuditPack(const LegalScalar: TBytes);
var
Reader: THotPDF;
EcKey: THPDFPFXKeyMaterial;
begin
Reader := THotPDF.Create(nil);
try
Reader.AutoLaunch := False;
// l'identité RSA devient la clé primaire
Reader.PubSecKeyMaterial :=
HPDFParsePFX(TFile.ReadAllBytes('reviewer-a.pfx'), 'pfx-password');
// identité EC depuis un PFX : courbe, scalaire et point vont dans la liste d'accord
EcKey := HPDFParsePFX(TFile.ReadAllBytes('reviewer-c-p256.pfx'), 'pfx-password');
if EcKey.KeyAlgorithm = pfxkaECDSA then
Reader.AddPubSecAgreementKeyMaterial(EcKey.CurveOID,
EcKey.PrivateKey, EcKey.PublicKey);
// scalaire X25519 brut détenu par l'équipe juridique ; aucun point nécessaire
Reader.AddPubSecAgreementKeyMaterial(HPDFOIDX25519, LegalScalar, nil);
Reader.LoadFromFile('audit-pack.pdf', '');
Writeln('Pages: ', Reader.GetLoadedPageCount);
finally
Reader.Free;
end;
end;
Laissez faire, et HotPDF essaie chaque enveloppe contre chaque clé enregistrée : la clé RSA primaire, puis chaque clé RSA supplémentaire, puis la matière EC. PubSecRecipientQuery resserre cela sur une seule enveloppe. Le callback reçoit le nombre d'enveloppes et doit renvoyer un index à base zéro à l'intérieur du tableau ; toute autre valeur, -1 comprise, lève pendant le chargement. Pour essayer chaque enveloppe, laissez la propriété non affectée. Sous Delphi le type est une référence à une fonction, donc une méthode anonyme marche, tandis que sous FPC c'est un simple pointeur de fonction dont l'argument Context vient de PubSecRecipientQueryContext
Un comportement mérite un test rien que pour lui. Quand aucune clé enregistrée ne déballe aucune enveloppe, la récupération de clé se termine sans clé de fichier et ne lève pas pour cette raison. Affirmez sur du contenu que vous savez présent dans le document, telle une chaîne de texte ou une valeur de métadonnées ; un appel LoadFromFile qui revient normalement ne montre que la structure du fichier parsée
Que vérifier dans le lecteur cible ?
L'écrivain et le lecteur de HotPDF s'accordent octet pour octet, et les enveloppes suivent les structures CMS citées plus haut. Les lecteurs tiers sont une autre question : le support du transport de clé RSA-OAEP, et plus encore celui des destinataires X25519 ou X448, varie selon les lecteurs et selon les versions du même lecteur. Si le document doit s'ouvrir dans un lecteur précis, chiffrez un fichier de test pour un certificat de test du même type de clé et ouvrez-le là avant de vous engager sur un schéma. Quand le lecteur est hors de votre contrôle, c'est sa liste de support qui choisit le schéma, et cela passe devant l'argument de commodité X25519 fait plus haut
Les permissions portées par les enveloppes restent une politique que les logiciels conformes honorent, exactement comme sous chiffrement par mot de passe. La qualité de la graine est votre affaire : AESGenerateRandomBytes existe pour ce travail, et HotPDF efface sa propre copie de la graine une fois la clé de fichier dérivée. Si une chaîne, un flux ou une pièce jointe a besoin d'un autre crypt filter, le guide des politiques de crypt filter pour StmF, StrF et EFF montre comment en affecter un ; le gestionnaire à clé publique accepte DefaultCryptFilter ou Identity
Les enveloppes par destinataire pour certificats RSA et à courbes elliptiques, avec le chargement de clés au retour, font partie du composant PDF HotPDF pour Delphi et C++Builder, à côté du chiffrement par mot de passe et des signatures numériques