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 7512pkcs11:URIs (Uri), and thePkcs11.Smodule 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) withdlopenthrough hand-written C stubs. Noctypes. 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 samePkcs11.S: the specification's session state machine, object store and EC signing mechanisms, for hermetic tests of code written againstPkcs11.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.
Related work#
- cryptosense/pkcs11 -- OCaml
types for the full PKCS#11 surface, with
ctypes-based bindings inpkcs11-driver. This library instead types the slice needed for key management and signing, with hand-written stubs and noctypesdependency. - 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.