阿里云生成签名问题

troubles generating signature for alibaba cloud

正在阅读HTTP API docs。我的请求因签名错误而失败。从错误消息中我可以看到我要签名的字符串是正确的,但看起来我无法生成正确的 HMAC-SHA1(说真的,为什么还要使用 SHA1??)。

所以我决定尝试在同一文档中复制样本的签名。

[47] pry(main)> to_sign = "GET&%2F&AccessKeyId%3Dtestid&Action%3DDescribeRegions&Format%3DXML&SignatureMethod%3DHMAC-SHA1&SignatureNonce%3D3ee8c1b8-83d3-44af-a94f-4e0ad82fd6cf&SignatureVersion%3D1.0&Timestamp%3D2016-02-23T12%253A46%253A24Z&Version%3D2014-05-26"

[48] pry(main)> Base64.encode64 OpenSSL::HMAC.digest("sha1", "testsecret", to_sign)
=> "MLAxpXej4jJ7TL0smgWpOgynR7s=\n"

[49] pry(main)> Base64.encode64 OpenSSL::HMAC.digest("sha1", "testsecret&", to_sign)
=> "VyBL52idtt+oImX0NZC+2ngk15Q=\n"

[50] pry(main)> Base64.encode64 OpenSSL::HMAC.hexdigest("sha1", "testsecret&", to_sign)
=> "NTcyMDRiZTc2ODlkYjZkZmE4MjI2NWY0MzU5MGJlZGE3ODI0ZDc5NA==\n"

[51] pry(main)> Base64.encode64 OpenSSL::HMAC.hexdigest("sha1", "testsecret", to_sign)
=> "MzBiMDMxYTU3N2EzZTIzMjdiNGNiZDJjOWEwNWE5M2EwY2E3NDdiYg==\n"

[52] pry(main)> OpenSSL::HMAC.hexdigest("sha1", "testsecret&", to_sign)
=> "57204be7689db6dfa82265f43590beda7824d794"

[53] pry(main)> OpenSSL::HMAC.hexdigest("sha1", "testsecret", to_sign)
=> "30b031a577a3e2327b4cbd2c9a05a93a0ca747bb"

很明显,其中 none 与 CT9X0VtwR86fNWSnsc6v8YGOjuE= 的示例签名匹配。知道这里缺少什么吗?

更新: 从 Golang 客户端工具获取 tcpdump 我看到它做了一个 POST 请求,如:

POST /?AccessKeyId=**********&Action=DescribeRegions&Format=JSON&RegionId=cn-qingdao&Signature=aHZVpIMb0%2BFKdoWSIVaFJ7bd2LA%3D&SignatureMethod=HMAC-SHA1&SignatureNonce=c29a0e28964c470a8997aebca4848b57&SignatureType=&SignatureVersion=1.0&Timestamp=2018-07-16T19%3A46%3A33Z&Version=2014-05-26 HTTP/1.1

    Host: ecs.aliyuncs.com
    User-Agent: Aliyun-CLI-V3.0.3
    Content-Length: 0
    Content-Type: application/x-www-form-urlencoded
    x-sdk-client: golang/1.0.0
    x-sdk-core-version: 0.0.1
    x-sdk-invoke-type: common
    Accept-Encoding: gzip

当我从上述请求中获取参数并生成签名时,它 匹配。所以我尝试了所有树:GETPOST 和 URL 参数以及 POST 和正文中的参数。每次我收到签名错误。如果我使用与 golang 工具完全相同的参数重做请求,我会收到 nonce already used 错误(如预期的那样)。

  • 好像是这个aliyun ruby sdk (non official, just for reference) works. You may want to check how it's implemented.
  • 检查其 string_to_sign 的外观。我做了一个 运行,它似乎与您提供的略有不同。参数与 & 而不是 %26 连接。 GET&%2F&AccessKeyId%3Dtestid&Action%3DDescribeRegions&Format%3DXML&SignatureMethod%3DHMAC-SHA1&SignatureNonce%3D3ee8c1b8-83d3-44af-a94f-4e0ad82fd6cf&SignatureVersion%3D1.0&Timestamp%3D2016-02-23T12%253A46%253A24Z&Version%3D2014-05-26
    require 'rubygems'
    require 'aliyun'

    $DEBUG = true

    options = {
      :access_key_id => "k",
      :access_key_secret => "s",
      :service => :ecs
    }

    service = Aliyun::Service.new options

    puts service.DescribeRegions({})

