PDFiumPas, Windows, Linux ve macOS üzerinde PKCS#11 token ile PAdES belge imzalar ve bu binding'in çalışıp çalışmayacağını iki platform gerçeği belirler: CK_ULONG C'deki unsigned long'dur, yani Windows'ta 4 bayt, Linux ve macOS'ta 8 bayttır; PKCS#11 header'ları #pragma pack(1) direktifini yalnızca Windows'ta uygular ve bu da function table içindeki bütün pointer'ları yerinden oynatır. İkisinden birini yanlış yaparsanız module yine yüklenir, çağrılar yine döner ve geri gelen sayılar garbage olur. Beklemeniz gereken bug biçimi budur. Linker error gelmez, çünkü linklenen hiçbir şey yoktur: module, runtime'da path üzerinden açtığınız bir .so, .dylib veya .dll'dir ve bütün yüzey cast edip çağırdığınız function pointer'lardan oluşan bir struct'tır. Compiler diğer taraftaki C header'ın nasıl göründüğünü bilemez. Her mismatch ancak crash olana kadar sessizdir
PKCS#11 binding neden temiz bir hata yerine rastgele CKR code'larıyla başarısız olur?
Çünkü bir ABI mismatch hiç error condition üretmez; yanlış bir adres veya yanlış bir ofset üretir ve token, bu durumun denk geldiği soruya neyse onu yanıtlar. Record declaration ile module arasında anlaşmazlığı fark edebilecek bir katman yoktur. Buradan iki ayrı failure mode çıkar. Packing yanlışsa C_GetSlotList olarak okuduğunuz slot bir pointer'ın altı baytını ve sonrakinin iki baytını taşır; onu çağırmak unmapped memory'ye veya daha kötüsü başka bir function'ın ortasına atlar. Bu access violation'dır. CK_ULONG genişliği yanlışsa adresler doğrudur ama data değildir: LP64 module'ün içine 8 bayt yazdığı var Count: CK_ULONG out-parameter'ı 4 bayt genişlikte declare etmek stack frame'in sonraki dört baytını sessizce overwrite eder; ValueLen'i yanlış ofsette duran bir CK_ATTRIBUTE template'i de module'ün length field'ını sizin Value pointer'ınızdan okumasına neden olur. Token, hiç sormadığınız bir soru için tamamen meşru CKR_BUFFER_TOO_SMALL veya CKR_ATTRIBUTE_VALUE_INVALID döndürür. Bu code'lar insanları saatlerce token configuration aramaya yollar. Bug, type declaration'da dört satır yukarıdadır
CK_ULONG C unsigned long'dur, fixed-width type değildir
CK_ULONG, PKCS#11 header'ları tarafından C unsigned long olarak tanımlanır; bu da genişliğinin specification'a değil platform data model'ine bağlı olduğu anlamına gelir. Windows LLP64'tür; bu yüzden 64 bitlik bir process'te bile unsigned long 32 bit kalır. Linux ve macOS LP64'tür; pointer'ı izler ve 64 bit olur. Bu, bütün unit içindeki en sonuç doğuran satırdır, çünkü PKCS#11'de pratik olarak her scalar bir CK_ULONG'dur: slot ID'leri, session handle'ları, object handle'ları, object class'ları, key type'ları, attribute type'ları, mechanism type'ları, buffer length'leri ve CK_RV return value'sunun kendisi
type
{$IFDEF MSWINDOWS}
// Windows LLP64'tür: C unsigned long orada 32 bit kalır
CK_ULONG = LongWord;
{$ELSE}
// Linux ve macOS LP64'tür: unsigned long pointer genişliğini izler
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;
Bunların her birini doğrudan LongWord veya UInt64 yerine CK_ULONG'a alias'lamak alıştırmanın amacıdır. Conditional yalnızca bir kez görünür. Herhangi birini concrete biçimde yazarsanız gelecekteki bir portun basacağı bir mayın döşemiş olursunuz; o da unuttuğunuz tek yerde basar
pragma pack(1) PKCS#11 function table'a ne yapar?
CK_FUNCTION_LIST içindeki her function pointer'ı kaydırır, çünkü table iki baytlık bir CK_VERSION ile açılır. Natural alignment altında compiler bu version'dan sonra altı bayt padding ekler ve ilk function pointer ofset 8'e düşer. Byte packing altında padding yoktur ve ofset 2 olur. Sonraki her entry aynı displacement'i devralır; bu nedenle packing hatası tek field problemi değil bütün table problemidir. Tuzak, PKCS#11 header'larının #pragma pack(1)'i yalnızca Windows'ta uygulamasıdır. Bu module farkı değil platform farkıdır: aynı vendor library'nin iki build'i geldikleri host'a göre bu konuda ayrışır. Ayrıca packing, field'larının tümü pointer-width olan ve çoğu record'u oluşturan yapılar için hiçbir şeyi değiştirmez; dolayısıyla yalnızca CK_SLOT_INFO'ya dokunan naif test, altındaki table altı bayt kaymışken neşeyle geçer
{$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; // iki bayt ve table'ın hareket etmesinin nedeni
C_Initialize: Pointer; // packed ofset 2, aligned ofset 8
C_Finalize: Pointer;
C_GetInfo: Pointer;
C_GetFunctionList: Pointer;
C_GetSlotList: Pointer;
// ... table sabit sıradadır; bu backend'in çağırdığı her şeye ulaşmak
// için C_Sign'e kadar olan prefix'i bildirmek yeterlidir
C_SignInit: Pointer;
C_Sign: Pointer;
end;
PCK_FUNCTION_LIST = ^CK_FUNCTION_LIST;
{$IFDEF FPC}{$PACKRECORDS DEFAULT}{$ELSE}{$A8}{$ENDIF}
Bu blokta göründüğünden daha önemli üç şey var. {$PACKRECORDS C} "directive yok" ile aynı değildir; Free Pascal'a platform C compiler'ının alignment kurallarını izlemeyi söyler ve Linux ile macOS'ta ihtiyacınız olan sözleşme tam olarak budur. Delphi branch'i koşulsuz {$A1}'dir, çünkü PDFiumPas Delphi build'leri Windows'u hedefler; FPC Linux ve macOS build'lerini taşır. Alttaki restore satırı kozmetik değildir: unit'i packed bırakırsanız bundan sonra declare edilen her record'un layout'u sessizce değişir. Bu, PDFium component binding'ini ABI ve memory-safety fault'larına karşı harden etme çalışmasının ortadan kaldırmak istediği action-at-a-distance kusurunun tam kendisidir
Pkcs11AbiLayout: layout'u assertion'a çevirmek
Pkcs11AbiLayout, build'in gerçekte çözdüğü layout'u ulong=4 attr=16 pss=12 table=2 biçiminde assertion yapılabilir tek bir string olarak raporlar. 64 bit Windows build'i tam olarak bunu, LP64 hedefi ise ulong=8 attr=24 pss=24 table=8 raporlamalıdır. Başka her şey function table üzerinden yapılacak bir çağrının yanlış slot'a ineceği anlamına gelir; bu function, bir comment'in iddia etmesi yerine unit test'in bunu yüksek sesle söylemesi için vardır
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;
// C_GetFunctionList table'ı döndürdükten sonra load zamanında:
// makul olmayan version veya nil entry point yanlış packing ya da CK_ULONG
// genişliğiyle layout yapıldığını gösterir; module'ü reddet
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;
Dört sayı rastgele değildir. attr, bir CK_ULONG, bir pointer ve bir CK_ULONG taşıyan CK_ATTRIBUTE'ın boyutudur: Windows x64'te packed 4 + 8 + 4, LP64'te aligned 8 + 8 + 8. pss, üç CK_ULONG field'ı olan CK_RSA_PKCS_PSS_PARAMS'ın boyutudur; 12 veya 24 olur. table, ilk function pointer'ın ofsetidir ve packing hatasını ilk yakalayan değerdir. Delphi test case string'i {$IFDEF MSWINDOWS} altında assertion yapar; Lazarus paketi de aynı şeyi yapar. Tek equality check, aksi halde bir C header'ı Pascal record'la yan yana okuyup kendinize güvenerek doğrulayabileceğiniz bir layout'u kapsar. Load-time check aynı fikrin ikinci yarısıdır. PDFiumPas, C_GetFunctionList'i GetProcAddress veya GetProcedureAddress ile name üzerinden çözer ve diğer tüm entry point'leri bu çağrının döndürdüğü table'dan alır; OASIS PKCS #11 base specification'ın bir module'e ulaşmayı amaçladığı biçim budur ve vendor başına symbol naming ihtiyacını ortadan kaldırır. Sonra geri geleni sanity-check eder. 2 ile 3 dışındaki major version veya nil C_Initialize, C_GetSlotList ya da C_Sign, record'un hizasız olduğu anlamına gelir; module çağrılmak yerine düşürülür
Table üzerinden imzalama: mechanism'ler, DigestInfo ve iki geçişli C_Sign
Layout doğru olduğunda signing işi küçüktür, çünkü PDFiumPas'ın bir backend'den karşılamasını istediği ICmsSigner contract'ı beş method taşır ve bunların dördü yalnızca OID ile signer identifier döndürür. Yalnızca SignSignedAttrsDigest iş yapar: signed attribute'ların 32 baytlık SHA-256 digest'ini alır ve signature baytlarını döndürür. CMS assembly, ASN.1, RFC 3161 timestamping ve DSS/LTV platformdan bağımsızdır ve zaten yapılmıştır; HSM veya cloud key service'e karşı remote PAdES signing session'larının aynı seam'e takılmasını sağlayan iş bölümü de budur. Üç mechanism ayrıntısını atlarsanız verification başarısız olur. CKM_RSA_PKCS, PKCS#1 v1.5 padding uygular ama DigestInfo'yu kurmaz; bu nedenle çağıran RFC 8017'deki 19 baytlık SHA-256 DigestInfo prefix'ini kendisi başa ekler. Token'a çıplak digest verirseniz yanlış şey üzerinde well-formed bir signature alırsınız. CKM_RSA_PKCS_PSS ve CKM_ECDSA digest'i verildiği gibi alır, ancak CKM_ECDSA raw r||s çiftiyle cevap verir ve CMS, RFC 3279 §2.2.3'ün ECDSA-Sig-Value SEQUENCE'ini ister; PDFiumPas bu dönüşümü yapar. C_Sign tasarım gereği iki geçişlidir: signature length'i token'a sormak için nil buffer ile çağırın, sonra o boyutta buffer ile tekrar çağırın
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);
// Token yeni bir platformda sorun çıkarırsa her şeyden önce bunu günlüğe yaz
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;
İlk token'ınızdan önce bilinmeye değer birkaç küçük şey var. Module'ler path'e göre cache edilir, çünkü C_Initialize process başına module başına bir kezdir; tekrar çağrı CKR_CRYPTOKI_ALREADY_INITIALIZED (0x00000190) döndürür ve PDFiumPas, host'un başka bir parçasının aynı library'yi zaten initialize ettiğini varsayarak bunu başarı sayar. Slot description ve token label gibi token string'leri NUL-terminated değil, sonu boşluklarla doldurulmuş fixed-width field'lardır; kuyruktan trim edilmeleri gerekir. Bir de CKO_CERTIFICATE 1'dir, 2 değil — 0 CKO_DATA, 2 CKO_PUBLIC_KEY'dir. Bu constant'ı hafızadan yazmak tamamen hatadır; hiçbir error olmadan empty search result üretir
Neyin doğrulandığı ve garantinin nerede bittiği
Sınırı açıkça bilin, çünkü feature description'ın düşündürdüğünden dardır. PDFiumPas bugün ABI layout'unun her iki branch'te de C header'larla field field eşleştiğini, eksik veya yüklenemeyen module'ün crash yerine raporlanmış failure'a döndüğünü ve hem Delphi hem FPC toolchain'lerinin unit'i build ettiğini doğrular. Gerçek token yolları — C_Login, object search ve hardware'a karşı C_Sign — geliştirme host'unda hiç PKCS#11 module'ü kurulu olmadığı için çalıştırılmadı. Önce SoftHSM2'yi ayağa kaldırın ve fiziksel token bağlamadan önce Pkcs11AbiLayout'u doğrulayın; böylece ABI problemi ile token problemi aynı anda teşhis edilmek zorunda kalmaz. Bir asimetriyi daha adlandırmak gerekir. Signing tarafı artık cross-platform'dur; verification tarafı değildir. PDFiumPas içindeki CMS verification hâlâ {$IFDEF MSWINDOWS} ile korunur ve başka yerde pcsUnsupported döndürür; signer backend'e eşdeğer provider injection point'i yoktur. Yani bir Linux service token-held key üzerinde PAdES B-B signature üretebilir ama aynı makinede kendi çıktısını henüz kontrol edemez. Bu boşluk kapanana kadar verification adımını Windows'a veya external validator'a planlayın
Ders PKCS#11'in ötesine genellenir. Koşullu biçimde pack edilen bir C struct'ı aynalayan her Pascal record üç şeye ihtiyaç duyar: platforma göre değişen scalar için width kararını tam bir yerde tutan tek conditional alias, declaration'ları çevreleyen ve sonrasında restore edilen packing directive'leri ve çözülmüş layout'u testin assertion yapabileceği bir şey olarak raporlayan runtime function. Bir struct'ın header'la eşleştiğini iddia eden comment'ler hiçbir şey etmez; startup'ta yazdırılan SizeOf ve field offset çok şey eder. PKCS#11 backend, CNG backend ve signing stack'in geri kalanı, ABI plumbing'i zaten koşullandırılmış halde gelen PDFium Component for Delphi and C++Builder içinde sunulur; kodunuz problem'in token tarafında kalabilir