Το PDFium Component for Delphi ελέγχει κάθε υπογραφή PDF ECDSA και EdDSA απέναντι στο προφίλ αλγορίθμων του ISO/TS 32002 και αναφέρει την ετυμηγορία στο TPadesSignatureValidation.AlgorithmPolicyStatus. Μόνο P-256, P-384, P-521, οι τρεις καμπύλες Brainpool r1, Ed25519 και Ed448 μπορούν να περάσουν, καθεμία με ταίριαστο digest, και μια ασυμφωνία σηκώνει ppeiSignatureAlgorithmMismatch. Εκείνη η πολιτική μετράει περισσότερο απ' όσο ακούγεται. Μια υπογραφή σε brainpoolP160r1, ή ένα κλειδί P-256 που υπογράφει digest SHA-512, μπορεί να επαληθεύει απόλυτα στο επίπεδο των μαθηματικών, οπότε το Windows CryptoAPI αναφέρει την τιμή της υπογραφής ως καλή ενώ ένας αυστηρός validator PDF 2.0 απορρίπτει το αρχείο. Ο έλεγχος πολιτικής κλείνει εκείνο το κενό, και είναι σκόπιμα ξεχωριστός από την ερώτηση αν τα bytes της υπογραφής είναι κρυπτογραφικά σωστά
Τι επιτρέπει στην πραγματικότητα το ISO/TS 32002 για υπογραφές ελλειπτικών καμπυλών;
Το ISO/TS 32002 επιτρέπει ακριβώς έξι καμπύλες ECDSA και δύο σχήματα EdDSA σε υπογραφές PDF, και δένει κάθε καμπύλη με τα μεγέθη digest που επιτρέπεται να κουβαλά. Το PDFium Component κωδικοποιεί εκείνον τον πίνακα στο PadesCurveDigestAllowed, κλειδωμένο με το OID της καμπύλης από το πιστοποιητικό του υπογράφοντα. Οι καμπύλες NIST είναι αυστηρές: το digest πρέπει να έχει το ίδιο πλάτος bit με την καμπύλη, είτε SHA-2 είτε SHA-3. Οι καμπύλες Brainpool είναι πιο χαλαρές και δέχονται το δικό τους πλάτος ή οτιδήποτε φαρδύτερο:
- P-256 (
1.2.840.10045.3.1.7): μόνο SHA-256 ή SHA3-256 - P-384 (
1.3.132.0.34): μόνο SHA-384 ή SHA3-384 - P-521 (
1.3.132.0.35): μόνο SHA-512 ή SHA3-512 - brainpoolP256r1 (
1.3.36.3.3.2.8.1.1.7): οποιοδήποτε digest SHA-2 ή SHA-3 από 256 έως 512 bits - brainpoolP384r1 (
1.3.36.3.3.2.8.1.1.11): SHA-2 / SHA-3 384 ή 512 bit - brainpoolP512r1 (
1.3.36.3.3.2.8.1.1.13): μόνο SHA-512 ή SHA3-512
Το EdDSA δεν έχει επιλογή καμπύλης ούτε επιλογή digest, που είναι ακριβώς ο λόγος που οι κανόνες του αφορούν κωδικοποίηση και όχι ισχύ. Κατά το RFC 8419, ένα SignerInfo Ed25519 πρέπει να δηλώνει SHA-512 ως digestAlgorithm του χωρίς parameters, και ένα SignerInfo Ed448, στο μονοπάτι signed-attributes που το PAdES χρησιμοποιεί πάντα, πρέπει να δηλώνει id-shake256-len (2.16.840.1.101.3.4.2.18) με parameter INTEGER ακριβώς 512. Και για τα δύο σχήματα το AlgorithmIdentifier της υπογραφής και το AlgorithmIdentifier του δημόσιου κλειδιού του πιστοποιητικού πρέπει να μην κουβαλούν καθόλου parameters. Ένας παραγωγός που γράφει NULL εκεί, τη συνήθεια που οι RSA encoders έχουν καλλιεργήσει σε πολλές βιβλιοθήκες ASN.1, παράγει μη σύμφωνη υπογραφή παρότι το κλειδί και η τιμή της υπογραφής είναι μια χαρά
Πώς εξάγει το PDFium Component το τριπλό των αλγορίθμων από το CMS
Το ίδιο το PDFium δεν μπορεί να απαντήσει αυτή την ερώτηση, επειδή το public API υπογραφών του διαβάζει το signature dictionary αλλά ούτε επαληθεύει το CMS ούτε εκθέτει την καμπύλη του πιστοποιητικού του υπογράφοντα. Η στιβάδα επιθεώρησης PAdES χτισμένη πάνω στο PDFium κάνει επομένως parse την ίδια το CMS SignedData (RFC 5652). Το InspectPadesSignatureAlgorithm διαβάζει το digestAlgorithm και το signatureAlgorithm του πρώτου SignerInfo, μετά βρίσκει το πιστοποιητικό του υπογράφοντα και διαβάζει το SubjectPublicKeyInfo του για να πάρει τον αλγόριθμο κλειδιού και την καμπύλη. Η αναζήτηση πιστοποιητικού είναι σκόπιμα φραγμένη: το πολύ 64 πιστοποιητικά στο σύνολο certificates του CMS εξετάζονται, το ταίριασμα είναι ακριβής σύγκριση bytes του issuer και του serial number από το issuerAndSerialNumber, και ο κώδικας πέφτει πίσω στο «το μοναδικό πιστοποιητικό που υπάρχει» μόνο όταν το σύνολο κρατά ακριβώς ένα parseable πιστοποιητικό. Να διαλέξεις το πρώτο πιστοποιητικό EC από ένα αταξινόμητο σύνολο είναι εύκολο, και θα άφηνε ένα πιστοποιητικό CA να αποφασίσει ποια καμπύλη υποτίθεται ότι χρησιμοποίησε ο υπογράφων
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 εφαρμόζει την πολιτική ως μέρος του περάσματος συμμόρφωσής του, ξεκινώντας από το πιστοποιητικό που εντοπίζει μέσα στο CMS, και το TPdf.ValidatePadesTrust την ξανατρέχει απέναντι στο πιστοποιητικό του υπογράφοντα που το Windows CryptoAPI χρησιμοποίησε όντως για την επαλήθευση, οπότε το πιστοποιητικό που αναφέρει το CryptoAPI έχει την τελευταία λέξη. Κάθε raw είσοδος καταλήγει στο TPadesSignatureAlgorithmInfo, συμπεριλαμβανομένων των DigestParametersPresent, DigestParameterBits, SignatureParametersPresent και PublicKeyParametersAreNamedCurve, οπότε μια απόρριψη είναι πάντα επεξηγήσιμη από την εγγραφή και όχι από μια γραμμή log
Γιατί μια υπογραφή P-256 με SHA3-256 αποτυγχάνει στην πολιτική;
Μια υπογραφή P-256 αποτυγχάνει στην πολιτική του PDFium Component όποτε το digestAlgorithm του CMS και το digest που συνεπάγεται το signatureAlgorithm ECDSA διαφωνούν, ακόμα κι αν και τα δύο είναι ατομικά αποδεκτά για την καμπύλη. Το EvaluatePadesSignatureAlgorithm πρώτα αντιστοιχίζει τα ecdsa-with-SHA256, ecdsa-with-SHA3-256 και τα αδέρφια τους σε digest, το συγκρίνει με το δηλωμένο digestAlgorithm, και επιστρέφει pcsInvalid σε οποιαδήποτε διαφορά πριν συμβουλευτεί ο πίνακας καμπυλών. Η περίπτωση είναι υπαρκτή: ένα εργαλείο υπογραφής αλλάζει το hash του σε SHA3-256 αλλά κρατά καρφωμένο αναγνωριστικό ecdsa-with-SHA256, και το αποτέλεσμα είναι ένα αρχείο που κανένας σύμφωνος verifier δεν μπορεί να ερμηνεύσει συνεπώς. Η συνάρτηση είναι public, οπότε ο πίνακας καρφώνεται σε unit test χωρίς να χτίσεις 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); // ασυμφωνία digest
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, πλέον ταιριαστό
Assert(EvaluatePadesSignatureAlgorithm(Info) = pcsUnsupported); // καμπύλη εκτός προφίλ
end;
Η κωδικοποίηση της καμπύλης παίρνει την ίδια αυστηρότητα. Το RFC 5480 §2.1.1 αφήνει το ECParameters να είναι OID ονομαστής καμπύλης, έμμεση καμπύλη (NULL) ή πλήρες ρητό σύνολο parameters, και τα προφίλ PKIX απαιτούν την ονομαστή μορφή. Το PDFium Component επιστρέφει pcsInvalid όταν ένα πιστοποιητικό id-ecPublicKey κουβαλά κανένα parameters, έμμεσα parameters ή ρητά parameters, επειδή τα ρητά parameters αφήνουν έναν επιτιθέμενο να περιγράψει μια καμπύλη που μονάχα μοιάζει με κάποια πρότυπη. Μια σωστά ονομασμένη καμπύλη που απλώς λείπει από τη λίστα του ISO/TS 32002, όπως το brainpoolP160r1 πιο πάνω ή το secp256k1, παίρνει pcsUnsupported αντί αυτού
Άκυρο, μη υποστηριζόμενο ή αόριστο: διαβάζοντας την κατάσταση τίμια
Οι τρεις μη έγκυρες καταστάσεις του AlgorithmPolicyStatus σημαίνουν διαφορετικά πράγματα, και η κατάρρευσή τους σε έναν κουβά «απέτυχε» πετάει την πληροφορία που χρειάζονται οι auditors. Το pcsInvalid σημαίνει ότι ένας αναγνωρίσιμος συνδυασμός αλγορίθμων είναι κακοσχηματισμένος ή ασύμφωνος· προσθέτει ppeiSignatureAlgorithmMismatch στα TPadesValidationResult.Issues και σπρώχνει το συγκεντρωτικό IntegrityStatus σε pcsInvalid, οπότε το IsCryptographicallyValid επιστρέφει False ακόμα κι όταν η τιμή της υπογραφής CMS βγάζει σωστή. Το pcsUnsupported σημαίνει ότι η καμπύλη ή το digest είναι έξω από όσα ονομάζει το προφίλ, που είναι αποτέλεσμα ικανότητας, όχι απόδειξη παραποίησης. Το pcsIndeterminate σημαίνει ότι το πιστοποιητικό του υπογράφοντα δεν καρφώθηκε, συνήθως ένα CertificateSet με αρκετούς υποψηφίους και κανένα ακριβές ταίριασμα issuerAndSerialNumber, οπότε ο κώδικας αρνείται να μαντέψει την καμπύλη· από το v3.124.0 σημαδεύει επίσης υπογραφή RSA πάνω σε SHA-1 ή σε digest 112 bit όπως το SHA-224, που δεν συμφωνείται πλέον για τρέχουσα επαλήθευση. Ο ίδιος διαχωρισμός ισχύει για EdDSA σε μηχανή της οποίας το CryptoAPI δεν μπορεί να επαληθεύσει Ed25519 ή Ed448: το CmsSignatureStatus μένει pcsUnsupported ενώ το AlgorithmPolicyStatus μπορεί ακόμα να είναι pcsValid, επειδή η κωδικοποίηση ήταν σωστή και έλειπε μόνο ο verifier. Αν κυνηγάτε μια απόρριψη από Adobe ή από validator βασισμένο σε DSS, ο οδηγός για το γιατί απορρίπτουν οι validators υπογραφές PAdES καλύπτει τις άλλες συνηθισμένες αιτίες
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, χωρίς ανάκληση
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;
Τι δεν εγγυάται το pcsValid;
Το AlgorithmPolicyStatus = pcsValid πιστοποιεί μόνο ότι μια υπογραφή ECDSA ή EdDSA χρησιμοποιεί εγκεκριμένη καμπύλη με σύμφωνο, σωστά κωδικοποιημένο digest, και ότι μια υπογραφή RSA χρησιμοποιεί digest από τρέχουσα σουίτα· δεν λέει τίποτα για το αν η τιμή της υπογραφής είναι σωστή. Πριν το v3.124.0 ο κλάδος RSA του EvaluatePadesSignatureAlgorithm ήταν σκόπιμα φαρδύς: κάθε signatureAlgorithm κάτω από τον κλάδο PKCS #1 1.2.840.113549.1.1.* επέστρεφε pcsValid, με το παρωχημένο sha1WithRSAEncryption συμπεριλαμβανόμενο. Από το PDFiumPas v3.124.0 ο κλάδος RSA εφαρμόζει τις σουίτες υπογραφών ETSI TS 119 312. Digests MD2, MD4 και MD5 είναι pcsInvalid. Digests SHA-1 και 112 bit όπως το SHA-224 είναι pcsIndeterminate, οπότε μια υπογραφή SHA-1 κρατά το συνολικό αποτέλεσμα ακεραιότητάς της και σημαδεύεται για έλεγχο αντί να απορρίπτεται. Ένα digestAlgorithm που διαφέρει από το digest που καθορίζει ο αλγόριθμος υπογραφής, όπως sha256WithRSAEncryption πάνω σε digest SHA-1, ή αλγόριθμος υπογραφής RSA πάνω σε κλειδί υπογράφοντος μη RSA, είναι pcsInvalid, αναφέρεται ως ppeiSignatureAlgorithmMismatch και αποτυγχάνει την ακεραιότητα. Πιστοποιητικό υπογράφοντα που δεν βρίσκεται δίνει pcsIndeterminate, όπως έκανε ήδη για ECDSA, και μη αναγνωρίσιμα digests ή OIDs RSA που δεν είναι υπογραφές δίνουν pcsUnsupported. Το μήκος του modulus εξακολουθεί να μην ελέγχεται, τα PSS parameters δεν επικυρώνονται εδώ (το άρθρο για τα RSASSA-PSS-params του RFC 4055 καλύπτει πώς κωδικοποιούνται στην πλευρά υπογραφής), και digests SHA-1 ή MD5 σηκώνουν επιπλέον το ξεχωριστό ζήτημα ppeiBadDigestAlgorithm. Ομοίως, τα μαθηματικά της υπογραφής, η αλυσίδα πιστοποιητικών και η ανάκληση παραμένουν δουλειά των CmsSignatureStatus, CertificateTrustStatus και RevocationStatus, που έρχονται από το Windows CryptoAPI. Μεταχειριστείτε το pcsValid ως «το προφίλ αλγορίθμων στέκει», ποτέ ως «αυτό το κλειδί είναι αρκετά ισχυρό»
Για μια εφαρμογή Delphi που δέχεται υπογεγραμμένα PDF τιμολόγια, συμβόλαια ή πακέτα αρχειοθέτησης, η πρακτική διαρύθμιση είναι σύντομη: τρέξτε ValidatePadesTrust, απορρίψτε σε ppeiSignatureAlgorithmMismatch, δρομολογήστε pcsUnsupported και pcsIndeterminate σε άνθρωπο, και επιβάλετε το δικό σας κατώφλι μεγέθους κλειδιού RSA επειδή η πολιτική δεν θα το κάνει. Το PDFium Component for Delphi and Lazarus έρχεται με τον validator PAdES, τον builder αναφοράς αποδείξεων και το pipeline υπογραφής, ώστε η ίδια βιβλιοθήκη να παράγει αυτές τις υπογραφές και να τις ελέγχει από άκρη σε άκρη