From f47cb9721aac3794734cd63abf2397c9b82daaa3 Mon Sep 17 00:00:00 2001 From: Ben Westgate Date: Sun, 4 Oct 2026 18:07:57 -0500 Subject: [PATCH 1/2] generation: Validate roots before re-sharing A parsed Bitcoin secret validates the codex32 format but need not form a valid BIP32 root. Reject that state at the public from_secret ceremony boundary before identifier randomness or recovery-card output, using the existing stdlib-only validator. The supplied-byte path was already covered by PR #16. Retain format parsing and Core Lightning behavior. The header-collision comparison is kept on one formatted line in the same function; its behavior is unchanged and the package remains at 5,198 logical review lines. The 69 focused creation/API tests pass normally and with optimization; Ruff, strict mypy, build/twine, and whitespace checks pass. The full normal suite passes 953 tests; the optimized full suite is running. AI-assisted fix requested by the maintainer; fixes #125. --- docs/developer/api.md | 4 ++++ src/codex32/generation.py | 7 +++---- tests/test_generation.py | 17 +++++++++++++++++ 3 files changed, 24 insertions(+), 4 deletions(-) diff --git a/docs/developer/api.md b/docs/developer/api.md index 614a5a1..e16b152 100644 --- a/docs/developer/api.md +++ b/docs/developer/api.md @@ -237,6 +237,10 @@ requires interactive input and output, preflights local Bitcoin Core before entropy or recovery input, and initializes a user-selected wallet after every share is confirmed. CLI creation does not accept Core Lightning profiles; CL generation and sharing remain API-only. +Supplied Bitcoin seed bytes and existing Bitcoin secrets must form a valid +BIP32 root before a creation ceremony selects entropy or emits recovery cards. +Parsing a codex32 string remains a format check; Core Lightning has no BIP32 +root requirement. Without `--existing`, omitting the Bitcoin header creates an unshared master seed. With `--existing` and no sharing threshold, a supplied codex32 secret is emitted and confirmed unchanged, and the original validated artifact initializes diff --git a/src/codex32/generation.py b/src/codex32/generation.py index a99d0ac..818918f 100644 --- a/src/codex32/generation.py +++ b/src/codex32/generation.py @@ -295,13 +295,12 @@ def from_secret( """Start a ceremony that shares an existing validated secret.""" if not isinstance(secret, (MasterSeed, CoreLightningSecret)): raise TypeError("from_secret accepts only MasterSeed or CoreLightningSecret") + if isinstance(secret, MasterSeed) and not _valid_root(secret.seed_bytes): + raise CodexError("master seed does not form a valid BIP32 root") threshold = _threshold(threshold, allow_zero=False) random_identifier = identifier is None identifier = _random_identifier() if random_identifier else _identifier(identifier) - while (threshold, identifier) == ( - secret.header.threshold, - secret.header.identifier, - ): + while (threshold, identifier) == (secret.header.threshold, secret.header.identifier): if not random_identifier: raise HeaderCollision("new share set must use a different set header") identifier = _random_identifier() diff --git a/tests/test_generation.py b/tests/test_generation.py index 2ce8ab3..64870af 100644 --- a/tests/test_generation.py +++ b/tests/test_generation.py @@ -169,6 +169,23 @@ def test_supplied_seed_must_form_a_valid_bip32_root(monkeypatch: pytest.MonkeyPa generate_master_seed(bytes(16), identifier="test") +def test_existing_bitcoin_secret_requires_a_valid_root_before_any_entropy( + monkeypatch: pytest.MonkeyPatch, +) -> None: + source = MasterSeed.from_seed(bytes(range(16)), identifier="test") + lightning = generate_core_lightning_secret(bytes(range(32)), identifier="test") + monkeypatch.setattr(generation_module, "_valid_root", lambda _seed: False) + + def no_entropy(_length: int) -> bytes: + pytest.fail("invalid Bitcoin root reached entropy selection") + + monkeypatch.setattr(generation_module.secrets, "token_bytes", no_entropy) + with pytest.raises(CodexError, match="master seed does not form a valid BIP32 root"): + CreationCeremony.from_secret(source, threshold=2, indices="ac") + # BIP32 root validity does not apply to Core Lightning's HSM secret. + CreationCeremony.from_secret(lightning, threshold=2, indices="ac", identifier="name") + + def test_explicit_and_random_output_order_contracts() -> None: source = generate_master_seed(bytes(range(16)), identifier="test") _secret, shares = _complete( From 8cd584e02c33aad0043762215def5452a9c00f1d Mon Sep 17 00:00:00 2001 From: Ben Westgate Date: Sun, 4 Oct 2026 18:18:36 -0500 Subject: [PATCH 2/2] docs: Record the existing-root creation gate Clarify that a format-valid parsed MasterSeed does not by itself establish the BIP32-root precondition for creation. Document the from_secret check before identifier selection or entropy and distinguish Core Lightning. Matches the already-tested behavior of f47cb97; no runtime change. AI-assisted response to the PR #126 review; refs #125. --- docs/security/model.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/docs/security/model.md b/docs/security/model.md index 8a07c3d..dd2da50 100644 --- a/docs/security/model.md +++ b/docs/security/model.md @@ -156,7 +156,12 @@ multiplication. The original ceremony result, not re-entered text, remains the source for automatic wallet setup. Sharing an existing secret generates and confirms *k−1* random initial shares -before deriving the remaining shares. Recovery requires exactly the declared +before deriving the remaining shares. Supplied Bitcoin seed bytes and existing +Bitcoin secrets must form a valid BIP32 root. `CreationCeremony.from_secret` +checks this before selecting an identifier or drawing entropy; a parsed +`MasterSeed` alone establishes format validity, not this creation precondition. +Core Lightning secrets have no BIP32-root requirement. +Recovery requires exactly the declared threshold of compatible shares with distinct indices. Derivation requires a new share index not used by its inputs. Every output is reparsed before release.