Tehnički članak

Potpisivanje PAdES-a macOS Keychain identitetom u Delphiju

PDFium VCL potpisuje PAdES dokumente privatnim ključem pohranjenim u macOS Keychain kroz backend koji razrješuje svaki Security i CoreFoundation simbol u runtimeu s dlopen i dlsym. Ništa nije link-time vezano, što znači da krivo tipkano ime simbola ispliva kao KeychainAvailable koji vraća False i KeychainMissingSymbols koji imenuje krivca, umjesto kao linkerska greška ili crash

Taj je izbor bio prisiljen neugodnim ograničenjem, i način na koji je obrađen se generalizira. 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 provjeriti protiv headera. Krivi odgovor na tu situaciju je pažljivo napisati kod i nadati se. Pravi je urediti da neizbježne greške same najave sebe u najlakše locirajućem obliku

Zašto je dinamičko vezivanje pravi poziv čak i na ciljnoj platformi

Jer pretvara klasu neuspjeha koja zaustavlja program u klasu neuspjeha koja sama sebe javlja. Statički linkana referenca na framework koja je kriva pade u trenutku linkanja na cilju i nikad se ne linka drugdje. Dinamički vezana koja je kriva proizvodi nedostupan backend i popis nerazriješenih imena, i prvo pokretanje na Macu pretvara pitanje iz zašto je ovo nedostupno u jednu liniju koja imenuja tipfeler

Tu je druga korist koja se isplati svakodnevno umjesto jednom. Budući da unit ne linka nijedan framework, kompajlira se na svakoj platformi, pa obični Windows build i dalje provjerava njegovu sintaksu, njegove tipove i njegov uses clause. Unit koji se kompajlira samo na platformi koju nitko u timu nema je unit kojeg nijedan kompajler ne gleda, i truli tiho sa svakim refaktorom dijeljenog 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, nebitna velika/mala
  Options.PaddingScheme := psRsaPss;
end;

Dvije vrste izvezenog simbola, dva načina čitanja

Ovo je pojedinačno najzbunjujući detalj u cijelom vezivanju, i poremetiti ga se kompajlira čisto a pade u runtimeu. CoreFoundation i Security izvoze dvije 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 singletona su izvezene varijable čiji je sadržaj CFStringRef ili CFBooleanRef koje želite. dlsym vraća adresu te varijable, pa morate jednom dereferencirati da dobijete vrijednost. Strukture tablica callbackova poput dictionary key i value callbackova su izvezene strukture, i dlsym vraća adresu strukture, što je upravo pokazivač koji funkcija stvaranja dictionaryja očekuje. Dereferencirate li tu, predajete prvu mašinsku riječ strukture kao da je pokazivač

Nijedna greška ne proizvodi compile grešku, i nijedna ne proizvodi jasnu runtime grešku. Dobijete garbage pokazivač koji pade negdje nizvodno. Način da razliku učinite nemogućom za pogriješiti je prestati oslanjati se na pamćenje: dvije helper funkcije, jedna koja veže i dereferencira i jedna koja veže i ne dereferencira, pa call site deklarira koje vrste simbola traži a helper nameće ostalo

Dijagram macOS Keychain backenda PDFium VCL-a koji razrješuje Security i CoreFoundation simbole kroz dlsym: kSecClass je izvezena varijabla koju BindConstant jednom dereferencira da dobije CFStringRef vrijednost, dok je kCFTypeDictionaryKeyCallBacks izvezena struktura koju BindStruct predaje po adresi, i miješanje ta dva pravila daje garbage pokazivače nizvodno
Jedan dlsym poziv vraća dvije kategorijalno različite stvari: adresu varijable koja drži CFTypeRef i adresu strukture callbackova. Dva helpera donose odluku dereferencirati ili ne na mjestu vezivanja umjesto u memoriji
// Izvezena varijabla: dlsym daje adresu varijable koja drži
// CFTypeRef, pa jednom dereferencirajte
FSecClassKey := BindConstant(SecurityLib, 'kSecClass');

// Izvezena struktura: dlsym daje adresu STRUKTURE, a to je
// ono što API želi. Nemojte dereferencirati
FKeyCallbacks := BindStruct(CoreFoundationLib,
  'kCFTypeDictionaryKeyCallBacks');

Zašto RSA-PSS potpis treba dva odvojena fallbacka?

Jer algoritam može nedostajati na dva neovisna načina, i samo jedan od njih je pitanje verzije. PSS digest-signing algoritamska konstanta pojavila se u macOS 10.13, pa na starijem sustavu simbol jednostavno nije tamo i vezivanje dobiva nil. To je provjera verzije. Odvojeno, na sustavu gdje konstanta postoji, određeni ključ može je ipak odbiti, i framework odgovara na to pitanje kroz SecKeyIsAlgorithmSupported za taj ključ. Hardverski poduprt ključ ili ključ s restriktivnim atributima može odbiti PSS dok ga softverski ključ na istoj mašini prima

Oba puta moraju dovesti do istog fallbacka: prebaciti na PKCS#1 v1.5. I kritični dio je da fallback mora promijeniti i identifikator algoritma zapisan u CMS strukturu, a ne samo potpisni poziv. Emitirati PSS identifikator algoritma dok stvarno proizvodite v1.5 potpis daje dokument koji svaki verifier odmah odbija, što je strogo gore nego javiti da PSS nije podržan. Downgrade je prihvatljiv, neslaganje između onoga što deklarirate i onoga što ste učinili nije, i to je opće pravilo za potpisni kod a ne macOS čudnost. Implikacije na razini potpisa razložene su u potpisivanju PDF-ova s PAdES B-B

