Techninis straipsnis

PKCS#11 Delphi: CK_ULONG ir packing spąstai

PDFiumPas pasirašo PAdES dokumentus per PKCS#11 žetoną Windows, Linux ir macOS sistemose, o ar susiejimas apskritai veiks lemia du platformos faktai: CK_ULONG yra C unsigned long, taigi Windows sistemoje 4 baitai, o Linux ir macOS – 8, be to, PKCS#11 antraštės #pragma pack(1) taiko tik Windows sistemoje, todėl pasislenka kiekviena funkcijų lentelės rodyklė. Suklyskite bet kur ir modulis vis tiek įsikels, iškvietimai vis tiek grįš, o grįžtantys skaičiai bus šiukšlės. Tokio gedimo ir reikia tikėtis. Niekas nepateiks linkerio klaidos, nes niekas nesusieta: modulis yra .so, .dylib arba .dll, kurį vykdymo metu atveriate pagal kelią, o visas paviršius yra funkcijų rodyklių struktūra, kurią jūs paverčiate tipu ir iškviečiate. Kompiliatorius nežino, kaip atrodė kitoje pusėje esanti C antraštė. Kiekvienas neatitikimas tylus iki pat avarijos

Kodėl PKCS#11 susiejimas grąžina atsitiktinius CKR kodus, o ne aiškią klaidą?

Nes ABI neatitikimas apskritai nesukuria klaidos būsenos, jis sukuria neteisingą adresą arba neteisingą poslinkį, o žetonas pareigingai atsako į tą klausimą, kuris iš to išeina. Tarp jūsų įrašo deklaracijos ir modulio nėra sluoksnio, kuris galėtų pastebėti nesutarimą. Iš to kyla du skirtingi gedimo režimai. Jei neteisingas packing, vieta, kurią skaitote kaip C_GetSlotList, turi šešis vienos rodyklės baitus ir du kitos, o iškvietimas šoka į nesusietą atmintį arba, dar blogiau, į kitos funkcijos vidurį. Tai prieigos pažeidimas. Jei CK_ULONG plotis neteisingas, adresai geri, bet duomenys ne: var Count: CK_ULONG išvesties parametras, deklaruotas kaip 4 baitų, iš LP64 modulio gauna 8 įrašytus baitus ir tyliai perrašo kitus keturis steko kadro baitus, o CK_ATTRIBUTE šablonas, kurio ValueLen yra ne tuo poslinkiu, verčia modulį skaityti ilgio lauką iš jūsų Value rodyklės. Tada žetonas grąžina visiškai teisėtą CKR_BUFFER_TOO_SMALL arba CKR_ATTRIBUTE_VALUE_INVALID į klausimą, kurio niekada neuždavėte. Šie kodai verčia valandų valandas ieškoti žetono konfigūracijos. Klaida yra keturiomis eilutėmis aukščiau, tipo deklaracijoje

CK_ULONG yra C unsigned long, o ne fiksuoto pločio tipas

PKCS#11 antraštės CK_ULONG apibrėžia kaip C unsigned long, todėl jo plotis priklauso nuo platformos duomenų modelio, o ne nuo specifikacijos. Windows yra LLP64, todėl unsigned long net ir 64 bitų procese lieka 32 bitų. Linux ir macOS yra LP64, todėl jis seka rodyklės plotį ir tampa 64 bitų. Tai pati svarbiausia viso unito eilutė, nes PKCS#11 praktiškai kiekvienas skaliaras yra CK_ULONG: lizdų ID, seansų rankenos, objektų rankenos, objektų klasės, raktų tipai, atributų tipai, mechanizmų tipai, buferių ilgiai ir pati CK_RV grąžinama reikšmė

type
{$IFDEF MSWINDOWS}
  // Windows yra LLP64: C unsigned long ten lieka 32 bitų
  CK_ULONG = LongWord;
{$ELSE}
  // Linux ir macOS yra LP64: unsigned long seka rodyklės plotį
  CK_ULONG = PtrUInt;
{$ENDIF}
  CK_RV = CK_ULONG;
  CK_FLAGS = CK_ULONG;
  CK_SLOT_ID = CK_ULONG;
  CK_SESSION_HANDLE = CK_ULONG;
  CK_OBJECT_HANDLE = CK_ULONG;
  CK_OBJECT_CLASS = CK_ULONG;
  CK_ATTRIBUTE_TYPE = CK_ULONG;
  CK_MECHANISM_TYPE = CK_ULONG;
  PCK_ULONG = ^CK_ULONG;

