Skip to content

Commit

Permalink
feat(hmac-auth) add support for RSA signatures
Browse files Browse the repository at this point in the history
The hmac-auth plugin allow authentication with HMAC signatures based on
the draft-cavage-http-signatures draft. This commit aims to add support
for RSA signatures as described in the draft, providing a stronger layer
of security via asymmetric encryption.

This implementation has been made with backward compatibility in mind
and only one new field has been added to the DAOs to store the RSA
public key. Depending on the algorithm used during the request, the
plugin will use either the HMAC secret or the RSA public key to verify
the signature.
  • Loading branch information
mideuger committed Mar 28, 2022
1 parent 6983215 commit d7a471d
Show file tree
Hide file tree
Showing 7 changed files with 162 additions and 19 deletions.
69 changes: 51 additions & 18 deletions kong/plugins/hmac-auth/access.lua
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
local constants = require "kong.constants"
local sha256 = require "resty.sha256"
local openssl_hmac = require "resty.openssl.hmac"
local openssl_pkey = require "resty.openssl.pkey"
local utils = require "kong.tools.utils"


Expand Down Expand Up @@ -31,18 +32,45 @@ local SIGNATURE_NOT_VALID = "HMAC signature cannot be verified"
local SIGNATURE_NOT_SAME = "HMAC signature does not match"


local function verify_rsa(public_key, signature, signing_string, md_alg)
local pub, err = openssl_pkey.new(public_key)
if not pub then
kong.log.err("failed to create public key : ", err)
return false
end

local verified, err = pub:verify(signature, signing_string, md_alg)
if not err then
return verified
else
kong.log.err("failed to verify signature : ", err)
return false
end
end


local rsa = {
["rsa-sha256"] = function(public_key, signature, signing_string)
return verify_rsa(public_key, signature, signing_string, "sha256")
end,
["rsa-sha512"] = function(public_key, signature, signing_string)
return verify_rsa(public_key, signature, signing_string, "sha512")
end,
}


local hmac = {
["hmac-sha1"] = function(secret, data)
return hmac_sha1(secret, data)
["hmac-sha1"] = function(secret, signature, signing_string)
return signature == hmac_sha1(secret, signing_string)
end,
["hmac-sha256"] = function(secret, data)
return openssl_hmac.new(secret, "sha256"):final(data)
["hmac-sha256"] = function(secret, signature, signing_string)
return signature == openssl_hmac.new(secret, "sha256"):final(signing_string)
end,
["hmac-sha384"] = function(secret, data)
return openssl_hmac.new(secret, "sha384"):final(data)
["hmac-sha384"] = function(secret, signature, signing_string)
return signature == openssl_hmac.new(secret, "sha384"):final(signing_string)
end,
["hmac-sha512"] = function(secret, data)
return openssl_hmac.new(secret, "sha512"):final(data)
["hmac-sha512"] = function(secret, signature, signing_string)
return signature == openssl_hmac.new(secret, "sha512"):final(signing_string)
end,
}

Expand Down Expand Up @@ -97,7 +125,7 @@ local function retrieve_hmac_fields(authorization_header)
-- parse the header to retrieve hamc parameters
if authorization_header then
local iterator, iter_err = re_gmatch(authorization_header,
"\\s*[Hh]mac\\s*username=\"(.+)\"," ..
"(\\s*[Hh]mac)?\\s*username=\"(.+)\"," ..
"\\s*algorithm=\"(.+)\",\\s*header" ..
"s=\"(.+)\",\\s*signature=\"(.+)\"",
"jo")
Expand All @@ -113,10 +141,10 @@ local function retrieve_hmac_fields(authorization_header)
end

if m and #m >= 4 then
hmac_params.username = m[1]
hmac_params.algorithm = m[2]
hmac_params.hmac_headers = utils.split(m[3], " ")
hmac_params.signature = m[4]
hmac_params.username = m[2]
hmac_params.algorithm = m[3]
hmac_params.hmac_headers = utils.split(m[4], " ")
hmac_params.signature = m[5]
end
end

