Le PDFium Component pour Delphi vérifie chaque signature PDF ECDSA et EdDSA contre le profil d'algorithmes ISO/TS 32002 et rapporte le verdict dans TPadesSignatureValidation.AlgorithmPolicyStatus. Seules P-256, P-384, P-521, les trois courbes Brainpool r1, Ed25519 et Ed448 peuvent passer, chacune avec un condensé apparié, et un désaccord lève ppeiSignatureAlgorithmMismatch. Cette politique compte plus qu'elle ne sonne. Une signature sur brainpoolP160r1, ou une clé P-256 signant un condensé SHA-512, peut se vérifier parfaitement au niveau mathématique, si bien que Windows CryptoAPI rapporte la valeur de signature comme bonne tandis qu'un validateur PDF 2.0 strict rejette le fichier. Le contrôle de politique ferme cet écart, et il est délibérément séparé de la question de savoir si les octets de signature sont cryptographiquement corrects
Que permet réellement ISO/TS 32002 pour les signatures à courbes elliptiques ?
ISO/TS 32002 autorise exactement six courbes ECDSA et deux schémas EdDSA dans les signatures PDF, et il lie chaque courbe aux tailles de condensé qu'elle peut porter. Le PDFium Component encode cette table dans PadesCurveDigestAllowed, indexée par l'OID de courbe du certificat du signataire. Les courbes NIST sont strictes : le condensé doit avoir la même largeur en bits que la courbe, en SHA-2 ou en SHA-3. Les courbes Brainpool sont plus souples et acceptent leur propre largeur ou n'importe quoi de plus large :
- P-256 (
1.2.840.10045.3.1.7) : SHA-256 ou SHA3-256 seulement - P-384 (
1.3.132.0.34) : SHA-384 ou SHA3-384 seulement - P-521 (
1.3.132.0.35) : SHA-512 ou SHA3-512 seulement - brainpoolP256r1 (
1.3.36.3.3.2.8.1.1.7) : tout condensé SHA-2 ou SHA-3 de 256 à 512 bits - brainpoolP384r1 (
1.3.36.3.3.2.8.1.1.11) : SHA-2 / SHA-3 de 384 ou 512 bits - brainpoolP512r1 (
1.3.36.3.3.2.8.1.1.13) : SHA-512 ou SHA3-512 seulement
EdDSA n'a pas de choix de courbe ni de choix de condensé, et c'est exactement pourquoi ses règles portent sur le codage plutôt que sur la force. Selon RFC 8419, un SignerInfo Ed25519 doit déclarer SHA-512 comme son digestAlgorithm sans paramètres, et un SignerInfo Ed448, sur le chemin des attributs signés que PAdES utilise toujours, doit déclarer id-shake256-len (2.16.840.1.101.3.4.2.18) avec un paramètre INTEGER d'exactement 512. Pour les deux schémas, l'AlgorithmIdentifier de signature et l'AlgorithmIdentifier de clé publique du certificat ne doivent porter aucun paramètre du tout. Un producteur qui y écrit un NULL, l'habitude que les encodeurs RSA ont inculquée à beaucoup de bibliothèques ASN.1, produit une signature non conforme même si la clé et la valeur de signature sont bonnes
Comment le PDFium Component extrait le triple d'algorithmes du CMS
PDFium lui-même ne peut pas répondre à cette question, parce que son API de signature publique lit le dictionnaire de signature mais ne vérifie pas le CMS et n'expose pas la courbe du certificat du signataire. La couche d'inspection PAdES construite sur PDFium analyse donc elle-même le CMS SignedData (RFC 5652). InspectPadesSignatureAlgorithm lit le digestAlgorithm et le signatureAlgorithm du premier SignerInfo, puis trouve le certificat du signataire et lit son SubjectPublicKeyInfo pour obtenir l'algorithme de clé et la courbe. La recherche de certificat est délibérément bornée : au plus 64 certificats de l'ensemble certificates du CMS sont examinés, la correspondance est une comparaison d'octets exacte de l'émetteur et du numéro de série issus de issuerAndSerialNumber, et le code ne retombe sur « le seul certificat qui soit là » que quand l'ensemble contient exactement un certificat analysable. Prendre le premier certificat EC d'un ensemble non ordonné serait facile, et cela laisserait un certificat d'AC décider de la courbe que le signataire aurait prétendument utilisée
uses
PDFium, FPdfPades;
const
StatusNames: array[TPadesCryptoStatus] of string =
('not checked', 'valid', 'invalid', 'unsupported', 'indeterminate');
var
Pdf: TPdf;
R: TPadesValidationResult;
A: TPadesSignatureAlgorithmInfo;
I: Integer;
begin
Pdf := TPdf.Create(nil);
try
Pdf.FileName := 'invoice-ecdsa-signed.pdf';
Pdf.Active := True;
R := Pdf.ValidatePades;
for I := 0 to Length(R.Signatures) - 1 do
begin
A := R.Signatures[I].AlgorithmInfo;
Writeln('Signature ', I);
Writeln(' digestAlgorithm : ', string(A.DigestAlgorithmOid));
Writeln(' signatureAlgorithm : ', string(A.SignatureAlgorithmOid));
Writeln(' public key / curve : ', string(A.PublicKeyAlgorithmOid),
' / ', string(A.CurveOid));
Writeln(' policy : ',
StatusNames[R.Signatures[I].AlgorithmPolicyStatus]);
end;
if ppeiSignatureAlgorithmMismatch in R.Issues then
Writeln('At least one signature violates the ISO/TS 32002 profile');
finally
Pdf.Free;
end;
end;
TPdf.ValidatePades applique la politique dans le cadre de sa passe de conformité, à partir du certificat qu'il localise dans le CMS, et TPdf.ValidatePadesTrust la rejoue contre le certificat du signataire que Windows CryptoAPI a réellement utilisé pour la vérification, si bien que le certificat rapporté par CryptoAPI a le dernier mot. Chaque entrée brute atterrit dans TPadesSignatureAlgorithmInfo, y compris DigestParametersPresent, DigestParameterBits, SignatureParametersPresent et PublicKeyParametersAreNamedCurve, si bien qu'un rejet est toujours explicable depuis l'enregistrement plutôt que depuis une ligne de log
Pourquoi une signature P-256 avec SHA3-256 échoue-t-elle à la politique ?
Une signature P-256 échoue à la politique du PDFium Component chaque fois que le digestAlgorithm du CMS et le condensé impliqué par le signatureAlgorithm ECDSA sont en désaccord, même si tous deux sont individuellement acceptables pour la courbe. EvaluatePadesSignatureAlgorithm mappe d'abord ecdsa-with-SHA256, ecdsa-with-SHA3-256 et leurs frères vers un condensé, le compare au digestAlgorithm déclaré, et renvoie pcsInvalid à la moindre différence avant que la table de courbes ne soit consultée. Le cas est réel : un outil de signature passe son hachage à SHA3-256 mais garde un identifiant ecdsa-with-SHA256 codé en dur, et le résultat est un fichier qu'aucun vérificateur conforme ne peut interpréter de façon cohérente. La fonction est publique, si bien que la matrice peut être épinglée dans un test unitaire sans construire de PDF :
var
Info: TPadesSignatureAlgorithmInfo;
begin
Info := Default(TPadesSignatureAlgorithmInfo);
Info.Family := psafEcdsa;
Info.PublicKeyAlgorithmOid := '1.2.840.10045.2.1'; // id-ecPublicKey
Info.PublicKeyParametersPresent := True;
Info.PublicKeyParametersAreNamedCurve := True;
Info.CurveOid := '1.2.840.10045.3.1.7'; // P-256
Info.DigestAlgorithmOid := '2.16.840.1.101.3.4.2.8'; // SHA3-256
Info.SignatureAlgorithmOid := '2.16.840.1.101.3.4.3.10'; // ecdsa-with-SHA3-256
Assert(EvaluatePadesSignatureAlgorithm(Info) = pcsValid);
Info.SignatureAlgorithmOid := '1.2.840.10045.4.3.2'; // ecdsa-with-SHA256
Assert(EvaluatePadesSignatureAlgorithm(Info) = pcsInvalid); // désaccord de condensé
Info.CurveOid := '1.3.36.3.3.2.8.1.1.1'; // brainpoolP160r1
Info.DigestAlgorithmOid := '2.16.840.1.101.3.4.2.1'; // SHA-256, désormais congruent
Assert(EvaluatePadesSignatureAlgorithm(Info) = pcsUnsupported); // courbe hors du profil
end;
Le codage de la courbe reçoit la même sévérité. RFC 5480 §2.1.1 laisse ECParameters être un OID de courbe nommée, une courbe implicite (NULL) ou un jeu de paramètres explicites complet, et les profils PKIX exigent la forme nommée. Le PDFium Component renvoie pcsInvalid quand un certificat id-ecPublicKey ne porte aucun paramètre, des paramètres implicites ou des paramètres explicites, parce que les paramètres explicites laissent un attaquant décrire une courbe qui ressemble seulement à une courbe standard. Une courbe correctement nommée qui manque simplement à la liste ISO/TS 32002, comme brainpoolP160r1 ci-dessus ou secp256k1, reçoit pcsUnsupported à la place
Invalide, non pris en charge ou indéterminé : lire le statut honnêtement
Les trois statuts non valides de AlgorithmPolicyStatus signifient des choses différentes, et les aplatir dans un même panier « échoué » jette les informations dont les auditeurs ont besoin. pcsInvalid signifie qu'une combinaison d'algorithmes reconnue est mal formée ou en désaccord ; il ajoute ppeiSignatureAlgorithmMismatch à TPadesValidationResult.Issues et pousse l'IntegrityStatus agrégé à pcsInvalid, si bien que IsCryptographicallyValid renvoie False même quand la valeur de signature CMS se vérifie. pcsUnsupported signifie que la courbe ou le condensé est hors de ce que le profil nomme, ce qui est un résultat de capacité, pas une preuve de falsification. pcsIndeterminate signifie que le certificat du signataire n'a pas pu être épinglé, d'ordinaire un CertificateSet avec plusieurs candidats et aucune correspondance issuerAndSerialNumber exacte, si bien que le code refuse de deviner la courbe ; depuis la v3.124.0, il marque aussi une signature RSA sur SHA-1 ou sur un condensé de 112 bits tel que SHA-224, qui n'est plus admis pour une validation courante. Le même partage vaut pour EdDSA sur une machine dont le CryptoAPI ne peut pas vérifier Ed25519 ou Ed448 : CmsSignatureStatus reste pcsUnsupported tandis que AlgorithmPolicyStatus peut toujours être pcsValid, parce que le codage était correct et seul le vérificateur manquait. Si vous cherchez la cause d'un rejet d'Adobe ou d'un validateur basé sur DSS, le guide des raisons pour lesquelles les validateurs rejettent les signatures PAdES couvre les autres causes courantes
var
Pdf: TPdf;
Options: TPadesTrustValidationOptions;
R: TPadesValidationResult;
S: TPadesSignatureValidation;
I: Integer;
begin
Pdf := TPdf.Create(nil);
try
Pdf.FileName := 'invoice-ecdsa-signed.pdf';
Pdf.Active := True;
Options := TPadesTrustValidationOptions.Default; // hors ligne, sans révocation
R := Pdf.ValidatePadesTrust(Options);
for I := 0 to Length(R.Signatures) - 1 do
begin
S := R.Signatures[I];
if S.IsDocumentTimeStamp then
Continue;
case S.AlgorithmPolicyStatus of
pcsInvalid: Writeln(I, ': reject, algorithm combination is invalid');
pcsUnsupported: Writeln(I, ': manual review, curve or digest outside the profile');
pcsIndeterminate: Writeln(I, ': manual review, signer not identified or digest no longer agreed');
pcsValid:
if S.AlgorithmInfo.Family = psafRsa then
Writeln(I, ': RSA digest suite accepted, check key size yourself')
else
Writeln(I, ': EC/EdDSA profile satisfied');
end;
end;
Writeln('Cryptographically valid: ', R.IsCryptographicallyValid);
finally
Pdf.Free;
end;
end;
Que ne garantit pas pcsValid ?
AlgorithmPolicyStatus = pcsValid ne certifie qu'une signature ECDSA ou EdDSA utilise une courbe approuvée avec un condensé congruent et correctement codé, et qu'une signature RSA utilise un condensé d'une suite courante ; il ne dit rien de la justesse de la valeur de signature. Avant la v3.124.0, la branche RSA de EvaluatePadesSignatureAlgorithm était volontairement large : tout signatureAlgorithm sous l'arc PKCS #1 1.2.840.113549.1.1.* renvoyait pcsValid, sha1WithRSAEncryption historique compris. Depuis PDFiumPas v3.124.0, la branche RSA applique les suites de signatures ETSI TS 119 312. Les condensés MD2, MD4 et MD5 sont pcsInvalid. SHA-1 et les condensés de 112 bits tels que SHA-224 sont pcsIndeterminate, si bien qu'une signature SHA-1 garde son résultat d'intégrité global et est signalée pour revue plutôt que rejetée. Un digestAlgorithm qui diffère du condensé fixé par l'algorithme de signature, comme sha256WithRSAEncryption sur un condensé SHA-1, ou un algorithme de signature RSA sur une clé de signataire non RSA, est pcsInvalid, rapporté comme ppeiSignatureAlgorithmMismatch et fait échouer l'intégrité. Un certificat de signataire introuvable donne pcsIndeterminate, comme il le faisait déjà pour ECDSA, et les condensés non reconnus ou les OID RSA hors signature donnent pcsUnsupported. La longueur du module n'est toujours pas vérifiée, les paramètres PSS ne sont pas validés ici (l'article RFC 4055 sur les RSASSA-PSS-params couvre leur codage côté signature), et les condensés SHA-1 ou MD5 lèvent en plus le problème séparé ppeiBadDigestAlgorithm. De même, les maths de signature, la chaîne de certificats et la révocation restent le travail de CmsSignatureStatus, CertificateTrustStatus et RevocationStatus, qui viennent de Windows CryptoAPI. Traitez pcsValid comme « le profil d'algorithmes tient », jamais comme « cette clé est assez forte »
Pour une application Delphi qui accepte des factures PDF signées, des contrats ou des paquets d'archivage, la configuration pratique est courte : lancez ValidatePadesTrust, rejetez sur ppeiSignatureAlgorithmMismatch, routez pcsUnsupported et pcsIndeterminate vers un humain, et faites respecter votre propre plancher de taille de clé RSA parce que la politique ne le fera pas. Le PDFium Component pour Delphi et Lazarus embarque le validateur PAdES, le générateur de rapports de preuves et le pipeline de signature, si bien que la même bibliothèque peut produire ces signatures et les vérifier de bout en bout