Teknisk artikkel

PKCS#11 i Delphi: CK_ULONG og packing-fellen

PDFiumPas signerer PAdES-dokumenter gjennom et PKCS#11-token på Windows, Linux og macOS, og to plattformfakta avgjør om bindingen i det hele tatt virker: CK_ULONG er C-typen unsigned long, altså 4 byte på Windows og 8 byte på Linux og macOS, og PKCS#11-headerne bruker #pragma pack(1) bare på Windows, noe som flytter hver peker i funksjonstabellen. Får du én av dem feil, laster modulen fortsatt, kallene returnerer fortsatt og tallene som kommer tilbake, er søppel. Det er denne formen på feilen du bør forvente. Ingen gir deg en linkerfeil, fordi ingenting lenkes: Modulen er en .so, .dylib eller .dll du åpner ved kjøring etter sti, og hele flaten er en struct med funksjonspekere du caster og kaller. Kompilatoren aner ikke hvordan C-headeren på den andre siden så ut. Hver mismatch er stille helt til den blir et krasj

Hvorfor feiler en PKCS#11-binding med tilfeldige CKR-koder i stedet for en ren feil?

Fordi en ABI-mismatch ikke lager en feiltilstand i det hele tatt, men en feil adresse eller et feil offset, og tokenet svarer pliktoppfyllende på det spørsmålet dette tilfeldigvis ble. Det finnes ikke noe lag mellom record-deklarasjonen din og modulen som kan oppdage uenigheten. To forskjellige feilmodi følger av det. Hvis packingen er feil, inneholder slotten du leser som C_GetSlotList seks byte av én peker og to av den neste, og å kalle den hopper inn i umappet minne eller, verre, midt i en annen funksjon. Det er access violation. Hvis CK_ULONG har feil bredde, er adressene riktige, men dataene er det ikke: En var Count: CK_ULONG-out-parameter som er deklarert 4 byte bred, får 8 byte skrevet inn av en LP64-modul og overskriver stille de neste fire byte-ene i stack-rammen, mens en CK_ATTRIBUTE-mal der ValueLen ligger på feil offset, får modulen til å lese et lengdefelt ut av Value-pekeren din. Tokenet returnerer deretter en helt legitim CKR_BUFFER_TOO_SMALL eller CKR_ATTRIBUTE_VALUE_INVALID for et spørsmål du aldri stilte. Disse kodene sender folk på jakt gjennom tokenkonfigurasjon i timevis. Feilen ligger fire linjer høyere i en typedefinisjon

CK_ULONG er C unsigned long, ikke en type med fast bredde

CK_ULONG defineres av PKCS#11-headerne som C unsigned long, noe som betyr at bredden følger plattformens datamodell i stedet for spesifikasjonen. Windows er LLP64, så unsigned long forblir 32-bit selv i en 64-biters prosess. Linux og macOS er LP64, så den følger pekeren og blir 64-bit. Dette er den mest konsekvensrike linjen i hele enheten, fordi nesten hver skalar i PKCS#11 i praksis er en CK_ULONG: slot-ID-er, sesjonshåndtak, objekthåndtak, objektklasser, nøkkeltyper, attributtyper, mekanismetyper, bufferlengder og selve CK_RV-returverdien

type
{$IFDEF MSWINDOWS}
  // Windows er LLP64: en C unsigned long forblir 32-bit der
  CK_ULONG = LongWord;
{$ELSE}
  // Linux og macOS er LP64: unsigned long følger pekerbredden
  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;

Å aliasere alle disse til CK_ULONG i stedet for direkte til LongWord eller UInt64, er poenget med øvelsen. Det betyr at conditional-delen finnes nøyaktig ett sted. Skriv noen av dem konkret, og du har lagt en landmine en fremtidig port vil tråkke på, og den tråkker på den ene plassen du glemte

Hva gjør pragma pack(1) med PKCS#11-funksjonstabellen?

