PDFium VCL podpisuje dokumenty PAdES kluczem prywatnym trzymanym w Keychain macOS przez backend, który rozwiązuje każdy symbol Security i CoreFoundation w runtime przez dlopen i dlsym. Nic nie jest bindowane w czasie linkowania, co znaczy, że literówka w nazwie symbolu wychodzi jako KeychainAvailable zwracające False i KeychainMissingSymbols wskazujące winowajcę, a nie jako błąd linkera albo crash
Ten wybór został wymuszony niewygodnym ograniczeniem, a sposób jego obsłużenia się uogólnia. Unit był pisany na maszynie bez SDK macOS, więc każda nazwa symbolu frameworku i każda stała pochodziła z dokumentacji i nic z tego nie dało się sprawdzić względem nagłówka. Złą odpowiedzią na taką sytuację jest pisać kod starannie i mieć nadzieję. Właściwą jest zaaranżować, by nieuniknione pomyłki same się zgłaszały w najłatwiejszej do zlokalizowania postaci
Dlaczego bindowanie dynamiczne to trafny ruch nawet na platformie docelowej
Bo zamienia klasę awarii zatrzymujących program w klasę awarii, które same się zgłaszają. Źle napisana statycznie linkowana referencja do frameworka polegnie w czasie linkowania na targecie i nigdzie indziej się nie zlinkuje. Źle zbindowana dynamicznie produkuje niedostępny backend i listę nierozwiązanych nazw, a pierwsze odpalenie na Macu zamienia pytanie dlaczego to niedostępne w jedną linię wskazującą literówkę
Jest druga korzyść, która spłaca się codziennie, a nie raz. Ponieważ unit nie linkuje żadnych frameworków, kompiluje się na każdej platformie, więc zwykły build Windows wciąż sprawdza jego składnię, typy i klauzulę uses. Unit kompilujący się wyłącznie na platformie, której nikt w zespole nie ma, to unit bez żadnego kompilatora patrzącego mu na ręce i cichnie z każdym refaktorem współdzielonego typu
uses
FPdfCrypto, FPdfCryptoMac;
var
Options: TPadesSignerOptions;
begin
if not KeychainAvailable then
raise Exception.Create('Keychain backend unavailable, unresolved: ' +
KeychainMissingSymbols);
ConfigureKeychainSignerProvider; // instalacja jako backend podpisujący PAdES
ConfigureKeychainCmsVerifier; // i jako backend weryfikacji
Writeln('signer backend : ', PadesCryptoBackendName);
Writeln('verify backend : ', PadesCmsVerificationBackendName);
Options := TPadesSignerOptions.Default;
Options.CertificateThumbprint := 'B1 3F 9C ...'; // SHA-1, dowolna wielkość liter
Options.PaddingScheme := psRsaPss;
end;
Dwa rodzaje eksportowanych symboli, dwa sposoby ich czytania
To najbardziej mylący pojedynczy detal w całym bindingu i odwrócenie go kompiluje się czysto, a polega w runtime. CoreFoundation i Security eksportują dwie kategorycznie różne rzeczy tym samym wywołaniem dlsym i kod musi wiedzieć, które jest które
Nazwane stałe, jak klucze klas elementów keychain i booleanowe singletony CoreFoundation, to eksportowane zmienne, których zawartością jest CFStringRef albo CFBooleanRef, którego chcesz. dlsym zwraca adres tej zmiennej, więc musisz zdereferencjonować raz, by dostać wartość. Struktury tabel callbacków, jak callbacki klucza i wartości słownika, to eksportowane struktury i dlsym zwraca adres struktury, czyli dokładnie ten wskaźnik, którego oczekuje funkcja tworząca słownik. Zdereferencjonujesz ten i podajesz pierwsze maszynowe słowo struktury tak, jakby było wskaźnikiem
Żadna z tych pomyłek nie daje błędu kompilacji i żadna nie daje jasnego błędu runtime. Dostajesz śmieciowy wskaźnik, który polegnie gdzieś w dole. Sposób, by tę różnicę nie dało się pomylić, to przestać polegać na pamiętaniu: dwie funkcje pomocnicze, jedna bindująca i dereferencjonująca, druga bindująca bez dereferencji, więc miejsce wywołania deklaruje, jakiego rodzaju symbolu prosi, a helper egzekwuje resztę
// Eksportowana zmienna: dlsym daje adres zmiennej trzymającej
// CFTypeRef, więc dereferencjonuj raz
FSecClassKey := BindConstant(SecurityLib, 'kSecClass');
// Eksportowana struktura: dlsym daje adres STRUKTURY, czyli dokładnie to,
// czego chce API. Nie dereferencjonuj
FKeyCallbacks := BindStruct(CoreFoundationLib,
'kCFTypeDictionaryKeyCallBacks');
Dlaczego podpis RSA-PSS potrzebuje dwóch osobnych fallbacków?
Bo algorytm może być nieobecny na dwa niezależne sposoby i tylko jeden z nich to pytanie o wersję. Stała algorytmu podpisującego digest PSS pojawiła się w macOS 10.13, więc na starszym systemie symbolu po prostu nie ma i binding dostaje nil. To jest sprawdzenie wersji. Osobno, na systemie, gdzie stała istnieje, konkretny klucz może ją mimo wszystko odrzucić i framework odpowiada na to pytanie przez SecKeyIsAlgorithmSupported dla tego klucza. Klucz wsparty sprzętowo albo klucz z restrykcyjnymi atrybutami może odmówić PSS, podczas gdy programowy klucz na tej samej maszynie go przyjmie
Obie ścieżki muszą prowadzić do tego samego fallbacku: przełącz na PKCS#1 v1.5. A krytyczna część jest taka, że fallback musi zmienić także identyfikator algorytmu zapisywany do struktury CMS, nie tylko wywołanie podpisujące. Wyemitowanie identyfikatora algorytmu PSS przy faktycznej produkcji podpisu v1.5 daje dokument, który każdy weryfikator odrzuci w prostej linii, co jest ściśle gorsze niż zgłoszenie, że PSS nie jest wspierany. Downgrade jest akceptowalny, niezgodność między tym, co deklarujesz, a tym, co zrobiłeś — nie, i to jest ogólna reguła dla kodu podpisów, a nie kaprys macOS. Implikacje na poziomie podpisu rozkłada w artykule podpisywanie PDF-ów z PAdES B-B
Kodowanie podpisu ECDSA i odwrócenie warte odnotowania
Ścieżka krzywych eliptycznych nie potrzebuje na macOS żadnej konwersji w ogóle i to jest odwrotność tego, czego wymaga binding PKCS#11. Algorytm podpisujący digest dla ECDSA w frameworku Security zwraca podpis już w postaci X9.62 DER, czyli dokładnie to, czego chce CMS. Token PKCS#11 zwraca zamiast tego surową parę P1363 o stałej szerokości, którą trzeba przekodować, zanim trafi do struktury podpisu
Dwa backendy implementujące ten sam interfejs potrzebują więc przeciwnego traktowania dla tego samego algorytmu i żaden nie jest w błędzie. To dokładnie ten rodzaj różnicy, który abstrakcja musi wchłonąć, a nie wystawiać: warstwa PAdES prosi providera o podpis, a konwencje kodowania zostają w środku providera. Jeśli wyciekną w górę, każdy wołający skończy z warunkowym per backend. Ten sam kształt pojawia się w historii zdalnego podpisywania opisanej w artykule zdalne sesje podpisywania PAdES względem HSM
// Interfejs providera jest ten sam na każdej platformie, więc wybór to
// decyzja startowa, a nie per wywołanie
{$IFDEF DARWIN}
if KeychainAvailable then
ConfigureKeychainSignerProvider;
{$ENDIF}
{$IFDEF MSWINDOWS}
// Provider CNG Windows jest instalowany przez unit platformowy
{$ENDIF}
if not PadesCryptoAvailable then
raise Exception.Create('no signing backend on this platform');
// Stąd kod podpisujący jest neutralny platformowo
Signer := ResolvePadesSigner(Options);
Reguły zliczania referencji oddalone o trzy linie
Zarządzanie pamięcią Core Foundation podąża za konwencjami nazewniczymi, a pułapka polega na tym, że funkcje o różnych konwencjach pojawiają się obok siebie w tym samym krótkim bloku. Funkcja, która bierze certyfikat z obiektu trust, zwraca pożyczoną referencję, której nie wolno zwalniać. Funkcje, które kopiują certyfikat podpisującego albo kopiują jego dane, zwracają referencje posiadane, które trzeba zwolnić. Trzy wywołania pod rząd, dwie reguły własności, a zwolnienie pożyczonej nie polegnie w tej linii. Psuje licznik retain i zbija coś niezwiązanego później
Mitygacją jest czytanie czasownika w nazwie każdej funkcji frameworka przed napisaniem sprzątania, za każdym razem, bez wyjątku. To odpowiednik CoreFoundation sprawdzania, czy API zwraca kopię, czy widok, a koszt pomyłki to przerywany crash, a nie błąd
Czego ten backend nie twierdzi
Nigdy nie chodził na macOS w chwili pisania i powiedzenie tego wprost jest pożyteczniejsze niż sugerowane zapewnienie. Demonstracyjnie prawdziwe jest węższe i wciąż cenne: unit kompiluje się na Windows w ramach codziennego builda, każdy symbol frameworka jest bindowany po nazwie w runtime z wyliczonymi porażkami, a logika wyboru algorytmu, łącznie z oboma fallbackami PSS, to zwykły Pascal, który można recenzować i o nim rozumować. Pierwsze odpalenie na Macu albo zadziała, albo wyprodukuje listę nazw do poprawienia
Odpowiednik weryfikacyjny, który używa dekodera CMS wyższego poziomu zamiast składania struktury CMS ręcznie, jest opisany w artykule weryfikacja podpisów PDF na macOS z SecTrust i dzieli tę samą infrastrukturę bindingu i to samo podejście diagnostyczne
Przenośny pomysł stąd dotyczy rozmieszczania ryzyka, a nie macOS. Gdy musisz pisać kod względem interfejsu, którego nie możesz zweryfikować, wybierz konstrukcję, w której pomyłki są najtańsze do zlokalizowania. Bindowanie dynamiczne z jawną listą nierozwiązanych nazw zamienia dwadzieścia niemożliwych do zweryfikowania założeń w jedną linię diagnostyczną. Oba backendy są dostarczane jako źródła z komponentem PDFium dla Delphi, więc jeśli nazwa symbolu jednak wymaga poprawki, to zmiana jednej linii we własnym drzewie, a nie ticket na support