Tekninen artikkeli

PKCS#11 Delphissä: CK_ULONG ja packing-ansa

PDFiumPas allekirjoittaa PAdES-dokumentteja PKCS#11-tokenin kautta Windowsissa, Linuxissa ja macOS:ssä, ja kaksi alustafaktaa ratkaisee toimiiko sidonta lainkaan: CK_ULONG on C:n unsigned long, siis 4 tavua Windowsissa ja 8 Linuxissa ja macOS:ssä, ja PKCS#11-otsakkeet käyttävät #pragma pack(1)-määrettä vain Windowsissa, mikä siirtää jokaisen osoittimen function table -rakenteessa. Jos toinen menee väärin, moduuli latautuu silti, kutsut palaavat silti ja palautuvat numerot ovat roskaa. Tämän muodon bugin pitäisi olla odotettavissa. Linkkeri ei anna virhettä, koska mitään ei linkitetä: moduuli on .so-, .dylib- tai .dll-tiedosto jonka avaat ajon aikana polulla, ja koko pinta on funktionosoittimien struct jonka castaat ja kutsut. Kääntäjä ei tiedä miltä toisessa päässä oleva C-header näytti. Jokainen ristiriita on hiljainen kaatumiseen asti

Miksi PKCS#11-sidonta epäonnistuu satunnaisilla CKR-koodeilla eikä siistillä virheellä?

Koska ABI-ristiriita ei tuota virhetilaa lainkaan vaan väärän osoitteen tai offsetin, ja token vastaa tunnollisesti mihin tahansa kysymykseen jonka väärä osoite sattuu tarkoittamaan. Record-määrittelysi ja moduulin välissä ei ole kerrosta joka huomaisi eron. Tästä syntyy kaksi erillistä vikatilaa. Jos packing on väärin, C_GetSlotList-kenttänä lukemasi paikka sisältää kuusi tavua yhtä osoitinta ja kaksi seuraavaa, ja sen kutsuminen hyppää unmapped-muistiin tai pahempaa, jonkin toisen funktion keskelle. Se on access violation. Jos CK_ULONG:n leveys on väärä, osoitteet ovat oikein mutta data ei: neljän tavun levyiseksi ilmoitettu var Count: CK_ULONG -out-parametri saa LP64-moduulilta kahdeksan tavua ja ylikirjoittaa hiljaa stack framen seuraavat neljä tavua, ja CK_ATTRIBUTE-template jonka ValueLen on väärässä offsetissa saa moduulin lukemaan pituuskentän Value-osoittimesta. Token palauttaa silloin täysin laillisen CKR_BUFFER_TOO_SMALL- tai CKR_ATTRIBUTE_VALUE_INVALID-koodin kysymykseen jota et koskaan esittänyt. Nämä koodit lähettävät ihmiset tuntikausiksi tutkimaan tokenin asetuksia. Bugi on neljä riviä ylempänä tyyppimäärittelyssä

CK_ULONG on C:n unsigned long eikä kiinteän leveyden tyyppi

PKCS#11-headerit määrittelevät CK_ULONG:n C:n unsigned long -tyypiksi, joten sen leveys seuraa alustan datamallia eikä spesifikaatiota. Windows on LLP64, joten unsigned long pysyy 32-bittisenä myös 64-bittisessä prosessissa. Linux ja macOS ovat LP64, joten tyyppi seuraa osoitinta ja muuttuu 64-bittiseksi. Tämä on koko unitin tärkein yksittäinen rivi, koska PKCS#11:ssä käytännössä jokainen skalaari on CK_ULONG: slot ID:t, session handle -arvot, object handle -arvot, object classit, key typet, attribute typet, mechanism typet, buffer-pituudet ja itse CK_RV-paluuarvo

type
{$IFDEF MSWINDOWS}
  // Windows on LLP64: C:n unsigned long pysyy siellä 32-bittisenä
  CK_ULONG = LongWord;
{$ELSE}
  // Linux ja macOS ovat LP64: unsigned long seuraa osoittimen leveyttä
  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;

Kaikkien näiden aliasointi CK_ULONG-tyypille sen sijaan että ne aliasoitaisiin suoraan LongWord- tai UInt64-tyypille on harjoituksen pointti. Ehdollinen valinta esiintyy silloin täsmälleen kerran. Kirjoita jokin niistä konkreettisena ja olet asettanut miinan johon tuleva porttaus astuu, juuri siinä kohdassa jonka unohdit

Mitä pragma pack(1) tekee PKCS#11:n function tablelle?

Se siirtää jokaisen funktionosoittimen paikasta CK_FUNCTION_LIST, koska taulukko alkaa kaksitavuisella CK_VERSION-rakenteella. Luonnollisella kohdistuksella kääntäjä lisää version jälkeen kuusi täytetavua, joten ensimmäinen funktionosoitin osuu offsetiin 8. Tavupakkauksella täytettä ei ole, joten se osuu offsetiin 2. Jokainen seuraava alkio perii saman siirtymän, minkä vuoksi packing-virhe ei ole yhden kentän ongelma vaan koko taulukon ongelma. Ansa on siinä että PKCS#11-headerit käyttävät #pragma pack(1)-määritystä vain Windowsissa. Se on alustojen eikä moduulien ero: saman vendor-kirjaston kaksi buildiä ovat tämän suhteen erilaisia riippuen siitä mistä hostista ne tulivat. Huomaa myös ettei packing muuta rakenteita joiden kaikki kentät ovat osoitteen levyisiä, ja niitä on suurin osa, joten naiivi testi joka koskee vain CK_SLOT_INFO-rakennetta menee onnellisesti läpi vaikka sen alla oleva table on kuusi tavua väärässä paikassa

