TPM 2.0 command/response wire codec (TCG TPM 2.0 Library)
0

Configure Feed

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

OCaml 88.2%
C 6.5%
C++ 1.1%
Go 0.8%
Dune 0.4%
Shell 0.4%
Roff 0.1%
Other 2.5%
94 1 0

Clone this repository

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

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


README.md

ocaml-tpm#

A native, pure-OCaml TPM 2.0 client. It marshals the TCG TPM 2.0 command and response stream directly -- no FFI to tpm2-tss, so it links into a statically-linked binary or a unikernel-style space-init where a C TSS cannot.

Two packages:

  • tpm -- the pure codec and protocol library. No IO, no clock, no ambient randomness. Built on wire for the byte layout and digestif for hashing/HMAC. Each command is a small module exposing a command builder (bytes to send) and a response decoder (bytes received).
  • tpm-eio -- an Eio transport that carries those bytes to a TPM: a Unix/TCP socket (an swtpm emulator) or a kernel character device (/dev/tpmrm0, /dev/tpm0).

Design#

The split is deliberate: the tpm library is a pure bytes -> bytes transformation, so the same code runs under Eio, under a test harness with canned bytes, and under a fuzzer. Randomness (session nonces) and the clock are inputs the caller supplies.

Every TPM structure is described once as a wire codec -- the field definitions are the specification, used for both encoding and decoding. Authorization (HMAC sessions, cpHash, KDFa, password and policy sessions) and the object Name computation are implemented per TCG Part 1; a wrong Name or HMAC is rejected by the TPM, so the live tests double as correctness proofs.

Capabilities#

GetRandom, GetCapability, Startup/Shutdown, PCR_Read/PCR_Extend, StartAuthSession, FlushContext, CreatePrimary/Create/Load/Unseal (including sealing a secret to a PCR policy), the NV monotonic counter (NV_DefineSpace/Increment/Read), Quote attestation, and Sign with a loaded key (an unrestricted signing key signs a caller-computed digest, so the private key can serve a signer backend without ever leaving the TPM).

Install#

opam install tpm        # the pure codec and protocol library
opam install tpm-eio    # the Eio transport

If opam cannot find the packages, they may not yet be released in the public opam-repository. Add the overlay repository, then install them:

$ opam repo add samoht https://tangled.org/gazagnaire.org/opam-overlay.git
$ opam update
$ opam install tpm
$ opam install tpm-eio

Usage#

Seal a secret under an ECC storage primary and read it back. The body is a pure function of an Eio network capability: each codec call pairs a command builder (bytes to send) with a response decoder (bytes received), and the transport carries the bytes between them.

let unseal_demo ~sw ~net =
  let tpm = Tpm_eio.Transport.connect_unix ~sw net "/tmp/swtpm.sock" in
  let run c = Tpm_eio.Transport.transceive tpm c in
  let ok = function Ok v -> v | Error rc -> Fmt.failwith "%a" Tpm.Rc.pp rc in
  let owner = Tpm.Session.pw () in
  (* A storage primary in the owner hierarchy. *)
  let parent =
    ok
      (Tpm.Objects.Create_primary.response ~auth:owner
         (run
            (Tpm.Objects.Create_primary.command ~hierarchy:Tpm.Objects.rh_owner
               ~public:
                 (Tpm.Public.Ecc_p256_storage
                    { name_alg = Tpm.Alg.Sha256; auth_policy = "" })
               ~auth:owner ())))
  in
  (* Seal a secret to it, load the sealed object, unseal it. *)
  let pa = Tpm.Session.pw () in
  let blob =
    ok
      (Tpm.Objects.Create.response ~auth:pa
         (run
            (Tpm.Objects.Create.command ~parent:parent.handle
               ~parent_name:parent.name
               ~public:
                 (Tpm.Public.Keyedhash_seal
                    {
                      name_alg = Tpm.Alg.Sha256;
                      auth_policy = "";
                      user_auth = true;
                    })
               ~auth_value:"pw" ~data:"my secret" ~auth:pa ())))
  in
  let obj =
    ok
      (Tpm.Objects.Load.response ~auth:(Tpm.Session.pw ())
         (run
            (Tpm.Objects.Load.command ~parent:parent.handle
               ~parent_name:parent.name ~blob ~auth:(Tpm.Session.pw ()))))
  in
  let auth = Tpm.Session.pw ~auth:"pw" () in
  let secret =
    ok
      (Tpm.Objects.Unseal.response ~auth
         (run
            (Tpm.Objects.Unseal.command ~object_:obj.handle
               ~object_name:obj.name ~auth)))
  in
  assert (secret = "my secret")

