-- -- Licensed to the Apache Software Foundation (ASF) under one or more -- contributor license agreements. See the NOTICE file distributed with -- this work for additional information regarding copyright ownership. -- The ASF licenses this file to You under the Apache License, Version 2.0 -- (the "License"); you may not use this file except in compliance with -- the License. You may obtain a copy of the License at -- -- http://www.apache.org/licenses/LICENSE-2.0 -- -- Unless required by applicable law or agreed to in writing, software -- distributed under the License is distributed on an "AS IS" BASIS, -- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -- See the License for the specific language governing permissions and -- limitations under the License. local ngx = ngx local hmac = require("resty.hmac") local hex_encode = require("resty.string").to_hex local resty_sha256 = require("resty.sha256") local str_strip = require("pl.stringx").strip local norm_path = require("pl.path").normpath local pairs = pairs local tab_concat = table.concat local tab_sort = table.sort local os = os local plugin_name = "aws-lambda" local plugin_version = 0.1 local priority = -1899 local ALGO = "AWS4-HMAC-SHA256" 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) local digest = hash:final() return hex_encode(digest) end local function get_signature_key(key, datestamp, region, service) local kDate = hmac256("AWS4" .. key, datestamp) local kRegion = hmac256(kDate, region) local kService = hmac256(kRegion, service) local kSigning = hmac256(kService, "aws4_request") return kSigning end local aws_authz_schema = { type = "object", properties = { -- API Key based authorization apikey = {type = "string"}, -- IAM role based authorization, works via aws v4 request signing -- more at https://docs.aws.amazon.com/general/latest/gr/sigv4_signing.html iam = { type = "object", properties = { accesskey = { type = "string", description = "access key id from from aws iam console" }, secretkey = { type = "string", description = "secret access key from from aws iam console" }, aws_region = { type = "string", default = "us-east-1", description = "the aws region that is receiving the request" }, service = { type = "string", default = "execute-api", description = "the service that is receiving the request" } }, required = {"accesskey", "secretkey"} } } } local function request_processor(conf, ctx, params) local headers = params.headers -- set authorization headers if not already set by the client -- we are following not to overwrite the authz keys if not headers["x-api-key"] then if conf.authorization and conf.authorization.apikey then headers["x-api-key"] = conf.authorization.apikey return end end -- performing aws v4 request signing for IAM authorization -- visit https://docs.aws.amazon.com/general/latest/gr/sigv4-signed-request-examples.html -- to look at the pseudocode in python. if headers["authorization"] or not conf.authorization or not conf.authorization.iam then return end -- create a date for headers and the credential string local t = ngx.time() local amzdate = os.date("!%Y%m%dT%H%M%SZ", t) local datestamp = os.date("!%Y%m%d", t) -- Date w/o time, used in credential scope headers["X-Amz-Date"] = amzdate -- computing canonical uri local canonical_uri = norm_path(params.path) if canonical_uri ~= "/" then if canonical_uri:sub(-1, -1) == "/" then canonical_uri = canonical_uri:sub(1, -2) end if canonical_uri:sub(1, 1) ~= "/" then canonical_uri = "/" .. canonical_uri end end -- computing canonical query string local canonical_qs = {} local canonical_qs_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) end tab_sort(canonical_qs) canonical_qs = tab_concat(canonical_qs, "&") -- computing canonical and signed headers local canonical_headers, signed_headers = {}, {} local signed_headers_i = 0 for k, v in pairs(headers) do k = k:lower() if k ~= "connection" then signed_headers_i = signed_headers_i + 1 signed_headers[signed_headers_i] = k -- strip starting and trailing spaces including strip multiple spaces into single space canonical_headers[k] = str_strip(v) end end tab_sort(signed_headers) for i = 1, #signed_headers do local k = signed_headers[i] canonical_headers[i] = k .. ":" .. canonical_headers[k] .. "\n" end canonical_headers = tab_concat(canonical_headers, nil, 1, #signed_headers) signed_headers = tab_concat(signed_headers, ";") -- combining elements to form the canonical request (step-1) local canonical_request = params.method:upper() .. "\n" .. canonical_uri .. "\n" .. (canonical_qs or "") .. "\n" .. canonical_headers .. "\n" .. signed_headers .. "\n" .. sha256(params.body or "") -- creating the string to sign for aws signature v4 (step-2) local iam = conf.authorization.iam local credential_scope = datestamp .. "/" .. iam.aws_region .. "/" .. iam.service .. "/aws4_request" local string_to_sign = ALGO .. "\n" .. amzdate .. "\n" .. credential_scope .. "\n" .. sha256(canonical_request) -- calculate the signature (step-3) local signature_key = get_signature_key(iam.secretkey, datestamp, iam.aws_region, iam.service) local signature = hex_encode(hmac256(signature_key, string_to_sign)) -- add info to the headers (step-4) headers["authorization"] = ALGO .. " Credential=" .. iam.accesskey .. "/" .. credential_scope .. ", SignedHeaders=" .. signed_headers .. ", Signature=" .. signature end local serverless_obj = require("apisix.plugins.serverless.generic-upstream") return serverless_obj(plugin_name, plugin_version, priority, request_processor, aws_authz_schema)