{$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;      // kaksi tavua, ja syy siihen että table siirtyy
    C_Initialize: Pointer;    // offset 2 pakattuna, offset 8 kohdistettuna
    C_Finalize: Pointer;
    C_GetInfo: Pointer;
    C_GetFunctionList: Pointer;
    C_GetSlotList: Pointer;
    // ... tablella on kiinteä järjestys; C_Signiin asti menevän etuliitteen
    // määrittely riittää kaiken tämän backendin kutsuman saavuttamiseen
    C_SignInit: Pointer;
    C_Sign: Pointer;
  end;
  PCK_FUNCTION_LIST = ^CK_FUNCTION_LIST;

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

Kolme asiaa tässä lohkossa ovat tärkeämpiä kuin miltä näyttävät. {$PACKRECORDS C} ei ole sama asia kuin "ei määrettä"; se kertoo Free Pascalille että sen on noudatettava alustan C-kääntäjän kohdistussääntöjä, mikä on juuri Linuxissa ja macOS:ssä tarvitsemasi sopimus. Delphi-haara on ehdoton {$A1}, koska PDFiumPas:n Delphi-buildit kohdistuvat Windowsiin, kun taas FPC kantaa Linux- ja macOS-buildit. Lopun palautusrivi ei ole koristetta: jätä unit pakatuksi ja jokainen tämän kohdan jälkeen määritelty record muuttaa layoutiaan hiljaa, mikä on juuri sellaista action-at-a-distance -vikaa jonka PDFium-komponenttisidonnan koventaminen ABI- ja muistiturovallisuusvikoja vastaan pyrkii poistamaan

Pkcs11AbiLayout: layoutin muuttaminen assertioniksi

Pkcs11AbiLayout raportoi buildin oikeasti ratkaisemaan layoutin yhtenä assertioniin sopivana merkkijonona muodossa ulong=4 attr=16 pss=12 table=2. 64-bittisen Windows-buildin täytyy ilmoittaa täsmälleen tämä, ja LP64-kohteen täytyy ilmoittaa ulong=8 attr=24 pss=24 table=8. Kaikki muu tarkoittaa että funktion taulukon kautta tehty kutsu osuu väärään paikkaan, ja funktio on olemassa jotta unit-testi voi sanoa sen ääneen kommentin väittämän sijaan

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;

// Lataushetkellä, kun C_GetFunctionList on palauttanut taulukon:
// epäuskottava versio tai nil-entry point tarkoittaa että record on muodostettu
// väärällä packingillä tai CK_ULONG-leveydellä, joten moduuli hylätään
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;

Neljä numeroa eivät ole sattumanvaraisia. attr on CK_ATTRIBUTE-rakenteen koko, joka sisältää CK_ULONG:n, osoittimen ja CK_ULONG:n: Windows x64:n pakattuna 4 + 8 + 4 ja LP64:n kohdistettuna 8 + 8 + 8. pss on CK_RSA_PKCS_PSS_PARAMS-rakenteen koko, kolme CK_ULONG-kenttää eli 12 tai 24. table on ensimmäisen funktionosoittimen offset ja juuri se arvo joka nappaa packing-virheen ensimmäisenä. Delphi-testitapaus assertioi merkkijonon kohdan {$IFDEF MSWINDOWS} alla ja Lazarus-testipaketti assertioi saman. Yksi yhtäsuuruustarkistus kattaa layoutin joka muuten olisi todennettavissa vain lukemalla C-headeria Pascal-recordin rinnalla ja luottamalla itseensä. Lataushetken tarkistus on saman idean toinen puoli. PDFiumPas ratkaisee vain C_GetFunctionList-symbolin nimeltä GetProcAddress- tai GetProcedureAddress-kutsun kautta ja ottaa kaikki muut entry pointit tuon palauttamasta tablesta, kuten OASIS PKCS #11 -perusspesifikaatio tarkoittaa ja ohittaa vendor-kohtaisen symbolinimeämisen. Sitten se tekee järkevyystarkistuksen palautuneelle. Pääversion 2–3 ulkopuolinen arvo tai nil-arvo kentässä C_Initialize, C_GetSlotList tai C_Sign tarkoittaa väärin kohdistettua recordia, joten moduuli pudotetaan sen sijaan että sen läpi kutsuttaisiin

Allekirjoitus taulukon kautta: mekanismit, DigestInfo ja kaksivaiheinen C_Sign

Kun layout on oikein, allekirjoitustyö on pieni, koska PDFiumPas:n backendille asettama ICmsSigner-sopimus sisältää viisi metodia ja neljä niistä palauttaa vain OID:t ja allekirjoittajan tunnisteen. Vain SignSignedAttrsDigest tekee mitään: se ottaa allekirjoitettujen attribuuttien 32-tavuisen SHA-256-tiivisteen ja palauttaa allekirjoitustavut. CMS:n kokoaminen, ASN.1, RFC 3161 -aikaleimaus ja DSS/LTV ovat alustariippumattomia ja jo valmiiksi tehty, sama vastuunjako joka antaa HSM:ää tai pilviavainpalvelua käyttävien etäisten PAdES-allekirjoitusistuntojen liittyä samaan saumaan. Kolme mekanismiyksityiskohtaa tuottaa varmennuksen epäonnistumisen jos ne ohitetaan. CKM_RSA_PKCS käyttää PKCS#1 v1.5 -paddingia mutta ei rakenna DigestInfoa, joten kutsuja lisää RFC 8017:n 19-tavuisen SHA-256 DigestInfo -etuliitteen itse; anna tokenille paljas digest ja saat hyvin muodostetun allekirjoituksen väärästä kohteesta. CKM_RSA_PKCS_PSS ja CKM_ECDSA ottavat digestin sellaisena kuin se annetaan, mutta CKM_ECDSA vastaa raakana r||s-parina, ja CMS tarvitsee RFC 3279 §2.2.3:n ECDSA-Sig-Value SEQUENCE -rakenteen, joten PDFiumPas muuntaa sen. C_Sign on tarkoituksella kaksivaiheinen: kutsu sitä nil-puskurilla kysyäksesi tokenilta allekirjoituksen pituuden ja sitten uudelleen sen kokoisella puskurilla

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);
  // Kirjaa tämä ennen kaikkea muuta kun token käyttäytyy uudella alustalla väärin
  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;

