Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Supply chain

How a downloaded RS-Key release proves it came from this source tree, was built by this project’s CI, and pulls in only reviewed dependencies. And how you verify each claim yourself.

No private keys are involved here. The build provenance is keyless (sigstore/Fulcio, signed against this workflow’s GitHub OIDC identity, recorded in the public Rekor transparency log). The only signing key in the project is the secure-boot key. That is a different thing entirely. It seals an image so the RP2350 bootrom will run it. It has nothing to do with the supply chain. See production.md for that.

What every release carries

LayerArtifactWhat it proves
Reproducible buildthe 14 .uf2 flavorsthe binary is a pure function of the source at the tag. Anyone can rebuild it — which says the BUILD is deterministic, not that the machine code preserves what the source proves
Repro gate(CI, blocking)the release job fails if any flavor doesn’t rebuild bit-identical, so a non-reproducible image is never published
Checksums + signatureSHA256SUMS + SHA256SUMS.sigstore.jsonthe hashes were signed by this repo’s release workflow (keyless cosign). Up to v0.4.10 the same file is named SHA256SUMS.cosign.bundle
Build provenancea GitHub attestation, plus rs-key-<tag>.intoto.jsonl on the releasewhich reusable workflow, at which commit, on which runner built each .uf2. SLSA v1 Build L3, keyless via attest-build-provenance. The API copy is authoritative; the file is for offline checking
SBOMrs-key-<tag>-sbom.cdx.jsonthe CycloneDX bill of materials for the firmware crate
Dependency auditsupply-chain/ (in-repo)every dependency is covered by an imported audit or a recorded exemption (cargo-vet)

The toolchain that builds it

A reproducible build says the binary is a pure function of the source and the toolchain. This is the toolchain half: every tool a shipped byte passes through, and every tool whose word a published verification result rests on, with the file that pins each one. The table is generated from assurance/toolchain.toml and re-read out of flake.lock, Cargo.lock, .github/workflows/ and assurance/platform.toml on every check.sh run.

Generated by scripts/toolchain_gate.py from assurance/toolchain.toml and the files that pin it. Each pin below was re-read out of its provenance on the run that wrote this table; a row whose file no longer says the same thing fails check.sh.

ToolRolePinned byPin
arm-none-eabi-asassemblerflake.lock:nixpkgs331800de5053fcebacf6813adb5db9c9dca22a0c
arm-none-eabi-gcccompilerflake.lock:nixpkgs331800de5053fcebacf6813adb5db9c9dca22a0c
arm-none-eabi-nmpackagerflake.lock:nixpkgs331800de5053fcebacf6813adb5db9c9dca22a0c
cargopackagerflake.lock:fenix3a556b6fbd42412b6f3f0ea8d35959b1826f86ff
cargo-kaniproverworkflow:.github/workflows/ci.yml:KANI_VERSION0.67.0
cbmccheckernothingopen obligation PLAT-TOOL-003
cortex-massemblerCargo.lock:cortex-m8ec610d8f49840a5b376c69663b6369e71f4b34484b9b2eb29fb918d92516cb9
flip-linklinkerflake.lock:nixpkgs331800de5053fcebacf6813adb5db9c9dca22a0c
jre8runtimeflake.lock:nixpkgs331800de5053fcebacf6813adb5db9c9dca22a0c
picotoolpackagerflake.lock:nixpkgs331800de5053fcebacf6813adb5db9c9dca22a0c
rust-lldlinkerflake.lock:fenix3a556b6fbd42412b6f3f0ea8d35959b1826f86ff
rustccompilerflake.lock:fenix3a556b6fbd42412b6f3f0ea8d35959b1826f86ff
tlapluscheckerflake.lock:nixpkgs331800de5053fcebacf6813adb5db9c9dca22a0c

Root inputs of flake.lock deliberately outside this TCB:

  • flake-utils — eachDefaultSystem plumbing (flake.nix:36). It emits no binary into any build; it decides which systems the outputs are instantiated for.
  • nixpkgs-sdl2 — SDL2 alone, for the tools/emu –display window (flake.nix:9-15). Nothing from this pin enters a firmware build, a Kani run or a TLC run; it exists because unstable’s sdl2-compat emits SDL3 window events the Rust crate aborts on.

The FFI boundaries. Every place a name in this tree binds to a symbol Rust does not define, read out of the tree on the run that wrote this table: an item an extern block declares (import), an item defined here for a foreign caller (export), and a foreign translation unit a build.rs compiles into the image (unit). The set is held both ways — a crossing no [[boundary]] row claims fails check.sh, and so does a row for a crossing that is no longer there.

BoundaryKindDerived fromForeign side produced by
bignum_modexp_private_exponentimportcrates/rsk-rsa/src/lib.rs, extern "C", #[cfg(target_os = "none")]arm-none-eabi-gcc
bignum_modexp_public_exponentimportcrates/rsk-rsa/src/lib.rs, extern "C", #[cfg(target_os = "none")]arm-none-eabi-gcc
rsa_private_exp_crtimportcrates/rsk-rsa/src/lib.rs, extern "C", #[cfg(target_os = "none")]arm-none-eabi-gcc
__kvcnt_endimportfirmware/src/main.rs, extern "C", unconditionalrust-lld
__kvcnt_startimportfirmware/src/main.rs, extern "C", unconditionalrust-lld
__kvmain_endimportfirmware/src/main.rs, extern "C", unconditionalrust-lld
__kvmain_startimportfirmware/src/main.rs, extern "C", unconditionalrust-lld
_stack_endimportfirmware/src/main.rs, extern "C", unconditionalflip-link
crates/rsk-rsa/csrc/bignum_asm.Sunitcrates/rsk-rsa/build.rs, cc::Buildarm-none-eabi-as
crates/rsk-rsa/csrc/bignum_high_level.cunitcrates/rsk-rsa/build.rs, cc::Buildarm-none-eabi-gcc

Coverage against the six TCB categories. Enumerated here, each with a pin held against its source file:

  • assembler — arm-none-eabi-as, cortex-m
  • compiler — arm-none-eabi-gcc, rustc
  • ffi — the 10 boundary(s) in the table above, each joined to the pinned tool that produces its foreign half
  • linker — flip-link, rust-lld

Not enumerated by this generator at all, and not by any other:

  • bootrom — burned into the RP2350; no file here records a version or a hash for it (assurance/platform.toml’s PLAT-ROM-001 stands in its place)
  • llvm-passes — rustc’s LLVM version is written in no file of this tree and its pass pipeline in none anywhere; rustc -vV would answer for one machine

What the table proves is that each tool it names carries a pin equal to the value in the file that pins it. It does not prove that the tool which actually ran was that version, that a contributor’s PATH holds it, or that a /nix/store path still matches its narHash.

And the boundaries are the boundaries of this tree. A dependency declares its own, in its own source, where no reader of this checkout can see them: 25 of Cargo.lock’s 276 checksummed crates carry extern "C" or #[no_mangle], cortex-m and cortex-m-rt among them. For those, what is recorded here is the pin that fixes the crate, not the symbols it crosses on.

The release procedure, command by command

The table above pins the tools. This one records the procedure: every command a release runs, the inputs each one consumes, and — one column at a time — what that command actually settles. It is generated from release.yml, release-build.yml and firmware.nix on every check.sh run: every command, flavor, action pin and asset name below is read out of those three files rather than typed here.

Deriving the commands is not the same as holding the pipeline, and the earlier wording claimed it was. A key beside the commands decides whether the steps run at all, on which machine, and with which token — and a derivation that reads commands never sees one. Sixteen such edits were measured past the first version of this row, from if: false on the job to a set +e inside the reproducibility gate. They are refused by the same row now, by rule rather than by derivation: the job’s own keys, its runner and its scopes are pinned, a step that may not run or whose block is not bash is refused, and so is a caller that runs steps of its own or releases a tag outside v*. What is neither derived nor pinned is listed under What this table does not read, at the end.

Generated by scripts/release_gate.py from .github/workflows/release.yml, .github/workflows/release-build.yml, nix/firmware.nix and scripts/pt.sh. Every command and every input below was read out of those files on the run that wrote this table; a file that no longer says the same thing fails check.sh.

This is a recipe, not a record. It says what a release runs, over which inputs. It is bound to no tag, no commit and no artifact — see the open obligations at the end.

Two words that are not each other.

  • bit-for-bit — the same inputs produce the same output bytes. A property of the build, established below.
  • source->binary — the emitted machine code does what the source says. A property of the compiler, established by nothing here. PLAT-TOOLCHAIN-001 in assurance/platform.toml owns it and is pending; it writes the same gap source-to-binary in prose.

A miscompilation reproduces exactly as well as a correct compilation does, so no rebuild can settle the second word. The subject column is held against the shape of each step’s own command, and the second word is not a value that column can take.

What a subject means

The closed vocabulary the Subject column below is drawn from. Printed here because a gloss nothing reads is a gloss anything can be written into: rewriting one to say what source->binary says, under a key this gate was comparing and never opening, was the third way past the rule above.

SubjectWhat claiming it means
admissionwhich commits may become a release at all
bit-for-bitthe same inputs produce the same output bytes
integritythe bytes you have are the bytes that were published
inventorywhat went into the artifact
noneproduces or publishes an artifact; establishes nothing on its own
originwhich workflow, at which commit, on which runner built it

The steps a release runs

