diff --git a/kong/plugins/hmac-auth/access.lua b/kong/plugins/hmac-auth/access.lua index 4c9863071ac..19f8569b343 100644 --- a/kong/plugins/hmac-auth/access.lua +++ b/kong/plugins/hmac-auth/access.lua @@ -2,6 +2,7 @@ local utils = require "kong.tools.utils" local responses = require "kong.tools.responses" local constants = require "kong.constants" local singletons = require "kong.singletons" +local crypto = require "crypto" local resty_sha256 = require "resty.sha256" local math_abs = math.abs @@ -10,14 +11,16 @@ local ngx_gmatch = ngx.re.gmatch local ngx_decode_base64 = ngx.decode_base64 local ngx_encode_base64 = ngx.encode_base64 local ngx_parse_time = ngx.parse_http_time -local ngx_sha1 = ngx.hmac_sha1 local ngx_set_header = ngx.req.set_header local ngx_get_headers = ngx.req.get_headers local ngx_log = ngx.log local req_read_body = ngx.req.read_body local req_get_body_data = ngx.req.get_body_data +local ngx_hmac_sha1 = ngx.hmac_sha1 local split = utils.split +local fmt = string.format +local ipairs = ipairs local AUTHORIZATION = "authorization" local PROXY_AUTHORIZATION = "proxy-authorization" @@ -27,8 +30,70 @@ local DIGEST = "digest" local SIGNATURE_NOT_VALID = "HMAC signature cannot be verified" local SIGNATURE_NOT_SAME = "HMAC signature does not match" +local new_tab +do + local ok + ok, new_tab = pcall(require, "table.new") + if not ok then + new_tab = function() return {} end + end +end + local _M = {} +local hmac = { + ["hmac-sha1"] = function(secret, data) + return ngx_hmac_sha1(secret, data) + end, + ["hmac-sha256"] = function(secret, data) + return crypto.hmac.digest("sha256", data, secret, true) + end, + ["hmac-sha384"] = function(secret, data) + return crypto.hmac.digest("sha384", data, secret, true) + end, + ["hmac-sha512"] = function(secret, data) + return crypto.hmac.digest("sha512", data, secret, true) + end +} + +local function list_as_set(list) + local set = new_tab(0, #list) + for _, v in ipairs(list) do + set[v] = true + end + + return set +end + +local function validate_params(params, conf) + -- check username and signature are present + if not params.username and params.signature then + return nil, "username or signature missing" + end + + -- check enforced headers are present + if conf.enforce_headers and #conf.enforce_headers >= 1 then + local enforced_header_set = list_as_set(conf.enforce_headers) + for _, header in ipairs(params.hmac_headers) do + enforced_header_set[header] = nil + end + for _, header in ipairs(conf.enforce_headers) do + if enforced_header_set[header] then + return nil, "enforced header not used for signature creation" + end + end + end + + -- check supported alorithm used + for _, algo in ipairs(conf.algorithms) do + if algo == params.algorithm then + return true + end + end + + return nil, fmt("algorithm %s not supported", params.algorithm) +end + local function retrieve_hmac_fields(request, headers, header_name, conf) local hmac_params = {} local authorization_header = headers[header_name] @@ -84,7 +149,7 @@ local function create_hash(request, hmac_params, headers) signing_string = signing_string .. "\n" end end - return ngx_sha1(hmac_params.secret, signing_string) + return hmac[hmac_params.algorithm](hmac_params.secret, signing_string) end local function validate_signature(request, hmac_params, headers) @@ -196,26 +261,33 @@ local function do_authentication(conf) -- retrieve hmac parameter from Proxy-Authorization header local hmac_params = retrieve_hmac_fields(ngx.req, headers, PROXY_AUTHORIZATION, conf) + -- Try with the authorization header if not hmac_params.username then hmac_params = retrieve_hmac_fields(ngx.req, headers, AUTHORIZATION, conf) end - if not (hmac_params.username and hmac_params.signature) then + + local ok, err = validate_params(hmac_params, conf) + if not ok then + ngx_log(ngx.DEBUG, err) return false, {status = 403, message = SIGNATURE_NOT_VALID} end -- validate signature local credential = load_credential(hmac_params.username) if not credential then + ngx_log(ngx.DEBUG, "failed to retrieve credential for ", hmac_params.username) return false, {status = 403, message = SIGNATURE_NOT_VALID} end hmac_params.secret = credential.secret + if not validate_signature(ngx.req, hmac_params, headers) then return false, { status = 403, message = SIGNATURE_NOT_SAME } end -- If request body validation is enabled, then verify digest. if conf.validate_request_body and not validate_body(headers[DIGEST]) then + ngx_log(ngx.DEBUG, "digest validation failed") return false, { status = 403, message = SIGNATURE_NOT_SAME } end diff --git a/kong/plugins/hmac-auth/migrations/cassandra.lua b/kong/plugins/hmac-auth/migrations/cassandra.lua index 8037e19557d..a4af91de464 100644 --- a/kong/plugins/hmac-auth/migrations/cassandra.lua +++ b/kong/plugins/hmac-auth/migrations/cassandra.lua @@ -17,5 +17,26 @@ return { down = [[ DROP TABLE hmacauth_credentials; ]] + }, + { + name = "2017-06-21-132400_init_hmacauth", + up = function(_, _, dao) + local rows, err = dao.plugins:find_all { name = "hmac-auth" } + if err then + return err + end + + for _, row in ipairs(rows) do + row.config.validate_request_body = false + row.config.enforce_headers = {} + row.config.algorithms = { "hmac-sha1" } + local _, err = dao.plugins:update(row, row) + if err then + return err + end + end + end, + down = function() + end } } diff --git a/kong/plugins/hmac-auth/migrations/postgres.lua b/kong/plugins/hmac-auth/migrations/postgres.lua index c4c7320054c..94e5ebd3fca 100644 --- a/kong/plugins/hmac-auth/migrations/postgres.lua +++ b/kong/plugins/hmac-auth/migrations/postgres.lua @@ -24,5 +24,26 @@ return { down = [[ DROP TABLE hmacauth_credentials; ]] + }, + { + name = "2017-06-21-132400_init_hmacauth", + up = function(_, _, dao) + local rows, err = dao.plugins:find_all { name = "hmac-auth" } + if err then + return err + end + + for _, row in ipairs(rows) do + row.config.validate_request_body = false + row.config.enforce_headers = {} + row.config.algorithms = {"hmac-sha1"} + local _, err = dao.plugins:update(row, row) + if err then + return err + end + end + end, + down = function() + end } } diff --git a/kong/plugins/hmac-auth/schema.lua b/kong/plugins/hmac-auth/schema.lua index 56e7105873d..5cff964ca07 100644 --- a/kong/plugins/hmac-auth/schema.lua +++ b/kong/plugins/hmac-auth/schema.lua @@ -4,7 +4,7 @@ local function check_user(anonymous) if anonymous == "" or utils.is_valid_uuid(anonymous) then return true end - + return false, "the anonymous user must be empty or a valid uuid" end @@ -15,12 +15,21 @@ local function check_clock_skew_positive(v) return true end +local algorithms = { + "hmac-sha1", + "hmac-sha256", + "hmac-sha384", + "hmac-sha512", +} + return { no_consumer = true, fields = { hide_credentials = { type = "boolean", default = false }, clock_skew = { type = "number", default = 300, func = check_clock_skew_positive }, - anonymous = {type = "string", default = "", func = check_user}, + anonymous = { type = "string", default = "", func = check_user }, validate_request_body = { type = "boolean", default = false }, + enforce_headers = { type = "array", default = {} }, + algorithms = { type = "array", default = algorithms, enum = algorithms } } } diff --git a/spec/03-plugins/20-hmac-auth/01-schema_spec.lua b/spec/03-plugins/20-hmac-auth/01-schema_spec.lua index 55f343cda53..4dd0af558a5 100644 --- a/spec/03-plugins/20-hmac-auth/01-schema_spec.lua +++ b/spec/03-plugins/20-hmac-auth/01-schema_spec.lua @@ -6,16 +6,21 @@ describe("Plugin: hmac-auth (schema)", function() it("accepts empty config", function() local ok, err = validate_entity({}, hmac_auth_schema) assert.is_nil(err) - assert.True(ok) + assert.is_true(ok) end) it("accepts correct clock skew", function() local ok, err = validate_entity({clock_skew = 10}, hmac_auth_schema) assert.is_nil(err) - assert.True(ok) + assert.is_true(ok) end) it("errors with negative clock skew", function() local ok, err = validate_entity({clock_skew = -10}, hmac_auth_schema) assert.equal("Clock Skew should be positive", err.clock_skew) - assert.False(ok) + assert.is_false(ok) + end) + it("errors with wrong algorithm", function() + local ok, err = validate_entity({algorithms = {"sha1024"}}, hmac_auth_schema) + assert.equal('"sha1024" is not allowed. Allowed values are: "hmac-sha1", "hmac-sha256", "hmac-sha384", "hmac-sha512"', err.algorithms) + assert.is_false(ok) end) end) diff --git a/spec/03-plugins/20-hmac-auth/03-access_spec.lua b/spec/03-plugins/20-hmac-auth/03-access_spec.lua index 1da729e982d..2ab1fc10c91 100644 --- a/spec/03-plugins/20-hmac-auth/03-access_spec.lua +++ b/spec/03-plugins/20-hmac-auth/03-access_spec.lua @@ -82,6 +82,37 @@ describe("Plugin: hmac-auth (access)", function() } }) + local api5 = assert(helpers.dao.apis:insert { + name = "api-5", + hosts = { "hmacauth5.com" }, + upstream_url = "http://mockbin.com" + }) + assert(helpers.dao.plugins:insert { + name = "hmac-auth", + api_id = api5.id, + config = { + clock_skew = 3000, + enforce_headers = {"date", "request-line"}, + validate_request_body = true + } + }) + + local api6 = assert(helpers.dao.apis:insert { + name = "api-6", + hosts = { "hmacauth6.com" }, + upstream_url = "http://mockbin.com" + }) + assert(helpers.dao.plugins:insert { + name = "hmac-auth", + api_id = api6.id, + config = { + clock_skew = 3000, + enforce_headers = {"date", "request-line"}, + algorithms = {"hmac-sha1", "hmac-sha256"}, + validate_request_body = true + } + }) + assert(helpers.start_kong { real_ip_header = "X-Forwarded-For", real_ip_recursive = "on", @@ -602,12 +633,12 @@ describe("Plugin: hmac-auth (access)", function() assert.res_status(200, res) end) - it("should pass with GET with wrong algorithm", function() + it("should not pass with GET with wrong algorithm", function() local date = os.date("!%a, %d %b %Y %H:%M:%S GMT") local encodedSignature = ngx.encode_base64( - hmac_sha1_binary("secret", "date: " .. date .. "\n" - .. "content-md5: md5" .. "\nGET /requests HTTP/1.1")) - local hmacAuth = [[hmac username="bob",algorithm="hmac-sha256",]] + crypto.hmac.digest("sha256","date: " .. date .. "\n" + .. "content-md5: md5" .. "\nGET /requests HTTP/1.1", "secret", true)) + local hmacAuth = [[hmac username="bob",algorithm="hmac-sha",]] .. [[ headers="date content-md5 request-line",signature="]] .. encodedSignature .. [["]] local res = assert(client:send { @@ -622,14 +653,15 @@ describe("Plugin: hmac-auth (access)", function() ["content-md5"] = "md5" } }) - assert.res_status(200, res) + assert.res_status(403, res) end) it("should pass the right headers to the upstream server", function() local date = os.date("!%a, %d %b %Y %H:%M:%S GMT") local encodedSignature = ngx.encode_base64( - hmac_sha1_binary("secret", "date: " .. date .. "\n" - .. "content-md5: md5" .. "\nGET /requests HTTP/1.1")) + crypto.hmac.digest("sha256","date: " .. date .. "\n" + .. "content-md5: md5" .. "\nGET /requests HTTP/1.1", + "secret", true)) local hmacAuth = [[hmac username="bob",algorithm="hmac-sha256",]] .. [[ headers="date content-md5 request-line",signature="]] .. encodedSignature .. [["]] @@ -1015,10 +1047,140 @@ describe("Plugin: hmac-auth (access)", function() assert.equal("HMAC signature does not match", body.message) end) - end) -end) + it("should pass with GET with request-line", function() + local date = os.date("!%a, %d %b %Y %H:%M:%S GMT") + local encodedSignature = ngx.encode_base64( + hmac_sha1_binary("secret", "date: " + .. date .. "\n" .. "content-md5: md5" .. "\nGET /requests HTTP/1.1")) + local hmacAuth = [[hmac username="bob", algorithm="hmac-sha1", ]] + .. [[headers="date content-md5 request-line", signature="]] + .. encodedSignature .. [["]] + local res = assert(client:send { + method = "GET", + path = "/requests", + body = {}, + headers = { + ["HOST"] = "hmacauth5.com", + date = date, + ["proxy-authorization"] = hmacAuth, + ["content-md5"] = "md5" + } + }) + assert.res_status(200, res) + end) + + it("should fail with GET when enforced header request-line missing", function() + local date = os.date("!%a, %d %b %Y %H:%M:%S GMT") + local encodedSignature = ngx.encode_base64( + hmac_sha1_binary("secret", "date: " + .. date .. "\n" .. "content-md5: md5")) + local hmacAuth = [[hmac username="bob", algorithm="hmac-sha1", ]] + .. [[headers="date content-md5", signature="]] + .. encodedSignature .. [["]] + local res = assert(client:send { + method = "GET", + path = "/requests", + body = {}, + headers = { + ["HOST"] = "hmacauth5.com", + date = date, + ["proxy-authorization"] = hmacAuth, + ["content-md5"] = "md5" + } + }) + assert.res_status(403, res) + end) + it("should pass with GET with hmac-sha384", function() + local date = os.date("!%a, %d %b %Y %H:%M:%S GMT") + local encodedSignature = ngx.encode_base64( + crypto.hmac.digest("sha384","date: " .. date .. "\n" + .. "content-md5: md5" .. "\nGET /requests HTTP/1.1", "secret", true)) + local hmacAuth = [[hmac username="bob", algorithm="hmac-sha384", ]] + .. [[headers="date content-md5 request-line", signature="]] + .. encodedSignature .. [["]] + local res = assert(client:send { + method = "GET", + path = "/requests", + body = {}, + headers = { + ["HOST"] = "hmacauth5.com", + date = date, + ["proxy-authorization"] = hmacAuth, + ["content-md5"] = "md5" + } + }) + assert.res_status(200, res) + end) + it("should pass with GET with hmac-sha512", function() + local date = os.date("!%a, %d %b %Y %H:%M:%S GMT") + local encodedSignature = ngx.encode_base64( + crypto.hmac.digest("sha512","date: " .. date .. "\n" + .. "content-md5: md5" .. "\nGET /requests HTTP/1.1", "secret", true)) + local hmacAuth = [[hmac username="bob", algorithm="hmac-sha512", ]] + .. [[headers="date content-md5 request-line", signature="]] + .. encodedSignature .. [["]] + local res = assert(client:send { + method = "GET", + path = "/requests", + body = {}, + headers = { + ["HOST"] = "hmacauth5.com", + date = date, + ["proxy-authorization"] = hmacAuth, + ["content-md5"] = "md5" + } + }) + assert.res_status(200, res) + end) + + it("should not pass with hmac-sha512", function() + local date = os.date("!%a, %d %b %Y %H:%M:%S GMT") + local encodedSignature = ngx.encode_base64( + crypto.hmac.digest("sha512","date: " .. date .. "\n" + .. "content-md5: md5" .. "\nGET /requests HTTP/1.1", "secret", true)) + local hmacAuth = [[hmac username="bob", algorithm="hmac-sha512", ]] + .. [[headers="date content-md5 request-line", signature="]] + .. encodedSignature .. [["]] + local res = assert(client:send { + method = "GET", + path = "/requests", + body = {}, + headers = { + ["HOST"] = "hmacauth6.com", + date = date, + ["proxy-authorization"] = hmacAuth, + ["content-md5"] = "md5" + } + }) + assert.res_status(403, res) + end) + + it("should pass with hmac-sha1", function() + local date = os.date("!%a, %d %b %Y %H:%M:%S GMT") + local encodedSignature = ngx.encode_base64( + crypto.hmac.digest("sha1","date: " .. date .. "\n" + .. "content-md5: md5" .. "\nGET /requests HTTP/1.1", "secret", true)) + local hmacAuth = [[hmac username="bob", algorithm="hmac-sha1", ]] + .. [[headers="date content-md5 request-line", signature="]] + .. encodedSignature .. [["]] + local res = assert(client:send { + method = "GET", + path = "/requests", + body = {}, + headers = { + ["HOST"] = "hmacauth6.com", + date = date, + ["proxy-authorization"] = hmacAuth, + ["content-md5"] = "md5" + } + }) + assert.res_status(200, res) + end) + + end) +end) describe("Plugin: hmac-auth (access)", function()