Skip to content
Open
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
7 changes: 4 additions & 3 deletions docs/security/invariants.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,10 @@ and evidence.
Restore authenticates the recovered seed before any wallet is listed,
unlocked, or imported into: normally with the master fingerprint typed from
the wallet record, or by an explicit no-record choice made after seeing the
recovered fingerprint and whether the backup identifier was derived from the
seed. Fresh `ms32 create` ceremonies do not authenticate against a
pre-existing wallet; they require the operator to record the new fingerprint.
recovered fingerprint and whether the backup identifier matched a
seed-derived rule or its standard Bails check was unavailable. Fresh
`ms32 create` ceremonies do not authenticate against a pre-existing wallet;
they require the operator to record the new fingerprint.
5. Correction shares one mass bound and deadline across target lengths. The
public API fails closed on incomplete required work; CLI searches may return
one primary-best-so-far eligible candidate at the deadline. Incomplete
Expand Down
2 changes: 1 addition & 1 deletion docs/security/model.md
Original file line number Diff line number Diff line change
Expand Up @@ -231,7 +231,7 @@ signing setup belong to Bitcoin Core's maintained v32 workflow.
| Control | Required behavior |
|---|---|
| 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. |
| 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. If RIPEMD-160 is unavailable, the standard Bails identifier check is reported as inconclusive rather than a mismatch; the Bails-alpha SHA-256 rule remains checkable. 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. |
| 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. |
Expand Down
11 changes: 10 additions & 1 deletion src/codex32/_bitcoin_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,12 @@ def parse_fingerprint(text: str) -> bytes:

def identifier_note(origin: str | None) -> str:
# Say what `identifier_origin` found, for an operator restoring without a record.
if origin == "Bails check unavailable":
return (
"The standard Bails identifier could not be checked because RIPEMD-160 is unavailable. "
"This does not prove the cards are wrong or mixed up. Compare the fingerprint and any "
"other wallet record you have before restoring."
)
if origin is None:
return (
"The backup identifier was not made from this seed. That can be normal for codex32 backups "
Expand All @@ -77,15 +83,18 @@ def identifier_origin(secret: MasterSeed, fingerprint: bytes) -> str | None:
identifier = secret.header.identifier
if identifier == _fingerprint_identifier(fingerprint):
return "codex32"
ripemd_unavailable = False
for name, digest in (("Bails", "ripemd160"), ("Bails alpha", "sha256")):
try:
hashed = hashlib.new(digest, secret.seed_bytes).digest()
except ValueError:
if name == "Bails":
ripemd_unavailable = True
continue
derived = convertbits(hashed, 8, 5, pad=True)
if identifier[:3] == _u5_to_chars(tuple(derived[:3])):
return name
return None
return "Bails check unavailable" if ripemd_unavailable else None
Comment thread
BenWestgate marked this conversation as resolved.


@dataclass(frozen=True)
Expand Down
15 changes: 12 additions & 3 deletions tests/test_bitcoin_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -843,7 +843,13 @@ def test_identifier_origin_names_the_rule_that_made_it(identifier: str, origin:
assert ("matches this seed" in identifier_note(origin)) is (origin is not None)


def test_identifier_origin_still_checks_alpha_without_ripemd160(monkeypatch: pytest.MonkeyPatch) -> None:
@pytest.mark.parametrize(
("identifier", "expected"),
(("hezu", "Bails alpha"), ("d9k8", "Bails check unavailable")),
)
def test_identifier_origin_without_ripemd160(
monkeypatch: pytest.MonkeyPatch, identifier: str, expected: str
) -> None:
original_new = hashlib.new

def without_ripemd160(name: str, data: bytes = b"") -> object:
Expand All @@ -852,8 +858,11 @@ def without_ripemd160(name: str, data: bytes = b"") -> object:
return original_new(name, data)

monkeypatch.setattr(hashlib, "new", without_ripemd160)
secret = MasterSeed.from_seed(_BAILS_SEED, identifier="hezu")
assert identifier_origin(secret, _FINGERPRINT) == "Bails alpha"
secret = MasterSeed.from_seed(_BAILS_SEED, identifier=identifier)
assert identifier_origin(secret, _FINGERPRINT) == expected
if expected == "Bails check unavailable":
assert "could not be checked" in identifier_note(expected)
assert "does not prove" in identifier_note(expected)


def test_identifier_note_allows_supported_nonderived_codex32_identifiers() -> None:
Expand Down
Loading