You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@apisix.apache.org by me...@apache.org on 2020/09/18 12:25:51 UTC

[apisix] branch master updated: feat: `hmac-auth` add signed headers to calculate signature (#2239)

This is an automated email from the ASF dual-hosted git repository.

membphis pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/apisix.git


The following commit(s) were added to refs/heads/master by this push:
     new 0a4bc95  feat: `hmac-auth` add signed headers to  calculate signature (#2239)
0a4bc95 is described below

commit 0a4bc95e3d705ba7a7bb9cf077649f020121131d
Author: nic-chen <33...@users.noreply.github.com>
AuthorDate: Fri Sep 18 20:25:41 2020 +0800

    feat: `hmac-auth` add signed headers to  calculate signature (#2239)
    
    Co-authored-by: YuanSheng Wang <me...@gmail.com>
---
 apisix/plugins/hmac-auth.lua   |  98 ++++++++++++++++-----
 doc/plugins/hmac-auth.md       |  19 ++--
 doc/zh-cn/plugins/hmac-auth.md |  19 ++--
 t/APISIX.pm                    |   1 +
 t/plugin/custom_hmac_auth.t    |  11 ++-
 t/plugin/hmac-auth.t           | 194 ++++++++++++++++++++++++++++++++++++++---
 6 files changed, 298 insertions(+), 44 deletions(-)

diff --git a/apisix/plugins/hmac-auth.lua b/apisix/plugins/hmac-auth.lua
index ad578c4..33c1ddf 100644
--- a/apisix/plugins/hmac-auth.lua
+++ b/apisix/plugins/hmac-auth.lua
@@ -34,12 +34,18 @@ local SIGNATURE_KEY = "X-HMAC-SIGNATURE"
 local ALGORITHM_KEY = "X-HMAC-ALGORITHM"
 local TIMESTAMP_KEY = "X-HMAC-TIMESTAMP"
 local ACCESS_KEY    = "X-HMAC-ACCESS-KEY"
+local SIGNED_HEADERS_KEY = "X-HMAC-SIGNED-HEADERS"
 local plugin_name   = "hmac-auth"
 
 local schema = {
     type = "object",
     oneOf = {
         {
+            title = "work with route or service object",
+            properties = {},
+            additionalProperties = false,
+        },
+        {
             title = "work with consumer object",
             properties = {
                 access_key = {type = "string", minLength = 1, maxLength = 256},
@@ -52,16 +58,19 @@ local schema = {
                 clock_skew = {
                     type = "integer",
                     default = 300
-                }
+                },
+                signed_headers = {
+                    type = "array",
+                    items = {
+                        type = "string",
+                        minLength = 1,
+                        maxLength = 50,
+                    }
+                },
             },
             required = {"access_key", "secret_key"},
             additionalProperties = false,
         },
-        {
-            title = "work with route or service object",
-            properties = {},
-            additionalProperties = false,
-        }
     }
 }
 
@@ -101,6 +110,16 @@ local function try_attr(t, ...)
 end
 
 
+local function array_to_map(arr)
+    local map = core.table.new(0, #arr)
+    for _, v in ipairs(arr) do
+      map[v] = true
+    end
+
+    return map
+end
+
+
 local create_consumer_cache
 do
     local consumer_ids = {}
@@ -182,13 +201,29 @@ local function generate_signature(ctx, secret_key, params)
         canonical_query_string = core.table.concat(query_tab, "&")
     end
 
-    local req_body = core.request.get_body()
-    req_body = req_body or ""
+    local canonical_headers = {}
+
+    core.log.info("all headers: ",
+                  core.json.delay_encode(core.request.headers(ctx), true))
+
+    if params.signed_headers then
+        for _, h in ipairs(params.signed_headers) do
+            local canonical_header = core.request.header(ctx, h) or ""
+            core.table.insert(canonical_headers, canonical_header)
+            core.log.info("canonical_header name:", core.json.delay_encode(h))
+            core.log.info("canonical_header value: ",
+                          core.json.delay_encode(canonical_header))
+        end
+    end
 
     local signing_string = request_method .. canonical_uri
-                            .. canonical_query_string .. req_body
+                            .. canonical_query_string
                             .. params.access_key .. params.timestamp
-                            .. secret_key
+                            .. core.table.concat(canonical_headers, "")
+
+    core.log.info("signing_string:", signing_string,
+                  " params.signed_headers:",
+                  core.json.delay_encode(params.signed_headers))
 
     return hmac_funcs[params.algorithm](secret_key, signing_string)
 end
@@ -209,21 +244,33 @@ local function validate(ctx, params)
         return nil, {message = "algorithm " .. params.algorithm .. " not supported"}
     end
 
-    core.log.info("conf.clock_skew: ", conf.clock_skew)
+    core.log.info("clock_skew: ", conf.clock_skew)
     if conf.clock_skew and conf.clock_skew > 0 then
         local diff = abs(ngx_time() - params.timestamp)
-        core.log.info("conf.diff: ", diff)
+        core.log.info("timestamp diff: ", diff)
         if diff > conf.clock_skew then
           return nil, {message = "Invalid timestamp"}
         end
     end
 
+    -- validate headers
+    if conf.signed_headers and #conf.signed_headers >= 1 then
+        local headers_map = array_to_map(conf.signed_headers)
+        if params.signed_headers then
+            for _, header in ipairs(params.signed_headers) do
+                if not headers_map[header] then
+                    return nil, {message = "Invalid signed header " .. header}
+                end
+            end
+        end
+    end
+
     local secret_key          = conf and conf.secret_key
     local request_signature   = ngx_decode_base64(params.signature)
     local generated_signature = generate_signature(ctx, secret_key, params)
 
     core.log.info("request_signature: ", request_signature,
-        " generated_signature: ", generated_signature)
+                  " generated_signature: ", generated_signature)
 
     if request_signature ~= generated_signature then
         return nil, {message = "Invalid signature"}
@@ -235,27 +282,30 @@ end
 local function get_params(ctx)
     local params = {}
     local local_conf = core.config.local_conf()
+    local access_key = ACCESS_KEY
     local signature_key = SIGNATURE_KEY
     local algorithm_key = ALGORITHM_KEY
     local timestamp_key = TIMESTAMP_KEY
-    local access_key = ACCESS_KEY
+    local signed_headers_key = SIGNED_HEADERS_KEY
 
     if try_attr(local_conf, "plugin_attr", "hmac-auth") then
         local attr = local_conf.plugin_attr["hmac-auth"]
+        access_key = attr.access_key or access_key
         signature_key = attr.signature_key or signature_key
         algorithm_key = attr.algorithm_key or algorithm_key
         timestamp_key = attr.timestamp_key or timestamp_key
-        access_key = attr.access_key or access_key
+        signed_headers_key = attr.signed_headers_key or signed_headers_key
     end
 
-    local ak = core.request.header(ctx, access_key)
+    local app_key = core.request.header(ctx, access_key)
     local signature = core.request.header(ctx, signature_key)
     local algorithm = core.request.header(ctx, algorithm_key)
     local timestamp = core.request.header(ctx, timestamp_key)
+    local signed_headers = core.request.header(ctx, signed_headers_key)
     core.log.info("signature_key: ", signature_key)
 
     -- get params from header `Authorization`
-    if not ak then
+    if not app_key then
         local auth_string = core.request.header(ctx, "Authorization")
         if not auth_string then
             return params
@@ -263,19 +313,25 @@ local function get_params(ctx)
 
         local auth_data = ngx_re.split(auth_string, "#")
         core.log.info("auth_string: ", auth_string, " #auth_data: ",
-            #auth_data, " auth_data: ", core.json.delay_encode(auth_data))
-        if #auth_data == 5 and auth_data[1] == "hmac-auth-v1" then
-            ak = auth_data[2]
+                      #auth_data, " auth_data: ",
+                      core.json.delay_encode(auth_data))
+
+        if #auth_data == 6 and auth_data[1] == "hmac-auth-v2" then
+            app_key = auth_data[2]
             signature = auth_data[3]
             algorithm = auth_data[4]
             timestamp = auth_data[5]
+            signed_headers = auth_data[6]
         end
     end
 
-    params.access_key = ak
+    params.access_key = app_key
     params.algorithm  = algorithm
     params.signature  = signature
     params.timestamp  = timestamp or 0
+    params.signed_headers = signed_headers and ngx_re.split(signed_headers, ";")
+
+    core.log.info("params: ", core.json.delay_encode(params))
 
     return params
 end
diff --git a/doc/plugins/hmac-auth.md b/doc/plugins/hmac-auth.md
index af3c671..5d534dd 100644
--- a/doc/plugins/hmac-auth.md
+++ b/doc/plugins/hmac-auth.md
@@ -41,6 +41,7 @@ The `consumer` then adds its key to request header to verify its request.
 | secret_key      | required | none |Use as a pair with `access_key`|
 | algorithm    |  optional| hmac-sha256 |Encryption algorithm. support `hmac-sha1`, `hmac-sha256` and `hmac-sha512`|
 | clock_skew  | optional | 300 |The clock skew allowed by the signature in seconds. For example, if the time is allowed to skew by 10 seconds, then it should be set to `10`. especially, `0` means not checking timestamp.|
+| signed_headers  | optional | none |Restrict the headers that are added to the encrypted calculation. After the specified, the client request can only specify the headers within this range. When this item is empty, all the headers specified by the client request will be added to the encrypted calculation. Example:["User-Agent", "Accept-Language", "x-custom-a"]|
 
 ## How To Enable
 
@@ -54,7 +55,8 @@ curl http://127.0.0.1:9080/apisix/admin/consumers -H 'X-API-KEY: edd1c9f034335f1
         "hmac-auth": {
             "access_key": "user-key",
             "secret_key": "my-secret-key",
-            "clock_skew": 10
+            "clock_skew": 10,
+            "signed_headers": ["User-Agent", "Accept-Language", "x-custom-a"]
         }
     }
 }'
@@ -81,11 +83,12 @@ curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f13
 ## Test Plugin
 
 ### generate signature:
-The calculation formula of the signature is `signature = HMAC-SHAx-HEX(secret_key, signning_string)`. From the formula, it can be seen that in order to obtain the signature, two parameters, `SECRET_KEY` and `SIGNNING_STRING`, are required. Where secret_key is configured by the corresponding consumer, the calculation formula of `SIGNNING_STRING` is `signning_string = HTTP Method + HTTP URI + canonical_query_string + HTTP BODY + ACCESS_KEY + TIMESTAMP + SECRET_KEY`
+The calculation formula of the signature is `signature = HMAC-SHAx-HEX(secret_key, signning_string)`. From the formula, it can be seen that in order to obtain the signature, two parameters, `SECRET_KEY` and `SIGNNING_STRING`, are required. Where secret_key is configured by the corresponding consumer, the calculation formula of `SIGNNING_STRING` is `signning_string = HTTP Method + HTTP URI + canonical_query_string + access_key + timestamp + signed_headers_string`
 
 1. **HTTP Method** : Refers to the GET, PUT, POST and other request methods defined in the HTTP protocol, and must be in all uppercase.
 2. **HTTP URI** : `HTTP URI` requirements must start with "/", those that do not start with "/" need to be added, and the empty path is "/".
 3. **canonical_query_string** :`canonical_query_string` is the result of encoding the `query` in the URL (`query` is the string "key1 = valve1 & key2 = valve2" after the "?" in the URL).
+4. **signed_headers_string** :`signed_headers_string` is the result of obtaining the fields specified by the client from the request header and concatenating the strings in order.
 
 > The coding steps are as follows:
 
@@ -97,14 +100,20 @@ The calculation formula of the signature is `signature = HMAC-SHAx-HEX(secret_ke
     * When the item is in the form of key=value, the conversion formula is in the form of UriEncode(key) + "=" + UriEncode(value). Here value can be an empty string.
     * After converting each item, sort by key in lexicographic order (ASCII code from small to large), and connect them with the & symbol to generate the corresponding canonical_query_string.
 
+> The signed_headers_string generation steps are as follows:
+
+* Obtain the headers specified to be added to the calculation from the request header. For details, please refer to the placement of `SIGNED_HEADERS` in the next section `Use the generated signature to make a request attempt`.
+* Take out the headers specified by `SIGNED_HEADERS` in order from the request header, and splice them together in order. After splicing, a `signed_headers_string` is generated.
+
 ### Use the generated signature to try the request
 
-**Note: ACCESS_KEY, SIGNATURE, ALGORITHM, TIMESTAMP respectively represent the corresponding variables**
+**Note: ACCESS_KEY, SIGNATURE, ALGORITHM, TIMESTAMP, SIGNED_HEADERS respectively represent the corresponding variables**
+**Note: SIGNED_HEADERS is the headers specified by the client to join the encryption calculation, multiple separated by semicolons, Example: User-Agent;Accept-Language**
 
 * The signature information is put together in the request header `Authorization` field:
 
 ```shell
-$ curl http://127.0.0.1:9080/index.html -H 'Authorization: hmac-auth-v1# + ACCESS_KEY + # + base64_encode(SIGNATURE) + # + ALGORITHM + # + TIMESTAMP' -i
+$ curl http://127.0.0.1:9080/index.html -H 'Authorization: hmac-auth-v1# + ACCESS_KEY + # + base64_encode(SIGNATURE) + # + ALGORITHM + # + TIMESTAMP + # + SIGNED_HEADERS' -i
 HTTP/1.1 200 OK
 Content-Type: text/html
 Content-Length: 13175
@@ -119,7 +128,7 @@ Accept-Ranges: bytes
 * The signature information is separately placed in the request header:
 
 ```shell
-$ curl http://127.0.0.1:9080/index.html -H 'X-HMAC-SIGNATURE: base64_encode(SIGNATURE)' -H 'X-HMAC-ALGORITHM: ALGORITHM' -H 'X-HMAC-TIMESTAMP: TIMESTAMP' -H 'X-HMAC-ACCESS-KEY: ACCESS_KEY' -i
+$ curl http://127.0.0.1:9080/index.html -H 'X-HMAC-SIGNATURE: base64_encode(SIGNATURE)' -H 'X-HMAC-ALGORITHM: ALGORITHM' -H 'X-HMAC-TIMESTAMP: TIMESTAMP' -H 'X-HMAC-ACCESS-KEY: ACCESS_KEY' -H 'X-HMAC-SIGNED-HEADERS: SIGNED_HEADERS' -i
 HTTP/1.1 200 OK
 Content-Type: text/html
 Content-Length: 13175
diff --git a/doc/zh-cn/plugins/hmac-auth.md b/doc/zh-cn/plugins/hmac-auth.md
index 37305ee..6ac6b74 100644
--- a/doc/zh-cn/plugins/hmac-auth.md
+++ b/doc/zh-cn/plugins/hmac-auth.md
@@ -41,6 +41,8 @@
 | `secret_key`| 必须 | 无 | 与 `access_key` 配对使用。|
 | `algorithm` | 可选 | hmac-sha256 | 加密算法。目前支持 `hmac-sha1`, `hmac-sha256` 和 `hmac-sha512`。|
 | `clock_skew`| 可选 | 300 | 签名允许的时间偏移,以秒为单位的计时。比如允许时间偏移 10 秒钟,那么就应设置为 `10`。特别地,`0` 表示不对 `timestamp` 进行检查。|
+| `signed_headers` | 可选 | 无 | 限制加入加密计算的 headers ,指定后客户端请求只能在此范围内指定 headers ,此项为空时将把所有客户端请求指定的 headers 加入加密计算。如: ["User-Agent", "Accept-Language", "x-custom-a"]|
+
 
 ## 如何启用
 
@@ -54,7 +56,8 @@ curl http://127.0.0.1:9080/apisix/admin/consumers -H 'X-API-KEY: edd1c9f034335f1
         "hmac-auth": {
             "access_key": "user-key",
             "secret_key": "my-secret-key",
-            "clock_skew": 10
+            "clock_skew": 10,
+            "signed_headers": ["User-Agent", "Accept-Language", "x-custom-a"]
         }
     }
 }'
@@ -82,11 +85,12 @@ curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f13
 
 ### 签名生成公式
 
-签名的计算公式为 `signature = HMAC-SHAx-HEX(secret_key, signning_string)`,从公式可以看出,想要获得签名需要得到 `secret_key` 和 `signning_string` 两个参数。其中 `secret_key` 为对应 consumer 所配置的, `signning_string` 的计算公式为 `signning_string = HTTP Method + HTTP URI + canonical_query_string + HTTP BODY + access_key + timestamp + secret_key`
+签名的计算公式为 `signature = HMAC-SHAx-HEX(secret_key, signning_string)`,从公式可以看出,想要获得签名需要得到 `secret_key` 和 `signning_string` 两个参数。其中 `secret_key` 为对应 consumer 所配置的, `signning_string` 的计算公式为 `signning_string = HTTP Method + HTTP URI + canonical_query_string + access_key + timestamp + signed_headers_string`
 
 1. **HTTP Method**:指 HTTP 协议中定义的 GET、PUT、POST 等请求方法,必须使用全大写的形式。
 2. **HTTP URI**:要求必须以“/”开头,不以“/”开头的需要补充上,空路径为“/”。
 3. **canonical_query_string**:是对于 URL 中的 query( query 即 URL 中 ? 后面的 key1=valve1&key2=valve2 字符串)进行编码后的结果。
+4. **signed_headers_string**:是从请求头中获取客户端指定的字段,并按顺序拼接字符串的结果。
 
 > canonical_query_string 编码步骤如下:
 
@@ -97,16 +101,21 @@ curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f13
   * 当该项是 key=value 的形式时,转换公式为 url_encode(key) + "=" + url_encode(value) 的形式。这里 value 可以是空字符串。
   * 将每一项转换后,以 key 按照字典顺序( ASCII 码由小到大)排序,并使用 & 符号连接起来,生成相应的 canonical_query_string 。
 
+> signed_headers_string 生成步骤如下:
+
+* 从请求头中获取指定加入计算的 headers ,具体请参考下节 `使用生成好的签名进行请求尝试` 中的 `SIGNED_HEADERS` 放置的位置。
+* 从请求头中按顺序取出 `SIGNED_HEADERS` 指定的 headers ,并按顺序拼接起来,拼接完后就生成了 `signed_headers_string` 。
 
 
 ### 使用生成好的签名进行请求尝试
 
-**注: ACCESS_KEY,SIGNATURE,ALGORITHM,TIMESTAMP 分别代表对应的变量**
+**注: ACCESS_KEY, SIGNATURE, ALGORITHM, TIMESTAMP, SIGNED_HEADERS 分别代表对应的变量**
+**注: SIGNED_HEADERS 为客户端指定的加入加密计算的 headers ,多个以英文分号分隔如:User-Agent;Accept-Language**
 
 * 签名信息拼一起放到请求头 `Authorization` 字段中:
 
 ```shell
-$ curl http://127.0.0.1:9080/index.html -H 'Authorization: hmac-auth-v1# + ACCESS_KEY + # + base64_encode(SIGNATURE) + # + ALGORITHM + # + TIMESTAMP' -i
+$ curl http://127.0.0.1:9080/index.html -H 'Authorization: hmac-auth-v1# + ACCESS_KEY + # + base64_encode(SIGNATURE) + # + ALGORITHM + # + TIMESTAMP + # + SIGNED_HEADERS' -i
 HTTP/1.1 200 OK
 Content-Type: text/html
 Content-Length: 13175
@@ -121,7 +130,7 @@ Accept-Ranges: bytes
 * 签名信息分开分别放到请求头:
 
 ```shell
-$ curl http://127.0.0.1:9080/index.html -H 'X-HMAC-SIGNATURE: base64_encode(SIGNATURE)' -H 'X-HMAC-ALGORITHM: ALGORITHM' -H 'X-HMAC-TIMESTAMP: TIMESTAMP' -H 'X-HMAC-ACCESS-KEY: ACCESS_KEY' -i
+$ curl http://127.0.0.1:9080/index.html -H 'X-HMAC-SIGNATURE: base64_encode(SIGNATURE)' -H 'X-HMAC-ALGORITHM: ALGORITHM' -H 'X-HMAC-TIMESTAMP: TIMESTAMP' -H 'X-HMAC-ACCESS-KEY: ACCESS_KEY' -H 'X-HMAC-SIGNED-HEADERS: SIGNED_HEADERS' -i
 HTTP/1.1 200 OK
 Content-Type: text/html
 Content-Length: 13175
diff --git a/t/APISIX.pm b/t/APISIX.pm
index b7127ed..d577228 100644
--- a/t/APISIX.pm
+++ b/t/APISIX.pm
@@ -105,6 +105,7 @@ plugin_attr:
     algorithm_key: X-APISIX-HMAC-ALGORITHM
     timestamp_key: X-APISIX-HMAC-TIMESTAMP
     access_key: X-APISIX-HMAC-ACCESS-KEY
+    signed_headers_key: X-APISIX-HMAC-SIGNED-HEADERS
 _EOC_
 }
 
diff --git a/t/plugin/custom_hmac_auth.t b/t/plugin/custom_hmac_auth.t
index f52d13b..49e3287 100644
--- a/t/plugin/custom_hmac_auth.t
+++ b/t/plugin/custom_hmac_auth.t
@@ -239,8 +239,10 @@ location /t {
         local secret_key = "my-secret-key"
         local timestamp = ngx_time()
         local access_key = "my-access-key"
+        local custom_header_a = "asld$%dfasf"
+        local custom_header_b = "23879fmsldfk"
         local signing_string = "GET" .. "/hello" ..  "" ..
-        "" .. access_key .. timestamp .. secret_key
+            access_key .. timestamp .. custom_header_a .. custom_header_b
 
         local signature = hmac:new(secret_key, hmac.ALGOS.SHA256):final(signing_string)
         core.log.info("signature:", ngx_encode_base64(signature))
@@ -249,6 +251,9 @@ location /t {
         headers["X-APISIX-HMAC-ALGORITHM"] = "hmac-sha256"
         headers["X-APISIX-HMAC-TIMESTAMP"] = timestamp
         headers["X-APISIX-HMAC-ACCESS-KEY"] = access_key
+        headers["X-APISIX-HMAC-SIGNED-HEADERS"] = "x-custom-header-a;x-custom-header-b"
+        headers["x-custom-header-a"] = custom_header_a
+        headers["x-custom-header-b"] = custom_header_b
 
         local code, body = t.test('/hello',
             ngx.HTTP_GET,
@@ -331,8 +336,10 @@ location /t {
         local secret_key = "my-secret-key2"
         local timestamp = ngx_time()
         local access_key = "my-access-key2"
+        local custom_header_a = "asld$%dfasf"
+        local custom_header_b = "23879fmsldfk"
         local signing_string = "GET" .. "/hello" ..  "" ..
-        "" .. access_key .. timestamp .. secret_key
+            access_key .. timestamp .. custom_header_a .. custom_header_b
 
         ngx.sleep(2)
 
diff --git a/t/plugin/hmac-auth.t b/t/plugin/hmac-auth.t
index 772d22f..dd584dc 100644
--- a/t/plugin/hmac-auth.t
+++ b/t/plugin/hmac-auth.t
@@ -301,8 +301,11 @@ location /t {
         local secret_key = "my-secret-key"
         local timestamp = ngx_time()
         local access_key = "my-access-key"
+        local custom_header_a = "asld$%dfasf"
+        local custom_header_b = "23879fmsldfk"
+
         local signing_string = "GET" .. "/hello" ..  "" ..
-        "" .. access_key .. timestamp .. secret_key
+            access_key .. timestamp .. custom_header_a .. custom_header_b
 
         local signature = hmac:new(secret_key, hmac.ALGOS.SHA256):final(signing_string)
         core.log.info("signature:", ngx_encode_base64(signature))
@@ -311,6 +314,9 @@ location /t {
         headers["X-HMAC-ALGORITHM"] = "hmac-sha256"
         headers["X-HMAC-TIMESTAMP"] = timestamp
         headers["X-HMAC-ACCESS-KEY"] = access_key
+        headers["X-HMAC-SIGNED-HEADERS"] = "x-custom-header-a;x-custom-header-b"
+        headers["x-custom-header-a"] = custom_header_a
+        headers["x-custom-header-b"] = custom_header_b
 
         local code, body = t.test('/hello',
             ngx.HTTP_GET,
@@ -457,11 +463,14 @@ location /t {
         local secret_key = "my-secret-key2"
         local timestamp = ngx_time()
         local access_key = "my-access-key2"
-        local signing_string = "GET" .. "/hello" ..  "" ..
-        "" .. access_key .. timestamp .. secret_key
-
+        local custom_header_a = "asld$%dfasf"
+        local custom_header_b = "23879fmsldfk"
+        
         ngx.sleep(2)
 
+        local signing_string = "GET" .. "/hello" ..  "" ..
+            access_key .. timestamp .. custom_header_a .. custom_header_b
+
         local signature = hmac:new(secret_key, hmac.ALGOS.SHA256):final(signing_string)
         core.log.info("signature:", ngx_encode_base64(signature))
         local headers = {}
@@ -469,6 +478,9 @@ location /t {
         headers["X-HMAC-ALGORITHM"] = "hmac-sha256"
         headers["X-HMAC-TIMESTAMP"] = timestamp
         headers["X-HMAC-ACCESS-KEY"] = access_key
+        headers["X-HMAC-SIGNED-HEADERS"] = "x-custom-header-a;x-custom-header-b"
+        headers["x-custom-header-a"] = custom_header_a
+        headers["x-custom-header-b"] = custom_header_b
 
         local code, body = t.test('/hello',
             ngx.HTTP_GET,
@@ -508,8 +520,11 @@ location /t {
         local secret_key = "my-secret-key"
         local timestamp = ngx_time()
         local access_key = "my-access-key"
+        local custom_header_a = "asld$%dfasf"
+        local custom_header_b = "23879fmsldfk"
+
         local signing_string = "PUT" .. "/hello" ..  "" ..
-            req_body .. access_key .. timestamp .. secret_key
+            access_key .. timestamp .. custom_header_a .. custom_header_b
 
         local signature = hmac:new(secret_key, hmac.ALGOS.SHA256):final(signing_string)
         core.log.info("signature:", ngx_encode_base64(signature))
@@ -518,6 +533,9 @@ location /t {
         headers["X-HMAC-ALGORITHM"] = "hmac-sha256"
         headers["X-HMAC-TIMESTAMP"] = timestamp
         headers["X-HMAC-ACCESS-KEY"] = access_key
+        headers["X-HMAC-SIGNED-HEADERS"] = "x-custom-header-a;x-custom-header-b"
+        headers["x-custom-header-a"] = custom_header_a
+        headers["x-custom-header-b"] = custom_header_b
 
         local code, body = t.test('/hello',
             ngx.HTTP_PUT,
@@ -549,23 +567,29 @@ location /t {
         local hmac = require("resty.hmac")
         local ngx_encode_base64 = ngx.encode_base64
 
-        local data = {cert = ssl_cert, key = ssl_key, sni = "test.com"}
+        local data = {cert = "ssl_cert", key = "ssl_key", sni = "test.com"}
         local req_body = core.json.encode(data)
         req_body = req_body or ""
 
         local secret_key = "my-secret-key"
         local timestamp = ngx_time()
         local access_key = "my-access-key"
-        local signing_string = "PUT" .. "/hello" ..  "" ..
-        req_body .. access_key .. timestamp .. secret_key
+        local custom_header_a = "asld$%dfasf"
+        local custom_header_b = "23879fmsldfk"
 
+        local signing_string = "PUT" .. "/hello" ..  "" ..
+            access_key .. timestamp .. custom_header_a .. custom_header_b
+        core.log.info("signing_string:", signing_string)
         local signature = hmac:new(secret_key, hmac.ALGOS.SHA256):final(signing_string)
         core.log.info("signature:", ngx_encode_base64(signature))
-        local auth_string = "hmac-auth-v1#" .. access_key .. "#" .. ngx_encode_base64(signature) .. "#" ..
-        "hmac-sha256#" .. timestamp
+        local auth_string = "hmac-auth-v2#" .. access_key .. "#" .. ngx_encode_base64(signature) .. "#" ..
+        "hmac-sha256#" .. timestamp .. "#x-custom-header-a;x-custom-header-b"
+        
         local headers = {}
         headers["Authorization"] = auth_string
-
+        headers["x-custom-header-a"] = custom_header_a
+        headers["x-custom-header-b"] = custom_header_b        
+        
         local code, body = t.test('/hello',
             ngx.HTTP_PUT,
             req_body,
@@ -594,3 +618,151 @@ GET /hello
 {"message":"access key or signature missing"}
 --- no_error_log
 [error]
+
+
+
+=== TEST 19: add consumer with signed_headers
+--- config
+    location /t {
+        content_by_lua_block {
+            local t = require("lib.test_admin").test
+            local code, body = t('/apisix/admin/consumers',
+                ngx.HTTP_PUT,
+                [[{
+                    "username": "cook",
+                    "plugins": {
+                        "hmac-auth": {
+                            "access_key": "my-access-key5",
+                            "secret_key": "my-secret-key5",
+                            "signed_headers": ["x-custom-header-a", "x-custom-header-b"]
+                        }
+                    }
+                }]],
+                [[{
+                    "node": {
+                        "value": {
+                            "username": "cook",
+                            "plugins": {
+                                "hmac-auth": {
+                                    "access_key": "my-access-key5",
+                                    "secret_key": "my-secret-key5",
+                                    "algorithm": "hmac-sha256",
+                                    "clock_skew": 300,
+                                    "signed_headers": ["x-custom-header-a", "x-custom-header-b"]
+                                }
+                            }
+                        }
+                    },
+                    "action": "set"
+                }]]
+                )
+
+            ngx.status = code
+            ngx.say(body)
+        }
+    }
+--- request
+GET /t
+--- response_body
+passed
+--- no_error_log
+[error]
+
+
+
+=== TEST 20: verify with invalid signed header
+--- config
+location /t {
+    content_by_lua_block {
+        local ngx_time   = ngx.time
+        local core = require("apisix.core")
+        local t = require("lib.test_admin")
+        local hmac = require("resty.hmac")
+        local ngx_encode_base64 = ngx.encode_base64
+
+        local secret_key = "my-secret-key5"
+        local timestamp = ngx_time()
+        local access_key = "my-access-key5"
+        local custom_header_a = "asld$%dfasf"
+        local custom_header_c = "23879fmsldfk"
+
+        local signing_string = "GET" .. "/hello" ..  "" ..
+            access_key .. timestamp .. custom_header_a .. custom_header_c
+
+        local signature = hmac:new(secret_key, hmac.ALGOS.SHA256):final(signing_string)
+        core.log.info("signature:", ngx_encode_base64(signature))
+        local headers = {}
+        headers["X-HMAC-SIGNATURE"] = ngx_encode_base64(signature)
+        headers["X-HMAC-ALGORITHM"] = "hmac-sha256"
+        headers["X-HMAC-TIMESTAMP"] = timestamp
+        headers["X-HMAC-ACCESS-KEY"] = access_key
+        headers["X-HMAC-SIGNED-HEADERS"] = "x-custom-header-a;x-custom-header-c"
+        headers["x-custom-header-a"] = custom_header_a
+        headers["x-custom-header-c"] = custom_header_c
+
+        local code, body = t.test('/hello',
+            ngx.HTTP_GET,
+            "",
+            nil,
+            headers
+        )
+
+        ngx.status = code
+        ngx.say(body)
+    }
+}
+--- request
+GET /t
+--- error_code: 401
+--- response_body eval
+qr/\{"message":"Invalid signed header x-custom-header-c"\}/
+--- no_error_log
+[error]
+
+
+
+=== TEST 21: verify ok with signed headers
+--- config
+location /t {
+    content_by_lua_block {
+        local ngx_time   = ngx.time
+        local core = require("apisix.core")
+        local t = require("lib.test_admin")
+        local hmac = require("resty.hmac")
+        local ngx_encode_base64 = ngx.encode_base64
+
+        local secret_key = "my-secret-key5"
+        local timestamp = ngx_time()
+        local access_key = "my-access-key5"
+        local custom_header_a = "asld$%dfasf"
+
+        local signing_string = "GET" .. "/hello" ..  "" ..
+            access_key .. timestamp .. custom_header_a
+
+        local signature = hmac:new(secret_key, hmac.ALGOS.SHA256):final(signing_string)
+        core.log.info("signature:", ngx_encode_base64(signature))
+        local headers = {}
+        headers["X-HMAC-SIGNATURE"] = ngx_encode_base64(signature)
+        headers["X-HMAC-ALGORITHM"] = "hmac-sha256"
+        headers["X-HMAC-TIMESTAMP"] = timestamp
+        headers["X-HMAC-ACCESS-KEY"] = access_key
+        headers["X-HMAC-SIGNED-HEADERS"] = "x-custom-header-a"
+        headers["x-custom-header-a"] = custom_header_a
+
+        local code, body = t.test('/hello',
+            ngx.HTTP_GET,
+            "",
+            nil,
+            headers
+        )
+
+        ngx.status = code
+        ngx.say(body)
+    }
+}
+--- request
+GET /t
+--- response_body
+passed
+--- no_error_log
+[error]