Visus šiuos aliasus susiejus su CK_ULONG, o ne tiesiogiai su LongWord ar UInt64, ir yra visa užduoties esmė. Sąlyginis sprendimas egzistuoja lygiai vienoje vietoje. Išrašykite kurį nors tipą konkrečiai ir pasėjote miną, ant kurios būsimas portas užlips būtent ten, kur pamiršote

Ką pragma pack(1) daro PKCS#11 funkcijų lentelei?

Ji paslenka kiekvieną CK_FUNCTION_LIST funkcijos rodyklę, nes lentelė prasideda dviejų baitų CK_VERSION. Natūralaus lygiavimo atveju kompiliatorius po šios versijos įterpia šešis užpildo baitus, todėl pirmoji funkcijos rodyklė atsiduria 8 poslinkyje. Baitų packing atveju užpildo nėra, todėl ji atsiduria 2 poslinkyje. Kiekvienas vėlesnis įrašas paveldi tą patį poslinkį, todėl packing klaida nėra vieno lauko problema, o visos lentelės problema. Spąstai tokie, kad PKCS#11 antraštės #pragma pack(1) taiko tik Windows. Tai platformos, o ne modulio skirtumas: du tos pačios tiekėjo bibliotekos build nesutaria priklausomai nuo kompiuterio, kuriame sukurti. Taip pat atkreipkite dėmesį, kad packing nieko nekeičia struktūroms, kurių visi laukai yra rodyklės pločio, o tokių yra dauguma, todėl naivus testas, paliečiantis tik CK_SLOT_INFO, sėkmingai praeis, nors po juo esanti lentelė paslinkta šešiais baitais

{$IFDEF FPC}
  {$IFDEF MSWINDOWS}{$PACKRECORDS 1}{$ELSE}{$PACKRECORDS C}{$ENDIF}
{$ELSE}
  {$A1}
{$ENDIF}

  CK_VERSION = record
    Major: Byte;
    Minor: Byte;
  end;

  CK_ATTRIBUTE = record
    AttrType: CK_ATTRIBUTE_TYPE;
    Value: Pointer;
    ValueLen: CK_ULONG;
  end;

  CK_FUNCTION_LIST = record
    Version: CK_VERSION;      // du baitai, dėl kurių lentelė pasislenka
    C_Initialize: Pointer;    // 2 poslinkis packed, 8 sulygiuotas
    C_Finalize: Pointer;
    C_GetInfo: Pointer;
    C_GetFunctionList: Pointer;
    C_GetSlotList: Pointer;
    // ... lentelė turi fiksuotą tvarką; pakanka deklaruoti pradžią
    // iki C_Sign, kad pasiektume visus šio backend kviečiamus įrašus
    C_SignInit: Pointer;
    C_Sign: Pointer;
  end;
  PCK_FUNCTION_LIST = ^CK_FUNCTION_LIST;

{$IFDEF FPC}{$PACKRECORDS DEFAULT}{$ELSE}{$A8}{$ENDIF}

Trys dalykai šiame bloke svarbesni, nei atrodo. {$PACKRECORDS C} nėra tas pats, kas „jokios direktyvos“: Free Pascal nurodo laikytis platformos C kompiliatoriaus lygiavimo taisyklių, o būtent tai yra jums reikalinga sutartis Linux ir macOS. Delphi šaka besąlygiškai naudoja {$A1}, nes PDFiumPas Delphi build skirti Windows, o FPC apima Linux ir macOS build. Atkūrimo eilutė apačioje nėra kosmetika: palikus unitą supakuotą, kiekvienas po šio taško deklaruotas įrašas tyliai pakeičia maketą, būtent tokį veiksmą nutolusio poveikio defektu siekia panaikinti PDFium komponento susiejimo nuo ABI ir atminties saugos klaidų grūdinimas

Pkcs11AbiLayout: maketo pavertimas tvirtinimu

Pkcs11AbiLayout praneša apie faktinį build išspręstą maketą kaip vieną tvirtinamą eilutę formos ulong=4 attr=16 pss=12 table=2. 64 bitų Windows build privalo pateikti tiksliai tai, o LP64 taikinys – ulong=8 attr=24 pss=24 table=8. Bet kas kita reiškia, kad iškvietimas per funkcijų lentelę nusileis ne tame lizde, ir ši funkcija egzistuoja tam, kad testas tai pasakytų garsiai, užuot turėjus komentarą, kuris tik teigia, kad viskas gerai

