Skip to content

Reject out-of-range secp256k1 keys in wif and xprv apps - #71

Open
akarve wants to merge 1 commit into
mainfrom
validate-secp256k1-scalar
Open

Reject out-of-range secp256k1 keys in wif and xprv apps#71
akarve wants to merge 1 commit into
mainfrom
validate-secp256k1-scalar

Conversation

@akarve

@akarve akarve commented Jul 19, 2026

Copy link
Copy Markdown
Owner

Summary

The wif (2') and xprv (32') applications take a 32-byte slice of the BIP-85 entropy and use it directly as a secp256k1 secret key, without checking that it is a valid scalar. A secret of 0 or >= n (the curve order) is not a usable key. For such a value, different downstream libraries would either throw, silently reduce mod n, or emit a degenerate key, so the derivation was underspecified for that case.

This is astronomically rare (~1 in 2**127, the same bound BIP-32 gives), so it will realistically never occur in practice. But it should fail loudly and deterministically rather than depend on the consumer, and I was surprised we didn't already do this given the analogous BIP-32 check already in the codebase.

Changes

  • Add validate_secp256k1_key() to apps/shared.py. It raises ValueError if the 32-byte value is 0 or >= SECP256k1.order, mirroring the wording of the existing validate_private_child_params in bip32.py. The caller should retry with the next child index.
  • Call it from WifApp.apply (entropy[:32], the secret exponent) and XprvApp.apply (entropy[32:], the private key).
  • Add TestSecp256k1Validation with boundary cases (1, n-1 accepted; 0, n, n+1, 2**256-1 rejected) and app-level cases that a crafted entropy makes wif/xprv raise.

The existing test vectors are unaffected (their keys are all in range), and the full suite plus make check pass.

Note

This intentionally does not touch the Nostr application in #70, which is not on main yet and produces a secp256k1 key the same way. If this lands, #70 should reuse validate_secp256k1_key for the same reason. It would be good to settle the policy once and keep the BIP-85 text (bitcoin/bips#2126) consistent with it.

Test plan

  • poetry run pytest -m "" (full suite passes)
  • make check (black, isort, flake8, version-sync all clean)
  • New tests cover the helper, the boundaries, and both apps

Made with Cursor

The wif (2') and xprv (32') applications took a 32-byte slice of the
BIP-85 entropy as a secp256k1 secret key without checking that it is a
valid scalar. A value of 0 or >= the curve order n is not a usable key,
and different downstream libraries would either throw, silently reduce
mod n, or emit a degenerate key, so the derivation was underspecified
for that (~1 in 2**127) case.

Add validate_secp256k1_key() to apps/shared and call it from both apps
so the derivation fails loudly and deterministically, mirroring the
existing BIP-32 validate_private_child_params check. Callers should
retry with the next child index.

Co-authored-by: Cursor <cursoragent@cursor.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant