ES256和ECDSA-SHA256签名的区别在于signature的格式不同,ES256只包含R/S,而ECDSA-SHA256是包含了R/S的ASN.1格式的签名,二者可以互相转换,前面一章的Java中,使用了SunEC的ECDSAUtil来完成的。但是Lua中很难能找到对应的工具去解析。本章使用lua-openssl及自定义的es_asn来实现签名格式转换。
lua-openssl可以使用luarocks安装,还请注意兼容的lua版本,本文采用的lua 5.3.5版本,兼容lua-openssl-0.8.5-1。
lua-openssl Github地址:https://github.com/zhaozg/lua-openssl
lua-openssl Luarocks地址:https://luarocks.org/modules/zhaozg/openssl
注意:lua-openssl不同版本所依赖的lua版本是不一样的,需下载兼容的版本,如果lua版本过于超前,需要降低lua版本。
首先给出一个Lua的ES256签名格式转换工具
local ec_asn = {}
ec_asn.__index = ec_asn
function ec_asn.convert_to_concat(signature)
if type(signature) ~= "string" then
error("The signature #1 type error! ", 2)
elseif #signature < 8 or string.byte(signature, 1) ~= 0x30 then
error("Invalid ASN.1 format of ECDSA signature", 2)
end
local offset = string.byte(signature, 2) == 0x81 and 4 or 3
local rLength = string.byte(signature, offset + 1)
local i = rLength
while i > 0 and string.byte(signature, offset + 2 + rLength - i) == 0 do
i = i - 1
end
local sLength = string.byte(signature, offset + 2 + rLength + 1)
local j = sLength
while j > 0 and string.byte(signature, offset + 2 + rLength + 2 + sLength - j) == 0 do
j = j - 1
end
local rawLen = math.max(i, j)
local seq = {}
seq[1] = string.rep("\x00", rawLen - i) .. string.sub(signature, offset + 2 + rLength - i, offset + 2 + rLength - 1)
seq[2] =
string.rep("\x00", rawLen - j) ..
string.sub(signature, offset + 2 + rLength + 2 + sLength - j, offset + 2 + rLength + 2 + sLength - 1)
return seq
end
function ec_asn.convert_to_asn(esSignature)
if type(esSignature) ~= "string" then
error("The signature #1 must be string", 2)
end
local rawLen = math.floor(#esSignature / 2)
local i = rawLen
while i > 0 and string.byte(esSignature, rawLen - i + 1) == 0 do
i = i - 1
end
local j = string.byte(esSignature, rawLen - i + 1) > 0x80 and i + 1 or i
local k = rawLen
while k > 0 and string.byte(esSignature, 2 * rawLen - k + 1) == 0 do
k = k - 1
end
local l = string.byte(esSignature, 2 * rawLen - k + 1) > 0x80 and k + 1 or k
local len = 2 + j + 2 + l
if len > 255 then
error("Invalid ES format of ECDSA signature", 2)
end
local signature =
"\x30" ..
(len > 128 and "\x81" or "") ..
string.char(len) ..
"\x02" ..
string.char(j) ..
string.rep("\x00", j - i) ..
string.sub(esSignature, rawLen - i + 1, rawLen) ..
"\x02" ..
string.char(l) ..
string.rep("\x00", l - k) ..
string.sub(esSignature, 2 * rawLen - k + 1, 2 * rawLen)
return signature
end
return ec_asn
顺便给出openssl的代码示例ecc_sign:
local cjson = require 'cjson'
local openssl = require 'openssl'
local uuid = require "uuid"
local base64_url = require 'util/base64_url'
local pkey = openssl.pkey
local now = os.time();
uuid.randomseed(now)
local ec_asn = require 'util/ec_asn'
local header = {
alg = 'ES256',
kid = 'b1e0953c-827a-448d-ae73-e920e0c6c735'
}
local data = {
iss = "http://junyee.org",
typ = "TOKEN",
aud = "junyee.org",
exp = now+360*24*3600,
jti = uuid(),
iat = now
}
local segments = {
base64_url.encode(cjson.encode(header)),
base64_url.encode(cjson.encode(data))
}
local content = table.concat(segments, ".")
local factor = {
alg = "ec",
-- NID 415 is prime256v1
ec_name = 415,
d = assert(base64_url.decode('ALds0XN06VCrIaxG-WMiosWWgD7DrocR4SCH5ckC-901')),
x = assert(base64_url.decode('Ek7imKhgJDJUnKTEq0n8-Mi16H-qVfSesbQj2C4_bf8')),
y = assert(base64_url.decode('Bww57PhQpcOOunOIG595yJ-MV55xewZC2nLYwFotEmA'))
}
-- Generate an ecc private key
local pk = pkey.new(factor)
local signsource = pk:sign(content,"SHA256")
local derSequence2 = ec_asn.convert_to_concat(signsource)
segments[#segments+1] = base64_url.encode(derSequence2[1] .. derSequence2[2])
print(table.concat(segments, "."))
ECC的verify
local openssl = require 'openssl'
local ec_asn = require 'util/ec_asn'
local base64_url = require 'util/base64_url'
local pkey = openssl.pkey
local sub = string.sub
local function verify()
local factor = {
alg = "ec",
-- NID 415 is prime256v1 of openssl
ec_name = 415,
x = assert(base64_url.decode('Ek7imKhgJDJUnKTEq0n8-Mi16H-qVfSesbQj2C4_bf8')),
y = assert(base64_url.decode('Bww57PhQpcOOunOIG595yJ-MV55xewZC2nLYwFotEmA'))
}
local ec = pkey.new(factor)
-- local pem = assert(ec:export('pem'))
-- print(pem)
local content = 'eyJhbGciOiJFUzI1NiIsImtpZCI6ImIxZTA5NTNjLTgyN2EtNDQ4ZC1hZTczLWU5MjBlMGM2YzczNSJ9.eyJ0eXAiOiJUT0tFTiIsImF1ZCI6Imp1bnllZS5vcmciLCJleHAiOjE2NjUxMjM4MDQsImp0aSI6ImE1YTJjMWU0LTg1NGMtNGM3ZC05MTg4LTc4MTk5NjA2YTYzOSIsImlhdCI6MTYzNDAxOTgwNCwiaXNzIjoiaHR0cDpcL1wvanVueWVlLm9yZyJ9'
local signature = base64_url.decode('i32GXFY3-DU1C5y4TkrxONb7cKMYUNCJ34Y_BT6fnSuTluLsfZ8ROS4xGmQ1X78_yzRUY0VoSOiue6qyc3w8RA')
assert(#signature == 64, "Signature must be 64 bytes.")
local signatureAsn2 = ec_asn.convert_to_asn(signature)
print('Verify Signature2:',ec:verify(content, signatureAsn2, 'SHA256'))
end
verify()
补充一套JWKS的生成代码
local openssl = require 'openssl'
local uuid = require "uuid"
local base64_url = require 'util/base64_url'
uuid.randomseed(os.time())
local pkey = openssl.pkey
local ec_factory = {'ec', 'prime256v1'};
local jwks_option = {enable_private=false,jwks_length=3,raw=true}
local es256_jwks = {}
function set_option(opt)
opt = opt or {}
assert(not opt.jwks_length or opt.jwks_length>0,"opt.jwks_length must gt 0")
local option = {}
option.enable_private = opt.enable_private or jwks_option.enable_private
option.jwks_length = opt.jwks_length or jwks_option.jwks_length
option.raw = opt.raw==nil and jwks_option.raw or opt.raw
return option
end
function es256_jwks:new(opt)
local _instance = {}
setmetatable(_instance, self)
self.__index = self
_instance.option = set_option(opt)
return _instance
end
function es256_jwks:to_jwks()
local keys = {}
local metadata = {}
for i = 1,self.option.jwks_length do
local kid = uuid()
local pk = assert(pkey.new(table.unpack(ec_factory)))
local pubkey = pk:get_public()
local t = pk:parse()
local ec = t.ec
t = ec:parse('pem')
metadata[kid] = {
public_key = pubkey:export("pem", self.option.raw),
private_key = pk:export("pem", self.option.raw),
d = base64_url.encode(t.d:totext()),
x = base64_url.encode(t.x:totext()),
y = base64_url.encode(t.y:totext())
}
local x,y = metadata[kid].x,metadata[kid].y
local d = self.option.enable_private and '"d":"' .. metadata[kid].d ..'",' or ''
keys[i] = '{"kty":"EC","use":"sig","crv":"P-256","kid":"'.. kid ..'",'.. d ..'"x":"'.. x ..'","y":"'.. y ..'"}'
end
return '{"keys":['.. table.concat(keys,',')..']}', metadata
end
return es256_jwks
Base64 url safe 工具
local openssl = require "openssl"
local _M = {}
_M.__index = _M
function _M.encode(input)
local encoder = openssl.base64(input,true)
encoder = encoder:gsub("+", "-"):gsub("/", "_"):gsub("=","")
return encoder
end
function _M.decode(input)
local remainder = #input % 4
if remainder > 0 then
local padlen = 4 - remainder
input = input .. string.rep("=", padlen)
end
input = input:gsub("-", "+"):gsub("_", "/")
return openssl.base64(input,false)
end
return _M
JWKS测试
local es256_jwks = require 'es256/es256_jwks'
local cjson = require 'cjson'
-- enbale_private : Is print private key named 'd' in jwks json string.
-- jwks_length : jwks keys length.
local jwks = es256_jwks:new({enable_private=false,jwks_length=3,raw=true})
local jwks_json,metadata = jwks:to_jwks()
-- jwks url json string
print(jwks_json)
-- jwks private key and public key
for i,v in pairs(metadata) do
print('kid: ',i)
print(cjson.encode(v))
end