function Pkcs11AbiLayout: string;
var
  Table: CK_FUNCTION_LIST;
begin
  Result := 'ulong=' + IntToStr(SizeOf(CK_ULONG)) +
    ' attr=' + IntToStr(SizeOf(CK_ATTRIBUTE)) +
    ' pss=' + IntToStr(SizeOf(CK_RSA_PKCS_PSS_PARAMS)) +
    ' table=' + IntToStr(NativeUInt(@Table.C_Initialize) - NativeUInt(@Table));
end;

// Įkėlimo metu, kai C_GetFunctionList grąžino lentelę:
// neįtikėtina versija arba nil entry point reiškia, kad įrašas buvo išdėstytas
// su neteisingu packing arba CK_ULONG pločiu, todėl modulio atsisakome
if (FList^.Version.Major < 2) or (FList^.Version.Major > 3) or
  not Assigned(FList^.C_Initialize) or not Assigned(FList^.C_GetSlotList) or
  not Assigned(FList^.C_Sign) then
begin
  FList := nil;
  Exit;
end;

Keturi skaičiai nėra savavališki. attr yra CK_ATTRIBUTE dydis, o jame yra CK_ULONG, rodyklė ir CK_ULONG: 4 + 8 + 4 packed Windows x64, 8 + 8 + 8 sulygiuotoje LP64. pss yra CK_RSA_PKCS_PSS_PARAMS, trys CK_ULONG laukai, todėl 12 arba 24. table yra pirmosios funkcijos rodyklės poslinkis ir tai reikšmė, kuri pirmiausia pagauna packing klaidą. Delphi testas tvirtina eilutę po {$IFDEF MSWINDOWS}, o Lazarus rinkinys tvirtina tą patį. Viena lygybės patikra apima maketą, kurį kitu atveju būtų galima patikrinti tik laikant C antraštę šalia Pascal įrašo ir pasitikint savimi. Įkėlimo patikra yra antroji tos pačios idėjos pusė. PDFiumPas tik C_GetFunctionList išsprendžia pagal vardą per GetProcAddress arba GetProcedureAddress, o kiekvieną kitą entry point paima iš lentelės, kurią grąžina tas iškvietimas, taip OASIS PKCS #11 bazinė specifikacija numato pasiekti modulį ir išvengiama tiekėjo specifinių simbolių vardų. Tada grįžę duomenys patikrinami. Pagrindinė versija už 2–3 ribų arba nil C_Initialize, C_GetSlotList ar C_Sign reiškia neteisingai sulygiuotą įrašą, todėl modulis atmetamas, o ne kviečiamas per jį

Pasirašymas per lentelę: mechanizmai, DigestInfo ir dviejų etapų C_Sign

Kai maketas teisingas, pasirašymo darbas nedidelis, nes ICmsSigner sutartis, kurią PDFiumPas prašo backend įgyvendinti, turi penkis metodus, o keturi iš jų tik grąžina OID ir pasirašančiojo identifikatorių. Tik SignSignedAttrsDigest ką nors daro: jis paima pasirašytų atributų 32 baitų SHA-256 santrauką ir grąžina parašo baitus. CMS surinkimas, ASN.1, RFC 3161 laiko žymės ir DSS/LTV yra nepriklausomi nuo platformos ir jau įgyvendinti, taip pat kaip ir nuotoliniai PAdES pasirašymo seansai su HSM arba debesijos rakto paslauga jungiasi prie tos pačios vietos. Jei praleisite tris mechanizmų detales, gausite nepavykusį tikrinimą. CKM_RSA_PKCS taiko PKCS#1 v1.5 užpildą, bet nesukuria DigestInfo, todėl iškvietėjas pats prideda 19 baitų SHA-256 DigestInfo prefiksą iš RFC 8017; paduokite žetonui gryną santrauką ir gausite taisyklingos formos parašą, pasirašantį ne tą dalyką. CKM_RSA_PKCS_PSS ir CKM_ECDSA priima pateiktą santrauką, bet CKM_ECDSA atsako neapdorota r||s pora, o CMS reikia RFC 3279 §2.2.3 ECDSA-Sig-Value SEQUENCE, todėl PDFiumPas ją konvertuoja. O C_Sign sąmoningai yra dviejų etapų: pirmiausia iškvieskite su nil buferiu, kad paklaustumėte žetono parašo ilgio, tada dar kartą su tokio dydžio buferiu

