Skip to content

descriptor: validate public keys when parsing - #10951

Open
fametrano wants to merge 1 commit into
spesmilo:masterfrom
fametrano:btclib-666-validate-pubkey
Open

fametrano wants to merge 1 commit into
spesmilo:masterfrom
fametrano:btclib-666-validate-pubkey

Conversation

@fametrano

Copy link
Copy Markdown
Contributor

A raw-hex public key in a descriptor is never checked to be a valid point, so pkh(<64 hex chars that are not a curve point>) parses. Bitcoin Core rejects an invalid key at parse time.

This validates a raw-hex key against the acceptance set Core uses in ParsePubkeyInner, context-aware:

  • inside tr(): a 32-byte x-only key or a 33-byte compressed key;
  • elsewhere: a 33-byte compressed key, and a 65-byte uncompressed key only where the context permits it (rejected in the segwit contexts wpkh()/wsh(), per BIP141);
  • hybrid (0x06/0x07) and off-curve or wrong-length keys are rejected.

Validation uses electrum_ecc. The tr() internal key is now parsed in P2TR context so a 32-byte x-only key is recognised there. Extended-key (xpub) expressions are unaffected. Tests cover the acceptance and rejection cases.

The equivalent change for Bitcoin Core's HWI, which shares this code, is bitcoin-core/HWI#859.

Comment thread electrum/descriptor.py Outdated
Comment on lines +317 to +328
try:
if len(data) == 65 and data[:1] != b"\x04":
raise ecc.InvalidECPointException() # reject hybrid keys, as Core does
ecc.ECPubkey(data) # 33-byte compressed or 65-byte uncompressed point
except ecc.InvalidECPointException:
if is_taproot and len(data) == 32:
try:
ecc.ECPubkey(b"\x02" + data) # even-y x-only point per BIP340
return
except ecc.InvalidECPointException:
pass
raise ValueError(f"invalid public key in descriptor: {self.pubkey!r}")

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

untested but this looks needlessly verbose.

Suggested change
try:
if len(data) == 65 and data[:1] != b"\x04":
raise ecc.InvalidECPointException() # reject hybrid keys, as Core does
ecc.ECPubkey(data) # 33-byte compressed or 65-byte uncompressed point
except ecc.InvalidECPointException:
if is_taproot and len(data) == 32:
try:
ecc.ECPubkey(b"\x02" + data) # even-y x-only point per BIP340
return
except ecc.InvalidECPointException:
pass
raise ValueError(f"invalid public key in descriptor: {self.pubkey!r}")
if len(data) in (33, 65) and ecc.ECPubkey.is_pubkey_bytes(data):
pass
elif len(data) == 32 and is_taproot and ecc.ECPubkey.is_pubkey_bytes(b"\x02" + data):
pass
else:
raise ValueError(f"invalid public key in descriptor: {self.pubkey!r}")

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Applied, with one addition to the suggestion: a prefix check (data[0] in (2, 3, 4)) so hybrid keys (0x06/0x07) are still rejected. is_pubkey_bytes accepts them, since libsecp256k1 parses hybrid encodings, while Core's descriptor parser rejects them; without the check a hybrid key would also get past has_uncompressed_pubkey() in wpkh()/wsh(). The existing test covers that case.

@SomberNight

Copy link
Copy Markdown
Member

note: checking if a bytevector is a valid public key is relatively expensive, so would be good to know if this is used in any hot code path

@fametrano

Copy link
Copy Markdown
Contributor Author

Not a hot path: validate_pubkey runs only inside parse_descriptor — when a descriptor string is parsed (wallet load / import) — once per key. Address derivation reuses the parsed Descriptor through expand() and never re-parses, so the point check doesn't recur per address or per signature. Happy to adjust if you have a call site in mind where descriptors get re-parsed on a hot path.

@fametrano
fametrano force-pushed the btclib-666-validate-pubkey branch from fed1751 to 5141e87 Compare September 11, 2026 22:06
A raw-hex pubkey in a descriptor was not checked against the curve, so
an expression like pkh(<hex that is not a point>) parsed successfully.
Validate the key where it is parsed: inside tr() accept a 32-byte x-only
point or a compressed point, elsewhere a compressed or uncompressed
point, and reject anything off-curve, of the wrong length, or of a form
its context does not allow. Keys derived from an extended key are valid
by construction and are left alone.
@fametrano
fametrano force-pushed the btclib-666-validate-pubkey branch from 5141e87 to 02148e1 Compare September 12, 2026 12:29
@fametrano

Copy link
Copy Markdown
Contributor Author

These three PRs are waiting on workflow approval: #10948, #10951 and #10952. All three were rebased onto master 39cab57 yesterday with no content change, so the tests and regtest workflow runs on the current tips have never started.

Locally on 3.14.6, pytest tests/ gives the same result on all three branches and on base: one failure, test_format_date_by_section, which reproduces on base too and is a locale artifact (gio vs Thu), not something these branches touch. Each branch passes one more test than base (1092 vs 1091), matching the one test it adds. flake8 with CI's select/ignore, and ban_unicode, are clean on all three. I haven't run regtest or the other interpreters in the matrix.

Could a maintainer approve the workflow runs so CI can confirm this?

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.

2 participants