Skip to content
This repository was archived by the owner on Jul 10, 2026. It is now read-only.

fix(axon): reject requests with missing signature in default_verify#3393

Merged
basfroman merged 5 commits into
RaoFoundation:stagingfrom
Thykof:fix/default-verify-empty-signature-bypass
Jun 22, 2026
Merged

fix(axon): reject requests with missing signature in default_verify#3393
basfroman merged 5 commits into
RaoFoundation:stagingfrom
Thykof:fix/default-verify-empty-signature-bypass

Conversation

@Thykof

@Thykof Thykof commented Jun 18, 2026

Copy link
Copy Markdown
Contributor

Bug

Issue: #3392

Summary: Axon.default_verify (bittensor/core/axon.py) skips signature verification entirely when the incoming request carries no signature, allowing any caller to impersonate any dendrite hotkey.

Description of the Change

default_verify reconstructs the signed message from the request headers and then runs three checks in order: it rejects a missing nonce ("Missing Nonce"), rejects a stale nonce, and finally verifies the signature.

The signature check was written as a single combined condition:

  if synapse.dendrite.signature and not keypair.verify(message, synapse.dendrite.signature):
      raise Exception("Signature mismatch ...")

Because signature presence is and-ed with the verification, an empty or absent synapse.dendrite.signature short-circuits the expression to False, so keypair.verify(...) is never called. The request then falls through the rest of
default_verify as authenticated — the nonce is even recorded. There is a "Missing Nonce" guard but no matching "Missing Signature" guard.

This change adds the missing presence guard (mirroring the nonce guard) and then verifies unconditionally:

  if not synapse.dendrite.signature:
      raise Exception("Missing signature")

  if not keypair.verify(message, synapse.dendrite.signature):
      raise Exception(f"Signature mismatch with {message} and {synapse.dendrite.signature}")

A present-but-invalid signature was already rejected correctly (keypair.verify → False → raise). The only hole was the empty/absent case, which this closes.

Impact: an attacker can claim any hotkey in bt_header_dendrite_hotkey — including a high-stake validator — and send no signature. The forged request passes default_verify and reaches the subnet's blacklist/forward. On the standard miner
blacklist (gated on validator_permit + stake), this lets an attacker harvest miner outputs while masquerading as a legitimate validator, with no secret material required.

Alternate Designs

  • Keep one combined condition (if not signature or not keypair.verify(...): raise). Functionally equivalent, but it collapses two distinct failure modes into one message and loses parity with the existing "Missing Nonce" guard. The explicit
    two-step form is clearer and gives an actionable error.
  • Let keypair.verify handle the empty input (drop the presence check, always call verify). This relies on keypair.verify("") returning False for every keypair backend — implementation-dependent and easy to regress. An explicit presence
    check is safer and self-documenting.

The proposed version was chosen for explicitness, a clear distinct error message, and symmetry with the surrounding nonce checks.

Possible Drawbacks

  • Requests that previously "succeeded" with no signature will now be rejected. That path was the vulnerability itself, so this is the intended behavior — no legitimately signed client is affected (the dendrite always attaches a signature).
  • No change to the public API, headers, or the valid-signature path; the only behavioral change is that the previously-bypassing unsigned case now fails closed.

Verification Process

  • Unit: a synapse with dendrite.signature = "" (and None) now raises "Missing signature" from default_verify instead of returning successfully.
  • Unit (regression): a correctly signed synapse still verifies; a present-but-wrong signature still raises "Signature mismatch".
  • Integration: an unsigned request claiming a registered validator hotkey is rejected at the axon (HTTP 401) and never reaches forward.
  • Run: pytest tests/unit_tests/test_axon.py.

Release Notes

Reject requests with missing signature

Branch Acknowledgement

  • I am acknowledging that I am opening this branch against staging.

@Thykof Thykof force-pushed the fix/default-verify-empty-signature-bypass branch from 58e5f28 to c2fec1f Compare June 18, 2026 16:06
Thykof and others added 3 commits June 18, 2026 18:10
default_verify gated the signature check behind
`if synapse.dendrite.signature`, so a request with an empty or absent
signature skipped verification entirely. This lets anyone impersonate
any dendrite hotkey (e.g. a high-stake validator) by simply omitting the
signature — the request then passes verify and reaches blacklist/forward
as if it were genuinely signed.

Require a signature to be present, mirroring the existing "Missing Nonce"
guard, then verify it. A present-but-invalid signature was already
rejected; this closes the empty-signature hole.
default_verify gated the signature check behind
`if synapse.dendrite.signature`, so a request with an empty or absent
signature skipped verification entirely. This lets anyone impersonate
any dendrite hotkey (e.g. a high-stake validator) by simply omitting the
signature — the request then passes verify and reaches blacklist/forward
as if it were genuinely signed.

Require a signature to be present, mirroring the existing "Missing Nonce"
guard, then verify it. A present-but-invalid signature was already
rejected; this closes the empty-signature hole.
Exercise Axon.default_verify directly: a valid signature passes (nonce
recorded), an empty or absent signature now raises "Missing Signature"
(the auth-bypass regression this branch fixes), and a present-but-invalid
signature still raises "Signature mismatch".

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@Thykof Thykof force-pushed the fix/default-verify-empty-signature-bypass branch from c2fec1f to 2071469 Compare June 18, 2026 16:10

@basfroman basfroman left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Nice fix, the change is correct and well-documented.

One suggestion: test_default_verify_empty_signature_raises and test_default_verify_missing_signature_raises test the same code path (if not synapse.dendrite.signature) with different falsy values. Consider merging them into a single parametrized test to avoid near-duplication:

@pytest.mark.parametrize("empty_sig", ["", None])
async def test_default_verify_missing_signature_raises(empty_sig):
...

Otherwise LGTM. Please address this and re-request review.

… test

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@Thykof Thykof requested a review from basfroman June 19, 2026 11:38
@Thykof

Thykof commented Jun 19, 2026

Copy link
Copy Markdown
Contributor Author

Hi @basfroman, thank you for the review. I have pushed the requested changes.

@basfroman basfroman merged commit 43879ed into RaoFoundation:staging Jun 22, 2026
802 checks passed
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants