Die PDFium Component for Delphi prüft jede ECDSA- und EdDSA-PDF-Signatur gegen das ISO/TS-32002-Algorithmenprofil und meldet das Urteil in TPadesSignatureValidation.AlgorithmPolicyStatus. Nur P-256, P-384, P-521, die drei Brainpool-r1-Kurven, Ed25519 und Ed448 können bestehen, jeweils mit passendem Digest, und eine Diskrepanz wirft ppeiSignatureAlgorithmMismatch. Diese Policy ist wichtiger, als sie klingt. Eine Signatur auf brainpoolP160r1 oder ein P-256-Schlüssel, der einen SHA-512-Digest signiert, verifiziert auf mathematischer Ebene völlig korrekt, also meldet Windows CryptoAPI den Signaturwert als gut, während ein strenger PDF-2.0-Validator die Datei zurückweist. Der Policy-Check schließt diese Lücke, und er ist bewusst getrennt von der Frage, ob die Signatur-Bytes kryptografisch korrekt sind
Was erlaubt ISO/TS 32002 für Elliptic-Curve-Signaturen wirklich?
ISO/TS 32002 erlaubt genau sechs ECDSA-Kurven und zwei EdDSA-Schemata in PDF-Signaturen und bindet jede Kurve an die Digest-Größen, die sie tragen darf. Die PDFium Component kodiert diese Tabelle in PadesCurveDigestAllowed, mit der Kurven-OID aus dem Signer-Zertifikat als Schlüssel. Die NIST-Kurven sind streng: Der Digest muss dieselbe Bitbreite wie die Kurve haben, entweder SHA-2 oder SHA-3. Die Brainpool-Kurven sind großzügiger und akzeptieren die eigene Breite oder alles breitere:
- P-256 (
1.2.840.10045.3.1.7): nur SHA-256 oder SHA3-256 - P-384 (
1.3.132.0.34): nur SHA-384 oder SHA3-384 - P-521 (
1.3.132.0.35): nur SHA-512 oder SHA3-512 - brainpoolP256r1 (
1.3.36.3.3.2.8.1.1.7): jeder SHA-2- oder SHA-3-Digest von 256 bis 512 Bits - brainpoolP384r1 (
1.3.36.3.3.2.8.1.1.11): 384- oder 512-Bit-SHA-2 / SHA-3 - brainpoolP512r1 (
1.3.36.3.3.2.8.1.1.13): nur SHA-512 oder SHA3-512
EdDSA hat keine Kurvenwahl und keine Digest-Wahl – genau deshalb drehen sich seine Regeln um Kodierung statt um Stärke. Laut RFC 8419 muss ein Ed25519-SignerInfo SHA-512 als sein digestAlgorithm ohne Parameter deklarieren, und ein Ed448-SignerInfo auf dem Pfad der signierten Attribute, den PAdES stets nutzt, muss id-shake256-len (2.16.840.1.101.3.4.2.18) mit einem INTEGER-Parameter von exakt 512 deklarieren. Für beide Schemata dürfen der AlgorithmIdentifier der Signatur und der AlgorithmIdentifier des öffentlichen Zertifikatsschlüssels überhaupt keine Parameter tragen. Ein Producer, der dort ein NULL schreibt – die Gewohnheit, die RSA-Encoder vielen ASN.1-Bibliotheken antrainiert haben –, produziert eine nicht-konforme Signatur, obwohl Schlüssel und Signaturwert in Ordnung sind
Wie die PDFium Component das Algorithmus-Tripel aus dem CMS extrahiert
PDFium selbst kann diese Frage nicht beantworten, denn seine öffentliche Signatur-API liest das Signature-Dictionary, verifiziert aber weder das CMS noch legt es die Kurve des Signer-Zertifikats offen. Die auf PDFium aufgebaute PAdES-Inspektions-Schicht parst das CMS-SignedData (RFC 5652) deshalb selbst. InspectPadesSignatureAlgorithm liest digestAlgorithm und signatureAlgorithm des ersten SignerInfo, sucht dann das Signer-Zertifikat und liest dessen SubjectPublicKeyInfo, um Schlüsselalgorithmus und Kurve zu bekommen. Die Zertifikatssuche ist bewusst begrenzt: Höchstens 64 Zertifikate im certificates-Set des CMS werden untersucht, der Match ist ein exakter Bytevergleich von Issuer und Seriennummer aus issuerAndSerialNumber, und der Code fällt nur dann auf „das einzige vorhandene Zertifikat“ zurück, wenn das Set genau ein parsebares Zertifikat hält. Das erste EC-Zertifikat aus einer ungeordneten Menge zu greifen wäre leicht – und würde einem CA-Zertifikat erlauben zu entscheiden, welche Kurve der Signer angeblich genutzt hat
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 wendet die Policy als Teil seines Compliance-Durchgangs an, ausgehend vom Zertifikat, das es im CMS lokalisiert, und TPdf.ValidatePadesTrust führt sie erneut gegen das Signer-Zertifikat aus, das Windows CryptoAPI tatsächlich für die Verifikation genutzt hat – das von CryptoAPI gemeldete Zertifikat hat also das letzte Wort. Jede rohe Eingabe landet in TPadesSignatureAlgorithmInfo, eingeschlossen DigestParametersPresent, DigestParameterBits, SignatureParametersPresent und PublicKeyParametersAreNamedCurve, sodass eine Zurückweisung stets aus dem Record heraus erklärbar ist statt aus einer Logzeile
Warum scheitert eine P-256-Signatur mit SHA3-256 an der Policy?
Eine P-256-Signatur fällt durch die Policy der PDFium Component, wann immer digestAlgorithm im CMS und der von der ECDSA-signatureAlgorithm implizierte Digest auseinanderlaufen, selbst wenn beide für die Kurve einzeln akzeptabel sind. EvaluatePadesSignatureAlgorithm mappt zuerst ecdsa-with-SHA256, ecdsa-with-SHA3-256 und ihre Geschwister auf einen Digest, vergleicht diesen mit dem deklarierten digestAlgorithm und liefert bei jeder Differenz pcsInvalid, bevor die Kurventabelle befragt wird. Der Fall ist real: Ein Signaturwerkzeug schaltet seinen Hash auf SHA3-256 um, behält aber einen hart-kodierten ecdsa-with-SHA256-Bezeichner, und das Ergebnis ist eine Datei, die kein konformer Verifizierer konsistent interpretieren kann. Die Funktion ist öffentlich, also lässt sich die Matrix im Unit-Test pinnen, ohne ein PDF zu bauen:
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); // Digest-Diskrepanz
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, jetzt kongruent
Assert(EvaluatePadesSignatureAlgorithm(Info) = pcsUnsupported); // Kurve nicht im Profil
end;
Die Kurvenkodierung bekommt dieselbe Strenge. RFC 5480 §2.1.1 lässt ECParameters eine benannte Kurven-OID, eine implizite Kurve (NULL) oder einen vollständigen expliziten Parametersatz sein, und die PKIX-Profile verlangen die benannte Form. Die PDFium Component liefert pcsInvalid, wenn ein id-ecPublicKey-Zertifikat keine Parameter, implizite Parameter oder explizite Parameter trägt, denn explizite Parameter lassen einen Angreifer eine Kurve beschreiben, die einer Standardkurve bloß ähnelt. Eine korrekt benannte Kurve, die schlicht nicht auf der ISO/TS-32002-Liste steht, etwa brainpoolP160r1 oben oder secp256k1, bekommt stattdessen pcsUnsupported
Invalid, unsupported oder indeterminate: den Status ehrlich lesen
Die drei Nicht-valid-Status von AlgorithmPolicyStatus bedeuten Unterschiedliches, und sie in einen einzigen „gescheitert“-Eimer zu werfen, wirft die Information weg, die Auditoren brauchen. pcsInvalid bedeutet, eine erkannte Algorithmenkombination ist missgebildet oder unpassend; es hängt ppeiSignatureAlgorithmMismatch an TPadesValidationResult.Issues an und treibt den aggregierten IntegrityStatus zu pcsInvalid, sodass IsCryptographicallyValid False liefert, selbst wenn der CMS-Signaturwert in Ordnung geht. pcsUnsupported bedeutet, Kurve oder Digest liegen außerhalb dessen, was das Profil benennt – ein Capability-Ergebnis, kein Tampering-Beweis. pcsIndeterminate bedeutet, das Signer-Zertifikat ließ sich nicht festnageln, meist ein CertificateSet mit mehreren Kandidaten und keinem exakten issuerAndSerialNumber-Match, also weigert sich der Code, die Kurve zu raten; seit v3.124.0 markiert es auch eine RSA-Signatur über SHA-1 oder einen 112-Bit-Digest wie SHA-224, die für aktuelle Validierung nicht mehr vereinbart ist. Dasselbe Split gilt für EdDSA auf einer Maschine, deren CryptoAPI Ed25519 oder Ed448 nicht verifizieren kann: CmsSignatureStatus bleibt pcsUnsupported, während AlgorithmPolicyStatus weiterhin pcsValid sein kann, denn die Kodierung war korrekt und nur der Verifizierer fehlte. Wenn Sie einer Zurückweisung von Adobe oder einem DSS-basierten Validator hinterherjagen, deckt der Leitfaden, warum Validatoren PAdES-Signaturen zurückweisen die anderen häufigen Ursachen ab
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; // offline, keine Revocation
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;
Was garantiert pcsValid nicht?
AlgorithmPolicyStatus = pcsValid bescheinigt nur, dass eine ECDSA- oder EdDSA-Signatur eine erlaubte Kurve mit einem kongruenten, korrekt kodierten Digest nutzt und eine RSA-Signatur einen Digest aus einer aktuellen Suite; ob der Signaturwert korrekt ist, sagt es nicht. Vor v3.124.0 war der RSA-Zweig von EvaluatePadesSignatureAlgorithm bewusst weit: Jede signatureAlgorithm unter dem PKCS-#1-Arc 1.2.840.113549.1.1.* lieferte pcsValid, das Legacy-sha1WithRSAEncryption eingeschlossen. Seit PDFiumPas v3.124.0 wendet der RSA-Zweig die ETSI-TS-119-312-Signatur-Suiten an. MD2-, MD4- und MD5-Digests sind pcsInvalid. SHA-1 und 112-Bit-Digests wie SHA-224 sind pcsIndeterminate, also behält eine SHA-1-Signatur ihr gesamtes Integritätsergebnis und wird zur Prüfung markiert statt zurückgewiesen. Ein digestAlgorithm, der vom vom Signaturalgorithmus festgelegten Digest abweicht, etwa sha256WithRSAEncryption über einem SHA-1-Digest, oder ein RSA-Signaturalgorithmus auf einem Nicht-RSA-Signerschlüssel ist pcsInvalid, wird als ppeiSignatureAlgorithmMismatch gemeldet und lässt die Integrität fallen. Ein nicht auffindbares Signer-Zertifikat gibt pcsIndeterminate, wie es bei ECDSA schon der Fall war, und unbekannte Digests oder Nicht-Signatur-RSA-OIDs geben pcsUnsupported. Die Modulus-Länge wird weiterhin nicht geprüft, PSS-Parameter werden hier nicht validiert (der Artikel zu den RFC-4055-RSASSA-PSS-Parametern beschreibt, wie sie auf der Signierseite kodiert werden), und SHA-1- oder MD5-Digests werfen zusätzlich das separate Issue ppeiBadDigestAlgorithm auf. Ebenso bleiben Signatur-Mathe, Zertifikatskette und Revocation Sache von CmsSignatureStatus, CertificateTrustStatus und RevocationStatus, die aus Windows CryptoAPI kommen. Behandeln Sie pcsValid als „das Algorithmenprofil hält“, nie als „dieser Schlüssel ist stark genug“
Für eine Delphi-Anwendung, die signierte PDF-Rechnungen, Verträge oder Archivpakete annimmt, ist das praktische Setup kurz: ValidatePadesTrust ausführen, bei ppeiSignatureAlgorithmMismatch zurückweisen, pcsUnsupported und pcsIndeterminate an einen Menschen weiterleiten und eine eigene RSA-Schlüsselgrößen-Untergrenze durchsetzen, weil die Policy es nicht tut. Die PDFium Component for Delphi and Lazarus liefert den PAdES-Validator, den Evidence-Report-Builder und die Signatur-Pipeline, sodass dieselbe Bibliothek diese Signaturen erzeugen und End-to-End prüfen kann