Expand All @@ -126,7 +154,7 @@ end

-- plugin assumes the request parameters being used for creating
-- signature by client are not changed by core or any other plugin
local function create_hash(request_uri, hmac_params)
local function generate_signing_string(request_uri, hmac_params)
local signing_string = ""
local hmac_headers = hmac_params.hmac_headers

Expand Down Expand Up @@ -161,14 +189,18 @@ local function create_hash(request_uri, hmac_params)
end
end

return hmac[hmac_params.algorithm](hmac_params.secret, signing_string)
return signing_string
end


local function validate_signature(hmac_params)
local signature_1 = create_hash(kong_request.get_path_with_query(), hmac_params)
local signature_2 = decode_base64(hmac_params.signature)
return signature_1 == signature_2
local signature = decode_base64(hmac_params.signature)
local signing_string = generate_signing_string(kong_request.get_path_with_query(), hmac_params)
if hmac_params.algorithm:sub(1, 4) == "rsa-" then
return rsa[hmac_params.algorithm](hmac_params.public_key, signature, signing_string)
else
return hmac[hmac_params.algorithm](hmac_params.secret, signature, signing_string)
end
end


Expand Down Expand Up @@ -326,6 +358,7 @@ local function do_authentication(conf)
end

hmac_params.secret = credential.secret
hmac_params.public_key = credential.public_key

if not validate_signature(hmac_params) then
return false, { status = 401, message = SIGNATURE_NOT_SAME }
Expand Down
1 change: 1 addition & 0 deletions kong/plugins/hmac-auth/daos.lua
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ return {
{ consumer = { type = "foreign", reference = "consumers", required = true, on_delete = "cascade", }, },
{ username = { type = "string", required = true, unique = true }, },
{ secret = { type = "string", auto = true }, },
{ public_key = { type = "string" }, },
{ tags = typedefs.tags },
},
},
Expand Down
18 changes: 18 additions & 0 deletions kong/plugins/hmac-auth/migrations/004_add_public_key.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
return {
postgres = {
up = [[
DO $$
BEGIN
ALTER TABLE IF EXISTS ONLY hmacauth_credentials ADD public_key TEXT;
EXCEPTION WHEN DUPLICATE_COLUMN THEN
-- Do nothing, accept existing state
END$$;
]],
},
cassandra = {
up = [[
ALTER TABLE hmacauth_credentials ADD public_key text;
]],
}
}
1 change: 1 addition & 0 deletions kong/plugins/hmac-auth/migrations/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@ return {
"000_base_hmac_auth",
"002_130_to_140",
"003_200_to_210",
"004_add_public_key",
}
2 changes: 2 additions & 0 deletions kong/plugins/hmac-auth/schema.lua
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ local ALGORITHMS = {
"hmac-sha256",
"hmac-sha384",
"hmac-sha512",
"rsa-sha256",
"rsa-sha512",
}


