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
- 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)
- 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
- 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
- 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
- Keep one shared signing function per project so param exclusion (signature, format) and unescaping never drift between call sites
- Unit-test the signer against a known payload/signature pair captured from a working curl request
- Strip whitespace from pasted secrets and store the pair together so access_key/secret_key cannot get mixed
- Log the signed payload at DEBUG level on the client; it is the fastest way to diff against the server's 'signature doesn't match' warning
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.