Skip to content

Commit 689e1ce

Browse files
committed
Fix ES256 and ES512 support
1 parent 0c251f1 commit 689e1ce

8 files changed

+281
-6
lines changed

lib/resty/evp.lua

+120
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,35 @@ EVP_PKEY *EVP_PKEY_new_mac_key(int type, ENGINE *e,
7777
void EVP_PKEY_free(EVP_PKEY *key);
7878
int i2d_RSA(RSA *a, unsigned char **out);
7979

80+
// Additional typedef of ECC operations (DER/RAW sig conversion)
81+
typedef struct bignum_st BIGNUM;
82+
BIGNUM *BN_new(void);
83+
void BN_free(BIGNUM *a);
84+
int BN_num_bits(const BIGNUM *a);
85+
int BN_bn2bin(const BIGNUM *a, unsigned char *to);
86+
BIGNUM *BN_bin2bn(const unsigned char *s, int len, BIGNUM *ret);
87+
char *BN_bn2hex(const BIGNUM *a);
88+
89+
90+
typedef struct ECDSA_SIG_st {
91+
BIGNUM *r;
92+
BIGNUM *s;} ECDSA_SIG;
93+
ECDSA_SIG* ECDSA_SIG_new(void);
94+
int i2d_ECDSA_SIG(const ECDSA_SIG *sig, unsigned char **pp);
95+
ECDSA_SIG* d2i_ECDSA_SIG(ECDSA_SIG **sig, unsigned char **pp,
96+
long len);
97+
void ECDSA_SIG_free(ECDSA_SIG *sig);
98+
99+
typedef struct ecgroup_st EC_GROUP;
100+
typedef struct eckey_st EC_KEY;
101+
102+
void EC_GROUP_free(EC_GROUP *group);
103+
EC_GROUP *EC_KEY_get0_group(const EC_KEY *key);
104+
void EC_KEY_free(EC_KEY *key);
105+
EC_KEY *EVP_PKEY_get0_EC_KEY(EVP_PKEY *pkey);
106+
int EC_GROUP_get_order(const EC_GROUP *group, BIGNUM *order, void *ctx);
107+
108+
80109
// PUBKEY
81110
EVP_PKEY *PEM_read_bio_PUBKEY(BIO *bp, EVP_PKEY **x,
82111
pem_password_cb *cb, void *u);
@@ -365,6 +394,47 @@ function ECSigner.sign(self, message, digest_name)
365394
return RSASigner.sign(self, message, digest_name)
366395
end
367396

397+
--- Converts a ASN.1 DER signature to RAW r,s
398+
-- @param signature The ASN.1 DER signature
399+
-- @returns signature, error_string
400+
function ECSigner.get_raw_sig(self, signature)
401+
if not signature then
402+
return nil, "Must pass a signature to convert"
403+
end
404+
local sig_ptr = ffi_new("unsigned char *[1]")
405+
local sig_bin = ffi_new("unsigned char [?]", #signature)
406+
ffi_copy(sig_bin, signature, #signature)
407+
408+
sig_ptr[0] = sig_bin
409+
local sig = _C.d2i_ECDSA_SIG(nil, sig_ptr, #signature)
410+
ffi_gc(sig, _C.ECDSA_SIG_free)
411+
412+
local rbytes = math.floor((_C.BN_num_bits(sig.r)+7)/8)
413+
local sbytes = math.floor((_C.BN_num_bits(sig.s)+7)/8)
414+
415+
-- Ensure we copy the BN in a padded form
416+
local ec = _C.EVP_PKEY_get0_EC_KEY(self.evp_pkey)
417+
local ecgroup = _C.EC_KEY_get0_group(ec)
418+
419+
local order = _C.BN_new()
420+
ffi_gc(order, _C.BN_free)
421+
422+
-- res is an int, if 0, curve not found
423+
local res = _C.EC_GROUP_get_order(ecgroup, order, nil)
424+
425+
-- BN_num_bytes is a #define, so have to use BN_num_bits
426+
local order_size_bytes = math.floor((_C.BN_num_bits(order)+7)/8)
427+
local resbuf_len = order_size_bytes *2
428+
local resbuf = ffi_new("unsigned char[?]", resbuf_len)
429+
430+
-- Let's whilst preserving MSB
431+
_C.BN_bn2bin(sig.r, resbuf + order_size_bytes - rbytes)
432+
_C.BN_bn2bin(sig.s, resbuf + (order_size_bytes*2) - sbytes)
433+
434+
local raw = ffi_string(resbuf, resbuf_len)
435+
return raw, nil
436+
end
437+
368438
local RSAVerifier = {}
369439
_M.RSAVerifier = RSAVerifier
370440

@@ -418,6 +488,56 @@ function RSAVerifier.verify(self, message, sig, digest_name)
418488
end
419489
end
420490

491+
--- Converts a RAW r,s signature to ASN.1 DER signature (ECDSA)
492+
-- @param signature The raw signature
493+
-- @returns signature, error_string
494+
function RSAVerifier.get_der_sig(self, signature)
495+
-- TODO: should be declared in a new class rather than RSAVerifier
496+
if not signature then
497+
return nil, "Must pass a signature to convert"
498+
end
499+
-- inspired from https://bit.ly/2yZxzxJ
500+
local ec = _C.EVP_PKEY_get0_EC_KEY(self.evp_pkey)
501+
local ecgroup = _C.EC_KEY_get0_group(ec)
502+
503+
local order = _C.BN_new()
504+
ffi_gc(order, _C.BN_free)
505+
506+
-- res is an int, if 0, curve not found
507+
local res = _C.EC_GROUP_get_order(ecgroup, order, nil)
508+
509+
-- BN_num_bytes is a #define, so have to use BN_num_bits
510+
local order_size_bytes = math.floor((_C.BN_num_bits(order)+7)/8)
511+
512+
if #signature ~= 2 * order_size_bytes then
513+
return nil, "signature length != 2 * order length"
514+
end
515+
516+
local sig_bytes = ffi_new("unsigned char [?]", #signature)
517+
ffi_copy(sig_bytes, signature, #signature)
518+
local ecdsa = _C.ECDSA_SIG_new()
519+
ffi_gc(ecdsa, _C.ECDSA_SIG_free)
520+
521+
local r = _C.BN_bin2bn(sig_bytes, order_size_bytes, nil)
522+
local s = _C.BN_bin2bn(sig_bytes + order_size_bytes, order_size_bytes, nil)
523+
ffi_gc(r, _C.BN_free)
524+
ffi_gc(s, _C.BN_free)
525+
526+
ecdsa.r = r
527+
ecdsa.s = s
528+
529+
-- Gives us the buffer size to allocate
530+
local der_len = _C.i2d_ECDSA_SIG(ecdsa, nil)
531+
532+
local der_sig_ptr = ffi_new("unsigned char *[1]")
533+
local der_sig_bin = ffi_new("unsigned char [?]", der_len)
534+
der_sig_ptr[0] = der_sig_bin
535+
der_len = _C.i2d_ECDSA_SIG(ecdsa, der_sig_ptr)
536+
537+
local der_str = ffi_string(der_sig_bin, der_len)
538+
return der_str, nil
539+
end
540+
421541

422542
local Cert = {}
423543
_M.Cert = Cert

lib/resty/jwt.lua

+33-5
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@ local evp = require "resty.evp"
55
local hmac = require "resty.hmac"
66
local resty_random = require "resty.random"
77

8+
local ngx = ngx
9+
10+
811
local _M = {_VERSION="0.2.2"}
912

1013
local mt = {
@@ -60,6 +63,7 @@ local str_const = {
6063
HS512 = "HS512",
6164
RS256 = "RS256",
6265
ES256 = "ES256",
66+
ES512 = "ES512",
6367
RS512 = "RS512",
6468
A128CBC_HS256 = "A128CBC-HS256",
6569
A256CBC_HS512 = "A256CBC-HS512",
@@ -529,7 +533,6 @@ function _M.sign(self, secret_key, jwt_obj)
529533
local raw_header = get_raw_part(str_const.header, jwt_obj)
530534
local raw_payload = get_raw_part(str_const.payload, jwt_obj)
531535
local message = string_format(str_const.regex_join_msg, raw_header, raw_payload)
532-
533536
local alg = jwt_obj[str_const.header][str_const.alg]
534537
local signature = ""
535538
if alg == str_const.HS256 then
@@ -544,12 +547,25 @@ function _M.sign(self, secret_key, jwt_obj)
544547
error({reason="signer error: " .. err})
545548
end
546549
signature = signer:sign(message, evp.CONST.SHA256_DIGEST)
547-
elseif alg == str_const.ES256 then
550+
elseif alg == str_const.ES256 or alg == str_const.ES512 then
548551
local signer, err = evp.ECSigner:new(secret_key)
549552
if not signer then
550553
error({reason="signer error: " .. err})
551554
end
552-
signature = signer:sign(message, evp.CONST.SHA256_DIGEST)
555+
if alg == str_const.ES256 then
556+
-- OpenSSL will generate a DER encoded signature that needs to be converted
557+
local der_signature = signer:sign(message, evp.CONST.SHA256_DIGEST)
558+
signature, err = signer:get_raw_sig(der_signature)
559+
if not signature then
560+
error({reason="signature error: " .. err})
561+
end
562+
elseif alg == str_const.ES512 then
563+
local der_signature = signer:sign(message, evp.CONST.SHA512_DIGEST)
564+
signature, err = signer:get_raw_sig(der_signature)
565+
if not signature then
566+
error({reason="signature error: " .. err})
567+
end
568+
end
553569
else
554570
error({reason="unsupported alg: " .. alg})
555571
end
@@ -803,7 +819,7 @@ function _M.verify_jwt_obj(self, secret, jwt_obj, ...)
803819
-- signature check
804820
jwt_obj[str_const.reason] = "signature mismatch: " .. jwt_obj[str_const.signature]
805821
end
806-
elseif alg == str_const.RS256 or alg == str_const.RS512 or alg == str_const.ES256 then
822+
elseif alg == str_const.RS256 or alg == str_const.RS512 or alg == str_const.ES256 or alg == str_const.ES512 then
807823
local cert, err
808824
if self.trusted_certs_file ~= nil then
809825
local cert_str = extract_certificate(jwt_obj, self.x5u_content_retriever)
@@ -857,10 +873,22 @@ function _M.verify_jwt_obj(self, secret, jwt_obj, ...)
857873
local verified = false
858874
err = "verify error: reason unknown"
859875

860-
if alg == str_const.RS256 or alg == str_const.ES256 then
876+
if alg == str_const.RS256 then
861877
verified, err = verifier:verify(message, sig, evp.CONST.SHA256_DIGEST)
878+
elseif alg == str_const.ES256 then
879+
local der_sig = ""
880+
der_sig, err = verifier:get_der_sig(sig)
881+
if der_sig then
882+
verified, err = verifier:verify(message, der_sig, evp.CONST.SHA256_DIGEST)
883+
end
862884
elseif alg == str_const.RS512 then
863885
verified, err = verifier:verify(message, sig, evp.CONST.SHA512_DIGEST)
886+
elseif alg == str_const.ES512 then
887+
local der_sig = ""
888+
der_sig, err = verifier:get_der_sig(sig)
889+
if der_sig then
890+
verified, err = verifier:verify(message, der_sig, evp.CONST.SHA512_DIGEST)
891+
end
864892
end
865893
if not verified then
866894
jwt_obj[str_const.reason] = err

t/load-verify.t

+35
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@ our $HttpConfig = <<'_EOC_';
1010
lua_package_path 'lib/?.lua;;';
1111
_EOC_
1212
13+
log_level('debug');
14+
15+
1316
no_long_string();
1417
1518
run_tests();
@@ -659,4 +662,36 @@ true
659662
everything is awesome~ :p
660663
bar
661664
--- no_error_log
665+
[error]
666+
667+
=== TEST 22: Verify valid ES256 signed jwt using a EC public key
668+
--- http_config eval: $::HttpConfig
669+
--- config
670+
location /t {
671+
content_by_lua '
672+
local jwt = require "resty.jwt"
673+
674+
local public_key = [[
675+
-----BEGIN PUBLIC KEY-----
676+
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEm9ehYHp34sZPfZoxJlotxG/LF02e
677+
ZPmM51hCYIL1jn50e30i8KqEL6y6wl06z6P4co0uew5CzD7JlOQlLB+Ryg==
678+
-----END PUBLIC KEY-----
679+
]]
680+
681+
jwt:set_alg_whitelist({ ES256 = 1 })
682+
local jwt_token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiJ9.eyJpc3MiOiJ0ZXN0IiwiaWF0IjoxNDYxOTE0MDE3fQ.U38g80dOEKrGQG08KDRY_XWXvolBAhz6G16QZqgePFQljooqsZXw9sIyH6hXFpsAxQbupQBqgUAw6IwUqbAXzg"
683+
684+
local jwt_obj = jwt:verify(public_key, jwt_token)
685+
ngx.say(jwt_obj["verified"])
686+
ngx.say(jwt_obj["reason"])
687+
ngx.say(jwt_obj["payload"]["iss"])
688+
';
689+
}
690+
--- request
691+
GET /t
692+
--- response_body
693+
true
694+
everything is awesome~ :p
695+
test
696+
--- no_error_log
662697
[error]

t/sign-verify.t

+44-1
Original file line numberDiff line numberDiff line change
@@ -448,4 +448,47 @@ GET /t
448448
false
449449
Verification failed
450450
--- no_error_log
451-
[error]
451+
[error]
452+
453+
=== TEST 14: JWT sign and verify ES512
454+
--- http_config eval: $::HttpConfig
455+
--- config
456+
location /t {
457+
content_by_lua '
458+
local jwt = require "resty.jwt"
459+
local function get_testcert(name)
460+
local f = io.open("/lua-resty-jwt/testcerts/" .. name)
461+
local contents = f:read("*all")
462+
f:close()
463+
return contents
464+
end
465+
-- x5c wants a base64 encoded der, not pem.. aka, the pem minus the header+footer
466+
local pubkey_pem = get_testcert("ec_cert_p521.pem")
467+
local ssl = require "ngx.ssl"
468+
local der, err = ssl.cert_pem_to_der(pubkey_pem)
469+
local jwt_token = jwt:sign(
470+
get_testcert("ec_cert_p521-key.pem"),
471+
{
472+
header={
473+
typ="JWT",
474+
alg="ES512",
475+
x5c={
476+
ngx.encode_base64(der),
477+
} },
478+
payload={foo="bar", exp=9999999999}
479+
}
480+
)
481+
local jwt_obj = jwt:verify(get_testcert("ec_cert_p521.pem"), jwt_token)
482+
ngx.say(jwt_obj["verified"])
483+
ngx.say(jwt_obj["reason"])
484+
ngx.say(jwt_obj["payload"]["foo"])
485+
';
486+
}
487+
--- request
488+
GET /t
489+
--- response_body
490+
true
491+
everything is awesome~ :p
492+
bar
493+
--- no_error_log
494+
[error]

testcerts/ec_cert_p521-key.pem

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
-----BEGIN EC PRIVATE KEY-----
2+
MIHcAgEBBEIB4lFSZyyNtFjANtO9ERz4g/vt+EGruo9Y342NHHoN/uGnq8d9zP7/
3+
mufPqeo6qEg6EKV74d/IJR6AcjYDfMKG0OSgBwYFK4EEACOhgYkDgYYABADMTquc
4+
1h+hiwxhSyfHNUsGJuKRzAgCrlUXqwLgE+nu3utlL1Nfv76oDzHkSLurKYqQMUDs
5+
UZQDsTZ3ZEvj+GmRJgABNPqrFls6PtC3jlnwCeJX3V2C/mUOORr7nHgGsvr0SLRX
6+
hxGW2bHkNIIQKARln/8EEi4RqDrhYCRbj1TqO1cFRQ==
7+
-----END EC PRIVATE KEY-----

testcerts/ec_cert_p521.csr

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
-----BEGIN CERTIFICATE REQUEST-----
2+
MIICDjCCAW8CAQAwejELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24x
3+
EDAOBgNVBAcTB1NlYXR0bGUxDDAKBgNVBAoTA0pXVDEWMBQGA1UECxMNTm90IFdv
4+
cmxkd2lkZTEeMBwGA1UEAxMVdGVzdGluZy5qd3Qud29ybGR3aWRlMIGbMBAGByqG
5+
SM49AgEGBSuBBAAjA4GGAAQAzE6rnNYfoYsMYUsnxzVLBibikcwIAq5VF6sC4BPp
6+
7t7rZS9TX7++qA8x5Ei7qymKkDFA7FGUA7E2d2RL4/hpkSYAATT6qxZbOj7Qt45Z
7+
8AniV91dgv5lDjka+5x4BrL69Ei0V4cRltmx5DSCECgEZZ//BBIuEag64WAkW49U
8+
6jtXBUWgUDBOBgkqhkiG9w0BCQ4xQTA/MD0GA1UdEQQ2MDSCFXRlc3Rpbmcuand0
9+
Lndvcmxkd2lkZYIbbG9jYWwudGVzdGluZy5qd3Qud29ybGR3aWRlMAoGCCqGSM49
10+
BAMEA4GMADCBiAJCAVhjl+K7NCW0g/Az3qovrCK6UC+kD7lzzAYm5L5vsnJVlcIk
11+
sspNqH2Ea/z9XIlGfqZFUXo81ZmQ5qHyhYsLTg4aAkIB1sDRBeGEtmh17I33HN/1
12+
vuLR6BVk18hf+L5VoYSD5NG0jO2JDpHYA/cMsW1Ohsz5hzoRQ7KpkpW7K5aKPmNP
13+
Ur4=
14+
-----END CERTIFICATE REQUEST-----

testcerts/ec_cert_p521.pem

+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
-----BEGIN CERTIFICATE-----
2+
MIIDrTCCApWgAwIBAgIUenHXNHWN90RdaeTIqIoCvLQGfQUwDQYJKoZIhvcNAQEL
3+
BQAwdzELMAkGA1UEBhMCVVMxETAPBgNVBAgTCE5ldyBZb3JrMREwDwYDVQQHEwhO
4+
ZXcgWW9yazEMMAoGA1UEChMDSldUMRIwEAYDVQQLEwlXT1JMRFdJREUxIDAeBgNV
5+
BAMTF29wZW5yZXN0eS1qd3QtdGVzdC1jZXJ0MB4XDTIwMDUxNTE1MjYwMFoXDTMw
6+
MDUxMzE1MjYwMFowejELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24x
7+
EDAOBgNVBAcTB1NlYXR0bGUxDDAKBgNVBAoTA0pXVDEWMBQGA1UECxMNTm90IFdv
8+
cmxkd2lkZTEeMBwGA1UEAxMVdGVzdGluZy5qd3Qud29ybGR3aWRlMIGbMBAGByqG
9+
SM49AgEGBSuBBAAjA4GGAAQAzE6rnNYfoYsMYUsnxzVLBibikcwIAq5VF6sC4BPp
10+
7t7rZS9TX7++qA8x5Ei7qymKkDFA7FGUA7E2d2RL4/hpkSYAATT6qxZbOj7Qt45Z
11+
8AniV91dgv5lDjka+5x4BrL69Ei0V4cRltmx5DSCECgEZZ//BBIuEag64WAkW49U
12+
6jtXBUWjgbUwgbIwDgYDVR0PAQH/BAQDAgWgMBMGA1UdJQQMMAoGCCsGAQUFBwMC
13+
MAwGA1UdEwEB/wQCMAAwHQYDVR0OBBYEFMvQqOcuqxWuCqeyhPgebt/fx2XKMB8G
14+
A1UdIwQYMBaAFKsUt/gHRBzTx08L6IE1kkvxYmANMD0GA1UdEQQ2MDSCFXRlc3Rp
15+
bmcuand0Lndvcmxkd2lkZYIbbG9jYWwudGVzdGluZy5qd3Qud29ybGR3aWRlMA0G
16+
CSqGSIb3DQEBCwUAA4IBAQAu4co5CHaRwnn30YiDKgakLBiEHhyRfjPekzoDiUBr
17+
Vvj6Hub6e79UtyFsip+5SwJ40br1pIb242jQqIqAAP6dN+At5D5KCZzijz0UnDMs
18+
890aV+UE5Cp6OoWGuWMF8E5iDWsUAD5oZ1qQNuo5G0lGuauMUDp44GtW+kQZCCcF
19+
Yorsl5NZwIA2LN55icOGqLq4cm/KzqsMJqUX3XBNuJpuFx6FfDrTQWa/cPw/nhHX
20+
ddzAQFkF+1bWcRMEX1uA4ega2Mmt8H+GPAgAPX1WEP7J3SBT4hEjqd3eRv9Qu2tM
21+
YKMXx2+T7trqM1BK31RfMLxbJKaKViUy7QXsamCLusiC
22+
-----END CERTIFICATE-----

testcerts/ec_cert_p521_pubkey.pem

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
-----BEGIN PUBLIC KEY-----
2+
MIGbMBAGByqGSM49AgEGBSuBBAAjA4GGAAQAzE6rnNYfoYsMYUsnxzVLBibikcwI
3+
Aq5VF6sC4BPp7t7rZS9TX7++qA8x5Ei7qymKkDFA7FGUA7E2d2RL4/hpkSYAATT6
4+
qxZbOj7Qt45Z8AniV91dgv5lDjka+5x4BrL69Ei0V4cRltmx5DSCECgEZZ//BBIu
5+
Eag64WAkW49U6jtXBUU=
6+
-----END PUBLIC KEY-----

0 commit comments

Comments
 (0)