Skip to content

Commit ff0d02c

Browse files
committed
fix(pkey) use EVP_PKEY_fromdata to load JWK
This fixes the key not able to be called get/set_params later
1 parent 8d0b908 commit ff0d02c

File tree

8 files changed

+287
-24
lines changed

8 files changed

+287
-24
lines changed

README.md

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -861,12 +861,13 @@ pkey.new(pem_or_der_text, {
861861

862862
When loading JWK, there are couple of caveats:
863863
- Make sure the encoded JSON text is passed in, it must have been base64 decoded.
864-
- Constraint `type` on JWK key is not supported, the parameters
865-
in provided JSON will decide if a private or public key is loaded.
864+
- When using OpenSSL 1.1.1 or lua-resty-openssl earlier than 1.6.0, constraint `type`
865+
on JWK key is only supported on OpenSSL 3.x and lua-resty-openssl 1.6.0.
866+
Otherwise the parameters in provided JSON will decide if a private or public key is loaded,
867+
specifying `type` will result in an error; also public key part for `OKP` keys (the `x` parameter)
868+
is not honored and derived from private key part (the `d` parameter) if it's specified.
866869
- Only key type of `RSA`, `P-256`, `P-384` and `P-512` `EC`,
867870
`Ed25519`, `X25519`, `Ed448` and `X448` `OKP` keys are supported.
868-
- Public key part for `OKP` keys (the `x` parameter) is always not honored and derived
869-
from private key part (the `d` parameter) if it's specified.
870871
- Signatures and verification must use `ecdsa_use_raw` option to work with JWS standards
871872
for EC keys. See [pkey:sign](#pkeysign) and [pkey.verify](#pkeyverify) for detail.
872873
- When running outside of OpenResty, needs to install a JSON library (`cjson` or `dkjson`)

lib/resty/openssl/auxiliary/jwk.lua

Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,11 @@ local bn_lib = require "resty.openssl.bn"
1111
local digest_lib = require "resty.openssl.digest"
1212
local encode_base64url = require "resty.openssl.auxiliary.compat".encode_base64url
1313
local decode_base64url = require "resty.openssl.auxiliary.compat".decode_base64url
14+
local param_lib = require "resty.openssl.param"
1415
local json = require "resty.openssl.auxiliary.compat".json
16+
local format_error = require "resty.openssl.err".format_error
17+
18+
local OPENSSL_3X = require("resty.openssl.version").OPENSSL_3X
1519

1620
local _M = {}
1721

@@ -266,4 +270,163 @@ function _M.dump_jwk(pkey, is_priv)
266270
return json.encode(jwk)
267271
end
268272

273+
274+
-- 3.x load_jwk
275+
276+
local settable_schema = {}
277+
278+
local function get_settable_schema(type, selection, properties)
279+
-- the pctx can't be reused after EVP_PKEY_fromdata_settable, so we create a new here
280+
local pctx = C.EVP_PKEY_CTX_new_from_name(nil, type, properties)
281+
if pctx == nil then
282+
return nil, "EVP_PKEY_CTX_new_from_name() failed"
283+
end
284+
ffi.gc(pctx, C.EVP_PKEY_CTX_free)
285+
286+
if C.EVP_PKEY_fromdata_init(pctx) ~= 1 then
287+
return nil, "EVP_PKEY_fromdata_init() failed"
288+
end
289+
290+
if settable_schema[type] then
291+
return settable_schema[type]
292+
end
293+
294+
local settable = C.EVP_PKEY_fromdata_settable(pctx, selection)
295+
if settable == nil then
296+
return nil, "EVP_PKEY_fromdata_settable() failed"
297+
end
298+
299+
local schema = {}
300+
param_lib.parse_params_schema(settable, schema, nil)
301+
302+
settable_schema[type] = schema
303+
return schema
304+
end
305+
306+
local ossl_params_jwk_mapping = {
307+
RSA = {
308+
n = "n",
309+
e = "e",
310+
d = "d",
311+
p = "rsa-factor1",
312+
q = "rsa-factor2",
313+
dp = "rsa-exponent1",
314+
dq = "rsa-exponent2",
315+
qi = "rsa-coefficient1",
316+
},
317+
EC = {
318+
x = "x",
319+
y = "y",
320+
d = "priv",
321+
crv = "group",
322+
},
323+
OKP = {
324+
x = "pub",
325+
d = "priv",
326+
},
327+
}
328+
329+
local jwk_params_required_mapping = {
330+
RSA = {
331+
n = true,
332+
e = true,
333+
},
334+
EC = {
335+
crv = true,
336+
x = true,
337+
y = true,
338+
},
339+
OKP = {
340+
-- crv = true, handled earlier
341+
x = true,
342+
},
343+
}
344+
345+
function _M.load_jwk_ex(txt, ptyp, properties)
346+
local tbl, err = json.decode(txt)
347+
if err then
348+
return nil, "jwk:load_jwk: error decoding JSON from JWK: " .. err
349+
elseif type(tbl) ~= "table" then
350+
return nil, "jwk:load_jwk: except input to be decoded as a table, got " .. type(tbl)
351+
elseif not tbl["kty"] then
352+
return nil, "jwk:load_jwk: missing \"kty\" parameter from JWK"
353+
end
354+
355+
local kty = tbl["kty"]
356+
tbl["kty"] = nil
357+
local selection = ptyp == "pu" and evp_macro.EVP_PKEY_PUBLIC_KEY or evp_macro.EVP_PKEY_KEYPAIR
358+
359+
local pkey_name = kty
360+
if kty == "OKP" then
361+
pkey_name = tbl["crv"]
362+
if not pkey_name then
363+
return nil, "jwk:load_jwk: missing \"crv\" parameter from OKP JWK"
364+
end
365+
tbl["crv"] = nil
366+
end
367+
368+
local ctx = ffi.new("EVP_PKEY*[1]")
369+
local pctx = C.EVP_PKEY_CTX_new_from_name(nil, pkey_name, nil)
370+
if pctx == nil then
371+
return nil, "jwk:load_jwk: EVP_PKEY_CTX_new_from_name() failed"
372+
end
373+
ffi.gc(pctx, C.EVP_PKEY_CTX_free)
374+
375+
if C.EVP_PKEY_fromdata_init(pctx) ~= 1 then
376+
return nil, "jwk:load_jwk: EVP_PKEY_fromdata_init() failed"
377+
end
378+
379+
local schema, err = get_settable_schema(pkey_name, selection, properties)
380+
if not schema then
381+
return nil, "jwk:load_jwk: failed to get key schema for " .. pkey_name .. " key: " .. err
382+
end
383+
384+
local mapping = ossl_params_jwk_mapping[kty]
385+
if not mapping then
386+
return nil, "jwk:load_jwk: not yet supported jwk type \"" .. (tbl["kty"] or "nil") .. "\""
387+
end
388+
local required = jwk_params_required_mapping[kty]
389+
390+
local params_t = {}
391+
392+
for kfrom, kto in pairs(mapping) do
393+
local v = tbl[kfrom]
394+
if type(v) == "string" and (selection == evp_macro.EVP_PKEY_KEYPAIR or required[kfrom]) then
395+
if kfrom ~= "crv" then
396+
v = decode_base64url(v)
397+
if not v then
398+
return nil, "jwk:load_jwk: cannot decode parameter \"" .. kfrom .. "\" from base64 " .. tbl[kfrom]
399+
end
400+
v = v:reverse() -- endian switch
401+
end
402+
403+
params_t[kto] = v
404+
elseif required[kfrom] then
405+
return nil, "jwk:load_jwk: missing required parameter \"" .. kfrom .. "\""
406+
end
407+
end
408+
409+
if kty == "EC" then
410+
if params_t["x"] and params_t["y"] then
411+
params_t["pub"] = "\x04" .. params_t["x"]:reverse() .. params_t["y"]:reverse()
412+
params_t["x"], params_t["y"] = nil, nil
413+
end
414+
end
415+
416+
local params, err = param_lib.construct(params_t, nil, schema)
417+
if params == nil then
418+
return nil, "jwk:load_jwk: failed to construct parameters for " .. kty .. " key: " .. err
419+
end
420+
421+
if C.EVP_PKEY_fromdata(pctx, ctx, selection, params) ~= 1 then
422+
return nil, format_error("jwk:load_jwk: EVP_PKEY_fromdata()")
423+
end
424+
425+
return ctx[0]
426+
end
427+
428+
if OPENSSL_3X then
429+
_M.load_jwk = _M.load_jwk_ex
430+
end
431+
269432
return _M

lib/resty/openssl/include/evp.lua

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,10 +88,25 @@ local _M = {
8888
EVP_PKEY_CTRL_SCRYPT_MAXMEM_BYTES = EVP_PKEY_ALG_CTRL + 13,
8989
}
9090

91+
local OSSL_KEYMGMT_SELECT_PRIVATE_KEY = 0x01
92+
local OSSL_KEYMGMT_SELECT_PUBLIC_KEY = 0x02
93+
local OSSL_KEYMGMT_SELECT_DOMAIN_PARAMETERS = 0x04
94+
local OSSL_KEYMGMT_SELECT_OTHER_PARAMETERS = 0x80
95+
local OSSL_KEYMGMT_SELECT_ALL_PARAMETERS = OSSL_KEYMGMT_SELECT_DOMAIN_PARAMETERS +
96+
OSSL_KEYMGMT_SELECT_OTHER_PARAMETERS
97+
-- local OSSL_KEYMGMT_SELECT_KEYPAIR = OSSL_KEYMGMT_SELECT_PRIVATE_KEY +
98+
-- OSSL_KEYMGMT_SELECT_PUBLIC_KEY
99+
-- local OSSL_KEYMGMT_SELECT_ALL = OSSL_KEYMGMT_SELECT_KEYPAIR +
100+
-- OSSL_KEYMGMT_SELECT_ALL_PARAMETERS
101+
91102
if not OPENSSL_3X then
92103
_M.EVP_PKEY_OP_CRYPT = _M.EVP_PKEY_OP_ENCRYPT + _M.EVP_PKEY_OP_DECRYPT
93104
_M.EVP_PKEY_OP_SIG = _M.EVP_PKEY_OP_SIGN + _M.EVP_PKEY_OP_VERIFY + _M.EVP_PKEY_OP_VERIFYRECOVER +
94105
_M.EVP_PKEY_OP_SIGNCTX + _M.EVP_PKEY_OP_VERIFYCTX
106+
else
107+
_M.EVP_PKEY_KEY_PARAMETERS = OSSL_KEYMGMT_SELECT_ALL_PARAMETERS
108+
_M.EVP_PKEY_PUBLIC_KEY = _M.EVP_PKEY_KEY_PARAMETERS + OSSL_KEYMGMT_SELECT_PUBLIC_KEY
109+
_M.EVP_PKEY_KEYPAIR = _M.EVP_PKEY_PUBLIC_KEY + OSSL_KEYMGMT_SELECT_PRIVATE_KEY
95110
end
96111

97112
-- clean up error occurs during OBJ_txt2*

lib/resty/openssl/include/evp/pkey.lua

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,14 @@ if OPENSSL_3X then
123123
int EVP_PKEY_set_params(EVP_PKEY *pkey, OSSL_PARAM params[]);
124124
int EVP_PKEY_get_params(EVP_PKEY *ctx, OSSL_PARAM params[]);
125125
const OSSL_PARAM *EVP_PKEY_gettable_params(EVP_PKEY *ctx);
126+
127+
EVP_PKEY_CTX *EVP_PKEY_CTX_new_from_name(OSSL_LIB_CTX *libctx,
128+
const char *name,
129+
const char *propquery);
130+
int EVP_PKEY_fromdata_init(EVP_PKEY_CTX *ctx);
131+
int EVP_PKEY_fromdata(EVP_PKEY_CTX *ctx, EVP_PKEY **ppkey, int selection,
132+
OSSL_PARAM params[]);
133+
const OSSL_PARAM *EVP_PKEY_fromdata_settable(EVP_PKEY_CTX *ctx, int selection);
126134
]]
127135

128136
_M.EVP_PKEY_CTX_set_ec_paramgen_curve_nid = function(pctx, nid)

lib/resty/openssl/param.lua

Lines changed: 22 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -49,12 +49,14 @@ local function construct(buf_t, length, types_map, types_size)
4949
buf = ffi_new("char[?]", size)
5050
end
5151
else
52-
local numeric = type(value) == "number"
53-
if (numeric and typ >= OSSL_PARAM_UTF8_STRING) or
54-
(not numeric and typ <= OSSL_PARAM_UNSIGNED_INTEGER) then
52+
local string_input = type(value) == "string"
53+
-- binary input is also accepted as string
54+
local integer_input = type(value) == "number" or string_input
55+
if (not string_input and typ >= OSSL_PARAM_UTF8_STRING) or
56+
(not integer_input and typ <= OSSL_PARAM_UNSIGNED_INTEGER) then
5557
local alter_typ = types_map[alter_type_key] and types_map[alter_type_key][key]
56-
if alter_typ and ((numeric and alter_typ <= OSSL_PARAM_UNSIGNED_INTEGER) or
57-
(not numeric and alter_typ >= OSSL_PARAM_UTF8_STRING)) then
58+
if alter_typ and ((not string_input and alter_typ <= OSSL_PARAM_UNSIGNED_INTEGER) or
59+
(not integer_input and alter_typ >= OSSL_PARAM_UTF8_STRING)) then
5860
typ = alter_typ
5961
else
6062
return nil, "param:construct: key \"" .. key .. "\" can't be a " .. type(value)
@@ -68,12 +70,22 @@ local function construct(buf_t, length, types_map, types_size)
6870
buf_param = buf_param or {}
6971
buf_param[key] = param
7072
elseif typ == OSSL_PARAM_INTEGER then
71-
buf = value and ffi_new("int[1]", value) or ffi_new("int[1]")
72-
param = C.OSSL_PARAM_construct_int(key, buf)
73+
if value and type(value) == "string" then -- in only
74+
local bin = ffi_cast("char *", value)
75+
param = C.OSSL_PARAM_construct_BN(key, bin, #value)
76+
else
77+
buf = value and ffi_new("int[1]", value) or ffi_new("int[1]")
78+
param = C.OSSL_PARAM_construct_int(key, buf)
79+
end
7380
elseif typ == OSSL_PARAM_UNSIGNED_INTEGER then
74-
buf = value and ffi_new("unsigned int[1]", value) or
75-
ffi_new("unsigned int[1]")
76-
param = C.OSSL_PARAM_construct_uint(key, buf)
81+
if value and type(value) == "string" then -- in only
82+
local bin = ffi_cast("char *", value)
83+
param = C.OSSL_PARAM_construct_BN(key, bin, #value)
84+
else
85+
buf = value and ffi_new("unsigned int[1]", value) or
86+
ffi_new("unsigned int[1]")
87+
param = C.OSSL_PARAM_construct_uint(key, buf)
88+
end
7789
elseif typ == OSSL_PARAM_UTF8_STRING then
7890
buf = value ~= nil and ffi_cast("char *", value) or buf
7991
param = C.OSSL_PARAM_construct_utf8_string(key, buf, value and #value or size)

lib/resty/openssl/pkey.lua

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -65,8 +65,8 @@ local function load_pem_der(txt, opts, funcs)
6565
return nil, "expecting 'pr', 'pu' or '*' as \"type\""
6666
end
6767

68-
if fmt == "JWK" and (typ == "pu" or type == "pr") then
69-
return nil, "explictly load private or public key from JWK format is not supported"
68+
if fmt == "JWK" and (typ == "pu" or type == "pr") and not OPENSSL_3X then
69+
return nil, "explictly load private or public key from JWK format is not supported on OpenSSL 1.1.1"
7070
end
7171

7272
log_debug("load key using fmt: ", fmt, ", type: ", typ)
@@ -85,7 +85,7 @@ local function load_pem_der(txt, opts, funcs)
8585
-- don't need BIO when loading JWK key: we parse it in Lua land
8686
if f == "load_jwk" then
8787
local err
88-
ctx, err = jwk_lib[f](txt)
88+
ctx, err = jwk_lib[f](txt, typ, opts and opts.properties)
8989
if ctx == nil then
9090
-- if fmt is explictly set to JWK, we should return an error now
9191
if fmt == "JWK" then
@@ -484,6 +484,7 @@ local load_key_try_funcs = {} do
484484
},
485485
JWK = {
486486
pr = { ['load_jwk'] = {}, },
487+
pu = { ['load_jwk'] = {}, },
487488
}
488489
}
489490
-- populate * funcs

lib/resty/openssl/version.lua

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,7 @@ return setmetatable({
104104
return ffi_str(info_func(t))
105105
end,
106106
-- the following has implict upper bound of 4.x
107+
OPENSSL_30 = version_num >= 0x30000000 and version_num < 0x30100000,
107108
OPENSSL_3X = version_num >= 0x30000000 and version_num < 0x40000000,
108109
OPENSSL_111 = version_num >= 0x10101000 and version_num < 0x10200000,
109110
}, {

0 commit comments

Comments
 (0)