Skip to content
Draft
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
59 changes: 42 additions & 17 deletions docs/developer/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,8 @@ 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` |
| masks, identifiers, output indices from caller entropy | `generation.py` | `test_generation.py` |
| Bitcoin Core randomness for `ms32 create` | `_bitcoin_core.py` | `test_bitcoin_core.py`, regtest fixture |
| 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 +68,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` alone turns entropy into seeds, shares, identifiers, and
indices; every byte comes from the caller's `entropy` source. It 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 +155,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 turns entropy into secrets. It generates
and splits BIP93 master seeds only. Every random byte comes from the caller's
`entropy` callable, which must return exactly the requested number of bytes;
the CLI passes Bitcoin Core's (see [Bitcoin Core randomness](#bitcoin-core-randomness)).

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 +174,20 @@ 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`. Supplied seed bytes with an `identifier` need no 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,10 +214,10 @@ 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
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 treated as entropy. Each
byte is mapped with `value & 31`, which maps exactly eight byte values to each
u5 value.

Expand Down Expand Up @@ -258,9 +266,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 draws
indices from the 31 ordinary ones by unbiased 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 @@ -613,7 +621,7 @@ Immediately before adding the key, every target property is checked again. The
original `CreationCeremony.finish()` result or validated recovered master seed
supplies the root xprv. Confirmation text is never reparsed into this source.
The key is sent only through `bitcoin-cli -stdin`; raw Core errors are suppressed,
and no passphrase interface exists.
and codex32 never asks for or handles the operator's wallet passphrase.

For wallet restoration and `ms32 create --existing`, callers make the wallet-record
decision before initialization. `BitcoinCore.initialize()` calls `verify_identity()`
Expand All @@ -637,7 +645,24 @@ the BIP32 fingerprint.
The Core calls are fixed: `getnetworkinfo`, `getblockchaininfo`, `listwallets`,
`getwalletinfo`, `listdescriptors`, `getdescriptorinfo`, `deriveaddresses`,
`validateaddress`, `addhdkey`,
`createwalletdescriptor`, `importdescriptors`, and `walletlock`.
`createwalletdescriptor`, `importdescriptors`, and `walletlock`, plus
`createwallet`, `walletpassphrase`, `gethdkeys`, and `unloadwallet` for the
temporary randomness wallet.

### Bitcoin Core randomness

`ms32 create` takes every random byte from Bitcoin Core, whose
`GetStrongRandBytes` mixes the OS generator with CPU RDRAND/RDSEED where
available, timestamps, and accumulated state. On the first draw `CoreEntropy`
creates a blank wallet in a new folder under `/dev/shm`, or the system
temporary folder where that does not exist. The wallet is encrypted from birth
with a 256-bit passphrase from Python `secrets` that only `bitcoin-cli -stdin`
receives. Each draw unlocks it, adds one fresh HD key per 32 bytes with
`addhdkey`, reads it with `gethdkeys private=true`, uses the SHA-256 of the
xprv, and relocks. Creation unloads and deletes the wallet when it ends, fails,
or is interrupted. If Core cannot create the wallet or draw from it, `create`
stops; there is no fallback. A killed process leaves the encrypted folder until
it is deleted, which under `/dev/shm` happens at the latest on shutdown.
Bitcoin Core alone creates wallets, selects encryption, handles
passphrases, stores keys, and provides normal wallet behavior.

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 entropy call for each random initial share,
gated by confirmation. `ms32 create` draws only from Bitcoin Core, 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, and the temporary
randomness wallet is relocked after each draw and deleted after creation.
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
18 changes: 11 additions & 7 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 | Bitcoin Core's random generator, which mixes the operating-system cryptographically secure random-number generator (OS-CSPRNG) with other sources, supplies every random byte of `ms32 create`; the Python API trusts its caller's `entropy` source. 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. It
protects only an encrypted wallet that is deleted after creation, and outside
`/dev/shm` that wallet briefly exists on 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,7 +119,7 @@ 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 the entropy source, Bitcoin Core in the CLI. 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
complete Bitcoin master seeds are confirmed unchanged and initialize the wallet
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 takes 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,11 @@ 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. It creates only the temporary randomness wallet. Every call uses loopback and the selected chain. |
| Randomness | `ms32 create` draws only from fresh Core HD keys in a temporary wallet that is encrypted from birth, relocked after each draw, and unloaded and deleted on every exit path. If Core cannot supply randomness, creation stops. |
| 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; the temporary wallet's own passphrase travels only through stdin. Raw Core errors are suppressed. |
| 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
7 changes: 5 additions & 2 deletions docs/user/guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,10 @@ 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
Run Bitcoin Core before starting. `create` takes all of its randomness from
Core and stops if Core cannot supply it. While cards are being made, Bitcoin-Qt
may list a temporary encrypted wallet in a `codex32-` folder; codex32 deletes it
before wallet setup. 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.

Expand Down Expand Up @@ -156,7 +159,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