Skip to content

mschwaig/laut

Repository files navigation

laut /laʊt/ - distributed trust and verifiable provenance data for Nix

The name is german for1

  • loud, noisy, blatant 📢
  • (as) per, according to, in accordance with 🕵️‍♀️

🚧 This is an in-progress implementation of https://dl.acm.org/doi/10.1145/3689944.3696169. 🚧


Build results in Nix today can travel from builder to cache to user, "trusted" along the way but not attributed to whoever actually produced them — like a game of telephone. laut is a standalone tool that ships as a secondary binary alongside Nix and is more pedantic about where things come from. Each claim about a build is signed by whoever made it and is designed to be precise enough that laut can aggregate signatures from original sources rather than from whichever cache a result happened to pass through. This lets you pick who you trust independently from everyone else and change your mind about it over time.

The fundamentals are in place, with a few things still needing work (marked ❎):

  • configurable trust model2 ✅, ...
  • which can be re-configured over time, ✅ based on ...
  • verifiable provenance data for builders ❎
  • like realizations for CA derivations ✅, and also for IA derivations ✅
  • based on a new proposed signature format on top of JWS ✅, with
  • arbitrary additional ✅ but detachable ❎ metadata

Right now laut can resolve the dependencies for and verify a fully content-addressing or input-addressing hello binary, end to end, in our VM tests. The implementation is approaching a state where it is ready for its first users; until then expect breakage and short iteration times. As a project we are also not yet committed to supporting the current shape of the signatures long term — the envelope format and a few payload fields may still change. More paranoid features (provenance transparency log entries, remote attestation, sigstore-style integration) are on the horizon, and the dream is to get the signatures (or log entries) widely available, e.g. via the NixOS Hydra instance.

If you do something that's inspired by this project, please give me a shoutout — it helps me demonstrate the relevance of this work in an academic context.

How can I use it

This is a standalone command line tool called laut, which has two subcommands.

The first one is

laut sign-and-upload --to [HTTP cache URL] --secret-key-file [KEY] [DRV_PATH]

which signs a derivation with the new signature format and uploads it to the traces/ namespace of the provided HTTP cache. This is meant to run from a Nix post-build hook, in the same slot where legacy signatures are normally uploaded from nix-based builders. Exit codes: 0 = signed and uploaded, 117 = no-op (the hook fired on the unresolved drv, or on a FOD), 1 = error. The $OUT_PATHS environment variable set by nix in the post-build hook supplies the output paths.

The second one is

laut verify --cache [HTTP cache URL] --trusted-key [path to public key file] [derivation path or flake reference]

which is run manually by the user after building or obtaining an output from a cache. It tries to verify that an output can be derived from a given derivation according to the stricter validation criteria of the tool: it resolves the dependency tree itself, gathers signatures from the configured caches, and feeds the resulting facts into a trust-model evaluator that decides whether the configured trust model is satisfied.

How does it work

It's a Rust workspace (laut-cli for argument parsing and dispatch, laut-sign for sign-side orchestration and the shared core, and laut-verify for verification). The hashing schemes and ATerm / castore encoding come from nix-compat / laut-compat on the mschwaig/snix#fanfic branch, and the signature envelope is JWS-based.

The signing side is straightforward: it walks the derivation, computes the resolved input hash, gathers output content hashes, and assembles a signed JWS token. Both content-addressed and input-addressed derivations are supported — for IA derivations the signer walks the runtime closure to substitute synthetic CA paths and computes the CA-equivalent resolved input hash.

The verification is more complicated, as it instantiates an actual dependency tree in memory, then walks through that tree to gather information. As part of this verification phase, the tool also gathers signatures from a set of caches, taking into account possible combinations of inputs by content hash, which could satisfy the dependency on those same inputs by input hash. This data then feeds into a verifier that decides whether the configured trust model is satisfied. The verification semantics are defined in docs/semantics.md.

How can I test it

There are Rust unit tests (next to the code) and integration tests, runnable inside a nix develop shell with

cargo test --workspace

plus NixOS VM tests that exercise the end-to-end signer + verifier flow. Build a test driver with, for example,

nix build .#checks.x86_64-linux.small-ca-sign.driverInteractive

then run the resulting binary and call test_script() inside the driver shell. The VM tests come in {small,medium,large} × {ca,ia} flavors (the IA flavors currently exist as red baselines until IA support is wired up end-to-end).

In the future different VM tests should exercise different trust models, but right now they all uniformly only trust builderA and builderB in combination.

FAQ

Q: Do you want to upstram this?
A: Yes. With this project, I want to lead a credible effort to propose a specific signature format, which does what I want from such a format, as outlined in my paper.

Q: Do you accept contributions?
A: Yes, I am enthusiastic about collaborating on this, and helping people with getting started on that. I also want to reply to proposals and criticism within a week. If I don't and you're waiting on an answer from me, please remind me.

Q: What do you want from a signature format in Nix?
A: To turn Nix into a leading edge supply chain security tool. Nix has interesting properties in that area, but it is not living up to its potential yet.

Q: Are you interested in working with different implementations of Nix?
A: Yes, definitely. I feel like a flexible enough signature format can be especially useful in an increasingly diverse ecosystem. Let's make it possible to let Nix evolve over time, try new ideas, AND interoperate as much as possible while doing it. It's important to me to have a good working relationship with others in the community across various implementations of Nix, who care about these issues as well. Please open issues, reach me on matrix or via email at m@groundry.org.

Q: Why are you not implementing this in Nix or any of its implementations directly?
A: Eventually that is definitely the way you would want to do this kind of thing, but for now it is meant to prove the concept (also across implementations) and introduce it to an expert audience, with a lot of breakage much shorter iteration times.

Q: Can I use this now?
A: The implementation is approaching a point where it is ready for its first users, and the VM tests already verify a full CA or IA hello end to end. Until it lands there, expect breakage and short iteration times, and treat the current signature shape as not yet final. If you want to help shape it, now is a good time to get involved.

Glossary

Here is a list of technical terms we use in this project with their definitions:

