Skip to content

Commit

Permalink
feat(jwt) add ES256 signatures support (#1920)
Browse files Browse the repository at this point in the history
* Added support for ES256 signatures in the JWT plugin.
  • Loading branch information
Tieske authored Dec 22, 2016
1 parent a023f60 commit 63864aa
Show file tree
Hide file tree
Showing 6 changed files with 358 additions and 7 deletions.
1 change: 1 addition & 0 deletions kong-0.9.7-0.rockspec
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,7 @@ build = {
["kong.plugins.jwt.api"] = "kong/plugins/jwt/api.lua",
["kong.plugins.jwt.daos"] = "kong/plugins/jwt/daos.lua",
["kong.plugins.jwt.jwt_parser"] = "kong/plugins/jwt/jwt_parser.lua",
["kong.plugins.jwt.asn_sequence"] = "kong/plugins/jwt/asn_sequence.lua",

["kong.plugins.hmac-auth.migrations.cassandra"] = "kong/plugins/hmac-auth/migrations/cassandra.lua",
["kong.plugins.hmac-auth.migrations.postgres"] = "kong/plugins/hmac-auth/migrations/postgres.lua",
Expand Down
128 changes: 128 additions & 0 deletions kong/plugins/jwt/asn_sequence.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
local error = error
local string_sub = string.sub
local string_byte = string.byte
local string_char = string.char
local table_sort = table.sort
local table_insert = table.insert

local _M = {}
_M.__index = _M

function _M.create_simple_sequence(input)
if type(input) ~= "table"
then error("Argument #1 must be a table", 2)
end
local sortTable = {}
for pair in pairs(input) do
table_insert(sortTable, pair)
end
table_sort(sortTable)
local numbers = ""
for i,n in ipairs(sortTable) do
if type(n) ~= "number"
then error("Table must use numbers as keys", 2)
end
local number = input[sortTable[i]]
if type(number) ~= "string" then
error("Table contains non-string value.", 2)
end
local length = #number
if length > 0x7F then
error("Mult-byte lengths are not supported")
end
numbers = numbers .. "\x02" .. string_char(length) .. number
end
if #numbers > 0x7F
then error("Multi-byte lengths are not supported")
end
return "\x30" .. string_char(#numbers) .. numbers
end

function _M.parse_simple_sequence(input)
if type(input) ~= "string" then
error("Argument #1 must be string", 2)
elseif #input == 0 then
error("Argument #1 must not be empty", 2)
end
if string_byte(input, 1) ~= 0x30 then
error("Argument #1 is not a sequence")
end
local length = string_byte(input, 2)
if length == nil then
error("Sequence is incomplete")
elseif length > 0x7F then
error("Multi-byte lengths are not supported")
elseif length ~= #input-2 then
error("Sequence's asn length does not match expected length")
end
local seq = {}
local counter = 1
local position = 3
while true do
if position == #input+1 then
break
elseif position > #input+1 then
error("Sequence moved out of bounds.")
elseif counter > 0xFF then
error("Sequence is too long")
end
local chunk = string_sub(input, position)
if string_byte(chunk, 1) ~= 0x2 then
error("Sequence did not contain integers")
end
local integerLength = string_byte(chunk, 2)
if integerLength > 0x7F then
error("Multi-byte lengths are not supported.")
elseif integerLength > #chunk-2 then
error("Integer is longer than remaining length")
end
local integer = string_sub(chunk, 3, integerLength+2)
seq[counter] = integer
position = position + integerLength + 2
counter = counter + 1
end
return seq
end

function _M.unsign_integer(input, len)
if type(input) ~= "string" then
error("Argument #1 must be string", 2)
elseif #input == 0 then
error("Argument #1 must not be empty", 2)
end
if string_byte(input) ~= 0 and #input > len then
error("Cannot unsign integer to length.", 2)
elseif string_byte(input) == 0 and #input == len+1 then
return string_sub(input, 2)
end
if #input == len then
return input
elseif #input < len then
while #input < len do
input = "\x00" .. input
end
return input
else
error("Unable to unsign integer")
end
end

-- @param input (string) 32 characters, format to be validated before calling
function _M.resign_integer(input)
if type(input) ~= "string" then
error("Argument #1 must be string", 2)
end
if string_byte(input) > 0x7F then
input = "\x00" .. input
end
while true do
if string_byte(input) == 0 and string_byte(input, 2) <= 0x7F then
input = string_sub(input, 2, -1)
else
break
end
end
return input
end

return _M
37 changes: 30 additions & 7 deletions kong/plugins/jwt/jwt_parser.lua
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,15 @@
local json = require "cjson"
local utils = require "kong.tools.utils"
local crypto = require "crypto"
local asn_sequence = require "kong.plugins.jwt.asn_sequence"

local error = error
local type = type
local pcall = pcall
local ngx_time = ngx.time
local string_rep = string.rep
local string_sub = string.sub
local table_concat = table.concat
local setmetatable = setmetatable
local encode_base64 = ngx.encode_base64
local decode_base64 = ngx.decode_base64
Expand All @@ -23,7 +26,18 @@ local alg_sign = {
["HS256"] = function(data, key) return crypto.hmac.digest("sha256", data, key, true) end,
--["HS384"] = function(data, key) return crypto.hmac.digest("sha384", data, key, true) end,
--["HS512"] = function(data, key) return crypto.hmac.digest("sha512", data, key, true) end
["RS256"] = function(data, key) return crypto.sign('sha256', data, crypto.pkey.from_pem(key, true)) end
["RS256"] = function(data, key) return crypto.sign('sha256', data, crypto.pkey.from_pem(key, true)) end,
["ES256"] = function(data, key)
local pkeyPrivate = crypto.pkey.from_pem(key, true)
local signature = crypto.sign('sha256', data, pkeyPrivate)

local derSequence = asn_sequence.parse_simple_sequence(signature)
local r = asn_sequence.unsign_integer(derSequence[1], 32)
local s = asn_sequence.unsign_integer(derSequence[2], 32)
assert(#r == 32)
assert(#s == 32)
return r .. s
end
}

--- Supported algorithms for verifying tokens.
Expand All @@ -32,8 +46,17 @@ local alg_verify = {
--["HS384"] = function(data, signature, key) return signature == alg_sign["HS384"](data, key) end,
--["HS512"] = function(data, signature, key) return signature == alg_sign["HS512"](data, key) end
["RS256"] = function(data, signature, key)
local pkey = assert(crypto.pkey.from_pem(key),"Consumer Public Key is Invalid")
local pkey = assert(crypto.pkey.from_pem(key), "Consumer Public Key is Invalid")
return crypto.verify('sha256', data, signature, pkey)
end,
["ES256"] = function(data, signature, key)
local pkey = assert(crypto.pkey.from_pem(key), "Consumer Public Key is Invalid")
assert(#signature == 64, "Signature must be 64 bytes.")
local asn = {}
asn[1] = asn_sequence.resign_integer(string_sub(signature, 1, 32))
asn[2] = asn_sequence.resign_integer(string_sub(signature, 33, 64))
local signatureAsn = asn_sequence.create_simple_sequence(asn)
return crypto.verify('sha256', data, signatureAsn, pkey)
end
}

Expand All @@ -50,10 +73,10 @@ end
-- @param input String to base64 decode
-- @return Base64 decoded string
local function b64_decode(input)
local reminder = #input % 4
local remainder = #input % 4

if reminder > 0 then
local padlen = 4 - reminder
if remainder > 0 then
local padlen = 4 - remainder
input = input..string_rep('=', padlen)
end

Expand Down Expand Up @@ -146,10 +169,10 @@ local function encode_token(data, key, alg, header)
b64_encode(json.encode(data))
}

local signing_input = table.concat(segments, ".")
local signing_input = table_concat(segments, ".")
local signature = alg_sign[alg](signing_input, key)
segments[#segments+1] = b64_encode(signature)
return table.concat(segments, ".")
return table_concat(segments, ".")
end

--[[
Expand Down
17 changes: 17 additions & 0 deletions spec/03-plugins/06-jwt/01-jwt_parser_spec.lua
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,15 @@ describe("Plugin: jwt (parser)", function()
..[[tcFKN-Pi7_rWzBtQwP2u4CrFD4ZJbn2sxobzSlFb9fn4nRh_-rPPjDSeHVKwrpsYp]]
..[[FSLBJxwX-KhbeGUfalg2eu9tHLDPHC4gTCpoQKxxRIwfMjW5zlHOZhohKZV2ZtpcgA]] , token)
end)

it("should encode using ES256", function()
local token = jwt_parser.encode({
sub = "5656565656",
name = "Jane Doe",
admin = true
}, fixtures.es256_private_key, 'ES256')
assert.truthy(token)
end)
end)
describe("Decoding", function()
it("throws an error if not given a string", function()
Expand Down Expand Up @@ -81,6 +90,14 @@ describe("Plugin: jwt (parser)", function()
assert.True(jwt:verify_signature(fixtures.rs256_public_key))
assert.False(jwt:verify_signature(fixtures.rs256_public_key:gsub('QAB', 'zzz')))
end)
it("using ES256", function()
for _ = 1, 500 do
local token = jwt_parser.encode({sub = "foo"}, fixtures.es256_private_key, 'ES256')
local jwt = assert(jwt_parser:new(token))
assert.True(jwt:verify_signature(fixtures.es256_public_key))
assert.False(jwt:verify_signature(fixtures.rs256_public_key:gsub('1z+', 'zzz')))
end
end)
end)
describe("verify registered claims", function()
it("requires claims passed as arguments", function()
Expand Down
Loading

0 comments on commit 63864aa

Please sign in to comment.