Vous passez une routine de signature Delphi au SHA3-512 et OpenSSL rejette le résultat : le CMS déclare SHA3-512, mais son attribut messageDigest contient 32 octets là où cet algorithme en produit 64. Oui, du code Delphi peut signer en SHA-3 : SignPadesForDigest dans PDFium Component accepte SHA3-256, SHA3-384 ou SHA3-512 et écrit chaque champ du CMS dépendant du condensat pour ce seul choix, comme l'ISO/TS 32001 s'y attend
Ce que l'ISO/TS 32001 ajoute à une signature PDF 2.0
L'ISO/TS 32001 est une courte extension de l'ISO 32000-2. Elle élargit l'ensemble des fonctions de hachage qu'une signature PDF peut déclarer. La liste de base siège au tableau 260 de l'ISO 32000-2 §12.8.3.1, et pour les sous-filtres à base de CMS elle s'arrête à SHA-256, SHA-384, SHA-512 et RIPEMD-160, avec SHA-1 déprécié. La spécification technique ajoute la famille SHA-3 issue de FIPS 202. SHA-3 est une construction éponge qui ne partage aucune structure interne avec SHA-2, et c'est là tout l'intérêt pour une politique de signature qui veut une seconde famille de hachage sur l'étagère au cas où SHA-2 faiblirait un jour. Dans le CMS, chaque variante est un identifiant d'objet sous l'arc NIST. Les identifiants d'algorithme de signature combinés siègent juste à côté :
TCmsDigestAlgorithm | OID de condensat | Sortie | RSA PKCS#1 v1.5 | ECDSA |
|---|---|---|---|---|
cdaSha256 | 2.16.840.1.101.3.4.2.1 | 32 octets | 1.2.840.113549.1.1.11 | 1.2.840.10045.4.3.2 |
cdaSha3_256 | 2.16.840.1.101.3.4.2.8 | 32 octets | 2.16.840.1.101.3.4.3.14 | 2.16.840.1.101.3.4.3.10 |
cdaSha3_384 | 2.16.840.1.101.3.4.2.9 | 48 octets | 2.16.840.1.101.3.4.3.15 | 2.16.840.1.101.3.4.3.11 |
cdaSha3_512 | 2.16.840.1.101.3.4.2.10 | 64 octets | 2.16.840.1.101.3.4.3.16 | 2.16.840.1.101.3.4.3.12 |
Dans PDFium Component, tout le choix tient en une énumération, TCmsDigestAlgorithm de l'unité FPdfCms. Laissez la clé choisir le membre. Une courbe NIST attend un condensat de sa propre largeur, si bien que P-256 fait paire avec SHA3-256, P-384 avec SHA3-384 et P-521 avec SHA3-512, et un validateur qui applique la politique de courbes et de condensats de l'ISO/TS 32002 marque toute autre paire invalide. RSA n'a pas une telle règle. L'argument de force est de toute façon court : une clé RSA-2048 délivre à peu près 112 bits de sécurité et RSA-3072 environ 128, si bien que SHA3-256 dépasse déjà la clé, tandis que SHA3-512 sur une clé RSA-2048 ajoute 32 octets au CMS et rien à la sécurité. Nous recommandons SHA3-256 pour RSA, sauf si une politique écrite nomme une variante plus grande
Cinq champs du CMS qui changent avec le condensat
Échanger la fonction de hachage est la plus petite partie du travail. Une signature CAdES détachée nomme son condensat en cinq endroits, et un vérificateur strict les recoupe. Si l'un d'eux dit encore SHA-256, la signature échoue même si l'arithmétique RSA ou ECDSA est correcte :
digestAlgorithmdans le SignerInfo, reflété dans l'ensembledigestAlgorithmsdu SignedData, porte l'OID de condensat, par exemple 2.16.840.1.101.3.4.2.9 pour SHA3-384- L'attribut signé
messageDigest(RFC 5652) contient le haché des deux plages/ByteRange, si bien que son OCTET STRING fait 32, 48 ou 64 octets signatureAlgorithmnomme le schéma combiné du tableau ci-dessus. RSASSA-PSS est l'exception : son OID reste id-RSASSA-PSS (1.2.840.113549.1.1.10) et le condensat passe dans les paramètres- Les paramètres RSASSA-PSS répètent le condensat deux fois, comme
hashAlgorithmet comme hachage MGF1, et posentsaltLengthà la longueur du condensat. Omettez-les et un vérificateur suppose SHA-1, MGF1 avec SHA-1 et un sel de 20 octets, ce qui est un autre algorithme ; les règles d'étiquetage des RSASSA-PSS-params de la RFC 4055 comptent ici aussi - L'attribut signé CMSAlgorithmProtection (RFC 6211) copie les identifiants du condensat et de l'algorithme de signature dans les attributs signés, où personne ne peut les échanger à l'insu de personne. Il doit correspondre aux champs 1 et 3 octet pour octet
Un champ ne change pas du tout. Le ESSCertIDv2 à l'intérieur de l'attribut signing-certificate-v2 (RFC 5035) hache le certificat du signataire avec son propre hashAlgorithm, qui vaut SHA-256 par défaut et est indépendant du condensat du SignedData, si bien que PDFium Component le garde à SHA-256 dans l'encodage par défaut. L'erreur qui apparaît le plus dans du code CMS écrit à la main est la longueur du messageDigest. Du code écrit pour SHA-256 tend à contenir le haché dans un enregistrement fixe de 32 octets et à copier cet enregistrement dans l'attribut. Poussez un résultat SHA3-512 à travers et l'attribut reçoit les 32 premiers octets pendant que digestAlgorithm dit .10 : le rejet OpenSSL de l'ouverture. La même erreur vit aussi un étage plus bas, où le EVP_PKEY_sign d'OpenSSL prend la longueur du condensat comme tbslen et la vérifie contre le condensat posé sur le contexte, si bien qu'un signataire qui passe toujours 32 reçoit une erreur de longueur qui ne mentionne jamais SHA-3. PDFium Component calcule le condensat du document depuis l'algorithme que le signataire résolu déclare et dérive les attributs, les paramètres PSS et l'attribut de protection d'algorithme de cette même déclaration, si bien qu'aucun champ ne peut dévier des autres
Signer un PDF local avec SignPadesForDigest
Sur le composant TPdf les points d'entrée SHA-3 sont SignPadesForDigest(const FileName: string; const Options: TPadesSignOptions; DigestAlgorithm: TCmsDigestAlgorithm): Boolean et son jumeau de flux SignPadesToStreamForDigest. Tous deux prennent le même enregistrement TPadesSignOptions que SignPades, et cdaSha256 produit la même signature que la surcharge existante, si bien qu'un seul chemin de code sert les deux familles. Sous Windows, une empreinte SHA-1 sélectionne un certificat dans le magasin MY de l'utilisateur courant. Le backend OpenSSL sur les autres plateformes signe avec la clé et le certificat donnés à ConfigureSslSigningCredentials et refuse une empreinte. Vérifiez aussi la version du document. L'ISO/TS 32001 étend PDF 2.0, et la signature entre comme une mise à jour incrémentale qui garde l'en-tête d'origine, si bien qu'une partie s'appuyant sur les règles PDF 2.0 a besoin d'un fichier 2.0 avant que vous ne signiez
uses
System.SysUtils, System.Classes, System.IOUtils,
PDFium, FPdfCms, FPdfCrypto, FPdfPades
{$IFDEF MSWINDOWS}, FPdfCryptoWin{$ELSE}, FPdfCryptoSsl{$ENDIF};
function Sha3SigningAvailable: Boolean;
begin
{$IFDEF MSWINDOWS}
Result := WinSha3DigestAvailable; // fournisseurs SHA-3 CNG, Windows 11 24H2+
{$ELSE}
Result := SslSha3SigningAvailable; // False tant que les identifiants ne sont pas configurés
{$ENDIF}
end;
procedure SignWithSha3(const SourceFile, SignedFile, Thumbprint: string;
const KeyPassword: AnsiString; const Log: TProc<string>);
var
Pdf: TPdf;
Opts: TPadesSignOptions;
Signed: TMemoryStream;
begin
{$IFNDEF MSWINDOWS}
// OpenSSL signe avec du matériel explicitement configuré ; pas de chaîne dans ce croquis
ConfigureSslSigningCredentials(
TFile.ReadAllBytes('/etc/signing/signer-key.pem'),
TFile.ReadAllBytes('/etc/signing/signer.der'), nil, KeyPassword);
{$ENDIF}
if not Sha3SigningAvailable then
raise Exception.Create('No SHA-3 capable signing backend on this machine');
Opts := TPadesSignOptions.Default; // B-B, RSASSA-PSS
{$IFDEF MSWINDOWS}
Opts.CertificateThumbprint := Thumbprint; // certificat dans le MY de l'utilisateur courant
{$ENDIF}
Opts.Reason := 'Approved for release';
Pdf := TPdf.Create(nil);
Signed := TMemoryStream.Create;
try
Pdf.FileName := SourceFile;
Pdf.Active := True;
if Pdf.PdfVersion <> pv20 then
Log(SourceFile + ' is not a PDF 2.0 file');
try
if Pdf.SignPadesToStreamForDigest(Signed, Opts, cdaSha3_256) then
Signed.SaveToFile(SignedFile);
except
on E: EPadesCryptoUnavailable do
Log('SHA-3 signing unavailable: ' + E.Message);
end;
finally
Signed.Free;
Pdf.Free;
end;
end;
Quand le backend ne peut pas produire du SHA-3, l'appel lève EPadesCryptoUnavailable au lieu de retomber sur SHA-256. C'est le comportement qu'on veut. Quelqu'un a demandé SHA3-256 pour une raison, d'ordinaire un document de politique de la partie s'appuyant sur la signature, et une signature SHA-256 produite à sa place passerait tous vos contrôles locaux pour être refusée chez eux, des semaines plus tard, quand plus personne dans votre équipe ne se souvient de quelle machine a signé. Attrapez l'exception là où vous pouvez encore dire à l'opérateur quelle machine manque de l'algorithme
Là où le SHA-3 est disponible : CNG de Windows et OpenSSL
La disponibilité appartient à la machine. Sous Windows le backend par défaut est CNG, et CNG livre des fournisseurs d'algorithmes SHA-3 à partir de Windows 11 24H2 ; sur les builds antérieurs, BCryptOpenAlgorithmProvider pour "SHA3-256" échoue avec STATUS_NOT_FOUND. WinSha3DigestAvailable ouvre une fois les fournisseurs SHA3-256, SHA3-384 et SHA3-512 et met la réponse en cache, puisqu'elle ne peut pas changer pendant que tourne le processus. Partout ailleurs, le backend OpenSSL signe, et ce qu'il peut faire dépend du libcrypto qu'il trouve :
- OpenSSL 3.x exporte toujours
EVP_sha3_256,EVP_sha3_384etEVP_sha3_512, si bien que la signature SHA-3 marche sur toute plateforme où le backend se charge - Le chargeur accepte aussi une bibliothèque de série 1.1 (
libcrypto-1_1.dll,libcrypto.so.1.1). Le SHA-3 est arrivé en OpenSSL 1.1.1 et est absent de 1.1.0, si bien que sur d'anciennes images Linux,SslSha3SigningAvailabledécide SslSha3SigningAvailableexige aussi des identifiants de signature configurés. Demandé avantConfigureSslSigningCredentials, il répond False même quandSslLibraryVersionrapporte une très bonne bibliothèque 3.x. Cela ressemble à un backend manquant. Ce n'est qu'un problème d'ordre- Un exécutable 32 bits ne peut pas charger
libcrypto-3-x64.dll. Quand ce backend a été mis en place, un build Win32 pointé vers la DLL x64 rapportaitSslAvailable = Falsetandis queSslMissingSymbolsne renvoyait rien d'autre que le chemin de la bibliothèque, un symptôme qui se lit comme une liaison de symboles cassée ; la cause était la largeur de bits, et une DLL appariée à l'exécutable a réglé le problème
Un parc Windows qui compte encore des machines antérieures à 24H2 peut garder CNG là où il a le SHA-3 et router le reste à travers OpenSSL avec un fournisseur de signataires. IPadesSignerProvider a deux méthodes. ResolveSslSignerForDigest dans FPdfCryptoSsl renvoie un signataire dont le condensat déclaré, l'algorithme de signature, la longueur du condensat et le sel PSS suivent tous l'algorithme demandé, et avec un fournisseur configuré, ResolvePadesSignerForDigest vérifie que le signataire renvoyé déclare le condensat demandé par l'appel, en levant EPadesCrypto sinon. Un fournisseur bâti pour SHA3-256 ne peut pas estampiller une requête SHA3-384. Le fournisseur est toutefois à l'échelle du processus, si bien qu'un serveur qui signe concurremment devrait le configurer une fois au démarrage plutôt qu'à chaque appel comme le fait ce croquis ; ConfigureSslLibraryPath pointe le chargeur vers libcrypto quand la DLL n'est pas sur le chemin de recherche
uses
System.SysUtils, System.Classes, System.IOUtils,
PDFium, FPdfCms, FPdfCrypto, FPdfCryptoWin, FPdfCryptoSsl, FPdfPades;
type
TOpenSslSha3Provider = class(TInterfacedObject, IPadesSignerProvider)
private
FDigest: TCmsDigestAlgorithm;
public
constructor Create(ADigest: TCmsDigestAlgorithm);
function BackendName: string;
function Resolve(const Options: TPadesSignerOptions): TPadesResolvedSigner;
end;
constructor TOpenSslSha3Provider.Create(ADigest: TCmsDigestAlgorithm);
begin
inherited Create;
FDigest := ADigest;
end;
function TOpenSslSha3Provider.BackendName: string;
begin
Result := 'OpenSSL';
end;
function TOpenSslSha3Provider.Resolve(
const Options: TPadesSignerOptions): TPadesResolvedSigner;
begin
Result := ResolveSslSignerForDigest(Options, FDigest);
end;
procedure SignSha3OnAnyWindows(Pdf: TPdf; Dest: TStream;
const Opts: TPadesSignOptions; const KeyPassword: AnsiString);
var
Provider: IPadesSignerProvider;
Chain: TPadesChainCertArray;
SslOpts: TPadesSignOptions;
begin
if WinSha3DigestAvailable then
begin
Pdf.SignPadesToStreamForDigest(Dest, Opts, cdaSha3_256); // chemin CNG
Exit;
end;
Chain := nil;
ConfigureSslSigningCredentials(
TFile.ReadAllBytes('C:\Signing\signer-key.pem'),
TFile.ReadAllBytes('C:\Signing\signer.der'), Chain, KeyPassword);
if not SslSha3SigningAvailable then
raise Exception.Create('No SHA-3 in loaded OpenSSL ' + SslLibraryVersion);
SslOpts := Opts;
SslOpts.CertificateThumbprint := ''; // OpenSSL signe avec le certificat configuré
Provider := TOpenSslSha3Provider.Create(cdaSha3_256);
ConfigurePadesSignerProvider(Provider);
try
Pdf.SignPadesToStreamForDigest(Dest, SslOpts, cdaSha3_256);
finally
ConfigurePadesSignerProvider(nil);
end;
end;
Comment un signataire distant ou HSM apprend-il le condensat ?
La signature distante partage le travail en deux. PreparePadesRemoteSignature écrit un PDF avec un emplacement /Contents vide plus un TPadesRemoteSigningRequest, et le détenteur de la clé renvoie plus tard un CMS détaché, souvent depuis un autre processus ou une autre machine, à travers la session de préparation et de complétion utilisée pour les clés HSM et cloud. Côté préparation, vous posez TPadesRemoteSignOptions.DigestAlgorithm, disons à cdaSha3_384, et la requête enregistre le choix dans TPadesRemoteSigningRequest.DigestAlgorithm à côté de DocumentDigest, la valeur que le messageDigest du CMS doit porter. À partir de là, la requête fait autorité. CompletePadesRemoteSignature recalcule le condensat /ByteRange avec l'algorithme stocké dans la requête et ne consulte plus jamais les options ; sinon, un changement de configuration entre préparation et complétion jugerait une vieille requête avec un algorithme nouveau. Un service de signature bâti sur le composant devrait prendre l'algorithme au même endroit :
uses
System.SysUtils, System.Classes,
FPdfCms, FPdfCrypto, FPdfCryptoSsl, FPdfPades,
FPdfPadesRemote; // lie l'implémentation Enterprise
function SignRemoteRequest(const RequestFile: string;
Padding: TPadesPaddingScheme): TBytes;
var
Stream: TFileStream;
Request: TPadesRemoteSigningRequest;
SignerOpts: TPadesSignerOptions;
Resolved: TPadesResolvedSigner;
begin
Stream := TFileStream.Create(RequestFile, fmOpenRead or fmShareDenyWrite);
try
Request := LoadPadesRemoteSigningRequest(Stream);
finally
Stream.Free;
end;
SignerOpts := TPadesSignerOptions.Default; // empreinte vide pour OpenSSL
SignerOpts.PaddingScheme := Padding;
// La requête décide du condensat ; pas la configuration de ce service
Resolved := ResolveSslSignerForDigest(SignerOpts, Request.DigestAlgorithm);
Result := BuildSignedDataEx(Request.DocumentDigest, Resolved.Signer,
Resolved.SignerCertDer, Resolved.ChainCertDer, TCmsBuildOptions.Default);
end;
À l'intérieur de la requête, la longueur du condensat est la seule preuve de l'algorithme. Elle sert aussi d'empreinte. Avant de lire le moindre flux, CompletePadesRemoteSignature compare Length(DocumentDigest) avec CmsDigestByteLength(DigestAlgorithm), si bien qu'un condensat tronqué ou un algorithme réétiqueté échoue à ce moment et que le flux de destination n'est jamais touché. La longueur seule ne peut pas distinguer SHA3-256 de SHA-256, tous deux à 32 octets, c'est pourquoi la valeur du condensat est aussi comparée au messageDigest à l'intérieur du CMS renvoyé. Le PDF préparé, en revanche, est identique à l'octet près pour SHA-256 et SHA3-384 : l'emplacement réservé ne contient que des nombres /ByteRange et aucun condensat. Des fichiers identiques se lisent facilement à tort comme un réglage ignoré. La signature distante est livrée dans l'édition Enterprise via l'unité FPdfPadesRemote
La variante est rarement la décision difficile ; pour une clé RSA c'est d'ordinaire cdaSha3_256, et ce qui échoue en production, c'est un hôte dont le CNG ou le libcrypto ne peut pas la produire, c'est pourquoi le contrôle de disponibilité se place au démarrage de l'application dans tout déploiement du PDFium Component pour Delphi