hpyhacking/peatio · error · APIv2::IncorrectSignatureError

2005

2005

Error message

Signature #{signature} is incorrect.

What it means

Error code 2005 (IncorrectSignatureError) is raised by APIv2::Auth::Authenticator#check_signature! when params[:signature] != Utils.hmac_signature(token.secret_key, payload). The expected signature is OpenSSL::HMAC.hexdigest('SHA256', secret_key, payload) where payload is "HTTP_VERB|/api/v2<path>|<canonical_query>" (see payload/canonical_query in authenticator.rb). canonical_query is every request param except signature, format and route_info, run through Rails' to_query (k=v&... with sorted keys) and then URI.unescape'd. Note that authenticate! runs check_tonce! BEFORE check_signature!, so a request with a bad signature still consumes its tonce.

Source

Thrown at app/api/api_v2/auth/authenticator.rb:31

        check_signature!
        token
      end

      def token
        @token ||= APIToken.joins(:member).where(access_key: @params[:access_key]).first
      end

      def check_token!
        raise InvalidAccessKeyError, @params[:access_key] unless token
        raise DisabledAccessKeyError, @params[:access_key] if token.member.api_disabled
        raise ExpiredAccessKeyError, @params[:access_key] if token.expired?
        raise OutOfScopeError unless token.in_scopes?(route_scopes)
      end

      def check_signature!
        if @params[:signature] != Utils.hmac_signature(token.secret_key, payload)
          Rails.logger.warn "APIv2 auth failed: signature doesn't match. token: #{token.access_key} payload: #{payload}"
          raise IncorrectSignatureError, @params[:signature]
        end
      end

      def check_tonce!
        key = "api_v2:tonce:#{token.access_key}:#{tonce}"
        if Utils.cache.read(key)
          Rails.logger.warn "APIv2 auth failed: used tonce. token: #{token.access_key} payload: #{payload} tonce: #{tonce}"
          raise TonceUsedError.new(token.access_key, tonce)
        end
        Utils.cache.write key, tonce, 61 # forget after 61 seconds

        now = Time.now.to_i*1000
        if tonce < now-30000 || tonce > now+30000 # within 30 seconds
          Rails.logger.warn "APIv2 auth failed: invalid tonce. token: #{token.access_key} payload: #{payload} tonce: #{tonce} current timestamp: #{now}"
          raise InvalidTonceError.new(tonce, now)
        end
      end

View on GitHub (pinned to dab8641137)

Solutions

  1. Recompute the payload exactly as the server does: "#{method}|/api/v2#{path}|#{URI.unescape(params.reject { |k,_| %w[signature format route_info].include?(k) }.to_query)}" and sign it with OpenSSL::HMAC.hexdigest('SHA256', secret_key, payload)
  2. Confirm the access_key and secret_key are a matched pair (regenerate the token in the UI if in doubt) and that the secret carries no stray whitespace or newline
  3. Compare the payload string you signed with the one the server logs in 'APIv2 auth failed: signature doesn't match ... payload: <payload>' and diff them param by param
  4. On any retry, regenerate tonce AND signature: check_tonce! already consumed the tonce even though the signature step failed

Example fix

# before — signs params in insertion order and URL-escaped form
query = URI.encode_www_form(params)
payload = [method, path, query].join('|')
params[:signature] = OpenSSL::HMAC.hexdigest('SHA256', secret, payload)

# after — matches APIv2::Auth::Authenticator#payload exactly
canon = params.except(:signature, :format).to_query            # Rails sorts keys
payload = "#{method}|/api/v2#{path}|#{URI.unescape(canon)}"
params[:signature] = OpenSSL::HMAC.hexdigest('SHA256', secret_key, payload)
Defensive patterns

Strategy: validation

Validate before calling

# Client-side: recompute the signature the server expects before sending
def signed_params(method, path, params, secret_key)
  canon = params.except('signature', 'format', 'route_info').to_query  # sorted k=v&...
  payload = "#{method}|/api/v2#{path}|#{URI.unescape(canon)}"
  params.merge(signature: OpenSSL::HMAC.hexdigest('SHA256', secret_key, payload))
end
# if you cannot reproduce Utils.hmac_signature byte-for-byte, fix canonicalization BEFORE sending

Try / catch

Catch the 2005 response body, log the exact payload string you signed, and treat it as non-retryable until the canonical string or the key pair is corrected; any resend must use a fresh tonce and a re-computed signature (the failed attempt already consumed the tonce).

Prevention

When it happens

Trigger: Any signed APIv2 call whose canonical string differs from the server's: including 'signature' or 'format' in the signed params, signing the URL-escaped query instead of the unescaped one (or vice versa), sending params in a different order or encoding than Rails to_query produces, signing the wrong path (missing the /api/v2 prefix) or wrong HTTP verb, or HMACing with a secret key that is not the pair of the sent access_key.

Common situations: Secret key copied with a trailing newline or whitespace; a client library that sorts/encodes query params differently from Rails to_query; endpoint paths or verbs changed during an API upgrade while signing code stayed the same; the token pair was regenerated server-side but the client still holds the old secret in its env/config.

Related errors


AI-assisted analysis of hpyhacking/peatio@dab8641137 (2026-08-23). Data as JSON: /api/errors/65c629bcda8ff60a. Report an issue: GitHub.