Το PDFium VCL στέλνει αιτήματα timestamp RFC 3161 μέσω libcurl σε στόχους εκτός Windows, δεμένο δυναμικά σε οκτώ symbols, κατοπτρίζοντας το σχήμα του Windows backend που δένεται σε WinHTTP. Δύο ρυθμίσεις options αποφασίζουν αν η μεταφορά είναι αξιόπιστη κάτω από φορτίο, και όλη η μονάδα επαληθεύτηκε σε μηχανή που δεν μπορούσε να τη μεταγλωττίσει για την πλατφόρμα στόχο της
Η χρονική σήμανση είναι όσο γυρνά μια υπογραφή σε κάτι που επιβιώνει από τη λήξη certificate, και είναι μια δικτυακή πράξη που κάθεται μέσα σε μια πράξη υπογραφής. Εκείνος ο συνδυασμός κάνει την επιλογή μεταφοράς επακόλουθη με τρόπο που συνήθως δεν είναι: τρέχει σε worker thread, μιλά σε server που δεν ελέγχετε, και ένα κόλλημα εκεί μπλοκάρει pipeline υπογραφής και όχι φόρτωση σελίδας
Γιατί libcurl αντί για τον HTTP client του FPC;
Γιατί η εναλλακτική σέρνει TLS stack μέσα στο repository και μετά σας κάνει να συντηρείτε την ανίχνευση έκδοσής του. Η προφανής διαδρομή σε Free Pascal είναι η fphttpclient με το socket στρώμα OpenSSL, και αποτυγχάνει στις λεπτομέρειες: τα OpenSSL bindings του FPC 3.2.2 ανιχνεύουν OpenSSL 3.x αναξιόπιστα στις περισσότερες τρέχουσες διανομές, και το macOS προσθέτει διαφορές LibreSSL από πάνω. Αυτό που ξεκινά ως μικρή κλήση HTTP γίνεται διαρκής συντήρηση του TLS ABI κάποιου άλλου
Η libcurl επιλύει μόνη της το TLS backend της και επικυρώνει αλυσίδες απέναντι στο trust store της πλατφόρμας, οπότε η πλευρά Pascal δεν χρειάζεται τίποτα από αυτά. Το στρώμα δέσμευσης είναι οκτώ symbols. Εκείνο το πλήθος είναι το επιχείρημα: μικρότερη επιφάνεια ανάμεσα στον κώδικά σας και σε μια κινούμενη εξάρτηση σημαίνει λιγότερα σημεία όπου μια αναβάθμιση διανομής σας σπάει, και ταιριάζει με το υπάρχον Windows backend, που δένει μια χουφτά σημεία εισόδου WinHTTP με τον ίδιο τρόπο
uses
FPdfTsaFpc;
var
ReqDer, RespDer: TBytes;
begin
if not TsaHttpAvailable then
raise Exception.Create('no HTTP transport for timestamping');
Writeln('TSA transport: ', TsaHttpBackendName);
ReqDer := BuildTimeStampQuery(DocumentDigest);
if PostTimeStampQuery('https://tsa.example.org/tsr', ReqDer, RespDer) then
AttachTimeStampToken(RespDer)
else
raise Exception.Create('timestamp request failed');
end;
Δήλωση variadic συνάρτησης C σε Pascal
Τα curl_easy_setopt και curl_easy_getinfo είναι variadic στην πλευρά C, και η Object Pascal δεν έχει τρόπο να το εκφράσει. Η προσέγγιση που δουλεύει είναι να δηλώσεις αρκετά σταθερά prototypes, ένα ανά κατηγορία ορίσματος, όλα δείχνοντας στο ίδιο εξαγόμενο symbol: μια παραλλαγή που παίρνει long, μια που παίρνει pointer, και ούτω καθ εξής, διαλεγμένες στο σημείο κλήσης από ό,τι περνάτε στην πραγματικότητα
Αυτό είναι ασφαλές για συγκεκριμένο λόγο που αξίζει να το καταλάβετε και όχι να το αντιγράψετε. Καθένας από εκείνους τους τύπους ορισμάτων περνά σε register ακεραίου κάτω από τις συμβάσεις κλήσης της πλατφόρμας που παίζουν, που είναι ακριβώς εκεί απ όπου το διαβάζει η va_arg της C υλοποίησης. Το κόλπο άρα ισχύει για ακεραίους, pointers και handles, και δεν ισχύει για ορίσματα κινητής υποδιαστολής, που ταξιδεύουν σε διαφορετικά registers. Μην προσθέσετε παραλλαγή που παίρνει double με την υπόθεση ότι το μοτίβο γενικεύει
// Ένα εξαγόμενο symbol, αρκετά σταθερά prototypes. Κάθε παραλλαγή περνά
// το όρισμά της σε register ακεραίου, που είναι εκεί όπου διαβάζει η πλευρά
// C. Παραλλαγή κινητής υποδιαστολής δεν θα δούλευε και δεν πρέπει να μπει
type
TCurlSetOptLong = function(Handle: Pointer; Option: Integer;
Value: NativeInt): Integer; cdecl;
TCurlSetOptPtr = function(Handle: Pointer; Option: Integer;
Value: Pointer): Integer; cdecl;
var
curl_easy_setopt_long: TCurlSetOptLong;
curl_easy_setopt_ptr: TCurlSetOptPtr;
Δύο ρυθμίσεις που αποφασίζουν αν το αίτημα ολοκληρώνεται
Η πρώτη είναι ρητή κενή κεφαλίδα Expect:. Η libcurl ανάβει το χειραψία HTTP 100-continue για σώματα αιτημάτων πάνω από περίπου ένα kilobyte, και ένα ερώτημα timestamp με αίτημα certificate συνήθως ξεπερνά εκείνο το κατώφλι. Μερικοί TSA servers δεν απαντούν ποτέ τη συνέχεια, οπότε ο client περιμένει ολόκληρο timeout προτού στείλει σώμα που ο server θα δεχόταν αμέσως. Η αποστολή κενής κεφαλίδας Expect: καταστέλλει την χειραψία, και το αίτημα περνά σε ένα round trip
Η δεύτερη είναι το CURLOPT_NOSIGNAL, που πρέπει να οριστεί. Χωρίς αυτό η libcurl υλοποιεί το timeout επίλυσης ονόματος με SIGALRM, και εκείνος ο μηχανισμός δεν είναι thread-safe. Η υπογραφή τρέχει σε worker thread, οπότε η προεπιλεγμένη συμπεριφορά είναι λανθάνουσα κρασάρισμα που εμφανίζεται κάτω από ταυτόχρονη εκτέλεση και ποτέ σε test ενός νήματος. Ο ορισμός της σημαίας απενεργοποιεί το μονοπάτι βάσει σήματος και κοστίζει μόνο τη λεπτότητα του timeout του resolver
Και τα δύο ελαττώματα μοιράζονται προφίλ που τα κάνει ακριβά να βρεθούν αργότερα. Κανένα δεν εμφανίζεται σε λειτουργικό test απέναντι σε ευγενικό server σε ένα νήμα. Και τα δύο εμφανίζονται στην παραγωγή, απέναντι σε έναν συγκεκριμένο TSA, κάτω από φορτίο. Όταν δένετε δικτυακή βιβλιοθήκη, διαβάστε τι υποθέτουν οι προεπιλογές της για τη διεργασία σας προτού υποθέσετε ότι ταιριάζουν
Πώς επαληθεύετε κώδικα που ο compiler σας δεν θα δει ποτέ;
Κάνοντας τον compiler να τον δει ούτως ή άλλως, μέσω ελεγχόμενου αντιγράφου. Η μηχανή ανάπτυξης εδώ δεν έχει cross-compiler για Linux ή macOS, οπότε οι μη-Windows κλάδες της μονάδας χρονικής σήμανσης δεν φτάνουν ποτέ στον γεννήτρια κώδικα κατά ένα συνηθισμένο build. Κώδικας που δεν μεταγλωττίζεται ποτέ είναι κώδικας που σαπίζει σιωπηλά: μια μετονομασία σε κοινό τύπο, μια αλλαγμένη λίστα παραμέτρων, μια νέα εξάρτηση μονάδας, και κανείς δεν το προσέχει για μήνες
Η τεχνική είναι μηχανική. Αντιγράψτε τη μονάδα σε προσωρινό κατάλογο, μετονομάστε την, και αντικαταστήστε κάθε Windows συνθήκη, και τη μορφή {$IFDEF MSWINDOWS} και τη μορφή {$IF DEFINED(MSWINDOWS), με symbol που δεν ορίζεται ποτέ. Μετά μεταγλωττίστε το αντίγραφο. Όταν και οι 3.828 γραμμές μεταγλωττίζονται, έχετε αποδείξει ότι το μη-Windows μονοπάτι χρησιμοποιεί μονάδες που υπάρχουν, καλεί συναρτήσεις backend με ταιριαστές υπογραφές, και αναφέρεται σε τύπους που είναι σε εμβέλεια. Δεν είναι απόδειξη ότι η μεταφορά δουλεύει, και τίποτα λιγότερο από την πλατφόρμα στόχο δεν θα σας το δώσει. Είναι απόδειξη ότι ο κλάδος δεν είναι ήδη σπασμένος, που είναι η μορφή αποτυχίας που πραγματικά συσσωρεύεται
Η συνοδός συνήθεια είναι να αφήσετε την ίδια τη μονάδα libcurl ελεύθερη από guards πλατφόρμας, ώστε να συμμετέχει στο συνηθισμένο Windows build παρό που τίποτα εκεί δεν την αναφέρει. Το καθημερινό build τότε εξακολουθεί να φυλάει τη σύνταξη και τους τύπους της δωρεάν. Μια μονάδα που μεταγλωττίζεται μόνο σε πλατφόρμα που δεν έχετε είναι μονάδα χωρίς κανέναν compiler να την ελέγχει, και η ίδια λογική ισχύει σε όλη τη δουλειά cross-compiler που περιγράφεται στα παγίδες cross-compiler Delphi και FPC
Περιορισμός όσων επιστρέφουν
Μια απόκριση timestamp είναι μικρή δομή DER, και τίποτα στη μεταφορά δεν το επιβάλλει. Ένας server που παραβιάστηκε, ρυθμίστηκε λάθος, ή απλώς δείχνει σε λάθος URL μπορεί να γυρίσει αυθαίρετη ροή, και ένας client που διαβάζει μέχρι να κλείσει η σύνδεση θα την συσσωρεύσει πρόθυμα. Και οι δύο μεταφορές επομένως βάζουν ταβάνι στην απόκριση, που είναι το σωστό σημείο για το όριο: η άρνηση στη μεταφορά εμποδίζει ένα υπερμέγεθες σώμα να δεσμευτεί ποτέ στη μνήμη, ενώ έλεγχος επιπέδου parser πυροδοτείται μόνο αφού η μνήμη έχει δεσμευτεί
Η ίδια λογική ισχύει και για το URL. Το backend δέχεται μόνο schemes που μπορεί να μιλήσει ουσιαστικά, οπότε ένα ρυθμιστικό λάθος αποτυγχάνει αμέσως με καθαρό μήνυμα αντί να παραδοθεί στην libcurl για ερμηνεία όποιον τρόπο επιτρέπει η υποστήριξη πρωτοκόλλων της
Που κάθεται η μεταφορά στην ιστορία της υπογραφής
Η χρονική σήμανση είναι το πρώτο βήμα της ιστορίας μακροχρόνιας επικύρωσης και όχι όλη η ιστορία. Το token πρέπει να επισυναφθεί στην υπογραφή, το υλικό επικύρωσης πρέπει να καταγραφεί στο document security store, και τα archive timestamps πρέπει να ανανεώνονται προτού αποδυναμωθεί το τρέχον. Εκείνος ολόκληρος τόξο καλύπτεται στα μακροχρόνιες υπογραφές PDF με timestamps RFC 3161 και DSS
Η μεταφορά είναι επίσης ένα κομμάτι φαρδύτερης θέσης φορητότητας: ο φυσικός loader βιβλιοθήκης που περιγράφεται στο φόρτωση της φυσικής βιβλιοθήκης σε οποιονδήποτε στόχο χειρίζεται την ίδια κατηγορία προβλήματος για το ίδιο το δυαδικό PDFium. Και στις δύο περιπτώσεις το μοτίβο είναι ταυτόσημο, δέστε έναν μικρό αριθμό symbols δυναμικά, αναφέρετε ακριβώς τι απέτυχε να δεθεί, και μην αφήσετε ποτέ μια λείπουσα εξάρτηση να γίνει αποτυχία χρόνου σύνδεσης που σταματά την εφαρμογή από το να ξεκινήσει
Τα Windows και μη-Windows backends timestamp παραδίδονται και τα δύο με το PDFium Delphi component, διαλεγμένα ανά στόχο και όχι ανά configuration, ώστε μια εφαρμογή Lazarus σε Linux και μια εφαρμογή Delphi σε Windows να παράγουν την ίδια υπογραφή με timestamp μέσω διαφορετικής υδραυλικής