PKCS#11 (Cryptoki) client with a typed core and a dlopen driver
0

Configure Feed

Select the types of activity you want to include in your feed.

17 1 0

Clone this repository

https://git.vm.fail/gazagnaire.org/ocaml-pkcs11 https://git.vm.fail/did:plc:vlr5vhaxtpf4st4du2qdyjwm
ssh://git@git.recoil.org:2222/gazagnaire.org/ocaml-pkcs11 ssh://git@git.recoil.org:2222/did:plc:vlr5vhaxtpf4st4du2qdyjwm

For self-hosted knots, clone URLs may differ based on your setup.


README.md

ocaml-pkcs11#

A PKCS#11 (Cryptoki) client for OCaml. It talks to the tokens that hold signing keys -- hardware security modules, smart cards, and software tokens such as SoftHSM2 -- through a typed core that never exposes raw C structures.

The opam package is nox-pkcs11 (the pkcs11 name on opam belongs to Cryptosense's bindings); the OCaml library it exposes is Pkcs11.

Three libraries:

  • nox-pkcs11 -- the pure core: typed return values (Rv), object attributes (Attribute), mechanisms (Mechanism), slot, token and session descriptions, RFC 7512 pkcs11: URIs (Uri), and the Pkcs11.S module type capturing the Cryptoki function-list slice needed to enumerate tokens, manage sessions and objects, and sign.
  • nox-pkcs11.dl -- the driver: loads a vendor PKCS#11 module (libsofthsm2.so, OpenSC, a vendor HSM library) with dlopen through hand-written C stubs. No ctypes. Calls release the OCaml runtime lock, so a slow token does not stall other domains.
  • nox-pkcs11.fake -- a pure-OCaml in-memory token implementing the same Pkcs11.S: the specification's session state machine, object store and EC signing mechanisms, for hermetic tests of code written against Pkcs11.S.

Cryptoki in one paragraph#

PKCS#11 (OASIS, v3.0) defines a C ABI, not a wire protocol: an application dlopens a vendor module, obtains its CK_FUNCTION_LIST, and calls C_* entry points. A token sits in a slot; an application opens sessions on a token, logs in with a PIN, finds objects (keys, certificates) by attribute template, and invokes mechanisms (such as CKM_ECDSA) against object handles. Private keys never leave the token; signing happens inside it.

Example#

The same code drives hardware and the in-memory token; here the fake, bootstrapped the way factory hardware is (C_InitToken under the security officer, then C_InitPIN for the user):

# Crypto_rng_unix.use_default ()
- : unit = ()
# open Pkcs11
# let m = Pkcs11_fake.v ()
val m : Pkcs11_fake.t = <abstr>
# let session =
    let ( let* ) = Result.bind in
    Result.get_ok
      (let* () = Pkcs11_fake.initialize m in
       let* slot =
         Result.map List.hd (Pkcs11_fake.slots m ~token_present:true)
       in
       let* () =
         Pkcs11_fake.init_token m slot ~so_pin:"so-pin" ~label:"demo"
       in
       let* s = Pkcs11_fake.open_session m slot ~read_write:true in
       let* () = Pkcs11_fake.login m s User_type.So ~pin:"so-pin" in
       let* () = Pkcs11_fake.init_pin m s ~pin:"1234" in
       let* () = Pkcs11_fake.logout m s in
       let* () = Pkcs11_fake.login m s User_type.User ~pin:"1234" in
       Ok s)
val session : Session.t = <abstr>
# let _public, key =
    Result.get_ok
      (Pkcs11_fake.generate_key_pair m session Mechanism.Ec_key_pair_gen
         ~public_template:[ Attribute.Ec_params (Ec.der_of_curve Ec.P256) ]
         ~private_template:[ Attribute.Label "release-key" ])
val _public : Object_handle.t = <abstr>
val key : Object_handle.t = <abstr>
# match Pkcs11_fake.sign m session Mechanism.Ecdsa_sha256 ~key "hello" with
  | Ok raw -> `Signature_bytes (String.length raw)
  | Error rv -> `Failed (Rv.name rv)
- : [> `Failed of string | `Signature_bytes of int ] = `Signature_bytes 64

RFC 7512 URIs name keys textually:

# match
    Uri.of_string "pkcs11:token=demo;object=release-key;type=private"
  with
  | Ok uri -> uri.Uri.object_label
  | Error (`Msg _) -> None
- : string option = Some "release-key"

Against hardware only the module changes -- everything after Pkcs11_dl.load is the same Pkcs11.S surface driven above:

# let with_module path f =
    match Pkcs11_dl.load path with
    | Error (`Msg _) as e -> e
    | Ok m ->
        Fun.protect ~finally:(fun () -> Pkcs11_dl.unload m) (fun () -> f m)
val with_module :
  string ->
  (Pkcs11_dl.t -> ('a, [> `Msg of string ] as 'b) result) -> ('a, 'b) result =
  <fun>

Testing#

The unit suites run against nox-pkcs11.fake and need no hardware. The driver's integration suite self-gates on a discoverable SoftHSM2 module (Homebrew and Debian paths are probed) and skips when none is present.

  • cryptosense/pkcs11 -- OCaml types for the full PKCS#11 surface, with ctypes-based bindings in pkcs11-driver. This library instead types the slice needed for key management and signing, with hand-written stubs and no ctypes dependency.
  • p11-kit -- the C ecosystem's module loader and RPC remoting. Its socket protocol would suit a pure-OCaml client, but it is an undocumented implementation detail of p11-kit, so this library binds the standard C ABI instead.
  • SoftHSM2 -- the software token used as the integration-test oracle.

License#

ISC. See LICENSE.md.