Skip to content

embit: pure-Python BIP340 signing diverges from libsecp256k1 on the default aux, and the parity test is skipped in CI #679

Description

@fametrano

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:134self.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).

Metadata

Metadata

Assignees

No one assigned

    Labels

    upstream-reportNeeds verification against the upstream implementation before a report can be made

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions