The Update Framework (TUF) security model over COSE_Sign1/CBOR
0

Configure Feed

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

OCaml 93.9%
Python 1.6%
Dune 1.0%
Other 3.5%
21 1 0

Clone this repository

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

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


README.md

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.