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
15 changes: 15 additions & 0 deletions src/bipsea/apps/shared.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,19 @@
from ecdsa import SECP256k1


def hardened_int(segment: str) -> int:
if not segment.endswith("'"):
raise ValueError(f"Expected hardened segment, got {segment}")
return int(segment[:-1])


def validate_secp256k1_key(key: bytes) -> bytes:
# BIP-32: in case parse256(key) >= n or key = 0 the secret is invalid, and
# one should proceed with the next index. (Probability lower than 1 in 2**127.)
secret = int.from_bytes(key, "big")
if secret == 0 or secret >= SECP256k1.order:
raise ValueError(
"Rare invalid secret key (0 or >= secp256k1 order). "
"Retry with the next child index."
)
return key
3 changes: 2 additions & 1 deletion src/bipsea/apps/wif/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import base58

from bipsea.app_protocol import Param, TestVector
from bipsea.apps.shared import validate_secp256k1_key


class WifApp:
Expand All @@ -20,7 +21,7 @@ def parse_path(self, segments: list[str]) -> dict[str, Any]:
return {}

def apply(self, entropy: bytes, network: str = "mainnet", **_) -> dict[str, Any]:
trimmed = entropy[:32]
trimmed = validate_secp256k1_key(entropy[:32])
prefix = b"\x80" if network == "mainnet" else b"\xef"
suffix = b"\x01" # use with compressed public keys because BIP-32
extended = prefix + trimmed + suffix
Expand Down
6 changes: 4 additions & 2 deletions src/bipsea/apps/xprv/app.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from typing import Any

from bipsea.app_protocol import Param, TestVector
from bipsea.apps.shared import validate_secp256k1_key
from bipsea.bip32 import VERSIONS, ExtendedKey


Expand All @@ -19,16 +20,17 @@ def parse_path(self, segments: list[str]) -> dict[str, Any]:
return {}

def apply(self, entropy: bytes, **_) -> dict[str, Any]:
key = validate_secp256k1_key(entropy[32:])
derived_key = ExtendedKey(
version=VERSIONS["mainnet"]["private"],
depth=bytes(1),
finger=bytes(4),
child_number=bytes(4),
chain_code=entropy[:32],
data=bytes(1) + entropy[32:],
data=bytes(1) + key,
)
return {
"entropy": entropy[32:],
"entropy": key,
"application": str(derived_key),
}

Expand Down
29 changes: 29 additions & 0 deletions tests/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,17 @@
from click.testing import CliRunner
from data.bip39_vectors import VECTORS
from data.bip85_vectors import COMMON_XPRV
from ecdsa import SECP256k1

from bipsea.app_protocol import Param
from bipsea.apps.base64.app import app as base64_app
from bipsea.apps.base85.app import app as base85_app
from bipsea.apps.dice.app import app as dice_app
from bipsea.apps.hex.app import app as hex_app
from bipsea.apps.mnemonic.app import app as mnemonic_app
from bipsea.apps.shared import validate_secp256k1_key
from bipsea.apps.wif.app import app as wif_app
from bipsea.apps.xprv.app import app as xprv_app
from bipsea.bip32types import validate_prv_str
from bipsea.bip39 import LANGUAGES, validate_mnemonic_words
from bipsea.bipsea import ISO_TO_LANGUAGE, N_WORDS_ALLOWED, cli, try_for_pipe_input
Expand Down Expand Up @@ -481,3 +484,29 @@ def test_real_app_params(self):
flags, kwargs = param_to_click_option(param)
assert flags == param.flags
assert "type" in kwargs


class TestSecp256k1Validation:
N = SECP256k1.order

def test_accepts_boundary_keys(self):
for secret in (1, self.N - 1):
key = secret.to_bytes(32, "big")
assert validate_secp256k1_key(key) == key

@pytest.mark.parametrize("secret", [0, N, N + 1, 2**256 - 1])
def test_rejects_out_of_range(self, secret):
with pytest.raises(ValueError, match="Rare invalid secret key"):
validate_secp256k1_key(secret.to_bytes(32, "big"))

def test_wif_rejects_out_of_range(self):
# entropy[:32] is the WIF secret exponent
entropy = b"\xff" * 32 + b"\x00" * 32
with pytest.raises(ValueError, match="Rare invalid secret key"):
wif_app.apply(entropy)

def test_xprv_rejects_out_of_range(self):
# entropy[32:] is the XPRV private key
entropy = b"\x00" * 32 + b"\xff" * 32
with pytest.raises(ValueError, match="Rare invalid secret key"):
xprv_app.apply(entropy)
Loading