PDFiumPas signerer PAdES-dokumenter gennem et PKCS#11-token på Windows, Linux og macOS, og to platformfakta afgør, om bindingen overhovedet virker: CK_ULONG er C unsigned long, altså 4 bytes på Windows og 8 bytes på Linux og macOS, og PKCS#11-headerne anvender #pragma pack(1) kun på Windows, hvilket flytter hver pointer i function table. Få bare én af dem forkert, og modulet indlæses stadig, kald returnerer stadig, og tallene, der kommer tilbage, er skrald. Det er den form på fejlen, du bør forvente. Ingen giver dig en linkerfejl, fordi intet linkes: Modulet er en .so, .dylib eller .dll, som åbnes ved runtime via en sti, og hele overfladen er en struct med function pointers, du caster og kalder. Compileren har ingen idé om, hvordan C-headeren på den anden side så ud. Enhver mismatch er lydløs indtil et crash
Hvorfor fejler en PKCS#11-binding med tilfældige CKR-koder i stedet for en ren fejl?
Fordi et ABI-mismatch slet ikke producerer en fejltilstand, men en forkert adresse eller en forkert offset, og tokenet svarer pligtskyldigt på det spørgsmål, som det tilfældigvis bliver til. Der er intet lag mellem din recorddeklaration og modulet, der kan opdage uenigheden. To forskellige fejltilstande kommer ud af det. Hvis packingen er forkert, indeholder den slot, du læser som C_GetSlotList, seks bytes fra én pointer og to fra den næste, og at kalde den hopper ind i unmapped memory eller, værre, midt i en anden funktion. Det er access violation. Hvis CK_ULONG har forkert bredde, er adresserne fine, men dataene er ikke: En var Count: CK_ULONG-out-parameter, der er deklareret 4 bytes bred, får 8 bytes skrevet ind i sig af et LP64-modul og overskriver lydløst de næste fire bytes i din stack frame, og en CK_ATTRIBUTE-template, hvis ValueLen ligger ved den forkerte offset, får modulet til at læse et længdefelt ud af din Value-pointer. Tokenet returnerer derefter en helt legitim CKR_BUFFER_TOO_SMALL eller CKR_ATTRIBUTE_VALUE_INVALID for et spørgsmål, du aldrig stillede. De koder sender folk på jagt gennem tokenkonfigurationen i timevis. Fejlen ligger fire linjer højere oppe i en typedefinition
CK_ULONG er C unsigned long og ikke en fast bredde
CK_ULONG defineres af PKCS#11-headerne som C unsigned long, hvilket betyder, at bredden følger platformens datamodel og ikke specifikationen. Windows er LLP64, så unsigned long forbliver 32-bit, selv i en 64-bit-proces. Linux og macOS er LP64, så den følger pointeren og bliver 64-bit. Det er den mest konsekvensrige linje i hele uniten, fordi næsten enhver scalar i PKCS#11 er en CK_ULONG: slot IDs, session handles, object handles, object classes, key types, attribute types, mechanism types, bufferlængder og selve CK_RV-returværdien
type
{$IFDEF MSWINDOWS}
// Windows er LLP64: en C unsigned long forbliver 32-bit dér
CK_ULONG = LongWord;
{$ELSE}
// Linux og macOS er LP64: unsigned long følger pointerbredden
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;
At aliasere hver af dem til CK_ULONG i stedet for direkte til LongWord eller UInt64 er pointen med øvelsen. Det betyder, at conditionalen optræder præcis ét sted. Skriv en af dem konkret, og du har skrevet en landmine, som en fremtidig port vil træde på, og den træder på den ene placering, du glemte
Hvad gør pragma pack(1) ved PKCS#11-function-tabellen?
Den flytter hver function pointer i CK_FUNCTION_LIST, fordi tabellen begynder med en tobytes CK_VERSION. Under naturlig alignment indsætter compileren seks bytes padding efter versionen, så den første function pointer lander ved offset 8. Under byte-packing er der ingen padding, så den lander ved offset 2. Hver efterfølgende entry arver den samme forskydning, og derfor er en packing-fejl ikke et one-field-problem, men et whole-table-problem. Fælden er, at PKCS#11-headerne kun anvender #pragma pack(1) på Windows. Det er en platformforskel og ikke en modulforskel: To builds af det samme leverandørbibliotek er uenige om dette alt efter, hvilken host de kom fra. Bemærk også, at packing ikke ændrer noget for strukturer, hvis felter alle har pointerbredde, hvilket gælder de fleste, så en naiv test, der kun rører CK_SLOT_INFO, består lystigt, mens tabellen under den er forskudt med seks 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; // to bytes og grunden til, at tabellen flytter sig
C_Initialize: Pointer; // offset 2 packed, offset 8 aligned
C_Finalize: Pointer;
C_GetInfo: Pointer;
C_GetFunctionList: Pointer;
C_GetSlotList: Pointer;
// ... tabellen har en fast rækkefølge; det er nok at deklarere præfikset
// til og med C_Sign for at nå alt, denne backend kalder
C_SignInit: Pointer;
C_Sign: Pointer;
end;
PCK_FUNCTION_LIST = ^CK_FUNCTION_LIST;
{$IFDEF FPC}{$PACKRECORDS DEFAULT}{$ELSE}{$A8}{$ENDIF}
Tre ting i den blok betyder mere, end de ser ud til. {$PACKRECORDS C} er ikke det samme som "ingen directive"; den fortæller Free Pascal at følge platformens C-compilers alignment-regler, hvilket præcis er den kontrakt, du har brug for på Linux og macOS. Delphi-grenen er ubetinget {$A1}, fordi Delphi-builds af PDFiumPas retter sig mod Windows, mens FPC bærer Linux- og macOS-builds. Og restore-linjen i bunden er ikke kosmetisk: Lad uniten være pakket, så ændrer hver record, der deklareres efter dette punkt, lydløst layout også, hvilket netop er den slags action-at-a-distance-defekt, som hardening af en PDFium-komponentbinding mod ABI- og memory-safety-fejl skal fjerne
Pkcs11AbiLayout: sådan gør du layoutet til en assertion
Pkcs11AbiLayout rapporterer det layout, buildet faktisk opløste, som én string, der kan asserts, på formen ulong=4 attr=16 pss=12 table=2. Et 64-bit-Windows-build skal rapportere præcis det, og et LP64-mål skal rapportere ulong=8 attr=24 pss=24 table=8. Alt andet betyder, at et kald gennem function table ville lande på den forkerte slot, og funktionen findes, så en unit-test kan sige det højt i stedet for en kommentar, der hævder det
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;
// Ved load-tidspunktet, efter at C_GetFunctionList har givet tabellen tilbage:
// en usandsynlig version eller et nil entry point betyder, at recorden blev
// lagt med forkert packing eller CK_ULONG-bredde, så modulet skal afvises
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;
De fire tal er ikke tilfældige. attr er størrelsen på CK_ATTRIBUTE, som indeholder en CK_ULONG, en pointer og en CK_ULONG: 4 + 8 + 4 packed på Windows x64, 8 + 8 + 8 aligned på LP64. pss er CK_RSA_PKCS_PSS_PARAMS, tre CK_ULONG-felter, altså 12 eller 24. table er offsetten til den første function pointer, og det er den værdi, der først fanger en packing-fejl. Delphi-testcasen hævder strengen under {$IFDEF MSWINDOWS}; Lazarus-suiten hævder det samme. Én lighedskontrol dækker et layout, der ellers kun kunne verificeres ved at læse en C-header side om side med en Pascal-record og stole på sig selv. Load-time-kontrollen er den anden halvdel af samme idé. PDFiumPas opløser kun C_GetFunctionList ved navn gennem GetProcAddress eller GetProcedureAddress og tager alle andre entry points fra den tabel, som kaldet returnerer, hvilket er sådan, OASIS PKCS #11-base-specifikationen har tænkt, at et modul skal nås, og som undgår symbolsnavne pr. leverandør. Derefter sanity-checker den det, der kom tilbage. En major-version uden for 2 til 3 eller en nil C_Initialize, C_GetSlotList eller C_Sign betyder, at recorden er fejljusteret, og modulet droppes i stedet for at blive kaldt gennem
Signering gennem tabellen: mekanismer, DigestInfo og to-pass C_Sign
Når layoutet er rigtigt, er signeringsarbejdet lille, fordi den ICmsSigner-kontrakt, PDFiumPas beder en backend opfylde, har fem metoder, og fire af dem returnerer blot OIDs og signer-identifikatoren. Kun SignSignedAttrsDigest gør noget: Den tager SHA-256-digesten på 32 bytes af de signerede attributter og returnerer signature-bytes. CMS-samling, ASN.1, RFC 3161-timestamping og DSS/LTV er alle platformuafhængige og allerede udført, hvilket er den samme arbejdsdeling, som lader remote PAdES-signersessioner mod en HSM eller en cloud key-service sætte sig ind i den identiske seam. Tre mekanismedetaljer koster dig en fejlet verificering, hvis du springer dem over. CKM_RSA_PKCS anvender PKCS#1 v1.5-padding, men konstruerer ikke DigestInfo, så kalderen prepender selv det 19 bytes lange SHA-256-DigestInfo-præfiks fra RFC 8017; giv tokenet den rå digest, og du får en velformet signatur over det forkerte. CKM_RSA_PKCS_PSS og CKM_ECDSA tager digesten, som den er, men CKM_ECDSA svarer med det rå r||s-par, og CMS kræver ECDSA-Sig-Value-SEQUENCE i RFC 3279 §2.2.3, så PDFiumPas konverterer. Og C_Sign er to-pass med vilje: Kald den med en nil-buffer for at spørge tokenet om signaturlængden, og derefter igen med en buffer af den størrelse
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);
// Log dette før alt andet, når et token opfører sig forkert på en ny platform
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;
Nogle mindre ting er værd at vide, før du møder dit første token. Moduler caches efter sti, fordi C_Initialize er én gang pr. proces pr. modul, og et gentaget kald svarer CKR_CRYPTOKI_ALREADY_INITIALIZED (0x00000190), hvilket PDFiumPas behandler som succes ud fra antagelsen om, at en anden del af hosten allerede initialiserede det samme bibliotek. Tokenstrings som slotbeskrivelsen og tokenlabelen er fixed-width-felter padded med blanks og ikke NUL-terminerede, så de skal trimmes fra halen. Og CKO_CERTIFICATE er 1 og ikke 2 — 0 er CKO_DATA, og 2 er CKO_PUBLIC_KEY. At skrive den konstant ud fra hukommelsen er en fejl, der producerer et tomt søgeresultat og ingen fejl overhovedet
Hvad verificeres, og hvor stopper garantien?
Vær tydelig om grænsen, fordi den er smallere, end featurebeskrivelsen antyder. Det, der verificeres i PDFiumPas i dag, er, at ABI-layoutet matcher C-headerne felt for felt på begge grene, at et fraværende eller uindlæseligt modul degraderer til en rapporteret fejl i stedet for et crash, og at både Delphi- og FPC-toolchains bygger uniten. De reelle tokenstier — C_Login, objektsøgning og C_Sign mod hardware — er ikke blevet kørt, fordi udviklingshosten slet ikke har noget PKCS#11-modul installeret. Start SoftHSM2 først, og bekræft Pkcs11AbiLayout, før du tilslutter et fysisk token, så et ABI-problem og et tokenproblem aldrig skal diagnosticeres på samme tid. Endnu en asymmetri fortjener et navn. Signeringssiden er nu cross-platform; verificeringssiden er ikke. CMS-verificering inde i PDFiumPas er stadig beskyttet af {$IFDEF MSWINDOWS} og returnerer pcsUnsupported andre steder, og den har intet provider-injection-point svarende til signer-backenden. Så en Linux-service kan producere en PAdES B-B-signatur over en tokenholdt nøgle og kan endnu ikke kontrollere sit eget output på den samme maskine. Planlæg verificeringstrinnet på Windows eller på en ekstern validator, indtil hullet lukkes
Læren generaliserer ud over PKCS#11. Enhver Pascal-record, der spejler en conditionally packed C-struct, har brug for tre ting: ét conditional alias for den platformvariable scalar, så breddebeslutningen findes præcis ét sted, packing-direktiver, der indrammer deklarationerne og gendannes bagefter, og en runtime-funktion, der rapporterer det opløste layout som noget, en test kan hævde. Kommentarer, der hævder, at en struct matcher sin header, er ingenting værd; SizeOf og en felt-offset, der printes ved startup, er meget værd. PKCS#11-backenden, CNG-backenden og resten af signeringsstakken leveres i PDFium Component til Delphi og C++Builder, hvor ABI-plumbing allerede er conditionaliseret, så din kode kan blive på tokensiden af problemet