november 2026 · 4 min

The Key Never Left. The Signer Said Yes.

Binance's preliminary update after the October 29 theft says a compromised DevOps engineer's laptop may have let an attacker "gain access to a service container and submit unauthorized withdrawal transactions" (via DEV). The other half of the statement is the important one: "We have found no evidence of any wallet private key leak" (Decrypt).

So, by Binance's account, the key was not taken. The signer signed what it was asked to sign. Blockaid's CEO called it "a compromise of signing infrastructure" (Cointelegraph). That's the class: an HSM, an MPC cluster or a cloud KMS protects the key. It doesn't protect the decision to use it.

Binance hasn't published a root cause yet. It has said there were "additional points of breach", CEO Richard Teng said the attacker "did not simply use one engineer's permissions", and it promised a technical post-mortem (Decrypt). What follows doesn't depend on those details.

What the Signatures Were

The on-chain side is public. TRM Labs describes 50 EIP-7702 authorizations, signed by the Ethereum hot wallet for consecutive nonces so one would be valid at execution, submitted in a single transaction. They delegated the hot wallet's code to a sweeper contract that took instructions only from the attacker's operator. A little over three minutes later the balance was gone. On other chains, TRM saw ordinary signed transfers in the same window, and its most likely explanation is that the attacker could make the signing system produce transactions of their choosing.

Look at what those 50 signatures are. An EIP-7702 authorization isn't a transfer. It's the account owner signing keccak(0x05 || rlp([chain_id, address, nonce])), which says "run this contract's code as me", and the delegation stays until it's replaced. A withdrawal wallet has no business ever signing one. Fifty of them in a row is not a gray area.

A signer that could tell what it was signing had every reason to refuse. Most signers can't, because the request is a 32-byte digest and the decision was made upstream, by whatever service composed it. Whoever owns that service owns the key, whatever the HSM's certification says.

Put the Policy in the Signer

The signer should receive the full preimage, decode it, hash it itself, and refuse anything outside a short list. The first byte already tells you a lot: typed transactions lead with their type (EIP-2718), EIP-7702 authorizations with 0x05 (not a transaction type), signed messages with 0x19 (EIP-191).

import time
from collections import deque

class Refuse(Exception):
    pass

WINDOW_S, MAX_SIGS = 300, 20
recent = deque()

def check(preimage: bytes) -> None:
    """Runs inside the signer, on the bytes it is about to hash and sign."""
    if not preimage:
        raise Refuse("empty")
    if len(preimage) == 32:
        raise Refuse("bare digest: the signer can't see what it would sign")
    if preimage[0] == 0x05:
        raise Refuse("EIP-7702 authorization: a hot wallet never delegates its code")
    if preimage[0] == 0x19:
        raise Refuse("off-chain message: not a withdrawal")
    now = time.monotonic()
    while recent and now - recent[0] > WINDOW_S:
        recent.popleft()
    if len(recent) >= MAX_SIGS:
        raise Refuse(f"{MAX_SIGS} signatures in {WINDOW_S}s")
    recent.append(now)
    # next: decode the transaction, check destination and amount

A legacy transaction starts at 0xc0 or above and a typed one at its type byte, so none of these checks touches a real withdrawal. It's a sketch, not a wallet. The point is where it runs: next to the key, on bytes the caller can't reinterpret, with limits the caller can't raise. After the type check come the rules that matter for the plain transfers: destination allowlists, per-window value caps, and a second, independent approval path for anything above them. Those approvals should be on the payload itself, so a compromised service can't send a different payload with a valid approval attached.

The same holds outside crypto. A release-signing key in a KMS, called by CI, will sign whatever a compromised pipeline hands it. The key is safe and the signature is worthless. If you're signing artifacts, the signer should know which repo, which commit and which workflow it's signing for, and refuse the rest.

The Tradeoff

Policy in the signer is friction you chose. Velocity caps will stop a legitimate large move on a busy day, and someone has to own the slow path for that. The policy itself becomes a target: if the same laptop can deploy a new policy, you've moved the problem one hop. Policy changes need their own approvals, held by people who aren't the ones who operate the service that composes the requests.

Protect the key, then decide what the key is allowed to sign. Only the second one stops this.

← all posts