Daml + Nix: Reproducible Builds as a Security Primitive

Daml + Nix: Reproducible Builds as a Security Primitive

At Obsidian, we say that reproducibility is a security primitive not because it proves that software is correct, but because it makes an otherwise invisible part of the release process inspectable.

When a build is reproducible, two different people compiling the same source code on different machines produce bit-for-bit identical output. That lets you ask a question every serious software operator eventually has to ask: was the artifact running in production actually built from the source that was reviewed, audited, and approved?

If builds across machines are not reproducible, they are not correct.

For most language ecosystems this is an interesting question. For Canton, that question is not academic. A .dar may be loaded into infrastructure operated by institutions with audit, change-control, and compliance obligations. “Trust our CI” is not a very satisfying answer. “Here is the source, here is the pinned toolchain, here is the derivation, and here is how you can rebuild the artifact yourself” is much better.

nix-daml-sdk is an open-source Nix packaging of the Daml SDK that Obsidian Systems has operated in production for years, both for our own work and with Daml development teams that have adopted it. It pins the toolchain and makes builds reproducible. It gives you reproducible dev shells. It ships through a binary cache so you don't pay full build costs every time. (The cache is a performance optimization: teams can accept signed substitutes for convenience, and independently rebuild when they need stronger assurance.)

For teams already building serious Daml/Canton systems, the trade-off is unusually favorable: modest additional build-system discipline in exchange for reproducibility, cleaner onboarding, more reliable CI, and a stronger provenance and auditability story.

Why Nix

If you haven't used Nix before, it's a package manager and build system whose central idea is that software builds should be pure functions from inputs (source, compiler version, build flags, environment) to outputs. Same inputs in, same output out, on any machine (for a given target platform).

Inputs to Nix derivation to hashed Nix ouput path.

In ordinary Nix, store paths are input-addressed: the hash is derived from the build recipe and dependency graph. That gives every artifact an identity tied to the exact inputs that were declared. When assurance matters, you can rebuild from those inputs and compare the resulting output bytes. If they match, you have evidence that the artifact was produced from the reviewed source and pinned toolchain.

A few things flow from this proposition:

  • Reproducibility is something you can check. If two developers, or a developer and CI, build the same project, the outputs hash to the same value or one of them is wrong. No more "works on my machine."
  • Builds can be made hermetic. In a sandboxed Nix build, the builder does not get to rely on a developer’s home directory, globally installed tools, or undeclared dependencies. Network access is disabled for ordinary derivations, and whatever the build needs has to come in through declared inputs. That removes a class of bugs where the build accidentally depended on something already installed on the developer’s machine.
  • Multiple SDK versions can coexist. Different projects can pin different SDK versions on the same machine without fighting. There is no global daml-sdk to swap.
  • Development environments are themselves artifacts. The environment is a Nix expression: hand someone the expression and they get the same environment you have, with the same SDK, same JDK, same dpm, same version of the same VS Code extension.

None of these properties are specific to Daml. This is what Nix brings to anything it touches. They also happen to line up well with what Daml's audience needs, which tends to be financial-infrastructure teams operating under compliance constraints. Whether "the build was reproducible" can take on importance as a regulatory, compliance, and security question.

Daml and Nix in practice

Putting Daml and Nix together provides real practical benefits to development teams.

Developer onboarding, before and after Nix

Onboarding is the obvious place to start. Setting up a new developer on a Daml project usually means installing the SDK, the matching JDK, Canton, and the VS Code extension, then aligning the versions across them. With nix-daml-sdk, that work is captured once, in the Nix expression. A new team member installs Nix, clones the project, runs nix-shell, and has the SDK, Canton, dpm, the JDK, and the right VS Code extension version. Nothing depends on what was already on their laptop, and the install document collapses to a paragraph.

Once someone is onboarded, their local build should match what CI produces. Because the build closure is pinned, the artifacts produced in CI are the artifacts you can reproduce locally, and vice versa. The class of bugs that only appear in one environment shrinks toward zero.

If a team works on more than one Daml project, per-project pins keep everything straight and avoid the travails of global state. A team maintaining several Daml projects across different SDK versions can keep them all working without juggling daml install. Each project carries its own pin, and they coexist on the same machine.

Pinning the build closure has effects past the team's own machines, too. A .dar built via Nix is hashed by everything that went into producing it, so it carries a verifiable identity. "Was this artifact built from exactly this source, with exactly this SDK?" becomes a question of comparing two hashes and doesn't have to involve trust in a particular builder.

The same logic extends to a harder question (one that deserves its own post): "Did this binary come from the source we think it came from?" has an easy answer when only the binary or only the source could have been tampered with. It has a harder answer when the build environment itself could have been. Reproducible builds do not eliminate trust from the system, but they move trust to places that can be inspected like sources, pinned dependencies, toolchain metadata, and review process. If the build infrastructure alone is compromised, an independent rebuild from the reviewed source and pinned toolchain should reveal the mismatch. That is a major improvement over a release process where the CI system is simply trusted to have done the right thing.

Why this matters for Canton

As Canton adoption grows among financial institutions, the package and build layer becomes part of the institutional assurance story. It is not enough for a Daml application to be well-designed: institutions also need confidence that the .dar they load corresponds to the source, SDK, dependencies, and review process they approved.

Nix gives Daml teams a practical way to make that claim. A reproducible build does not replace audit, review, or operational controls, but it makes the artifact/source/toolchain relationship verifiable. That is the difference between trusting a release process and being able to inspect it.

Looking ahead

nix-daml-sdk is a solid foundation. It pins the toolchain, makes builds reproducible, and gives you a working cache and dev-shell story. This is what we use in production.

What's ahead is the rest of a Daml package ecosystem built on this foundation: a place to publish Daml libraries, a way for consumers to discover them, audit metadata attached to specific package versions, and a way for validator node operators to confirm that the binary they're loading was built from reviewed source. We have more to say about all of it soon. For now, the appendix below walks through what's in nix-daml-sdk today.

What we'd like to see in the Daml ecosystem

Appendix: A quick technical tour

What's in the package and how to drop into a Daml shell. For developers who want to dig in.

Inside nix-daml-sdk

The package contains the working parts of the Daml toolchain:

  • dpm, the core daml CLI tool
  • the daml assistant with auto-install disabled, so it can't silently fetch a different SDK
  • the matching Canton runtime, with both open-source and enterprise builds supported
  • a VS Code editor pre-configured with the Daml extension at the same SDK version
  • a JDK such as jdk17, selectable so the Java side is part of the same pinned closure
  • a binary cache operated by Obsidian, so most artifacts can be fetched prebuilt

Supported SDK releases today cover the 2.x and 3.x lines (currently 2.5.5 through 3.4.11). Adding a new version is generally as simple as adding a JSON metadata blob to the repository.

Getting started

If you have Nix installed and want to drop into a Daml shell:

git clone https://github.com/obsidiansystems/nix-daml-sdk
cd nix-daml-sdk
nix-shell

To select a particular version:

nix-shell --argstr sdkVersion 3.4.11 --argstr jdkVersion jdk17

You are now in a shell with daml, canton, and dpm on your PATH at exactly the versions you pinned. Quit the shell and your global environment is untouched.

For projects already using Nix, importing nix-daml-sdk into your existing shell expression follows the integration pattern in the README. For a hermetic .dar build, the same machinery works inside a Nix derivation. The output .dar lives at a uniquely-hashed /nix/store/ path, and anyone who builds it from the same inputs gets the same hash.

The repository is available on GitHub. If you'd like to use it on a Daml project of your own, we'd be glad to help.