{"record":{"id":"65c629bcda8ff60a","repo":"hpyhacking/peatio","slug":"2005","errorCode":"2005","errorMessage":"Signature #{signature} is incorrect.","messagePattern":"Signature #(.+?) is incorrect\\.","errorType":"exception","errorClass":"APIv2::IncorrectSignatureError","httpStatus":401,"severity":"error","filePath":"app/api/api_v2/auth/authenticator.rb","lineNumber":31,"sourceCode":"        check_signature!\n        token\n      end\n\n      def token\n        @token ||= APIToken.joins(:member).where(access_key: @params[:access_key]).first\n      end\n\n      def check_token!\n        raise InvalidAccessKeyError, @params[:access_key] unless token\n        raise DisabledAccessKeyError, @params[:access_key] if token.member.api_disabled\n        raise ExpiredAccessKeyError, @params[:access_key] if token.expired?\n        raise OutOfScopeError unless token.in_scopes?(route_scopes)\n      end\n\n      def check_signature!\n        if @params[:signature] != Utils.hmac_signature(token.secret_key, payload)\n          Rails.logger.warn \"APIv2 auth failed: signature doesn't match. token: #{token.access_key} payload: #{payload}\"\n          raise IncorrectSignatureError, @params[:signature]\n        end\n      end\n\n      def check_tonce!\n        key = \"api_v2:tonce:#{token.access_key}:#{tonce}\"\n        if Utils.cache.read(key)\n          Rails.logger.warn \"APIv2 auth failed: used tonce. token: #{token.access_key} payload: #{payload} tonce: #{tonce}\"\n          raise TonceUsedError.new(token.access_key, tonce)\n        end\n        Utils.cache.write key, tonce, 61 # forget after 61 seconds\n\n        now = Time.now.to_i*1000\n        if tonce < now-30000 || tonce > now+30000 # within 30 seconds\n          Rails.logger.warn \"APIv2 auth failed: invalid tonce. token: #{token.access_key} payload: #{payload} tonce: #{tonce} current timestamp: #{now}\"\n          raise InvalidTonceError.new(tonce, now)\n        end\n      end\n","sourceCodeStart":13,"sourceCodeEnd":49,"githubUrl":"https://github.com/hpyhacking/peatio/blob/dab8641137c008928c835409342519bfef4eae7f/app/api/api_v2/auth/authenticator.rb#L13-L49","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":"# before — signs params in insertion order and URL-escaped form\nquery = URI.encode_www_form(params)\npayload = [method, path, query].join('|')\nparams[:signature] = OpenSSL::HMAC.hexdigest('SHA256', secret, payload)\n\n# after — matches APIv2::Auth::Authenticator#payload exactly\ncanon = params.except(:signature, :format).to_query            # Rails sorts keys\npayload = \"#{method}|/api/v2#{path}|#{URI.unescape(canon)}\"\nparams[:signature] = OpenSSL::HMAC.hexdigest('SHA256', secret_key, payload)","handlingStrategy":"validation","validationCode":"# Client-side: recompute the signature the server expects before sending\ndef signed_params(method, path, params, secret_key)\n  canon = params.except('signature', 'format', 'route_info').to_query  # sorted k=v&...\n  payload = \"#{method}|/api/v2#{path}|#{URI.unescape(canon)}\"\n  params.merge(signature: OpenSSL::HMAC.hexdigest('SHA256', secret_key, payload))\nend\n# if you cannot reproduce Utils.hmac_signature byte-for-byte, fix canonicalization BEFORE sending","typeGuard":null,"tryCatchPattern":"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).","preventionTips":["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"],"tags":["ruby","grape","hmac","api-auth","signature","exchange"],"backgroundTag":"hmac-signature-mismatch","analyzedSha":"dab8641137c008928c835409342519bfef4eae7f","analyzedAt":"2026-08-23T09:59:18.005Z","schemaVersion":2},"datasetVersion":"2026-08-23T13:39:53.451Z"}