PDFium VCL pasirašo PAdES dokumentus privačiu raktu, saugomu macOS Keychain, per posistemę, kuri vykdymo metu iškviečia kiekvieną Security ir CoreFoundation simbolį per dlopen ir dlsym. Niekas nesusieta linkinimo metu, tad klaidingai įvestas simbolio vardas pasireiškia ne kaip linker klaida ar avarija, o kaip KeychainAvailable, grąžinantis False, ir KeychainMissingSymbols, įvardijantis kaltininką
Šį pasirinkimą lėmė nepatogus apribojimas, o būdas, kuriuo jis suvaldytas, tinka ir platesniam atvejui. Modulis rašytas mašinoje be macOS SDK, tad kiekvienas framework simbolio vardas ir kiekviena konstanta paimti iš dokumentacijos, ir nieko nebūvo galima pasitikrinti pagal header failą. Neteisingas atsakas į tokią situaciją yra rašyti kodą atidžiai ir tikėtis. Teisingas — paruošti taip, kad neišvengiamos klaidos paskelbtų apie save pačia lengviausiai randama forma
Kodėl dinaminis susiejimas yra teisingas pasirinkimas net ir tikslinėje platformoje
Nes jis klaidų klasę, sustabdančią programą, paverčia klaidų klase, kuri praneša apie save pati. Neteisinga statiškai susieta framework nuoroda krinta linkinimo metu tikslinėje platformoje ir niekur kitur nesusieja. Neteisinga dinamiškai susieta duoda neprieinamą posistemę ir neišspręstų vardų sąrašą, o pirmas paleidimas Mac kompiuteryje klausimą „kodėl neprieinama“ paverčia viena eilute, įvardijančia rašybos klaidą
Yra ir antra nauda, kuri atsiperka kasdien, o ne vieną kartą. Kadangi modulis nesusieja nė vieno framework, jis kompiliuojasi kiekvienoje platformoje, tad įprasta Windows statyba toliau tikrina jo sintaksę, tipus ir uses skyrių. Modulis, kompiliuojamasis tik platformoje, kurios neturi nė vienas komandos narys, yra modulis, į kurį nežiūri nė vienas kompiliatorius, ir jis tyliai irsta su kiekvienu bendro tipo refactor
uses
FPdfCrypto, FPdfCryptoMac;
var
Options: TPadesSignerOptions;
begin
if not KeychainAvailable then
raise Exception.Create('Keychain backend unavailable, unresolved: ' +
KeychainMissingSymbols);
ConfigureKeychainSignerProvider; // instaliuoti kaip PAdES pasirašymo posistemę
ConfigureKeychainCmsVerifier; // ir kaip tikrinimo posistemę
Writeln('signer backend : ', PadesCryptoBackendName);
Writeln('verify backend : ', PadesCmsVerificationBackendName);
Options := TPadesSignerOptions.Default;
Options.CertificateThumbprint := 'B1 3F 9C ...'; // SHA-1, bet koks registras
Options.PaddingScheme := psRsaPss;
end;
Du eksportuotų simbolių tipai, du būdai juos skaityti
Tai pats painiausias viso susiejimo niuansas: supainiojus juos, kodas kompiliuojasi švariai, o krinta tik vykdymo metu. CoreFoundation ir Security per tą patį dlsym iškvietimą eksportuoja du kategoriškai skirtingus dalykus, ir kodas turi žinoti, kuris kuris
Įvardintos konstantos, tokios kaip keychain elementų klasių raktai ir CoreFoundation boolean singletonai, yra eksportuoti kintamieji, kurių turinys yra norimas CFStringRef arba CFBooleanRef. dlsym grąžina to kintamojo adresą, tad norėdami gauti reikšmę, turite vieną kartą dereferencuoti. Callback lentelės struktūros, tokios kaip žodyno rakto ir reikšmės callbackai, yra eksportuotos struktūros, ir dlsym grąžina pačios struktūros adresą — būtent tą rodyklę, kurios tikisi žodyno kūrimo funkcija. Dereferencavus šią, į funkciją perduodate pirmą struktūros mašininį žodį, lyg tai būtų rodyklė
Nė viena klaida neduoda kompiliavimo klaidos, ir nė viena neduoda aiškios vykdymo klaidos. Gaunate rodyklę į atminties šiukšles, kuri nulūžta kažkur toliau. Būdas padaryti, kad šio skirtumo būtų neįmanoma supainioti, yra nustoti pasikliauti atmintimi: dvi pagalbinės funkcijos, viena susieja ir dereferencuoja, kita susieja ir nedereferencuoja, tad iškvietimo vieta deklaruoja, kokio tipo simbolio prašo, o pagalbinė funkcija užtikrina likusį
// Eksportuotas kintamasis: dlsym duoda kintamojo, laikančio CFTypeRef,
// adresą, tad dereferencuokite vieną kartą
FSecClassKey := BindConstant(SecurityLib, 'kSecClass');
// Eksportuota struktūra: dlsym duoda pačios struktūros adresą, ir būtent
// to API ir nori. Nedereferencuokite
FKeyCallbacks := BindStruct(CoreFoundationLib,
'kCFTypeDictionaryKeyCallBacks');
Kodėl RSA-PSS parašui reikia dviejų atskirų atsarginių kelių?
Nes algoritmas gali dingti dviem nepriklausomais būdais, ir tik vienas jų yra versijos klausimas. PSS digest-signing algoritmo konstanta atsirado macOS 10.13, tad senesnėje sistemoje simbolio tiesiog nėra, ir susiejimas gauna nil. Tai yra versijos patikra. Atskirai: sistemoje, kurioje konstanta yra, konkretus raktas vis tiek gali jos atsisakyti, ir framework į tą klausimą atsako per SecKeyIsAlgorithmSupported konkrečiam raktui. Aparatu saugomas raktas arba raktas su ribojančiais atributais gali atmesti PSS, kai programinis raktas toje pačioje mašinoje jį priima
Abu keliai turi privesti prie to paties atsarginio kelio: persijungti į PKCS#1 v1.5. Kritinė dalis yra ta, kad atsarginis kelias turi pakeisti ir į CMS struktūrą rašomą algoritmo identifikatorių, ne tik patį pasirašymo iškvietimą. PSS algoritmo identifikatoriaus išdavimas, kol iš tikrųjų gaminamas v1.5 parašas, duoda dokumentą, kurį kiekvienas tikrintuvas atmeta iš karto, ir tai griežtai blogiau nei pranešti, kad PSS nepalaikomas. Pažeminimas yra priimtinas, nesutapimas tarp to, ką deklaruojate, ir to, ką padarėte, nėra, ir tai bendra taisyklė parašų kodui, o ne macOS ypatybė. Parašo lygmens išvados išdėstytos straipsnyje PDF pasirašymas su PAdES B-B
ECDSA parašo kodavimas ir vertas dėmesio priešingybė
Elipsinių kreivių kelias macOS nereikalauja jokios konversijos, ir tai priešinga tai, ko reikalauja PKCS#11 susiejimas. Security framework digest-signing algoritmas ECDSA grąžina parašą jau X9.62 DER forma, ir būtent to nori CMS. PKCS#11 tokenas vietoj to grąžina žalią fiksuoto pločio P1363 porą, kurią reikia perkoduoti, kol ji patenka į parašo struktūrą
Tad dvi posistemės, realizuojančios tą pačią sąsają, tam pačiam algoritmui reikalauja priešingo elgesio, ir nė viena iš jų nėra neteisinga. Tai būtent tas skirtumas, kurį abstrakcija turi sugerti, o ne atiduoti į išorę: PAdES sluoksnis prašo providerio pasirašyti, o kodavimo konvencijos lieka providerio viduje. Jei jos nuteka į viršų, kiekvienas kvietėjas gauna po sąlygą, priklausančią nuo posistemės. Ta pati forma pasikartoja nuotolinio pasirašymo istorijoje, aprašytoje straipsnyje nuotolinės PAdES pasirašymo sesijos su HSM
// Providerio sąsaja visose platformose ta pati, tad pasirinkimas yra
// paleidimo metu priimamas sprendimas, o ne kiekvieno iškvietimo
{$IFDEF DARWIN}
if KeychainAvailable then
ConfigureKeychainSignerProvider;
{$ENDIF}
{$IFDEF MSWINDOWS}
// Windows CNG providerį instaliuoja platformos modulis
{$ENDIF}
if not PadesCryptoAvailable then
raise Exception.Create('no signing backend on this platform');
// Nuo čia pasirašymo kodas yra platformų neutralus
Signer := ResolvePadesSigner(Options);
Nuorodų skaitlių taisyklės, atskirtos trijomis eilutėmis
Core Foundation atminties valdymas seka pavadinimų konvencijas, ir spąstai čia yra tie, kad skirtingas konvencijas naudojančios funkcijos atsiranda viena šalia kitos tame pačiame trumpame bloke. Funkcija, kuri paima sertifikatą iš trust objekto, grąžina pasiskolintą nuorodą, kurios atlaisvinti negalima. Funkcijos, kurios kopijuoja pasirašytojo sertifikatą arba kopijuoja jo duomenis, grąžina nuosavas nuorodas, kurias reikia atlaisvinti. Trys iškvietimai iš eilės, dvi nuosavybės taisyklės, o pasiskolintosios atlaisvinimas nesukelia klaidos toje pačioje eilutėje. Jis sugadina retain skaitiklį ir vėliau nulauža kažką visai nesusijusio
Apsauga yra skaityti veiksmažodį kiekvieno framework funkcijos pavadinime prieš rašant išvalymą, kiekvieną kartą, be išimčių. Tai CoreFoundation atitikmuo patikrinimui, ar API grąžina kopiją, ar vaizdą, ir paklaidos kaina čia yra periodinė avarija, o ne klaida
Ko ši posistemė neteigia
Rašymo metu ji niekada nebėgo ant macOS, ir tai pasakyti atvirai naudingiau nei numanoma garantija. Įrodoma tiesa yra siauresnė ir vis tiek vertinga: modulis kompiliuojasi ant Windows kaip kasdienės statybos dalis, kiekvienas framework simbolis vykdymo metu susiejamas vardu su išvardintais nesėkmėmis, o algoritmo pasirinkimo logika, įskaitant abu PSS atsarginius kelius, yra paprastas Pascal, kurį galima peržiūrėti ir apie jį samprotuoti. Pirmas paleidimas ant Mac arba veiks, arba duos vardų sąrašą taisymui
Tikrinimo atitikmuo, naudojantis aukštesnio lygio CMS dekoderiu, o ne CMS struktūros surinkimu rankomis, aprašytas straipsnyje PDF parašų tikrinimas ant macOS su SecTrust, ir jis dalijasi ta pačia susiejimo infrastruktūra bei tokiu pačiu diagnostikos požiūriu
Perkeliamoji čia idėja yra apie rizikos išdėstymą, o ne apie macOS. Kai kodą tenka rašyti prieš sąsają, kurios patikrinti negalite, pasirinkite tokią konstrukciją, kurioje klaidos randamos pigiausiai. Dinaminis susiejimas su aiškiu neišspręstų vardų sąrašu dvidešimt nepatikrinamų prielaidų paverčia viena diagnostikos eilute. Abi posistemės keliauja kaip šaltinis su PDFium Delphi komponentu, tad jei simbolio vardą vis tiek reikia pataisyti, tai vienos eilutės pakeitimas jūsų pačių medyje, o ne palaikymo bilietas