Drive it from an Eio main loop against a running swtpm:

let run_unseal_demo env =
  Eio.Switch.run @@ fun sw -> unseal_demo ~sw ~net:(Eio.Stdenv.net env)

Testing#

Deterministic unit tests (dune test) cover the codecs with spec-anchored vectors (FIPS/RFC digests, exact command bytes, response-code decoding). An Alcobar fuzz target asserts the decoders never crash on hostile bytes and checks codec round-trips.

Behaviour is verified end-to-end against swtpm: the eio/test/*_smoke.ml executables drive a real emulator. Start one and point a smoke test at it:

swtpm socket --tpm2 --tpmstate dir=/tmp/mytpm \
  --ctrl type=unixio,path=/tmp/mytpm/ctrl.sock \
  --server type=unixio,path=/tmp/mytpm/data.sock \
  --flags startup-clear,not-need-init &
dune exec ocaml-tpm/eio/test/policy_seal_smoke.exe -- /tmp/mytpm/data.sock

Security#

The library parses bytes a TPM -- or a host that can sit on the bus between the CPU and the TPM -- produces, so every response is treated as hostile input. Being I/O-free, it has no ambient attack surface: no sockets, no clock, no global randomness.

What the protocol layer enforces:

  • Authorized responses are authenticated before they are trusted. The response HMAC is checked in constant time (Eqaf.equal) before any response parameter is decoded or decrypted, nonceTPM is rolled on every exchange (anti-replay), and a nonceCaller shorter than 16 octets is rejected so the authorization HMAC can never be made predictable.
  • Sealed secrets never cross the bus in clear. Parameter encryption (AES-128-CFB) is keyed by sessionValue = sessionKey || authValue per Part 1 sec 21.2, so an unsealed value is encrypted to the session even on a snooped bus.
  • Quotes are checked for structure, not just signature. Quote.verify confirms the signed structure is a quote (magic, attestType) committing to the caller's challenge nonce before validating the ECDSA signature, so a replayed or structurally-confused attestation is rejected.
  • Framing is bounded and exact. response_length caps the advertised responseSize at 64 KiB, so a hostile bus cannot announce a multi-gigabyte response and exhaust memory; unframe requires responseSize to equal the bytes received, so a padded or truncated packet cannot smuggle trailing bytes through as parameters. Every decoder fails closed on a truncated or oversized TPM2B rather than reading out of bounds (CVE-2023-1018 regression), and response-code decoding is total over all 32-bit codes (CVE-2023-22745 regression). Adversarial coverage lives in the per-module hostile tests and the Alcobar fuzz target.

Key hygiene: a session holds its key in a wipeable buffer (Tpm.Secret, an abstract type so the bytes cannot be mutated except by an explicit wipe), and Session.destroy zeros it once the session is finished -- so the long-lived copy of the key, the realistic cold-boot or core-dump target, does not linger resident. The transient copies a hash or concatenation makes internally are short-lived and left to the collector; the salt and authorization values the caller supplies are the caller's to clear. Elliptic-curve point validation (rejecting off-curve salt and signing keys) is delegated to nox-crypto-ec.

License#

ISC.