Το HotPDF επαληθεύει τις ψηφιακές υπογραφές σε φορτωμένα έγγραφα PDF μέσω τριών μεθόδων της THotPDF: των GetLoadedSignatureInfo, VerifyLoadedSignature και VerifyLoadedSignatureEx, οι οποίες εισήχθησαν στην έκδοση v2.259.0. Το συστατικό επαναϋπολογίζει τη σύνοψη (hash) των τμημάτων /ByteRange του αρχικού αρχείου, ελέγχει το χαρακτηριστικό messageDigest του CMS, και εκτελεί μια επαλήθευση RSA PKCS#1 v1.5 έναντι του ενσωματωμένου πιστοποιητικού του υπογράφοντος, επιστρέφοντας svValid όταν τα bytes του εγγράφου είναι ανέπαφα
Το σενάριο είναι πεζό αλλά το διακύβευμα δεν είναι. Ένας αντισυμβαλλόμενος επιστρέφει ένα υπογεγραμμένο συμβόλαιο, η ροή εργασίας σας πρέπει να το αρχειοθετήσει, και κάποιος κάνει τη μόνη ερώτηση που έχει σημασία: είναι αυτό το έγγραφο που στείλαμε, byte προς byte, υπογεγραμμένο από το πιστοποιητικό που ισχυρίζεται; Η απάντηση σε αυτό μέσω κώδικα είναι η πλευρά της επαλήθευσης στην ιστορία των υπογραφών· η πλευρά της υπογραφής, δηλαδή η δημιουργία και η ενσωμάτωση υπογραφών PAdES εξαρχής, καλύπτεται στο συνοδευτικό άρθρο σχετικά με τη δημιουργία ψηφιακών υπογραφών PAdES με το HotPDF. Αυτό το άρθρο αφορά την άλλη κατεύθυνση: ένα PDF φτάνει ήδη υπογεγραμμένο, και θέλετε μια προγραμματιστική ετυμηγορία αντί για ένα στιγμιότυπο οθόνης του πράσινου σημαδιού ελέγχου του Acrobat
Πώς ένα υπογεγραμμένο PDF αποδεικνύει ότι δεν έχει υποστεί παραποίηση;
Μια υπογραφή PDF προστατεύει συγκεκριμένα εύρη byte (byte ranges) του αρχείου, όχι μια αφηρημένη έννοια του "εγγράφου". Το ISO 32000-1 §12.8 ορίζει τον μηχανισμό: το πεδίο φόρμας υπογραφής φέρει ένα λεξικό του οποίου η καταχώριση /Contents περιέχει ένα κοντέινερ CMS SignedData (RFC 5652), και του οποίου ο πίνακας /ByteRange ονομάζει τις ακριβείς περιοχές του αρχείου που καλύπτει η υπογραφή, σύμφωνα με την §12.8.1. Ο πίνακας είναι μια λίστα ζευγών μετατόπισης και μήκους, στην πράξη δύο τμήματα: ό,τι βρίσκεται πριν από τη δεκαεξαδική συμβολοσειρά /Contents και ό,τι βρίσκεται μετά από αυτήν. Η τιμή της υπογραφής δεν μπορεί να καλύψει τον εαυτό της, οπότε το αρχείο κατακερματίζεται (hashed) γύρω από αυτήν την τρύπα
Αυτός ο σχεδιασμός έχει μια συνέπεια που διαμορφώνει ολόκληρο το API: η επαλήθευση πρέπει να κατακερματίσει τα αρχικά σειριοποιημένα bytes, ακριβώς όπως βρίσκονται στο δίσκο. Ένα αναλυμένο μοντέλο αντικειμένων είναι άχρηστο για αυτό, επειδή η εκ νέου σειριοποίηση ακόμη και ενός αμετάβλητου εγγράφου παράγει διαφορετικά bytes. Επομένως, το HotPDF επαληθεύει έναντι του αρχείου προέλευσης από το οποίο φορτώθηκε το έγγραφο, ή έναντι ενός TStream ακατέργαστων bytes που παρέχετε εσείς, ποτέ έναντι της αναπαράστασής του στη μνήμη
Ανάγνωση μεταδεδομένων υπογραφής πριν από οποιαδήποτε επαλήθευση
Η GetLoadedSignatureInfo αναλύει το λεξικό υπογραφής και το κοντέινερ CMS χωρίς να αγγίξει ούτε ένα byte του εγγράφου, γεγονός που την καθιστά τη σωστή πρώτη κλήση όταν χρειάζεται απλώς να εμφανίσετε ποιος υπέγραψε και πότε. Τα πεδία υπογραφής έχουν δείκτες από το 0 με τη σειρά των πεδίων φόρμας, και η GetLoadedSignatureFieldCount σας λέει πόσα υπάρχουν. Η επιστρεφόμενη εγγραφή THPDFSignatureInfo μεταφέρει το όνομα του πεδίου, το /SubFilter, το κοινό όνομα (common name) του πιστοποιητικού του υπογράφοντος, τα διακεκριμένα ονόματα (distinguished names) του θέματος και του εκδότη, τον σειριακό αριθμό, τις ημερομηνίες ισχύος, την ώρα υπογραφής (από το υπογεγραμμένο χαρακτηριστικό όταν είναι παρόν, αλλιώς την καταχώριση /M του λεξικού), το όνομα του αλγορίθμου σύνοψης, και τις συμβολοσειρές /Reason, /Location και /ContactInfo. Το μέλος Status παραμένει svNotVerified, μια ειλικρινής ετικέτα για το "αναλύθηκε, δεν επαληθεύτηκε"
var
Pdf: THotPDF;
Info: THPDFSignatureInfo;
I: Integer;
begin
Pdf := THotPDF.Create(nil);
try
Pdf.LoadFromFile('signed-contract.pdf');
for I := 0 to Pdf.GetLoadedSignatureFieldCount - 1 do
begin
Info := Pdf.GetLoadedSignatureInfo(I);
Writeln('Field: ', Info.FieldName);
Writeln('Signer: ', Info.SignerName);
Writeln('Issuer: ', Info.IssuerDN);
Writeln('Algorithm: ', Info.HashAlgorithm);
Writeln('SubFilter: ', Info.SubFilter);
end;
finally
Pdf.Free;
end;
end;
Εκτέλεση του κρυπτογραφικού ελέγχου
Η VerifyLoadedSignatureEx εκτελεί την πλήρη επαλήθευση για ένα έγγραφο που έχει φορτωθεί από αρχείο και επιστρέφει τη συμπληρωμένη εγγραφή πληροφοριών σε μία κλήση: ανοίγει ξανά το αρχείο προέλευσης, κατακερματίζει τα τμήματα /ByteRange με τον αλγόριθμο σύνοψης του SignerInfo, συγκρίνει το αποτέλεσμα με το υπογεγραμμένο χαρακτηριστικό messageDigest (RFC 5652 §5.4), και στη συνέχεια επαληθεύει με RSA την υπογραφή πάνω στην επανακωδικοποίηση DER SET των υπογεγραμμένων χαρακτηριστικών. Όταν μια υπογραφή δεν φέρει υπογεγραμμένα χαρακτηριστικά, ο έλεγχος RSA εκτελείται απευθείας πάνω στη σύνοψη του εγγράφου. Οι υποστηριζόμενες υπογραφές είναι RSA PKCS#1 v1.5 με συνόψεις SHA-1, SHA-256, SHA-384, ή SHA-512, γεγονός που καλύπτει τα subfilters adbe.pkcs7.detached και ETSI.CAdES.detached που παράγονται από τα κύρια εργαλεία υπογραφής
var
Status: THPDFSignatureVerifyStatus;
Info: THPDFSignatureInfo;
begin
Status := Pdf.VerifyLoadedSignatureEx(0, Info);
case Status of
svValid:
if Info.CoversWholeDocument then
Writeln('Valid; signature covers the whole file')
else
Writeln('Valid; file was extended after signing');
svDigestMismatch:
Writeln('Document bytes changed after signing');
svSignatureInvalid:
Writeln('RSA check failed over signed attributes');
svUnsupportedAlgorithm:
Writeln('Non-RSA key or unknown digest algorithm');
svMalformed:
Writeln('CMS container could not be parsed');
svSourceUnavailable:
Writeln('No source bytes; use the TStream overload');
end;
end;
Μια λεπτομέρεια που αξίζει να γνωρίζετε επειδή εξηγεί αποτυχίες που φαίνονται μυστηριώδεις από έξω. Πρώτον, ο έλεγχος των υπογεγραμμένων χαρακτηριστικών (signed attributes) είναι ιδιότροπος με την κωδικοποίηση: μέσα στο αρχείο τα χαρακτηριστικά φέρουν την ετικέτα [0] IMPLICIT, αλλά η υπογραφή υπολογίστηκε πάνω στη μορφή DER SET OF, οπότε ο επαληθευτής αλλάζει την ετικέτα πριν από τον κατακερματισμό, ακριβώς όπως απαιτεί το RFC 5652 §5.4. Ένας πρόχειρος επαληθευτής που κατακερματίζει τα bytes όπως εμφανίζονται στο αρχείο θα απορρίψει κάθε σωστά υπογεγραμμένο έγγραφο. Δεύτερον, η καταχώριση /Contents είναι συμβατικά γεμισμένη με μηδενικά (zero-padded) σε έναν δεσμευμένο προϋπολογισμό byte, οπότε ο επαληθευτής περικόπτει το blob DER στο πραγματικό μήκος της εξωτερικής του SEQUENCE πριν από την ανάλυση· τα μηδενικά στο τέλος που μοιάζουν με σκουπίδια είναι φυσιολογικά και δεν αποτελούν αλλοίωση. Η ίδια οικογένεια κινδύνων ανάλυσης ASN.1, από την πλευρά της εισαγωγής πιστοποιητικού, είναι το θέμα του άρθρου σχετικά με τη θωράκιση ασφαλείας PKCS#12 και ASN.1 στο HotPDF
Τι εγγυάται πραγματικά μια έγκυρη υπογραφή;
Το svValid σημαίνει ακριβώς αυτό: τα bytes που ορίζονται από το /ByteRange κατακερματίζονται στην τιμή που υπέγραψε ο υπογράφων, και η υπογραφή επαληθεύεται με το δημόσιο κλειδί του πιστοποιητικού που είναι ενσωματωμένο στο κοντέινερ CMS. Αυτό σημαίνει ακεραιότητα byte συν σύνδεση κλειδιού, και τίποτα περισσότερο. Η επικύρωση της αλυσίδας πιστοποιητικών και της εμπιστοσύνης είναι ρητά εκτός σκοπού για τον επαληθευτή του HotPDF: δεν διατρέχει την αλυσίδα σε μια ρίζα (root), δεν ελέγχει την ανάκληση, ούτε συμβουλεύεται κάποιο αποθετήριο εμπιστοσύνης. Ένα αυτο-υπογεγραμμένο πιστοποιητικό από έναν εισβολέα που υπέγραψε εκ νέου ένα τροποποιημένο έγγραφο θα επαληθευτεί ως svValid, επειδή τα μαθηματικά είναι εσωτερικά συνεπή. Το αν ο υπογράφων είναι αυτός που ισχυρίζεται, και το αν πρέπει να τον εμπιστευτεί κανείς, είναι μια απόφαση πολιτικής που ανήκει σε διαφορετικό επίπεδο, είτε πρόκειται για τη λευκή λίστα πιστοποιητικών του οργανισμού σας, είτε για το αποθετήριο πιστοποιητικών των Windows, είτε για μια αρχή επικύρωσης
Η σημασία της σημαίας CoversWholeDocument προστατεύει από ένα πιο λεπτό κενό. Μια υπογραφή καλύπτει πάντα μόνο το δικό της /ByteRange, και ο μηχανισμός σταδιακής ενημέρωσης του PDF επιτρέπει την προσάρτηση περιεχομένου μετά από μια υπογραφή χωρίς να την ακυρώσει, κάτι που γίνεται από σχεδιασμό και αποτελεί τον τρόπο λειτουργίας των ροών εργασίας πολλαπλών υπογραφών. Η σημαία υπολογίζεται κατά την επαλήθευση και είναι αληθής μόνο όταν τα δύο τμήματα συν το κενό του /Contents καλύπτουν ολόκληρο το αρχείο. Όταν το svValid φτάνει με τη σημαία CoversWholeDocument ψευδή, η υπογεγραμμένη αναθεώρηση είναι ανέπαφη αλλά το αρχείο περιέχει μεταγενέστερες προσθήκες, και το τι άλλαξαν αυτές οι προσθήκες είναι κάτι που η ροή εργασίας σας πρέπει να αποφασίσει αν θα ανεχτεί
Τα έγγραφα που φορτώνονται από ροή και τα κρυπτογραφημένα έγγραφα χρειάζονται τα δικά τους bytes προέλευσης
Οι VerifyLoadedSignature και VerifyLoadedSignatureEx χωρίς παραμέτρους εξαρτώνται από το ότι το στοιχείο θυμάται από ποιο αρχείο προήλθε το έγγραφο. Αν φορτώσετε το έγγραφο από μια ροή, δεν υπάρχει όνομα αρχείου για να ξανανοίξει· το ίδιο ισχύει μετά τη διαδρομή επαναφόρτωσης κωδικού πρόσβασης που χρησιμοποιείται για κρυπτογραφημένα έγγραφα, τη ροή εργασίας που περιγράφεται στο άρθρο σχετικά με την κρυπτογράφηση PDF με AES-256 στο HotPDF. Και στις δύο περιπτώσεις, οι υπερφορτώσεις που υποστηρίζονται από αρχεία επιστρέφουν svSourceUnavailable αντί να μαντέψουν. Η λύση είναι η υπερφόρτωση TStream, η οποία σας επιτρέπει να παραδώσετε τα αρχικά ακατέργαστα bytes από όπου τα φυλάξατε, ένα αρχείο που έχετε ακόμα, έναν buffer μνήμης, ή ένα blob βάσης δεδομένων
var
Src: TFileStream;
Status: THPDFSignatureVerifyStatus;
Info: THPDFSignatureInfo;
begin
// Stream-loaded document: the component holds no source
// file name, so supply the original bytes yourself.
Src := TFileStream.Create('signed-contract.pdf',
fmOpenRead or fmShareDenyWrite);
try
Status := Pdf.VerifyLoadedSignature(0, Src, Info);
if Status <> svValid then
Writeln('Verification failed: ', Ord(Status));
finally
Src.Free;
end;
end;
Αναφορά όσων δεν μπορείτε να επαληθεύσετε
Ένας επαληθευτής που γνωρίζει μόνο το "έκυρο" και το "άκυρο" θα αναφέρει εσφαλμένα έγγραφα που απλώς δεν κατανοεί, οπότε η απαρίθμηση κατάστασης διαχωρίζει τις περιπτώσεις που πρέπει να διακρίνει η διεπαφή χρήστη σας. Το svDigestMismatch σημαίνει ότι τα bytes του εγγράφου άλλαξαν μετά την υπογραφή, το κλασικό σήμα παραποίησης. Το svSignatureInvalid σημαίνει ότι τα bytes κατακερματίζονται σωστά αλλά ο έλεγχος RSA απέτυχε, γεγονός που δείχνει μια κατεστραμμένη ή πλαστογραφημένη τιμή υπογραφής. Το svUnsupportedAlgorithm είναι η ειλικρινής απάντηση για κλειδιά ECDSA και μη αναγνωρισμένες συνόψεις: η υπογραφή μπορεί να είναι απολύτως καλή, το HotPDF απλά δεν μπορεί να την ελέγξει, και η αναφορά της ως "άκυρης" θα δυσφημούσε ένα υγιές έγγραφο. Το svMalformed επισημαίνει ένα κοντέινερ CMS που δεν μπόρεσε να αναλυθεί καθόλου. Για ελέγχους τύπου πύλης (gate-style checks), η VerifyAllLoadedSignatures επιστρέφει true μόνο όταν υπάρχει τουλάχιστον ένα πεδίο υπογραφής και καθένα από αυτά επαληθεύεται ως svValid, μια βολική ενιαία τιμή boolean για μια ροή εργασίας εισαγωγής αρχείων που απορρίπτει οτιδήποτε λιγότερο
Η επαλήθευση υπογραφής, η υπογραφή PAdES, η κρυπτογράφηση AES-256 και το API επεξεργασίας φορτωμένου εγγράφου αποστέλλονται στην ίδια εγγενή βιβλιοθήκη VCL για Delphi και C++Builder, χωρίς εξωτερικές εξαρτήσεις DLL· η πλήρης λίστα δυνατοτήτων και οι υποστηριζόμενες εκδόσεις IDE βρίσκονται στη σελίδα προϊόντος του HotPDF Component