Muutama pienempi asia kannattaa tietää ennen ensimmäistä tokenia. Moduulit cachetetaan polun perusteella, koska C_Initialize suoritetaan kerran prosessia ja moduulia kohden, ja uusi kutsu palauttaa koodin CKR_CRYPTOKI_ALREADY_INITIALIZED (0x00000190), jonka PDFiumPas käsittelee onnistumisena olettaen että jokin muu osa hostia alusti saman kirjaston jo. Tokenin merkkijonot, kuten slot-kuvaus ja tokenin label, ovat NUL-terminoimattomia kiinteän levyisiä kenttiä jotka on täytetty tyhjeillä, joten ne täytyy trimmata lopusta. Ja CKO_CERTIFICATE on 1 eikä 2 — 0 on CKO_DATA ja 2 on CKO_PUBLIC_KEY. Tämän vakion kirjoittaminen muistista on virhe joka tuottaa tyhjän hakutuloksen ilman ainuttakaan virhettä

Mitä varmennetaan ja mihin takuu päättyy?

Raja kannattaa pitää selkeänä, koska se on kapeampi kuin ominaisuuskuvaus antaa ymmärtää. PDFiumPas:ssa varmennetaan tällä hetkellä että ABI-layout vastaa C-headereita kenttä kentältä molemmilla haaroilla, puuttuva tai latauskelvoton moduuli muuttuu raportoiduksi epäonnistumiseksi kaatumisen sijaan ja sekä Delphi- että FPC-toolchain kääntävät unitin. Oikeita token-polkuja — C_Login, objektihaku ja laitteistoa vasten tehtävä C_Sign — ei ole ajettu, koska kehityshostissa ei ole lainkaan PKCS#11-moduulia asennettuna. Käynnistä SoftHSM2 ensin ja varmista Pkcs11AbiLayout ennen fyysisen tokenin kytkemistä, jotta ABI-ongelmaa ja token-ongelmaa ei tarvitse diagnosoida samalla kertaa. Yksi epäsymmetria on vielä nimettävä. Allekirjoituspuoli on nyt monialustainen mutta varmennuspuoli ei. PDFiumPas:n sisäinen CMS-varmennus on yhä suojattu {$IFDEF MSWINDOWS}-ehdolla ja palauttaa muualla arvon pcsUnsupported, eikä sillä ole allekirjoittajan backendia vastaavaa provider-injektiopistettä. Linux-palvelu voi siis tuottaa tokenin hallussa olevalla avaimella tehdyn PAdES B-B -allekirjoituksen mutta ei vielä tarkistaa omaa tulostaan samalla koneella. Suunnittele varmennus Windowsille tai ulkoiselle validatorille kunnes aukko sulkeutuu

Oppi yleistyy PKCS#11:n ulkopuolelle. Jokainen ehdollisesti pakattua C-structia vastaava Pascal-record tarvitsee kolme asiaa: yhden ehdollisen aliasin alustakohtaisesti muuttujan skalaariin, jotta leveyspäätös on täsmälleen yhdessä paikassa, packing-määreet jotka rajaavat määrittelyt ja palautetaan lopuksi sekä ajonaikaisen funktion joka ilmoittaa ratkaistun layoutin testin assertioitavana asiana. Kommentit jotka väittävät structin vastaavan headeria eivät ole minkään arvoisia; SizeOf ja käynnistyksessä tulostettu kenttäoffset ovat. PKCS#11-backend, CNG-backend ja muu allekirjoituspino toimitetaan PDFium Component for Delphi and C++Builder -komponentissa, jossa ABI-putkitus on jo ehdollistettu jotta oma koodisi voi pysyä token-puolella ongelmaa