Tehnički članak

PKCS#11 u Delphi-ju: zamka CK_ULONG i packing-a

PDFiumPas potpisuje PAdES dokumente preko PKCS#11 tokena na Windows-u, Linux-u i macOS-u, a dve činjenice o platformi odlučuju da li binding uopšte radi: CK_ULONG je C unsigned long, pa ima 4 bajta na Windows-u i 8 bajtova na Linux-u i macOS-u, a PKCS#11 zaglavlja primenjuju #pragma pack(1) samo na Windows-u, čime se pomera svaki pokazivač u tabeli funkcija. Pogrešite bilo šta od ta dva i modul se i dalje učitava, pozivi i dalje vraćaju, a brojevi koji se vraćaju su đubre. To je oblik baga koji treba očekivati. Niko vam ne daje linker error, jer se ništa ne povezuje: modul je .so, .dylib ili .dll koji otvarate tokom izvršavanja po putanji, a cela površina je struct pokazivača na funkcije koje cast-ujete i pozivate. Kompajler nema pojma kako je izgledalo C zaglavlje sa druge strane. Svako neslaganje ostaje tiho sve do pada

Zašto PKCS#11 binding vraća nasumične CKR kodove umesto čiste greške?

Zato što ABI neslaganje uopšte ne proizvodi uslov greške; ono proizvodi pogrešnu adresu ili pogrešan offset, a token poslušno odgovara na pitanje koje se ispostavi iz toga. Ne postoji sloj između vaše deklaracije zapisa i modula koji bi neslaganje mogao da primeti. Iz toga izlaze dva različita režima otkaza. Ako je packing pogrešan, slot koji čitate kao C_GetSlotList drži šest bajtova jednog pokazivača i dva sledećeg, a poziv skače u unmapped memoriju ili, još gore, u sredinu neke druge funkcije. To je access violation. Ako je CK_ULONG pogrešne širine, adrese su dobre, ali podaci nisu: var Count: CK_ULONG out-parametar deklarisan kao 4 bajta dobija 8 bajtova od LP64 modula i tiho prepisuje sledeća četiri bajta stack frame-a, dok CK_ATTRIBUTE template čiji ValueLen sedi na pogrešnom offsetu navodi modul da čita length polje iz vašeg Value pokazivača. Token zatim vraća sasvim legitimni CKR_BUFFER_TOO_SMALL ili CKR_ATTRIBUTE_VALUE_INVALID za pitanje koje nikada niste postavili. Ti kodovi šalju ljude da satima traže problem u konfiguraciji tokena. Bag je četiri linije iznad, u deklaraciji tipa

CK_ULONG je C unsigned long, a ne tip fiksne širine

PKCS#11 zaglavlja definišu CK_ULONG kao C unsigned long, što znači da njegova širina prati model podataka platforme, a ne specifikaciju. Windows je LLP64, pa unsigned long ostaje 32-bitni i u 64-bitnom procesu. Linux i macOS su LP64, pa prati pokazivač i postaje 64-bitni. Ovo je najvažnija linija u celoj jedinici, jer je u PKCS#11 praktično svaki skalar CK_ULONG: ID-jevi slotova, handle-ovi sesija, handle-ovi objekata, klase objekata, tipovi ključeva, tipovi atributa, tipovi mehanizama, dužine bafera i sama povratna vrednost CK_RV

type
{$IFDEF MSWINDOWS}
  // Windows je LLP64: C unsigned long tamo ostaje 32-bitni
  CK_ULONG = LongWord;
{$ELSE}
  // Linux i macOS su LP64: unsigned long prati širinu pokazivača
  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;

Aliasovanje svakog od tih tipova na CK_ULONG, umesto direktno na LongWord ili UInt64, jeste poenta vežbe. Tako se conditional pojavljuje tačno jednom. Napišite bilo koji od njih konkretno i postavili ste minu na koju će budući port stati, i staće na mestu koje ste zaboravili

Šta pragma pack(1) radi tabeli PKCS#11 funkcija?

