diff --git a/lib/resty/evp.lua b/lib/resty/evp.lua index 584ff5a..c856878 100644 --- a/lib/resty/evp.lua +++ b/lib/resty/evp.lua @@ -316,6 +316,45 @@ local function _create_evp_ctx(self, encrypt) return self.ctx end +local evp_pkey_ctx_ptr_ptr_ct = ffi.typeof('EVP_PKEY_CTX*[1]') + +local function _create_sign_verify_ctx(self, finit_ex, fint, md_alg) + + local pkey_ctx, ppkey_ctx + if self.padding then + pkey_ctx = _C.EVP_PKEY_CTX_new(self.evp_pkey, nil) + if pkey_ctx == nil then + return nil, "pkey:_create_sign_verify_ctx EVP_PKEY_CTX_new()" + end + ffi_gc(pkey_ctx, _C.EVP_PKEY_CTX_free) + + ppkey_ctx = evp_pkey_ctx_ptr_ptr_ct() + ppkey_ctx[0] = pkey_ctx + end + + local md_ctx = ctx_new() + if md_ctx == nil then + return nil, "pkey:_create_sign_verify_ctx: EVP_MD_CTX_new() failed" + end + ctx_free(md_ctx) + + if finit_ex(md_ctx, md_alg, nil) ~= 1 then + return nil, "pkey:_create_sign_verify_ctx: Init_ex() failed" + end + + if fint(md_ctx, ppkey_ctx, md_alg, nil, self.evp_pkey) ~= 1 then + return nil, "pkey:_create_sign_verify_ctx: Init failed" + end + + if ppkey_ctx and self.padding == CONST.RSA_PKCS1_PSS_PADDING then + if _C.EVP_PKEY_CTX_ctrl(ppkey_ctx[0], CONST.EVP_PKEY_RSA, -1, CONST.EVP_PKEY_CTRL_RSA_PADDING, self.padding, nil) <= 0 then + return nil, "pkey:_create_sign_verify_ctx: EVP_PKEY_CTX_ctrl() failed" + end + end + + return md_ctx +end + local RSASigner = {algo="RSA"} _M.RSASigner = RSASigner @@ -323,7 +362,8 @@ _M.RSASigner = RSASigner -- @param pem_private_key A private key string in PEM format -- @param password password for the private key (if required) -- @returns RSASigner, err_string -function RSASigner.new(self, pem_private_key, password) +function RSASigner.new(self, pem_private_key, password, padding) + self.padding = padding return _new_key ( self, { @@ -342,29 +382,20 @@ function RSASigner.sign(self, message, digest_name) local buf = ffi_new("unsigned char[?]", 1024) local len = ffi_new("size_t[1]", 1024) - local ctx = ctx_new() - if ctx == nil then - return _err() - end - ctx_free(ctx) - local md = _C.EVP_get_digestbyname(digest_name) if md == nil then return _err() end - if _C.EVP_DigestInit_ex(ctx, md, nil) ~= 1 then + local md_ctx, err = _create_sign_verify_ctx(self, _C.EVP_DigestInit_ex, _C.EVP_DigestSignInit, md) + if err then return _err() end - local ret = _C.EVP_DigestSignInit(ctx, nil, md, nil, self.evp_pkey) - if ret ~= 1 then - return _err() - end - if _C.EVP_DigestUpdate(ctx, message, #message) ~= 1 then + if _C.EVP_DigestUpdate(md_ctx, message, #message) ~= 1 then return _err() end - if _C.EVP_DigestSignFinal(ctx, buf, len) ~= 1 then + if _C.EVP_DigestSignFinal(md_ctx, buf, len) ~= 1 then return _err() end return ffi_string(buf, len[0]), nil @@ -438,12 +469,13 @@ _M.RSAVerifier = RSAVerifier --- Create a new RSAVerifier -- @param key_source An instance of Cert or PublicKey used for verification -- @returns RSAVerifier, error_string -function RSAVerifier.new(self, key_source) +function RSAVerifier.new(self, key_source, padding) if not key_source then return nil, "You must pass in an key_source for a public key" end local evp_public_key = key_source.public_key self.evp_pkey = evp_public_key + self.padding = padding return self, nil end @@ -458,26 +490,17 @@ function RSAVerifier.verify(self, message, sig, digest_name) return _err(false) end - local ctx = ctx_new() - if ctx == nil then - return _err(false) - end - ctx_free(ctx) - - if _C.EVP_DigestInit_ex(ctx, md, nil) ~= 1 then + local md_ctx, err = _create_sign_verify_ctx(self, _C.EVP_DigestInit_ex, _C.EVP_DigestVerifyInit, md) + if err then return _err(false) end - local ret = _C.EVP_DigestVerifyInit(ctx, nil, md, nil, self.evp_pkey) - if ret ~= 1 then - return _err(false) - end - if _C.EVP_DigestUpdate(ctx, message, #message) ~= 1 then + if _C.EVP_DigestUpdate(md_ctx, message, #message) ~= 1 then return _err(false) end local sig_bin = ffi_new("unsigned char[?]", #sig) ffi_copy(sig_bin, sig, #sig) - if _C.EVP_DigestVerifyFinal(ctx, sig_bin, #sig) == 1 then + if _C.EVP_DigestVerifyFinal(md_ctx, sig_bin, #sig) == 1 then return true, nil else return false, "Verification failed" diff --git a/lib/resty/jwt.lua b/lib/resty/jwt.lua index accba11..aae890b 100644 --- a/lib/resty/jwt.lua +++ b/lib/resty/jwt.lua @@ -64,9 +64,11 @@ local str_const = { HS256 = "HS256", HS512 = "HS512", RS256 = "RS256", + RS512 = "RS512", + PS256 = "PS256", + PS512 = "PS512", ES256 = "ES256", ES512 = "ES512", - RS512 = "RS512", A128CBC_HS256 = "A128CBC-HS256", A128CBC_HS256_CIPHER_MODE = "aes-128-cbc", A256CBC_HS512 = "A256CBC-HS512", @@ -564,14 +566,19 @@ function _M.sign(self, secret_key, jwt_obj) elseif alg == str_const.HS512 then local secret_str = get_secret_str(secret_key, jwt_obj) signature = hmac:new(secret_str, hmac.ALGOS.SHA512):final(message) - elseif alg == str_const.RS256 or alg == str_const.RS512 then - local signer, err = evp.RSASigner:new(secret_key) + elseif alg == str_const.RS256 or alg == str_const.RS512 or alg == str_const.PS256 or alg == str_const.PS512 then + local signer, err + if alg == str_const.PS256 or alg == str_const.PS512 then + signer, err = evp.RSASigner:new(secret_key, nil, evp.CONST.RSA_PKCS1_PSS_PADDING) + else + signer, err = evp.RSASigner:new(secret_key) + end if not signer then error({reason="signer error: " .. err}) end - if alg == str_const.RS256 then + if alg == str_const.RS256 or alg == str_const.PS256 then signature = signer:sign(message, evp.CONST.SHA256_DIGEST) - elseif alg == str_const.RS512 then + elseif alg == str_const.RS512 or alg == str_const.PS512 then signature = signer:sign(message, evp.CONST.SHA512_DIGEST) end elseif alg == str_const.ES256 or alg == str_const.ES512 then @@ -847,7 +854,9 @@ function _M.verify_jwt_obj(self, secret, jwt_obj, ...) -- signature check jwt_obj[str_const.reason] = "signature mismatch: " .. jwt_obj[str_const.signature] end - elseif alg == str_const.RS256 or alg == str_const.RS512 or alg == str_const.ES256 or alg == str_const.ES512 then + elseif alg == str_const.RS256 or alg == str_const.RS512 or + alg == str_const.ES256 or alg == str_const.ES512 or + alg == str_const.PS256 or alg == str_const.PS512 then local cert, err if self.trusted_certs_file ~= nil then local cert_str = extract_certificate(jwt_obj, self.x5u_content_retriever) @@ -882,6 +891,8 @@ function _M.verify_jwt_obj(self, secret, jwt_obj, ...) local verifier = '' if alg == str_const.RS256 or alg == str_const.RS512 then verifier = evp.RSAVerifier:new(cert) + elseif alg == str_const.PS256 or alg == str_const.PS512 then + verifier = evp.RSAVerifier:new(cert, evp.CONST.RSA_PKCS1_PSS_PADDING) elseif alg == str_const.ES256 or alg == str_const.ES512 then verifier = evp.ECVerifier:new(cert) end @@ -906,9 +917,9 @@ function _M.verify_jwt_obj(self, secret, jwt_obj, ...) local verified = false err = "verify error: reason unknown" - if alg == str_const.RS256 or alg == str_const.ES256 then + if alg == str_const.RS256 or alg == str_const.ES256 or alg == str_const.PS256 then verified, err = verifier:verify(message, sig, evp.CONST.SHA256_DIGEST) - elseif alg == str_const.RS512 or alg == str_const.ES512 then + elseif alg == str_const.RS512 or alg == str_const.ES512 or alg == str_const.PS512 then verified, err = verifier:verify(message, sig, evp.CONST.SHA512_DIGEST) end if not verified then diff --git a/t/load-verify.t b/t/load-verify.t index 9509473..fe50225 100644 --- a/t/load-verify.t +++ b/t/load-verify.t @@ -803,4 +803,78 @@ true everything is awesome~ :p test --- no_error_log -[error] \ No newline at end of file +[error] + +=== TEST 26: Verify valid PS256 signed jwt using a rsa public key +--- http_config eval: $::HttpConfig +--- config + location /t { + content_by_lua ' + local jwt = require "resty.jwt" + + local public_key = [[ +-----BEGIN PUBLIC KEY----- +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvE+myl5JR22jRgb2+pv4 +h29IH26WdenM6FsJ7Ks2nZDdSfL8S0ahp8Tk/nrC7Zi/MMUkNeZlkLFkS5mHeeyw +v5UpGJqbP3rKnsiP/Uomt8NJufusv/HWZSpI784rGzkHy+FY5Vs/nexw9K5os/mm +wZqjsEtxZU5kfg9Ye+VCtXW3ArXstCEu2Gd93C91NEX//g67ahAI3ii6TAsml8VA +Qcl2+7pK6HPgAmcPECnhhfNaqEPIcdxOToAymCG3SLB54MIyN3kvOFjK0Ztj82U0 +qqibxZcAAw2yK07kz2sSF8VtM5B3mgE9DFpskezhSIRc+nuhHoyTn8pscisa2JNq +dQIDAQAB +-----END PUBLIC KEY----- + ]] + + jwt:set_alg_whitelist({ PS256 = 1 }) + local jwt_token = "eyJraWQiOiJlYWY3YzFkNS0xMzI1LTQ3NmMtOTdlYi03N2VkNGEzMDNhZTciLCJhbGciOiJQUzI1NiJ9.ew0KICAiaXNzIjogInRlc3QiLA0KICAiaWF0IjogMTUxNjIzOTAyMg0KfQ.n2i-wYB_XOTJSo7oIAVq_zxm5BGG7cfPTwo9ZD0agoJvLQy-D0btxkhaNJj7lJcAtxi3ffpYB2kHVcUa7YKNO6szNU1AC4r9iIgQn1wjgfLcmxVxnOvHt7EUwn6fVoNxNU7AX-s-1eMaAuIPxretYGvFFfc3kXJYWdfQcr_4LbtlG3EDg-WUetJ75JmzfZPW963TdUWZ17uyPf8TjwLDpJl0OyPDAvo-sh4J2ySj43VVNpEhR50tqE2FrHM6mz1d9MliNU9HbUWkEdbpLwmDrHgfpaaKyWMKCWkxiOpcxivfw9CmvrYciQg1VWYDp149yvEUOLjytZ4NXRVSbSKtnw" + + local jwt_obj = jwt:verify(public_key, jwt_token) + ngx.say(jwt_obj["verified"]) + ngx.say(jwt_obj["reason"]) + ngx.say(jwt_obj["payload"]["iss"]) + '; + } +--- request +GET /t +--- response_body +true +everything is awesome~ :p +test +--- no_error_log +[error] + +=== TEST 25: Verify valid PS512 signed jwt using a rsa public key +--- http_config eval: $::HttpConfig +--- config + location /t { + content_by_lua ' + local jwt = require "resty.jwt" + + local public_key = [[ +-----BEGIN PUBLIC KEY----- +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqy2xX7I8txBV5WXLebw/ +pVzbnDNQ4dbR+Cqe0VHB6H9QIztM36AN6a8WX+5er71lsSEHoz5ivTOju5INBMft +44fD+UAdWWLtFq17PX2EGNLkO0yQSNh+nb6MCym0Q+wShyMBPWfJSPv6eU4Ixx1r +DfyNMkZ2mhoWqAIahCI+Sz7DMJ60h4k7DRWQX9kfQbMC+0suyNolAiBOYlHdqIIC +t6FtpnTaLDHNx5ExavD/wtcdycj3z/G+zUec4hVI5j/DUdkg/9IwkzUvC9HV4Qek +pdWoqSSrJKHruf0nqJ15vupBgalbuRePJg7p/9XjtTPP9Tm9g99Ikwqf4eIrb/Uo +AQIDAQAB +-----END PUBLIC KEY----- + ]] + + jwt:set_alg_whitelist({ PS512 = 1 }) + local jwt_token = "eyJraWQiOiI4MTlmMmY5OS0xNDY4LTQyYmItOGIyMy00ZTkyOWUwNWJjMDQiLCJhbGciOiJQUzUxMiJ9.ew0KICAiaXNzIjogInRlc3QiLA0KICAiaWF0IjogMTUxNjIzOTAyMg0KfQ.hHoWVr_Slu1xpe4ehqsvmeIqsKxJVS6MLV-58rw08zQvhIVjzoUjRr5QkdakxA_0NF6ubJUztprxfclFfBNO1tLAMv1BLujXdSTgWDxG9Vr4hhDRIJY53xrUY6ozIPBsZKYPRJQjoOMk3EotI8vPRNYyj778EmwT3nNr1W9kRjpKa9lKmcAIwSDeYN_7BrvJk7oqpRyQDfEkcOaEsVwgvgz2lGMzoA-6NXzLplG6JC8I7Ncq3ympV80e0TZ3YnNR_74If_yIh57M0EOl8c4C0YWGNaFTFXw18zpRgHS6svqpZ3LNXM8O-yBPaixfV1Dr9qJvA6Q_u8okDY618S_iqw" + + local jwt_obj = jwt:verify(public_key, jwt_token) + ngx.say(jwt_obj["verified"]) + ngx.say(jwt_obj["reason"]) + ngx.say(jwt_obj["payload"]["iss"]) + '; + } +--- request +GET /t +--- response_body +true +everything is awesome~ :p +test +--- no_error_log +[error] diff --git a/t/sign-verify.t b/t/sign-verify.t index a60f68e..49f6bd4 100644 --- a/t/sign-verify.t +++ b/t/sign-verify.t @@ -419,6 +419,8 @@ everything is awesome~ :p bar --- no_error_log [error] + + === TEST 13: JWT simple verify failure --- http_config eval: $::HttpConfig --- config @@ -450,6 +452,7 @@ Verification failed --- no_error_log [error] + === TEST 14: JWT sign and verify ES512 --- http_config eval: $::HttpConfig --- config @@ -493,6 +496,7 @@ bar --- no_error_log [error] + === TEST 15: JWT sign and verify RS512 --- http_config eval: $::HttpConfig --- config @@ -537,4 +541,157 @@ true everything is awesome~ :p bar --- no_error_log -[error] \ No newline at end of file +[error] + + +=== TEST 16: JWT sign and verify PS256 +--- http_config eval: $::HttpConfig +--- config + location /t { + content_by_lua ' + local jwt = require "resty.jwt" + + local function get_testcert(name) + local f = io.open("/lua-resty-jwt/testcerts/" .. name) + local contents = f:read("*all") + f:close() + return contents + end + + -- x5c wants a base64 encoded der, not pem.. aka, the pem minus the header+footer + local pubkey_pem = get_testcert("cert.pem") + local ssl = require "ngx.ssl" + local der, err = ssl.cert_pem_to_der(pubkey_pem) + local jwt_token = jwt:sign( + get_testcert("cert-key.pem"), + { + header={ + typ="JWT", + alg="PS256", + x5c={ + ngx.encode_base64(der), + } }, + payload={foo="bar", exp=9999999999} + } + ) + + local jwt_obj = jwt:verify(get_testcert("cert.pem"), jwt_token) + ngx.say(jwt_obj["verified"]) + ngx.say(jwt_obj["reason"]) + ngx.say(jwt_obj["payload"]["foo"]) + '; + } +--- request +GET /t +--- response_body +true +everything is awesome~ :p +bar +--- no_error_log +[error] + + +=== TEST 17: JWT sign and verify PS256 - Take 2 +--- http_config eval: $::HttpConfig +--- config + location /t { + content_by_lua ' + local jwt = require "resty.jwt" + + 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 function get_public_key(url, iss, kid) + if iss ~= "Authz" then + error("No issuer. Duh :(") + end + + if kid ~= "IamAPubl1cKeV" then + error("No key identifier. Duh :(") + end + + return get_testcert("cert.pem") + end + + jwt:set_trusted_certs_file("/lua-resty-jwt/testcerts/root.pem") + jwt:set_alg_whitelist({ PS256 = 1 }) + jwt:set_x5u_content_retriever(get_public_key) + + local jwt_token = jwt:sign( + get_testcert("cert-key.pem"), + { + header={ + typ="JWT", + alg="PS256", + x5u="https://dummy.com/certs", + kid="IamAPubl1cKeV", + }, + payload={foo="bar", iss="Authz", exp=9999999999} + } + ) + + local jwt_obj = jwt:verify(nil, jwt_token) + ngx.say(jwt_obj["verified"]) + ngx.say(jwt_obj["reason"]) + ngx.say(jwt_obj["payload"]["foo"]) + '; + } +--- request +GET /t +--- response_body +true +everything is awesome~ :p +bar +--- no_error_log +[error] + + +=== TEST 18: JWT sign and verify PS512 +--- http_config eval: $::HttpConfig +--- config + location /t { + content_by_lua ' + local jwt = require "resty.jwt" + + local function get_testcert(name) + local f = io.open("/lua-resty-jwt/testcerts/" .. name) + local contents = f:read("*all") + f:close() + return contents + end + + -- x5c wants a base64 encoded der, not pem.. aka, the pem minus the header+footer + local pubkey_pem = get_testcert("cert.pem") + local ssl = require "ngx.ssl" + local der, err = ssl.cert_pem_to_der(pubkey_pem) + local jwt_token = jwt:sign( + get_testcert("cert-key.pem"), + { + header={ + typ="JWT", + alg="PS512", + x5c={ + ngx.encode_base64(der), + } }, + payload={foo="bar", exp=9999999999} + } + ) + + local jwt_obj = jwt:verify(get_testcert("cert.pem"), jwt_token) + ngx.say(jwt_obj["verified"]) + ngx.say(jwt_obj["reason"]) + ngx.say(jwt_obj["payload"]["foo"]) + '; + } +--- request +GET /t +--- response_body +true +everything is awesome~ :p +bar +--- no_error_log +[error]