diff --git a/README.md b/README.md index c9c94ff..a2f101b 100644 --- a/README.md +++ b/README.md @@ -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"} } ``` @@ -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) @@ -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`: ``` { diff --git a/lib/resty/jwt.lua b/lib/resty/jwt.lua index 1fb39a6..baec0f3 100644 --- a/lib/resty/jwt.lua +++ b/lib/resty/jwt.lua @@ -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 = { @@ -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", @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 diff --git a/t/load-verify-jwe.t b/t/load-verify-jwe.t new file mode 100644 index 0000000..8f16b65 --- /dev/null +++ b/t/load-verify-jwe.t @@ -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]