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