22 KiB
Binary Handling Strategy for n-OS-tr
Status: Design. Defines how compiled binaries (
nsigner,nostr-id-tui,c-relay,ginxsom,fips,nak,nostr_core_lib) get from upstream source into the n-OS-tr ISO. Establishes a single canonical pattern across all components, replacing the current ad-hoc mix of committed binaries, copy-pasted source trees, and an undocumented/live-artifacts/staging directory.Companion to
plans/iso_architecture.md(high-level ISO architecture) andplans/n_signer_integration.md(the integration this strategy unblocks).
1. Why this matters
n-OS-tr is a security distro for cryptographic identity. The trust story has to be: "the binary on this ISO was built deterministically from auditable source code pinned to a specific git commit." Anything weaker than that defeats the threat model in plans/threat_model.md.
The current state of includes/ and the build hook iso/config/hooks/live/0010-fetch-artifacts.hook.chroot is a mix of three patterns with no documented rule:
| Component | Today's pattern | Trust story |
|---|---|---|
includes/c_relay_static_x86_64 |
Pre-built binary committed to git | "Trust whoever built it." Weak. |
includes/nak-v0.19.7-linux-amd64 |
Pre-built binary committed to git | Same. |
iso/config/includes.chroot/usr/local/bin/ginxsom/ginxsom-fcgi |
Pre-built binary committed to git | Same. |
includes/n_signer/ |
Full source tree copied in (no submodule) | Reproducible, but updates are manual file copies. |
includes/c-relay/ |
Full source tree copied in | Same. |
includes/ginxsom/ |
Full source tree copied in | Same. |
includes/fips/ |
Partial source (just packaging/) |
Incomplete. |
includes/nostr_core_lib/ |
Full source tree copied in | Reproducible, but tracked manually. |
nostr-id-tui |
Full source in stack/nostr-id-tui/ |
Reproducible. |
nostr_terminal (nt) |
Not yet in repo | Needs adding |
otp |
Not yet in repo | Needs adding |
didactyl |
Not yet in repo | Needs adding |
We also have a /live-artifacts/ staging convention where the build hook reads pre-built binaries from a chroot-internal directory populated by iso/config/includes.chroot_before_packages/live-artifacts/ (gitignored, see .gitignore:26). That convention is undocumented and the only way to get artifacts in there today is to drop them in manually before running lb build.
This is a mess. We standardize.
2. Decisions
2.1 Tier rules (in priority order)
Every binary that ends up in the ISO must follow one of these three tiers. No exceptions for "we'll fix it later."
Tier 1: Custom components — git submodule + build from source
For every component we author or substantially customize (n_signer, c-relay, ginxsom, nostr_core_lib, nostr-id-tui, fips):
- Vendored as a git submodule under
includes/pinned to a specific commit hash. - Built from source during
lb buildvia the component's ownbuild_static.sh(Alpine + musl, Docker-driven). - Output binary is staged into the chroot at a predictable path.
- No pre-built binaries committed to this repo. Ever.
This is the dominant tier — it covers everything we control.
Tier 2: Standard Debian software — apt packages
For software that's already in Debian's main repos (nginx, socat, openssl, bash, xxd, coreutils, etc.):
- Listed in
iso/config/package-lists/*.list.chroot. - live-build installs them via
apt-get installfrom Debian's signed mirrors. - We never repackage Debian software; we consume it.
No change from today. Documented here for completeness.
Tier 3: Closed-source or unbuildable third-party — committed binary with SHA256 + provenance README
For things we genuinely cannot build from source (closed-source firmware blobs; third-party tools where building is impractical):
- The binary is committed to
includes/third-party/<name>/under Git LFS if larger than 1 MB. - A
PROVENANCE.mdnext to the binary states: upstream URL, version, SHA256, when fetched, by whom, what the build process upstream is. - The SHA256 is also recorded in
iso/SHA256SUMSand verified by the build hook before staging.
Tier 3 should be rare. As of today, only nak is a candidate, and even that we should audit (see §6).
2.2 The single artifact-staging convention
All binaries — Tier 1 build outputs, Tier 3 verified blobs — flow through one staging directory inside the chroot during lb build:
chroot:/live-artifacts/
├── nsigner # built from includes/n_signer → /usr/local/sbin/nsigner
├── nostr-id-tui # built from includes/nostr-id-tui → /usr/local/sbin/nostr-id-tui
├── c-relay # built from includes/c-relay → /opt/c-relay/c-relay
├── ginxsom-fcgi # built from includes/ginxsom → /usr/local/bin/ginxsom/ginxsom-fcgi
├── fips # built from includes/fips → /usr/local/bin/fips (or .deb via dpkg)
├── didactyl # built from includes/didactyl → /usr/local/bin/didactyl
├── nt # built from includes/nostr_terminal → /usr/local/bin/nt
├── otp # built from includes/otp → /usr/local/bin/otp
├── nak # tier-3 verified (or future Tier 1) → /usr/local/bin/nak
└── SHA256SUMS # generated at build time, all built+staged binaries hashed
Daemons (nsigner, c-relay, ginxsom, fips, didactyl) → /usr/local/sbin/ or /opt/ per service convention. User-facing CLIs (nostr-id-tui, nt, otp, nak) → /usr/local/bin/ so they're on PATH.
The current 0010-fetch-artifacts.hook.chroot already reads from /live-artifacts/ — we keep that consumer-side convention. What changes is how /live-artifacts/ gets populated: from a lb build hook that runs before the chroot runs, instead of the user dropping files there manually.
2.3 Pre-chroot build hook
Before lb build enters the chroot, a host-side script runs each component's build_static.sh, collects the output binaries, and stages them into iso/config/includes.chroot_before_packages/live-artifacts/. Live-build then copies that into /live-artifacts/ inside the chroot, and the existing chroot-side 0010-fetch-artifacts.hook.chroot does its job unchanged.
The pre-chroot script lives at scripts/build-binaries.sh (new). Outline:
#!/bin/bash
# Build all Tier-1 binaries from source, stage them for live-build.
set -euo pipefail
REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
STAGE="$REPO_ROOT/iso/config/includes.chroot_before_packages/live-artifacts"
mkdir -p "$STAGE"
# nostr_core_lib (C library; built once and consumed by downstream C builds at link time)
( cd "$REPO_ROOT/includes/nostr_core_lib" && ./build.sh x64 )
# (no binary to stage; consumed at link time by other builds)
# n_signer
( cd "$REPO_ROOT/includes/n_signer" && ./build_static.sh )
install -m 0755 "$REPO_ROOT/includes/n_signer/build/nsigner_static_x86_64" "$STAGE/nsigner"
# nostr-id-tui (n-OS-tr-native; lives at includes/nostr-id-tui/ after migration)
( cd "$REPO_ROOT/includes/nostr-id-tui" && ./build_static.sh )
install -m 0755 "$REPO_ROOT/includes/nostr-id-tui/build/nostr-id-tui_static_x86_64" "$STAGE/nostr-id-tui"
# c-relay
( cd "$REPO_ROOT/includes/c-relay" && ./build_static.sh )
install -m 0755 "$REPO_ROOT/includes/c-relay/build/c_relay_static_x86_64" "$STAGE/c-relay"
# ginxsom
( cd "$REPO_ROOT/includes/ginxsom" && ./build_static.sh )
install -m 0755 "$REPO_ROOT/includes/ginxsom/build/ginxsom-fcgi_static_x86_64" "$STAGE/ginxsom-fcgi"
# fips (Rust + cargo-deb produces a .deb)
( cd "$REPO_ROOT/includes/fips/packaging/debian" && ./build-deb.sh )
install -m 0644 "$REPO_ROOT/includes/fips/packaging/debian"/fips_*_amd64.deb "$STAGE/"
# didactyl (LLM-based agent daemon)
( cd "$REPO_ROOT/includes/didactyl" && ./build_static.sh )
install -m 0755 "$REPO_ROOT/includes/didactyl/build/didactyl_static_x86_64" "$STAGE/didactyl"
# nt (nostr_terminal — user CLI)
( cd "$REPO_ROOT/includes/nostr_terminal" && ./build_static.sh )
install -m 0755 "$REPO_ROOT/includes/nostr_terminal/build/nt_static_x86_64" "$STAGE/nt"
# otp (one-time pad CLI)
( cd "$REPO_ROOT/includes/otp" && make static )
install -m 0755 "$REPO_ROOT/includes/otp/build/otp-x86_64" "$STAGE/otp"
# Tier-3: verify + stage
sha256sum -c "$REPO_ROOT/iso/SHA256SUMS.tier3" # fails if any committed blob has wrong hash
install -m 0755 "$REPO_ROOT/includes/third-party/nak/nak" "$STAGE/nak"
# Generate manifest
( cd "$STAGE" && sha256sum * > SHA256SUMS )
echo "Staged binaries:"
ls -la "$STAGE"
The lb-wrapper.sh (or build.sh) at the repo root invokes scripts/build-binaries.sh before calling lb build.
3. Migration plan: turning includes/ into submodules
3.1 Per-component conversion
For each component currently checked in as a plain directory under includes/:
- Confirm there's an upstream git URL for the component.
git rm -r includes/<name>(this removes the files from this repo's tracking).git submodule add <upstream-url> includes/<name>cd includes/<name> && git checkout <pinned-tag-or-commit>cd ../.. && git commit -m "vendor <name> as submodule at <ref>"
Result: this repo records which commit of the upstream we ship, but not the contents. git clone --recurse-submodules produces an identical working tree to today.
3.2 Component-by-component decisions
All upstream URLs are now confirmed (per user, 2026-05-04). They all live under https://git.laantungir.net/laantungir/:
| Component | Upstream URL | Action |
|---|---|---|
n_signer |
https://git.laantungir.net/laantungir/n_signer |
Convert to submodule. Source already at includes/n_signer/ but is stale (pre-v0.0.16, no --preapprove). Refresh from upstream during conversion. |
c-relay |
https://git.laantungir.net/laantungir/c-relay |
Convert to submodule. |
ginxsom |
https://git.laantungir.net/laantungir/ginxsom |
Convert to submodule. |
fips |
https://git.laantungir.net/laantungir/fips |
Convert to submodule. Note: user does not control this repo (vendored from elsewhere); pin a known-good ref. |
nostr_core_lib |
https://git.laantungir.net/laantungir/nostr_core_lib |
Convert to submodule. |
nostr_terminal (nt) |
https://git.laantungir.net/laantungir/nostr_terminal |
Add new submodule at includes/nostr_terminal/. |
otp |
https://git.laantungir.net/laantungir/otp |
Add new submodule at includes/otp/. |
didactyl |
https://git.laantungir.net/laantungir/didactyl |
Add new submodule at includes/didactyl/. |
nostr-id-tui |
n-OS-tr-native (no upstream) | Move from stack/nostr-id-tui/ to includes/nostr-id-tui/ for consistency. Stays in this repo as plain source — no submodule. |
B1 closed.
3.3 Things to delete after migration
Once submodules are in place and scripts/build-binaries.sh works:
includes/c_relay_static_x86_64— pre-built binary, replaced by source-built outputiso/config/includes.chroot/usr/local/bin/ginxsom/ginxsom-fcgi— sameiso/config/includes.chroot/usr/local/bin/nak— moved to Tier 3 location with PROVENANCE.md (or built from Go source, see §6)includes/nak-v0.19.7-linux-amd64— sameincludes/ginxsom/build/— checked-in build output, regenerated from sourcestack/nostr-id-tui/build-host/— checked-in build output
3.4 Build host requirements (consequence of Tier 1)
Anyone building n-OS-tr from scratch needs:
- Linux host with Docker (for the Alpine+musl static builds)
- ~10 GB free disk for Docker layer cache
- Network access the first time (to pull
alpine:3.19, plus apk packages duringdocker build); offline thereafter live-build(the Debian package) on the hostgitwith submodule support (git clone --recurse-submodules)- For Rust components like fips: the
rustandcargo-debpackages, OR a Rust-capable Docker recipe (preferred, for consistency with the Tier 1 pattern)
This is documented in docs/RUNNING.md and docs/debian_live_notes.md.
4. Reproducibility ladder
We aim for this property: given a commit hash of n-OS-tr, two different people on two different days produce byte-identical ISOs. Achieving the full level requires more than just "build from source"; it requires deterministic builds. The ladder:
| Level | Guarantee | Cost |
|---|---|---|
| L0: source-pinned | Same source produces same binary on the same machine | Free if you follow Tier 1 |
| L1: same-platform reproducible | Same source produces same binary on any Alpine 3.19 amd64 host | Requires SOURCE_DATE_EPOCH, no __DATE__/__TIME__ macros |
| L2: cross-platform reproducible | Same source produces same binary on amd64 and arm64 hosts | Hard. Compiler must produce same output regardless of build host. |
| L3: fully reproducible ISO | Same source produces byte-identical ISO | Requires L2 + reproducible squashfs + reproducible bootloader timestamps |
Today we don't claim any level explicitly. Target for v1: L1. That's what most security distros (Tails, Qubes) achieve. L2 and L3 are nice-to-have and can come later.
The Tier 1 + build_static.sh foundation makes L1 the path of least resistance — SOURCE_DATE_EPOCH is set to the git commit timestamp at build time, and the Alpine/musl recipe doesn't pull in any non-deterministic dependencies.
5. Update workflow
5.1 To bump n_signer to a new version
cd includes/n_signer
git fetch
git checkout v0.0.11 # or whatever tag
cd ../..
./scripts/build-binaries.sh # verify it still builds
git add includes/n_signer
git commit -m "bump n_signer to v0.0.11"
The diff in this repo's git log shows exactly: "submodule pointer changed from to ". Anyone reviewing the commit can cd includes/n_signer && git log <old-sha>..<new-sha> to see what actually changed.
5.2 To bump a Tier-3 binary (e.g., nak)
# Download new version, verify upstream signature/checksum
curl -O https://github.com/fiatjaf/nak/releases/download/v0.20.0/nak-v0.20.0-linux-amd64
sha256sum nak-v0.20.0-linux-amd64
# Compare against upstream-published checksum
mv nak-v0.20.0-linux-amd64 includes/third-party/nak/nak
sha256sum includes/third-party/nak/nak >> iso/SHA256SUMS.tier3 # remove old line
# Update PROVENANCE.md with new version, URL, hash, date
git add includes/third-party/nak iso/SHA256SUMS.tier3
git commit -m "bump nak to v0.20.0"
5.3 To add a new component
- Decide tier (1, 2, or 3).
- Tier 1: add submodule, write/verify
build_static.sh, add stage step toscripts/build-binaries.sh, add chroot install step to0010-fetch-artifacts.hook.chroot. - Tier 2: add to a
*.list.chroot. - Tier 3: put binary at
includes/third-party/<name>/, writePROVENANCE.md, add SHA256 toiso/SHA256SUMS.tier3, add toscripts/build-binaries.sh.
6. The nak problem
includes/nak-v0.19.7-linux-amd64 is a Go binary built upstream by fiatjaf/nak. Two paths:
Path A: keep as Tier 3. nak is a small CLI, fiatjaf is a known upstream, the binary is statically linked Go. Document with PROVENANCE.md, pin SHA256, done. Acceptable if we want to ship soon.
Path B: move to Tier 1 (Go build). Add a Go-capable Docker builder under stack/build-common/ (or a Dockerfile.alpine-go next to the existing musl one), make nak a submodule, build from source. Better for the trust story; more work.
Recommendation: Path A for v1, Path B as a follow-up before Phase 2 launches. The reasoning is that nak is currently used in n-os-tr-firstboot for one-shot operations and has been removed from the slimmed firstboot script (see plans/n_signer_integration.md). Whether it's even still needed should be checked — if not, we can just remove it.
Open question (B2): Is
nakactually used anywhere after then_signerintegration removes it fromn-os-tr-firstboot? If no, delete it entirely.
7. Threat-model implications
This strategy strengthens plans/threat_model.md:
- F26 (deterministic build). Tier 1 + L1 reproducibility means anyone can re-build the ISO from a git checkout and get the same binaries. Pre-built binaries can never satisfy this; they're a permanent "trust me" hole.
- F27 (signed releases). Once we have Tier 1 across the board, we can sign the ISO with a key that vouches for the entire source tree. Without source pinning, a release signature only vouches for the publisher, not the contents.
- Supply-chain attacks. A compromised build host that produces malicious binaries committed to git is undetectable. A compromised build host that produces malicious binaries from source is detectable: rebuild on a clean host, compare. L1 reproducibility is the foundation.
8. Open questions
- B1.
Confirm canonical upstream URLs.Closed (2026-05-04): all components live underhttps://git.laantungir.net/laantungir/with names matching the directory names.nostr-id-tuiis n-OS-tr-native (no upstream).nakupstream is athttps://github.com/nostrapps/nostr-army-knife. - B2. Is
nakstill needed after then_signerintegration? Tentative: yes — useful CLI for users to inspect events from the shell on tty3+. Keep as Tier 3 with PROVENANCE.md until we know otherwise. - B3. Should
nostr-id-tuimove fromstack/toincludes/for consistency? Yes — confirmed. Keepstack/build-common/for shared build infra; everything else underincludes/. - B4. Do we want
iso/SHA256SUMSto track Tier-1 build outputs as well, for a single manifest? Trade-off: convenient end-state, but the file would change every commit since binaries are non-deterministic until we hit L1. Defer until L1 is in place. - B5. Should
scripts/build-binaries.shbe parallelized? Currently sequential. With nine components, sequential builds take ~10–20 minutes. Parallelizing across components could halve this. Optional optimization. - B6. Is
cargo-deb(used for fips) compatible with Alpine/musl? Or do we need a separate Debian-based builder for fips's.deb? Likely: separate Debian builder. Rust+musl works fine, butcargo-debexpects Debian packaging tooling (dpkg-dev, debhelper) which isn't trivially in Alpine. Cleanest answer: aDockerfile.debian-rustnext to the existing musl Dockerfiles, used only by fips. Investigate when we get to fips conversion. - B7. What index should didactyl get in the role schedule? Closed: index 5 (
plans/n_signer_integration.md§2). - B8. What should be the user CLI signing UX (always-prompt vs. preapproved for the user's uid)? Closed: preapproved for
caller=uid:1000,nostr_index=0(plans/n_signer_integration.md§7.2). - B9.
ntandotpneed code changes upstream to usen_signerinstead of their own mnemonic flow. Tracked as T11 inplans/upstream_nsigner_tickets.md.
9. Mermaid: artifact flow
flowchart TB
subgraph repo[n_os_tr repo]
SUB[includes/* git submodules<br/>pinned commits]
STA[stack/build-common/<br/>Dockerfile.alpine-musl + build_static.sh]
T3[includes/third-party/nak<br/>+ PROVENANCE.md + SHA256]
end
subgraph host[Build host]
BUILD[scripts/build-binaries.sh]
DCK[Docker daemon<br/>Alpine 3.19 musl]
STAGE[iso/config/includes.chroot_before_packages/<br/>live-artifacts/]
end
subgraph lb[live-build]
LB[lb build]
CHROOT[chroot /live-artifacts/]
HOOK[0010-fetch-artifacts.hook.chroot]
ISO[n-os-tr.iso]
end
SUB --> BUILD
STA --> BUILD
T3 --> BUILD
BUILD --> DCK
DCK --> STAGE
STAGE --> LB
LB --> CHROOT
CHROOT --> HOOK
HOOK --> ISO
10. Summary
| Question | Answer |
|---|---|
| How do binaries get into the ISO? | Built from source at ISO build time via Docker+Alpine+musl |
| Where is source kept? | includes/<name>/ as git submodules, pinned to commits |
| What about pre-built binaries in git? | Forbidden for Tier 1. Allowed only for Tier 3 (closed-source/unbuildable third-party) with PROVENANCE.md and SHA256. |
| What about apt packages? | Tier 2: standard Debian packages via *.list.chroot. No change. |
| Can the build run offline? | Once you've cloned --recurse-submodules and pulled alpine:3.19 once, yes. |
| Does the ISO download anything at runtime? | No. Never. The whole point of an ISO is offline-bootable. |
| What's the trust story? | Git commit hash → submodule commit hashes → identical source → identical binaries (L1 reproducibility) → identical ISO. |