What is measured
diybitcoinhardware/embit, pinned at
eb6104fd
(2026-08-08): src/embit/util/key.py:572,
def sign_schnorr(key, msg, aux=None, ...). With aux=None, line 594
sets t = sec.to_bytes(32, "big") — the raw secret key, with no aux
mask applied.
libsecp256k1's nonce_function_bip340_impl, given a NULL aux, XORs the
key with ZERO_MASK, a precomputed TaggedHash("BIP0340/aux", 0x0000...00) — not the raw key. embit.ec.PrivateKey.schnorr_sign
(src/embit/ec.py:234-235) never passes aux through, so every call
goes through this default.
Reproduced
BIP340 vector 0 (seckey 3, all-zero message): signing with the
fallback and an explicit zero-byte aux (matching libsecp256k1's
convention) reproduces the vector's expected signature, E907831F....
Signing with embit's actual default (aux=None, no masking) produces a
different signature, 8801B9FF....
The same key and the same message sign differently depending on which
backend embit loads — the ctypes/libsecp256k1 backend when available,
the pure-Python fallback otherwise. This is silent: nothing in embit's
public API surfaces which backend produced a given signature.
Why the test that would catch this doesn't run
tests/tests/test_bindings.py:134 — self.assertEqual(sig1, sig2) —
asserts exactly this parity between backends, but the test is
@skipUnless(_SCHNORR_AVAILABLE, ...) and .github/workflows/ci.yml
never installs libsecp256k1, so the check has been silently skipped in
CI since it was written.
Fix
t = sec.to_bytes(32, "big") should be masked with the BIP340
TaggedHash("BIP0340/aux", bytes(32)) constant when aux is None,
matching libsecp256k1's own default. Separately: add a CI cell that
installs libsecp256k1 so test_bindings.py's parity check actually runs.
Before sending upstream
Draft only. This is the highest-value finding in the embit survey — a
reproduced, silent, backend-dependent correctness bug in a library
targeting hardware-wallet firmware. Worth prioritizing if only one embit
report goes out. Include the official BIP340 vectors as part of the fix's
test coverage (tests/ has none today — correctness currently rests
entirely on the skipped parity test).
What is measured
diybitcoinhardware/embit, pinned ateb6104fd(2026-08-08):
src/embit/util/key.py:572,def sign_schnorr(key, msg, aux=None, ...). Withaux=None, line 594sets
t = sec.to_bytes(32, "big")— the raw secret key, with no auxmask applied.
libsecp256k1's
nonce_function_bip340_impl, given a NULL aux, XORs thekey with
ZERO_MASK, a precomputedTaggedHash("BIP0340/aux", 0x0000...00)— not the raw key.embit.ec.PrivateKey.schnorr_sign(
src/embit/ec.py:234-235) never passesauxthrough, so every callgoes through this default.
Reproduced
BIP340 vector 0 (seckey 3, all-zero message): signing with the
fallback and an explicit zero-byte aux (matching libsecp256k1's
convention) reproduces the vector's expected signature,
E907831F....Signing with embit's actual default (
aux=None, no masking) produces adifferent signature,
8801B9FF....The same key and the same message sign differently depending on which
backend embit loads — the ctypes/libsecp256k1 backend when available,
the pure-Python fallback otherwise. This is silent: nothing in embit's
public API surfaces which backend produced a given signature.
Why the test that would catch this doesn't run
tests/tests/test_bindings.py:134—self.assertEqual(sig1, sig2)—asserts exactly this parity between backends, but the test is
@skipUnless(_SCHNORR_AVAILABLE, ...)and.github/workflows/ci.ymlnever installs libsecp256k1, so the check has been silently skipped in
CI since it was written.
Fix
t = sec.to_bytes(32, "big")should be masked with the BIP340TaggedHash("BIP0340/aux", bytes(32))constant whenauxisNone,matching libsecp256k1's own default. Separately: add a CI cell that
installs libsecp256k1 so
test_bindings.py's parity check actually runs.Before sending upstream
Draft only. This is the highest-value finding in the embit survey — a
reproduced, silent, backend-dependent correctness bug in a library
targeting hardware-wallet firmware. Worth prioritizing if only one embit
report goes out. Include the official BIP340 vectors as part of the fix's
test coverage (
tests/has none today — correctness currently restsentirely on the skipped parity test).