Το PDFium VCL υπογράφει έγγραφα PAdES με ιδιωτικό κλειδί που κρατιέται στο Keychain του macOS μέσω backend που επιλύει κάθε symbol των Security και CoreFoundation κατά το runtime με dlopen και dlsym. Τίποτα δεν δένεται στον χρόνο σύνδεσης, που σημαίνει ότι ένα λάθος πληκτρολογημένο όνομα symbol έρχεται στην επιφάνεια ως KeychainAvailable που γυρνά False και KeychainMissingSymbols που κατονομάζει τον ένοχο, αντί ως σφάλμα linker ή κρασάρισμα
Εκείνη η επιλογή επιβλήθηκε από άβολη συνέπεια, και ο τρόπος που χειρίστηκε γενικεύει. Η μονάδα γράφτηκε σε μηχανή χωρίς macOS SDK, οπότε κάθε όνομα symbol framework και κάθε σταθερά ήρθε από τεκμηρίωση και τίποτα δεν μπορούσε να ελεγχθεί απέναντι σε header. Η λάθος απάντηση σε εκείνη την κατάσταση είναι να γράψεις τον κώδικα προσεκτικά και να ελπίσεις. Η σωστή είναι να κανονίσεις τα αναπόφευκτα λάθη να ανακοινώνουν τον εαυτό τους στην πιο εντοπίσιμη μορφή δυνατή
Γιατί η δυναμική δέσμευση είναι η σωστή κίνηση ακόμα και στην πλατφόρμα στόχο
Γιατί μετατρέπει μια κατηγορία αποτυχίας που σταματά το πρόγραμμα σε κατηγορία αποτυχίας που αναφέρει τον εαυτό της. Μια στατικά συνδεδεμένη αναφορά framework που είναι λάθος αποτυγχάνει στον χρόνο σύνδεσης στη στόχο και δεν συνδέεται ποτέ αλλού. Μια δυναμικά δεμένη που είναι λάθος παράγει μη διαθέσιμο backend και λίστα άλυτων ονομάτων, και το πρώτο τρέξιμο σε Mac γυρνά την ερώτηση από γιατί είναι αυτό μη διαθέσιμο σε μία γραμμή που κατονομάζει τυπογραφικό
Υπάρχει δεύτερο όφελος που πληρώνει καθημερινά και όχι μία φορά. Επειδή η μονάδα δεν δένει frameworks, μεταγλωττίζεται σε κάθε πλατφόρμα, οπότε το συνηθισμένο Windows build εξακολουθεί να ελέγχει τη σύνταξη, τους τύπους και τη uses ρήτρα της. Μια μονάδα που μεταγλωττίζεται μόνο σε πλατφόρμα που κανείς στην ομάδα δεν έχει είναι μονάδα χωρίς compiler να την κοιτάζει, και παρακμάζει ήσυχα με κάθε refactor κοινού τύπου
uses
FPdfCrypto, FPdfCryptoMac;
var
Options: TPadesSignerOptions;
begin
if not KeychainAvailable then
raise Exception.Create('Keychain backend unavailable, unresolved: ' +
KeychainMissingSymbols);
ConfigureKeychainSignerProvider; // εγκατάσταση ως backend υπογραφέα PAdES
ConfigureKeychainCmsVerifier; // και ως backend επικύρωσης
Writeln('signer backend : ', PadesCryptoBackendName);
Writeln('verify backend : ', PadesCmsVerificationBackendName);
Options := TPadesSignerOptions.Default;
Options.CertificateThumbprint := 'B1 3F 9C ...'; // SHA-1, οποιοδήποτε κεφαλαία/μικρά
Options.PaddingScheme := psRsaPss;
end;
Δύο είδη εξαγόμενων symbols, δύο τρόποι ανάγνωσής τους
Αυτή είναι η πιο συγχεόμενη λεπτομέρεια σε όλη τη δέσμευση, και το να την πάρεις ανάποδα μεταγλωττίζεται καθαρά και αποτυγχάνει στο runtime. Η CoreFoundation και η Security εξάγουν δύο ουσιαστικά διαφορετικά πράγματα μέσω της ίδιας κλήσης dlsym, και ο κώδικας πρέπει να ξέρει ποιο είναι ποιο
Ονομασμένες σταθερές όπως τα κλειδιά κλάσεων στοιχείων keychain και τα boolean singletons της CoreFoundation είναι εξαγόμενες μεταβλητές των οποίων τα περιεχόμενα είναι το CFStringRef ή CFBooleanRef που θέλετε. Η dlsym γυρνά τη διεύθυνση εκείνης της μεταβλητής, οπότε πρέπει να κάνετε dereference μία φορά για να πάρετε την τιμή. Δομές πινάκων callback όπως οι callbacks κλειδιών και τιμών λεξικού είναι εξαγόμενες δομές, και η dlsym γυρνά τη διεύθυνση της δομής, που είναι ακριβώς ο δείκτης που περιμένει η συνάρτηση δημιουργίας λεξικού. Κάνετε dereference εκείνον και περνάτε την πρώτη λέξη μηχανής της δομής σαν να ήταν δείκτης
Κανένα από τα δύο λάθη δεν παράγει σφάλμα μεταγλώττισης, και κανένα δεν παράγει καθαρό σφάλμα runtime. Παίρνετε δείκτη σκουπιδιών που αποτυγχάνει κάπου κατάντη. Ο τρόπος να γίνει η διάκριση αδύνατο να εκληφθεί λάθος είναι να σταματήσετε να βασίζεστε στο να τη θυμάστε: δύο βοηθητικές συναρτήσεις, μία που δένει και κάνει dereference και μία που δένει και δεν κάνει, ώστε το σημείο κλήσης να δηλώνει ποιο είδος symbol ζητά και ο βοηθός να επιβάλλει τα υπόλοιπα
// Εξαγόμενη μεταβλητή: η dlsym δίνει τη διεύθυνση μεταβλητής που κρατά
// το CFTypeRef, οπότε dereference μία φορά
FSecClassKey := BindConstant(SecurityLib, 'kSecClass');
// Εξαγόμενη δομή: η dlsym δίνει τη διεύθυνση ΤΗΣ δομής, που είναι
// όσο θέλει το API. Μην κάνετε dereference
FKeyCallbacks := BindStruct(CoreFoundationLib,
'kCFTypeDictionaryKeyCallBacks');
Γιατί μια υπογραφή RSA-PSS θέλει δύο χωριστές εφεδρείες;
Γιατί ο αλγόριθμος μπορεί να λείπει με δύο ανεξάρτητους τρόπους, και μόνο ένας από αυτούς είναι ερώτηση έκδοσης. Η σταθερά αλγορίθμου ψηφιακής σύνοψης υπογραφής PSS εμφανίστηκε στο macOS 10.13, οπότε σε παλαιότερο σύστημα το symbol απλώς δεν υπάρχει και η δέσμευση παίρνει nil. Εκείνος είναι ο έλεγχος έκδοσης. Χωριστά, σε σύστημα όπου η σταθερά υπάρχει, ένα συγκεκριμένο κλειδί μπορεί ακόμα να την αρνηθεί, και το framework απαντά εκείνη την ερώτηση μέσω SecKeyIsAlgorithmSupported για εκείνο το κλειδί. Ένα κλειδί με υλική βάση ή κλειδί με περιοριστικά attributes μπορεί να απορρίψει PSS ενώ ένα software κλειδί στην ίδια μηχανή το δέχεται
Και τα δύο μονοπάτια πρέπει να οδηγούν στην ίδια εφεδρεία: μετάβαση σε PKCS#1 v1.5. Και το κρίσιμο είναι ότι η εφεδρεία πρέπει να αλλάζει και τον αναγνωριστικό αλγορίθμου που γράφεται στη δομή CMS, όχι μόνο την κλήση υπογραφής. Έκδοση αναγνωριστικού αλγορίθμου PSS ενώ παράγεται στην πραγματικότητα υπογραφή v1.5 δίνει έγγραφο που κάθε verifier απορρίπτει κατηγορηματικά, που είναι αυστηρά χειρότερο από την αναφορά ότι το PSS δεν υποστηρίζεται. Μια υποβάθμιση είναι αποδεκτή, μια αναντιστοιχία ανάμεσα σε όσα δηλώνετε και όσα κάνατε δεν είναι, και αυτός είναι γενικός κανόνας για κώδικα υπογραφών και όχι quirk του macOS. Οι συνέπειες επιπέδου υπογραφής εκτίθενται στο υπογραφή PDF με PAdES B-B
Κωδικοποίηση υπογραφής ECDSA, και μια αντιστροφή που αξίζει προσοχή
Το μονοπάτι ελλειπτικών καμπυλών δεν θέλει καθόλου μετατροπή στο macOS, και αυτό είναι το αντίθετο από όσο απαιτεί μια δέσμευση PKCS#11. Ο αλγόριθμος digest-signing του Security framework για ECDSA γυρνά την υπογραφή ήδη σε μορφή X9.62 DER, που είναι ακριβώς όσο θέλει το CMS. Ένα token PKCS#11 γυρντά το ακατέργαστο ζεύγος σταθερού πλάτους P1363 αντίθετα, που πρέπει να επαν-κωδικοποιηθεί προτού μπει σε δομή υπογραφής
Έτσι δύο backends που υλοποιούν το ίδιο interface θέλουν αντίθετη μεταχείριση για τον ίδιο αλγόριθμο, και κανένα από τα δύο δεν είναι λάθος. Αυτό είναι ακριβώς το είδος διαφοράς που μια αφαίρεση πρέπει να απορροφά και όχι να εκθέτει: το στρώμα PAdES ζητά από provider να υπογράψει, και οι συμβάσεις κωδικοποίησης μένουν μέσα στον provider. Αν διαρρέουν προς τα πάνω, κάθε caller καταλήγει να κουβαλά conditional ανά backend. Το ίδιο σχήμα εμφανίζεται στην ιστορία απομακρυσμένης υπογραφής που περιγράφεται στις απομακρυσμένες συνεδρίες υπογραφής PAdES απέναντι σε HSM
// Το interface του provider είναι το ίδιο σε κάθε πλατφόρμα, οπότε η
// επιλογή είναι απόφαση εκκίνησης και όχι ανά κλήση
{$IFDEF DARWIN}
if KeychainAvailable then
ConfigureKeychainSignerProvider;
{$ENDIF}
{$IFDEF MSWINDOWS}
// Ο provider CNG των Windows εγκαθίσταται από τη μονάδα πλατφόρμας
{$ENDIF}
if not PadesCryptoAvailable then
raise Exception.Create('no signing backend on this platform');
// Από εδώ και πέρα ο κώδικας υπογραφής είναι ουδέτερος πλατφόρμας
Signer := ResolvePadesSigner(Options);
Κανόνες μέτρησης αναφορών που κάθονται τρεις γραμμές μακριά
Η διαχείριση μνήμης της Core Foundation ακολουθεί συμβάσεις ονοματοδοσίας, και η παγίδα εδώ είναι ότι συναρτήσεις με διαφορετικές συμβάσεις εμφανίζονται δίπλα η μία στην άλλη στο ίδιο σύντομο block. Μια συνάρτηση που παίρνει certificate από trust αντικείμενο γυρνά δανεική αναφορά που δεν πρέπει να απελευθερωθεί. Συναρτήσεις που αντιγράφουν certificate υπογραφέα ή αντιγράφουν τα δεδομένα του γυρνάνε owned αναφορές που πρέπει να απελευθερωθούν. Τρεις κλήσεις στη σειρά, δύο κανόνες ιδιοκτησίας, και η απελευθέρωση της δανεικής δεν αποτυγχάνει σε εκείνη τη γραμμή. Χαλάει έναν μετρητή retain και ρίχνει κάτι άσχετο αργότερα
Η μετριαστική λύση είναι να διαβάζετε το ρήμα σε κάθε όνομα συνάρτησης framework προτού γράψετε τον καθαρισμό, κάθε φορά, χωρίς εξαίρεση. Είναι το ισοδύναμο CoreFoundation του να ελέγχεις αν ένα API γυρνά αντίγραφο ή θέα, και το κόστος του λάθους είναι διαλείπον κρασάρισμα και όχι σφάλμα
Τι δεν ισχυρίζεται αυτό το backend
Δεν έχει τρέξει ποτέ σε macOS τη στιγμή της συγγραφής, και το να το πεις καθαρά είναι πιο χρήσιμο από μια υπονοούμενη διαβεβαίωση. Αυτό που αποδεικνύεται αληθές είναι στενότερο και εξακολουθεί να αξίζει: η μονάδα μεταγλωττίζεται σε Windows ως μέρος του καθημερινού build, κάθε symbol framework δένεται με όνομα στο runtime με τις αποτυχίες απαριθμημένες, και η λογική επιλογής αλγορίθμων συμπεριλαμβανομένων και των δύο εφεδρειών PSS είναι συνηθισμένη Pascal που μπορεί να αναθεωρηθεί και να συλλογιστεί. Το πρώτο τρέξιμο σε Mac είτε θα δουλέψει είτε θα παράγει λίστα ονομάτων προς διόρθωση
Το αντίστοιχο επικύρωσης, που χρησιμοποιεί τον decoder CMS υψηλότερου επιπέδου αντί να συνθέτει τη δομή CMS στο χέρι, καλύπτεται στο επικύρωση υπογραφών PDF στο macOS με SecTrust, και μοιράζεται την ίδια υποδομή δέσμευσης και την ίδια διαγνωστική προσέγγιση
Η μεταφέρεμε ιδέα εδώ αφορά τοποθέτηση κινδύνου και όχι macOS. Όταν πρέπει να γράψετε κώδικα απέναντι σε interface που δεν μπορείτε να επαληθεύσετε, διαλέξτε την κατασκευή όπου τα λάθη είναι φθηνότερο να εντοπιστούν. Δυναμική δέσμευση με ρητή λίστα άλυτων ονομάτων γυρνά είκοσι μη επαληθεύσιμες υποθέσεις σε μία διαγνωστική γραμμή. Και τα δύο backends παραδίδονται ως πηγαίος κώδικας με το PDFium Delphi component, οπότε αν κάποιο όνομα symbol χρειαστεί διόρθωση, είναι αλλαγή μίας γραμμής στο δικό σας δέντρο και όχι support ticket