PDFiumPas semnează documente PAdES printr-un token PKCS#11 pe Windows, Linux și macOS, iar două fapte de platformă decid dacă binding-ul funcționează deloc: CK_ULONG este C unsigned long, deci are 4 bytes pe Windows și 8 bytes pe Linux și macOS, iar headers PKCS#11 aplică #pragma pack(1) doar pe Windows, mutând fiecare pointer din function table. Greșește oricare dintre ele și modulul se încarcă în continuare, apelurile se întorc în continuare, iar numerele care revin sunt garbage. Aceasta este forma de bug la care trebuie să te aștepți. Nimeni nu îți dă un linker error, deoarece nimic nu este linked: modulul este un .so, .dylib sau .dll deschis la runtime după path, iar întreaga suprafață este un struct de function pointers pe care îl castezi și îl apelezi. Compiler-ul nu știe cum arăta C header-ul de cealaltă parte. Fiecare mismatch rămâne tăcut până la crash
De ce eșuează un binding PKCS#11 cu CKR codes aleatoare în locul unei erori curate?
Deoarece un ABI mismatch nu produce deloc o condiție de eroare, ci un address sau offset greșit, iar token-ul răspunde disciplinat la orice întrebare care rezultă. Nu există niciun layer între record declaration și modul care să poată observa dezacordul. De aici ies două failure modes distincte. Dacă packing-ul este greșit, slotul pe care îl citești ca C_GetSlotList conține șase bytes dintr-un pointer și doi din următorul, iar apelarea lui sare într-o memorie unmapped sau, mai rău, în mijlocul unei alte funcții. Aceasta este access violation. Dacă CK_ULONG are lățimea greșită, adresele sunt bune, dar datele nu: un out-parameter var Count: CK_ULONG declarat pe 4 bytes primește 8 bytes scriși de un modul LP64, suprascriind în tăcere următorii patru bytes din stack frame, iar un template CK_ATTRIBUTE al cărui ValueLen stă la offset greșit face modulul să citească un length field din pointer-ul tău Value. Token-ul returnează apoi un CKR_BUFFER_TOO_SMALL sau CKR_ATTRIBUTE_VALUE_INVALID perfect legitim pentru o întrebare pe care nu ai pus-o. Codurile acestea trimit oamenii ore întregi să caute în token configuration. Bug-ul este cu patru linii mai sus, într-o type declaration
CK_ULONG este C unsigned long, nu un type cu lățime fixă
CK_ULONG este definit în headers PKCS#11 ca un C unsigned long, ceea ce înseamnă că lățimea lui urmează data model-ul platformei, nu specificația. Windows este LLP64, așa că unsigned long rămâne pe 32 de biți chiar și într-un process pe 64 de biți. Linux și macOS sunt LP64, așa că urmărește pointer-ul și devine pe 64 de biți. Aceasta este linia cu cea mai mare consecință din întregul unit, deoarece în PKCS#11 aproape fiecare scalar este un CK_ULONG: slot IDs, session handles, object handles, object classes, key types, attribute types, mechanism types, buffer lengths și chiar valoarea de retur CK_RV
type
{$IFDEF MSWINDOWS}
// Windows este LLP64: un C unsigned long rămâne pe 32 de biți
CK_ULONG = LongWord;
{$ELSE}
// Linux și macOS sunt LP64: unsigned long urmează lățimea pointer-ului
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;
Aliasarea tuturor acestor types la CK_ULONG în loc de LongWord sau UInt64 direct este scopul exercițiului. Înseamnă că decizia condiționată apare exact o dată. Scrie oricare dintre ele concret și ai plantat o mină pe care un port viitor o va călca, exact în locul pe care ai uitat să îl verifici
Ce face pragma pack(1) tabelului de funcții PKCS#11?
Mută fiecare function pointer din CK_FUNCTION_LIST, deoarece tabelul începe cu un CK_VERSION de doi bytes. Sub natural alignment, compiler-ul inserează șase bytes de padding după version, astfel încât primul function pointer ajunge la offset 8. Sub byte packing nu există padding, așa că ajunge la offset 2. Fiecare entry următor moștenește aceeași deplasare, de aceea o packing mistake nu este o problemă de un singur field, ci de întregul table. Capcana este că headers PKCS#11 aplică #pragma pack(1) doar pe Windows. Este o diferență de platformă, nu de module: două build-uri ale aceleiași vendor library nu sunt de acord în funcție de host-ul de pe care provin. Observă și că packing-ul nu schimbă nimic pentru structs ale căror fields sunt toate pointer-width, adică majoritatea, așa că un test naiv care atinge doar CK_SLOT_INFO va trece liniștit în timp ce tabelul de dedesubt este deplasat cu șase bytes
{$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; // doi bytes, motivul pentru care se mută tabelul
C_Initialize: Pointer; // offset 2 packed, offset 8 aligned
C_Finalize: Pointer;
C_GetInfo: Pointer;
C_GetFunctionList: Pointer;
C_GetSlotList: Pointer;
// ... tabelul are ordine fixă; declararea prefixului până la C_Sign
// este suficientă pentru a ajunge la toate entry point-urile apelate de backend
C_SignInit: Pointer;
C_Sign: Pointer;
end;
PCK_FUNCTION_LIST = ^CK_FUNCTION_LIST;
{$IFDEF FPC}{$PACKRECORDS DEFAULT}{$ELSE}{$A8}{$ENDIF}
Trei lucruri din acel block contează mai mult decât par. {$PACKRECORDS C} nu este același lucru cu „nicio directivă”; îi spune lui Free Pascal să urmeze regulile de alignment ale compiler-ului C de pe platformă, exact contractul de care ai nevoie pe Linux și macOS. Ramura Delphi este necondiționat {$A1} deoarece build-urile Delphi ale PDFiumPas țintesc Windows, în timp ce FPC poartă build-urile Linux și macOS. Iar linia de restore de la final nu este cosmetică: lasă unit-ul packed și fiecare record declarat după acest punct își schimbă în tăcere layout-ul, exact genul de defect action-at-a-distance pe care hardening-ul unui binding PDFium împotriva defectelor ABI și de memory safety încearcă să îl elimine
Pkcs11AbiLayout: transformarea layout-ului într-o aserțiune
Pkcs11AbiLayout raportează layout-ul rezolvat efectiv de build ca un singur string assertable de forma ulong=4 attr=16 pss=12 table=2. Un build Windows pe 64 de biți trebuie să raporteze exact atât, iar un target LP64 trebuie să raporteze ulong=8 attr=24 pss=24 table=8. Orice altceva înseamnă că un apel prin function table ar ajunge pe slotul greșit, iar funcția există pentru ca un unit test să poată spune asta cu voce tare, în loc ca un comment să pretindă că totul este bine
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;
// La load time, după ce C_GetFunctionList a returnat tabelul:
// o versiune neverosimilă sau un entry point nil înseamnă că record-ul a fost
// așezat cu packing sau CK_ULONG width greșite, așa că modulul este refuzat
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;
Cele patru numere nu sunt arbitrare. attr este dimensiunea lui CK_ATTRIBUTE, care conține un CK_ULONG, un pointer și un CK_ULONG: 4 + 8 + 4 packed pe Windows x64, 8 + 8 + 8 aligned pe LP64. pss este CK_RSA_PKCS_PSS_PARAMS, cu trei fields CK_ULONG, deci 12 sau 24. table este offset-ul primului function pointer și valoarea care prinde prima o greșeală de packing. Test case-ul Delphi verifică string-ul sub {$IFDEF MSWINDOWS}, iar suita Lazarus verifică același lucru. Un singur equality check acoperă un layout care altfel ar putea fi verificat doar citind un C header alături de un Pascal record și având încredere în tine. Check-ul la load time este a doua jumătate a aceleiași idei. PDFiumPas rezolvă doar C_GetFunctionList după nume prin GetProcAddress sau GetProcedureAddress și ia orice alt entry point din tabelul returnat de apel, exact cum intenționează OASIS PKCS #11 base specification și fără simboluri numite per vendor. Apoi face sanity-check la ce a primit. O versiune majoră în afara intervalului 2 până la 3 sau un C_Initialize, C_GetSlotList ori C_Sign nil înseamnă că record-ul este desaliniat, iar modulul este abandonat în loc să fie apelat prin el
Signing prin tabel: mechanisms, DigestInfo și C_Sign în două treceri
După ce layout-ul este corect, signing work este mic, deoarece contractul ICmsSigner pe care PDFiumPas îl cere unui backend are cinci methods, iar patru dintre ele returnează doar OID-uri și signer identifier. Doar SignSignedAttrsDigest face ceva: primește digest-ul SHA-256 de 32 de bytes al signed attributes și returnează signature bytes. CMS assembly, ASN.1, RFC 3161 timestamping și DSS/LTV sunt toate platform-independent și deja realizate, aceeași împărțire care permite remote PAdES signing sessions către un HSM sau un cloud key service să intre în același seam. Trei detalii de mechanism îți vor costa o verification failure dacă le sari. CKM_RSA_PKCS aplică padding PKCS#1 v1.5, dar nu construiește DigestInfo, așa că caller-ul adaugă singur prefixul DigestInfo SHA-256 de 19 bytes din RFC 8017; transmite digest-ul gol token-ului și primești o semnătură bine formată peste lucrul greșit. CKM_RSA_PKCS_PSS și CKM_ECDSA primesc digest-ul așa cum este, dar CKM_ECDSA răspunde cu perechea raw r||s, iar CMS are nevoie de ECDSA-Sig-Value SEQUENCE din RFC 3279 §2.2.3, așa că PDFiumPas o convertește. Iar C_Sign este two-pass prin design: apelează-l cu un buffer nil ca să ceri token-ului lungimea semnăturii, apoi din nou cu un buffer de acea dimensiune
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);
// Loghează asta înainte de orice altceva când un token se comportă ciudat pe o platformă nouă
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;
Câteva lucruri mai mici merită știute înainte de primul token. Modulele sunt cache-uite după path deoarece C_Initialize se face o singură dată per process per module, iar un apel repetat răspunde CKR_CRYPTOKI_ALREADY_INITIALIZED (0x00000190), pe care PDFiumPas îl tratează ca succes, presupunând că o altă parte a host-ului a inițializat deja aceeași bibliotecă. Token strings precum slot description și token label sunt fields fixed-width cu padding de spații, nu NUL-terminated, așa că trebuie tăiate de la coadă. Iar CKO_CERTIFICATE este 1, nu 2 — 0 este CKO_DATA, iar 2 este CKO_PUBLIC_KEY. Scrierea acestei constante din memorie produce un search result gol și nicio eroare
Ce este verificat și unde se oprește garanția
Fii clar cu granița, deoarece este mai îngustă decât sugerează descrierea feature-ului. Ce este verificat astăzi în PDFiumPas este că ABI layout-ul corespunde field cu field C headers pe ambele branches, că un modul absent sau imposibil de încărcat degradează la un failure raportat în locul unui crash și că atât toolchain-urile Delphi, cât și cele FPC construiesc unit-ul. Rutele token-ului real — C_Login, object search, C_Sign împotriva hardware — nu au fost exercitate, deoarece host-ul de development nu are instalat niciun modul PKCS#11. Pornește întâi SoftHSM2 și confirmă Pkcs11AbiLayout înainte să conectezi un token fizic, astfel încât o problemă ABI și una de token să nu trebuiască diagnosticate în același timp. O altă asimetrie merită numită. Signing side este acum cross-platform; verification side nu este. Verificarea CMS în PDFiumPas este încă protejată de {$IFDEF MSWINDOWS} și returnează pcsUnsupported în rest, fără un provider injection point echivalent backend-ului de signer. Așadar un serviciu Linux poate produce o semnătură PAdES B-B peste o cheie deținută de token, dar încă nu își poate verifica output-ul pe aceeași mașină. Planifică verificarea pe Windows sau într-un validator extern până când această diferență este închisă
Lecția se generalizează dincolo de PKCS#11. Orice Pascal record care oglindește un C struct cu packing condiționat are nevoie de trei lucruri: un alias condiționat pentru scalar-ul cu lățime variabilă al platformei, astfel încât decizia de width să existe într-un singur loc, packing directives care învelesc declarations și sunt restaurate ulterior și o funcție runtime care raportează layout-ul rezolvat în ceva ce un test poate verifica. Comments care pretind că un struct corespunde header-ului nu valorează nimic; SizeOf și un field offset tipărit la startup valorează mult. Backend-ul PKCS#11, backend-ul CNG și restul signing stack-ului sunt livrate în PDFium Component pentru Delphi și C++Builder, unde ABI plumbing-ul este deja condiționat ca tu să poți rămâne pe partea de token a problemei