From 6ceaace43c517f6b0a1d4b89a1031c998aba17bc Mon Sep 17 00:00:00 2001 From: Yoann Ciabaud Date: Fri, 3 Jun 2022 15:33:06 +0200 Subject: [PATCH] (feat) add rsa-oaep algo support for JWE --- lib/resty/evp.lua | 1 + lib/resty/jwt.lua | 19 ++- t/load-verify-jwe.t | 355 ++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 370 insertions(+), 5 deletions(-) diff --git a/lib/resty/evp.lua b/lib/resty/evp.lua index 584ff5a..5498a64 100644 --- a/lib/resty/evp.lua +++ b/lib/resty/evp.lua @@ -12,6 +12,7 @@ local ngx = ngx local CONST = { + SHA1_DIGEST = "SHA1", SHA256_DIGEST = "SHA256", SHA512_DIGEST = "SHA512", -- ref : https://github.com/openssl/openssl/blob/master/include/openssl/rsa.h diff --git a/lib/resty/jwt.lua b/lib/resty/jwt.lua index accba11..d8b7c06 100644 --- a/lib/resty/jwt.lua +++ b/lib/resty/jwt.lua @@ -73,6 +73,7 @@ local str_const = { A256CBC_HS512_CIPHER_MODE = "aes-256-cbc", A256GCM = "A256GCM", A256GCM_CIPHER_MODE = "aes-256-gcm", + RSA_OAEP = "RSA-OAEP", RSA_OAEP_256 = "RSA-OAEP-256", DIR = "dir", reason = "reason", @@ -255,7 +256,7 @@ local function parse_jwe(self, preshared_key, encoded_header, encoded_encrypted_ end local alg = header.alg - if alg ~= str_const.DIR and alg ~= str_const.RSA_OAEP_256 then + if alg ~= str_const.DIR and alg ~= str_const.RSA_OAEP_256 and alg ~= str_const.RSA_OAEP then error({reason="invalid algorithm: " .. alg}) end @@ -265,11 +266,15 @@ local function parse_jwe(self, preshared_key, encoded_header, encoded_encrypted_ error({reason="preshared key must not be null"}) end key, _, enc_key = derive_keys(header.enc, preshared_key) - elseif alg == str_const.RSA_OAEP_256 then + elseif alg == str_const.RSA_OAEP_256 or alg == str_const.RSA_OAEP then if not preshared_key then error({reason="rsa private key must not be null"}) end - local rsa_decryptor, err = evp.RSADecryptor:new(preshared_key, nil, evp.CONST.RSA_PKCS1_OAEP_PADDING, evp.CONST.SHA256_DIGEST) + local digest_alg = evp.CONST.SHA256_DIGEST + if alg == str_const.RSA_OAEP then + digest_alg = evp.CONST.SHA1_DIGEST + end + local rsa_decryptor, err = evp.RSADecryptor:new(preshared_key, nil, evp.CONST.RSA_PKCS1_OAEP_PADDING, digest_alg) if err then error({reason="failed to create rsa object: ".. err}) end @@ -467,7 +472,7 @@ local function sign_jwe(self, secret_key, jwt_obj) if alg == str_const.DIR then _, mac_key, enc_key = derive_keys(enc, secret_key) encrypted_key = "" - elseif alg == str_const.RSA_OAEP_256 then + elseif alg == str_const.RSA_OAEP_256 or alg == str_const.RSA_OAEP then local cert, err if secret_key:find("CERTIFICATE") then cert, err = evp.Cert:new(secret_key) @@ -477,7 +482,11 @@ local function sign_jwe(self, secret_key, jwt_obj) if not cert then error({reason="Decode secret is not a valid cert/public key: " .. (err and err or secret_key)}) end - local rsa_encryptor = evp.RSAEncryptor:new(cert, evp.CONST.RSA_PKCS1_OAEP_PADDING, evp.CONST.SHA256_DIGEST) + local digest_alg = evp.CONST.SHA256_DIGEST + if alg == str_const.RSA_OAEP then + digest_alg = evp.CONST.SHA1_DIGEST + end + local rsa_encryptor = evp.RSAEncryptor:new(cert, evp.CONST.RSA_PKCS1_OAEP_PADDING, digest_alg) if err then error("failed to create rsa object for encryption ".. err) end diff --git a/t/load-verify-jwe.t b/t/load-verify-jwe.t index 19f3373..093886d 100644 --- a/t/load-verify-jwe.t +++ b/t/load-verify-jwe.t @@ -548,3 +548,358 @@ verified: false error: true --- no_error_log [error] + + +=== TEST 13: Use rsa oeap for encryption +--- http_config eval: $::HttpConfig +--- config + location /t { + content_by_lua ' + local jwt = require "resty.jwt" + local cjson = require "cjson" + + local function get_testcert(name) + local f = io.open("/lua-resty-jwt/testcerts/" .. name) + local contents = f:read("*all") + f:close() + return contents + end + + local table_of_jwt = { + header = { + alg = "RSA-OAEP", + enc = "A256CBC-HS512", + typ = "JWE", + kid = "myKey" + }, + payload = { + foo = "bar" + } + } + + local jwt_token = jwt:sign(get_testcert("cert-pubkey.pem"), table_of_jwt) + local jwt_obj = jwt:verify(get_testcert("cert-key.pem"), jwt_token) + print(cjson.encode(jwt_obj)) + 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 14: Use rsa oeap for encryption invalid typ +--- http_config eval: $::HttpConfig +--- config + location /t { + content_by_lua ' + local jwt = require "resty.jwt" + local cjson = require "cjson" + + local function get_testcert(name) + local f = io.open("/lua-resty-jwt/testcerts/" .. name) + local contents = f:read("*all") + f:close() + return contents + end + + local table_of_jwt = { + header = { + alg = "RSA-OAEP", + enc = "A256CBC-HS512", + typ = "INVALID", + kid = "myKey" + }, + payload = { + foo = "bar" + } + } + + local success, err = pcall(function () jwt:sign( + get_testcert("cert-pubkey.pem"), + table_of_jwt + ) + end) + ngx.say(err.reason) + '; + } +--- request +GET /t +--- response_body +invalid typ: INVALID +--- no_error_log +[error] + + + +=== TEST 15: Use rsa oeap for encryption invalid key +--- http_config eval: $::HttpConfig +--- config + location /t { + content_by_lua ' + local jwt = require "resty.jwt" + local cjson = require "cjson" + + local function get_testcert(name) + local f = io.open("/lua-resty-jwt/testcerts/" .. name) + local contents = f:read("*all") + f:close() + return contents + end + + local table_of_jwt = { + header = { + alg = "RSA-OAEP", + enc = "A256CBC-HS512", + typ = "JWE", + kid = "myKey" + }, + payload = { + foo = "bar" + } + } + + local success, err = pcall(function () jwt:sign( + "invalid RSA", + table_of_jwt + ) + end) + ngx.say(err.reason) + '; + } +--- request +GET /t +--- response_body +Decode secret is not a valid cert/public key: invalid RSA +--- no_error_log +[error] + + + +=== TEST 16: Use rsa oeap for encryption invalid enc algo +--- http_config eval: $::HttpConfig +--- config + location /t { + content_by_lua ' + local jwt = require "resty.jwt" + local cjson = require "cjson" + + local function get_testcert(name) + local f = io.open("/lua-resty-jwt/testcerts/" .. name) + local contents = f:read("*all") + f:close() + return contents + end + + local table_of_jwt = { + header = { + alg = "RSA-OAEP", + enc = "A256CBC", + typ = "JWE", + kid = "myKey" + }, + payload = { + foo = "bar" + } + } + + local success, err = pcall(function () jwt:sign( + get_testcert("cert-pubkey.pem"), + table_of_jwt + ) + end) + ngx.say(err.reason) + '; + } +--- request +GET /t +--- response_body +unsupported payload encryption algorithm :A256CBC +--- no_error_log +[error] + + + +=== TEST 17: Use rsa oeap for encryption with custom payload encoder/decoder +--- http_config eval: $::HttpConfig +--- config + location /t { + content_by_lua ' + local jwt_module = require "resty.jwt" + local cjson = require "cjson" + + local function split_string(str, delim) + local result = {} + local sep = string.format("([^%s]+)", delim) + for m in str:gmatch(sep) do + result[#result+1]=m + end + return result + end + + local jwt = jwt_module.new() + + jwt:set_payload_encoder(function(tab) + local str = "" + for i, v in ipairs(tab) do + if (i ~= 1) then + str = str .. ":" + end + str = str .. ":" .. v + end + return str + end + ) + + jwt:set_payload_decoder(function(str) + return split_string(str, ":") + end + ) + + local function get_testcert(name) + local f = io.open("/lua-resty-jwt/testcerts/" .. name) + local contents = f:read("*all") + f:close() + return contents + end + + local table_of_jwt = { + header = { + alg = "RSA-OAEP", + enc = "A256CBC-HS512", + typ = "JWE", + kid = "myKey" + }, + payload = { + "foo" , "bar" + } + } + + local jwt_token = jwt:sign(get_testcert("cert-pubkey.pem"), table_of_jwt) + local jwt_obj = jwt:verify(get_testcert("cert-key.pem"), jwt_token) + print(cjson.encode(jwt_obj)) + 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 18: Use rsa oeap with aes-256-gcm for encryption +--- http_config eval: $::HttpConfig +--- config + location /t { + content_by_lua ' + local jwt = require "resty.jwt" + local cjson = require "cjson" + + local function get_testcert(name) + local f = io.open("/lua-resty-jwt/testcerts/" .. name) + local contents = f:read("*all") + f:close() + return contents + end + + local table_of_jwt = { + header = { + alg = "RSA-OAEP", + enc = "A256GCM", + typ = "JWE", + kid = "myKey" + }, + payload = { + foo = "bar" + } + } + + local jwt_token = jwt:sign(get_testcert("cert-pubkey.pem"), table_of_jwt) + local jwt_obj = jwt:verify(get_testcert("cert-key.pem"), jwt_token) + print(cjson.encode(jwt_obj)) + 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 19: verify jwe create with rsa-oaep and aes-256-gcm with invalid tag +--- http_config eval: $::HttpConfig +--- config + location /t { + content_by_lua ' + local jwt = require "resty.jwt" + local cjson = require "cjson" + + local function get_testcert(name) + local f = io.open("/lua-resty-jwt/testcerts/" .. name) + local contents = f:read("*all") + f:close() + return contents + end + + local table_of_jwt = { + header = { + alg = "RSA-OAEP", + enc = "A256GCM", + typ = "JWE", + kid = "myKey" + }, + payload = { + foo = "bar" + } + } + + local jwt_token = "eyJlbmMiOiJBMjU2R0NNIiwia2lkIjoibXlLZXkiLCJhbGciOiJSU0EtT0FFUC0yNTYifQ" .. "." .. + "HcMWB6Gh03hYZjsrH08L69aDe8FKv6bZ8e-M8_FggGFyyRdmq1zbHchdbUKMxup1rW9HaIKlNgYpaHiWh7f_BRWAmH4oMzqop4_SmA1LN4nkz3d-P2_MBO2Rm9yVA-4Y4ju0F9QqQ7QbvPLiBknKOmKwEHzL371jN52OK5gByLEA8sSE75rIbfHVoTGtPkz_aIrDp40gcPyojMtMEy4Edm3og2yC8FZl80YRIlVeo9y5qfuwRG5IIFYv60vCdfPXzNN_OBGXUuHPr4szVAu3FV3bwXbM_EyuYPMc1crH42cXFz9zTei8eONU1xmA1H3Z2Jplgj0zUOJtLsgOSeZCwQ" .. "." .. + "mROZHYnNXD2Db6vl" .. "." .. + "iO8YLN0EiL3QfmP40Q" .. "." .. + "vlvUs6U8P6coJk1wyjwxFw" + local jwt_obj = jwt:verify(get_testcert("cert-key.pem"), jwt_token) + print(cjson.encode(jwt_obj)) + local err = "false" + if string.find(jwt_obj.reason, "failed to decrypt payload") then + err = "true" + end + ngx.say( + cjson.encode(table_of_jwt.payload) == cjson.encode(jwt_obj.payload), "\\n", + "verified: ", jwt_obj.verified, "\\n", + "error: ", err + ) + '; + } +--- request +GET /t +--- response_body +false +verified: false +error: true +--- no_error_log +[error]