Tehnični članak

PAdES podpisi z identiteto verige ključev macOS v Delphi

PDFium VCL podpisuje dokumente PAdES z zasebnim ključem, shranjenim na verigi ključev macOS, skozi zaledje, ki vsak simbol Security in CoreFoundation razreši med zagonom z dlopen in dlsym. Nič ni vezano ob povezovanju, kar pomeni, da se napačno natipkano ime simbola pokaže kot KeychainAvailable, ki vrne False, in KeychainMissingSymbols, ki poimenuje krivca — in ne kot napaka povezovalnika ali sesutje

To izbiro je vsilila neprijetna omejitev, način ravnanja z njo pa se posplošuje. Enota je bila napisana na stroju brez macOS SDK, tako da je vsako ime simbola ogrodja in vsaka konstanta prišla iz dokumentacije in ničesar ni bilo mogoče preveriti proti glavi. Napačen odgovor na to situacijo je kodu zapisati skrbno in upati. Pravi pa je urediti, da se neizbežne napake javijo same v najbolje umestljivi obliki

Zakaj je dinamično vezanje prava odločitev, tudi na ciljni platformi

Ker razred odpovedi, ki ustavi program, spremeni v razred odpovedi, ki sporoči samo sebe. Statično povezan sklic na ogrodje, ki je napačen, spodleti ob povezovanju na cilju in se nikjer drugje ne poveže. Dinamično vezan, ki je napačen, proizvede nedostopno zaledje in seznam nerazrešenih imen, prvi zagon na Macu pa vprašanje zakaj to ni na voljo spremeni v eno vrstico, ki poimenuje tipkarsko napako

Obstaja še druga korist, ki se izplača dnevno in ne enkrat. Ker enota ne povezuje ogrodij, se prevede na vsaki platformi, običajna gradnja Windows pa še naprej preverja njeno skladnjo, tipe in stavek uses. Enota, ki se prevede le na platformi, ki je nihče v ekipi nima, je enota brez prevajalnika, ki bi jo gledal, in tiho propada z vsako prenovo deljenega tipa

uses
  FPdfCrypto, FPdfCryptoMac;

var
  Options: TPadesSignerOptions;
begin
  if not KeychainAvailable then
    raise Exception.Create('Keychain backend unavailable, unresolved: ' +
      KeychainMissingSymbols);

  ConfigureKeychainSignerProvider;   // namesti kot zaledje podpisovalca PAdES
  ConfigureKeychainCmsVerifier;      // in kot zaledje preverjanja

  Writeln('signer backend  : ', PadesCryptoBackendName);
  Writeln('verify backend  : ', PadesCmsVerificationBackendName);

  Options := TPadesSignerOptions.Default;
  Options.CertificateThumbprint := 'B1 3F 9C ...';   // SHA-1, katera koli velikost črk
  Options.PaddingScheme := psRsaPss;
end;

Dve vrsti izvoženih simbolov, dva načina branja

To je najbolj zmedena podrobnost v celem vezanju, obratno razumevanje pa se čisto prevede in spodleti med zagonom. CoreFoundation in Security izvažata dve kategorično različni stvari skozi isti klic dlsym in koda mora vedeti, katera je katera

Poimenovane konstante, kot so ključi razredov postavk verige ključev in boolean enojci CoreFoundation, so izvožene spremenljivke, katerih vsebina je CFStringRef ali CFBooleanRef, ki ga želite. dlsym vrne naslov te spremenljivke, zato morate enkrat dereferencirati, da pridobite vrednost. Strukture tabel povratnih klicev, kot so povratni klici ključev in vrednosti slovarja, pa so izvožene strukture in dlsym vrne naslov strukture — natanko kazalec, ki ga pričakuje funkcija za ustvarjanje slovarja. Če to dereferencirate, podate prvo strojno besedo strukture, kot da bi bila kazalec

Nobena napaka ne proizvede napake prevoda in nobena ne proizvede jasne napake med zagonom. Dobite smetni kazalec, ki spodleti nekje na spodnjem toku. Način, da razlike ni mogoče zamenjati, je prenehati se zanašati na zapomnitev: dve pomožni funkciji, ena, ki veže in dereferencira, in ena, ki veže in ne, tako da mesto klica razglasi, katero vrsto simbola sprašuje, pomožnica pa uveljavi ostalo

