Tehnički članak

Potpisivanje PAdES-a macOS Keychain identitetom u Delphi-ju

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

Dijagram macOS Keychain backend-a PDFium VCL koji razrešava Security i CoreFoundation simbole kroz dlsym: kSecClass je izvezena promenljiva koju BindConstant jednom dereferencira da dobije CFStringRef vrednost, dok je kCFTypeDictionaryKeyCallBacks izvezena struktura koju BindStruct prosleđuje po adresi, i mešanje ta dva pravila daje otpadne pointere nizvodno
Jedan dlsym poziv vraća dve kategorijalno različite stvari: adresu promenljive koja drži CFTypeRef i adresu strukture callback-ova. Dva pomoćnika donose odluku o dereferenciranju na mestu vezivanja umesto u memoriji
// 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

Lanac odlučivanja koji pokazuje zašto RSA-PSS potpisivanje u Keychain backend-u PDFium VCL treba dva nezavisna fallback-a: dlsym vraća nil za konstantu digest-signing algoritma na macOS verzijama pre 10.13, SecKeyIsAlgorithmSupported može odbiti ključ podržan hardverom, i obe kapije se slevaju u isti PKCS#1 v1.5 downgrade čiji se CMS identifikator algoritma mora menjati uz njega
PSS može biti nedostupan dvostruko, jednom po macOS verziji i jednom po ključu, i samo kapija verzije je sistemsko pitanje. Obe kapije se slevaju u isti v1.5 downgrade, i CMS identifikator prati

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

Poređenje ECDSA enkodiranja potpisa kroz dva backend-a PAdES potpisivača PDFium VCL: macOS Keychain Security framework vraća X9.62 DER koji CMS prihvata bez ijedne konverzije, dok PKCS#11 token vraća sirovi fiksne širine P1363 par koji se mora ponovo enkodirati, pa ResolvePadesSigner drži konvencije enkodiranja unutar provider-a
Isti ECDSA interfejs treba suprotan tretman po backend-u: Security uručuje gotov DER dok PKCS#11 token uručuje sirovi P1363, pa konverzija živi unutar provider-a i pozivaoci nikada ne vide uslov po backend-u
// 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