|
| 1 | +local helpers = require("spec.helpers") |
| 2 | +local cjson = require("cjson.safe") |
| 3 | +local openssl_hmac = require "resty.openssl.hmac" |
| 4 | +local to_hex = require"resty.string".to_hex |
| 5 | +local resty_sha256 = require "resty.sha256" |
| 6 | + |
| 7 | +local PLUGIN_NAME = "aws-request-signing" |
| 8 | + |
| 9 | +-- Create a new DNS mock and add some DNS records |
| 10 | +local fixtures = { |
| 11 | + http_mock = {}, |
| 12 | + stream_mock = {}, |
| 13 | + dns_mock = helpers.dns_mock.new() |
| 14 | +} |
| 15 | +fixtures.dns_mock:A{ |
| 16 | + name = "sts.amazonaws.com", |
| 17 | + address = "127.0.0.1" |
| 18 | +} |
| 19 | +fixtures.dns_mock:A{ |
| 20 | + name = "test2a.com", |
| 21 | + address = "127.0.0.1" |
| 22 | +} |
| 23 | + |
| 24 | +-- This block is for mocking the call to sts.amazonaws.com |
| 25 | +fixtures.http_mock.sts_server_block = [[ |
| 26 | + server { |
| 27 | + listen 443 ssl; |
| 28 | + server_name sts.amazonaws.com; |
| 29 | + ssl_certificate /kong/spec/fixtures/kong_spec.crt; |
| 30 | + ssl_certificate_key /kong/spec/fixtures/kong_spec.key; |
| 31 | + ssl_protocols TLSv1.1 TLSv1.2 TLSv1.3; |
| 32 | + location "/" { |
| 33 | + return 200 '{ |
| 34 | + "AssumeRoleWithWebIdentityResponse":{ |
| 35 | + "AssumeRoleWithWebIdentityResult":{ |
| 36 | + "Credentials":{ |
| 37 | + "AccessKeyId":"A", |
| 38 | + "SecretAccessKey":"B", |
| 39 | + "SessionToken":"C", |
| 40 | + "Expiration":1726572582 |
| 41 | + } |
| 42 | + } |
| 43 | + } |
| 44 | + }'; |
| 45 | + } |
| 46 | + } |
| 47 | +]] |
| 48 | + |
| 49 | +-- This bloc is for mocking the overrided host specified in one of the tests |
| 50 | +fixtures.http_mock.test_server_block = [[ |
| 51 | + server { |
| 52 | + listen 9443 ssl; |
| 53 | + server_name test2a.com; |
| 54 | + ssl_certificate /kong/spec/fixtures/kong_spec.crt; |
| 55 | + ssl_certificate_key /kong/spec/fixtures/kong_spec.key; |
| 56 | + ssl_protocols TLSv1.1 TLSv1.2 TLSv1.3; |
| 57 | + location /testoverride { |
| 58 | + return 200 '{"host": "$http_host", "uri": "$uri", "agent":"$http_user_agent"}'; |
| 59 | + } |
| 60 | + } |
| 61 | +]] |
| 62 | + |
| 63 | +-- These functions are used to calculate the signature for the request |
| 64 | +local function hmac(secret, data) |
| 65 | + return openssl_hmac.new(secret, "sha256"):final(data) |
| 66 | +end |
| 67 | + |
| 68 | +local function derive_signing_key(kSecret, date, region, service) |
| 69 | + local kDate = hmac("AWS4" .. kSecret, date) |
| 70 | + local kRegion = hmac(kDate, region) |
| 71 | + local kService = hmac(kRegion, service) |
| 72 | + return hmac(kService, "aws4_request") |
| 73 | +end |
| 74 | + |
| 75 | +local function hash(str) |
| 76 | + local sha256 = resty_sha256:new() |
| 77 | + sha256:update(str) |
| 78 | + return sha256:final() |
| 79 | +end |
| 80 | + |
| 81 | +local function calulate_signature(headers, method, uri) |
| 82 | + local canonical_request = method .. "\n" .. uri .. "\n\nhost:" .. headers["host"] .. "\nx-amz-content-sha256:" .. |
| 83 | + headers["x-amz-content-sha256"] .. "\nx-amz-date:" .. headers["x-amz-date"] .. |
| 84 | + "\nx-amz-security-token:" .. headers["x-amz-security-token"] .. |
| 85 | + "\n\nhost;x-amz-content-sha256;x-amz-date;x-amz-security-token\n" .. |
| 86 | + headers["x-amz-content-sha256"] |
| 87 | + local string_to_sign = "AWS4-HMAC-SHA256\n" .. headers["x-amz-date"] .. "\n" .. |
| 88 | + (string.sub(headers["x-amz-date"], 1, 8)) .. "/eu-west-1/lambda/aws4_request" .. "\n" .. |
| 89 | + to_hex(hash(canonical_request)) |
| 90 | + local signing_key = derive_signing_key("B", string.sub(headers["x-amz-date"], 1, 8), "eu-west-1", "lambda") |
| 91 | + local signature = to_hex(hmac(signing_key, string_to_sign)) |
| 92 | + return signature |
| 93 | +end |
| 94 | + |
| 95 | +-- Now orchestrate the tests |
| 96 | +for _, strategy in helpers.all_strategies() do |
| 97 | + |
| 98 | + describe("Plugin: " .. PLUGIN_NAME .. ": (access) [#" .. strategy .. "]", function() |
| 99 | + local proxy_client |
| 100 | + |
| 101 | + lazy_setup(function() |
| 102 | + local bp = helpers.get_db_utils(strategy, nil, {PLUGIN_NAME}) |
| 103 | + |
| 104 | + -- Assets for test: "should place a valid signature in headers by default" |
| 105 | + local route1 = bp.routes:insert({ |
| 106 | + hosts = {"test1.com"}, |
| 107 | + name = "route1" |
| 108 | + }) |
| 109 | + bp.plugins:insert{ |
| 110 | + name = PLUGIN_NAME, |
| 111 | + route = { |
| 112 | + id = route1.id |
| 113 | + }, |
| 114 | + config = { |
| 115 | + aws_region = "eu-west-1", |
| 116 | + aws_service = "lambda", |
| 117 | + aws_assume_role_name = "test-role-name", |
| 118 | + aws_assume_role_arn = "arn:aws:iam::123456789012:role/test-role-name" |
| 119 | + } |
| 120 | + } |
| 121 | + |
| 122 | + -- Assets for test: "should override host when configured" |
| 123 | + local service2 = bp.services:insert({ |
| 124 | + connect_timeout = 1000, |
| 125 | + name = "service2", |
| 126 | + url = "https://test2.com:6443", |
| 127 | + retries = 0 |
| 128 | + }) |
| 129 | + local route2 = bp.routes:insert({ |
| 130 | + name = "route2", |
| 131 | + paths = {"/testoverride"}, |
| 132 | + service = service2, |
| 133 | + strip_path = false |
| 134 | + }) |
| 135 | + bp.plugins:insert{ |
| 136 | + name = PLUGIN_NAME, |
| 137 | + route = { |
| 138 | + id = route2.id |
| 139 | + }, |
| 140 | + config = { |
| 141 | + aws_region = "eu-west-1", |
| 142 | + aws_service = "lambda", |
| 143 | + aws_assume_role_name = "test-role-name", |
| 144 | + aws_assume_role_arn = "arn:aws:iam::123456789012:role/test-role-name", |
| 145 | + override_target_host = "test2a.com", |
| 146 | + override_target_port = 9443 |
| 147 | + } |
| 148 | + } |
| 149 | + |
| 150 | + -- Assets for test: "should place signature information in query string when config enables it" |
| 151 | + local route3 = bp.routes:insert({ |
| 152 | + hosts = {"test3.com"}, |
| 153 | + name = "route3" |
| 154 | + }) |
| 155 | + bp.plugins:insert{ |
| 156 | + name = PLUGIN_NAME, |
| 157 | + route = { |
| 158 | + id = route3.id |
| 159 | + }, |
| 160 | + config = { |
| 161 | + aws_region = "eu-west-1", |
| 162 | + aws_service = "lambda", |
| 163 | + aws_assume_role_name = "test-role-name", |
| 164 | + aws_assume_role_arn = "arn:aws:iam::123456789012:role/test-role-name", |
| 165 | + sign_query = true |
| 166 | + } |
| 167 | + } |
| 168 | + |
| 169 | + assert(helpers.start_kong({ |
| 170 | + database = strategy, |
| 171 | + nginx_conf = "spec/fixtures/custom_nginx.template", |
| 172 | + plugins = "bundled," .. PLUGIN_NAME |
| 173 | + }, nil, nil, fixtures)) |
| 174 | + end) |
| 175 | + |
| 176 | + lazy_teardown(function() |
| 177 | + helpers.stop_kong(nil, true) |
| 178 | + end) |
| 179 | + |
| 180 | + before_each(function() |
| 181 | + proxy_client = helpers.proxy_client() |
| 182 | + end) |
| 183 | + |
| 184 | + after_each(function() |
| 185 | + if proxy_client then |
| 186 | + proxy_client:close() |
| 187 | + end |
| 188 | + end) |
| 189 | + |
| 190 | + describe("Adding signature to request", function() |
| 191 | + |
| 192 | + it("should place a valid signature in headers by default", function() |
| 193 | + local res = assert(proxy_client:send{ |
| 194 | + method = "GET", |
| 195 | + path = "/status/200", |
| 196 | + headers = { |
| 197 | + ["Host"] = "test1.com" |
| 198 | + } |
| 199 | + }) |
| 200 | + local body = assert.res_status(200, res) |
| 201 | + local json = cjson.decode(body) |
| 202 | + assert.is.truthy(json.headers["x-amz-content-sha256"]) |
| 203 | + assert.is.truthy(json.headers["x-amz-date"]) |
| 204 | + assert.is.truthy(json.headers["x-amz-security-token"]) |
| 205 | + local calculated_signature = calulate_signature(json.headers, json.vars.request_method, json.vars.uri) |
| 206 | + local _, _, signature_from_header = string.find(json.headers["authorization"], "Signature=(.*)") |
| 207 | + assert.match(calculated_signature, signature_from_header) |
| 208 | + end) |
| 209 | + |
| 210 | + it("should override host when configured", function() |
| 211 | + local res = proxy_client:get("/testoverride", { |
| 212 | + headers = { |
| 213 | + ["Host"] = "test2.com" |
| 214 | + } |
| 215 | + }) |
| 216 | + local body = assert.res_status(200, res) |
| 217 | + local json = cjson.decode(body) |
| 218 | + assert.match("test2a.com", json["host"]) |
| 219 | + end) |
| 220 | + |
| 221 | + it("should place signature information in query string when config 'sign_query' is true", function() |
| 222 | + local res = assert(proxy_client:send{ |
| 223 | + method = "GET", |
| 224 | + path = "/status/200", |
| 225 | + headers = { |
| 226 | + ["Host"] = "test3.com" |
| 227 | + } |
| 228 | + }) |
| 229 | + local body = assert.res_status(200, res) |
| 230 | + local json = cjson.decode(body) |
| 231 | + -- the x-amz-content-sha256 will still be in the header |
| 232 | + assert.is.truthy(json.headers["x-amz-content-sha256"]) |
| 233 | + -- check signature info is in the uri |
| 234 | + assert.is.truthy(json.uri_args["X-Amz-Date"]) |
| 235 | + assert.is.truthy(json.uri_args["X-Amz-Security-Token"]) |
| 236 | + assert.is.truthy(json.uri_args["X-Amz-Signature"]) |
| 237 | + -- check signature info is in the headers |
| 238 | + assert.is.falsy(json.headers["x-amz-date"]) |
| 239 | + assert.is.falsy(json.headers["x-amz-security-token"]) |
| 240 | + end) |
| 241 | + |
| 242 | + end) |
| 243 | + end) |
| 244 | +end |
0 commit comments