Diagram zaledja verige ključev macOS v PDFium VCL, ki razrešuje simbole Security in CoreFoundation skozi dlsym: kSecClass je izvožena spremenljivka, ki jo BindConstant enkrat dereferencira, da pridobi vrednost CFStringRef, kCFTypeDictionaryKeyCallBacks pa je izvožena struktura, ki jo BindStruct poda po naslovu, zamešanje obeh pravil pa da smetne kazalce na spodnjem toku
En klic dlsym vrne dve kategorično različni stvari: naslov spremenljivke, ki drži CFTypeRef, in naslov strukture povratnih klicev. Dve pomožnici naredita odločitev dereferencirati ali ne na mestu vezanja in ne v pomnilniku
// Izvožena spremenljivka: dlsym da naslov spremenljivke, ki drži
// CFTypeRef, zato enkrat dereferencirajte
FSecClassKey := BindConstant(SecurityLib, 'kSecClass');

// Izvožena struktura: dlsym da naslov STRUKTURE, kar je
// tisto, kar API želi. Ne dereferencirajte
FKeyCallbacks := BindStruct(CoreFoundationLib,
  'kCFTypeDictionaryKeyCallBacks');

Zakaj potrebuje podpis RSA-PSS dva ločena zasilna izhoda?

Ker se lahko algoritem manjka na dva neodvisna načina in je le eno od njiju vprašanje verzije. Konstanta algoritma za podpisovanje povzetkov PSS se je pojavila v macOS 10.13, tako da na starejšem sistemu simbola preprosto ni in vezanje dobi nil. To je preverjanje verzije. Ločeno pa lahko na sistemu, kjer konstanta obstaja, določen ključ vseeno odkloni, ogrodje pa to vprašanje odgovori skozi SecKeyIsAlgorithmSupported za tisti ključ. Ključ, podprt s strojno opremo, ali ključ z omejujočimi atributi lahko odklonita PSS, medtem ko ga programski ključ na istem stroju sprejme

Obi poti morata voditi k istemu zasilnemu izhodu: preklop na PKCS#1 v1.5. Kritični del pa je, da mora zasilni izhod spremeniti tudi identifikator algoritma, zapisan v strukturo CMS, in ne le podpisni klic. Oddajanje identifikatorja algoritma PSS, medtem ko dejansko nastaja podpis v1.5, da dokument, ki ga vsak preverjevalnik odkloni na mestu — kar je strogo slabše kot sporočiti, da PSS ni podprt. Povzdignitev navzdol je sprejemljiva, neskladje med tem, kar razglasite, in tem, kar ste storili, pa ni, in to je splošno pravilo za podpisno kodo in ne posebnost macOS. Implikacije na ravni podpisa so razložene v podpisovanju PDF-jev s PAdES B-B

Veriga odločitev, ki prikazuje, zakaj podpisovanje RSA-PSS v zaledju verige ključev PDFium VCL potrebuje dva neodvisna zasilna izhoda: dlsym vrne nil za konstanto podpisovanja povzetkov na verzijah macOS pred 10.13, SecKeyIsAlgorithmSupported lahko odkloni ključ, podprt s strojno opremo, obe vrati pa se lijejo v isto povzdignitev navzdol na PKCS#1 v1.5, katerega identifikator algoritma CMS se mora spremeniti z njim
PSS je lahko dvakrat odsoten — enkrat na verzijo macOS in enkrat na ključ — in le vrata verzije so sistemsko vprašanje. Obe vrati se lijejo v isto povzdignitev navzdol na v1.5, identifikator CMS pa ji sledi

Kodiranje podpisov ECDSA in obrat, vreden omembe

Pot eliptičnih krivulj na macOS ne potrebuje nobene pretvorbe in to je nasprotje tega, kar zahteva vezava PKCS#11. Algoritem ogrodja Security za podpisovanje povzetkov ECDSA vrne podpis že v obliki X9.62 DER — natanko to, kar želi CMS. Žeton PKCS#11 pa vrne surovi par P1363 fiksne širine, ki ga je treba znova kodirati, preden gre v strukturo podpisa

Tako dve zaledji, ki implementirata isti vmesnik, potrebujeta nasprotno obravnavo za isti algoritem in nobeno ni napačno. To je natanko tista vrsta razlike, ki jo mora abstrakcija absorbirati in ne izpostaviti: plast PAdES prosi ponudnika, naj podpiše, konvencije kodiranja pa ostanejo znotraj ponudnika. Če pronicajo navzgor, vsak klicatelj na koncu nosi pogoj na zaledje. Isti vzorec se pokaže v zgodbi oddaljenega podpisovanja, opisani v oddaljenih sejah podpisovanja PAdES proti HSM

Primerjava kodiranja podpisov ECDSA čez dve zaledji podpisovalca PAdES v PDFium VCL: ogrodje Security verige ključev macOS vrne X9.62 DER, ki ga CMS sprejme z ničelno pretvorbo, žeton PKCS#11 pa vrne surovi par P1363 fiksne širine, ki ga je treba znova kodirati, zato ResolvePadesSigner obdrži konvencije kodiranja znotraj ponudnika
Isti vmesnik ECDSA potrebuje nasprotno obravnavo na zaledje: Security podaja končani DER, žeton PKCS#11 pa surovi P1363, zato pretvorba živi znotraj ponudnika in klicatelji nikoli ne vidijo pogoja na zaledje
// Vmesnik ponudnika je isti na vsaki platformi, zato je izbira
// odločitev ob zagonu in ne na klic
{$IFDEF DARWIN}
  if KeychainAvailable then
    ConfigureKeychainSignerProvider;
{$ENDIF}
{$IFDEF MSWINDOWS}
  // Ponudnik CNG Windows namesti platformska enota
{$ENDIF}

if not PadesCryptoAvailable then
  raise Exception.Create('no signing backend on this platform');

// Od tu naprej je podpisna koda platformsko nevtralna
Signer := ResolvePadesSigner(Options);

Pravila štetja sklicev, ki sedejo tri vrstice narazen

Upravljanje pomnilnika Core Foundation sledi poimenovalnim konvencijam in past tu je, da se funkcije z različnimi konvencijami pojavijo druga ob drugi v istem kratkem bloku. Funkcija, ki dobi certifikat iz objekta zaupanja, vrne izposojeni sklic, ki se ga ne sme sprostiti. Funkcije, ki kopirajo certifikat podpisovalca ali kopirajo njegove podatke, vrnejo lastniške sklice, ki se jih mora sprostiti. Trije klici v zaporedju, dve pravili lastništva, sprostitev izposojenega pa ne spodleti na tisti vrstici. Pokvari števec obdržev in pozneje podre nekaj nepovezanega

Ublažitev je prebrati glagol v imenu vsake funkcije ogrodja, preden zapišete čiščenje — vsakič, brez izjem. To je CoreFoundation ustreznica preverjanja, ali API vrne izvod ali pogled, cena napake pa je občasno sesutje in ne napaka

Česa to zaledje ne trdi

Ob času pisanja še nikoli ni tekel na macOS in povedati to na glas je uporabneje kot implicitno zagotovilo. Dokazljivo resnica je ožja in še vedno vredna: enota se prevede na Windows kot del dnevne gradnje, vsak simbol ogrodja je vezan po imenu med zagonom z naštetimi odpovedmi, logika izbire algoritma — vključno z obema zasilnima izhodoma PSS — pa je navaden Pascal, ki ga je mogoče pregledati in o njem sklepati. Prvi zagon na Macu bo ali deloval ali proizvedel seznam imen za popravilo

Preverjevalna stran, ki uporablja dekoder CMS višje ravni in ne sestavljanja strukture CMS na roko, je pokrita v preverjanju podpisov PDF na macOS s SecTrust in si deli isto vezalno infrastrukturo ter isti diagnostični pristop

Prenosljiva ideja tu gre za umestitev tveganja in ne za macOS. Ko morate pisati kodo proti vmesniku, ki ga ne morete preveriti, izberite konstrukcijo, kjer so napake najcenejše za lociranje. Dinamično vezanje z izrecnim seznamom nerazrešenih imen spremeni dvajset neverificiranih predpostavk v eno diagnostično vrstico. Obe zaledji prideta kot vir z PDFium Delphi component, tako da če ime simbola res potrebuje popravek, je to sprememba ene vrstice v vašem lastnem drevesu in ne prijava na podporo