From 0fa492907757e44578bce7d65d16997ff3361a7c Mon Sep 17 00:00:00 2001 From: Abhishek Choudhary Date: Mon, 12 Oct 2026 13:50:10 +0545 Subject: [PATCH] fix(evp): NULL-safe FFI error handling and key type checks Port of the evp.lua fixes from cdbattags/lua-resty-jwt v0.4.0. - Check the key type against the alg family before verifying, signing or encrypting: ES* needs an EC key, RS* and RSA encryption need an RSA key. A mismatch returns "key is not an EC key" or "key is not an RSA key" instead of dereferencing NULL. - _err falls back to ERR_error_string_n when an error code has no reason string, so an unloadable trusted certs file no longer crashes the worker. - NULL-check BIO_new, PEM_read_bio_*PrivateKey, d2i_ECDSA_SIG, BN_new, ECDSA_SIG_new, BN_bin2bn, EC_GROUP_get_order, i2d_ECDSA_SIG and X509_STORE_CTX_new, and validate argument types. - get_raw_sig rejects trailing data and r/s larger than the curve order. - verify_jwt_obj reports verify_trust's error instead of a nil one. Co-authored-by: spacewander Co-authored-by: Christian Battaglia --- lib/resty/evp.lua | 288 +++++++++++++++++++++++++++++++--------- lib/resty/jwt.lua | 4 +- t/evp-hardening.t | 328 ++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 553 insertions(+), 67 deletions(-) create mode 100644 t/evp-hardening.t diff --git a/lib/resty/evp.lua b/lib/resty/evp.lua index 40c4545..671b3fe 100644 --- a/lib/resty/evp.lua +++ b/lib/resty/evp.lua @@ -28,7 +28,10 @@ local CONST = { EVP_PKEY_CTRL_RSA_PADDING = 0x1000 + 1, EVP_PKEY_OP_TYPE_CRYPT = 768, - EVP_PKEY_CTRL_RSA_OAEP_MD = 0x1000 + 9 + EVP_PKEY_CTRL_RSA_OAEP_MD = 0x1000 + 9, + + EVP_PKEY_RSA_PSS = 912, + EVP_PKEY_EC = 408 } _M.CONST = CONST @@ -37,7 +40,9 @@ _M.CONST = CONST ffi.cdef[[ // Error handling unsigned long ERR_get_error(void); +void ERR_clear_error(void); const char * ERR_reason_error_string(unsigned long e); +void ERR_error_string_n(unsigned long e, char *buf, size_t len); // Basic IO typedef struct bio_st BIO; @@ -196,7 +201,16 @@ local function _err(ret) local errs = {} local code = _C.ERR_get_error() while code ~= 0 do - table.insert(errs, 1, ffi_string(_C.ERR_reason_error_string(code))) + -- Some codes (e.g. system library errors) have no reason string + local reason = _C.ERR_reason_error_string(code) + if reason ~= nil then + reason = ffi_string(reason) + else + local buf = ffi_new("char[?]", 256) + _C.ERR_error_string_n(code, buf, 256) + reason = ffi_string(buf) + end + table.insert(errs, 1, reason) code = _C.ERR_get_error() end @@ -206,6 +220,97 @@ local function _err(ret) return ret, table.concat(errs, ": ") end +-- OpenSSL 3.x renamed EVP_PKEY_base_id to EVP_PKEY_get_base_id and only +-- keeps the old name as a macro, so resolve whichever symbol exists. +local _pkey_base_id +for _, name in ipairs({ "EVP_PKEY_get_base_id", "EVP_PKEY_base_id" }) do + pcall(ffi.cdef, "int " .. name .. "(const EVP_PKEY *pkey);") + local ok, fn = pcall(function() return _C[name] end) + if ok then + _pkey_base_id = fn + break + end +end + +--- Check that an EVP_PKEY is non-NULL and of one of the given base types +-- @returns true or nil, error_string +local function _check_pkey_type(pkey, expected, name) + if pkey == nil then + return nil, "no key loaded" + end + if not _pkey_base_id then + -- Cannot tell, let OpenSSL reject a mismatching key later on + return true + end + local id = _pkey_base_id(pkey) + for _, t in ipairs(expected) do + if id == t then + return true + end + end + return nil, "key is not an " .. name .. " key" +end + +local RSA_KEY_TYPES = { CONST.EVP_PKEY_RSA, CONST.EVP_PKEY_RSA_PSS } +local EC_KEY_TYPES = { CONST.EVP_PKEY_EC } + +--- Get the EC_KEY of an EVP_PKEY, or nil, error_string if it isn't one +local function _get_ec_key(pkey) + local ok, err = _check_pkey_type(pkey, EC_KEY_TYPES, "EC") + if not ok then + return nil, err + end + local ec = _C.EVP_PKEY_get0_EC_KEY(pkey) + if ec == nil then + _C.ERR_clear_error() + return nil, "key is not an EC key" + end + return ec +end + +--- Get the byte size of the order of the curve of an EC_KEY +local function _ec_order_size(ec) + local ecgroup = _C.EC_KEY_get0_group(ec) + if ecgroup == nil then + return nil, "EC key has no group" + end + + local order = _C.BN_new() + if order == nil then + return _err() + end + ffi_gc(order, _C.BN_free) + + -- returns 0 if the curve is not found + if _C.EC_GROUP_get_order(ecgroup, order, nil) ~= 1 then + return _err() + end + + -- BN_num_bytes is a #define, so have to use BN_num_bits + local order_size_bytes = math.floor((_C.BN_num_bits(order)+7)/8) + if order_size_bytes <= 0 then + return nil, "EC key has an invalid group order" + end + return order_size_bytes +end + +--- Create a memory BIO holding the given string +local function _new_mem_bio(payload, as_binary) + local bio = _C.BIO_new(_C.BIO_s_mem()) + if bio == nil then + return _err() + end + ffi_gc(bio, _C.BIO_vfree) + if as_binary then + if _C.BIO_write(bio, payload, #payload) < 0 then + return _err() + end + elseif _C.BIO_puts(bio, payload) < 0 then + return _err() + end + return bio +end + local ctx_new, ctx_free local openssl11, e = pcall(function () local ctx = _C.EVP_MD_CTX_new() @@ -232,10 +337,12 @@ else end local function _new_key(self, opts) - local bio = _C.BIO_new(_C.BIO_s_mem()) - ffi_gc(bio, _C.BIO_vfree) - if _C.BIO_puts(bio, opts.pem_private_key) < 0 then - return _err() + if type(opts.pem_private_key) ~= "string" then + return nil, "Must pass a PEM private key" + end + local bio, err = _new_mem_bio(opts.pem_private_key) + if bio == nil then + return nil, err end local pass @@ -247,15 +354,21 @@ local function _new_key(self, opts) local key = nil if self.algo == "RSA" then - key = _C.PEM_read_bio_RSAPrivateKey(bio, nil, nil, pass) - ffi_gc(key, _C.RSA_free) + key = _C.PEM_read_bio_RSAPrivateKey(bio, nil, nil, pass) + if key == nil then + return _err() + end + ffi_gc(key, _C.RSA_free) elseif self.algo == "ECDSA" then key = _C.PEM_read_bio_ECPrivateKey(bio, nil, nil, pass) + if key == nil then + return _err() + end ffi_gc(key, _C.EC_KEY_free) end - if not key then - return _err() + if key == nil then + return nil, "Unsupported key algorithm" end local evp_pkey = _C.EVP_PKEY_new() @@ -394,30 +507,45 @@ end -- @param signature The ASN.1 DER signature -- @returns signature, error_string function ECSigner.get_raw_sig(self, signature) - if not signature then + if type(signature) ~= "string" or #signature == 0 then return nil, "Must pass a signature to convert" end - local buf = ffi_new("const unsigned char*", signature) - local buf_ptr = ffi_new("const unsigned char*[1]", buf) - local sig = _C.d2i_ECDSA_SIG(nil, buf_ptr, #signature) + -- Ensure we copy the BN in a padded form + local ec, err = _get_ec_key(self.evp_pkey) + if ec == nil then + return nil, err + end + local order_size_bytes + order_size_bytes, err = _ec_order_size(ec) + if not order_size_bytes then + return nil, err + end + + local sig_ptr = ffi_new("const unsigned char *[1]") + local sig_bin = ffi_new("unsigned char [?]", #signature) + ffi_copy(sig_bin, signature, #signature) + + sig_ptr[0] = sig_bin + local sig = _C.d2i_ECDSA_SIG(nil, sig_ptr, #signature) + if sig == nil then + _C.ERR_clear_error() + return nil, "Invalid DER signature" + end ffi_gc(sig, _C.ECDSA_SIG_free) + if sig_ptr[0] ~= sig_bin + #signature then + return nil, "Invalid DER signature: trailing data" + end + if sig.r == nil or sig.s == nil then + return nil, "Invalid DER signature" + end local rbytes = math.floor((_C.BN_num_bits(sig.r)+7)/8) local sbytes = math.floor((_C.BN_num_bits(sig.s)+7)/8) + if rbytes > order_size_bytes or sbytes > order_size_bytes then + return nil, "Invalid DER signature: r or s larger than curve order" + end - -- Ensure we copy the BN in a padded form - local ec = _C.EVP_PKEY_get0_EC_KEY(self.evp_pkey) - local ecgroup = _C.EC_KEY_get0_group(ec) - - local order = _C.BN_new() - ffi_gc(order, _C.BN_free) - - -- res is an int, if 0, curve not found - local res = _C.EC_GROUP_get_order(ecgroup, order, nil) - - -- BN_num_bytes is a #define, so have to use BN_num_bits - local order_size_bytes = math.floor((_C.BN_num_bits(order)+7)/8) local resbuf_len = order_size_bytes *2 local resbuf = ffi_new("unsigned char[?]", resbuf_len) @@ -437,7 +565,7 @@ _M.RSAVerifier = RSAVerifier -- @param key_source An instance of Cert or PublicKey used for verification -- @returns RSAVerifier, error_string function RSAVerifier.new(self, key_source) - if not key_source then + if type(key_source) ~= "table" or key_source.public_key == nil then return nil, "You must pass in an key_source for a public key" end local evp_public_key = key_source.public_key @@ -450,7 +578,11 @@ end -- @param the signature to verify -- @param digest_name The digest type that was used to sign -- @returns bool, error_string -function RSAVerifier.verify(self, message, sig, digest_name) +local function _verify(self, message, sig, digest_name) + if type(sig) ~= "string" then + return false, "Must pass a signature to verify" + end + local md = _C.EVP_get_digestbyname(digest_name) if md == nil then return _err(false) @@ -478,10 +610,20 @@ function RSAVerifier.verify(self, message, sig, digest_name) if _C.EVP_DigestVerifyFinal(ctx, sig_bin, #sig) == 1 then return true, nil else + -- Don't leave decoding errors in the queue for the next caller + _C.ERR_clear_error() return false, "Verification failed" end end +function RSAVerifier.verify(self, message, sig, digest_name) + local ok, err = _check_pkey_type(self.evp_pkey, RSA_KEY_TYPES, "RSA") + if not ok then + return false, err + end + return _verify(self, message, sig, digest_name) +end + local ECVerifier = {} _M.ECVerifier = ECVerifier --- Create a new ECVerifier @@ -502,28 +644,26 @@ function ECVerifier.verify(self, message, sig, digest_name) if not der_sig then return nil, err end - return RSAVerifier.verify(self, message, der_sig, digest_name) + return _verify(self, message, der_sig, digest_name) end --- Converts a RAW r,s signature to ASN.1 DER signature (ECDSA) -- @param signature The raw signature -- @returns signature, error_string function ECVerifier.get_der_sig(self, signature) - if not signature then + if type(signature) ~= "string" then return nil, "Must pass a signature to convert" end -- inspired from https://bit.ly/2yZxzxJ - local ec = _C.EVP_PKEY_get0_EC_KEY(self.evp_pkey) - local ecgroup = _C.EC_KEY_get0_group(ec) - - local order = _C.BN_new() - ffi_gc(order, _C.BN_free) - - -- res is an int, if 0, curve not found - local res = _C.EC_GROUP_get_order(ecgroup, order, nil) - - -- BN_num_bytes is a #define, so have to use BN_num_bits - local order_size_bytes = math.floor((_C.BN_num_bits(order)+7)/8) + local ec, err = _get_ec_key(self.evp_pkey) + if ec == nil then + return nil, err + end + local order_size_bytes + order_size_bytes, err = _ec_order_size(ec) + if not order_size_bytes then + return nil, err + end if #signature ~= 2 * order_size_bytes then return nil, "signature length != 2 * order length" @@ -532,6 +672,9 @@ function ECVerifier.get_der_sig(self, signature) local sig_bytes = ffi_new("unsigned char [?]", #signature) ffi_copy(sig_bytes, signature, #signature) local ecdsa = _C.ECDSA_SIG_new() + if ecdsa == nil then + return _err() + end ffi_gc(ecdsa, _C.ECDSA_SIG_free) -- Those do not need to be GCed as they are cleared by the ECDSA_SIG_free() @@ -540,14 +683,23 @@ function ECVerifier.get_der_sig(self, signature) ecdsa.r = r ecdsa.s = s + if r == nil or s == nil then + return _err() + end -- Gives us the buffer size to allocate local der_len = _C.i2d_ECDSA_SIG(ecdsa, nil) + if der_len <= 0 then + return _err() + end local der_sig_ptr = ffi_new("unsigned char *[1]") local der_sig_bin = ffi_new("unsigned char [?]", der_len) der_sig_ptr[0] = der_sig_bin der_len = _C.i2d_ECDSA_SIG(ecdsa, der_sig_ptr) + if der_len <= 0 then + return _err() + end local der_str = ffi_string(der_sig_bin, der_len) return der_str, nil @@ -562,21 +714,18 @@ _M.Cert = Cert -- @param payload A PEM or DER format X509 certificate -- @returns Cert, error_string function Cert.new(self, payload) - if not payload then + if type(payload) ~= "string" then return nil, "Must pass a PEM or binary DER cert" end - local bio = _C.BIO_new(_C.BIO_s_mem()) - ffi_gc(bio, _C.BIO_vfree) + local is_pem = payload:find('-----BEGIN') ~= nil + local bio, err = _new_mem_bio(payload, not is_pem) + if bio == nil then + return nil, err + end local x509 - if payload:find('-----BEGIN') then - if _C.BIO_puts(bio, payload) < 0 then - return _err() - end + if is_pem then x509 = _C.PEM_read_bio_X509(bio, nil, nil, nil) else - if _C.BIO_write(bio, payload, #payload) < 0 then - return _err() - end x509 = _C.d2i_X509_bio(bio, nil) end if x509 == nil then @@ -584,8 +733,9 @@ function Cert.new(self, payload) end ffi_gc(x509, _C.X509_free) self.x509 = x509 - local public_key, err = self:get_public_key() - if not public_key then + local public_key + public_key, err = self:get_public_key() + if public_key == nil then return nil, err end @@ -642,6 +792,12 @@ end -- @param trusted_cert_file File path to a list of PEM encoded trusted certificates -- @return bool, error_string function Cert.verify_trust(self, trusted_cert_file) + if type(trusted_cert_file) ~= "string" then + return false, "Must pass a trusted certs file path" + end + if self.x509 == nil then + return false, "No certificate loaded" + end local store = _C.X509_STORE_new() if store == nil then return _err(false) @@ -652,7 +808,7 @@ function Cert.verify_trust(self, trusted_cert_file) end local ctx = _C.X509_STORE_CTX_new() - if store == nil then + if ctx == nil then return _err(false) end ffi_gc(ctx, _C.X509_STORE_CTX_free) @@ -664,6 +820,7 @@ function Cert.verify_trust(self, trusted_cert_file) local code = _C.X509_STORE_CTX_get_error(ctx) local msg = ffi_string(_C.X509_verify_cert_error_string(code)) _C.X509_STORE_CTX_cleanup(ctx) + _C.ERR_clear_error() return false, msg end _C.X509_STORE_CTX_cleanup(ctx) @@ -683,21 +840,18 @@ _M.PublicKey = PublicKey -- @param payload A PEM or DER format public key file -- @return PublicKey, error_string function PublicKey.new(self, payload) - if not payload then + if type(payload) ~= "string" then return nil, "Must pass a PEM or binary DER public key" end - local bio = _C.BIO_new(_C.BIO_s_mem()) - ffi_gc(bio, _C.BIO_vfree) + local is_pem = payload:find('-----BEGIN') ~= nil + local bio, err = _new_mem_bio(payload, not is_pem) + if bio == nil then + return nil, err + end local pkey - if payload:find('-----BEGIN') then - if _C.BIO_puts(bio, payload) < 0 then - return _err() - end + if is_pem then pkey = _C.PEM_read_bio_PUBKEY(bio, nil, nil, nil) else - if _C.BIO_write(bio, payload, #payload) < 0 then - return _err() - end pkey = _C.d2i_PUBKEY_bio(bio, nil) end if pkey == nil then @@ -717,7 +871,7 @@ _M.RSAEncryptor = RSAEncryptor -- @param digest_alg digest algorithm to use -- @returns RSAEncryptor, err_string function RSAEncryptor.new(self, key_source, padding, digest_alg) - if not key_source then + if type(key_source) ~= "table" or key_source.public_key == nil then return nil, "You must pass in an key_source for a public key" end local evp_public_key = key_source.public_key @@ -733,6 +887,10 @@ end -- @param payload plain text payload -- @returns encrypted payload, error_string function RSAEncryptor.encrypt(self, payload) + local ok, err = _check_pkey_type(self.evp_pkey, RSA_KEY_TYPES, "RSA") + if not ok then + return nil, err + end local ctx, err_str = _create_evp_ctx(self, true) diff --git a/lib/resty/jwt.lua b/lib/resty/jwt.lua index 226a808..3b129e7 100644 --- a/lib/resty/jwt.lua +++ b/lib/resty/jwt.lua @@ -871,9 +871,9 @@ function _M.verify_jwt_obj(self, secret, jwt_obj, ...) return jwt_obj end -- Try validating against trusted CA's, then a cert passed as secret - local trusted = cert:verify_trust(self.trusted_certs_file) + local trusted, trust_err = cert:verify_trust(self.trusted_certs_file) if not trusted then - jwt_obj[str_const.reason] = "Cert used to sign the JWT isn't trusted: " .. err + jwt_obj[str_const.reason] = "Cert used to sign the JWT isn't trusted: " .. (trust_err or "unknown error") return jwt_obj end elseif secret ~= nil then diff --git a/t/evp-hardening.t b/t/evp-hardening.t new file mode 100644 index 0000000..72db374 --- /dev/null +++ b/t/evp-hardening.t @@ -0,0 +1,328 @@ +use Test::Nginx::Socket::Lua; + +repeat_each(2); + +plan tests => repeat_each() * (3 * blocks()); + +our $HttpConfig = <<'_EOC_'; + lua_package_path 'lib/?.lua;;'; + init_worker_by_lua_block { + local h = {} + function h.cert(name) + local f = assert(io.open("/lua-resty-jwt/testcerts/" .. name)) + local contents = f:read("*all") + f:close() + return contents + end + function h.b64url(s) + return (ngx.encode_base64(s):gsub('+', '-'):gsub('/', '_'):gsub('=', '')) + end + -- signature bytes don't matter, the key type is checked first + function h.token(alg, sig_len) + return h.b64url('{"typ":"JWT","alg":"' .. alg .. '"}') .. + "." .. h.b64url('{"sub":"test"}') .. + "." .. h.b64url(string.rep("A", sig_len)) + end + package.loaded["evp_hardening_helpers"] = h + } +_EOC_ + +no_long_string(); + +run_tests(); + +__DATA__ + + +=== TEST 1: ES256 token verified with an RSA public key fails cleanly +--- http_config eval: $::HttpConfig +--- config + location /t { + content_by_lua_block { + local jwt = require "resty.jwt" + local h = require "evp_hardening_helpers" + local jwt_obj = jwt:verify(h.cert("pubkey.pem"), h.token("ES256", 64)) + ngx.say(jwt_obj.verified) + ngx.say(jwt_obj.reason) + } + } +--- request +GET /t +--- response_body +false +key is not an EC key +--- no_error_log +[error] + + +=== TEST 2: ES512 token verified with an RSA public key and certificate fails cleanly +--- http_config eval: $::HttpConfig +--- config + location /t { + content_by_lua_block { + local jwt = require "resty.jwt" + local h = require "evp_hardening_helpers" + for _, name in ipairs({ "pubkey.pem", "cert.pem" }) do + local jwt_obj = jwt:verify(h.cert(name), h.token("ES512", 132)) + ngx.say(jwt_obj.verified, " ", jwt_obj.reason) + end + } + } +--- request +GET /t +--- response_body +false key is not an EC key +false key is not an EC key +--- no_error_log +[error] + + +=== TEST 3: RS256 token verified with an EC public key and certificate is rejected by key type +--- http_config eval: $::HttpConfig +--- config + location /t { + content_by_lua_block { + local jwt = require "resty.jwt" + local h = require "evp_hardening_helpers" + for _, name in ipairs({ "ec_cert_p521_pubkey.pem", "ec_cert.pem" }) do + local jwt_obj = jwt:verify(h.cert(name), h.token("RS256", 256)) + ngx.say(jwt_obj.verified, " ", jwt_obj.reason) + end + } + } +--- request +GET /t +--- response_body +false key is not an RSA key +false key is not an RSA key +--- no_error_log +[error] + + +=== TEST 4: ES256 signing with an RSA private key returns a signer error +--- http_config eval: $::HttpConfig +--- config + location /t { + content_by_lua_block { + local jwt = require "resty.jwt" + local h = require "evp_hardening_helpers" + for _, name in ipairs({ "privatekey.pem", "cert-key.pem" }) do + local ok, err = pcall(jwt.sign, jwt, h.cert(name), + { header = { typ = "JWT", alg = "ES256" }, payload = { foo = "bar" } }) + ngx.say(ok, " ", type(err) == "table" + and err.reason:match("^signer error: ") ~= nil) + end + } + } +--- request +GET /t +--- response_body +false true +false true +--- no_error_log +[error] + + +=== TEST 5: signers reject a missing private key +--- http_config eval: $::HttpConfig +--- config + location /t { + content_by_lua_block { + local evp = require "resty.evp" + ngx.say("RSASigner: ", evp.RSASigner:new(nil)) + ngx.say("ECSigner: ", evp.ECSigner:new(nil)) + } + } +--- request +GET /t +--- response_body +RSASigner: nilMust pass a PEM private key +ECSigner: nilMust pass a PEM private key +--- no_error_log +[error] + + +=== TEST 6: ECSigner.get_raw_sig rejects malformed DER +--- http_config eval: $::HttpConfig +--- config + location /t { + content_by_lua_block { + local evp = require "resty.evp" + local h = require "evp_hardening_helpers" + local signer = assert(evp.ECSigner:new(h.cert("ec_cert-key.pem"))) + local cases = { + { "empty", "" }, + { "garbage", "not a DER signature" }, + { "truncated", "\48\6\2\1\1\2\1" }, + { "wrong tag", "\4\6\2\1\1\2\1\1" }, + } + for _, c in ipairs(cases) do + local raw, err = signer:get_raw_sig(c[2]) + ngx.say(c[1], ": ", raw, " ", err) + end + } + } +--- request +GET /t +--- response_body +empty: nil Must pass a signature to convert +garbage: nil Invalid DER signature +truncated: nil Invalid DER signature +wrong tag: nil Invalid DER signature +--- no_error_log +[error] + + +=== TEST 7: ECSigner.get_raw_sig rejects r/s larger than the curve order and trailing data +--- http_config eval: $::HttpConfig +--- config + location /t { + content_by_lua_block { + local evp = require "resty.evp" + local h = require "evp_hardening_helpers" + local signer = assert(evp.ECSigner:new(h.cert("ec_cert-key.pem"))) + -- SEQUENCE { INTEGER r (200 bytes), INTEGER 1 } + local r = "\0" .. string.rep("\255", 199) + local body = "\2\129\200" .. r .. "\2\1\1" + local raw, err = signer:get_raw_sig("\48\129" .. string.char(#body) .. body) + ngx.say("oversized r: ", raw, " ", err) + + local der = assert(signer:sign("hello", evp.CONST.SHA256_DIGEST)) + raw, err = signer:get_raw_sig(der .. "\0") + ngx.say("trailing data: ", raw, " ", err) + + raw, err = signer:get_raw_sig(der) + ngx.say("valid: ", #raw, " ", err) + local verifier = assert(evp.ECVerifier:new( + assert(evp.Cert:new(h.cert("ec_cert.pem"))))) + ngx.say("verifies: ", (verifier:verify("hello", raw, evp.CONST.SHA256_DIGEST))) + } + } +--- request +GET /t +--- response_body +oversized r: nil Invalid DER signature: r or s larger than curve order +trailing data: nil Invalid DER signature: trailing data +valid: 64 nil +verifies: true +--- no_error_log +[error] + + +=== TEST 8: ECSigner.get_raw_sig with an RSA key fails cleanly +--- http_config eval: $::HttpConfig +--- config + location /t { + content_by_lua_block { + local evp = require "resty.evp" + local h = require "evp_hardening_helpers" + local ec_signer = assert(evp.ECSigner:new(h.cert("ec_cert-key.pem"))) + local der = assert(ec_signer:sign("hello", evp.CONST.SHA256_DIGEST)) + local rsa_signer = assert(evp.RSASigner:new(h.cert("privatekey.pem"))) + ngx.say(evp.ECSigner.get_raw_sig(rsa_signer, der)) + } + } +--- request +GET /t +--- response_body +nilkey is not an EC key +--- no_error_log +[error] + + +=== TEST 9: verifiers and encryptor reject a key source without a public key +--- http_config eval: $::HttpConfig +--- config + location /t { + content_by_lua_block { + local evp = require "resty.evp" + ngx.say("RSAVerifier: ", evp.RSAVerifier:new({})) + ngx.say("ECVerifier: ", evp.ECVerifier:new({})) + ngx.say("RSAEncryptor: ", evp.RSAEncryptor:new({})) + } + } +--- request +GET /t +--- response_body +RSAVerifier: nilYou must pass in an key_source for a public key +ECVerifier: nilYou must pass in an key_source for a public key +RSAEncryptor: nilYou must pass in an key_source for a public key +--- no_error_log +[error] + + +=== TEST 10: RSAEncryptor with an EC public key is rejected by key type +--- http_config eval: $::HttpConfig +--- config + location /t { + content_by_lua_block { + local evp = require "resty.evp" + local h = require "evp_hardening_helpers" + local pub = assert(evp.PublicKey:new(h.cert("ec_cert_p521_pubkey.pem"))) + local enc = assert(evp.RSAEncryptor:new(pub)) + ngx.say(enc:encrypt("secret")) + } + } +--- request +GET /t +--- response_body +nilkey is not an RSA key +--- no_error_log +[error] + + +=== TEST 11: Cert.verify_trust with an unloadable trusted certs file fails cleanly +--- http_config eval: $::HttpConfig +--- config + location /t { + content_by_lua_block { + local evp = require "resty.evp" + local h = require "evp_hardening_helpers" + local cert = assert(evp.Cert:new(h.cert("cert.pem"))) + local ok, err = cert:verify_trust("/nonexistent/trusted.pem") + ngx.say("missing file: ", ok, " ", err:find("No such file or directory", 1, true) ~= nil) + ok, err = cert:verify_trust("/lua-resty-jwt/testcerts") + ngx.say("directory: ", ok, " ", type(err)) + ngx.say("nil: ", cert:verify_trust(nil)) + ngx.say("trusted: ", (cert:verify_trust("/lua-resty-jwt/testcerts/root.pem"))) + } + } +--- request +GET /t +--- response_body +missing file: false true +directory: false string +nil: falseMust pass a trusted certs file path +trusted: true +--- no_error_log +[error] + + +=== TEST 12: x5c token with an unloadable trusted certs file returns a reason +--- http_config eval: $::HttpConfig +--- config + location /t { + content_by_lua_block { + local jwt = require "resty.jwt" + local h = require "evp_hardening_helpers" + local ssl = require "ngx.ssl" + local der = assert(ssl.cert_pem_to_der(h.cert("cert.pem"))) + local token = jwt:sign(h.cert("cert-key.pem"), { + header = { typ = "JWT", alg = "RS256", x5c = { ngx.encode_base64(der) } }, + payload = { foo = "bar" }, + }) + jwt:set_trusted_certs_file("/nonexistent/trusted.pem") + local jwt_obj = jwt:verify(nil, token) + jwt:set_trusted_certs_file(nil) + ngx.say(jwt_obj.verified) + local prefix = "Cert used to sign the JWT isn't trusted: " + ngx.say(jwt_obj.reason:sub(1, #prefix) == prefix and #jwt_obj.reason > #prefix) + } + } +--- request +GET /t +--- response_body +false +true +--- no_error_log +[error]