| Severity | low |
| Status | fixed |
| Found | 2026-04-18 |
| Fixed | 2026-04-19 |
| Area | bindu/utils/did |
Symptom
bindu.utils.did.signature.verify_signature is the crypto core of
the DID authentication layer. It’s called from the Hydra middleware
for every DID-authenticated request. Operators reading server logs
saw exactly one message for every failure mode:
- Genuine crypto mismatch (attacker tampering / wrong key)
- Caller sent malformed base58
- Caller sent a public key of wrong length
- AttributeError in our own code because we mis-typed a method name
- ImportError because a dependency went missing
- Any other Python exception that happened to fire inside the try block
Root cause
Atbindu/utils/did/signature.py
pre-fix:
Exception in the tuple does the damage. Exception
is the base class of almost every runtime error in Python —
including AttributeError, ImportError, KeyError,
ZeroDivisionError, and every random third-party exception. The
try block wrapped four quite different operations (decode, decode,
construct, verify) and the except treated them all as “signature
failure.”
Why did this ship? The author clearly intended the tuple to be a
belt-and-braces catch-all: “the narrow exceptions we expect, plus
anything weird.” But in Python that’s an anti-pattern — catching
the base class after the narrow classes makes the narrow ones
redundant, and the catch-all swallows bugs. The right shape is
narrow catches only, with distinct try blocks per concern.
Second issue: because the inner except swallowed everything, the
outer except (ImportError, UnicodeEncodeError, ValueError, TypeError) that wraps the whole function was effectively dead
code for the decode/verify path. It never saw any of those
exceptions.
Fix
Three narrow try blocks, each for a single failure mode, with distinct log reasons and no catch-all:timestamp_out_of_window— replay-window rejectmalformed_input— base58 decode failure or wrong key lengthcrypto_mismatch— genuine signature math failure
False.
Tests: tests/unit/utils/did/test_signature.py
— 9 cases:
- Happy path: valid signature accepted.
- Six legitimate reject cases, one per failure mode (timestamp, malformed signature, malformed public key, wrong-length key, valid-base58-but-wrong-signature, body tampering).
- Two regression guards: a patched
VerifyKey.verifyraisingRuntimeErrorand a patchedcreate_signature_payloadraisingAttributeError. Pre-fix these would have been swallowed and silently returnedFalse. Post-fix they propagate — if the broad-except bug ever comes back, these tests fail loudly.
Why the tests didn’t catch it
There were no direct unit tests forverify_signature at all.
Tests that exercised it transitively (the Hydra middleware tests
at tests/unit/server/middleware/test_hydra_did_signature.py) all
used happy-path crypto — they passed real keys and real
signatures. The interesting cases — malformed input, bugs inside
the try block — were never exercised.
The new regression tests do the one thing the old suite never
did: inject unexpected exceptions mid-verification and assert they
propagate. Any future change that reintroduces a broad except
will fail those tests.
Class of bug — where else to watch
The general shape: a try-except tuple that mixes narrow expected exceptions with a catch-all base class. In Python, the canonical form of this anti-pattern is:Exception at the end silently subsumes the specific ones and
catches literally every bug. Grep target: except \(.*Exception\)
(tuple that ends in Exception).
Places to audit for the same shape:
- Any crypto / signing / verifying code added in the future. The same footgun is extremely tempting when dealing with library code that can raise many exception types.
- Webhook delivery paths (notifications). Easy to reach for a broad catch when “any network failure counts as delivery failure.”
- Middleware in general — a catch-all that maps every exception to a generic 500 is an observability disaster.
try/except where the corresponding log
line says only “failed” without naming the specific reason.
Those are the paths where triage goes to die.
Follow-ups
- Apply the same pattern discipline to the x402 payment middleware
— it has several broad
except Exceptionblocks (seebugs/known-issues.mdentriesx402-*) that swallow crypto / RPC errors the same way. - The Hydra middleware tests (
test_hydra_did_signature.py) could be extended with the same “inject unexpected exception, assert it propagates” pattern for the middleware layer, not justverify_signatureitself.