Den flytter hver funksjonspeker i CK_FUNCTION_LIST, fordi tabellen åpner med en to-byte CK_VERSION. Under naturlig alignment legger kompilatoren inn seks padding-byte etter versjonen, så den første funksjonspekeren havner på offset 8. Under byte-packing finnes ingen padding, så den havner på offset 2. Hver etterfølgende oppføring arver samme forskyvning, og det er grunnen til at en packing-feil ikke er et problem med ett felt, men med hele tabellen. Fellen er at PKCS#11-headerne bruker #pragma pack(1) bare på Windows. Det er en plattformforskjell, ikke en modulforskjell: To bygg av det samme leverandørbiblioteket er uenige om dette avhengig av hvilken vert de kom fra. Legg også merke til at packing ikke endrer noe for strukturer hvis felt alle har pekerbredde, noe som gjelder de fleste, så en naiv test som bare rører CK_SLOT_INFO, består lykkelig mens tabellen under er forskjøvet med seks 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;      // to byte, og grunnen til at tabellen flytter seg
    C_Initialize: Pointer;    // offset 2 pakket, offset 8 justert
    C_Finalize: Pointer;
    C_GetInfo: Pointer;
    C_GetFunctionList: Pointer;
    C_GetSlotList: Pointer;
    // ... tabellen har en fast rekkefølge; å deklarere prefikset
    // til og med C_Sign er nok til å nå alt denne backenden kaller
    C_SignInit: Pointer;
    C_Sign: Pointer;
  end;
  PCK_FUNCTION_LIST = ^CK_FUNCTION_LIST;

{$IFDEF FPC}{$PACKRECORDS DEFAULT}{$ELSE}{$A8}{$ENDIF}

Tre ting i den blokken betyr mer enn de ser ut til. {$PACKRECORDS C} er ikke det samme som "ingen direktiv"; det ber Free Pascal følge plattformens C-kompilators alignment-regler, som er nøyaktig kontrakten du trenger på Linux og macOS. Delphi-grenen er ubetinget {$A1} fordi Delphi-bygg av PDFiumPas retter seg mot Windows, mens FPC bærer Linux- og macOS-byggene. Og restore-linjen nederst er ikke kosmetisk: La enheten være pakket, så endrer alle records som deklareres etter dette, layout stille, og det er nøyaktig typen action-at-a-distance-feil som herding av en PDFium-komponentbinding mot ABI- og minnesikkerhetsfeil skal eliminere

Pkcs11AbiLayout: gjør layouten til en assertion

Pkcs11AbiLayout rapporterer layouten bygget faktisk løste til én assertbar streng på formen ulong=4 attr=16 pss=12 table=2. Et 64-biters Windows-bygg må rapportere nøyaktig dette, og et LP64-mål må rapportere ulong=8 attr=24 pss=24 table=8. Alt annet betyr at et kall gjennom funksjonstabellen ville lande på feil slot, og funksjonen finnes slik at en enhetstest kan si det høyt i stedet for at en kommentar påstår 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 lasting, etter at C_GetFunctionList har levert tabellen tilbake:
// en usannsynlig versjon eller et nil-entry point betyr at record-en er lagt ut
// med feil packing eller CK_ULONG-bredde, så modulen skal avvises
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 tallene er ikke tilfeldige. attr er størrelsen på CK_ATTRIBUTE, som inneholder en CK_ULONG, en peker og en CK_ULONG: 4 + 8 + 4 pakket på Windows x64, 8 + 8 + 8 justert på LP64. pss er CK_RSA_PKCS_PSS_PARAMS, tre CK_ULONG-felt, altså 12 eller 24. table er offset-et til den første funksjonspekeren, og det er verdien som først fanger en packing-feil. Delphi-testen hevder strengen under {$IFDEF MSWINDOWS}; Lazarus-pakken hevder det samme. Én likhetskontroll dekker en layout som ellers bare kunne verifiseres ved å lese en C-header side om side med en Pascal-record og stole på deg selv. Lastingskontrollen er den andre halvdelen av samme idé. PDFiumPas løser bare C_GetFunctionList ved navn gjennom GetProcAddress eller GetProcedureAddress og tar alle andre entry points ut av tabellen dette kallet returnerer, som er måten OASIS PKCS #11-base spesifiserer at en modul skal nås på, og det omgår leverandørspesifikk symbolnavngivning. Deretter sanity-sjekker den det som kom tilbake. En hovedversjon utenfor 2 til 3, eller et nil-C_Initialize, C_GetSlotList eller C_Sign, betyr at record-en er feiljustert, og modulen forkastes i stedet for å kalles gjennom

Signering gjennom tabellen: mekanismer, DigestInfo og to-pass C_Sign

