Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 48 additions & 17 deletions docs/developer/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ generic parse-length failure.
| checksum and CRC arithmetic | `checksums.py` | official vectors, `test_crc.py` |
| optional application rules and S types | `profiles/ms32.py`, `cl32.py`, `bip39.py` | profile and BIP39 tests |
| immutable base artifacts and interpolation | `bip93.py` | BIP93 vectors, `test_sharing.py` |
| entropy, masks, identifiers, output indices | `generation.py` | `test_generation.py` |
| entropy, masks, identifiers, output indices | `generation.py`, `CoreEntropy` | `test_generation.py`, `test_bitcoin_core.py` |
| shared GF(32) arithmetic | `gf32.py` | sharing vectors and correction corpus |
| fixed BCH and worksheet correction | `correction.py` | `test_correction_bch.py` |
| structural alignment and admission | `indel.py` | `test_correction_indel.py`, `test_correction_capture.py` |
Expand Down Expand Up @@ -67,7 +67,9 @@ generic parse-length failure.
`.text` attribute.
- Sharing interpolates payload and checksum together, explicitly constructs the
target header, and reparses the result.
- `generation.py` is the only entropy owner; it generates and shares only `ms`.
- `generation.py` is the only entropy owner; it draws every byte from its
caller's `entropy` source (Bitcoin Core in the CLI) and generates and shares
only `ms`.
- Correction never edits the HRP or separator and reparses every candidate.
- `_bip32.py` stops at HMAC-SHA512 root derivation, scalar validity, and root
xprv/tprv Base58Check serialization. It performs no child derivation or
Expand Down Expand Up @@ -152,8 +154,10 @@ accepts only `ms` artifacts.

## Secret generation

