PDFiumPas potpisuje PAdES dokumente kroz PKCS#11 token na Windowsu, Linuxu i macOS-u, a dvije činjenice o platformi odlučuju hoće li to povezivanje uopće raditi: CK_ULONG je C-ov unsigned long, dakle 4 bajta na Windowsu i 8 bajtova na Linuxu i macOS-u, a zaglavlja PKCS#11 primjenjuju #pragma pack(1) samo na Windowsu, što pomiče svaki pokazivač u tablici funkcija. Pogriješite li bilo što od toga, modul se i dalje učitava, pozivi i dalje vraćaju, a brojevi koji dolaze natrag su smeće. To je oblik kvara koji treba očekivati. Nitko vam ne preda linkerovu pogrešku jer se ništa ne povezuje: modul je .so, .dylib ili .dll koji se u izvođenju otvara po putanji, a cijela je površina zapis pokazivača na funkcije koji pretvarate i pozivate. Kompajler nema pojma kako je izgledalo C zaglavlje s druge strane. Svako neslaganje ostaje tiho sve do rušenja
Zašto PKCS#11 povezivanje zakazuje nasumičnim CKR kodovima umjesto čistom pogreškom?
Jer neslaganje ABI-ja uopće ne proizvodi stanje pogreške, nego pogrešnu adresu ili pomak, a token poslušno odgovara na pitanje koje se time slučajno postavilo. Ne postoji sloj između vaše deklaracije zapisa i modula koji bi neslaganje mogao primijetiti. Iz toga izlaze dva različita načina kvara. Ako je pakiranje pogrešno, utor koji čitate kao C_GetSlotList drži šest bajtova jednog pokazivača i dva sljedećega, a poziv skače u nemapiranu memoriju ili, još gore, u sredinu neke druge funkcije. To je access violation. Ako je širina CK_ULONG-a pogrešna, adrese su dobre, ali podaci nisu: izlazni parametar var Count: CK_ULONG deklariran kao širok 4 bajta dobije 8 zapisanih bajtova iz LP64 modula i tiho prepiše sljedeća četiri bajta vašeg stack framea, a predložak CK_ATTRIBUTE čiji je ValueLen na pogrešnom pomaku navodi modul da polje duljine čita iz vašeg pokazivača Value. Token tada vrati posve legitiman CKR_BUFFER_TOO_SMALL ili CKR_ATTRIBUTE_VALUE_INVALID za pitanje koje nikad niste postavili. Ti kodovi ljude satima šalju u potragu kroz konfiguraciju tokena. Pogreška je četiri retka više u deklaraciji tipa
CK_ULONG je C-ov unsigned long, a ne tip fiksne širine
CK_ULONG je u zaglavljima PKCS#11 definiran kao C-ov unsigned long, što znači da mu širina slijedi podatkovni model platforme, a ne specifikaciju. Windows je LLP64, pa unsigned long ostaje širok 32 bita čak i u 64-bitnom procesu. Linux i macOS su LP64, pa prati pokazivač i postaje širok 64 bita. To je najvažnija linija u cijeloj jedinici jer je u PKCS#11 praktički svaki skalar CK_ULONG: ID-ovi slotova, ručke sesija, ručke objekata, klase objekata, tipovi ključeva, tipovi atributa, tipovi mehanizama, duljine međuspremnika i sama povratna vrijednost CK_RV
type
{$IFDEF MSWINDOWS}
// Windows je LLP64: C unsigned long ondje ostaje širok 32 bita
CK_ULONG = LongWord;
{$ELSE}
// Linux i macOS su LP64: unsigned long slijedi š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;
Poanta je svakom od tih tipova dati alias CK_ULONG umjesto izravnog aliasa na LongWord ili UInt64. Time se uvjet pojavljuje točno jednom. Napišite bilo koji od njih konkretno i postavili ste minu na koju će budući port nagaziti, i to na mjestu koje ste zaboravili
Što pragma pack(1) radi tablici funkcija PKCS#11?
Pomiče svaki pokazivač funkcije u CK_FUNCTION_LIST jer se tablica otvara dvobajtnim CK_VERSIONom. Pri prirodnom poravnanju kompajler iza te verzije umeće šest bajtova nadopune, pa prvi pokazivač funkcije sjeda na pomak 8. Pri pakiranju po bajtu nema nadopune, pa sjeda na pomak 2. Svaki sljedeći unos nasljeđuje isti pomak, zbog čega pogrešno pakiranje nije problem jednog polja nego cijele tablice. Zamka je da zaglavlja PKCS#11 primjenjuju #pragma pack(1) samo na Windowsu. To je razlika platforme, a ne modula: dvije izgradnje iste biblioteke dobavljača ne slažu se o tome, ovisno o hostu iz kojeg dolaze. Primijetite i da pakiranje ništa ne mijenja za strukture čija su sva polja široka koliko i pokazivač, što je većina njih, pa naivan test koji dira samo CK_SLOT_INFO rado prolazi dok je tablica ispod pomaknuta 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 i razlog zbog kojeg se tablica pomiče
C_Initialize: Pointer; // pomak 2 pri pakiranju, pomak 8 pri poravnanju
C_Finalize: Pointer;
C_GetInfo: Pointer;
C_GetFunctionList: Pointer;
C_GetSlotList: Pointer;
// Tablica ima fiksni redoslijed; deklaracija prefiksa
// do C_Sign dovoljna je da se dođe do svega što ovaj backend poziva
C_SignInit: Pointer;
C_Sign: Pointer;
end;
PCK_FUNCTION_LIST = ^CK_FUNCTION_LIST;
{$IFDEF FPC}{$PACKRECORDS DEFAULT}{$ELSE}{$A8}{$ENDIF}
Tri su stvari u tom bloku važnije nego što izgledaju. {$PACKRECORDS C} nije isto što i nema direktive; govori Free Pascalu da slijedi pravila poravnanja platformskog C kompajlera, što je upravo ugovor koji trebate na Linuxu i macOS-u. Grana Delphija bezuvjetni je {$A1} jer izgradnje PDFiumPasa u Delphiju ciljaju Windows, dok FPC nosi izgradnje za Linux i macOS. A redak za vraćanje na dnu nije kozmetika: ostavite jedinicu spakiranu i svaki zapis deklariran nakon ove točke tiho promijeni raspored, što je upravo ona vrsta kvara s djelovanjem na daljinu koju učvršćivanje povezivanja PDFium komponente protiv ABI i memorijskih kvarova treba ukloniti
Pkcs11AbiLayout: pretvaranje rasporeda u tvrdnju
Pkcs11AbiLayout prijavljuje raspored koji je izgradnja stvarno razriješila kao jedan string koji se može provjeriti, oblika ulong=4 attr=16 pss=12 table=2. 64-bitna izgradnja Windowsa mora prijaviti točno to, a LP64 cilj mora prijaviti ulong=8 attr=24 pss=24 table=8. Sve drugo znači da bi poziv kroz tablicu funkcija sletio na pogrešan utor, a funkcija postoji kako bi jedinični test to mogao glasno reći umjesto komentara koji 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 C_GetFunctionList vrati tablicu:
// nevjerojatna verzija ili nil ulazna točka znači da je zapis složen
// s pogrešnim pakiranjem ili širinom CK_ULONG-a, 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 spakirano na Windowsu x64, 8 + 8 + 8 poravnato na LP64. pss je CK_RSA_PKCS_PSS_PARAMS, tri polja CK_ULONG-a, dakle 12 ili 24. table je pomak prvog pokazivača funkcije i vrijednost koja prva uhvati pogrešno pakiranje. Delphi testni slučaj provjerava string pod {$IFDEF MSWINDOWS}; Lazarusov paket provjerava isto. Jedna provjera jednakosti pokriva raspored koji bi se inače mogao provjeriti samo čitanjem C zaglavlja uz Pascalov zapis i povjerenjem u sebe. Provjera pri učitavanju druga je polovica iste ideje. PDFiumPas imenom kroz GetProcAddress ili GetProcedureAddress razrješava samo C_GetFunctionList i svaki drugi ulaz uzima iz tablice koju taj poziv vrati, što je način na koji osnovna specifikacija OASIS PKCS #11 predviđa dohvat modula i zaobilazi imenovanje simbola po dobavljaču. Zatim provjerava ono što je dobio. Glavna verzija izvan raspona 2 do 3 ili nil C_Initialize, C_GetSlotList ili C_Sign znači da je zapis neporavnat i modul se odbacuje umjesto da se kroz njega poziva
Potpisivanje kroz tablicu: mehanizmi, DigestInfo i dvoprolazni C_Sign
Kada je raspored ispravan, posao potpisivanja malen je jer ugovor ICmsSigner koji PDFiumPas traži od backenda ima pet metoda, a četiri od njih samo vraćaju OID-ove i identifikator potpisnika. Samo SignSignedAttrsDigest nešto radi: uzima 32-bajtni SHA-256 sažetak potpisanih atributa i vraća bajtove potpisa. Sastavljanje CMS-a, ASN.1, označavanje vremena prema RFC 3161 i DSS/LTV ne ovise o platformi i već su dovršeni, što je ista podjela posla koja omogućuje da se udaljene PAdES sesije potpisivanja prema HSM-u ili servisu ključeva u oblaku ukopčaju u isti spoj. Tri detalja mehanizma koštat će vas neuspjele provjere ako ih preskočite. CKM_RSA_PKCS primjenjuje PKCS#1 v1.5 padding, ali ne gradi DigestInfo, pa pozivatelj sam dodaje 19-bajtni prefiks SHA-256 DigestInfo iz RFC-a 8017; predajte tokenu goli sažetak i dobit ćete ispravan potpis nad pogrešnom stvari. CKM_RSA_PKCS_PSS i CKM_ECDSA uzimaju sažetak kako je predan, ali CKM_ECDSA odgovara sirovim parom r||s, a CMS treba SEQUENCE ECDSA-Sig-Value iz RFC-a 3279 §2.2.3, pa ga PDFiumPas pretvara. A C_Sign je namjerno u dva prolaza: pozovite ga s nil međuspremnikom da od tokena zatražite duljinu potpisa, pa ponovno s međuspremnikom 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);
// Zapiši ovo prije svega drugog 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 vrijedi znati prije prvog tokena. Moduli se keširaju po putanji jer je C_Initialize jednom po procesu i modulu, a ponovljeni poziv odgovara s CKR_CRYPTOKI_ALREADY_INITIALIZED (0x00000190), što PDFiumPas tretira kao uspjeh uz pretpostavku da je drugi dio hosta već inicijalizirao istu biblioteku. Stringovi tokena, poput opisa slota i oznake tokena, fiksna su polja popunjena razmacima, a ne završena NUL-om, pa ih treba obrezati s kraja. A CKO_CERTIFICATE je 1, a ne 2 — 0 je CKO_DATA, a 2 je CKO_PUBLIC_KEY. Zapisivanje te konstante iz sjećanja pogreška je koja proizvodi prazan rezultat pretrage i nikakvu pogrešku
Što je provjereno i gdje jamstvo prestaje?
Budite jasni o granici jer je uža nego što opis značajke sugerira. Ono što je danas provjereno u PDFiumPasu jest da ABI raspored odgovara C zaglavljima polje po polje na objema granama, da se odsutan ili nečitljiv modul pretvara u prijavljeni neuspjeh umjesto u rušenje i da i Delphi i FPC lanci alata grade jedinicu. Stvarni putovi tokena — C_Login, pretraga objekata, C_Sign nad hardverom — nisu pokrenuti jer razvojni host uopće nema instaliran PKCS#11 modul. Najprije podignite SoftHSM2 i potvrdite Pkcs11AbiLayout prije nego što priključite fizički token, kako se ABI problem i problem tokena nikad ne bi morali dijagnosticirati istodobno. Još jednu asimetriju vrijedi imenovati. Strana potpisivanja sada je prenosiva među platformama; strana provjere nije. Provjera CMS-a unutar PDFiumPasa i dalje je zaštićena s {$IFDEF MSWINDOWS} i drugdje vraća pcsUnsupported, a nema točku za ubrizgavanje providera jednaku pozadini potpisnika. Zato Linux servis može proizvesti PAdES B-B potpis nad ključem koji drži token, ali još ne može provjeriti vlastiti izlaz na istom računalu. Dok se ta praznina ne zatvori, korak provjere planirajte na Windowsu ili u vanjskom validatoru
Pouka seže i izvan PKCS#11. Svaki Pascalov zapis koji zrcali uvjetno spakiranu C strukturu treba tri stvari: jedan uvjetni alias za skalar promjenjiv po platformi, tako da odluka o širini postoji točno na jednom mjestu, direktive pakiranja koje obuhvaćaju deklaracije i nakon toga se vraćaju, te runtime funkciju koja razriješeni raspored prijavljuje kao nešto što test može provjeriti. Komentari koji tvrde da zapis odgovara zaglavlju ne vrijede ništa; SizeOf i pomak polja ispisan pri pokretanju vrijede mnogo. PKCS#11 backend, CNG backend i ostatak skupa za potpisivanje isporučuju se u PDFium Component for Delphi and C++Builder, gdje je ABI vodovod već uvjetovan kako bi vaš kod mogao ostati na strani tokena tog problema