#StepSubjectWhat it settles
1actions/checkoutnoneFetches the tagged tree, deep enough that the tag gate below can read origin/main out of it.
2DeterminateSystems/nix-installer-actionnoneInstalls Nix. The build’s determinism rests on the sandbox this provides, not on the runner image.
3nix-community/cache-nix-actionnoneA cache keyed on flake.lock. A cache hit and a cache miss must produce the same bytes; the rebuild step is what says they do.
4actions/cachenoneThe cargo registry cache. Outside the Nix sandbox, so it feeds no shipped byte — nix build vendors from Cargo.lock.
5sigstore/cosign-installernoneInstalls the signer. Its pin is part of the trust base: this is the code that holds the OIDC token.
6resolve tagadmissionShape, charset, and an ancestor-of-main test. Defence in depth only: an actor who can push a tag also controls this file at that ref, so the primary control is a repository tag ruleset.
7build the 14 reproducible firmware flavorsnoneProduces the images. A build alone establishes nothing — it is the rebuild below that turns it into evidence.
8reproducibility gate — rebuild all 14, require bit-identicalbit-for-bitRecompiles every flavor already in the store and fails on a hash mismatch, so a non-reproducible image is never published. Says the BUILD is a function of its inputs; says nothing about what the machine code means.
9generate the CycloneDX SBOMinventoryThe firmware crate’s dependency tree, scoped to the shipped target.
10checksumsintegrityA digest of every .uf2 and of the SBOM. It does not cover itself, its own signature or the provenance bundle — the last two are written after this step, and the attestation is what stands behind them.
11attest build provenanceoriginGitHub build provenance for the .uf2 files this step is handed, bound to THIS reusable workflow’s identity — the difference between SLSA Build L3 and L2.
12attach the provenance bundle as a release assetnoneAn offline copy of the attestation. The API and Rekor stay authoritative; immutable releases forbid adding it later, so it is attached at create time.
13sign SHA256SUMS (keyless cosign)integrityKeyless cosign over the checksum file. No private key exists: the signer is this workflow’s OIDC identity.
14extract release notes from CHANGELOGnoneExtracts the version’s CHANGELOG section. Cosmetic, and capped because an oversized body fails the job after the tag is already pushed.
15create the GitHub ReleasenonePublishes dist/*. Refuses to publish a no-touch asset, which would ship a build with the physical-consent gate removed.

The commands those steps run, as this gate reads them: \ continuations joined, and a comment cut where a shell would cut one — a # inside a quoted string is part of the command, so an awk '…' program’s own text stays whole and nothing hides behind a quote.

# 1. uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
# 2. uses: DeterminateSystems/nix-installer-action@3138316df39ed29be04236d7ffc686fa525866aa # v23
# 3. uses: nix-community/cache-nix-action@7df957e333c1e5da7721f60227dbba6d06080569 # v7.0.2
# 4. uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
# 5. uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.2
# 6. resolve tag
case "$TAG" in
v[0-9]*.[0-9]*.[0-9]*) ;;
*) echo "refusing: tag is not vMAJOR.MINOR.PATCH" >&2; exit 1 ;;
esac
case "$TAG" in
*[!0-9A-Za-z.+-]*)
echo "refusing: tag has characters not allowed in a release tag" >&2
exit 1 ;;
esac
git fetch --no-tags --quiet origin +refs/heads/main:refs/remotes/origin/main
if ! git merge-base --is-ancestor "$TAG^{commit}" origin/main; then
echo "refusing: $TAG does not point at a commit on main" >&2
exit 1
fi
echo "tag=$TAG" >> "$GITHUB_OUTPUT"
echo "version=${TAG#v}" >> "$GITHUB_OUTPUT"
# 7. build the 14 reproducible firmware flavors
tag="${{ steps.tag.outputs.tag }}"
mkdir -p dist
for pkg in firmware firmware-pqc firmware-fips firmware-fips-pqc firmware-strong-pin firmware-strong-pin-pqc firmware-always-uv firmware-always-uv-pqc firmware-strict-up firmware-strict-up-pqc firmware-display firmware-2mb firmware-16mb firmware-strict-config; do
echo "::group::nix build .#$pkg"
out="$(nix build ".#$pkg" --no-link --print-out-paths)"
label="${pkg#firmware}"; label="${label#-}"
[ -z "$label" ] && label="default"
cp "$out/$pkg.uf2" "dist/rs-key-${tag}-${label}.uf2"
echo "::endgroup::"
done
ls -l dist
# 8. reproducibility gate — rebuild all 14, require bit-identical
for pkg in firmware firmware-pqc firmware-fips firmware-fips-pqc firmware-strong-pin firmware-strong-pin-pqc firmware-always-uv firmware-always-uv-pqc firmware-strict-up firmware-strict-up-pqc firmware-display firmware-2mb firmware-16mb firmware-strict-config; do
echo "::group::nix build .#$pkg --rebuild"
nix build ".#$pkg" --rebuild --no-link
echo "::endgroup::"
done
echo "all 14 flavors rebuilt bit-identical"
# 9. generate the CycloneDX SBOM
tag="${{ steps.tag.outputs.tag }}"
nix develop -c cargo cyclonedx --manifest-path firmware/Cargo.toml --target thumbv8m.main-none-eabihf --format json
sbom="firmware/firmware.cdx.json"
if [ ! -f "$sbom" ]; then
for f in firmware/*.cdx.json; do
[ -e "$f" ] || continue
sbom="$f"
break
done
fi
if [ ! -f "$sbom" ]; then echo "no SBOM produced" >&2; exit 1; fi
cp "$sbom" "dist/rs-key-${tag}-sbom.cdx.json"
# 10. checksums
cd dist
sha256sum ./*.uf2 ./*.cdx.json > SHA256SUMS
cat SHA256SUMS
# 11. uses: actions/attest-build-provenance@4d101475d8b20a2381f78447822ac1eab6504dd8 # v4.2.2
# 12. attach the provenance bundle as a release asset
cp "${{ steps.attest.outputs.bundle-path }}" "dist/rs-key-${{ steps.tag.outputs.tag }}.intoto.jsonl"
# 13. sign SHA256SUMS (keyless cosign)
cosign sign-blob --bundle dist/SHA256SUMS.sigstore.json dist/SHA256SUMS
# 14. extract release notes from CHANGELOG
ver="${{ steps.tag.outputs.version }}"
awk -v v="$ver" '
$0 ~ "^## \\[" v "\\]" { f = 1; print; next }
/^## \[/ { if (f) exit }
f { print }
' CHANGELOG.md > release-notes-full.md
cp release-notes-full.md release-notes.md
if [ ! -s release-notes.md ]; then
echo "RS-Key ${{ steps.tag.outputs.tag }}" > release-notes.md
fi
if [ "$(wc -c < release-notes.md)" -gt 118000 ]; then
awk '
/^### / && $0 !~ /^### TL;DR/ { exit }
{ print }
' release-notes.md > release-notes.short
if [ "$(wc -c < release-notes.short)" -lt 200 ]; then
awk '
{ total += length($0) + 1 }
total > 115000 { exit }
/^- \*\*/ { last = NR }
{ line[NR] = $0; kept = NR }
END {
stop = (last > 1) ? last - 1 : kept
for (i = 1; i <= stop; i++) print line[i]
}
' release-notes.md > release-notes.short
fi
mv release-notes.short release-notes.md
{
echo
echo "_Shortened to fit GitHub's release-body limit._"
echo "_Every entry is in [CHANGELOG.md](https://github.com/${{ github.repository }}/blob/${{ steps.tag.outputs.tag }}/CHANGELOG.md) at this tag._"
} >> release-notes.md
fi
if grep -qE '<!-- @increase-anti-rollback-epoch([^>]*)-->' release-notes-full.md; then
marker="$(grep -oE '<!-- @increase-anti-rollback-epoch([^>]*)-->' release-notes-full.md | head -n1)"
if ! grep -qF "$marker" release-notes.md; then
printf '\n%s\n' "$marker" >> release-notes.md
fi
fi
{
echo
echo "---"
echo "Verify a download: https://github.com/${{ github.repository }}/blob/${{ steps.tag.outputs.tag }}/docs/supply-chain.md"
} >> release-notes.md
cat release-notes.md
# 15. create the GitHub Release
tag="${{ steps.tag.outputs.tag }}"
if ls dist/*no-touch* >/dev/null 2>&1; then
echo "refusing to publish a no-touch firmware asset" >&2
exit 1
fi
gh release create "$tag" --title "RS-Key $tag" --notes-file release-notes.md dist/*

The image each flavor is

The build step and the reproducibility gate iterate the same list, and a difference between them is a check.sh failure. Every published image is nix/firmware.nix’s package of the same name.

FlavorPublished asBuild selection
firmwarers-key-<tag>-default.uf2no flags, no knobs
firmware-pqcrs-key-<tag>-pqc.uf2--features advertise-pqc
firmware-fipsrs-key-<tag>-fips.uf2--features fips-profile
firmware-fips-pqcrs-key-<tag>-fips-pqc.uf2--features fips-profile,advertise-pqc
firmware-strong-pinrs-key-<tag>-strong-pin.uf2--features strong-pin
firmware-strong-pin-pqcrs-key-<tag>-strong-pin-pqc.uf2--features strong-pin,advertise-pqc
firmware-always-uvrs-key-<tag>-always-uv.uf2--features always-uv
firmware-always-uv-pqcrs-key-<tag>-always-uv-pqc.uf2--features always-uv,advertise-pqc
firmware-strict-uprs-key-<tag>-strict-up.uf2--features strict-up
firmware-strict-up-pqcrs-key-<tag>-strict-up-pqc.uf2--features strict-up,advertise-pqc
firmware-displayrs-key-<tag>-display.uf2--features display, flashSize = "16M", ledKind = "none"
firmware-2mbrs-key-<tag>-2mb.uf2flashSize = "2M", kvmain = "896K"
firmware-16mbrs-key-<tag>-16mb.uf2flashSize = "16M"
firmware-strict-configrs-key-<tag>-strict-config.uf2--features strict-config

Inside the sandbox

What nix build .#<flavor> runs, out of nix/firmware.nix:

# buildPhase
export PATH="${toolchain}/lib/rustlib/$(rustc -vV | sed -n 's/^host: //p')/bin:$PATH"
cargo build --release --offline --frozen -p firmware --target ${target} --config "target.${target}.rustflags=[\"--remap-path-prefix=$NIX_BUILD_TOP=/build\",\"--remap-path-prefix=${toolchain}=/toolchain\"]" ${lib.escapeShellArgs cargoFlags}
# installPhase
mkdir -p "$out"
bash scripts/pt.sh "target/${target}/release/firmware" "$out/${name}.elf"
picotool uf2 convert "$out/${name}.elf" -t elf "$out/${name}.uf2"

Inputs

The files that decide what a release is. A digest here covers the parts the tables above do not print, and it moves when its file moves and at no other time.

Filesha256
.github/workflows/release.yml943eae75595831de403c51f1468be0d255b182a20fb155ab9a9eca956c54270f
.github/workflows/release-build.yml1ddbf12b1acfd1e09793a9b574128e2e692cf76351b5e5614545086894655584
nix/firmware.nix93468064b0328a06e8059b418e3e220df3ee4f273b4445052494f832eb5e26d8
scripts/pt.shc55ba6255421a664c13ba8b1e3b05b01af842d3ba8e95cd986a7a46005c877ed

The CI code that runs them, pinned by commit:

ActionPinRelease
actions/checkout3d3c42e5aac5ba805825da76410c181273ba90b1v7.0.1
DeterminateSystems/nix-installer-action3138316df39ed29be04236d7ffc686fa525866aav23
nix-community/cache-nix-action7df957e333c1e5da7721f60227dbba6d06080569v7.0.2
actions/cache55cc8345863c7cc4c66a329aec7e433d2d1c52a9v6.1.0
sigstore/cosign-installer6f9f17788090df1f26f669e9d70d6ae9567deba6v4.1.2
actions/attest-build-provenance4d101475d8b20a2381f78447822ac1eab6504dd8v4.2.2

The toolchain closure — flake.lock, Cargo.lock and the tools they pin — is the table above this section, and is deliberately not restated here: one copy, one place it can rot.

Published assets: SHA256SUMS, SHA256SUMS.sigstore.json, rs-key-<tag>-16mb.uf2, rs-key-<tag>-2mb.uf2, rs-key-<tag>-always-uv-pqc.uf2, rs-key-<tag>-always-uv.uf2, rs-key-<tag>-default.uf2, rs-key-<tag>-display.uf2, rs-key-<tag>-fips-pqc.uf2, rs-key-<tag>-fips.uf2, rs-key-<tag>-pqc.uf2, rs-key-<tag>-sbom.cdx.json, rs-key-<tag>-strict-config.uf2, rs-key-<tag>-strict-up-pqc.uf2, rs-key-<tag>-strict-up.uf2, rs-key-<tag>-strong-pin-pqc.uf2, rs-key-<tag>-strong-pin.uf2, rs-key-<tag>.intoto.jsonl.

What this table does not read

  • a step’s with: and env: blocks. They are inside the digest above and are printed nowhere, so a statement in the table must not rest on one — subject-path, fetch-depth and COSIGN_YES are settings this page describes only in the round.
  • the workflow’s own header comments. They are prose about the procedure, not the procedure.
  • what a pinned action DOES. The uses: steps run code out of another repository; the table above records which commit of it a release runs, and nothing whatsoever about what that commit does.
  • what a command MEANS. The subject column is decided by a command’s shape; a step that is present, spelled right and wrong about the world is outside every rule here.

Open obligations

  • no entry on this page is bound to a release artifact. Binding one needs a v* tag and a release-build.yml run: the tag, the commit, the runner, the per-.uf2 sha256 and the Rekor entry all come into existence there and in no checkout. Cost: one tagged release, by the maintainer, plus a manifest field per bound value and a gate rule holding it against the attestation API. Until then this page is a recipe: what a release runs, not what one ran.
  • source->binary is established by nothing here. PLAT-TOOLCHAIN-001 in assurance/platform.toml owns it and is still pending. A reproducible build is a statement about determinism; a miscompilation reproduces exactly as well as a correct compilation does.

Verifying a download

1. Reproducible build

Rebuild from the tagged source and compare. This is the strongest check, because it needs no trust in the publisher at all:

git checkout <tag>
nix build .#firmware            # or .#firmware-pqc, .#firmware-fips, …
sha256sum result/firmware.uf2   # compare against SHA256SUMS

CI already enforces this: the release job rebuilds all fourteen flavors with nix build --rebuild and fails on any bit-level difference before publishing.

2. Checksum signature (keyless cosign)

cosign verify-blob \
  --bundle SHA256SUMS.sigstore.json \
  --certificate-identity-regexp '^https://github.com/TheMaxMur/RS-Key/\.github/workflows/release-build\.yml@.*$' \
  --certificate-oidc-issuer https://token.actions.githubusercontent.com \
  SHA256SUMS
sha256sum -c SHA256SUMS          # then check the artifacts against it

The certificate identity is release-build.yml, not release.yml: cosign runs inside the reusable builder. Sigstore stamps the cert with the reusable workflow’s identity (job_workflow_ref).

3. Build provenance (GitHub attestation)

gh attestation verify rs-key-<tag>-default.uf2 \
  --repo TheMaxMur/RS-Key \
  --signer-workflow TheMaxMur/RS-Key/.github/workflows/release-build.yml

This confirms the .uf2 was built by the release-build.yml reusable workflow in this repo. The attestation records the workflow, commit and runner, so a hand-built upload won’t verify. Pinning --signer-workflow to the reusable builder is the SLSA Build L3 check: it proves a specific, trusted workflow produced the artifact, not merely that something in the repo did. (Dropping --signer-workflow still verifies an attestation exists for this repo, a weaker Build-L2-style check.) The provenance is a GitHub attestation (Sigstore-signed, logged in Rekor) kept in the attestation API rather than as a release asset, so it stays available even though the published release is immutable.

Dependency review — cargo-vet

cargo-deny already blocks bad licenses and known advisories. cargo-vet answers a different question (has anyone actually reviewed this crate’s code?) by requiring every dependency to be covered by a recorded audit.

The audit set lives in supply-chain/: imported audits from Mozilla, Google, ISRG and Zcash, plus our own exemptions for everything they don’t cover. The gate runs in check.sh:

nix develop -c cargo vet --locked

Honest scope. RS-Key is an embedded tree: embassy, the RP2350 HAL, defmt and many RustCrypto crates are not in the big organizations’ audit sets, so they are recorded as exemptions (grandfathered in, not yet line-reviewed). The point isn’t that every line is audited. It’s that a new, unreviewed crate cannot enter the tree silently. It fails cargo vet until it’s audited, imported, or explicitly exempted. To see the current state and shrink the exemption list:

nix develop -c cargo vet              # what's audited vs exempted
nix develop -c cargo vet suggest      # diffs to review next

The host tools/tui workspace is separate and not yet under cargo-vet. It is covered by Dependabot and cargo-deny.

Transparency-log monitoring

The Rekor log that backs every signature above is tamper-evident, not tamper-proof: it records misuse, but only if someone looks. A scheduled workflow, rekor-monitor.yml, does the looking. Every hour it runs sigstore/rekor-monitor and files a GitHub issue listing any Rekor entry signed as an RS-Key GitHub-Actions identity (any …/RS-Key/.github/workflows/* subject under the GitHub OIDC issuer).

This is the detection half that complements the verification above: the attestations prove a legitimate release is genuine, while the monitor surfaces an illegitimate signature, one made with our identity by something we didn’t run (a compromised OIDC token, repo, or runner). Each real release adds a couple of expected entries from release-build.yml, so a known issue or two per release is normal. The alarm is an entry you don’t recognise.

What’s deliberately not here

  • No cargo-vet of a fully line-reviewed tree. See the honest scope above.
  • No image encryption / signed-for-boot release artifacts. The published .uf2s are unsigned for secure boot by design. You seal them with your own key (production.md). The signatures on this page attest the build, not the boot.

See also: releases.md for the release index, and COMPLIANCE.md for the licensing posture.