Ένα signatureValue ECDSA μέσα σε ένα container CMS είναι ένα DER SEQUENCE { INTEGER r, INTEGER s }. Η συνάρτηση CNG των Windows BCryptVerifySignature δεν δέχεται κανένα από τα δύο: θέλει IEEE P1363 σταθερού πλάτους r || s, χωρίς tags και χωρίς μήκη. Το HotPDF, το native VCL PDF component για Delphi και C++Builder, μετατρέπει μεταξύ των δύο υπό αυστηρούς κανόνες DER πριν εισάγει ένα κλειδί
Η αποτυχία που αυτό αποτρέπει είναι μια συγκεκριμένη και απογοητευτική. Το Acrobat ανοίγει το έγγραφο και δείχνει πράσινο τσεκ. Ο δικός σας verifier, διατρέχοντας τα ίδια bytes, επιστρέφει άκυρο, ή το CNG επιστρέφει STATUS_INVALID_SIGNATURE χωρίς περαιτέρω εξήγηση. Τίποτα δεν είναι λάθος με την υπογραφή. Αυτό που είναι λάθος είναι ότι περίπου εβδομήντα bytes ASN.1 δόθηκαν σε ένα API που περίμενε εξήντα τέσσερα bytes ακατέργαστου integer, και η αναντιστοιχία είναι αόρατη εκτός αν κάποιος ξέρει να την αναζητήσει
Γιατί η BCryptVerifySignature απορρίπτει μια έγκυρη υπογραφή ECDSA;
Επειδή οι δύο πλευρές της κλήσης μιλούν διαφορετικές κωδικοποιήσεις υπογραφής, και καμία από τις δύο δεν το ανακοινώνει. Το ISO 32000-1 §12.8 λέει ότι ένα signature dictionary φέρει ένα blob CMS στο /Contents· το RFC 5652 §5.3 λέει ότι το signatureValue σε κάθε SignerInfo είναι ένα OCTET STRING του οποίου το περιεχόμενο είναι ό,τι ορίζει ο αλγόριθμος υπογραφής. Για το ECDSA εκείνο το περιεχόμενο είναι η δομή DER SEC 1: ένα SEQUENCE που κρατά δύο INTEGERs. Είναι μεταβλητού μήκους εκ σχεδιασμού, επειδή τα r και s είναι ακέραιοι και το DER αφαιρεί τα αρχικά μηδενικά octets από τους ακεραίους
Το IEEE P1363 έχει την αντίθετη άποψη. Ορίζει την υπογραφή ως τη συνένωση των δύο συντεταγμένων, καθεμία γεμισμένη αριστερά με μηδενικά στο ακριβές πλάτος σε bytes του πεδίου της καμπύλης. Μια υπογραφή P-256 είναι πάντα 64 bytes. Μια κωδικοποίηση DER της ίδιας υπογραφής είναι κανονικά 70 ή 71 bytes και μπορεί να είναι οπουδήποτε από περίπου 8 έως 72. Αν δοθεί η μορφή DER στην BCryptVerifySignature, μόνο ο έλεγχος μήκους καταδικάζει την κλήση, κάτι που είναι ο λόγος για τον οποίο το HotPDF κανονικοποιεί πριν επαληθεύσει και όχι μετά
uses
HPDFECDSA;
// Converts a CMS signatureValue into the fixed-width form CNG expects.
// ARaw comes back as 64 bytes for P-256, 96 for P-384, 132 for P-521.
function ToP1363(const ADerSig: TBytes; ACurve: THPDFECDSACurve;
out ARaw: TBytes): Boolean;
begin
Result := HPDFECDSANormalizeSignature(ADerSig, ACurve, eseDER, ARaw);
end;
Οι κανόνες DER που ένας parser υπογραφής δεν πρέπει να χαλαρώνει
Κάθε απόρριψη που απαριθμείται εδώ είναι μια απόρριψη που το HotPDF εκτελεί σκόπιμα, και η καθεμία κλείνει μια διαδρομή που ένας επιεικής parser θα άφηνε ανοιχτή. Ο πειρασμός όταν γράφεται ένας converter είναι να βρεθούν οι δύο κόμβοι INTEGER, να αντιγραφεί το περιεχόμενό τους, και να προχωρήσει κανείς. Αυτό λειτουργεί σε καλά διαμορφωμένη είσοδο και αποδέχεται σιωπηλά μια οικογένεια εύπλαστων επανακωδικοποιήσεων σε εχθρική είσοδο. Έτσι η HPDFECDSANormalizeSignature απορρίπτει έναν αρνητικό ακέραιο, δηλαδή οποιοδήποτε r ή s του οποίου το πρώτο octet περιεχομένου έχει το high bit ενεργό, επειδή ένα έγκυρο βαθμωτό ECDSA είναι θετικό. Απορρίπτει μια τιμή που είναι εντελώς μηδέν, αφού r = 0 ή s = 0 δεν είναι ποτέ νόμιμη υπογραφή. Απορρίπτει ένα περιττό αρχικό μηδενικό octet: το X.690 §8.3 επιτρέπει ακριβώς ένα, και μόνο όταν το επόμενο octet θα διαβαζόταν διαφορετικά ως αρνητικό, οπότε ένα 00 ακολουθούμενο από ένα octet κάτω από 0x80 είναι επανακωδικοποίηση, όχι υπογραφή. Απορρίπτει μια μη ελάχιστη κεφαλίδα μήκους, επειδή το X.690 §10.1 απαιτεί την οριστική μορφή κωδικοποιημένη στα λιγότερα δυνατά octets, και ένα μήκος long-form που θα μπορούσε να ήταν short-form είναι διαφορετική ακολουθία bytes που μεταφέρει το ίδιο νόημα. Απορρίπτει έναν ακέραιο πλατύτερο από το μέγεθος συντεταγμένης της καμπύλης, αφού εκείνη η τιμή δεν μπορεί να είναι στοιχείο πεδίου. Και απορρίπτει οποιονδήποτε κόμβο μετά το s, μαζί με ένα εξωτερικό SEQUENCE του οποίου το συνολικό μήκος δεν ισούται με το μήκος ολόκληρου του blob
Αυτά τα δύο τελευταία έχουν μεγαλύτερη σημασία από όσο φαίνεται. Τα bytes μετά το SEQUENCE είναι το κλασικό κόλπο ευπλαστότητας υπογραφής: η προσάρτηση σκουπιδιών αφήνει έναν επιεική verifier να λέει ακόμα έγκυρο ενώ η ακολουθία bytes που επικύρωσε δεν είναι η ακολουθία που υπογράφηκε. Το ίδιο ένστικτο οδηγεί την ενίσχυση μήκους ASN.1 που περιγράφεται στη σημείωση για την ανάλυση PKCS#12, και είναι το ίδιο ένστικτο εδώ. Σε μια διαδρομή επαλήθευσης, μια αποδεκτή δομή που δεν εκπέμφθηκε ποτέ από έναν συμμορφούμενο signer είναι ελάττωμα, όχι ευγένεια
Το πλάτος συντεταγμένης ανήκει στην καμπύλη, όχι στην υπογραφή
Το HotPDF παράγει το πλάτος εξόδου από το OID της επώνυμης καμπύλης, ποτέ από το μήκος του DER που μόλις αναλύθηκε. Αυτό είναι το δεύτερο μισό της μετατροπής και το μισό που είναι εύκολο να γίνει λάθος διακριτικά. Το RFC 5480 §2.1.1 ταυτοποιεί την καμπύλη στις παραμέτρους SubjectPublicKeyInfo του πιστοποιητικού, και η HPDFECDSACurveFromOID αντιστοιχίζει τα τρία OID που υποστηρίζει το HotPDF: 1.2.840.10045.3.1.7 για P-256, 1.3.132.0.34 για P-384, και 1.3.132.0.35 για P-521. Η HPDFECDSACoordinateSize επιστρέφει τότε 32, 48, ή 66 bytes, και το buffer P1363 είναι το διπλάσιο αυτού: 64, 96, ή 132. Κάθε αποκωδικοποιημένος ακέραιος στοιχίζεται δεξιά μέσα στο μισό του, οπότε ένα κοντό r γεμίζεται με μηδενικά αριστερά αντί να μετατοπιστεί. Το P-521 είναι αυτό που παγιδεύει τον κόσμο, επειδή τα 521 bits είναι 65,125 bytes και στρογγυλοποιούνται προς τα πάνω σε 66, δίνοντας μια υπογραφή 132 bytes που καμία διαίσθηση δύναμης-του-δύο δεν θα είχε προβλέψει. Το δημόσιο κλειδί ταξιδεύει παράλληλα ως ασυμπίεστο σημείο EC σύμφωνα με το RFC 5480 §2.2, που είναι 0x04 ακολουθούμενο από X και Y, οπότε το HotPDF ελέγχει ότι είναι ακριβώς 1 + 2 * CoordinateSize bytes και ξεκινά με 0x04 πριν αγγίξει το CNG
var
Digest, SigDER, PublicPoint: TBytes;
Curve: THPDFECDSACurve;
Res: THPDFECDSAVerifyResult;
begin
// secp256r1, taken from the certificate SubjectPublicKeyInfo parameters
Curve := HPDFECDSACurveFromOID('1.2.840.10045.3.1.7');
// PublicPoint must be $04 || X || Y, so 1 + 2 * 32 = 65 bytes for P-256
Res := HPDFECDSAVerifyDigest(Digest, SigDER, PublicPoint, Curve, eseDER);
case Res of
evrValid:
Memo1.Lines.Add('signature verifies');
evrInvalid:
Memo1.Lines.Add('signature does not match the digest');
evrMalformed:
Memo1.Lines.Add('DER encoding or public point rejected');
evrUnsupported:
Memo1.Lines.Add('curve or algorithm not supported here');
evrProviderUnavailable:
Memo1.Lines.Add('bcrypt.dll or the curve provider is missing');
evrProviderError:
Memo1.Lines.Add('CNG returned an unexpected status');
end;
end;
Παρατηρήστε την τελευταία παράμετρο. Η HPDFECDSAVerifyDigest δέχεται επίσης eseP1363 για καλούντες που ήδη κρατούν μια υπογραφή σταθερού πλάτους, από ένα hardware token ή μια απομακρυσμένη υπηρεσία υπογραφής που επιστρέφει ακατέργαστο r || s. Εκείνη η διαδρομή εξακολουθεί να επιβάλλει τον έλεγχο μήκους και τον έλεγχο μη-μηδενικού και στα δύο μισά, οπότε ένα buffer του σωστού μεγέθους γεμάτο μηδενικά απορρίπτεται αντί να περάσει στον provider
Γιατί το γενικό όνομα αλγορίθμου ECDSA αποτυγχάνει σε παλαιότερα Windows;
Επειδή το γενικό όνομα είναι νεότερο από τη βάση ανάπτυξης στην οποία στοχεύετε. Το CNG εκθέτει ένα αναγνωριστικό αλγορίθμου ECDSA που συμπεραίνει την καμπύλη από το εισαγόμενο κλειδί, και είναι ο καθαρός τρόπος να γραφεί αυτός ο κώδικας, αλλά η BCryptOpenAlgorithmProvider είναι εγγυημένο ότι το επιλύει μόνο σε νεότερες εκδόσεις Windows. Σε ένα παλαιότερο μηχάνημα η κλήση ανοίγματος αποτυγχάνει, η λαβή provider παραμένει nil, και κάθε επαλήθευση ECDSA στην εφαρμογή σας αναφέρει μη υποστηριζόμενο πάνω σε μια υπογραφή που είναι απολύτως καλή. Το HotPDF αποφεύγει τον γκρεμό ανοίγοντας αντί αυτού τα αναγνωριστικά ανά καμπύλη. Επιλύει τα ECDSA_P256, ECDSA_P384, και ECDSA_P521 μία φορά, αποθηκεύει σε cache μία λαβή provider ανά καμπύλη, και τις κλείνει στο unit finalization. Κάθε επαλήθευση τότε κάνει μόνο τη φθηνή δουλειά: εισάγει ένα προσωρινό δημόσιο κλειδί από ένα ECCPUBLICBLOB, καλεί την BCryptVerifySignature, καταστρέφει το κλειδί. Καμία επαναλαμβανόμενη LoadLibrary, καμία επαναλαμβανόμενη GetProcAddress, κανένα άνοιγμα και κλείσιμο provider ανά υπογραφή. Η μαζική επαλήθευση μερικών εκατοντάδων εγγράφων νιώθει τη διαφορά, όπως και μια διεργασία υπηρεσίας που διαφορετικά θα κατανάλωνε λαβές provider υπό φόρτο
Οι κωδικοί αποτελέσματος παραμένουν ειλικρινείς για τη διάκριση. Το evrProviderUnavailable σημαίνει ότι το μηχάνημα δεν μπόρεσε να δώσει στο HotPDF έναν provider· το evrInvalid σημαίνει ότι το CNG απάντησε STATUS_INVALID_SIGNATURE. Η συμπίεση αυτών των δύο σε μία αποτυχία είναι το πώς ένα πρόβλημα ανάπτυξης αναφέρεται λανθασμένα ως πλαστογραφημένο έγγραφο. Ο ίδιος διαχωρισμός μεταξύ αποτυχίας περιβάλλοντος και κρυπτογραφικής αποτυχίας διατρέχει τον χειρισμό CNG και CAPI στην πλευρά της υπογραφής, που καλύπτεται στο άρθρο για την υπογραφή με cert store και τη σειρά bytes
Ποιο πιστοποιητικό το υπέγραψε; Το SignerIdentifier είναι δύο διαφορετικά πράγματα
Το RFC 5652 §5.3 κάνει το SignerIdentifier ένα CHOICE, και ένας verifier που χειρίζεται μόνο τον έναν κλάδο θα επαληθεύσει σιωπηλά έναντι λάθος κλειδιού. Ο πρώτος κλάδος είναι το issuerAndSerialNumber, ένα SEQUENCE που κρατά το Name του εκδότη σε ακατέργαστο DER και τον σειριακό INTEGER, και η αντιστοίχισή του είναι μια σύγκριση bytes έναντι κάθε πιστοποιητικού στο σύνολο certificates του CMS. Ο δεύτερος κλάδος είναι το [0] subjectKeyIdentifier, ένα OCTET STRING με έμμεση ετικέτα, και η αντιστοίχισή του απαιτεί σκάψιμο μέσα στο πιστοποιητικό αντί για σύγκριση των πεδίων κεφαλίδας του
Το σκάψιμο έχει ένα επίπεδο που εκπλήσσει τον κόσμο. Το αναγνωριστικό κλειδιού ζει σε μια επέκταση X.509v3, οπότε το HotPDF διατρέχει το πεδίο επεκτάσεων [3] του tbsCertificate, βρίσκει την επέκταση της οποίας το OID είναι 2.5.29.14, παραλείπει το προαιρετικό BOOLEAN critical, και παίρνει το OCTET STRING extnValue. Εκείνη η ακολουθία octet δεν είναι το αναγνωριστικό. Σύμφωνα με το RFC 5280 §4.2.1.2 το περιεχόμενό της είναι το ίδιο DER, και ο τύπος KeyIdentifier είναι ένα ακόμα OCTET STRING, οπότε γίνεται ανάλυση δεύτερη φορά για να φτάσει κανείς στα πραγματικά bytes. Αν σταματήσει κανείς ένα επίπεδο νωρίτερα συγκρίνει έναν wrapper 22 bytes έναντι ενός αναγνωριστικού 20 bytes, κανένα πιστοποιητικό δεν ταιριάζει ποτέ, και ο verifier καταφεύγει σε όποια ευρετική μέθοδο γράφτηκε στη συνέχεια, που είναι ο πραγματικός κίνδυνος. Το να πάρει κανείς το πρώτο πιστοποιητικό στο σύνολο είναι μια δελεαστική συντόμευση και είναι λάθος όποτε το CMS φέρει μια αλυσίδα, που είναι οι περισσότερες φορές, επειδή δεν απαιτείται το leaf να έρθει πρώτο. Το HotPDF δέχεται ένα ασύμφωνο πιστοποιητικό μόνο όταν το container κρατά ακριβώς ένα· με πολλαπλά πιστοποιητικά παρόντα, μια ακριβής αντιστοίχιση SignerIdentifier είναι υποχρεωτική. Η επαλήθευση ενός digest έναντι δημόσιου κλειδιού ενδιάμεσης CA δεν παράγει ένα φιλικό σφάλμα, παράγει ένα σίγουρο άκυρο πάνω σε ένα έγγραφο που είναι εντάξει
var
Pdf: THotPDF;
Info: THPDFSignatureInfo;
I: Integer;
begin
Pdf := THotPDF.Create(nil);
try
if Pdf.LoadFromFile('signed.pdf') > 0 then
for I := 0 to Pdf.GetLoadedSignatureFieldCount - 1 do
if Pdf.VerifyLoadedSignatureEx(I, Info) = svValid then
Memo1.Lines.Add(Format('%s: %s %s over %s, signer %s',
[String(Info.FieldName), String(Info.PublicKeyAlgorithm),
String(Info.CurveName), String(Info.HashAlgorithm),
String(Info.SignerName)]))
else
Memo1.Lines.Add(Format('%s: not valid', [String(Info.FieldName)]));
finally
Pdf.Free;
end;
end;
Το THPDFSignatureInfo.CurveName αναφέρει P-256, P-384, ή P-521 ώστε ένα audit log να καταγράφει ποια καμπύλη χρησιμοποιήθηκε πράγματι αντί απλώς τη λέξη ECDSA. Η υδραυλική σε επίπεδο εγγράφου γύρω από αυτή την κλήση, ειδικότερα το πώς κατακερματίζονται τα τμήματα /ByteRange και γιατί το digest πρέπει να υπολογίζεται πάνω στο αρχείο αντί για το αναλυμένο δέντρο αντικειμένων, είναι το θέμα του συνοδευτικού άρθρου για την επαλήθευση υπογραφών PDF
Τι δεν σας δίνει αυτό
Ένα πράσινο αποτέλεσμα από την HPDFECDSAVerifyDigest απαντά σε ένα μόνο ερώτημα: αυτά τα bytes υπογράφηκαν από το ιδιωτικό κλειδί που ταιριάζει με αυτό το δημόσιο κλειδί. Δεν λέει τίποτα για το αν εκείνο το κλειδί ανήκει σε κάποιον που πρέπει να εμπιστεύεστε. Η κατασκευή αλυσίδας προς μια άγκυρα εμπιστοσύνης, η ανάκληση μέσω CRL ή OCSP, και οι έλεγχοι πολιτικής είναι ξεχωριστή δουλειά, και κάθε προϊόν που αναφέρει έγκυρη υπογραφή χωρίς αυτά αναφέρει λιγότερα από όσα υποθέτει ο χρήστης. Οι ημερομηνίες ισχύος πιστοποιητικού εμφανίζονται ξεχωριστά στο THPDFSignatureInfo ακριβώς για αυτόν τον λόγο: μια υπογραφή μπορεί να επαληθεύεται κρυπτογραφικά ενώ το πιστοποιητικό που την έφτιαξε έληξε πριν από δύο χρόνια. Η υποστήριξη καμπυλών είναι επίσης σκόπιμα στενή. Τρεις καμπύλες πρώτου NIST υποστηρίζονται, και μια υπογραφή πάνω σε οποιαδήποτε άλλη καμπύλη επιστρέφει μη υποστηριζόμενο αντί για εικασία. Η διαδρομή CNG είναι μόνο για Windows, που είναι το σωστό αντάλλαγμα για ένα VCL component αλλά αξίζει να δηλωθεί πριν σχεδιαστεί μια cross-platform υπηρεσία γύρω από αυτό. Και η αυστηρότητα δεν είναι διαμορφώσιμη: δεν υπάρχει επιεικής λειτουργία που δέχεται μη ελάχιστο μήκος DER επειδή κάποιος παλαιός signer το εξέπεμψε. Αν συναντηθεί ένα τέτοιο αρχείο σε παραγωγή, η ειλικρινής απάντηση είναι να καταγραφεί και να αναζητηθεί ο παραγωγός, όχι να διευρυνθεί ο parser μέχρι να περάσει το αρχείο
Η διαδρομή επαλήθευσης ECDSA που περιγράφεται εδώ διατίθεται ως μέρος του τυπικού HotPDF Component για Delphi και C++Builder, μαζί με τις διαδρομές RSA PKCS#1 v1.5 και RSA-PSS και την πλήρη εγγραφή πληροφοριών υπογραφής· η σελίδα προϊόντος φέρει την πλήρη αναφορά ψηφιακής υπογραφής