终于成功了。在我的案例中,主要问题是我一直在对签名参数进行双百分比编码,因此它被证明是无效的。对我帮助最大的是 运行 aliyun cli 实用程序和捕获流量,然后是 运行 具有完全相同参数的查询以比较确切的查询字符串。

但让我为我列出一些要点:

  1. 生成 hmac-sha1 sig 后,不要对其进行百分号编码,只需将其以正常形式的 www 编码添加到查询中即可
  2. HTTP 查询中参数的顺序并不重要;签名字符串中的参数顺序 虽然重要
  3. 我发现以下所有类型的请求都有效:GET,POST 参数在 URL 查询中,POST 参数在请求正文形式的 www 编码中;我在每个文档中使用 GET,但我看到 aliyun 使用 POST 与查询参数和查询中的有序参数
  4. 生成 HMAC-SHA1 时必须在密钥末​​尾添加 & 个字符
  5. 以二进制形式生成 HMAC-SHA1,然后编码为 Base64(无十六进制值)
  6. 一些参数可能不区分大小写,例如Format 同时作为 jsonJSON
  7. 我看到 aliyun、@wanghq 和 John 将 UUID 4 用于 SignatureNonce,但我推迟到纯随机(根据文档),因为它似乎只是一个重播攻击保护。所以加密安全的随机数一定是不必要的。
  8. +*~的特殊编码规则似乎只适用于签名字符串,实际上并没有在HTTP查询中以这种方式对数据进行编码。

我决定不使用@wanghq 的包装器,因为它对我也不起作用disables certificate validation,但也许它会被修复。只是我认为一旦签名被弄清楚,查询就足够简单了,额外的间接层是不值得的。对他的回答 +1,因为这有助于我的签名正确。

下面是示例 ruby 代码来发出一个简单的请求:

require 'base64'
require 'cgi'
require 'openssl'
require 'time'
require 'rest-client'

# perform a request against Alibaba Cloud API
# @see https://www.alibabacloud.com/help/doc-detail/25489.htm
def request(action:, params: {})
  api_url = "https://ecs.aliyuncs.com/"

  # method = "POST"
  method = "GET"
  process_params!(http: method, action: action, params: params)
  RestClient::Request.new(method: method, url: api_url, headers: {params: params})
  # RestClient::Request.new(method: method, url: api_url, payload: params)
  # RestClient::Request.new(method: method, url: api_url, payload: params.map{|k,v| "#{k}=#{CGI.escape(v)}"}.join("&"))
end

# generates the required common params for a request and adds them to params
# @return undefined
# @see https://www.alibabacloud.com/help/doc-detail/25490.htm
def process_params!(http:, action:, params:)
  params.merge!({
    "Action" => action,
    "AccessKeyId" => config[:auth][:key_id],
    "Format" => "JSON",
    "Version" => "2014-05-26",
    "Timestamp" => Time.now.utc.iso8601
  })
  sign!(http: http, action: action, params: params)
end

# generate request signature and adds to params
# @return undefined
# @see https://www.alibabacloud.com/help/doc-detail/25492.htm
def sign!(http:, action:, params:)
  params.delete "Signature"
  params["SignatureMethod"] = "HMAC-SHA1"
  params["SignatureVersion"] = "1.0"
  params["SignatureNonce"] = "#{rand(1_000_000_000_000)}"
  # params["SignatureNonce"] = SecureRandom.uuid.gsub("-", "")

  canonicalized_query_string = params.sort.map { |key, value|
    "#{key}=#{percent_encode value}"
  }.join("&")

  string_to_sign = %{#{http}&#{percent_encode("/")}&#{percent_encode(canonicalized_query_string)}}

  params["Signature"] = hmac_sha1(string_to_sign)
end

# @param data [String]
# @return [String]
def hmac_sha1(data, secret: config[:auth][:key_secret])
  Base64.encode64(OpenSSL::HMAC.digest('sha1', "#{secret}&", data)).strip
end

# encode strings per Alibaba cloud rules for signing
# @return [String] encoded string
# @see https://www.alibabacloud.com/help/doc-detail/25492.htm
def percent_encode(str)
  CGI.escape(str).gsub(?+, "%20").gsub(?*, "%2A").gsub("%7E", ?~)
end

## example call
request(action: "DescribeRegions")

代码可以稍微简化,但决定使其尽可能接近文档说明。

P.S。不知道为什么 John 删除了他的答案,但在他的网页上方留下 link 供任何 python 寻找示例代码的人使用