PDFium VCL podpisuje PAdES dokumenty súkromným kľúčom uloženým v macOS Keychain cez backend, ktorý resolveuje každý symbol Security a CoreFoundation za behu cez dlopen a dlsym. Nič nie je viazané v čase linkovania, takže preklep v názve symbolu sa prejaví ako KeychainAvailable vracajúce False a ako KeychainMissingSymbols vymenujúci vinníka, nie ako linková chyba alebo pád
Túto voľbu vynútilo nepríjemné obmedzenie a spôsob, akým bolo ošetrené, sa dá zovšeobecniť. Unita vznikla na stroji bez macOS SDK, takže každý názov frameworkového symbolu a každá konštanta pochádzali z dokumentácie a nič sa nedalo skontrolovať proti hlavičkovému súboru. Zlá reakcia na takú situáciu je písať kód opatrne a dúfať. Tá správna je zariadiť, aby sa nevyhnutné chyby prihlásili samy v čo najľahšie dohľadateľnej podobe
Prečo je dynamické viazanie správna voľba aj na cieľovej platforme
Pretože premieňa triedu zlyhaní, ktoré zastavia program, na triedu zlyhaní, ktoré sa prihlásia samy. Staticky linkovaný odkaz na framework, ktorý je zlý, zlyhá pri linkovaní na cieli a nikde inde sa nezlinkuje. Dynamicky viazaný zlý symbol vyprodukuje nedostupný backend a zoznam nevyriešených názvov, a prvé spustenie na Macu zmení otázku prečo je to nedostupné na jediný riadok vymenujúci preklep
Je tu aj druhý prínos, ktorý sa vypláca denne, nie raz. Pretože unita nezlinkuje žiadne frameworky, skompiluje sa na každej platforme, takže obyčajný Windows build naďalej kontroluje jej syntax, typy a uses klauzulu. Unita, ktorá sa kompiluje len na platforme, ktorú nikto v tíme nemá, je unita bez jediného prekladača, ktorý by sa na ňu pozrel, a ticho chátra pri každom refaktore zdieľaného typu
uses
FPdfCrypto, FPdfCryptoMac;
var
Options: TPadesSignerOptions;
begin
if not KeychainAvailable then
raise Exception.Create('Keychain backend unavailable, unresolved: ' +
KeychainMissingSymbols);
ConfigureKeychainSignerProvider; // inštaluje sa ako PAdES podpisový backend
ConfigureKeychainCmsVerifier; // a zároveň ako overovací backend
Writeln('signer backend : ', PadesCryptoBackendName);
Writeln('verify backend : ', PadesCmsVerificationBackendName);
Options := TPadesSignerOptions.Default;
Options.CertificateThumbprint := 'B1 3F 9C ...'; // SHA-1, ľubovoľná veľkosť písmen
Options.PaddingScheme := psRsaPss;
end;
Dva druhy exportovaných symbolov, dva spôsoby, ako ich čítať
Toto je jediný najmenej zrozumiteľný detail celého viazania a pomýliť sa v ňom znamená čisto sa skompilovať a zlyhať až za behu. CoreFoundation a Security exportujú cez rovnaké volanie dlsym dve veci kategoricky odlišného druhu a kód musí vedieť, ktorý je ktorý
Menované konštanty, ako sú kľúče tried položiek keychainu či CoreFoundation boolean singletony, sú exportované premenné, ktorých obsahom je CFStringRef alebo CFBooleanRef, ktorý chcete. dlsym vráti adresu tej premennej, takže musíte raz dereferencovať, aby ste získali hodnotu. Štruktúry callback tabuliek, ako sú callbacky kľúčov a hodnôt slovníka, sú exportované štruktúry a dlsym vráti adresu štruktúry, čo je presne ten pointer, ktorý funkcia na vytváranie slovníka očakáva. Keď dereferencujete túto, odovzdáte prvé strojové slovo štruktúry, ako keby to bol pointer
Ani jedna z omylov neprodukuje compile error a ani jedna neprodukuje zrozumiteľnú runtime chybu. Dostanete garbage pointer, ktorý zlyhá niekde po prúde. Spôsob, ako urobiť rozlišovanie neomyliteľným, je prestať sa spoliehať na pamätanie si: dve helper funkcie, jedna viaže a dereferencuje a druhá viaže a nedereferencuje, takže miesto volania deklaruje, aký druh symbolu žiada, a helper vynúti zvyšok
// Exportovaná premenná: dlsym dáva adresu premennej držiacej
// CFTypeRef, takže raz dereferencujte
FSecClassKey := BindConstant(SecurityLib, 'kSecClass');
// Exportovaná štruktúra: dlsym dáva adresu ŠTRUKTÚRY, čo je presne
// to, čo API chce. Nedereferencujte
FKeyCallbacks := BindStruct(CoreFoundationLib,
'kCFTypeDictionaryKeyCallBacks');
Prečo potrebuje podpis RSA-PSS dva samostatné fallbacky?
Pretože algoritmus môže chýbať dvoma nezávislými spôsobmi a len jeden z nich je otázka verzie. Konštanta PSS digest-signing algoritmu sa objavila v macOS 10.13, takže na staršom systéme symbol jednoducho nie je a viazanie dostane nil. To je kontrola verzie. Nezávisle na tom môže na systéme, kde konštanta existuje, konkrétny kľúč PSS aj tak odmietnuť, a framework odpovedá na túto otázku cez SecKeyIsAlgorithmSupported pre daný kľúč. Kľúč podložený hardvérom alebo kľúč s reštriktívnymi atribútmi môže PSS odmietnuť, kým softvérový kľúč na tom istom stroji ho prijme
Oba cesty musia viesť k tomu istému fallbacku: prepnúť na PKCS#1 v1.5. A kritická časť je, že fallback musí zmeniť aj identifikátor algoritmu zapísaný do CMS štruktúry, nielen podpisové volanie. Vyprodukovať identifikátor algoritmu PSS, kým reálne vzniká podpis v1.5, dáva dokument, ktorý každý overovač odmietne rovno, čo je striktne horšie než nahlásiť, že PSS nie je podporované. Downgrade je prijateľný, nesúlad medzi tým, čo deklarujete, a tým, čo ste urobili, nie, a to je všeobecné pravidlo pre podpisový kód, nie macOS kuriozita. Dôsledky na úrovni podpisu rozoberá článok podpisovanie PDF s PAdES B-B
Kódovanie podpisov ECDSA a otočka, ktorá stojí za zmienku
Eliptická krivka nepotrebuje na macOS žiadnu konverziu, čo je presný opak toho, čo vyžaduje PKCS#11 viazanie. Digest-signing algoritmus Security frameworku pre ECDSA vracia podpis už v tvare X9.62 DER, čo je presne to, čo CMS chce. PKCS#11 token namiesto toho vracia surový fixed-width pár P1363, ktorý sa musí pred vložením do podpisovej štruktúry prekódovať
Takže dva backendy implementujúce to isté rozhranie potrebujú pre ten istý algoritmus opačné zaobchádzanie a ani jeden nie je zlý. Presne takýto rozdiel musí abstrakcia pohltnúť namiesto toho, aby ho vystavovala: PAdES vrstva požiada providera o podpis a konvencie kódovania zostávajú vo vnútri providera. Ak unikajú nahor, každý volajúci končí s podmienkou podľa backendu. Rovnaký vzor sa objavuje v príbehu vzdialeného podpisovania popísanom v článku vzdialené PAdES podpisové relácie voči HSM
// Rozhranie providera je na každej platforme rovnaké, takže výber je
// rozhodnutie pri štarte, nie pri každom volaní
{$IFDEF DARWIN}
if KeychainAvailable then
ConfigureKeychainSignerProvider;
{$ENDIF}
{$IFDEF MSWINDOWS}
// Windows CNG provider inštaluje platformová unita
{$ENDIF}
if not PadesCryptoAvailable then
raise Exception.Create('no signing backend on this platform');
// Odtiaľto je podpisový kód platformovo neutrálny
Signer := ResolvePadesSigner(Options);
Pravidlá referenčného počítania tri riadky od seba
Správa pamäte Core Foundation sa riadi konvenciami názvov a pascu je, že funkcie s odlišnými konvenciami stoja vedľa seba v tom istom krátkom bloku. Funkcia, ktorá získa certifikát z trust objektu, vracia požičanú referenciu, ktorá sa nesmie uvoľniť. Funkcie, ktoré kopírujú podpisový certifikát alebo jeho dáta, vracajú vlastnené referencie, ktoré sa uvoľniť musia. Tri volania za sebou, dve pravidlá vlastníctva a uvoľnenie požičanej na tom riadku nezlyhá. Pokazí retain count a neskôr zhodí niečo úplne nesúvisiace
Zmierňuje sa to čítaním slovesa v názve každej frameworkovej funkcie pred písaním upratovania, zakaždým, bez výnimky. Je to CoreFoundation ekvivalent kontroly, či API vracia kópiu alebo pohľad, a cena omylu je občasný pád namiesto chyby
Čo tento backend netvrdí
V čase písania nikdy nebežal na macOS a povedať to rovno je užitočnejšie než naznačené uistenie. Dôkazne pravda je užšia a stále cenná: unita sa kompiluje na Windowse ako súčasť denného buildu, každý frameworkový symbol sa viaže menom za behu s vymenovanými zlyhaniami a logika výberu algoritmov vrátane oboch PSS fallbackov je obyčajný Pascal, ktorý sa dá reviewovať a o ktorom sa dá uvažovať. Prvé spustenie na Macu buď funguje, alebo vyprodukuje zoznam názvov na opravu
Overovacia strana, ktorá používa CMS dekóder vyššej úrovne namiesto ručného skladania CMS štruktúry, je rozobratá v článku overovanie PDF podpisov na macOS s SecTrust a zdieľa rovnakú infraštruktúru viazania a rovnaký diagnostický prístup
Prenositeľná myšlienka je o umiestnení rizika, nie o macOS. Keď musíte písať kód proti rozhraniu, ktoré nedokážete overiť, zvoľte konštrukciu, kde sú chyby najlacnejšie na nájdenie. Dynamické viazanie s explicitným zoznamom nevyriešených názvov mení dvadsať neoveriteľných predpokladov na jeden diagnostický riadok. Oba backendy sa dodávajú ako zdroják s PDFium Delphi komponentom, takže ak sa názov symbolu musí opraviť, je to zmena jedného riadku vo vašom vlastnom strome, nie support ticket