local ffi = require "ffi" local C = ffi.C local ffi_new = ffi.new local ffi_str = ffi.string local ffi_cast = ffi.cast require "resty.openssl.include.param" local format_error = require("resty.openssl.err").format_error local bn_lib = require("resty.openssl.bn") local null = require("resty.openssl.auxiliary.ctypes").null local nkeys = require "resty.openssl.auxiliary.compat".nkeys local OSSL_PARAM_INTEGER = 1 local OSSL_PARAM_UNSIGNED_INTEGER = 2 local OSSL_PARAM_REAL = 3 local OSSL_PARAM_UTF8_STRING = 4 local OSSL_PARAM_OCTET_STRING = 5 local OSSL_PARAM_UTF8_PTR = 6 local OSSL_PARAM_OCTET_PTR = 7 local alter_type_key = {} local buf_param_key = {} local buf_anchor_key = {} local function construct(buf_t, length, types_map, types_size) if not length then length = nkeys(buf_t) end local params = ffi_new("OSSL_PARAM[?]", length + 1) local i = 0 local buf_param, buf_anchored for key, value in pairs(buf_t) do if key == buf_anchor_key then goto continue end local typ = types_map[key] if not typ then return nil, "param:construct: unknown key \"" .. key .. "\"" end local param, buf, size if value == null then -- out value = nil size = types_size and types_size[key] or 100 if typ == OSSL_PARAM_UTF8_STRING or typ == OSSL_PARAM_OCTET_STRING then buf = ffi_new("char[?]", size) end else local string_input = type(value) == "string" -- binary input is also accepted as string local integer_input = type(value) == "number" or string_input if (not string_input and typ >= OSSL_PARAM_UTF8_STRING) or (not integer_input and typ <= OSSL_PARAM_UNSIGNED_INTEGER) then local alter_typ = types_map[alter_type_key] and types_map[alter_type_key][key] if alter_typ and ((not string_input and alter_typ <= OSSL_PARAM_UNSIGNED_INTEGER) or (not integer_input and alter_typ >= OSSL_PARAM_UTF8_STRING)) then typ = alter_typ else return nil, "param:construct: key \"" .. key .. "\" can't be a " .. type(value) end end end if typ == "bn" then -- out only buf = ffi_new("char[?]", size) param = C.OSSL_PARAM_construct_BN(key, buf, size) buf_param = buf_param or {} buf_param[key] = param elseif typ == OSSL_PARAM_INTEGER then if value and type(value) == "string" then -- in only local bin = ffi_cast("char *", value) param = C.OSSL_PARAM_construct_BN(key, bin, #value) else buf = value and ffi_new("int[1]", value) or ffi_new("int[1]") param = C.OSSL_PARAM_construct_int(key, buf) end elseif typ == OSSL_PARAM_UNSIGNED_INTEGER then if value and type(value) == "string" then -- in only local bin = ffi_cast("char *", value) param = C.OSSL_PARAM_construct_BN(key, bin, #value) else buf = value and ffi_new("unsigned int[1]", value) or ffi_new("unsigned int[1]") param = C.OSSL_PARAM_construct_uint(key, buf) end elseif typ == OSSL_PARAM_UTF8_STRING then buf = value ~= nil and ffi_cast("char *", value) or buf param = C.OSSL_PARAM_construct_utf8_string(key, buf, value and #value or size) elseif typ == OSSL_PARAM_OCTET_STRING then buf = value ~= nil and ffi_cast("char *", value) or buf param = C.OSSL_PARAM_construct_octet_string(key, ffi_cast("void*", buf), value and #value or size) elseif typ == OSSL_PARAM_UTF8_PTR then -- out only buf = ffi_new("char*[1]") param = C.OSSL_PARAM_construct_utf8_ptr(key, buf, 0) elseif typ == OSSL_PARAM_OCTET_PTR then -- out only buf = ffi_new("char*[1]") param = C.OSSL_PARAM_construct_octet_ptr(key, ffi_cast("void**", buf), 0) else error("type " .. typ .. " is not yet implemented") end if value == nil then -- out buf_t[key] = buf else -- in -- save value as OSSL_PARAM_construct_* doesn't copy the value buf_anchored = buf_anchored or {} buf_anchored[key] = buf end params[i] = param i = i + 1 ::continue:: end buf_t[buf_anchor_key] = buf_anchored buf_t[buf_param_key] = buf_param params[length] = C.OSSL_PARAM_construct_end() return params end local function parse(buf_t, length, types_map, types_size) for key, buf in pairs(buf_t) do local typ = types_map[key] local sz = types_size and types_size[key] if key == buf_param_key then -- luacheck: ignore -- ignore elseif buf == nil or buf[0] == nil then buf_t[key] = nil elseif typ == "bn" then local bn_t = ffi_new("BIGNUM*[1]") local param = buf_t[buf_param_key][key] if C.OSSL_PARAM_get_BN(param, bn_t) ~= 1 then return nil, format_error("param:parse: OSSL_PARAM_get_BN") end buf_t[key] = assert(bn_lib.dup(bn_t[0])) C.BN_free(bn_t[0]) elseif typ == OSSL_PARAM_INTEGER or typ == OSSL_PARAM_UNSIGNED_INTEGER then buf_t[key] = tonumber(buf[0]) elseif typ == OSSL_PARAM_UTF8_STRING or typ == OSSL_PARAM_OCTET_STRING then buf_t[key] = sz and ffi_str(buf, sz) or ffi_str(buf) elseif typ == OSSL_PARAM_UTF8_PTR or typ == OSSL_PARAM_OCTET_PTR then buf_t[key] = sz and ffi_str(buf[0], sz) or ffi_str(buf[0]) elseif not typ then return nil, "param:parse: unknown key type \"" .. key .. "\"" else error("type " .. typ .. " is not yet implemented") end end -- for GC buf_t[buf_param_key] = nil return buf_t end local param_type_readable = { [OSSL_PARAM_UNSIGNED_INTEGER] = "unsigned integer", [OSSL_PARAM_INTEGER] = "integer", [OSSL_PARAM_REAL] = "real number", [OSSL_PARAM_UTF8_PTR] = "pointer to a UTF8 encoded string", [OSSL_PARAM_UTF8_STRING] = "UTF8 encoded string", [OSSL_PARAM_OCTET_PTR] = "pointer to an octet string", [OSSL_PARAM_OCTET_STRING] = "octet string", } local function readable_data_type(p) local typ = p.data_type local literal = param_type_readable[typ] if not literal then literal = string.format("unknown type [%d]", typ) end local sz = tonumber(p.data_size) if sz == 0 then literal = literal .. " (arbitrary size)" else literal = literal .. string.format(" (max %d bytes large)", sz) end return literal end local function parse_params_schema(params, schema, schema_readable) if params == nil then return nil, format_error("parse_params_schema") end local i = 0 while true do local p = params[i] if p.key == nil then break end local key = ffi_str(p.key) if schema then -- TODO: don't support same key with different types for now -- prefer string type over integer types local typ = tonumber(p.data_type) if schema[key] then schema[alter_type_key] = schema[alter_type_key] or {} schema[alter_type_key][key] = typ else schema[key] = typ end end -- if schema_return_size then -- only non-ptr string types are needed actually -- schema_return_size[key] = tonumber(p.return_size) -- end if schema_readable then table.insert(schema_readable, { key, readable_data_type(p) }) end i = i + 1 end return schema end local param_maps_set, param_maps_get = {}, {} local function get_params_func(typ, field) local typ_lower = typ:sub(5):lower() if typ_lower:sub(-4) == "_ctx" then typ_lower = typ_lower:sub(0, -5) end -- field name for indexing schema, usually the (const) one created by -- EVP_TYP_fetch or EVP_get_typebynam,e field = field or "algo" local cf_settable = C[typ .. "_settable_params"] local settable = function(self, raw) local k = self[field] if raw and param_maps_set[k] then return param_maps_set[k] end local param = cf_settable(self.ctx) -- no params, this is fine, shouldn't be regarded as an error if param == nil then param_maps_set[k] = {} return {} end local schema, schema_reabale = {}, raw and nil or {} parse_params_schema(param, schema, schema_reabale) param_maps_set[k] = schema return raw and schema or schema_reabale end local cf_set = C[typ .. "_set_params"] local set = function(self, params) if not param_maps_set[self[field]] then local ok, err = self:settable_params(true) -- only query raw schema to save memory if not ok then return false, typ_lower .. ":set_params: " .. err end end local oparams, err = construct(params, nil, param_maps_set[self[field]]) if err then return false, typ_lower .. ":set_params: " .. err end if cf_set(self.ctx, oparams) ~= 1 then return false, format_error(typ_lower .. ":set_params: " .. typ .. "_set_params") end return true end local cf_gettable = C[typ .. "_gettable_params"] local gettable = function(self, raw) local k = self[field] if raw and param_maps_get[k] then return param_maps_get[k] end local param = cf_gettable(self.ctx) -- no params, this is fine, shouldn't be regarded as an error if param == nil then param_maps_get[k] = {} return {} end local schema, schema_reabale = {}, raw and nil or {} parse_params_schema(param, schema, schema_reabale) param_maps_get[k] = schema return raw and schema or schema_reabale end local cf_get = C[typ .. "_get_params"] local get_buffer, get_size_map = {}, {} local get = function(self, key, want_size, want_type) if not param_maps_get[self[field]] then local ok, err = self:gettable_params(true) -- only query raw schema to save memory if not ok then return false, typ_lower .. ":set_params: " .. err end end local schema = param_maps_get[self[field]] if schema == nil or not schema[key] then -- nil or null return nil, typ_lower .. ":get_param: unknown key \"" .. key .. "\"" end table.clear(get_buffer) table.clear(get_size_map) get_buffer[key] = null get_size_map[key] = want_size schema = want_type and { [key] = want_type } or schema local req, err = construct(get_buffer, 1, schema, get_size_map) if not req then return nil, typ_lower .. ":get_param: failed to construct params: " .. err end if cf_get(self.ctx, req) ~= 1 then return nil, format_error(typ_lower .. ":get_param:get") end get_buffer, err = parse(get_buffer, 1, schema, get_size_map) if err then return nil, typ_lower .. ":get_param: failed to parse params: " .. err end return get_buffer[key] end return settable, set, gettable, get end return { OSSL_PARAM_INTEGER = OSSL_PARAM_INTEGER, OSSL_PARAM_UNSIGNED_INTEGER = OSSL_PARAM_INTEGER, OSSL_PARAM_REAL = OSSL_PARAM_REAL, OSSL_PARAM_UTF8_STRING = OSSL_PARAM_UTF8_STRING, OSSL_PARAM_OCTET_STRING = OSSL_PARAM_OCTET_STRING, OSSL_PARAM_UTF8_PTR = OSSL_PARAM_UTF8_PTR, OSSL_PARAM_OCTET_PTR = OSSL_PARAM_OCTET_PTR, construct = construct, parse = parse, parse_params_schema = parse_params_schema, get_params_func = get_params_func, }