PDFium VCL potpisuje PAdES dokumente privatnim ključem držanim u macOS Keychain-u kroz backend koji razrešava svaki Security i CoreFoundation simbol u vremenu izvršavanja sa dlopen i dlsym. Ništa nije vezano u trenutku povezivanja, što znači da se pogrešno ukucano ime simbola pokazuje kao KeychainAvailable koje vraća False i KeychainMissingSymbols koje imenuje krivca, umesto kao greška linker-a ili pad
Taj izbor je nametnula neudobna ograničenja, i način na koji je obrađen se generališe. Unit je pisan na mašini bez macOS SDK-a, pa je svako ime framework simbola i svaka konstanta došla iz dokumentacije i nijedno se nije moglo proveriti naspram header-a. Pogrešan odgovor na tu situaciju je napisati kod pažljivo i nadati se. Ispravan je urediti da se neizbežne greške same jave u obliku koji se najlakše locira
Zašto je dinamičko vezivanje pravi poziv čak i na ciljnoj platformi
Zato što klasu otkaza koja zaustavlja program pretvara u klasu otkaza koja sama sebe prijavljuje. Pogrešna statički vezana referenca na framework pada u trenutku povezivanja na meti i nikada se ne poveže drugde. Pogrešno dinamički vezana proizvodi nedostupan backend i spisak nerazrešenih imena, i prvo pokretanje na Mac-u pretvara pitanje zašto ovo nije dostupno u jednu liniju koja imenuje prekucaj
Postoji i druga dobit koja se isplati svakog dana, a ne jednom. Pošto unit ne vezuje nikakve framework-ove, kompajlira se na svakoj platformi, pa obični Windows build nastavlja da proverava njegovu sintaksu, tipove i uses klauzu. Unit koji se kompajlira samo na platformi koju niko u timu nema je unit na koji nijedan kompajler ne gleda, i tiho truli uz svaki refactor deljenog tipa
uses
FPdfCrypto, FPdfCryptoMac;
var
Options: TPadesSignerOptions;
begin
if not KeychainAvailable then
raise Exception.Create('Keychain backend unavailable, unresolved: ' +
KeychainMissingSymbols);
ConfigureKeychainSignerProvider; // instaliraj kao PAdES signer backend
ConfigureKeychainCmsVerifier; // i kao verification backend
Writeln('signer backend : ', PadesCryptoBackendName);
Writeln('verify backend : ', PadesCmsVerificationBackendName);
Options := TPadesSignerOptions.Default;
Options.CertificateThumbprint := 'B1 3F 9C ...'; // SHA-1, bilo koje veliko ili malo slovo
Options.PaddingScheme := psRsaPss;
end;
Dve vrste izvezenih simbola, dva načina da se pročitaju
Ovo je najzbunjujući pojedinačni detalj u celom vezivanju, i njegovo pogrešno shvatanje se čisto kompajlira a pada u vremenu izvršavanja. CoreFoundation i Security izvoze dve kategorijalno različite stvari kroz isti dlsym poziv, i kod mora znati koje je koje
Imenovane konstante poput ključeva klasa keychain stavki i CoreFoundation boolean singleton-a izvezene su promenljive čiji sadržaj jeste CFStringRef ili CFBooleanRef koji želite. dlsym vraća adresu te promenljive, pa morate jednom dereferencirati da dobijete vrednost. Strukture tabela callback-ova poput callback-ova ključa i vrednosti rečnika izvezene su strukture, i dlsym vraća adresu strukture, što je upravo pointer koji funkcija za stvaranje rečnika očekuje. Dereferencirajte taj i prosleđujete prvu mašinsku reč strukture kao da je pointer
Ni jedna greška ne proizvodi grešku kompajliranja, i nijedna ne proizvodi jasnu runtime grešku. Dobijate otpadni pointer koji padne negde nizvodno. Način da razliku učinite nemogućom za pogrešiti jeste da prestanete da se oslanjate na pamćenje: dve pomoćne funkcije, jedna koja vezuje i dereferencira i jedna koja vezuje a ne dereferencira, pa mesto poziva deklariše koju vrstu simbola traži, a pomoćnik nameće ostalo
// Izvezena promenljiva: dlsym daje adresu promenljive koja drži
// CFTypeRef, pa dereferenciraj jednom
FSecClassKey := BindConstant(SecurityLib, 'kSecClass');
// Izvezena struktura: dlsym daje adresu STRUKTURE, što je ono
// što API želi. Ne dereferenciraj
FKeyCallbacks := BindStruct(CoreFoundationLib,
'kCFTypeDictionaryKeyCallBacks');
Zašto RSA-PSS potpis treba dva odvojena fallback-a?
Zato što algoritam može nedostajati na dva nezavisna načina, i samo jedan od njih je pitanje verzije. Konstanta PSS digest-signing algoritma pojavila se u macOS 10.13, pa na starijem sistemu simbol jednostavno nije tamo i vezivanje dobija nil. To je provera verzije. Odvojeno, na sistemu gde konstanta postoji, konkretan ključ može je i dalje odbiti, i framework na to pitanje odgovara kroz SecKeyIsAlgorithmSupported za taj ključ. Ključ podržan hardverom ili ključ sa ograničavajućim atributima može da odbije PSS dok softverski ključ na istoj mašini prihvata
Oba puta moraju voditi u isti fallback: prebacite na PKCS#1 v1.5. I kritični deo je da fallback mora da promeni i identifikator algoritma upisan u CMS strukturu, a ne samo poziv potpisivanja. Emitovanje PSS identifikatora algoritma uz stvarno proizvođenje v1.5 potpisa daje dokument koji svaki verifikator odbija u startu, što je strogo gore od prijave da PSS nije podržan. Downgrade je prihvatljiv, nesklad između onoga što deklarišete i onoga što ste uradili nije, i to je opšte pravilo za kod potpisa, a ne macOS ćud. Implikacije na nivou potpisa izložene su u članku o potpisivanju PDF-ova sa PAdES B-B
ECDSA enkodiranje potpisa i obrt vredan pomena
Putanja eliptičkih krivih ne treba nikakvu konverziju na macOS-u, i to je suprotno od onoga što vezivanje PKCS#11 zahteva. Security framework digest-signing algoritam za ECDSA vraća potpis već u X9.62 DER obliku, što je baš ono što CMS hoće. PKCS#11 token umesto toga vraća sirovi fiksne širine P1363 par, koji se mora ponovo enkodirati pre nego što uđe u strukturu potpisa
Dakle, dva backend-a koja implementiraju isti interfejs trebaju suprotan tretman za isti algoritam, i nijedan nije pogrešan. Ovo je baš vrsta razlike koju apstrakcija mora da upije, a ne da je izloži: PAdES sloj traži od provider-a da potpiše, i konvencije enkodiranja ostaju unutar provider-a. Ako procure naviše, svaki pozivalac završi noseći uslov po backend-u. Isti oblik pojavljuje se u priči o daljinskom potpisivanju opisanoj u članku o daljinskim PAdES sesijama potpisivanja naspram HSM-a
// Provider interfejs je isti na svakoj platformi, pa je izbor
// odluka pri pokretanju, a ne po pozivu
{$IFDEF DARWIN}
if KeychainAvailable then
ConfigureKeychainSignerProvider;
{$ENDIF}
{$IFDEF MSWINDOWS}
// Windows CNG provider instalira platformski unit
{$ENDIF}
if not PadesCryptoAvailable then
raise Exception.Create('no signing backend on this platform');
// Odavde je kod potpisivanja nezavistan od platforme
Signer := ResolvePadesSigner(Options);
Pravila brojanja referenci koja sede tri linije razmaknuta
Upravljanje memorijom Core Foundation-a prati konvencije imenovanja, i zamka je što funkcije sa različitim konvencijama stoje jedne uz druge u istom kratkom bloku. Funkcija koja dobija sertifikat iz trust objekta vraća pozajmljenu referencu koja se ne sme osloboditi. Funkcije koje kopiraju sertifikat potpisivača ili kopiraju njegove podatke vraćaju posedničke reference koje se moraju osloboditi. Tri poziva zaredom, dva pravila vlasništva, i oslobađanje pozajmljene ne pada na toj liniji. Ona korumpira retain brojač i obara nešto nepovezano kasnije
Ublažavanje je da se glagol u imenu svake framework funkcije pročita pre pisanja čišćenja, svaki put, bez izuzetka. To je CoreFoundation pandan provere da li API vraća kopiju ili prikaz, i cena pogreške je isprekidan pad, a ne greška
Šta ovaj backend ne tvrdi
Nikada nije radio na macOS-u u trenutku pisanja, i reći to ravno je korisnije od podrazumevane utehe. Dokazivo istinito je uže, ali i dalje vredno: unit se kompajlira na Windows-u kao deo dnevnog build-a, svaki framework simbol vezuje se imenom u vremenu izvršavanja sa izbrojanim padovima, i logika izbora algoritma uključujući oba PSS fallback-a je običan Pascal koji se može pregledati i o kome se može rasuđivati. Prvo pokretanje na Mac-u ili će raditi ili će proizvesti spisak imena za ispravku
Verifikacioni pandan, koji koristi viši CMS dekoder umesto ručnog sklapanja CMS strukture, pokriven je u članku o verifikaciji PDF potpisa na macOS-u sa SecTrust-om, i deli istu infrastrukturu vezivanja i isti dijagnostički pristup
Prenosiva ideja ovde je o smeštanju rizika, a ne o macOS-u. Kad morate pisati kod naspram interfejsa koji ne možete verifikovati, izaberite konstrukciju u kojoj su greške najjeftinije za lociranje. Dinamičko vezivanje sa eksplicitnim spiskom nerazrešenih imena pretvara dvadeset neverifikabilnih pretpostavki u jednu dijagnostičku liniju. Oba backend-a stižu kao izvor uz PDFium Delphi komponentu, pa ako ime simbola zaista treba ispraviti, to je izmena jedne linije u vašem stablu, a ne tiket podršci