derivation / drv
Nix uses this term for build steps, which are identified and defined by their characteristic input hash. In this project we will define a derivation strictly as an element i in the domain of a function build(i: input) -> output and not as the pair of both input output (i, build(i)).
unresolved derivation / udrv
A derivation, which depends on other derivations.
resolved derivation / rdrv
A derivation, which does not depend on other derivations (anymore). The content-addressed derivation RFC also calls this a basic derivation.
derivation output / output path
Each derivation can have more than one derivation output, which show up in the Nix store as/at separate output paths, but were created by building the same derivation. This step of indirection and distinction between individual outputs of a derivation is not an important concern when reasoning about trust, but it shows up in the technical details sometimes. Derivation outputs refers to the abstract names of these outputs, written as /nix/store/{hash}-{name}.drv$out, while output path refers to their "physical manifestation" in terms of a path / address and the contents of those outputs in the store, like /nix/store/{hash}-{name} and its content.
content hash
Describes the bitwise identity of a file or path by hashing it in a defined manner.
output map
A mapping from each derivation output name of a derivation to its corresponding content hash. Together with a resolved input hash, an output map describes one execution of build: the resolved input hash names the input set, and the output map names what was produced for each named output.
dependency resolution / resolution
The process of resolving a derivation, by replacing each dependency on another derivation in terms of a derivation output of another unresolved derivation with its bitwise identity in term of a content hash.
The following adds detail using a bunch of forward references: For derivations using the CA derivation experimental feature, this is done explicitly by replacing entries in the inputDrvs attribute of the drv with entries in the inputSrc attribute of the drv. For IA derivations or CA derivations with IA dependencies, this happens implicitly every time the contents of an IA path are accessed.
input hash
The identifying and defining hash of a derivation. If a derivation is the input to, and therefore an element in the domain of, a build function, the input hash is a lookup key, which identifies this element and can therfore be used to store and look up build outputs or their content hashes. All derivations in Nix have an input hash, even CA derivations.
unresolved input hash
A type of input hash which is constructed from the set of inputs recursively, so that reflects the bitwise identity of only the leaves in the dependency tree in question, and the build recipe identity of how they are put together. This is called a deep constructive trace up to terminal inputs in the build systems a la carte paper, and my first paper. In Nix it is the hash that is part of the store path of any regular (input-addressed derivation). It is why they are called input-addressed.
resolved input hash
A type of input hash which is constructed from the set of inputs and incorporates identity of all direct dependencies by a content hash. This is called a constructive trace in the build systems a la carte paper, and my first paper. In Nix it is the hash of a resolved content-addressed derivation. The derivation itself is still input-addressed, and it has an input hash, but the individual inputs that factor into that hash are direct dependencies that are included with their content hash.
IA derivation
A regular derivation in Nix is called input-addressed (IA), because it's path contains an unresolved input hash. This path containing the unresolved input hash is the lookup key to find the output of the derivation in the store. This means we look up o = build(unresolved_ia_i) in the store directly by accessing /nix/store/{path(unresolved_ia_i, drv_output)}, which contains build(unresolved_ia_i)
CA derivation
A content-addressed (CA) derivation uses the ca-derivations feature in Nix. Before building it does dependency resolution on the unresolved CA derivation, to obtain a resolved CA derivation. The input hash of the resolved CA derivation becomes the lookup key to find the output of the derivation outside the store. We look up o = build(resolved_ca_i) outside the store. Since the store is content-addressed, we then find the output in the store by looking up the output path /nix/store/{content_hash(o)}-{name(o)}, which contains o. CA derivations are sometimes called floating CA derivations to distinguish them from FODs.
leaves / leaf nodes
The outer nodes of any dependency tree might be things like sources files, or binary blobs. We call them leaves or leaf nodes, the build systems a la carte paper calls them terminal inputs.
FOD / FO derivation
FODs are a different kind of content-addressed derivation, which nix has supported for a long time. They pre-declare the hash of their outputs, which means their output paths can be pre-computed, even though they are content-addressed. When we use the term CA derivation, we do not include FODs. In our work FODs are considered content-addressed leaves, aka terminal inputs, in the dependency tree.
IA path
The output path of an IA derivation.
CA path
The output path a CA derivation or FOD.
build trace
A statement which associates the resolved input hash of a derivation with the output hashes of the set of produced output.
provenance log entry
A cyptograpically secured statement which associates the resolved input hash of a derivation with the output hashes of the set of produced output, and an open set of additional metadata about the builder. This potentially includes a source reference for the builders claimed software state and maybe even a remote attestation of said software state. This statement might be sigend, or be entered in a transparency log.
nix legacy signature
A statement which associates the unresolved input hash of a derivation with the output hash of a specific produced output. This does not contain any data about the builder, and depends on all of those implicit dependency resolutions that happen with IA derivations, because it uses an unresolved input hash. I'm calling it legacy because we are trying to replace it as the load-bearing component in terms of trust.
laut signature
A signature in the format specified in this repository.
trustlessly-resolved derivation
We call a derivation, for which the validator resolves all dependencies and then looks up build traces trustlessly resolved.
trustfully-resolved derivation
We call a derivation, for which the validator looks up a legacy signature and thereby trusts however its builder resolved its immediate dependencies trustfully resolved.
trust model
A set of trusted keys and additional, per key, validation criteria which must be met to consider a provenance log entry or nix legacy signature valid.
threshold function
The way trust models are constructed from trusted keys is using a threshold function. threshold(m, n = len(keys), keys: set), where only the mapping from inputs to outputs are considered trustworthy, which m out of n keys agree on. This is used to build OR and AND functions. We actually also not only allow keys as input to the threshold function, but also trust models, which allows for more complex trust model, but also makes the definition of trust model recursive.
builder
A verifier of the inputs to its own builds, as well as a a signer of the outputs of its own builds.
signer / producer
A builder, which produces signatures they have built themselves, and potentially uploads them to a cache.
verifier / consumer
A consumer (and verifier according to some trust model) of signatures, and possibly also build outputs, from a cache.
legacy signer
A builder or INTERMEDIARY, producing signatuers. While addressing this problem is out of scope for laut, signing intermediaries introduce transitive trust relationships that are difficult to revoke, which is why we think groups of builders should be organized around data structures like signed lists or transparency logs instead.

Footnotes

  1. according to https://en.langenscheidt.com/german-english/laut 📖

  2. set of trusted builders with additional constraining criteria, including consensus

About

A new signature format for Nix - for verifiable provenance data and SBOMs.

Resources

License

Stars

58 stars

Watchers

5 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors