PDFiumPas menandatangani dokumen PAdES melalui token PKCS#11 di Windows, Linux, dan macOS, dan dua fakta platform menentukan apakah binding itu dapat bekerja sama sekali: CK_ULONG adalah C unsigned long, sehingga 4 byte di Windows dan 8 byte di Linux serta macOS, sementara header PKCS#11 menerapkan #pragma pack(1) hanya di Windows, yang memindahkan setiap pointer dalam function table. Salah salah satunya dan module tetap termuat, call tetap mengembalikan nilai, tetapi angka yang datang kembali menjadi garbage. Itulah bentuk bug yang perlu Anda harapkan. Tidak ada linker error karena tidak ada yang di-link: module berupa .so, .dylib, atau .dll yang dibuka saat runtime berdasarkan path, dan seluruh surface-nya adalah struct function pointer yang Anda cast lalu panggil. Compiler tidak tahu seperti apa C header di sisi lain. Setiap mismatch bersifat diam-diam sampai berubah menjadi crash
Mengapa PKCS#11 binding gagal dengan CKR acak, bukan error yang bersih?
Karena ABI mismatch sama sekali tidak menghasilkan error condition; mismatch menghasilkan address atau offset yang salah, lalu token dengan patuh menjawab pertanyaan apa pun yang ternyata dituju. Tidak ada layer antara record declaration Anda dan module yang dapat menyadari ketidaksepakatan tersebut. Dari sini muncul dua failure mode berbeda. Jika packing salah, slot yang Anda baca sebagai C_GetSlotList berisi enam byte dari satu pointer dan dua byte dari pointer berikutnya, lalu pemanggilannya melompat ke unmapped memory atau, lebih buruk lagi, ke tengah function lain. Itulah access violation. Jika lebar CK_ULONG salah, address benar tetapi datanya tidak: out-parameter var Count: CK_ULONG yang dideklarasikan selebar 4 byte ditulisi 8 byte oleh module LP64, diam-diam menimpa empat byte berikutnya di stack frame Anda, dan template CK_ATTRIBUTE yang ValueLen-nya berada pada offset salah membuat module membaca length field dari pointer Value Anda. Token kemudian mengembalikan CKR_BUFFER_TOO_SMALL atau CKR_ATTRIBUTE_VALUE_INVALID yang sepenuhnya sah untuk pertanyaan yang tidak pernah Anda ajukan. Code tersebut membuat orang berjam-jam memburu konfigurasi token. Bug-nya berada empat baris di atas dalam type declaration
CK_ULONG adalah C unsigned long, bukan fixed-width type
CK_ULONG didefinisikan oleh header PKCS#11 sebagai C unsigned long, yang berarti lebarnya mengikuti platform data model, bukan specification. Windows adalah LLP64, sehingga unsigned long tetap 32-bit bahkan dalam proses 64-bit. Linux dan macOS adalah LP64, sehingga type itu mengikuti pointer dan menjadi 64-bit. Ini adalah satu baris paling konsekuensial dalam seluruh unit, karena di PKCS#11 hampir setiap scalar adalah CK_ULONG: slot ID, session handle, object handle, object class, key type, attribute type, mechanism type, buffer length, dan return value CK_RV itu sendiri
type
{$IFDEF MSWINDOWS}
// Windows adalah LLP64: C unsigned long tetap 32-bit di sana
CK_ULONG = LongWord;
{$ELSE}
// Linux dan macOS adalah LP64: unsigned long mengikuti lebar pointer
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;
Men-alias setiap type tersebut ke CK_ULONG, bukan langsung ke LongWord atau UInt64, adalah inti pekerjaannya. Dengan begitu conditional hanya muncul tepat satu kali. Tuliskan salah satunya secara konkret dan Anda telah menanam landmine yang akan diinjak port berikutnya, tepat pada tempat yang Anda lupa
Apa yang dilakukan pragma pack(1) terhadap PKCS#11 function table?
Directive itu menggeser setiap function pointer dalam CK_FUNCTION_LIST, karena table dibuka dengan CK_VERSION dua byte. Dalam natural alignment, compiler menyisipkan enam byte padding setelah version tersebut, sehingga function pointer pertama berada pada offset 8. Dalam byte packing tidak ada padding, sehingga pointer berada pada offset 2. Setiap entry berikutnya mewarisi displacement yang sama, dan itulah sebabnya packing mistake bukan masalah satu field, melainkan masalah seluruh table. Jebakannya adalah header PKCS#11 hanya menerapkan #pragma pack(1) di Windows. Ini perbedaan platform, bukan perbedaan module: dua build dari vendor library yang sama dapat tidak sepakat bergantung pada host asalnya. Perhatikan juga bahwa packing tidak mengubah apa pun pada structure yang semua field-nya berukuran pointer, dan sebagian besar memang demikian, sehingga test naif yang hanya menyentuh CK_SLOT_INFO akan lulus dengan senang hati sementara table di bawahnya bergeser enam byte
{$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; // dua byte, alasan table berpindah
C_Initialize: Pointer; // offset 2 packed, offset 8 aligned
C_Finalize: Pointer;
C_GetInfo: Pointer;
C_GetFunctionList: Pointer;
C_GetSlotList: Pointer;
// ... table berada dalam urutan tetap; mendeklarasikan prefix
// sampai C_Sign cukup untuk mencapai semua yang dipanggil backend ini
C_SignInit: Pointer;
C_Sign: Pointer;
end;
PCK_FUNCTION_LIST = ^CK_FUNCTION_LIST;
{$IFDEF FPC}{$PACKRECORDS DEFAULT}{$ELSE}{$A8}{$ENDIF}
Tiga hal di block tersebut lebih penting daripada kelihatannya. {$PACKRECORDS C} tidak sama dengan "tanpa directive"; directive tersebut memberi tahu Free Pascal untuk mengikuti aturan alignment compiler C platform, persis contract yang dibutuhkan di Linux dan macOS. Branch Delphi selalu memakai {$A1} karena build PDFiumPas Delphi menargetkan Windows, sedangkan FPC membawa build Linux dan macOS. Dan baris restore di bagian bawah bukan kosmetik: biarkan unit tetap packed, maka setiap record yang dideklarasikan setelah titik ini diam-diam mengubah layout juga, tepat jenis defect action-at-a-distance yang hendak dihilangkan oleh hardening binding component PDFium terhadap ABI dan memory-safety fault
Pkcs11AbiLayout: mengubah layout menjadi assertion
Pkcs11AbiLayout melaporkan layout yang benar-benar di-resolve oleh build sebagai satu string yang dapat di-assert dalam bentuk ulong=4 attr=16 pss=12 table=2. Build Windows 64-bit harus melaporkan tepat itu, sedangkan target LP64 harus melaporkan ulong=8 attr=24 pss=24 table=8. Nilai lain berarti call melalui function table akan mendarat pada slot yang salah, dan function tersebut ada agar unit test dapat mengatakannya secara terbuka, bukan membiarkan comment yang mengklaimnya
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;
// Saat load, setelah C_GetFunctionList mengembalikan table:
// version yang tidak masuk akal atau entry point nil berarti record dibentuk
// dengan packing atau lebar CK_ULONG yang salah, jadi module ditolak
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;
Empat angka tersebut bukan sembarang angka. attr adalah ukuran CK_ATTRIBUTE, yang memuat CK_ULONG, pointer, dan CK_ULONG: 4 + 8 + 4 packed pada Windows x64, 8 + 8 + 8 aligned pada LP64. pss adalah CK_RSA_PKCS_PSS_PARAMS, tiga CK_ULONG field, sehingga hasilnya 12 atau 24. table adalah offset function pointer pertama, dan nilai inilah yang pertama kali menangkap packing mistake. Delphi test case melakukan assertion atas string tersebut di bawah {$IFDEF MSWINDOWS}; Lazarus suite melakukan assertion yang sama. Satu equality check mencakup layout yang sebaliknya hanya dapat diverifikasi dengan membaca C header berdampingan dengan Pascal record lalu mempercayai diri sendiri. Load-time check adalah separuh kedua dari gagasan yang sama. PDFiumPas hanya me-resolve C_GetFunctionList berdasarkan nama melalui GetProcAddress atau GetProcedureAddress, lalu mengambil semua entry point lain dari table yang dikembalikan pemanggilan tersebut, sebagaimana module seharusnya diakses menurut OASIS PKCS #11 base specification, sekaligus menghindari symbol naming per vendor. Setelah itu PDFiumPas melakukan sanity check pada hasilnya. Major version di luar 2 sampai 3, atau C_Initialize, C_GetSlotList, atau C_Sign yang nil, berarti record misaligned, dan module dibuang, bukan dipanggil melalui layout tersebut
Signing melalui table: mechanism, DigestInfo, dan C_Sign dua pass
Setelah layout benar, pekerjaan signing kecil karena contract ICmsSigner yang diminta PDFiumPas dari backend memiliki lima method dan empat di antaranya hanya mengembalikan OID serta signer identifier. Hanya SignSignedAttrsDigest yang melakukan pekerjaan: method itu menerima digest SHA-256 32 byte dari signed attribute lalu mengembalikan signature byte. CMS assembly, ASN.1, RFC 3161 timestamping, dan DSS/LTV semuanya platform-independent dan sudah selesai, pembagian kerja yang sama yang memungkinkan remote PAdES signing session ke HSM atau cloud key service dipasang pada seam identik. Tiga detail mechanism akan membuat verification gagal jika dilewati. CKM_RSA_PKCS menerapkan PKCS#1 v1.5 padding tetapi tidak membangun DigestInfo, sehingga caller sendiri menambahkan 19-byte SHA-256 DigestInfo prefix dari RFC 8017; berikan bare digest ke token dan Anda memperoleh signature yang terbentuk dengan baik atas hal yang salah. CKM_RSA_PKCS_PSS dan CKM_ECDSA menerima digest apa adanya, tetapi CKM_ECDSA menjawab dengan raw r||s pair, sedangkan CMS membutuhkan ECDSA-Sig-Value SEQUENCE dari RFC 3279 §2.2.3, sehingga PDFiumPas melakukan konversi. Dan C_Sign memang two-pass: panggil dengan nil buffer untuk meminta panjang signature dari token, lalu panggil lagi dengan buffer sebesar itu
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);
// Catat ini terlebih dahulu ketika token bermasalah pada platform baru
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;
Beberapa hal kecil layak diketahui sebelum token pertama Anda. Module di-cache berdasarkan path karena C_Initialize dilakukan sekali per process per module, dan pemanggilan ulang menjawab CKR_CRYPTOKI_ALREADY_INITIALIZED (0x00000190), yang diperlakukan PDFiumPas sebagai sukses dengan asumsi bagian lain dari host sudah menginisialisasi library yang sama. String token seperti slot description dan token label adalah field fixed-width yang diisi spasi, bukan NUL-terminated, sehingga harus dipangkas dari bagian akhir. Dan CKO_CERTIFICATE adalah 1, bukan 2 — 0 adalah CKO_DATA dan 2 adalah CKO_PUBLIC_KEY. Menulis constant tersebut dari ingatan adalah kesalahan yang menghasilkan search result kosong tanpa error sama sekali
Apa yang diverifikasi, dan di mana jaminannya berhenti?
Jelaskan batasnya dengan terang karena lebih sempit daripada yang disarankan feature description. Yang diverifikasi PDFiumPas hari ini adalah bahwa ABI layout cocok dengan C header field demi field pada kedua branch, module yang tidak ada atau tidak dapat di-load turun menjadi failure yang dilaporkan, bukan crash, dan toolchain Delphi serta FPC sama-sama membangun unit tersebut. Real token path — C_Login, object search, dan C_Sign terhadap hardware — belum dijalankan karena development host sama sekali tidak memiliki PKCS#11 module terpasang. Jalankan SoftHSM2 terlebih dahulu dan pastikan Pkcs11AbiLayout benar sebelum memasang token fisik, agar ABI problem dan token problem tidak perlu didiagnosis bersamaan. Ada satu asymmetry lain yang patut disebut. Signing side kini cross-platform; verification side belum. CMS verification di dalam PDFiumPas masih dijaga oleh {$IFDEF MSWINDOWS} dan mengembalikan pcsUnsupported di tempat lain, serta tidak memiliki provider injection point yang setara dengan signer backend. Jadi Linux service dapat menghasilkan signature PAdES B-B dengan key yang dipegang token tetapi belum dapat memeriksa output-nya sendiri di mesin yang sama. Rencanakan langkah verification di Windows atau external validator sampai celah tersebut tertutup
Pelajarannya berlaku di luar PKCS#11. Setiap Pascal record yang mencerminkan C struct dengan packing conditional membutuhkan tiga hal: satu conditional alias untuk scalar yang lebar-nya bergantung platform agar keputusan width hanya ada di satu tempat, packing directive yang mengapit deklarasi lalu dipulihkan setelahnya, serta runtime function yang melaporkan layout ter-resolve sebagai sesuatu yang dapat di-assert oleh test. Comment yang mengklaim struct cocok dengan header tidak bernilai; SizeOf dan field offset yang dicetak saat startup sangat bernilai. PKCS#11 backend, CNG backend, dan signing stack lainnya tersedia dalam PDFium Component for Delphi and C++Builder, dengan ABI plumbing yang sudah dikondisikan agar code Anda dapat tetap berada di sisi token dari masalah ini