tuf#
The Update Framework's security model — role separation, key thresholds, version rollback protection, freshness/expiry, key rotation and delegation — over a COSE_Sign1/CBOR wire format instead of TUF's canonical JSON.
The split is deliberate. TUF's security guarantees come from what is signed and how it is checked (the role model and update workflow), not from the byte encoding. This library implements that model and verification logic, and carries each metadata document in a COSE_Sign1 envelope (RFC 9052) over CBOR (RFC 8949). Choosing COSE_Sign1 over canonical JSON avoids TUF's most error-prone corner: COSE signs the literal payload bytes, so a verifier never has to re-canonicalize to check a signature. The trade-off is that off-the-shelf TUF clients (go-tuf/python-tuf) cannot consume this metadata — it is for a closed system that owns its publisher and verifiers.
No cryptography is reimplemented: Ed25519 signing and verification are delegated
entirely to ocaml-cose, and CBOR to ocaml-cbor. The library
is pure and clock-injected, so it runs unchanged in a unikernel verifier.
Layers#
The package is four libraries, so the security model is independent of the wire format:
| Library | Contents |
|---|---|
tuf.core |
Meta (the role model — root/timestamp/snapshot/targets, keys, thresholds, delegations) and Client.Make (the verification workflow as a functor over a CODEC signature). Pure; no bytes. |
tuf.cose |
Codec — the COSE_Sign1/CBOR wire format, implementing tuf.core's CODEC. The closed-world default. |
tuf.json |
Canonical (canonical JSON) and Codec — the standard canonical-JSON TUF format, for go-tuf / python-tuf interop. Verify-only. |
tuf |
the toplevel: Tuf.Client/Tuf.Codec default to COSE, and Tuf.Json exposes the JSON codec. |
Client.Make applies every security rule — thresholds, rollback counters, version
consistency, expiry, rotation, delegation chaining — and never touches bytes; a
CODEC only parses a blob and reports which candidate keys signed it. A wire
format is just another CODEC: the same functor verifies COSE or canonical JSON
unchanged. For a JSON verifier, instantiate it directly:
module Client = Tuf_core.Client.Make (Tuf_json.Codec)
The tuf.json codec is checked against real python-tuf metadata: the interop
test (test/interop/python-tuf/) pins a python-tuf root and verifies the
timestamp/snapshot/targets chain, which passes only if this library's canonical
JSON and Ed25519 verification match python-tuf byte-for-byte.
Install#
opam install tuf
If opam cannot find the package, it may not yet be released in the public
opam-repository. Add the overlay repository, then install it:
$ opam repo add samoht https://tangled.org/gazagnaire.org/opam-overlay.git
$ opam update
$ opam install tuf
Usage#
A verifier is anchored on a pinned root (delivered out of band, e.g. baked into a device image), then grows its trust state by loading timestamp, snapshot and targets metadata in order. Each step enforces the TUF properties; the caller supplies the metadata bytes (the library is transport-free) and the current time.
open Tuf
let admit ~now ~pinned_root ~timestamp ~snapshot ~targets ~artifact_name
~artifact_length ~artifact_sha256 =
let ( let* ) = Result.bind in
let t = Client.of_root pinned_root in
let* t = Client.update_timestamp ~now t timestamp in
let* t = Client.update_snapshot ~now t snapshot in
let* t = Client.update_targets ~now t targets in
(* Resolve the artifact's name to a signed target, then check the bytes. *)
match Client.lookup ~resolve:(fun _ -> None) ~now t artifact_name with
| Ok (Some target) ->
Ok
(Client.matches target ~length:artifact_length
~sha256_hex:artifact_sha256)
| Ok None -> Ok false
| Error _ as e -> e
The launch check is uniform: a digest is admitted iff it is named in the signed
targets metadata and the bytes hash to it. Delegations let independent
publishers sign for their own namespace (lookup's resolve supplies a
delegated role's metadata on demand); the same check applies at every level.