var
  Options: TPdfPkcs11Options;
  Provider: IPdfPkcs11SignerProvider;
  Slot: TPdfPkcs11Slot;
begin
  Options := TPdfPkcs11Options.Default;
  Options.ModulePath := '/usr/lib/softhsm/libsofthsm2.so';
  Options.Pin := ReadOperatorPin;
  Options.CertificateLabel := 'Signing Certificate';

  if not Pkcs11ModuleAvailable(Options.ModulePath) then
    raise Exception.Create('No usable PKCS#11 module at ' + Options.ModulePath);
  // Įrašome tai prieš viską, kai žetonas naujoje platformoje elgiasi netinkamai
  Writeln('PKCS#11 ABI layout: ' + Pkcs11AbiLayout);

  Provider := ConfigurePkcs11SignerProvider(Options);
  for Slot in Provider.EnumerateSlots do
    if Slot.TokenPresent then
      Writeln(Slot.SlotID, ' ', Slot.TokenLabel);
end;

Prieš pirmą žetoną verta žinoti dar kelis mažesnius dalykus. Moduliai podėliuojami pagal kelią, nes C_Initialize vienam procesui ir moduliui kviečiamas kartą, o pakartotinis iškvietimas grąžina CKR_CRYPTOKI_ALREADY_INITIALIZED (0x00000190), kurį PDFiumPas traktuoja kaip sėkmę, manydamas, kad kitą tos pačios bibliotekos dalį šeimininkas jau inicializavo. Tokios žetono eilutės kaip lizdo aprašas ir žetono etiketė yra fiksuoto pločio, užpildytos tarpais, o ne baigiamos NUL, todėl tarpus reikia nukirpti nuo galo. Ir CKO_CERTIFICATE yra 1, o ne 2 – 0 yra CKO_DATA, o 2 – CKO_PUBLIC_KEY. Parašius šią konstantą iš atminties gaunamas tuščias paieškos rezultatas be jokios klaidos

Kas patikrinta ir kur baigiasi garantija?

Aiškiai žinokite ribą, nes ji siauresnė už funkcijos aprašą. Šiandien PDFiumPas patikrinta, kad ABI maketas abiejose šakose sutampa su C antraštėmis laukas po lauko, kad nesant modulio arba jo nepavykus įkelti gaunama pranešta nesėkmė, o ne avarija, ir kad tiek Delphi, tiek FPC toolchain sukompiliuoja unitą. Tikri žetono keliai – C_Login, objektų paieška ir aparatinio C_Sign iškvietimas – nebuvo išbandyti, nes kūrimo kompiuteryje apskritai nėra įdiegto PKCS#11 modulio. Pirmiausia paleiskite SoftHSM2 ir patvirtinkite Pkcs11AbiLayout, tik tada prijunkite fizinį žetoną, kad ABI ir žetono problemų nereikėtų diagnozuoti tuo pačiu metu. Verta įvardyti dar vieną asimetriją. Pasirašymo pusė dabar kryžminė, o tikrinimo pusė – ne. CMS tikrinimas PDFiumPas viduje vis dar saugomas {$IFDEF MSWINDOWS} ir kitur grąžina pcsUnsupported, be to, jis neturi tiekėjo įterpimo vietos, atitinkančios pasirašytojo backend. Todėl Linux paslauga gali sukurti PAdES B-B parašą raktu, laikomu žetone, bet dar negali toje pačioje mašinoje patikrinti savo išvesties. Kol spraga neužpildyta, tikrinimą planuokite Windows sistemoje arba išoriniame tikrintuve

Ši pamoka taikoma ne vien PKCS#11. Kiekvienam Pascal įrašui, atkartojančiam sąlygiškai supakuotą C struktūrą, reikia trijų dalykų: vieno sąlyginio platformai kintamo skaliaro aliaso, kad pločio sprendimas egzistuotų lygiai vienoje vietoje, packing direktyvų, apgaubiančių deklaracijas ir vėliau atkuriamų, ir vykdymo metu veikiančios funkcijos, kuri praneštų išspręstą maketą taip, kad testas galėtų jį patvirtinti. Komentarai, teigiantys, kad struktūra sutampa su antrašte, nieko verti; SizeOf ir paleidimo metu išspausdintas lauko poslinkis verti daug. PKCS#11 backend, CNG backend ir likęs pasirašymo stekas pateikiami PDFium komponente, skirtame Delphi ir C++Builder, kuriame ABI santechnika jau sąlygota, todėl jūsų kodas gali likti žetono problemos pusėje