Articol tehnic

Semnarea PAdES cu o identitate din Keychain macOS în Delphi

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

Diagramă a backend-ului Keychain macOS din PDFium VCL care rezolvă simbolurile Security și CoreFoundation prin dlsym: kSecClass e o variabilă exportată pe care BindConstant o dereferențiază o dată pentru a obține valoarea CFStringRef, în timp ce kCFTypeDictionaryKeyCallBacks e o structură exportată pe care BindStruct o transmite prin adresă, iar amestecarea celor două reguli produce pointeri de gunoaie aval
Un singur apel dlsym întoarce două lucruri categoric diferite: adresa unei variabile care ține un CFTypeRef și adresa unei structuri de callback. Două helper-uri iau decizia de dereferențiere sau nu la locul binding-ului, nu în memorie
// 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

Lanț de decizie care arată de ce semnarea RSA-PSS în backend-ul Keychain PDFium VCL are nevoie de două rezerve independente: dlsym întoarce nil pentru constanta de semnare a digestului pe versiunile macOS anterioare lui 10.13, SecKeyIsAlgorithmSupported poate refuza o cheie suportată hardware, iar ambele porți se varsă în același downgrade PKCS#1 v1.5 al cărui identificator de algoritm CMS trebuie să se schimbe odată cu el
PSS poate fi indisponibil de două ori, o dată per versiune macOS și o dată per cheie, iar doar poarta de versiune e o întrebare de sistem. Ambele porți se varsă în același downgrade v1.5, iar identificatorul CMS îl urmează

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

Comparație a encodării semnăturilor ECDSA între două backend-uri ale signer-ului PAdES PDFium VCL: framework-ul Security din Keychain macOS întoarce X9.62 DER pe care CMS îl acceptă fără nicio conversie, în timp ce un token PKCS#11 întoarce perechea brută P1363 cu lățime fixă care trebuie reencodată, deci ResolvePadesSigner ține convențiile de encodare în interiorul provider-ului
Aceeași interfață ECDSA are nevoie de tratament opus per backend: Security preda DER gata făcut, în timp ce un token PKCS#11 preda P1363 brut, deci conversia stă în interiorul provider-ului, iar apelanții nu văd niciodată o condiționată per backend
// 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