Når layouten er riktig, er signeringsarbeidet lite, fordi ICmsSigner-kontrakten PDFiumPas ber en backend oppfylle, har fem metoder, og fire av dem bare returnerer OID-er og signerens identifikator. Bare SignSignedAttrsDigest gjør noe: Den tar SHA-256-digesten på 32 byte av de signerte attributtene og returnerer signaturbyte. CMS-sammensetting, ASN.1, RFC 3161-tidsstempling og DSS/LTV er plattformuavhengig og allerede ferdig, og det er den samme arbeidsdelingen som lar eksterne PAdES-signeringssesjoner mot et HSM eller en skybasert nøkkeltjeneste koble seg til den identiske skjøten. Tre mekanismedetaljer koster deg en mislykket verifikasjon hvis du hopper over dem. CKM_RSA_PKCS bruker PKCS#1 v1.5-padding, men bygger ikke DigestInfo, så caller-en setter selv 19-byte SHA-256 DigestInfo-prefikset fra RFC 8017 foran; gir du tokenet den rå digesten, får du en velformet signatur over feil ting. CKM_RSA_PKCS_PSS og CKM_ECDSA tar digesten som den kommer, men CKM_ECDSA svarer med det rå r||s-paret, og CMS trenger ECDSA-Sig-Value-SEQUENCE-en fra RFC 3279 §2.2.3, så PDFiumPas konverterer. Og C_Sign er to-pass med hensikt: Kall det med en nil-buffer for å spørre tokenet om signaturlengden, deretter på nytt med en buffer av den størrelsen

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);
  // Logg dette før alt annet når et token oppfører seg feil på en ny plattform
  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;

Noen mindre ting er verdt å vite før det første tokenet ditt. Moduler caches etter sti fordi C_Initialize kjøres én gang per prosess per modul, og et gjentatt kall svarer CKR_CRYPTOKI_ALREADY_INITIALIZED (0x00000190), som PDFiumPas behandler som suksess under antakelsen om at en annen del av verten allerede initialiserte samme bibliotek. Token-strenger som slotbeskrivelsen og tokenetiketten er fastbrede felt med padding, ikke NUL-terminerte, så de må trimmes fra halen. Og CKO_CERTIFICATE er 1, ikke 2 — 0 er CKO_DATA og 2 er CKO_PUBLIC_KEY. Å skrive den konstanten fra hukommelsen er en feil som gir et tomt søkeresultat og ingen feil overhodet

Hva verifiseres, og hvor stopper garantien?

Vær tydelig på grensen, fordi den er smalere enn funksjonsbeskrivelsen antyder. Det som verifiseres i PDFiumPas i dag, er at ABI-layouten stemmer med C-headerne felt for felt på begge grenene, at en fraværende eller ulasterbar modul gir en rapportert feil i stedet for et krasj, og at både Delphi- og FPC-toolchainene bygger enheten. De virkelige token-stiene — C_Login, objektsøk og C_Sign mot maskinvare — er ikke utøvd, fordi utviklingsverten ikke har noen PKCS#11-modul installert i det hele tatt. Start med SoftHSM2 og bekreft Pkcs11AbiLayout før du kobler til et fysisk token, slik at et ABI-problem og et tokenproblem aldri må diagnostiseres samtidig. Én asymmetri til fortjener navn. Signeringssiden er nå kryssplattform, verifikasjonssiden er ikke. CMS-verifikasjon inne i PDFiumPas er fortsatt voktet av {$IFDEF MSWINDOWS} og returnerer pcsUnsupported andre steder, og den har ikke noe provider-injeksjonspunkt som svarer til signer-backenden. En Linux-tjeneste kan derfor produsere en PAdES B-B-signatur over en tokenholdt nøkkel, men kan ennå ikke kontrollere sitt eget output på samme maskin. Legg verifikasjonstrinnet til Windows eller til en ekstern validator inntil dette gapet lukkes

Lærdommen generaliserer utenfor PKCS#11. Enhver Pascal-record som speiler en betinget pakket C-struct, trenger tre ting: ett betinget alias for den plattformvariable skalaren, slik at breddevalget finnes nøyaktig ett sted, packing-direktiver som omslutter deklarasjonene og gjenopprettes etterpå, og en runtime-funksjon som rapporterer den løste layouten som noe en test kan hevde. Kommentarer som påstår at en struct matcher headeren, er verdiløse; SizeOf og et felt-offset skrevet ved oppstart er mye verdt. PKCS#11-backenden, CNG-backenden og resten av signeringsstakken leveres i PDFium Component for Delphi og C++Builder, der ABI-røropplegget allerede er betinget slik at koden din kan bli på tokensiden av problemet