使用 Coinbase API (Elixir) 生成有效 HMAC 签名时出现问题
Problems generating valid HMAC signature with Coinbase API (Elixir)
问题陈述:
当使用 Coinbase API 进行身份验证时,我收到此响应:
body: "{\"errors\":[{\"id\":\"authentication_error\",\"message\":\"invalid signature\"}]}"
目前的来源:
(对一般 Elixir 风格的反馈也很感激,这是我使用该语言的第一个项目)
defmodule Request do
defstruct(
method: "",
path: "",
base: "",
body: "",
timestamp: nil,
key: nil,
secret: nil,
signature: nil
)
require HTTPotion
require Poison
def new(method, path, body, key, secret, server_time) do
if !(Enum.member? [:GET, :POST, :PUT, :PATCH, :DELETE], method), do: raise ArgumentError, message: "Unsupported HTTP method #{method}"
base_url = "https://api.coinbase.com/v2"
request =
%Request{
method: method,
path: path,
body: body,
base: base_url,
key: key,
secret: secret,
timestamp: server_time,
signature: nil,
}
Request.sign(request)
end
def sign(request) do ## See https://docs.pro.coinbase.com/?ruby#signing-a-message
pre_hash =
Integer.to_string(request.timestamp) <>
Atom.to_string(request.method) <>
request.base <> request.path <> ## I've tried both with the path ("/accounts"), with the API version "/v2/accounts", and the full path ("https://")
request.body
## See note on what I've tried for variations on this bit:
decoded_secret = Base.decode64!(request.secret) ## Says to do this in the pro docs, but not in the normal ones. I've tried both ways.
signature = :crypto.hmac(:sha256, decoded_secret, pre_hash) |>
Base.encode16(case: :lower) |> ## Suggested in linked question. I've tried both with and without.
Base.encode64
%Request{request | signature: signature}
end
def send!(request) do
payload = [
body: request.body,
follow_redirects: true,
headers:
[
"CB-ACCESS-KEY": request.key,
"CB-ACCESS-SIGN": request.signature,
"CB-ACCESS-TIMESTAMP": request.timestamp,
"CB-VERSION": "2019-09-18",
"Content-Type": "application/json",
]
]
case request.method do
:GET ->
HTTPotion.get request.base <> request.path, payload
## ...
_ ->
raise "Unrecognized HTTP verb '#{request.method}'"
end
end
def server_time do
response = Poison.decode! HTTPotion.get("https://api.coinbase.com/v2/time").body
response["data"]["epoch"]
end
end
我使用的是:
iex(#)> request = Request.new(:GET, "/accounts", "", key, secret, Request.server_time)
iex(#)> request |> Request.send!
...
...
...
status_code: 401
}
iex(#)> request
%Request{
base: "https://api.coinbase.com/v2",
body: "",
key: "MY-KEY",
method: :GET,
path: "/accounts",
secret: "MY-SECRET",
signature: "ZTNjYWzEZjVjNTMxDOgzZjA5NGNjNzZkMWFiTKkwOIG0NGM1MzBjYmNmNzNhYzcyZGIxMmFhMTA0NTRjMWJjYg==", ## Not the real signature
timestamp: 1571800107
}
到目前为止我已经尝试过:
- Base64解码的秘密(提示在pro docs)
- Base16 编码(和小写)在 Base64 编码之前的签名,如本
中所建议
- 使用完整路径
"https://api.coinbase.com/v2/accounts"
- 仅使用资源路径:
/accounts
- (根据评论编辑):还尝试了
/v2/accounts
和 /v2/accounts/
- 路径等的许多变化
我做错了什么?
编辑:
来自pro docs:
Remember to first base64-decode the alphanumeric secret string (resulting in 64 bytes) before using it as the key for HMAC. Also, base64-encode the > digest output before sending in the header.
(强调我的)
我注意到我的 decoded_secret 的 byte_size/1
最后只有 24 个字节:
decoded_secret = Base.decode64!(request.secret)
IO.puts byte_size(decoded_secret) # => 24
不是文档指定的 64。仍在深入研究。
要生成有效签名:
def sign(method, path, body \ "", timestamp, secret) do
message = generate_message(timestamp, method, path, body)
_signature =
:crypto.mac(:hmac, :sha256, secret, message)
|> Base.encode16(case: :lower)
end
def generate_message(timestamp, method, path, body \ "") do
"#{timestamp}#{method}#{path}#{body}"
end
可以通过从官方 ruby 和 python 客户端生成各种签名来生成有效的可验证签名数据。
使用此处提供的数据,可以生成以下测试以确保签名正确:
describe "sign/5" do
test "produces the correct hash with the test data" do
secret = "bar"
timestamp = 1636971273
method = "GET"
path = "/zork"
body = "{'quux': 'zyzx'}"
signature = Sign.sign(method, path, body, timestamp, secret)
assert signature == "6aed30a898d9c87ef9f652d81e49464c65ff9406801e7edd238febe959f58dca"
end
test "produces the correct signature length with the test data" do
secret_fun = "bar"
timestamp = 1636971273
method = "GET"
path = "/zork"
body = "{'quux': 'zyzx'}"
signature = Sign.sign(method, path, body, timestamp, secret)
assert byte_size(signature) == 64
end
end
鉴于测试数据中定义的秘密、时间戳、方法、路径和正文,上述哈希是正确的。
已发布工作实现 on hex。
以下对我有用:
def sign(secret, timestamp, method, path, body) do
message = "#{timestamp}#{method}#{path}#{body}"
_signature =
:crypto.mac(:hmac, :sha256, Base.decode64!(secret), message)
|> Base.encode64()
end
问题陈述:
当使用 Coinbase API 进行身份验证时,我收到此响应:
body: "{\"errors\":[{\"id\":\"authentication_error\",\"message\":\"invalid signature\"}]}"
目前的来源:
(对一般 Elixir 风格的反馈也很感激,这是我使用该语言的第一个项目)
defmodule Request do
defstruct(
method: "",
path: "",
base: "",
body: "",
timestamp: nil,
key: nil,
secret: nil,
signature: nil
)
require HTTPotion
require Poison
def new(method, path, body, key, secret, server_time) do
if !(Enum.member? [:GET, :POST, :PUT, :PATCH, :DELETE], method), do: raise ArgumentError, message: "Unsupported HTTP method #{method}"
base_url = "https://api.coinbase.com/v2"
request =
%Request{
method: method,
path: path,
body: body,
base: base_url,
key: key,
secret: secret,
timestamp: server_time,
signature: nil,
}
Request.sign(request)
end
def sign(request) do ## See https://docs.pro.coinbase.com/?ruby#signing-a-message
pre_hash =
Integer.to_string(request.timestamp) <>
Atom.to_string(request.method) <>
request.base <> request.path <> ## I've tried both with the path ("/accounts"), with the API version "/v2/accounts", and the full path ("https://")
request.body
## See note on what I've tried for variations on this bit:
decoded_secret = Base.decode64!(request.secret) ## Says to do this in the pro docs, but not in the normal ones. I've tried both ways.
signature = :crypto.hmac(:sha256, decoded_secret, pre_hash) |>
Base.encode16(case: :lower) |> ## Suggested in linked question. I've tried both with and without.
Base.encode64
%Request{request | signature: signature}
end
def send!(request) do
payload = [
body: request.body,
follow_redirects: true,
headers:
[
"CB-ACCESS-KEY": request.key,
"CB-ACCESS-SIGN": request.signature,
"CB-ACCESS-TIMESTAMP": request.timestamp,
"CB-VERSION": "2019-09-18",
"Content-Type": "application/json",
]
]
case request.method do
:GET ->
HTTPotion.get request.base <> request.path, payload
## ...
_ ->
raise "Unrecognized HTTP verb '#{request.method}'"
end
end
def server_time do
response = Poison.decode! HTTPotion.get("https://api.coinbase.com/v2/time").body
response["data"]["epoch"]
end
end
我使用的是:
iex(#)> request = Request.new(:GET, "/accounts", "", key, secret, Request.server_time)
iex(#)> request |> Request.send!
...
...
...
status_code: 401
}
iex(#)> request
%Request{
base: "https://api.coinbase.com/v2",
body: "",
key: "MY-KEY",
method: :GET,
path: "/accounts",
secret: "MY-SECRET",
signature: "ZTNjYWzEZjVjNTMxDOgzZjA5NGNjNzZkMWFiTKkwOIG0NGM1MzBjYmNmNzNhYzcyZGIxMmFhMTA0NTRjMWJjYg==", ## Not the real signature
timestamp: 1571800107
}
到目前为止我已经尝试过:
- Base64解码的秘密(提示在pro docs)
- Base16 编码(和小写)在 Base64 编码之前的签名,如本
- 使用完整路径
"https://api.coinbase.com/v2/accounts"
- 仅使用资源路径:
/accounts
- (根据评论编辑):还尝试了
/v2/accounts
和/v2/accounts/
- 路径等的许多变化
我做错了什么?
编辑:
来自pro docs:
Remember to first base64-decode the alphanumeric secret string (resulting in 64 bytes) before using it as the key for HMAC. Also, base64-encode the > digest output before sending in the header.
(强调我的)
我注意到我的 decoded_secret 的 byte_size/1
最后只有 24 个字节:
decoded_secret = Base.decode64!(request.secret)
IO.puts byte_size(decoded_secret) # => 24
不是文档指定的 64。仍在深入研究。
要生成有效签名:
def sign(method, path, body \ "", timestamp, secret) do
message = generate_message(timestamp, method, path, body)
_signature =
:crypto.mac(:hmac, :sha256, secret, message)
|> Base.encode16(case: :lower)
end
def generate_message(timestamp, method, path, body \ "") do
"#{timestamp}#{method}#{path}#{body}"
end
可以通过从官方 ruby 和 python 客户端生成各种签名来生成有效的可验证签名数据。
使用此处提供的数据,可以生成以下测试以确保签名正确:
describe "sign/5" do
test "produces the correct hash with the test data" do
secret = "bar"
timestamp = 1636971273
method = "GET"
path = "/zork"
body = "{'quux': 'zyzx'}"
signature = Sign.sign(method, path, body, timestamp, secret)
assert signature == "6aed30a898d9c87ef9f652d81e49464c65ff9406801e7edd238febe959f58dca"
end
test "produces the correct signature length with the test data" do
secret_fun = "bar"
timestamp = 1636971273
method = "GET"
path = "/zork"
body = "{'quux': 'zyzx'}"
signature = Sign.sign(method, path, body, timestamp, secret)
assert byte_size(signature) == 64
end
end
鉴于测试数据中定义的秘密、时间戳、方法、路径和正文,上述哈希是正确的。
已发布工作实现 on hex。
以下对我有用:
def sign(secret, timestamp, method, path, body) do
message = "#{timestamp}#{method}#{path}#{body}"
_signature =
:crypto.mac(:hmac, :sha256, Base.decode64!(secret), message)
|> Base.encode64()
end