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

nic-6443 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 80b94960c fix(aws-lambda): build SigV4 canonical query string per spec 
(#13520)
80b94960c is described below

commit 80b94960c5d35e0d39846bbd938574cd9d21b1e2
Author: Nic <[email protected]>
AuthorDate: Fri Jun 12 10:49:56 2026 +0800

    fix(aws-lambda): build SigV4 canonical query string per spec (#13520)
---
 apisix/plugins/aws-lambda.lua |  51 ++++++++++++++--
 t/plugin/aws-lambda.t         | 138 ++++++++++++++++++++++++++++++++++++++++++
 2 files changed, 183 insertions(+), 6 deletions(-)

diff --git a/apisix/plugins/aws-lambda.lua b/apisix/plugins/aws-lambda.lua
index 9897ffbdb..c119fd950 100644
--- a/apisix/plugins/aws-lambda.lua
+++ b/apisix/plugins/aws-lambda.lua
@@ -21,6 +21,10 @@ local resty_sha256 = require("resty.sha256")
 local str_strip = require("pl.stringx").strip
 local norm_path = require("pl.path").normpath
 local pairs = pairs
+local ipairs = ipairs
+local type = type
+local str_format = string.format
+local str_byte = string.byte
 local tab_concat = table.concat
 local tab_sort = table.sort
 local os = os
@@ -43,6 +47,14 @@ local function sha256(msg)
     return hex_encode(digest)
 end
 
+-- URI-encode a string per the AWS SigV4 spec: percent-encode every character
+-- except the RFC3986 unreserved characters A-Z, a-z, 0-9, "-", "_", "." and 
"~"
+local function uri_encode(s)
+    return (s:gsub("[^A-Za-z0-9%-_.~]", function(c)
+        return str_format("%%%02X", str_byte(c))
+    end))
+end
+
 local function get_signature_key(key, datestamp, region, service)
     local kDate = hmac256("AWS4" .. key, datestamp)
     local kRegion = hmac256(kDate, region)
@@ -120,17 +132,44 @@ local function request_processor(conf, ctx, params)
         end
     end
 
-    -- computing canonical query string
-    local canonical_qs = {}
-    local canonical_qs_i = 0
+    -- computing canonical query string: URI-encode the name and value of
+    -- every pair, then sort the pairs by encoded name and encoded value.
+    -- params.query holds the percent-decoded args: a table value means the
+    -- arg appears multiple times and a true value means an arg without value
+    local query_pairs = {}
+    local query_pairs_i = 0
     for k, v in pairs(params.query) do
-        canonical_qs_i = canonical_qs_i + 1
-        canonical_qs[canonical_qs_i] = ngx.unescape_uri(k) .. "=" .. 
ngx.unescape_uri(v)
+        local name = uri_encode(k)
+        if type(v) ~= "table" then
+            v = {v}
+        end
+        for _, value in ipairs(v) do
+            if value == true then
+                value = ""
+            end
+            query_pairs_i = query_pairs_i + 1
+            query_pairs[query_pairs_i] = {name, uri_encode(value)}
+        end
     end
 
-    tab_sort(canonical_qs)
+    tab_sort(query_pairs, function(a, b)
+        if a[1] ~= b[1] then
+            return a[1] < b[1]
+        end
+        return a[2] < b[2]
+    end)
+
+    local canonical_qs = {}
+    for i = 1, query_pairs_i do
+        canonical_qs[i] = query_pairs[i][1] .. "=" .. query_pairs[i][2]
+    end
     canonical_qs = tab_concat(canonical_qs, "&")
 
+    -- send exactly the query string that gets signed: lua-resty-http passes
+    -- a string through unmodified, while a table would be re-encoded by
+    -- ngx.encode_args whose output may differ from the signed string
+    params.query = canonical_qs
+
     -- computing canonical and signed headers
 
     local canonical_headers, signed_headers = {}, {}
diff --git a/t/plugin/aws-lambda.t b/t/plugin/aws-lambda.t
index 9c072ac12..4a3ea8a6d 100644
--- a/t/plugin/aws-lambda.t
+++ b/t/plugin/aws-lambda.t
@@ -372,3 +372,141 @@ test-api-key
 secretkey encrypted: ok
 accesskey encrypted: ok
 apikey encrypted: ok
+
+
+
+=== TEST 9: IAM v4 signing with encoded, multi-value and valueless query params
+--- config
+    location /t {
+        content_by_lua_block {
+            local t = require("lib.test_admin").test
+
+            local code, body = t('/apisix/admin/routes/1',
+                 ngx.HTTP_PUT,
+                 [[{
+                        "plugins": {
+                            "aws-lambda": {
+                                "function_uri": 
"http://localhost:8765/generic";,
+                                "authorization": {
+                                    "iam": {
+                                        "accesskey": "KEY1",
+                                        "secretkey": "KeySecret"
+                                    }
+                                }
+                            }
+                        },
+                        "uri": "/aws"
+                }]]
+            )
+            if code >= 300 then
+                ngx.status = code
+                ngx.say("fail")
+                return
+            end
+
+            ngx.say(body)
+
+            -- unsorted query string with a percent-encoded key and value,
+            -- a value that needs encoding, repeated args and a valueless arg
+            local code, _, body = t(
+                "/aws?with%20space=a%2Fb%20c&multi=m2&multi=m1&flag&a=*&a-=x",
+                "GET")
+            if code >= 300 then
+                ngx.status = code
+            end
+            ngx.print(body)
+        }
+    }
+--- inside_lua_block
+-- emulate the AWS server side SigV4 validation: rebuild the canonical
+-- request from the request actually received and recompute the signature
+local hmac = require("resty.hmac")
+local resty_sha256 = require("resty.sha256")
+local hex_encode = require("resty.string").to_hex
+
+local function hmac256(key, msg)
+    return hmac:new(key, hmac.ALGOS.SHA256):final(msg)
+end
+
+local function sha256(msg)
+    local hash = resty_sha256:new()
+    hash:update(msg)
+    return hex_encode(hash:final())
+end
+
+local function uri_encode(s)
+    return (s:gsub("[^A-Za-z0-9%-_.~]", function(c)
+        return string.format("%%%02X", string.byte(c))
+    end))
+end
+
+ngx.say("query: ", ngx.var.args)
+
+local headers = ngx.req.get_headers()
+local credential, signed_headers, signature = headers["authorization"]:match(
+    "^AWS4%-HMAC%-SHA256 Credential=([^,]+), SignedHeaders=([^,]+), 
Signature=(%x+)$")
+local datestamp, region, service = credential:match(
+    "/(%d+)/([^/]+)/([^/]+)/aws4_request$")
+
+-- canonical query string: decode every pair received on the wire,
+-- then URI-encode and sort the pairs again
+local query_pairs = {}
+for pair in (ngx.var.args or ""):gmatch("[^&]+") do
+    local eq = pair:find("=", 1, true)
+    local k, v
+    if eq then
+        k, v = pair:sub(1, eq - 1), pair:sub(eq + 1)
+    else
+        k, v = pair, ""
+    end
+    table.insert(query_pairs,
+                 {uri_encode(ngx.unescape_uri(k)), 
uri_encode(ngx.unescape_uri(v))})
+end
+table.sort(query_pairs, function(a, b)
+    if a[1] ~= b[1] then
+        return a[1] < b[1]
+    end
+    return a[2] < b[2]
+end)
+local canonical_qs = {}
+for i, p in ipairs(query_pairs) do
+    canonical_qs[i] = p[1] .. "=" .. p[2]
+end
+
+local canonical_headers = {}
+local i = 0
+for name in signed_headers:gmatch("[^;]+") do
+    i = i + 1
+    local value = headers[name]:gsub("^%s+", ""):gsub("%s+$", "")
+    canonical_headers[i] = name .. ":" .. value .. "\n"
+end
+
+ngx.req.read_body()
+local canonical_request = ngx.req.get_method() .. "\n"
+    .. ngx.var.request_uri:match("^([^?]*)") .. "\n"
+    .. table.concat(canonical_qs, "&") .. "\n"
+    .. table.concat(canonical_headers) .. "\n"
+    .. signed_headers .. "\n"
+    .. sha256(ngx.req.get_body_data() or "")
+
+local string_to_sign = "AWS4-HMAC-SHA256\n"
+    .. headers["x-amz-date"] .. "\n"
+    .. datestamp .. "/" .. region .. "/" .. service .. "/aws4_request\n"
+    .. sha256(canonical_request)
+
+local sign_key = hmac256("AWS4" .. "KeySecret", datestamp)
+sign_key = hmac256(sign_key, region)
+sign_key = hmac256(sign_key, service)
+sign_key = hmac256(sign_key, "aws4_request")
+local expected = hex_encode(hmac256(sign_key, string_to_sign))
+
+if expected == signature then
+    ngx.say("signature: ok")
+else
+    ngx.say("signature mismatch: got ", signature, ", want ", expected)
+end
+
+--- response_body
+passed
+query: a=%2A&a-=x&flag=&multi=m1&multi=m2&with%20space=a%2Fb%20c
+signature: ok

Reply via email to