Skip to content

[wallet] Broker Travel Rule endpoints model signature as a request field, making them uncallable due to either ConstraintViolation or -1022 #238

Description

@AlexanderBartash

Hello. Sorry for the AI-generated ticket, this is just not a good timing, taking into account Travel Rule going live. Thank you :)

What's wrong

Two Travel Rule endpoints in the wallet module can't be called through the SDK's typed methods at all:

  • submitDepositQuestionnairePUT /sapi/v1/localentity/broker/deposit/provide-info
  • brokerWithdrawPOST /sapi/v1/localentity/broker/withdraw/apply

The problem is that both request models (SubmitDepositQuestionnaireRequest, BrokerWithdrawRequest) treat signature as a field you're supposed to fill in. But signature isn't something a caller provides — it's the HMAC that the SDK's own auth layer computes and attaches at send time. Modelling it as a required request field puts it in the wrong place, and that breaks the request two different ways depending on what you do with it.

What we tried, and what happened

Attempt Result
Leave signature unset (as you would for any auth-computed value) ❌ The SDK throws ConstraintViolationException before the HTTP call — the model's @NotNull validation rejects it. The request never even goes out.
Set signature("") as a placeholder to get past validation (this is what the SDK's own example does) ❌ Binance rejects it: HTTP 400 {"code":-1022,"msg":"Signature for this request is not valid."}

The reason the placeholder fails: the SDK copies your signature value into the request body, and the auth layer then signs the whole body — so the placeholder signature= ends up inside the signed data. Binance's signing scheme never includes the signature parameter in the string being signed, so the SDK's signature is computed over different bytes than the server expects. They can't match.

What works

  • The non-broker versions of these same endpoints work fine/sapi/v1/localentity/deposit/provide-info and /sapi/v1/localentity/withdraw/apply in the very same TravelRuleApi do not model signature, so the auth layer signs them correctly and Binance accepts them. This is the tell: the two broker endpoints are specified differently from their working twins.
  • Our workaround works: we bypass the typed SDK call entirely — build the query string ourselves, sign exactly those bytes, and send them unchanged so what's signed equals what's transmitted.

What doesn't work

  • Both broker Travel Rule endpoints via the typed SDK methods — there's no combination of inputs that produces a valid request, because the auth layer always re-signs and always includes the body's signature field in the signed data.

Versions

  • Reproduced on binance-wallet 4.0.1.
  • Still present on master (wallet v5.0.0, commit 5bc4d813) — upgrading does not fix it.

What the fix might be

Remove signature from the request schema of the two broker endpoints so they match their already-correct non-broker twins. The auth layer already adds the right signature; the model field is both unnecessary and the direct cause of the failure.

🤖 Full technical context for AI agents

Summary

Two TravelRuleApi endpoints in the wallet module model the authentication signature as a required request field. Because signature is an output of the signing pipeline (not an input), this makes the typed SDK calls unusable: they either fail Jakarta bean validation before the HTTP call, or — if a placeholder is supplied — get rejected by Binance with -1022 Signature for this request is not valid.

Affected versions

  • Confirmed on binance-wallet 4.0.1.
  • Still present on master @ 5bc4d813ee055412c00670923cc0e2e4fed1b627 (wallet v5.0.0).

Affected endpoints / symbols

  • submitDepositQuestionnairePUT /sapi/v1/localentity/broker/deposit/provide-info
    • Model SubmitDepositQuestionnaireRequest (field signature, @NotNull, in openapiRequiredFields).
  • brokerWithdrawPOST /sapi/v1/localentity/broker/withdraw/apply
    • Model BrokerWithdrawRequest (field signature).

Code links (permalinks, pinned to the v5.0.0 commit)

Everything verified against commit 5bc4d813ee055412c00670923cc0e2e4fed1b627 (wallet v5.0.0, current master). Here's what you need.

The broken serialization — API method writes the auth signature into the request body:

The model that forces signature to be non-null (fails bean validation before signing):

The auth layer that signs body-including-signature (behaving correctly — shown for contrast):

Contrast — the non-broker twins that omit signature (the correct spec):

Root cause (two coupled defects)

  1. Model requires signature.
    SubmitDepositQuestionnaireRequest.getSignature() is annotated @NotNull (line ~294) and signature is added to openapiRequiredFields (line 448).
    submitDepositQuestionnaireValidateBeforeCall(...) runs ExecutableValidator.validateParameters(...) BEFORE the HTTP call, so a null signature throws ConstraintViolationException and the request is never sent.

  2. API method serializes signature into the signed body.
    TravelRuleApi.submitDepositQuestionnaireCall(...) does localVarFormParams.put("signature", ...getSignature()) (line 1456), with Content-Type: application/x-www-form-urlencoded, method PUT, auth name binanceSignature. (Broker withdraw: line 193.)
    The auth layer SignatureAuthentication.applyToParams(...) then signs joinQueryParameters(query) + payload, where payload is the form body — which now contains signature=<placeholder> (lines 33–39). Binance's signing convention excludes the signature parameter from the signed string, so the SDK's signed input contains an extra signature= token that the server's expected input does not. The HMACs cannot match → -1022.
    (See https://developers.binance.com/docs/binance-spot-api-docs/rest-api/request-security)

Evidence of internal inconsistency

The non-broker twins in the SAME TravelRuleApi are specified correctly and
do NOT model signature:

  • PUT /sapi/v1/localentity/deposit/provide-info (line ~1596)
  • POST /sapi/v1/localentity/withdraw/apply
    Only the two broker endpoints wrongly include signature as a request field.

Minimal repro

WalletRestApi api = new WalletRestApi(clientConfig); // configured with API key + HMAC secret
SubmitDepositQuestionnaireRequest req = new SubmitDepositQuestionnaireRequest()
    .subAccountId("...").depositId(123L).questionnaire("{...}").beneficiaryPii("{...}");
// (a) signature unset -> ConstraintViolationException at validateBeforeCall
// (b) req.signature("") -> HTTP 400 {"code":-1022,"msg":"Signature for this request is not valid."}
api.submitDepositQuestionnaire(req);

Expected behaviour

signature (and timestamp) are injected by the signing/auth layer and must NOT be part of the request model or serialized into the signed payload — exactly as the non-broker provide-info / withdraw/apply endpoints already work.

Suggested fix

Remove signature from the request schema of the two broker Travel Rule endpoints in the OpenAPI spec (align them with their non-broker twins) and regenerate. The auth layer already adds the correct signature to the query string; the model field is both redundant and actively breaks signing.

Current workaround (for reference)

Bypass the typed call: build the percent-encoded query, HMAC-sign that exact string, and send those identical bytes (empty body), so signed == transmitted.

Note on one inferred detail

The server-side reason ("Binance excludes signature from the signed string") is from Binance's public request-security convention plus the observed -1022, not from server code. The SDK-side facts (model requires it; API method serializes it into the signed body; auth layer signs that body) are read directly from the linked source.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions