dwarves-slsa
dwarves' release-build mode (SLSA v1.0 Build L3; owner 2026-10-29: dwarves is the build server). A release build is a Runa pipeline (lane RELBUILD-RUNA): this crate plans it (runner::release_pipeline: sealed Sandbox containers and the forge's own Forge actions), and the dwarves forge runs it through the same Runa runner every CI pipeline runs through (dwarves::release_runa), with CI's caps and cancel. Its runs are pipeline release-build's runs in the forge's history (dwarves history, the UI's Pipeline tab, the server's History / GetRunReport / StepLog). This crate also writes the provenance from the run's record, the DSSE envelope the owner's YubiKey signs, its verification and the attested publish. The dwarves command reaches it as dwarves release-build … and as the Build Thing path dwarves buildthing <product> --suite S --variant V --release-build. There is no shell script and no container program here. First user: the crates.io releases of Holger and Gunnar. The library (statement, dsse, verify, subjects, lock) is what the monetize self-release and the media builds call later; a new kind of build gets its own build type URI.
| Builder id | https://git.gunnar.rs/nordisk/dwarves/slsa/builders/dwarves-server/v1 (statement::BUILDER_ID), THE dwarves builder id: a Build Thing build's provenance step names it too (dwarves' version and commit, releaseMode, runnerAccount and runnerSha256 in runDetails.builder.version) |
| Pipeline | release-build (pipelines/release-build.runa.ron is its shape; Runa run id <release>-<first 12 hex of the request's sha256>) |
| Build type | https://git.gunnar.rs/nordisk/dwarves/slsa/buildtypes/cargo-release/v1 |
| Image | ci4, localhost/nordisk-cargo-toolchain@sha256:875a7a32… (runner::DEFAULT_IMAGE) |
A release, step by step
# a lane asks the dwarves server (the CLI and the UI's "Request release" form call the
# same API, Dwarf.Trigger of release/release-build; neither writes the inbox):
dwarves release-build request --name holger-2.0.1 --steps-from ~/d-pub2/publish-holger-2.0.1.sh \
--pins holger-2.0.1.pins --org tunnr=mikronova --lock holger/Cargo.toml --lock holger/estate/Cargo.toml \
[--server 127.0.0.1:7911]
# -> queued release run holger-2.0.1-<12 hex>
dwarves history # the run, its release state
dwarves release-build show holger-2.0.1-<12 hex> # state, statement, artifacts and sha256, the signing command
# the owner, on loki (the YubiKey never comes near dwarf or the browser):
~/script-sign-release.sh release.statement.json # -> release.intoto.jsonl
dwarves release-build attach holger-2.0.1-<12 hex> release.intoto.jsonl # verified, then stored; the runner checks it again: signed
# then, on a copy of the signed run:
dwarves release-build wait <run> # or verify-run <run>
dwarves release-build publish <run> # dry run: each crate packaged again = the attested bytes
dwarves release-build publish <run> --for-real
dwarves release-build check-published <run> # what crates.io serves = the subjects
dwarves release-build attest <run> --repo ~/d-slsa3/holger # attestations/holger-2.0.1.intoto.jsonl; commit it
# a lane's own build (not release mode; into ~/slsa-runs):
dwarves release-build build --name … (the spec flags)
# a Build Thing variant (binaries from the sources, the repo at its clean HEAD):
dwarves buildthing gunnar --suite ORIGINAL --variant gunnar-open --release-build [--pins siblings.pins]
--steps is the STEPS table of a publish-*.sh script (n crate version repo path verify), --pins its pins file (repo full-sha). Versions crates.io already has are staged from crates.io by their index checksum and not rebuilt (--rebuild-published rebuilds them: a reproducibility check, not for publishing).
A spec (--spec) can also run an EXAMPLE of a workspace from the sources and take the files it writes as subjects (Heimdall's verify-bundle guests: dist builds the three wasm components and their .wasm.blake3). The guest is its own workspace, so its lock is a second lock:
lock = ["heimdall/Cargo.toml", "heimdall/guests/verify-bundle/Cargo.toml"]
[[workspace_example]]
manifest = "heimdall/guests/verify-bundle/Cargo.toml"
example = "dist"
args = ["{out}/guests", "/target/guests"] # {out} = the example's own output directory
outputs = ["guests/heimdall-verify-bundle.wasm", "guests/heimdall-verify-bundle.wasm.blake3", …]
It runs cargo run --release --locked --offline --example dist -- /example-out/guests /target/guests in a build container (network none, CARGO_NET_OFFLINE=true, so cargo the example starts is offline too); the forge then copies exactly the declared outputs to out/ (a missing, undeclared or linked file is red). The image needs what the example needs (Heimdall's: wasm32, clang and wasi-libc's headers, heim-ci by digest).
Release mode: the runner's own account (dwarf's host)
The lanes run as rickard. A runner that shares that account could have its binary or a run directory changed before the owner signs. In release mode it cannot:
- The runner is
dwarves release-build serve. It runs asrelbuild, a system account on dwarf's HOST: no sudo, no password, no SSH login, its own subuid/subgid range for rootless podman, linger on. The system unitdwarves-release-runner.serviceruns it. serverefuses root,rickardand any account but the configured one. It refuses any binary but the installed one:/usr/local/lib/dwarves-release/dwarves, a root-owned 0755 file in a root-owned directory, with the sha256 in/etc/dwarves-release/runner.toml. It refuses a runs directory that another account can write.- The dwarves server writes a REQUEST into
/srv/relbuild/inboxwhen a lane or the UI asks (dwarves release-build request, with the same flags asspec; the server is the inbox's only writer). A request is data only: build release X at commits Y. It hasrebuild_publishedand[spec](a release spec), and an unknown key is refused. The request cannot choose the image: it must be the runner's pinned ci4. It cannot choose where the sources come from either. The runner's config sets the forge: anonymous for public repositories, and relbuild's own read key formikronova. - Runs go only to
/srv/relbuild/runs/<release>-<utc>, which the lanes can read but not write. Each request's bytes and status are inruns/requests/<sha256>.{request.toml,status}. The status names the run, the statement and the statement's sha256. - The provenance's
runDetails.builder.versionnames the account (runnerAccount,runnerUid),releaseMode = "dedicated-account",runnerBinary,runnerSha256(the binary's sha256) andrequestSha256. Every run, in lane mode too, recordsrunnerAccount.verifycan require the account:Expect { runner_account: Some("relbuild"), .. }. build --release-mode, orDWARVES_RELEASE_MODE=1, refuses a lane's direct build, including the Build Thing--release-buildpath.- The runner runs each request as a Runa pipeline, its sealed containers on its OWN rootless podman API (
dwarves-release-podman.service,enginein the config,unix:///run/dwarves-release/podman.sock), and records the run in/srv/relbuild/runs/warehouse, which the forge reads. - The owner's signed bundle comes back through the server (
attach,Dwarf.ReleaseBundle): verified against the release key, the run's statement andout/before it is put in/srv/relbuild/inbox/bundles/; the runner verifies it again, stores it as the run'srelease.intoto.jsonland marks the requestsigned.
# once, and again for each new runner binary: as root on dwarf's HOST, from /
sudo bash scripts/relbuild-user.sh <dwarves binary: a host path, or a path in the claude container>
The owner signs the statement of /srv/relbuild/runs/<run>/ and checks its sha256 against the one show prints (the sign script shows it). The lanes then copy the signed run and run wait, publish, check-published and attest on the copy. After the signature, any change to a copy is refused.
scripts/relbuild-user.sh never touches the YubiKey.
The isolation (checked on the sealed containers' data before the pipeline exists)
- a fresh run directory per release; the sources checked out by commit id, read-only (
Forgesteps; git and git-lfs are the only programs the forge itself starts: no library does LFS); - every container is a Runa
Sandboxstep, run SEALED by the forge's one engine (draupnir, podman's REST API; never apodmancommand): the image by digest, never pulled, a read-only root, every capability dropped,no-new-privileges, only the run directory's own mounts, no$HOME, no key, no secret-named variable, no sccache, no shared registry or target, and onecargoargv (no shell); - the fetch and probe containers (network on) run
cargo vendor --lockedandcargo generate-lockfileonly; the forge checks every vendored crate against the locks; - every build container (
cargo package --locked --offlineper crate,cargo build --release --locked --offlineper binary,cargo run --release --locked --offline --exampleper workspace example) has the network none and the sources, vendor, stage, plan and published crates read-only; the forge stages each packaged crate for the next in Rust; - CI's budget holds during the run (the run directory under 40 GB, the disk over 110 GB free); the first red step cancels the rest;
- the provenance is written by the forge from the Runa run's record, after the run.
The release key is keys/release-signing-key.pub: the owner's YubiKey key (PIV slot 9c, P-256, PIN once, touch always), key id d24d3785e8281bcd37e8f2c40a3a85e8152d3ce2200a5c929a1a5ca636e8249e (release_key::RELEASE_KEY_ID). It is the same file as slsa/release-signing-key.pub in holger and gunnar, where its YubiKey attestation sits beside it. verify, wait, verify-run, publish, check-published and attest use it unless --key or $SLSA_KEY names another key; a bundle signed with any other key (SLSA_TEST_KEY included) is refused.
The one-time YubiKey setup is scripts/yubikey-setup.sh (the owner's). Verification for users: Holger's and Gunnar's docs/SLSA.md.