<ahref="#randomness">Where Randomness Comes From</a>
<ahref="#when">When the Seed Is Born</a>
<ahref="#guardrails">The Five Guardrails</a>
<ahref="#hardening">Recent Hardening</a>
<divclass="nav-section">Storage</div>
<ahref="#storage">What's Kept on Disk</a>
<ahref="#envelope">The Encrypted Envelope</a>
<divclass="nav-section">Derivation</div>
<ahref="#tree">The Derivation Tree</a>
<ahref="#lightning">The Lightning Special Case</a>
<ahref="#second-order">Second-Order Keys</a>
<ahref="#independent">What Is NOT Derived</a>
<divclass="nav-section">Failure & Recovery</div>
<ahref="#failures">Failure Modes & Fallbacks</a>
<ahref="#restore">The Restore Flow</a>
<ahref="#comes-back">What Comes Back (and What Doesn't)</a>
<ahref="#seedqr">SeedQR</a>
<divclass="nav-section">Assurance</div>
<ahref="#limitations">Honest Edges</a>
<ahref="#verify">Verify It Yourself</a>
</nav>
<main>
<divclass="hero">
<h1>Your Node's Seed & Entropy</h1>
<pclass="tagline">How 32 random bytes become every key your node owns — where that randomness comes from, what protects it, and exactly what your 24 words can (and cannot) bring back.</p>
<divclass="meta">
<span>256-bit entropy</span>
<span>BIP-39 · 24 words</span>
<span>HKDF-SHA256 derivation</span>
<span>Kernel CSPRNG only</span>
<span>KEY-05 hardened</span>
</div>
</div>
<h2id="intro">Introduction</h2>
<pclass="subtitle">One master secret, many keys — by design.</p>
<p>
Almost everything cryptographic on this node — its identity, its Nostr keys, its mesh
transport keys, its Lightning wallet — grows from a <strong>single master seed</strong>:
32 bytes (256 bits) of randomness drawn once, shown to you once as a 24-word recovery
phrase, and never stored in raw form anywhere.
</p>
<divclass="callout callout-learn">
<strong>The tree and the acorn</strong>
Think of the seed as an acorn. Every branch of the tree — your identity, your wallet,
your mesh radio's name — grows from it in a fixed, repeatable pattern. Plant the same
acorn on new hardware (by typing your 24 words) and the <em>same tree</em> grows back,
branch for branch. That's why those words are the single most valuable thing your node
ever shows you — and why anyone who copies them owns your tree.
</div>
<p>
This page explains, in both plain and technical language: where those 32 bytes come from,
the guardrails that make sure they're genuinely random, how they're protected on disk,
everything that is derived from them, everything that deliberately <em>isn't</em>, and
what happens in every failure and recovery scenario.
</p>
<h2id="big-picture">The Big Picture</h2>
<pclass="subtitle">From kernel randomness to every key on the node.</p>
<divclass="diagram">
<spanclass="blue">Linux kernel CSPRNG</span> (hardware noise: interrupts, timing jitter, CPU RNG)
│
│ getrandom(2) — via an explicitly named <spanclass="highlight">OsRng</span>, nothing else allowed
▼
<spanclass="highlight">32 bytes of raw entropy</span> ──▶ degenerate-draw check ──▶ <spanclass="red">refuse & wipe if suspicious</span>
│
│ BIP-39 encoding
▼
<spanclass="green">24-word recovery phrase</span> ←── the only form you ever see or back up
│
│ PBKDF2-HMAC-SHA512 × 2048 (empty passphrase)
▼
<spanclass="highlight">64-byte master seed</span> ←── lives only in RAM, never written to disk
If it's on the diagram above, your 24 words can rebuild it from scratch, on any hardware,
forever. If it's not on the diagram (session tokens, app passwords, WireGuard keys,
the Lightning channel state…), it's independent randomness — protected by other backups,
not by the words.
</div>
<h2id="randomness">Where Randomness Comes From</h2>
<pclass="subtitle">One named source. No mixing. No silent defaults.</p>
<divclass="callout callout-learn">
<strong>Dice you can audit</strong>
Computers can't invent randomness — they collect it. The Linux kernel constantly harvests
unpredictable physical noise (the exact nanosecond your network card interrupts, timing
jitter between CPU cores, the CPU's built-in hardware random generator) and distils it
into a cryptographic random pool. Archipelago rolls its dice by asking that pool
directly — and <em>only</em> that pool. There is deliberately no "mixing" of other
sources, because a single, named, well-studied source is auditable; a blend of
sources is a place for bugs to hide.
</div>
<h3>Technically</h3>
<p>
The master seed is generated by <code>MasterSeed::generate()</code> in
<code>core/archipelago/src/seed.rs</code>. It fills a 32-byte buffer using
<code>rand::rngs::OsRng</code> — a thin wrapper around the
<code>getrandom(2)</code> system call, which reads the kernel's CSPRNG
(the same source as <code>/dev/urandom</code>, but immune to file-descriptor
exhaustion and chroot tricks).
</p>
<ul>
<li><strong>Exactly 32 bytes / 256 bits</strong> — the maximum BIP-39 strength, encoding to 24 words.</li>
<li><strong>The RNG is named at the call site.</strong> No function anywhere in the codebase generates key material with a "default" or implicit RNG anymore (see <ahref="#hardening">Recent Hardening</a> for why this is stated so emphatically).</li>
<li><strong>The RNG type is compiler-enforced.</strong> Key-generation functions only accept RNGs on a <em>sealed allowlist</em> (<code>KeyGenRng</code> in <code>core/archipelago/src/entropy.rs</code>). The list has exactly one production member: <code>OsRng</code>. No other module — not even a future refactor — can add a weaker RNG without editing the allowlist file itself.</li>
<li><strong>The buffer is zeroized</strong> (securely wiped from memory) on every path, success or failure.</li>
</ul>
<pre><code>// core/archipelago/src/seed.rs — the actual draw
let mnemonic = bip39::Mnemonic::from_entropy(&entropy)?; // → 24 words
entropy.zeroize(); // wipe the raw bytes</code></pre>
<p>
The 24 words are then stretched into the 64-byte master seed using standard BIP-39:
<code>PBKDF2-HMAC-SHA512</code>, 2048 rounds, empty passphrase. (There is no "25th word"
passphrase — your login password protects the stored backup instead, see
<ahref="#envelope">The Encrypted Envelope</a>.) That 64-byte seed exists
<strong>only in memory</strong>, is re-computed from the words whenever needed, and is
never written to disk in any form.
</p>
<h2id="when">When the Seed Is Born</h2>
<pclass="subtitle">At onboarding — not at first boot.</p>
<divclass="steps">
<divclass="step">
<h4>First boot: a placeholder key</h4>
<p>A freshly-flashed node boots with a random <em>temporary</em> identity key so services can start. This key is NOT seed-derived and is about to be thrown away.</p>
</div>
<divclass="step">
<h4>Onboarding: the real draw</h4>
<p>When you reach the "Recovery phrase" step of setup, the <code>seed.generate</code> RPC performs the guarded 32-byte draw described above and shows you the 24 words — the only time they're ever displayed unprompted.</p>
</div>
<divclass="step">
<h4>Derivation: keys are materialised</h4>
<p>The node identity key, DID, Nostr key, FIPS mesh key, and your first personal identity are all derived from the seed and written to <code>/var/lib/archipelago/identity/</code> (each file mode 0600). The placeholder key from step 1 is overwritten.</p>
</div>
<divclass="step">
<h4>Password setup: the backup is sealed</h4>
<p>When you set your login password, the 24 words are encrypted under it (Argon2 + ChaCha20-Poly1305) and stored as <code>master_seed.enc</code> — so you can re-reveal them later from Settings → Backup.</p>
</div>
</div>
<divclass="callout callout-info">
<strong>Refresh-proof by design</strong>
If the onboarding page is retried within 10 minutes (browser refresh, flaky connection),
the node returns the <em>same</em> words instead of minting a second seed. Generation is
serialised behind a lock — there is no window where two competing seeds can exist.
</div>
<h2id="guardrails">The Five Guardrails</h2>
<pclass="subtitle">Defence in depth around a single random draw.</p>
<p>Key draws only compile against RNG types on a closed, private allowlist. Production allowlist: <code>OsRng</code>. Full stop. A refactor that swaps in a weak or deterministic RNG becomes a <em>compile error</em>, not a silent disaster.</p>
<p>Every draw is inspected for three tell-tale broken-RNG shapes: all zeros, all bytes identical, or a counting pattern. A match is <strong>refused and wiped — never retried</strong>, because retrying would mask a broken RNG instead of exposing it.</p>
<p>Before generating the seed, the node probes whether the kernel's random pool is fully initialised, and appends the verdict to a tamper-evident, append-only log at <code>security/csprng-readiness.jsonl</code> (0600). You can audit, forever, the entropy conditions your seed was born under.</p>
<p>The CI lint config bans <code>rand::random()</code> and <code>rand::thread_rng()</code> outright across the workspace — the two "convenient" RNG entry points that caused real-world wallet disasters elsewhere. Using either fails the build.</p>
<p>Raw entropy, mnemonics, and derived secrets are wiped from memory on every code path — including error paths — so key material doesn't linger in freed RAM or end up in crash dumps.</p>
</div>
</div>
<divclass="callout callout-learn">
<strong>Why so paranoid about one function?</strong>
In 2026 a well-known hardware wallet shipped a bug where a code refactor quietly switched
seed generation to a <em>predictable</em> random source — with no error, no warning, and
seeds that looked perfectly normal. Predictable randomness is invisible: the words look
random, the wallet works, and months later someone who can predict the generator drains
it. Archipelago's answer is to make that entire class of bug <em>impossible to compile</em>,
and to log the health of the random pool at the exact moment your seed was created.
</div>
<h3>What the degenerate check does — and doesn't — do</h3>
<p>
The check (<code>is_degenerate</code> in <code>entropy.rs</code>) is deliberately
<em>closed-form</em>: it recognises exactly three catastrophic failure shapes
(all-zero, all-identical, ±1 counter). It is <strong>not</strong> a statistical entropy
estimator — those can't actually distinguish good randomness from a cleverly broken RNG,
and they introduce false positives. The real security load is carried by guardrails 1,
3 and 4; the degenerate check is a tripwire for total RNG failure (e.g. a zeroed buffer
that was never filled).
</p>
<h2id="hardening">Recent Hardening (the KEY-05 work)</h2>
<pclass="subtitle">This system was audited and rebuilt in early August 2026.</p>
<p>
Triggered by the COLDCARD-class of entropy defects, a full entropy & seed-generation
audit (<code>docs/security/ENTROPY-SEED-AUDIT-2026-07-31.md</code>) reviewed every random
draw in the codebase. The headline finding: the library used for mnemonic generation was
silently choosing its own RNG via a transitive default. It happened to be a secure one —
but nothing <em>guaranteed</em> that, and a dependency update could have changed it
without any diff in Archipelago's own code.
</p>
<table>
<tr><th>Date</th><th>Change</th></tr>
<tr><td>Jul 30</td><td>Kernel CSPRNG readiness probe added; RNG non-determinism regression test (64 consecutive mnemonics must all be unique).</td></tr>
<tr><td>Jul 31</td><td>Full entropy audit published (findings F-01…F-13).</td></tr>
<tr><td>Aug 1</td><td><strong>The pivotal fix:</strong> master-seed RNG made explicit — <code>OsRng</code> named at the call site, injected through a testable seam, pinned by a known-answer test.</td></tr>
<tr><td>Aug 2</td><td>Audit widened: 43 defaulted-RNG call sites across 15 files found and migrated to explicit <code>OsRng</code> — including AEAD nonces and ecash key material.</td></tr>
<tr><td>Aug 2</td><td>Onboarding RPCs gated: <code>seed.restore</code> and friends now refuse on an already-provisioned node (previously an unauthenticated restore call could hijack a live node — fixed before any release shipped it).</td></tr>
<tr><td>Aug 2</td><td>KEY-05 enforcement layer landed: sealed <code>KeyGenRng</code> allowlist, guarded draws, durable readiness ledger, clippy bans, supply-chain version pinning on the <code>rand</code> crate.</td></tr>
<tr><td>Aug 2</td><td>Legacy Bitcoin Core wallet-import path deleted — the master xprv is no longer handed to any external wallet process.</td></tr>
</table>
<h2id="storage">What's Kept on Disk</h2>
<pclass="subtitle">The words, encrypted — and the keys, materialised. Never the raw seed.</p>
<tr><td><code>identities/<uuid>.json</code></td><td>Personal identity records (keys + metadata)</td><td>0600; keys seed-derived, <em>metadata is not</em></td></tr>
<tr><td><code>identity/lnd_aezeed.enc</code></td><td>Lightning wallet's own seed (see <ahref="#lightning">below</a>)</td><td>Encrypted under the LND wallet password</td></tr>
<tr><td><code>security/csprng-readiness.jsonl</code></td><td>Append-only entropy audit trail</td><td>0600; deliberately outside <code>identity/</code> so restores never touch it</td></tr>
</table>
<divclass="callout callout-success">
<strong>The raw seed never touches disk</strong>
What's stored is the <em>encrypted words</em> and the <em>derived keys</em>. The 64-byte
master seed itself is recomputed in RAM from the words when needed (e.g. during restore
or identity creation) and wiped afterwards.
</div>
<h2id="envelope">The Encrypted Envelope</h2>
<pclass="subtitle">How <code>master_seed.enc</code> is built.</p>
<tr><td><strong>Node identity (Ed25519)</strong> — signs everything, forms your DID</td><td>HKDF-SHA256</td><td><code>archipelago/node/ed25519/v1</code></td><td><code>identity/node_key</code></td></tr>
<tr><td><strong>Node Nostr key (secp256k1)</strong> — the node's npub</td><td>HKDF-SHA256</td><td><code>archipelago/nostr-node/secp256k1/v1</code></td><td><code>identity/nostr_secret</code></td></tr>
<tr><td><strong>FIPS mesh transport key</strong> — federation/mesh overlay identity</td><td>HKDF-SHA256</td><td><code>archipelago/fips/secp256k1/v1</code></td><td><code>identity/fips_key</code></td></tr>
<tr><td><strong>Personal Nostr key #i</strong> — standard NIP-06, importable into other Nostr apps</td><td>BIP-32</td><td><code>m/44'/1237'/0'/0/{i}</code></td><td><code>identities/<uuid>.json</code></td></tr>
<tr><td><strong>Lightning (LND) wallet entropy</strong> — 16 bytes</td><td>HKDF-SHA256</td><td><code>archipelago/lnd/entropy/v1</code></td><td>fed into LND at wallet init (see below)</td></tr>
<tr><td><strong>Bitcoin BIP-84 account xprv</strong> — native-segwit</td><td>BIP-32</td><td><code>m/84'/0'/0'</code></td><td><em>nowhere</em> — dormant, reserved for a future on-node cold vault</td></tr>
<tr><td><strong>Release-root signing key</strong> — fleet update signing</td><td>HKDF-SHA256</td><td><code>archipelago/release/root/ed25519/v1</code></td><td><em>never on a node</em> — derived offline by the publisher from a separate release mnemonic; nodes only pin the public key</td></tr>
</table>
<p>
All HKDF derivations are HKDF-SHA256 with a distinct, versioned info-label (the
<code>/v1</code> suffix means a future algorithm migration can introduce
<code>/v2</code> labels without ambiguity). Personal Nostr keys intentionally use the
NIP-06 standard path instead of HKDF so that <em>the same 24 words typed into any
NIP-06-compliant Nostr client reproduce the same npub</em> — your social identity is
portable beyond Archipelago.
</p>
<h2id="lightning">The Lightning Special Case</h2>
<pclass="subtitle">The one branch of the tree with a one-way gate in it.</p>
<divclass="diagram">
master seed ──HKDF──▶ 16 bytes of entropy ──▶ <spanclass="yellow">LND generates its own "aezeed"</span> ──▶ Lightning wallet
LND (the Lightning node) uses its own seed format called <em>aezeed</em>, which is not
BIP-39. Archipelago derives deterministic entropy from your master seed and hands it to
LND at wallet-creation time — but LND then wraps it with its own internal random salt,
so the resulting aezeed <strong>cannot be re-derived from your 24 words afterwards</strong>.
The node therefore captures the aezeed exactly once, at init, and stores it encrypted
alongside your other identity files. The Lightning seed backup screen shows it to you
with the same tap-to-reveal flow as the main phrase.
</p>
<divclass="callout callout-warn">
<strong>Back up the Lightning seed separately</strong>
Your 24 words alone restore your node identity and on-chain derivations — but
<em>not</em> an already-initialised Lightning wallet, and never the off-chain channel
balances (those additionally need channel backups, which Lightning requires by design).
Treat the LND aezeed shown in the Lightning app's backup screen as a second phrase
worth writing down. Note it restores into LND-based wallets (Zeus, Blixt, another
Archipelago node) — hardware wallets can't import it.
</div>
<h2id="second-order">Second-Order Keys</h2>
<pclass="subtitle">Derived from the node key — so still fully recoverable from the words.</p>
<p>
Several subsystems derive their keys from the <em>node identity key</em> rather than the
master seed directly. Since the node key is itself seed-derived, these all regrow from
your 24 words too — the chain is <code>words → master seed → node key → subsystem key</code>.
</p>
<table>
<tr><th>Subsystem</th><th>Derivation from <code>node_key</code></th></tr>
<tr><td><strong>Reticulum / RNS mesh identity</strong> (LoRa long-range mesh)</td><td>HKDF-SHA256, salt <code>archipelago-reticulum-identity-v1</code>, two labels for the X25519 + Ed25519 halves — yields a stable LXMF address that survives reinstalls</td></tr>
<tr><td><strong>Message-store encryption</strong> (chats at rest)</td><td><code>SHA-256("archipelago-message-store-v1" ‖ node_key)</code></td></tr>
<p>Every manifest-declared <code>generated_secret</code> (app database passwords, API keys), Bitcoin RPC credentials, the LND wallet <em>password</em> (distinct from its seed), Home Assistant tokens.</p>
</div>
<divclass="card-sm">
<h4>Host-level material</h4>
<p>WireGuard VPN keypairs (generated by <code>wg genkey</code>), SSH host keys and the TLS certificate (created by the installer image at first boot), the machine-id.</p>
</div>
<divclass="card-sm">
<h4>Opt-outs from derivability</h4>
<p>Extra identities created with "new random key" instead of seed derivation, and a node key after an explicit <code>rotate-key</code> — rotation <em>deliberately</em> breaks the link to the words, and says so.</p>
</div>
</div>
<divclass="callout callout-info">
<strong>Consequence for backups</strong>
A seed-only restore brings back everything in the derivation tree but none of the above.
App data, chat history and app secrets travel via the separate node backup/restore
feature; Lightning channels need channel backups; VPN peers re-pair. Your words are your
<em>identity's</em> lifeboat, not a full-system image.
</div>
<h2id="failures">Failure Modes & Fallbacks</h2>
<pclass="subtitle">What happens when something goes wrong — at every stage.</p>
<td>Kernel random pool not yet initialised (exotic first-boot case)</td>
<td><code>getrandom(2)</code><strong>blocks</strong> until the pool is seeded — an unseeded pool physically cannot produce a seed. The advisory probe logs a warning and records the verdict in the readiness ledger</td>
<td><spanclass="badge badge-yellow">Waits, then proceeds</span></td>
<td><code>master_seed.enc</code> missing (e.g. backup write failed during setup)</td>
<td>Node runs normally — every derived key is already materialised on disk. Only the <em>Reveal</em> feature and future re-derivation are unavailable; the UI says so explicitly</td>
<strong>The one true single point of failure is you</strong>
Every software failure above fails <em>safe</em>. The only unrecoverable scenario is
losing the 24 words <em>and</em> the node's disk together. Write the words down, store
them offline, and never type them into anything except a node you're restoring.
Anyone holding them can rebuild your entire identity tree — that's precisely
what makes them a perfect backup, and a perfect target.
</div>
<h2id="restore">The Restore Flow</h2>
<pclass="subtitle">Typing 24 words into a fresh node, step by step.</p>
<divclass="steps">
<divclass="step">
<h4>Gate check</h4>
<p>Restore only proceeds on an un-onboarded node. A provisioned node refuses — this gate is load-bearing and runs before anything else.</p>
</div>
<divclass="step">
<h4>Validation</h4>
<p>Exactly 24 words, checked against the BIP-39 wordlist and its built-in checksum — a typo is caught here, before anything is written.</p>
</div>
<divclass="step">
<h4>Identity regrowth</h4>
<p>Node Ed25519 key + DID, node Nostr key, and FIPS mesh key are re-derived and written — byte-identical to the originals, because the HKDF labels are fixed.</p>
</div>
<divclass="step">
<h4>Personal identity #0</h4>
<p>The identity index resets to 0 and your default "Personal" identity (Ed25519 + NIP-06 Nostr key) is recreated. If you had created more seed-derived identities, their keys re-derive on demand as the index walks forward — but their names, avatars and profiles were metadata, not key material, and come from a node backup instead.</p>
</div>
<divclass="step">
<h4>Mesh reactivation</h4>
<p>FIPS federation auto-activation kicks off in the background; the Reticulum identity re-derives from the restored node key, so your LXMF mesh address comes back too.</p>
</div>
<divclass="step">
<h4>Password & re-seal</h4>
<p>When you set the (new) login password, the words are re-encrypted into a fresh <code>master_seed.enc</code> — the reveal feature works on the restored node just like the original.</p>
</div>
</div>
<h2id="comes-back">What Comes Back — and What Doesn't</h2>
<divclass="card-grid">
<divclass="card-sm">
<h4><spanclass="badge badge-green">Restored by the words</span></h4>
<p>Node identity & DID · node npub · FIPS mesh key · Reticulum/LXMF address · personal identity keys & npubs · message-store / contacts / credential encryption keys · the (dormant) Bitcoin xprv · the ability to reveal the phrase again.</p>
</div>
<divclass="card-sm">
<h4><spanclass="badge badge-yellow">Needs its own backup</span></h4>
<p>Lightning wallet (aezeed — one-way gate, see above) and channel state · chat history & app data (node backup) · identity names/avatars (metadata) · app secrets (regenerate on reinstall).</p>
</div>
<divclass="card-sm">
<h4><spanclass="badge badge-red">Gone by design</span></h4>
<pclass="subtitle">Your words as a scannable code — using the open SeedSigner standard.</p>
<p>
Wherever the phrase is displayed, a <strong>QR code</strong> tab is offered alongside the
words. For the main (BIP-39) phrase the default format is <strong>SeedQR</strong>: each
word becomes its 4-digit position in the official wordlist (24 words → 96 digits),
encoded as a compact numeric QR. Hardware wallets like Passport, SeedSigner and Keystone
import this format directly — so you can move your on-chain identity into cold storage
without ever typing the words. A plain-text QR fallback exists for wallets that read the
phrase as text.
</p>
<ul>
<li>The QR contains <em>exactly the same secret</em> as the words — treat a printout or screenshot of it with identical care.</li>
<li>The Lightning aezeed is deliberately <em>never</em> SeedQR-encoded — it isn't BIP-39, hardware wallets can't import it, and pretending otherwise would be dishonest. It gets a plain-text QR with an explanation instead.</li>
<li>Restore is by typed/pasted words; there's no camera-based SeedQR scanner on the restore path today.</li>
</ul>
<h2id="limitations">Honest Edges</h2>
<pclass="subtitle">Known trade-offs and open hardening items, stated plainly.</p>
<p>
The audit that produced this system also tracked what it <em>didn't</em> fix. None of
these are secrets — honest security means naming the edges:
</p>
<ul>
<li><strong>The words cross the RPC boundary.</strong> During onboarding the phrase travels (over HTTPS/localhost) to your browser to be displayed, sits in the browser's session storage for the wizard's duration, and is held in server memory for the 10-minute idempotence window. This is the deliberate price of a refresh-proof, display-once flow.</li>
<li><strong>Argon2 parameters are the library defaults</strong> (≈19 MiB, 2 passes) rather than the heavier profile the design doc calls for. Still memory-hard and slow for attackers; scheduled for tightening.</li>
<li><strong>2FA backup codes carry a slight statistical bias</strong> from a modulo operation — cosmetically imperfect, cryptographically irrelevant at their length, and queued for cleanup.</li>
<li><strong>The lint ban covers the main workspace</strong>, but one small helper crate outside the workspace isn't reached by it yet.</li>
<li><strong>Best-effort backup sealing:</strong> if writing <code>master_seed.enc</code> fails during setup, the node continues (keys exist; only reveal is lost) — a loud failure might arguably be better, and this trade-off is under review.</li>
</ul>
<h2id="verify">Verify It Yourself</h2>
<pclass="subtitle">Don't trust — recompute.</p>
<p>
Because every derivation is deterministic and label-fixed, you can independently confirm
that your node's keys really do come from your words:
</p>
<ul>
<li><strong>Independent re-derivation script:</strong><code>scripts/verify-seed-derivation.py</code> in the Archipelago source — pure-standard-library Python, no Archipelago code. Paste your mnemonic (on a trusted, offline machine) and it recomputes <code>node_key</code>, <code>nostr_secret</code> and <code>fips_key</code>, byte-comparing them against <code>/var/lib/archipelago/identity/</code>.</li>
<li><strong>Known-answer tests:</strong> the test suite pins the exact expected keys for a fixed test mnemonic; any change to the derivation math, however subtle, turns the build red.</li>
<li><strong>Non-determinism regression test:</strong> 64 consecutive generated mnemonics are asserted unique — a canary against the "predictable RNG" failure class.</li>
<li><strong>Your entropy audit trail:</strong><code>/var/lib/archipelago/security/csprng-readiness.jsonl</code> records, append-only, the kernel randomness verdict at every key-generation event on <em>this</em> node — including the moment your seed was born.</li>
</ul>
<divclass="callout callout-success">
<strong>The whole story in one paragraph</strong>
Your node asked the Linux kernel for 32 bytes of hardware-grade randomness through a
single, named, compiler-enforced channel; refused to proceed unless the bytes looked
alive; wrote down the health of the random pool as evidence; turned the bytes into 24
words it showed you exactly once; locked an encrypted copy behind your password; and
then grew every identity and key it owns from those words along fixed, versioned,
independently-verifiable paths — so that the words in your drawer are, and will remain,