Τεχνικό Άρθρο

Υπογραφή PAdES με ταυτότητα Keychain macOS σε Delphi

Το 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 ζητά και ο βοηθός να επιβάλλει τα υπόλοιπα

Διάγραμμα του backend Keychain macOS του PDFium VCL που επιλύει symbols Security και CoreFoundation μέσω dlsym: το kSecClass είναι εξαγόμενη μεταβλητή που η BindConstant κάνει dereference μία φορά για να πάρει την τιμή CFStringRef, ενώ το kCFTypeDictionaryKeyCallBacks είναι εξαγόμενη δομή που η BindStruct περνά με διεύθυνση, και η ανάμειξη των δύο κανόνων δίνει δείκτες σκουπιδιών κατάντη
Μία κλήση dlsym γυρνά δύο ουσιαστικά διαφορετικά πράγματα: τη διεύθυνση μεταβλητής που κρατά CFTypeRef και τη διεύθυνση δομής callbacks. Δύο βοηθοί κάνουν την απόφαση dereference ή όχι στο σημείο δέσμευσης αντί στη μνήμη
// Εξαγόμενη μεταβλητή: η 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

Αλυσίδα αποφάσεων που δείχνει γιατί η υπογραφή RSA-PSS στο backend Keychain του PDFium VCL θέλει δύο ανεξάρτητες εφεδρείες: η dlsym γυρνά nil για τη σταθερά digest-signing σε εκδόσεις macOS πριν το 10.13, το SecKeyIsAlgorithmSupported μπορεί να απορρίψει κλειδί με υλική βάση, και οι δύο πύλες χοάνης στην ίδια υποβάθμιση PKCS#1 v1.5 της οποίας το αναγνωριστικό αλγορίθμου CMS πρέπει να αλλάζει μαζί της
Το PSS μπορεί να μη διατίθεται δύο φορές, μία ανά έκδοση macOS και μία ανά κλειδί, και μόνο η πύλη έκδοσης είναι ερώτηση συστήματος. Και οι δύο πύλες χοάνης στην ίδια υποβάθμιση v1.5, και το αναγνωριστικό CMS ακολουθεί

Κωδικοποίηση υπογραφής 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

Σύγκριση κωδικοποίησης υπογραφής ECDSA ανάμεσα σε δύο backends του υπογράφοντα PAdES του PDFium VCL: το Security framework του Keychain macOS γυρνά X9.62 DER που το CMS δέχεται με μηδενική μετατροπή, ενώ ένα token PKCS#11 γυρνά το ακατέργαστο ζεύγος σταθερού πλάτους P1363 που πρέπει να επαν-κωδικοποιηθεί, οπότε η ResolvePadesSigner κρατά τις συμβάσεις κωδικοποίησης μέσα στον provider
Το ίδιο interface ECDSA θέλει αντίθετη μεταχείριση ανά backend: η Security παραδίδει έτοιμο DER ενώ ένα token PKCS#11 παραδίδει ακατέργαστο P1363, οπότε η μετατροπή ζει μέσα στον provider και οι callers δεν βλέπουν ποτέ conditional ανά backend
// Το 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