PDFium VCL semnează documente PAdES cu o cheie privată ținută în Keychain-ul macOS printr-un backend care rezolvă fiecare simbol Security și CoreFoundation la runtime cu dlopen și dlsym. Nimic nu e legat la timp de linkare, ceea ce înseamnă că un nume de simbol scris greșit iese la suprafață ca KeychainAvailable întorcând False și KeychainMissingSymbols numind vinovatul, nu ca o eroare de linker sau un crash
Alegerea aceasta a fost forțată de o constrângere neplăcută, iar felul în care a fost tratată se generalizează. Unitatea a fost scrisă pe o mașină fără SDK macOS, deci fiecare nume de simbol de framework și fiecare constantă au venit din documentație și nimic nu putea fi verificat contra unui header. Răspunsul greșit la situația aceea e să scrii codul cu grijă și să speri. Cel corect e să aranjezi ca greșelile inevitabile să se anunțe în forma cât mai localizabilă posibil
De ce legarea dinamică e decizia corectă chiar și pe platforma țintă
Pentru că transformă o clasă de eșec care oprește programul într-o clasă de eșec care se raportează singură. O referință static legată spre un framework, greșită, pică la linkare pe țintă și nu se leagă nicăieri altundeva. Una legată dinamic, greșită, produce un backend indisponibil și o listă de nume nerezolvate, iar prima rulare pe un Mac transformă întrebarea din „de ce e indisponibil?” într-o singură linie care numește un typo
Există un al doilea beneficiu care se plătește zilnic, nu o dată. Pentru că unitatea nu leagă niciun framework, se compilează pe fiecare platformă, deci build-ul obișnuit Windows îi verifică în continuare sintaxa, tipurile și clauza uses. O unitate care se compilează doar pe o platformă pe care nimeni din echipă n-o are e o unitate fără niciun compilator care s-o privească și degenerează tăcut la fiecare refactor al unui tip partajat
uses
FPdfCrypto, FPdfCryptoMac;
var
Options: TPadesSignerOptions;
begin
if not KeychainAvailable then
raise Exception.Create('Keychain backend unavailable, unresolved: ' +
KeychainMissingSymbols);
ConfigureKeychainSignerProvider; // instalează drept backend de semnare PAdES
ConfigureKeychainCmsVerifier; // și drept backend de verificare
Writeln('signer backend : ', PadesCryptoBackendName);
Writeln('verify backend : ', PadesCmsVerificationBackendName);
Options := TPadesSignerOptions.Default;
Options.CertificateThumbprint := 'B1 3F 9C ...'; // SHA-1, orice majuscule
Options.PaddingScheme := psRsaPss;
end;
Două feluri de simbol exportat, două feluri de a le citi
Acesta e cel mai confuz detaliu din tot binding-ul, iar a-l lua invers se compilează curat și pică la runtime. CoreFoundation și Security exportă două lucruri categoric diferite prin același apel dlsym, iar codul trebuie să știe care e care
Constantele numite, precum cheile de clasă de element keychain și singleton-urile booleene CoreFoundation, sunt variabile exportate al căror conținut e CFStringRef-ul sau CFBooleanRef-ul dorit. dlsym întoarce adresa acelei variabile, deci trebuie să dereferențiați o dată ca să obțineți valoarea. Structurile de tabele de callback, precum callback-urile de cheie și valoare ale dicționarului, sunt structuri exportate, iar dlsym întoarce adresa structurii, adică exact pointerul pe care funcția de creare a dicționarului îl așteaptă. Dereferențiați pe acela și passați primul cuvânt de mașină al structurii ca și cum ar fi un pointer
Niciuna dintre greșeli nu produce o eroare de compilare, iar niciuna nu produce o eroare de runtime clară. Primiți un pointer de gunoaie care pică undeva aval. Felul de a face distincția imposibil de greșit e să încetați să vă bazați pe memorarea ei: două funcții ajutătoare, una care leagă și dereferențiază și una care leagă fără să dereferențieze, astfel încât locul apelului declară ce fel de simbol cere, iar helper-ul impune restul
// Variabilă exportată: dlsym dă adresa unei variabile care ține
// CFTypeRef-ul, deci dereferențiați o dată
FSecClassKey := BindConstant(SecurityLib, 'kSecClass');
// Structură exportată: dlsym dă adresa STRUCTURII, ceea ce vrea
// API-ul. Nu dereferențiați
FKeyCallbacks := BindStruct(CoreFoundationLib,
'kCFTypeDictionaryKeyCallBacks');
De ce are nevoie o semnătură RSA-PSS de două rezerve separate?
Pentru că algoritmul poate lipsi în două feluri independente, iar doar unul dintre ele e o întrebare de versiune. Constanta algoritmului de semnare a digestului PSS a apărut în macOS 10.13, deci pe un sistem mai vechi simbolul pur și simplu nu e acolo, iar binding-ul primește nil. Aceea e verificarea de versiune. Separat, pe un sistem unde constanta există, o anume cheie poate să o refuze totuși, iar framework-ul răspunde la întrebarea aceea prin SecKeyIsAlgorithmSupported pentru cheia respectivă. O cheie suportată hardware sau una cu atribute restrictive poate refuza PSS, în timp ce o cheie software pe aceeași mașină o acceptă
Ambele căi trebuie să conducă la aceeași rezervă: comutarea pe PKCS#1 v1.5. Iar partea critică e că rezerva trebuie să schimbe și identificatorul de algoritm scris în structura CMS, nu doar apelul de semnare. Emiterea unui identificator de algoritm PSS în timp ce produceți efectiv o semnătură v1.5 dă un document pe care orice verificator îl respinge categoric, ceea ce e strict mai rău decât a raporta că PSS nu e suportat. Un downgrade e acceptabil, o nepotrivire între ce declarați și ce ați făcut nu e, iar asta e o regulă generală pentru codul de semnături, nu o ciudățenie macOS. Implicațiile la nivel de semnătură sunt prezentate în semnarea PDF-urilor cu PAdES B-B
Encodarea semnăturilor ECDSA și o inversare care merită notată
Calea pe curbe eliptice nu are nevoie de nicio conversie pe macOS, iar asta e opusul a ce cere un binding PKCS#11. Algoritmul de semnare a digestului din framework-ul Security pentru ECDSA întoarce semnătura deja în formă X9.62 DER, exact ce vrea CMS. Un token PKCS#11 întoarce în schimb perechea brută P1363 cu lățime fixă, care trebuie reencodată înainte să intre într-o structură de semnătură
Deci două backend-uri care implementează aceeași interfață au nevoie de tratament opus pentru același algoritm, iar niciunul nu e greșit. Exact acest fel de diferență trebuie să o absoarbă o abstracție, nu să o expună: stratul PAdES cere unui provider să semneze, iar convențiile de encodare rămân în interiorul provider-ului. Dacă se scurg în sus, fiecare apelant ajunge să care o condiționată per backend. Aceeași formă apare în povestea semnării remote descrisă în sesiunile de semnare PAdES remote contra unui HSM
// Interfața de provider e aceeași pe fiecare platformă, deci selecția
// e o decizie de pornire, nu una per apel
{$IFDEF DARWIN}
if KeychainAvailable then
ConfigureKeychainSignerProvider;
{$ENDIF}
{$IFDEF MSWINDOWS}
// Provider-ul Windows CNG e instalat de unitatea platformei
{$ENDIF}
if not PadesCryptoAvailable then
raise Exception.Create('no signing backend on this platform');
// De aici codul de semnare e neutru de platformă
Signer := ResolvePadesSigner(Options);
Reguli de counting de referințe aflate la trei linii distanță
Gestionarea memoriei Core Foundation urmează convenții de numire, iar capcana aici e că funcții cu convenții diferite apar una lângă alta în același bloc scurt. O funcție care ia un certificat dintr-un obiect de încredere întoarce o referință împrumutată care nu trebuie eliberată. Funcțiile care copiază un certificat de semnatar sau îi copiază datele întorc referințe deținute care trebuie eliberate. Trei apeluri la rând, două reguli de proprietate, iar eliberarea celei împrumutate nu pică la linia aceea. Corupe un contor de retain și doboară ceva fără legătură mai târziu
Atenuarea e să citiți verbul în numele fiecărei funcții de framework înainte să scrieți curățenia, de fiecare dată, fără excepție. E echivalentul CoreFoundation al verificării dacă un API întoarce o copie sau o vedere, iar costul greșelii e un crash intermitent, nu o eroare
Ce nu pretinde acest backend
Niciodată n-a rulat pe macOS la momentul scrierii, iar a spune asta limpede e mai util decât o asigurare implicită. Ce e demonstrabil adevărat e mai îngust și tot valoros: unitatea se compilează pe Windows ca parte a build-ului zilnic, fiecare simbol de framework e legat pe nume la runtime cu eșecurile enumerate, iar logica de selecție a algoritmului, inclusiv ambele rezerve PSS, e Pascal obișnuit care poate fi revizuit și raționat. Prima rulare pe un Mac va funcționa ori va produce o listă de numere de corectat
Omologul de verificare, care folosește decoder-ul CMS de nivel mai înalt în loc să asambleze structura CMS de mână, e acoperit în verificarea semnăturilor PDF pe macOS cu SecTrust, și partajează aceeași infrastructură de binding și aceeași abordare de diagnostic
Ideea transferabilă de aici ține de plasarea riscului, nu de macOS. Când trebuie să scrieți cod contra unei interfețe pe care nu o puteți verifica, alegeți construcția în care greșelile sunt cele mai ieftin de localizat. Legarea dinamică cu o listă explicită de nume nerezolvate transformă douăzeci de presupuneri neverificabile într-o singură linie de diagnostic. Ambele backend-uri se livrează ca sursă cu PDFium Delphi component, deci dacă vreun nume de simbol chiar trebuie corectat, e o schimbare de o linie în propriul arbore, nu un tichet de suport