Skip to content

Commit

Permalink
make jwe token verification compatible with RFC (#45)
Browse files Browse the repository at this point in the history
* make jwe token verification compatible with RFC

* update docs

* fix globals
  • Loading branch information
saks authored and SkyLothar committed Aug 18, 2016
1 parent 113bf4f commit 2b91929
Show file tree
Hide file tree
Showing 3 changed files with 229 additions and 33 deletions.
10 changes: 5 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -169,13 +169,13 @@ sign-jwe

sign a table_of_jwt to a jwt_token.

The `alg` argument specifies which hashing algorithm to use for encrypting key (`DIR`).
The `enc` argument specifies which hashing algorithm to use for encrypting payload (`A128CBC_HS256`, `A256CBC_HS512`)
The `alg` argument specifies which hashing algorithm to use for encrypting key (`dir`).
The `enc` argument specifies which hashing algorithm to use for encrypting payload (`A128CBC-HS256`, `A256CBC-HS512`)

### sample of table_of_jwt ###
```
{
"header": {"typ": "JWE", "alg": "DIR", "enc":"A128CBC_HS256"},
"header": {"typ": "JWE", "alg": "dir", "enc":"A128CBC-HS256"},
"payload": {"foo": "bar"}
}
```
Expand Down Expand Up @@ -324,7 +324,7 @@ When using legacy `validation_options`, you *MUST ONLY* specify these options.
* When none of the `nbf` and `exp` claims can be found, verification will fail.

* `nbf` and `exp` claims are expected to be expressed in the jwt as numerical values. Wouldn't that be the case, verification will fail.

* Specifying this option is equivalent to calling:
```
validators.set_system_leeway(leeway)
Expand All @@ -342,7 +342,7 @@ When using legacy `validation_options`, you *MUST ONLY* specify these options.
* `require_nbf_claim`: Express if the `nbf` claim is optional or not. Value should be a boolean.
* When this validation option is set to `true` and no `lifetime_grace_period` has been specified, a zero (`0`) leeway is implied.
* Specifying this option is equivalent to specifying as a `claim_spec`:
```
{
Expand Down
82 changes: 54 additions & 28 deletions lib/resty/jwt.lua
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,13 @@ local string_rep = string.rep
local string_format = string.format
local string_sub = string.sub
local string_byte = string.byte
local string_char = string.char
local table_concat = table.concat
local ngx_encode_base64 = ngx.encode_base64
local ngx_decode_base64 = ngx.decode_base64
local cjson_encode = cjson.encode
local cjson_decode = cjson.decode
local tostring = tostring

-- define string constants to avoid string garbage collection
local str_const = {
Expand Down Expand Up @@ -55,9 +57,9 @@ local str_const = {
HS256 = "HS256",
HS512 = "HS512",
RS256 = "RS256",
A128CBC_HS256 = "A128CBC_HS256",
A256CBC_HS512 = "A256CBC_HS512",
DIR = "DIR",
A128CBC_HS256 = "A128CBC-HS256",
A256CBC_HS512 = "A256CBC-HS512",
DIR = "dir",
reason = "reason",
verified = "verified",
number = "number",
Expand Down Expand Up @@ -180,21 +182,26 @@ end
--@param secret key
--@return secret key, mac key and encryption key
local function derive_keys(enc, secret_key)
local key_size_bytes = 32
local mac_key_len, enc_key_len = 16, 16

if enc == str_const.A128CBC_HS256 then
key_size_bytes = 32
mac_key_len, enc_key_len = 16, 16
elseif enc == str_const.A256CBC_HS512 then
key_size_bytes = 64
mac_key_len, enc_key_len = 32, 32
end

local secret_key_len = mac_key_len + enc_key_len

if not secret_key then
secret_key = resty_random.bytes(key_size_bytes,true)
secret_key = resty_random.bytes(secret_key_len, true)
end
if #secret_key ~= key_size_bytes then
error({reason="The pre-shared content key must be ".. key_size_bytes})

if #secret_key ~= secret_key_len then
error({reason="The pre-shared content key must be ".. secret_key_len})
end
local derived_key_size = key_size_bytes / 2
mac_key = string_sub(secret_key, 1, derived_key_size)
enc_key =string_sub(secret_key, derived_key_size)

local mac_key = string_sub(secret_key, 1, mac_key_len)
local enc_key = string_sub(secret_key, enc_key_len + 1)
return secret_key, mac_key, enc_key
end

Expand All @@ -209,8 +216,9 @@ local function parse_jwe(preshared_key, encoded_header, encoded_encrypted_key, e
error({reason="invalid header: " .. encoded_header})
end

local key, mac_key, enc_key = derive_keys(header.enc, preshared_key)

-- use preshared key if given otherwise decrypt the encoded key
local key = preshared_key
if not preshared_key then
local encrypted_key = _M:jwt_decode(encoded_encrypted_key)
if header.alg == str_const.DIR then
Expand All @@ -227,15 +235,14 @@ local function parse_jwe(preshared_key, encoded_header, encoded_encrypted_key, e
internal = {
encoded_header = encoded_header,
cipher_text = cipher_text,
key=key,
key = key,
iv = iv
},
header=header,
signature=_M:jwt_decode(encoded_auth_tag)
}


local json_payload, err = decrypt_payload(key, cipher_text, header.enc, iv )
local json_payload, err = decrypt_payload(enc_key, cipher_text, header.enc, iv)
if not json_payload then
basic_jwe.reason = err

Expand Down Expand Up @@ -367,31 +374,49 @@ end

_M.x5u_content_retriever = nil

-- https://tools.ietf.org/html/rfc7516#appendix-B.3
-- TODO: do it in lua way
local function binlen(s)
if type(s) ~= 'string' then return end

local len = 8 * #s

return string_char(len / 0x0100000000000000 % 0x100)
.. string_char(len / 0x0001000000000000 % 0x100)
.. string_char(len / 0x0000010000000000 % 0x100)
.. string_char(len / 0x0000000100000000 % 0x100)
.. string_char(len / 0x0000000001000000 % 0x100)
.. string_char(len / 0x0000000000010000 % 0x100)
.. string_char(len / 0x0000000000000100 % 0x100)
.. string_char(len / 0x0000000000000001 % 0x100)
end

--@function sign jwe payload
--@param secret key : if used pre-shared or RSA key
--@param jwe payload
--@return jwe token
local function sign_jwe(secret_key, jwt_obj)
local header = jwt_obj.header
local enc = header.enc

local enc = jwt_obj.header.enc
local key, mac_key, enc_key = derive_keys(enc, secret_key)
local json_payload = cjson_encode(jwt_obj.payload)
local cipher_text, iv, err = encrypt_payload( key, json_payload, jwt_obj.header.enc )
local cipher_text, iv, err = encrypt_payload(enc_key, json_payload, enc)
if err then
error({reason="error while encrypting payload. Error: " .. err})
end
local alg = jwt_obj.header.alg
local alg = header.alg

if alg ~= str_const.DIR then
error({reason="unsupported alg: " .. alg})
error({reason="unsupported alg: " .. tostring(alg)})
end
-- remove type
if jwt_obj.header.typ then
jwt_obj.header.typ = nil
if header.typ then
header.typ = nil
end
local encoded_header = _M:jwt_encode(jwt_obj.header)
local encoded_header = _M:jwt_encode(header)

local encoded_header_length = #encoded_header -- FIXME : might be missin this logic
local encoded_header_length = binlen(encoded_header)
local mac_input = table_concat({encoded_header , iv, cipher_text , encoded_header_length})
local mac = hmac_digest(enc, mac_key, mac_input)
-- TODO: implement logic for creating enc key and mac key and then encrypt key
Expand Down Expand Up @@ -506,13 +531,14 @@ local function verify_jwe_obj(secret, jwt_obj)
local key, mac_key, enc_key = derive_keys(jwt_obj.header.enc, jwt_obj.internal.key)
local encoded_header = jwt_obj.internal.encoded_header

local encoded_header_length = #encoded_header -- FIXME: Not sure how to get this
local encoded_header_length = binlen(encoded_header)
local mac_input = table_concat({encoded_header , jwt_obj.internal.iv, jwt_obj.internal.cipher_text , encoded_header_length})
local mac = hmac_digest(jwt_obj.header.enc, mac_key, mac_input)
local auth_tag = string_sub(mac, 1, #mac/2)

if auth_tag ~= jwt_obj.signature then
jwt_obj[str_const.reason] = "signature mismatch: " .. jwt_obj[str_const.signature]
jwt_obj[str_const.reason] = "signature mismatch: " ..
tostring(jwt_obj[str_const.signature])

end
jwt_obj.internal = nil
Expand Down Expand Up @@ -722,7 +748,7 @@ function _M.verify_jwt_obj(self, secret, jwt_obj, ...)
jwt_obj[str_const.reason] = "signature mismatch: " .. jwt_obj[str_const.signature]
end
elseif alg == str_const.RS256 then
local cert
local cert, err
if self.trusted_certs_file ~= nil then
local cert_str = extract_certificate(jwt_obj, self.x5u_content_retriever)
if not cert_str then
Expand Down Expand Up @@ -791,7 +817,7 @@ end


function _M.verify(self, secret, jwt_str, ...)
jwt_obj = _M.load_jwt(self, jwt_str, secret)
local jwt_obj = _M.load_jwt(self, jwt_str, secret)
if not jwt_obj.valid then
return {verified=false, reason=jwt_obj[str_const.reason]}
end
Expand Down
170 changes: 170 additions & 0 deletions t/load-verify-jwe.t
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
use Test::Nginx::Socket::Lua;

repeat_each(2);

plan tests => repeat_each() * (3 * blocks());

our $HttpConfig = <<'_EOC_';
lua_package_path 'lib/?.lua;;';
_EOC_
no_long_string();
run_tests();
__DATA__
=== TEST 1: Verify A256CBC-HS512 Direct Encryption with a Shared Symmetric Key
--- http_config eval: $::HttpConfig
--- config
location /t {
content_by_lua '
local jwt = require "resty.jwt"
local cjson = require "cjson"
local shared_key = "12341234123412341234123412341234" ..
"12341234123412341234123412341234"
local jwt_obj = jwt:verify(
shared_key,
"eyJhbGciOiJkaXIiLCJlbmMiOiJBMjU2Q0JDLUhTNTEyIn0." ..
".M927Z_hNTmumFQE0rtRQCQ.nnd7AoE_2dgvws2-iay8qA.d" ..
"kyZuuks4Qm9Cd7VfEVSs07pi_Kyt0INVHTTesUC2BM"
)
ngx.say(
cjson.encode(jwt_obj)
)
';
}
--- request
GET /t
--- response_body
{"payload":{"foo":"bar"},"reason":"everything is awesome~ :p","header":{"alg":"dir","enc":"A256CBC-HS512"},"valid":true,"verified":true}
--- no_error_log
[error]
=== TEST 2: Verify A128CBC-HS256 Direct Encryption with a Shared Symmetric Key
--- http_config eval: $::HttpConfig
--- config
location /t {
content_by_lua '
local jwt = require "resty.jwt"
local cjson = require "cjson"
local shared_key = "12341234123412341234123412341234"
local jwt_obj = jwt:verify(
shared_key,
"eyJhbGciOiJkaXIiLCJlbmMiOiJBMTI4Q0JDLUhTMjU2In0." ..
".U6emIwy_yVkagUwQ4EjdFA.FrapgQVvG3uictQz9NPPMw.n" ..
"MoW0ShdgCN0JHw472SJjQ"
)
ngx.say(
cjson.encode(jwt_obj)
)
';
}
--- request
GET /t
--- response_body
{"payload":{"foo":"bar"},"reason":"everything is awesome~ :p","header":{"alg":"dir","enc":"A128CBC-HS256"},"valid":true,"verified":true}
--- no_error_log
[error]
=== TEST 3: Dont fail if extra chars added
--- http_config eval: $::HttpConfig
--- config
location /t {
content_by_lua '
local jwt = require "resty.jwt"
local cjson = require "cjson"
local shared_key = "12341234123412341234123412341234"
local jwt_obj = jwt:verify(
shared_key,
"eyJhbGciOiJkaXIiLCJlbmMiOiJBMTI4Q0JDLUhTMjU2In0." ..
".U6emIwy_yVkagUwQ4EjdFA.FrapgQVvG3uictQz9NPPMw.n" ..
"MoW0ShdgCN0JHw472SJjQ" ..
"xxx"
)
ngx.say(
"valid: ", jwt_obj.valid, "\\n",
"verified: ", jwt_obj.verified
)
';
}
--- request
GET /t
--- response_body
valid: true
verified: false
--- no_error_log
[error]
=== TEST 4: Encode A128CBC-HS256 Direct Encryption
--- http_config eval: $::HttpConfig
--- config
location /t {
content_by_lua '
local jwt = require "resty.jwt"
local cjson = require "cjson"
local shared_key = "12341234123412341234123412341234"
local table_of_jwt = {
header = { alg = "dir", enc = "A128CBC-HS256" },
payload = { foo = "bar" },
}
local jwt_token = jwt:sign(shared_key, table_of_jwt)
local jwt_obj = jwt:verify(shared_key, jwt_token)
ngx.say(
cjson.encode(table_of_jwt.payload) == cjson.encode(jwt_obj.payload), "\\n",
"valid: ", jwt_obj.valid, "\\n",
"verified: ", jwt_obj.verified
)
';
}
--- request
GET /t
--- response_body
true
valid: true
verified: true
--- no_error_log
[error]
=== TEST 5: Encode A256CBC-HS512 Direct Encryption
--- http_config eval: $::HttpConfig
--- config
location /t {
content_by_lua '
local jwt = require "resty.jwt"
local cjson = require "cjson"
local shared_key = "12341234123412341234123412341234" ..
"12341234123412341234123412341234"
local table_of_jwt = {
header = { alg = "dir", enc = "A256CBC-HS512" },
payload = { foo = "bar" },
}
local jwt_token = jwt:sign(shared_key, table_of_jwt)
local jwt_obj = jwt:verify(shared_key, jwt_token)
ngx.say(
cjson.encode(table_of_jwt.payload) == cjson.encode(jwt_obj.payload), "\\n",
"valid: ", jwt_obj.valid, "\\n",
"verified: ", jwt_obj.verified
)
';
}
--- request
GET /t
--- response_body
true
valid: true
verified: true
--- no_error_log
[error]

0 comments on commit 2b91929

Please sign in to comment.