Expand Down
2 changes: 1 addition & 1 deletion spec/03-plugins/19-hmac-auth/01-schema_spec.lua
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ describe("Plugin: hmac-auth (schema)", function()
it("errors with wrong algorithm", function()
local ok, err = v({ algorithms = { "sha1024" } }, schema_def)
assert.is_falsy(ok)
assert.equal("expected one of: hmac-sha1, hmac-sha256, hmac-sha384, hmac-sha512",
assert.equal("expected one of: hmac-sha1, hmac-sha256, hmac-sha384, hmac-sha512, rsa-sha256, rsa-sha512",
err.config.algorithms[1])
end)
end)
88 changes: 88 additions & 0 deletions spec/03-plugins/19-hmac-auth/03-access_spec.lua
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
local cjson = require "cjson"
local openssl_hmac = require "resty.openssl.hmac"
local openssl_pkey = require "resty.openssl.pkey"
local helpers = require "spec.helpers"
local utils = require "kong.tools.utils"
local resty_sha256 = require "resty.sha256"
Expand All @@ -20,6 +21,7 @@ for _, strategy in helpers.each_strategy() do
local proxy_client
local consumer
local credential
local rsa_key_pair

lazy_setup(function()
local bp = helpers.get_db_utils(strategy, {
Expand Down Expand Up @@ -70,6 +72,19 @@ for _, strategy in helpers.each_strategy() do
consumer = { id = consumer.id },
}

rsa_key_pair = openssl_pkey.new()

local consumer2 = bp.consumers:insert {
username = "alice",
custom_id = "123456"
}

bp.hmacauth_credentials:insert {
username = "alice",
public_key = rsa_key_pair:to_PEM("public"),
consumer = { id = consumer2.id },
}

local anonymous_user = bp.consumers:insert {
username = "no-body"
}
Expand Down Expand Up @@ -155,6 +170,19 @@ for _, strategy in helpers.each_strategy() do
}
}

local route8 = bp.routes:insert {
hosts = { "hmacauth8.com" },
}

bp.plugins:insert {
name = "hmac-auth",
route = { id = route8.id },
config = {
enforce_headers = {"date"},
algorithms = {"rsa-sha256", "rsa-sha512"},
}
}

assert(helpers.start_kong {
database = strategy,
real_ip_header = "X-Forwarded-For",
Expand Down Expand Up @@ -1694,6 +1722,66 @@ for _, strategy in helpers.each_strategy() do
assert.res_status(200, res)
end)

it("should not pass with GET with rsa-sha256 when signing with wrong key", function()
local date = os.date("!%a, %d %b %Y %H:%M:%S GMT")
local signature = openssl_pkey.new():sign("date: " .. date .. "\nGET /request HTTP/1.1", "sha256")
local encodedSignature = ngx.encode_base64(signature)
local hmacAuth = [[username="alice", algorithm="rsa-sha256", ]]
.. [[headers="date request-line", signature="]]
.. encodedSignature .. [["]]
local res = assert(proxy_client:send {
method = "GET",
path = "/request",
body = {},
headers = {
["HOST"] = "hmacauth8.com",
date = date,
["proxy-authorization"] = hmacAuth,
},
})
assert.res_status(401, res)
end)

it("should pass with GET with rsa-sha256", function()
local date = os.date("!%a, %d %b %Y %H:%M:%S GMT")
local signature = rsa_key_pair:sign("date: " .. date .. "\nGET /request HTTP/1.1", "sha256")
local encodedSignature = ngx.encode_base64(signature)
local hmacAuth = [[username="alice", algorithm="rsa-sha256", ]]
.. [[headers="date request-line", signature="]]
.. encodedSignature .. [["]]
local res = assert(proxy_client:send {
method = "GET",
path = "/request",
body = {},
headers = {
["HOST"] = "hmacauth8.com",
date = date,
["proxy-authorization"] = hmacAuth,
},
})
assert.res_status(200, res)
end)

it("should pass with GET with rsa-sha512", function()
local date = os.date("!%a, %d %b %Y %H:%M:%S GMT")
local signature = rsa_key_pair:sign("date: " .. date .. "\nGET /request HTTP/1.1", "sha512")
local encodedSignature = ngx.encode_base64(signature)
local hmacAuth = [[username="alice", algorithm="rsa-sha512", ]]
.. [[headers="date request-line", signature="]]
.. encodedSignature .. [["]]
local res = assert(proxy_client:send {
method = "GET",
path = "/request",
body = {},
headers = {
["HOST"] = "hmacauth8.com",
date = date,
["proxy-authorization"] = hmacAuth,
},
})
assert.res_status(200, res)
end)

end)
end)

Expand Down

0 comments on commit d7a471d

Please sign in to comment.