Pomera svaki pokazivač na funkciju u CK_FUNCTION_LIST, jer tabela počinje dvobajtnim CK_VERSION. Pri prirodnom poravnanju kompajler ubacuje šest bajtova padding-a posle te verzije, pa prvi pokazivač na funkciju pada na offset 8. Kod byte packing-a nema padding-a, pa pada na offset 2. Svaki sledeći unos nasleđuje isto pomeranje, zbog čega packing greška nije problem jednog polja, već cele tabele. Zamka je u tome što PKCS#11 zaglavlja primenjuju #pragma pack(1) samo na Windows-u. To je razlika platforme, a ne modula: dve izgradnje iste vendor biblioteke ne slažu se oko toga u zavisnosti od hosta iz kojeg dolaze. Takođe, packing ne menja ništa strukturama čija su sva polja širine pokazivača, a to je većina njih, pa naivan test koji dodiruje samo CK_SLOT_INFO veselo prolazi dok je tabela ispod pomerena za šest bajtova

{$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;      // dva bajta, razlog zbog kojeg se tabela pomera
    C_Initialize: Pointer;    // offset 2 packed, offset 8 aligned
    C_Finalize: Pointer;
    C_GetInfo: Pointer;
    C_GetFunctionList: Pointer;
    C_GetSlotList: Pointer;
    // ... tabela ima fiksan redosled; deklarisanje prefiksa
    // do C_Sign dovoljno je da se dohvati sve što backend poziva
    C_SignInit: Pointer;
    C_Sign: Pointer;
  end;
  PCK_FUNCTION_LIST = ^CK_FUNCTION_LIST;

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

Tri stvari u tom bloku važnije su nego što izgledaju. {$PACKRECORDS C} nije isto što i „bez direktive“; govori Free Pascal-u da prati alignment pravila platformskog C kompajlera, što je upravo ugovor potreban na Linux-u i macOS-u. Delphi grana bezuslovno koristi {$A1} zato što Delphi build-ovi PDFiumPas-a ciljaju Windows, dok FPC nosi Linux i macOS build-ove. A restore linija na dnu nije kozmetika: ostavite jedinicu packed i svaki zapis deklarisan posle ove tačke tiho menja layout, što je upravo vrsta action-at-a-distance defekta koju treba da ukloni učvršćivanje PDFium component binding-a protiv ABI i memory-safety grešaka

Pkcs11AbiLayout: pretvaranje layout-a u aserciju

Pkcs11AbiLayout prijavljuje layout koji je build zaista razrešio kao jedan string pogodan za aserciju u obliku ulong=4 attr=16 pss=12 table=2. 64-bitni Windows build mora tačno to da prijavi, a LP64 cilj mora da prijavi ulong=8 attr=24 pss=24 table=8. Sve drugo znači da bi poziv kroz tabelu funkcija pao na pogrešan slot, a funkcija postoji da unit test to kaže naglas umesto da komentar to samo tvrdi

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;

// Pri učitavanju, nakon što je C_GetFunctionList vratio tabelu:
// neverovatna verzija ili nil entry point znači da je zapis postavljen
// pogrešnim packing-om ili CK_ULONG širinom, pa modul odbij
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;

Četiri broja nisu proizvoljna. attr je veličina CK_ATTRIBUTE, koji drži CK_ULONG, pokazivač i CK_ULONG: 4 + 8 + 4 packed na Windows x64, 8 + 8 + 8 aligned na LP64. pss je veličina CK_RSA_PKCS_PSS_PARAMS, tri CK_ULONG polja, dakle 12 ili 24. table je offset prvog pokazivača na funkciju i vrednost koja prva hvata packing grešku. Delphi test case potvrđuje string pod {$IFDEF MSWINDOWS}, a Lazarus suite potvrđuje isto. Jedna provera jednakosti pokriva layout koji bi inače mogao da se proveri samo čitanjem C zaglavlja pored Pascal zapisa i poverenjem sebi. Provera pri učitavanju druga je polovina iste ideje. PDFiumPas po imenu razrešava samo C_GetFunctionList preko GetProcAddress ili GetProcedureAddress i svaku drugu entry point uzima iz tabele koju taj poziv vrati, što je način na koji OASIS PKCS #11 base specifikacija predviđa pristup modulu i ujedno zaobilazi imenovanje simbola po vendoru. Zatim proverava ono što je dobio. Major verzija izvan 2 do 3 ili nil C_Initialize, C_GetSlotList ili C_Sign znači da je zapis pogrešno poravnat, pa se modul odbacuje umesto da se pozove kroz njega

Potpisivanje kroz tabelu: mehanizmi, DigestInfo i dvoprolazni C_Sign

Kada je layout ispravan, potpisivanje je malo, jer ICmsSigner ugovor koji PDFiumPas traži od backend-a ima pet metoda, a četiri samo vraćaju OID-jeve i identifikator potpisnika. Samo SignSignedAttrsDigest nešto radi: uzima 32-bajtni SHA-256 digest potpisanih atributa i vraća bajtove potpisa. CMS sklapanje, ASN.1, RFC 3161 timestamping i DSS/LTV već su platform-independent i već završeni, što je ista podela posla koja omogućava da se udaljene PAdES signing sesije prema HSM-u ili cloud key servisu priključe na identičan seam. Tri detalja mehanizama koštaće vas neuspešne verifikacije ako ih preskočite. CKM_RSA_PKCS primenjuje PKCS#1 v1.5 padding, ali ne gradi DigestInfo, pa pozivalac sam dodaje 19-bajtni SHA-256 DigestInfo prefiks iz RFC 8017; prosledite tokenu goli digest i dobićete ispravan potpis nad pogrešnom stvari. CKM_RSA_PKCS_PSS i CKM_ECDSA uzimaju digest onako kako je prosleđen, ali CKM_ECDSA odgovara sirovim r||s parom, dok CMS zahteva ECDSA-Sig-Value SEQUENCE iz RFC 3279 §2.2.3, pa PDFiumPas konvertuje. A C_Sign je namerno dvoprolazan: pozovite ga sa nil baferom da pitate token za dužinu potpisa, a zatim ponovo sa baferom te veličine

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);
  // Loguj ovo pre svega ostalog kada se token loše ponaša na novoj platformi
  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;