`generation.py` is the only module that draws entropy. It generates and
splits BIP93 master seeds only.
`generation.py` is the only module that draws entropy, always by calling the
caller's `entropy(length)`, which must return exactly `length` bytes. The CLI
passes [Bitcoin Core's](#bitcoin-core-randomness). It generates and splits
BIP93 master seeds only.

Fresh unshared seeds default to 16 bytes and use the first 20 bits of their
BIP32 fingerprint as public identifier metadata. Fresh shared sets use four
Expand All @@ -169,17 +173,18 @@ boundary; there is no legacy decoder.
The one-shot function creates only unshared secrets:

```python
generate_master_seed(seed_bytes=None, *, byte_length=None, identifier=None, fingerprint=None)
generate_master_seed(seed_bytes=None, *, byte_length=None, identifier=None, fingerprint=None, entropy=None)
```

A fresh seed with no `identifier` needs `fingerprint`: a callable that takes
the seed bytes and returns their four-byte BIP32 master fingerprint. This
package has no secp256k1 arithmetic, so the CLI asks Bitcoin Core. Without
either argument, fresh generation raises `ValueError`.
either argument, fresh generation raises `ValueError`, as does any draw without
`entropy`.

Shared creation uses `CreationCeremony.master_seed(...)` or
`CreationCeremony.from_secret(...)`. Exactly one of `share_count` and `indices`
is required. `next_share()` returns one pending share, `confirm(text)` must
`CreationCeremony.from_secret(...)`; both require `entropy`. Exactly one of
`share_count` and `indices` is required. `next_share()` returns one pending share, `confirm(text)` must
accept its independently re-entered string, and `finish()` returns the secret
only after every requested share is confirmed. There is no public one-shot
sharing or `split_secret` function. Fresh and existing Bitcoin CLI creation
Expand All @@ -206,12 +211,12 @@ The ceremony follows the two BIP93 constructions:
- a fresh set draws `k` independent complete u5 masks;
- an existing S uses S plus `k-1` independent complete u5 masks.

Each direct share uses a separate `secrets.token_bytes` request and cannot be
followed by another draw until its string is confirmed. The pause gives the OS an
opportunity to mix fresh noise; Python cannot guarantee that new physical
entropy arrives between calls. User input is never treated as entropy. Each
byte is mapped with `value & 31`, which maps exactly eight byte values to each
u5 value.
Each direct share uses a separate `entropy` request and cannot be followed by
another draw until its string is confirmed. The pause gives the source an
opportunity to mix fresh noise; nothing guarantees that new physical entropy
arrives between calls. User input is never used as share bytes. Symbols are
the drawn bytes regrouped five bits at a time, so `n` symbols take
`ceil(5n/8)` bytes: 17 for a 128-bit share, 65 for a 512-bit one.

Creation confirmation preserves whitespace-separated entered groups as units,
even when moving characters between groups would reduce edits or red groups.
Expand Down Expand Up @@ -258,9 +263,9 @@ remain fixed. That padding rule is not BIP93 validity: parsed S strings may use
any application-valid discarded bits, which re-sharing preserves exactly. CRC
never applies to shares.

Explicit output indices preserve caller order. A share count uses
`SystemRandom.sample` over the 31 ordinary indices and preserves sample order.
There is no entropy injection, sorting, caller-supplied partial-basis
Explicit output indices preserve caller order. A share count picks
from the 31 ordinary indices by rejection sampling of `entropy` bytes, in draw
order. There is no sorting, caller-supplied partial-basis
completion, or BIP39 generation. Ceremonies reject copying and serialization;
the CLI does not resume an interrupted ceremony.

Expand Down Expand Up @@ -641,6 +646,32 @@ The Core calls are fixed: `getnetworkinfo`, `getblockchaininfo`, `listwallets`,
Bitcoin Core alone creates wallets, selects encryption, handles
passphrases, stores keys, and provides normal wallet behavior.

### Bitcoin Core randomness

`ms32 create` draws every random byte from `CoreEntropy`, so it stops if Core
cannot supply them. Core's `GetStrongRandBytes` mixes the OS generator with
RDRAND/RDSEED where available, timestamps, and accumulated state. Each draw
creates a blank temporary wallet in RAM: `/dev/shm` on Linux, a RAM disk from
`hdiutil` on macOS, otherwise the temp folder on disk with one warning. It is
encrypted from birth with a 256-bit `secrets` passphrase sent only on stdin.
The draw unlocks it, runs `addhdkey` once per 32 bytes, takes the SHA-256 of
the xprv from `gethdkeys private=true` followed by 32 bytes from `secrets`,
relocks it, and unloads and deletes it before returning any bytes. Hashing
stops the xpub, which the wallet file stores unencrypted, from checking
guesses of a share. The OS bytes stop a leaked file's xpubs from capping each
key at about 128-bit security, Bitcoin's own ECDLP bound, so a working OS
generator alone keeps full strength and a broken one leaves Core's. If the OS
generator is broken and the file leaks, the passphrase is weak too, and only
RAM keeps the file safe. Each draw removes its wallet even when it fails or is
interrupted by Ctrl-C, SIGHUP, or SIGTERM, so callers have nothing to close.
If the wallet can't be removed, creation stops before the next card. These
extra calls are `createwallet`, `walletpassphrase`, `gethdkeys`, and
`unloadwallet`.

The temporary wallet only works around Core having no call that returns
random bytes. If Core gains one, or can export a seed, only `CoreEntropy`
changes: `generation.py` and both front ends keep calling `entropy(length)`.

The wallet CLI is one leaf command:

```text
Expand Down
10 changes: 6 additions & 4 deletions docs/security/invariants.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,9 @@ and evidence.
HRPs remain opaque and never gain invented application semantics.
2. APIs accept only immutable validated artifacts. Outputs are reparsed;
recovery and derivation reject sets with mismatched normalized HRPs or shapes.
3. Shared creation uses a separate OS-CSPRNG call for each random initial share,
gated by confirmation. Input cannot replace entropy or the original secret.
3. Shared creation uses a separate Bitcoin Core draw for each random initial
share, gated by confirmation, with no fallback. Input cannot replace entropy
or the original secret.
4. Wallet setup uses the original ceremony result or a validated recovered seed.
Restore authenticates the recovered seed before any wallet is listed,
unlocked, or imported into: normally with the master fingerprint typed from
Expand All @@ -36,8 +37,9 @@ and evidence.
operator confirms an eligible descriptor wallet by exact name.
8. Wallet state is revalidated before handing Core the master key. Core must
accept that key and create every requested account-0 wallet descriptor.
9. codex32 has no passphrase channel. An unlocked encrypted signer is relocked
and verified on every exit path.
9. codex32 never handles the operator's wallet passphrase. An unlocked encrypted
signer is relocked and verified on every exit path; the temporary randomness
wallet is relocked after each draw.
10. External text, Core output, public wallet data, and PSBTs are untrusted.
11. Only Bitcoin Core descriptor wallets sign with codex32-derived keys.
Sensitive operations use only codex32 or Core on malware-free computers
Expand Down
19 changes: 11 additions & 8 deletions docs/security/model.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ fingerprints, and wallet history cannot spend funds but remain privacy-sensitive
| Package API | Recovery, derivation, sharing, and wallet operations accept validated artifacts, not unchecked text. |
| Python process | Secret objects remain in process memory; normal output must not reveal them unintentionally. |
| Operator and paper | The operator controls transcription, physical recovery cards, wallet records, and acceptance of correction suggestions. |
| Entropy and root keys | The operating-system cryptographically secure random-number generator (OS-CSPRNG) and Python stdlib HMAC/SHA-512 primitives are trusted. codex32 performs only BIP32 root validation and root xprv/tprv serialization in process. |
| Entropy and root keys | Every random byte of `ms32 create` hashes a fresh Bitcoin Core HD key, from a generator that mixes in the operating-system cryptographically secure random-number generator (OS-CSPRNG), with 32 more OS-CSPRNG bytes; either source alone keeps the bytes unpredictable. Python stdlib HMAC/SHA-512 primitives are trusted. codex32 performs only BIP32 root validation and root xprv/tprv serialization in process. |
| Bitcoin Core | The selected `bitcoin-cli`, its configuration, the selected local Core instance, and the destination computer are trusted for fingerprints, hardened/public-key derivation, and wallet initialization. |

## Operator assumptions
Expand Down Expand Up @@ -64,8 +64,11 @@ The operator must:
implementation. Elliptic-curve public-key derivation used for fingerprints,
xpubs, and public descriptors is delegated to the separately running Bitcoin
Core process.
- Separate OS-CSPRNG calls provide opportunities to mix fresh noise but cannot
- Separate Core draws provide opportunities to mix fresh noise but cannot
guarantee new physical entropy between calls.
- The temporary randomness wallet's passphrase comes from the OS-CSPRNG. Where
no RAM folder exists, as on Windows, the encrypted wallet briefly sits on
disk. Swap, RAM disks and tmpfs can page memory, including secrets, to disk.
- A checksum, generation-padding hint, fingerprint, or correction candidate
does not authenticate a backup or prove the operator's intent.
- Creation feedback identifies correct groups but does not prove the recovery card was corrected.
Expand Down Expand Up @@ -116,9 +119,9 @@ complete explicit `ms1` string.
## Creation, sharing, and recovery controls

Fresh shared creation generates *k* random initial shares. Each uses a separate
full-payload OS-CSPRNG request. The current share string must be re-entered
full-payload request to Bitcoin Core. The current share string must be re-entered
exactly, ignoring case and whitespace, before the next request. Confirmation
text is never reparsed as the source secret and contributes no entropy. Existing
text is never reparsed as the source secret or used as share bytes. Existing
complete Bitcoin master seeds are confirmed unchanged and initialize the wallet
without new entropy. The `ms32` façade rejects CL. The generic API parses,
inspects, and corrects unshared CL secrets only: Core Lightning rejects shares
Expand All @@ -132,8 +135,8 @@ Progressive group-level correctness feedback is explicitly accepted and does not

Confirmation shows that the operator can produce the correct recovery string during setup. It cannot prove that the physical backup was corrected rather than reconstructed using confirmation feedback.

The API accepts neither caller-provided entropy nor padding values, partial
bases, or resumable ceremony state. Fresh creation rejects final-share
The API draws randomness only from its caller's `entropy` source and accepts no
padding values, partial bases, or resumable ceremony state. Fresh creation rejects final-share
candidates whose generation padding is invalid. Master-seed creation also
rejects candidates that cannot form a valid BIP32 root. This check uses only
stdlib HMAC-SHA512 plus the secp256k1 scalar-order bound; it performs no point
Expand Down Expand Up @@ -234,10 +237,10 @@ signing setup belong to Bitcoin Core's maintained v32 workflow.
|---|---|
| Preflight | Before entropy or recovery input, explicit chain arguments probe the five standard local networks for Bitcoin Core 32 or newer. One response is selected automatically; multiple responses require operator selection. |
| Recovery identity | `ms32 wallet` and `ms32 create --existing` authenticate a recovered seed before any wallet is listed. Core derives the recovered fingerprint statelessly, and a mismatch raises `FingerprintMismatch` before any wallet RPC. The restore prompt does not show the recovered value, so the operator compares by typing the fingerprint from the wallet record. Without a record, the operator is shown the recovered fingerprint, whether the backup identifier was derived from the seed (the codex32 fingerprint rule, Bails' RIPEMD-160 rule, or its mid-2023 alpha's SHA-256 rule), and a warning, and then chooses. Fresh `ms32 create` has no pre-existing wallet to authenticate: it shows the newly created seed's fingerprint and requires the operator to acknowledge recording it. These checks catch mistakes such as wrong or mixed cards; anyone able to replace a threshold of cards could already read them. |
| Process boundary | codex32 invokes the reviewed `bitcoin-cli` from `PATH` as a child without a shell, direct RPC socket, wallet database, or wallet-creation operation. Every call uses loopback and the selected chain. |
| Process boundary | codex32 invokes the reviewed `bitcoin-cli` from `PATH` as a child without a shell, direct RPC socket, or wallet database; its only wallet creation is the temporary randomness wallet. Every call uses loopback and the selected chain. |
| Destination | Only an empty descriptor wallet with private keys enabled, no external signer, transactions, descriptors, keypool entries, or active scan is eligible. One eligible wallet is offered directly; multiple wallets are selected by number. New wallets are detected by polling, and rejection returns to every eligible wallet. The escaped name is confirmed exactly. |
| Seed source | The original ceremony result or validated recovered master seed supplies a root xprv for Core's reported chain. Core v32 creates BIP44, BIP49, BIP84, and BIP86 account-0 descriptors from that key. |
| Secret channel | The master xprv is sent only through the child's standard input. A timestamped rescan obtains one private descriptor from Core's captured stdout and returns it through stdin; neither value is printed or passed in arguments or diagnostics. codex32 has no passphrase channel and suppresses raw Core errors. |
| Secret channel | The master xprv is sent only through the child's standard input. A timestamped rescan obtains one private descriptor from Core's captured stdout and returns it through stdin; neither value is printed or passed in arguments or diagnostics. codex32 never handles the operator's wallet passphrase and suppresses raw Core errors. |
| Revalidation | Every destination property is checked again immediately before adding the key. `addhdkey` must accept it and `createwalletdescriptor` must return two public descriptors for each requested address type. Numeric recovery timestamps trigger Core's time-based rescan (genesis for `0`), with no guessed block height. Core is trusted to derive and store the wallet policy. |
| Relocking | Once Core reports an encrypted private-key wallet unlocked, a `finally`-protected obligation requests `walletlock` and verifies the locked state after success, failure, state change, or interruption. |

Expand Down
10 changes: 7 additions & 3 deletions docs/user/guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,8 +73,12 @@ Before running `create` for a real wallet, have your blank cards, a pen, and
wallet record ready, and choose separate trusted places for shared cards.
If you want to try the process first, use the signet practice setup below.

Run Bitcoin Core before starting. If practical, disconnect the computer from
external networks while recovery text is on screen. codex32 talks only to the
Run Bitcoin Core before starting: `create` needs Core for its randomness.
For each card's randomness, Core makes a temporary encrypted wallet in RAM
(macOS briefly shows a small `codex32-` disk) and deletes it before the card
is shown. If
practical, disconnect the computer from external networks while recovery text
is on screen. codex32 talks only to the
local Core instance at `127.0.0.1` during setup.

For graphical practice on signet, start Bitcoin-Qt with:
Expand Down Expand Up @@ -156,7 +160,7 @@ the selected encrypted wallet is locked, open **Window > Console** in
Bitcoin-Qt, select the named wallet, and enter
`walletpassphrase "YOUR PASSPHRASE" 5`. codex32 waits and continues
automatically. It imports account 0, verifies the public descriptors Core
accepted, and relocks the wallet. It does not create wallets or choose
accepted, and relocks the wallet. It does not create your wallet or choose its
encryption.

You are done with this step when the terminal says:
Expand Down
Loading
Loading