Lanac odluka pokazuje zašto RSA-PSS potpisivanje u PDFium VCL Keychain backendu treba dva neovisna fallbacka: dlsym vraća nil za digest-signing konstantu na macOS verzijama prije 10.13, SecKeyIsAlgorithmSupported može odbiti hardverski poduprt ključ, i obje kapi se slijevaju u isti PKCS#1 v1.5 downgrade čiji se CMS identifikator algoritma mora promijeniti s njim
PSS može biti nedostupan dvaput, jednom po macOS verziji i jednom po ključu, i samo je verzija vrata sistemsko pitanje. Obje kapi se slijevaju u isti v1.5 downgrade, i CMS identifikator prati

ECDSA encoding potpisa, i obrat koji vrijedi primijetiti

Put eliptičkih krivulja ne treba nikakvu konverziju na macOS-u, a to je suprotno od onoga što PKCS#11 vezivanje traži. Security framework digest-signing algoritam za ECDSA vraća potpis već u X9.62 DER obliku, što je točno ono što CMS želi. PKCS#11 token vraća umjesto toga sirovi fiksne širine P1363 par, koji se mora ponovno kodirati prije nego uđe u strukturu potpisa

Dakle dva backenda koja implementiraju isti interface trebaju suprotan tretman za isti algoritam, i nijedan nije kriv. To je točno ona vrsta razlike koju apstrakcija mora apsorbirati umjesto izložiti: PAdES sloj traži od providera da potpiše, i encoding konvencije ostaju unutar providera. Ako iscure naviše, svaki pozivatelj završi noseći per-backend uvjetni. Isti oblik se pojavljuje u priči o udaljenom potpisivanju opisanoj u udaljenim PAdES signing sesijama protiv HSM-a

Usporedba ECDSA encodinga potpisa kroz dva backenda PDFium VCL PAdES signera: macOS Keychain Security framework vraća X9.62 DER koji CMS prima bez ičake konverzije, dok PKCS#11 token vraća sirovi fiksne širine P1363 par koji se mora ponovno kodirati, pa ResolvePadesSigner drži encoding konvencije unutar providera
Isti ECDSA interface treba suprotan tretman po backendu: Security predaje gotov DER dok PKCS#11 token predaje sirovi P1363, pa konverzija živi unutar providera i pozivatelji nikad ne vide per-backend uvjetni
// Provider interface je isti na svakoj platformi, pa je izbor
// startup odluka umjesto 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');

// Odtud je potpisni kod platformski neutralan
Signer := ResolvePadesSigner(Options);

Pravila reference countinga koja sjede tri linije udaljena

Core Foundation upravljanje memorijom slijedi naming konvencije, i zamka je da se funkcije s različitim konvencijama pojavljuju jedne pokraj drugih u istom kratkom bloku. Funkcija koja dobavlja certifikat iz trust objekta vraća posuđenu referencu koja se ne smije otpustiti. Funkcije koje certifikat potpisnika kopiraju ili kopiraju njegove podatke vraćaju vlasničke reference koje se moraju otpustiti. Tri poziva zaredom, dva pravila vlasništva, i otpuštanje posuđene ne pade na toj liniji. Pokvari retain count i obori nešto nepovezano kasnije

Ublažavanje je pročitati glagol u imenu svake framework funkcije prije pisanja čišćenja, svaki put, bez izuzetka. To je CoreFoundation pandan provjeri vraća li API kopiju ili pogled, i cijena pogreške je isprekidani crash umjesto greške

Što ovaj backend ne tvrdi

Nikad nije trčao na macOS-u u trenutku pisanja, i reći to otvoreno korisnije je od impliciranog uvjeravanja. Ono što je dokazivo istina je uže i još uvijek vrijedno: unit se kompajlira na Windowsima kao dio dnevnog builda, svaki framework simbol se veže po imenu u runtimeu s enumeriranim neuspjesima, i logika izbora algoritma uključujući oba PSS fallbacka je običan Pascal koji se može pregledati i o njemu razmišljati. Prvo pokretanje na Macu ili će raditi ili će proizvesti popis imena za popraviti

Verifikacijski pandan, koji koristi višu razinu CMS dekoder umjesto da sastavlja CMS strukturu rukom, pokriven je u verificiranju PDF potpisa na macOS-u sa SecTrustom, i dijeli istu vezivnu infrastrukturu i isti dijagnostički pristup

Prenosiva ideja ovdje je o smještanju rizika umjesto o macOS-u. Kad morate pisati kod protiv interfacea koji ne možete verificirati, odaberite konstrukciju gdje su greške najjeftinije locirati. Dinamičko vezivanje s eksplicitnim popisom nerazriješenih imena pretvara dvadeset neverifikabilnih pretpostavki u jednu dijagnostičku liniju. Oba backenda isporučuju se kao source uz PDFium Delphi komponentu, pa ako ime simbola doista treba ispraviti, to je promjena jedne linije u vlastitom stablu umjesto support tiketa