Nekoliko manjih stvari vredi znati pre prvog tokena. Moduli se keširaju po putanji jer se C_Initialize izvršava jednom po procesu i modulu, a ponovljeni poziv vraća CKR_CRYPTOKI_ALREADY_INITIALIZED (0x00000190), što PDFiumPas tretira kao uspeh pod pretpostavkom da je drugi deo hosta već inicijalizovao istu biblioteku. Token stringovi poput opisa slota i label-e tokena jesu fiksna polja dopunjena razmacima, a ne NUL-terminated stringovi, pa ih treba odseći sa kraja. I CKO_CERTIFICATE je 1, a ne 2 — 0 je CKO_DATA, a 2 je CKO_PUBLIC_KEY. Upis te konstante iz sećanja greška je koja proizvodi prazan rezultat pretrage i nikakvu grešku

Šta je verifikovano i gde garancija prestaje

Budite jasni o granici, jer je uža od onoga što opis funkcije sugeriše. Ono što PDFiumPas danas proverava jeste da ABI layout odgovara C zaglavljima polje po polje na obe grane, da se odsutan ili neučitiv modul pretvara u prijavljen neuspeh umesto u pad i da i Delphi i FPC toolchain-i grade jedinicu. Prave token putanje — C_Login, pretraga objekata, C_Sign prema hardveru — nisu izvršene, jer razvojni host uopšte nema instaliran PKCS#11 modul. Prvo podignite SoftHSM2 i potvrdite Pkcs11AbiLayout pre nego što priključite fizički token, kako ABI problem i problem tokena nikada ne bi morali da se dijagnostikuju istovremeno. Još jednu asimetriju treba imenovati. Strana potpisivanja sada je cross-platform; strana verifikacije nije. CMS verifikacija unutar PDFiumPas-a i dalje je zaštićena sa {$IFDEF MSWINDOWS} i drugde vraća pcsUnsupported, bez injection point-a za provider ekvivalentnog signer backend-u. Tako Linux servis može da proizvede PAdES B-B potpis nad ključem koji drži token, ali još ne može da proveri sopstveni izlaz na istoj mašini. Planirajte verifikaciju na Windows-u ili u spoljašnjem validatoru dok se ta praznina ne zatvori

Pouka se proteže i izvan PKCS#11. Svaki Pascal zapis koji preslikava uslovno packed C struct treba da ima tri stvari: jedan conditional alias za skalar promenljive širine po platformi, tako da odluka o širini postoji na tačno jednom mestu, packing direktive koje obuhvataju deklaracije i zatim se vraćaju na prethodno stanje i runtime funkciju koja resolved layout prijavljuje u obliku koji test može da potvrdi. Komentari koji tvrde da se struct poklapa sa zaglavljem ne vrede ništa; SizeOf i field offset ispisan pri pokretanju vrede mnogo. PKCS#11 backend, CNG backend i ostatak signing stack-a isporučuju se u PDFium Component-u za Delphi i C++Builder, gde je ABI plumbing već uslovljen tako da vaš kod može da ostane na strani tokena